// ==UserScript== // @name AbemaTV Auto Reload // @namespace https://greasyfork.org/ja/scripts/25598 // @version 13 // @description AbemaTV(HTML5版)閲覧中に動画が止まったとき、動画を再読み込みします。 // @include https://abema.tv/ // @include https://abema.tv/now-on-air/* // @grant none // @downloadURL none // ==/UserScript== (() => { 'use strict'; /* ---------- Settings ---------- */ // 変更した値はブラウザのローカルストレージに保存するので // スクリプトをバージョンアップするたびに書き換える必要はありません。 // (値が0のとき、以前に変更した値か初期値を使用します) // 動画が止まったとき、チャンネルを切り替えるまでの待ち時間(ミリ秒) // 初期値:1500 // 有効値:1000 ~ 30000 let waitingReloadChannel = 0; // 画質を変更したとき、左上に情報を表示するかどうか // 1:表示する / 2:表示しない // 初期値:1 // 有効値:1 ~ 2 let displayInformation = 0; /* ------------------------------ */ const sid = 'AutoReload', ls = JSON.parse(localStorage.getItem(sid)) || {}, ss = JSON.parse(sessionStorage.getItem(sid)) || {}, moConfig = { attributes: true, characterData: true }, moConfig2 = { childList: true }, flag = { bufferedEnd: 0, change1: false, change2: false, changeChannel: false, checkKeyUp: false, checkQuality: false, countWaitShowVideo: 0, currentTime: 0, loadstart: false, reload: false, reloadTime: 0, show: false, type: 0, undoChannel: false }, timer = { init: 0, notify: 0, playerActiveQualityChanged: 0, showInfo: 0, showQuality: 0, showSpec: 0, waitShowVideo: 0 }, video = { height: [-1, 180, 240, 360, 480, 720, 1080], quality: ['自動で最適化', '通信節約モード', '最低画質', '低画質', '中画質', '高画質', '最高画質'] }; let eV, ePrev, eNext, chId, programTitle, programStartTime; //ページにイベントリスナーを追加 const addEventPage = () => { log('addEventPage'); document.addEventListener('keyup', checkKeyUp, false); }; //動画にイベントリスナーを追加 const addEventVideo = s => { log('addEventVideo', s); if (!flag.reload) { eV = returnVideo(`addEventVideo2 ${s}`); if (!eV) { checkChangeVideo('addEventVideo2'); return; } if (!eV.classList.contains(sid)) { eV.classList.add(sid); eV.addEventListener('error', videoError, false); eV.addEventListener('playing', videoPlaying, false); eV.addEventListener('stalled', videoStalled, false); eV.addEventListener('waiting', videoWaiting, false); if (flag.type === 1) { if (theoplayer.player && theoplayer.player(0).videoTracks.item(0).qualities) theoplayer.player(0).videoTracks.item(0).qualities.addEventListener('activequalitychanged', playerActiveQualityChanged, false); } if (ls.debug) { eV.addEventListener('canplay', videoCanplay, false); eV.addEventListener('canplaythrough', videoCanplaythrough, false); eV.addEventListener('durationchange', videoDurationchange, false); eV.addEventListener('emptied', videoEmpied, false); eV.addEventListener('ended', videoEnded, false); eV.addEventListener('loadeddata', videoLoadeddata, false); eV.addEventListener('loadedmetadata', videoLoadedmetadata, false); eV.addEventListener('loadstart', videoLoadstart, false); eV.addEventListener('pause', videoPause, false); eV.addEventListener('play', videoPlay, false); eV.addEventListener('progress', videoProgress, false); eV.addEventListener('seeked', videoSeeked, false); eV.addEventListener('seeking', videoSeeking, false); eV.addEventListener('suspend', videoSuspend, false); eV.addEventListener('timeupdate', videoTimeupdate, false); } checkQuality('addEventVideo'); } } }; //チャンネルを切り替える const changeChannel = s => { log('changeChannel', s); flag.reloadTime = 0; flag.changeChannel = true; eNext.click(); }; //番組名が変更したとき const changeProgramTitle = () => { log('changeProgramTitle1'); const title = returnProgramTitle(), time = returnProgramTime(); if (title) { programTitle = title.textContent.trim() || ''; log('returnProgramTitle2', programTitle); addEventVideo(); } if (time) { const t = time.textContent || ''; if (t) { programStartTime = t.slice(t.indexOf(')') + 1, t.indexOf(' ')); log('returnProgramTitle3', programStartTime); } } checkPlayingVideo('changeProgramTitle'); }; //画質を変更する const changeQuality = (s, n, c, a) => { log('changeQuality', s, n, c, a, ss.tempQuality, flag.checkKeyUp); if (flag.type === 1) { const p = theoplayer.player(0); if (!flag.checkKeyUp && p && p.videoTracks && p.videoTracks.item) { const q = p.videoTracks.item(0).qualities; if (n === 48) { for (let i = 0, j = q.length; i < j; i++) { if (q.getQualityByID(i).hasOwnProperty('enabled')) { if (c && a) { for (let k = 0; k < j; k++) { if (q.getQualityByID(k).hasOwnProperty('enabled')) q.getQualityByID(k).enabled = true; ls.automaticQualities[k] = true; } } else if (ls.automaticQualities.length > i && q.getQualityByID(i).hasOwnProperty('enabled')) q.getQualityByID(i).enabled = ls.automaticQualities[i]; } } if (!(c && a)) { q.targetQuality = null; ls.videoQuality = 0; ss.tempQuality = -1; } } else if (n >= 49 && n <= 54) { let id = n - 49, count = 0; if (c && a) { if (q.length >= id && !ls.videoQuality && ss.tempQuality === -1) { for (let i = 0, j = q.length; i < j; i++) { if (q.getQualityByID(i).enabled) count += 1; } if (count === 2 && q.getQualityByID(id).enabled) return; if (q.getQualityByID(id).hasOwnProperty('enabled')) { q.getQualityByID(id).enabled = !q.getQualityByID(id).enabled; ls.automaticQualities[id] = !ls.automaticQualities[id]; if (displayInformation === 1) showInfo(`自動最適化での${video.quality[id + 1]}:${ls.automaticQualities[id] ? '有効' : '無効'}`); } } } else if (c) { if (id > q.length - 1) id = q.length - 1; if (q.getQualityByID(id).hasOwnProperty('enabled')) { q.getQualityByID(id).enabled = true; q.targetQuality = q.getQualityByID(id); ss.tempQuality = n - 48; } } else if (flag.checkKeyUp && ss.tempQuality >= 0) { if (q.getQualityByID(ss.tempQuality).hasOwnProperty('enabled')) { q.getQualityByID(ss.tempQuality).enabled = true; q.targetQuality = q.getQualityByID(ss.tempQuality); } } else { if (id > q.length - 1) id = q.length - 1; if (q.getQualityByID(id).hasOwnProperty('enabled')) { q.getQualityByID(id).enabled = true; q.targetQuality = q.getQualityByID(id); ls.videoQuality = n - 48; ss.tempQuality = -1; } } } else { q.targetQuality = null; ls.videoQuality = 0; ss.tempQuality = -1; } saveLocalStorage(); saveSessionStorage(); } } }; //動画が切り替わったかを調べる const checkChangeVideo = async a => { log('checkChangeVideo'); const e = returnVideo(`checkChangeVideo ${a}`); if (!flag.change1 && !e) { log('checkChangeVideo1', a); flag.change1 = true; await sleep(550); flag.change1 = false; checkChangeVideo('checkChangeVideo'); } else if (!flag.change2 && e && (!eV || e.id !== eV.id)) { log('checkChangeVideo2', a); flag.change2 = true; await sleep(1000); flag.change2 = false; addEventVideo('checkChangeVideo'); } else { log('checkChangeVideo3', a); checkPlayingVideo('checkChangeVideo3'); } }; //動画配信の形式を調べる const checkVideoFormat = () => { if (theoplayer && theoplayer.player && theoplayer.player(0)) { flag.type = 1; return true; } const vi = document.getElementsByTagName('video'), au = document.getElementsByTagName('audio'); if (vi.length > 0 && vi.length === au.length && !document.getElementsByClassName('vjs-tech').length) { flag.type = 2; return true; } flag.type = 0; return false; }; //キーボードのキーを押したとき const checkKeyUp = e => { if (/input|textarea/i.test(e.target.tagName)) return; if (e.shiftKey) { const quality = async () => { flag.checkKeyUp = true; await sleep(250); flag.checkKeyUp = false; }, vi = returnVideo('checkKeyUp'), au = returnAudio(); checkVideoFormat(); if (e.keyCode === 8) { //Backspace if (vi) vi.playbackRate = 1; if (flag.type === 2 && au) au.playbackRate = 1; showInfo(`${vi.playbackRate}倍速`); } else if (e.keyCode === 32) { //Space if (flag.type === 1) { if (theoplayer.player(0).paused) theoplayer.player(0).play(); else theoplayer.player(0).pause(); } else if (flag.type === 2 && vi && au) { if (vi.paused) { vi.play(); au.play(); } else { vi.pause(); au.pause(); } } } else if (e.keyCode >= 48 && e.keyCode <= 54) { //0~6 changeQuality('checkKeyUp', e.keyCode, e.ctrlKey, e.altKey); quality(); } else if (e.keyCode === 57) checkQuality(); //9 else if (e.keyCode === 73) showSpec(); //i else if (e.keyCode === 221 || e.keyCode === 219) { //]・[ if (vi && vi.playbackRate) { const pr = vi.playbackRate.toFixed(1); if (e.keyCode === 221) { if (e.ctrlKey) { vi.playbackRate = 2; if (flag.type === 2 && au) au.playbackRate = 2; } else if (pr < 2) { vi.playbackRate += 0.1; if (flag.type === 2 && au) au.playbackRate += 0.1; } } else if (e.keyCode === 219) { if (e.ctrlKey) { vi.playbackRate = 0.5; if (flag.type === 2 && au) au.playbackRate = 0.5; } else if (pr > 0.5) { vi.playbackRate -= 0.1; if (flag.type === 2 && au) au.playbackRate -= 0.1; } } showInfo(`${Number(vi.playbackRate.toFixed(3))}倍速`, vi.playbackRate !== 1 ? -1 : null); } } } }; //動画が実際に再生中なのかを調べる const checkPlayingVideo = async s => { log('checkPlayingVideo', s); eV = returnVideo('checkPlayingVideo1'); if (eV) { let time1 = 0, time2 = 0; try { time1 = eV.currentTime; } catch(err) { log('checkPlayingVideo: time1', err, 'error'); } if (time1) { await sleep(100); try { time2 = eV.currentTime; } catch(err) { log('checkPlayingVideo: time2', err, 'error'); } if (time1 === time2) reloadVideo(`checkPlayingVideo1-${s}`); } else reloadVideo(`checkPlayingVideo2-${s}`); } else log('checkPlayingVideo: not found video'); }; //動画の画質を調べる const checkQuality = s => { log('checkQuality', s); if (flag.type === 1) { const p = theoplayer.player(0); if (p && p.videoTracks && p.videoTracks.item) { const q = p.videoTracks.item(0).qualities, aq = q.activeQuality, max = q.getQualityByID(q.length - 1); if (s) { flag.checkQuality = true; if (ss.tempQuality >= 0) changeQuality('checkQuality1', ss.tempQuality + 48); else if (ls.videoQuality >= 0) changeQuality('checkQuality2', ls.videoQuality + 48); } else if (aq.resolution.height) { const qn = ss.tempQuality >= 0 ? `*${ss.tempQuality}` : ls.videoQuality; let mes = `解像度:${p.videoWidth}×${p.videoHeight}\n` + `選択(${qn}):${aq.resolution.width}×${aq.resolution.height}, ${aq.frameRate}fps, ${(aq.bandwidth / 1000)}kbps`; if (max.resolution.height) mes += `\n最高(${(q.length)}):${max.resolution.width}×${max.resolution.height}, ${max.frameRate}fps, ${(max.bandwidth / 1000)}kbps`; notify('checkQuality', mes); } } } else if (flag.type === 2) { const vi = returnVideo(`checkQuality ${s}`); if (vi && vi.videoHeight) notify('checkQuality', `解像度:${vi.videoWidth}×${vi.videoHeight}`); } }; //解像度や画質などの情報を表示する要素を作成 const createInfo = () => { const css = '#AutoReload_Info, #AutoReload_Quality, #AutoReload_Spec {align-items:center; background-color:rgba(0,0,0,0.6); border-radius:4px; color:white; display:flex; justify-content:center; min-height:2em; min-width:4em; padding:0.5ex 1ex; position:fixed; -ms-user-select:none; -moz-user-select:none; -webkit-user-select:none; user-select:none;}' + '#AutoReload_Info {left:20px; top:80px; z-index: 2222; }' + '#AutoReload_Quality {left:20px; top:50px; z-index: 2224; }' + '#AutoReload_Spec {bottom:120px; font-size:90%; left:40px; width:calc(100% - 80px); max-width:60em; z-index: 2220; }' + '#AutoReload_Spec dl {display:flex; flex-wrap:wrap;}' + '#AutoReload_Spec dt {width:10em;}' + '#AutoReload_Spec dd {width:calc(100% - 10em); padding-left:1em;}' + '.ar_show {opacity:0.8; visibility:visible;}' + '.ar_hidden {opacity:0; visibility:hidden; transition:opacity 0.5s ease-out, visibility 0.5s ease-out;}', div1 = document.createElement('div'), div2 = document.createElement('div'), div3 = document.createElement('div'), style = document.createElement('style'); style.type = 'text/css'; style.textContent = css; document.head.appendChild(style); div1.id = 'AutoReload_Info'; div1.className = 'ar_hidden'; document.body.appendChild(div1); div2.id = 'AutoReload_Quality'; div2.className = 'ar_hidden'; document.body.appendChild(div2); div3.id = 'AutoReload_Spec'; div3.className = 'ar_hidden'; document.body.appendChild(div3); }; //ページを開いたときに1度だけ実行 const init = () => { log('init'); setupSettings(); waitShowVideo('init'); createInfo(); }; //デバッグ用 ログ const log = (...a) => { if (ls.debug) { if (/^debug$|^error$|^info$|^warn$/.test(a[a.length - 1])) { const b = a.pop(); console[b](sid, a.toString()); } else console.log(sid, a.toString()); } }; //デスクトップ通知 const notify = (f, m, s) => { const title = 'AbemaTV Auto Reload', message = `${m}\n${new Date().toLocaleTimeString()}`, delay = s >= 1000 ? s : 3000; let notifi; log('----- notify -----', f, m); if ('Notification' in window) { if (Notification.permission === 'granted') { notifi = new Notification(title, { body: message, tag: f }); } else if (Notification.permission !== 'denied') { Notification.requestPermission(permission => { if (permission === 'granted') { notifi = new Notification(title, { body: message, tag: f }); } }); } if (notifi) { clearTimeout(timer.notify); timer.notify = setTimeout(() => notifi.close(), delay); } } else console.log(title, message); }; //画質が変更されたとき const playerActiveQualityChanged = () => { log('playerActiveQualityChanged'); if (flag.type === 1) { const eQua = document.getElementById('AutoReload_Quality'), eSel = document.getElementsByClassName('theoplayer-configuration-panel-content')[0], p = theoplayer.player(0); let n = 0; if (!p || !p.videoTracks.item) return; if (ss.tempQuality > 0 && p.videoTracks.item(0).qualities.activeQuality.resolution.height !== video.height[ss.tempQuality]) changeQuality('playerActiveQualityChanged1', ss.tempQuality + 48); else if (ls.videoQuality > 0 && p.videoTracks.item(0).qualities.activeQuality.resolution.height !== video.height[ls.videoQuality]) changeQuality('playerActiveQualityChanged2', ls.videoQuality + 48); log(`${ls.videoQuality} / ${ss.tempQuality} : ${p.videoHeight} → ${p.videoTracks.item(0).qualities.activeQuality.resolution.height}`); if (displayInformation === 1) { clearInterval(timer.playerActiveQualityChanged); timer.playerActiveQualityChanged = setInterval(() => { if (p && p.videoTracks && p.videoTracks.item) { const w = p.videoWidth, h = p.videoHeight, a = p.videoTracks.item(0).qualities.activeQuality, r = (a) ? a.resolution : null; let after = '', before = ''; log('activequalitychanged 1', w, h); if (r && r.width && r.height && isFinite(r.width) && isFinite(r.height)) { log('activequalitychanged 2', eSel.selectedIndex, a.id, r.width, r.height, w, h); for (let i = 1, j = video.height.length; i < j; i++) { if (video.height[i] === h) before = video.quality[i]; if (video.height[i] === r.height) after = video.quality[i]; } if (h !== r.height && !eQua.classList.contains('ar_pre')) { log('activequalitychanged 3', eSel.selectedIndex, a.id, r.width, r.height, w, h); eQua.classList.add('ar_pre'); showQuality(`画質変更:${before} → ${after}`); } else if (r.height === h) { log('activequalitychanged 4', eSel.selectedIndex, a.id, r.width, r.height, w, h); eQua.classList.remove('ar_pre'); showQuality(`画質:${after}`); n = 0; clearInterval(timer.playerActiveQualityChanged); } else if (n > 60) { log('activequalitychanged 5', eSel.selectedIndex, a.id, r.width, r.height, w, h); n = 0; clearInterval(timer.playerActiveQualityChanged); eQua.classList.remove('ar_show'); eQua.classList.remove('ar_pre'); } else n += 1; } } }, 1000); } } }; //ページを再読み込みする const reloadPage = async s => { if (flag.reload) { log('reloadPage', s); await sleep(1000); location.reload(); } }; //動画を再読み込みする const reloadVideo = async s => { log(`reloadVideo: ${s}`); if (!flag.changeChannel && !flag.undoChannel) { if (flag.type === 1 && (!flag.reloadTime || (flag.reloadTime && Date.now() - flag.reloadTime < waitingReloadChannel))) { if (!flag.reloadTime) flag.reloadTime = Date.now(); log('reloadVideo-1', Date.now() - flag.reloadTime); theoplayer.player(0).load(); await sleep(500); checkPlayingVideo('reloadVideo'); } else if (flag.type === 2) { log('reloadVideo-2'); const vi = returnVideo(`reloadVideo ${s}`); if (vi && vi.error) vi.load(); } else { log('reloadVideo-3'); if (!flag.changeChannel && !flag.undoChannel) changeChannel('reloadVideo'); else if (!flag.reload) { flag.reload = true; reloadPage(s); } } } }; //THEOplayerのプレイヤーもしくはaudio要素を返す const returnAudio = () => { if (flag.type === 1) return theoplayer.player(0); if (flag.type === 2) { const vi = document.getElementsByTagName('video'), au = document.getElementsByTagName('audio'); let n = -1; for (let i = 0, j = vi.length; i < j; i++) { if (vi[i].src && getComputedStyle(vi[i]).display !== 'none') { n = i; break; } } if (n >= 0 && au[n].src) return au[n]; for (let k = 0, l = au.length; k < l; k++) { if (au[k].src) return au[k]; } } return null; }; //チャンネルロゴ画像の要素を返す const returnChLogo = s => { const e = document.querySelector('div[style] > img[srcset]'); if (s) { if (e) { chId = e.getAttribute('alt'); log('returnChLogo: chId', chId); } else log('returnChLogo: not found ch logo', 'error'); } return e; }; //番組詳細の「月日時分~月日時分」の要素を返す const returnProgramTime = () => document.querySelector('div > h2 + p + p'); //番組名の要素を返す const returnProgramTitle = () => document.querySelector('button[aria-label] + div + div span > span'); //HTML5動画の要素を返す const returnVideo = s => { const vi = document.getElementsByTagName('video'); if (!vi) log('returnVideo: not found video', s); for (let i = 0, j = vi.length; i < j; i++) { if (vi[i].src && vi[i].style.display !== 'none') return vi[i]; } return null; }; //ローカルストレージに設定を保存する const saveLocalStorage = () => localStorage.setItem(sid, JSON.stringify(ls)); //セッションストレージに設定を保存する const saveSessionStorage = () => sessionStorage.setItem(sid, JSON.stringify(ss)); //設定の値を用意する const setupSettings = () => { let rc = (Number.isInteger(Number(waitingReloadChannel))) ? Number(waitingReloadChannel) : 0, di = (Number.isInteger(Number(displayInformation))) ? Number(displayInformation) : 0; if (!Number.isInteger(Number(ls.videoQuality))) ls.videoQuality = 0; if (!Array.isArray(ls.automaticQualities)) ls.automaticQualities = [true, true, true, true, true]; rc = (rc === 0) ? 0 : (rc > 30000) ? 30000 : (rc < 1000) ? 1000 : rc; di = (di === 0) ? 0 : (di > 2) ? 2 : (di < 1) ? 1 : di; waitingReloadChannel = (ls.waitingReloadChannel) ? ls.waitingReloadChannel : (rc) ? rc : 1500; displayInformation = (ls.displayInformation) ? ls.displayInformation : (di) ? di : 1; if (rc && ls.waitingReloadChannel !== rc) { waitingReloadChannel = rc; ls.waitingReloadChannel = rc; saveLocalStorage(); } if (di && ls.displayInformation !== di) { displayInformation = di; ls.displayInformation = di; saveLocalStorage(); } }; //情報の要素を表示 const showInfo = (s, t, b) => { if (!b) log('showInfo', s); const e = document.getElementById('AutoReload_Info'), delay = t > 0 ? t : 2500; if (s) { e.textContent = s; e.classList.remove('ar_hidden'); e.classList.add('ar_show'); clearTimeout(timer.showInfo); if (t !== -1) { timer.showInfo = setTimeout(() => { e.classList.remove('ar_show'); e.classList.add('ar_hidden'); }, delay); } } else { clearTimeout(timer.showInfo); e.classList.remove('ar_show'); e.classList.add('ar_hidden'); } }; //画質情報の要素を表示 const showQuality = s => { log('showQuality', s); const e = document.getElementById('AutoReload_Quality'); e.textContent = s; e.classList.remove('ar_hidden'); e.classList.add('ar_show'); clearTimeout(timer.showQuality); timer.showQuality = setTimeout(() => { e.classList.remove('ar_show'); e.classList.add('ar_hidden'); }, 2500); }; //詳細情報の要素を表示 const showSpec = () => { log('showSpec'); const e = document.getElementById('AutoReload_Spec'), spec = s => { if (s) { e.innerHTML = s; e.classList.remove('ar_hidden'); e.classList.add('ar_show'); clearTimeout(timer.showSpec); timer.showSpec = setTimeout(() => { e.classList.remove('ar_show'); e.classList.add('ar_hidden'); }, 8000); } else { clearTimeout(timer.showSpec); e.classList.remove('ar_show'); e.classList.add('ar_hidden'); } }; if (flag.type === 1) { if (!e.classList.contains('ar_show')) { const p = theoplayer.player(0); if (p && p.videoTracks && p.videoTracks.item) { const vq = p.videoTracks.item(0).qualities, aq = p.audioTracks.item(0).qualities, vaq = vq.activeQuality, aaq = aq.activeQuality, vmax = vq.getQualityByID(vq.length - 1), amax = aq.getQualityByID(vq.length - 1), fr = p.frameRate, vc1 = ['avc1.42', 'avc1.4d', 'avc1.64'], vc2 = ['Baseline', 'Main', 'High'], ac1 = ['mp4a.40.5', 'mp4a.40.2', 'mp4a.69', 'mp4a.6b'], ac2 = ['HE-AAC', 'LC-AAC', 'MP3', 'MP3']; let s = '