動画の好きな所だけリピートしたい
Youtubeの動画に合わせて楽器の練習や耳コピをしたり、語学とか講座系の動画で勉強しているときには、特定の部分だけを繰り返し再生(A-Bリピート)させたいことが良くあります。
残念ながらYouTube自体には全体リピートの機能しかありません。もしかするとA-Bリピートのできる機能拡張があるのかもしれませんが、なんとなく機能拡張はあまり増やしたくないのです。
以前、ローカルの動画ファイルをA-Bリピートできるプレーヤーを作ったことがあります。その応用でYouTube上でA-Bリピートのできるブックマークレットを作りました。
ちなみにYouTube専用ではありません。HTML5標準のvideoタグで動画が配置されていて、ページ上の動画ファイルがひとつだけのサイトならどこでも使えます。
開発時はMacのChromeで確認しました。safariだとなぜか動きませんでした。
コントロールはA、B、Nのショートカット
使い方
1.あらかじめブックマークレットのコードをブックマークのURLとして設定しておきます。デモページにあるリンクをブックマークバーにドラッグアンドドロップするのが簡単かもしれません。
2.任意のYoutube動画を再生します
3.リピートさせたい場所の先頭でAキーを押します
4.リピートさせたい場所の最後でBキーを押すと、Aの場所に戻ります。以後はA-B間を自動リピートします
その他の機能
Nキーを押すと自動リピートはオフになります。もう一度Nキーを押すと再びA-B間をリピートします
マウスドラッグなどで再生時間を変更してA-B間から外れると、自動リピートはオフになります。Nキーで再リピートできます。
余談ですがNキーのショートカットを最初はCキーにしていたのですが、Youtubeの字幕(Caption)のショートカットとカブるんですね。
Bより後ろの時間で新たにAを設定すると、Bは動画の終わりに変更されます
Aより前の時間でBを設定するとAは動画の最初に変更されます
画面上にフィードバックはありませんが、javascriptコンソールに動作状態を書き出しています。
どんなものか試せるようにデモページを作りました。
ブックマークレットのコード(ブックマークのURLにコピペ)
javascriptjavascript:const videoElement=document.querySelector('video');let repeatTime_A=0;let repeatTime_B=videoElement.duration;let enable_loop=true;console.log('A-B repeat bookmarklet is on');document.addEventListener('keydown',function(e){const now=videoElement.currentTime;console.log(e.key);if(e.shiftKey||e.ctrlKey||e.metaKey||e.altKey||e.isComposing)return;switch(e.code){case'KeyA':repeatTime_A=now;if(now>repeatTime_B)repeatTime_B=videoElement.duration;enable_loop=true;console.log('repeat',repeatTime_A,repeatTime_B);break;case'KeyB':repeatTime_B=now<1?1:now;if(now<=repeatTime_A)repeatTime_A=0;enable_loop=true;console.log('repeat',repeatTime_A,repeatTime_B);break;case'KeyN':enable_loop=!enable_loop;console.log('repeat',enable_loop);break}});videoElement.addEventListener('seeking',(e)=>{const now=videoElement.currentTime;if(now>repeatTime_B||now<repeatTime_A){enable_loop=false;console.log('repeat is off')}else{enable_loop=true;console.log('repeat is on')}});videoElement.addEventListener('timeupdate',(e)=>{const now=videoElement.currentTime;if(enable_loop){if(now>repeatTime_B){videoElement.currentTime=repeatTime_A}else if(now<repeatTime_A){videoElement.currentTime=repeatTime_A}}});void(0);
ソースコード
const videoElement = document.querySelector("video"); let repeatTime_A = 0; let repeatTime_B = videoElement.duration; let enable_loop = true; console.log("A-B repeat bookmarklet is on"); document.addEventListener("keydown", function (e) { const now = videoElement.currentTime; console.log(e.key); if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey || e.isComposing) return; switch (e.code) { case 'KeyA': repeatTime_A = now; if (now > repeatTime_B) repeatTime_B = videoElement.duration; enable_loop = true; console.log("repeat", repeatTime_A, repeatTime_B); break; case 'KeyB': repeatTime_B = now < 1 ? 1 : now; if (now <= repeatTime_A) repeatTime_A = 0; enable_loop = true; console.log("repeat", repeatTime_A, repeatTime_B); break; case 'KeyN': enable_loop = !enable_loop; console.log("repeat", enable_loop); break; } }); videoElement.addEventListener("seeking", (e) => { const now = videoElement.currentTime; if (now > repeatTime_B || now < repeatTime_A) { enable_loop = false; console.log("repeat is off"); } else { enable_loop = true; console.log("repeat is on"); } }); videoElement.addEventListener("timeupdate", (e) => { const now = videoElement.currentTime; if (enable_loop) { if (now > repeatTime_B) { videoElement.currentTime = repeatTime_A; } else if (now < repeatTime_A) { videoElement.currentTime = repeatTime_A; } } });
ブックマークレットのショートカットは文字入力と干渉する
このブックマークレットではctrlなどの修飾キーを併用せずに、単純にA、B、Cを押すと機能します。
ショートカットはdocument.addEventListnerで設定しているのでページ上では常に反応します。
このため同じページの検索フィールドやコメント欄に文字を入力するとショートカットが働いてしまうことがあります。
ただ、検索フィールドに文字を入れる行為は、このページを離れることを意味しますし、A-Bリピートさせている動画にコメントを打つ人もあまりいないと思うので、実用上は問題ないでしょう。
ブックマークレットなのでリロードすれば外れるますし。
一応、修飾キーを使った他のショートカットとは干渉しないようにしてあります。
video要素がひとつしかないのでdocument.querySelector("video")で捕まえられる
このブックマークレットを作るときの最初の問題は、javascriptでどうやって動画の要素を補足するかでした。
はじめはvideo要素のidを調べようかと思いました。
しかしよく考えるとYoutubeの場合はvideo要素はひとつしかないので、document.querySelector("video") で簡単に捕まえられました。
const videoElement = document.querySelector("video");
あとはvideo要素のイベントやプロパティを使っていくだけです。
このブックマークレットでは以下のプロパティやイベントを使いました。(次章にある速度可変版も含む)
プロパティ
currentTime:現在の再生時間の取得と設定(秒)。float
duration:動画の長さの取得。秒。
paused:動画が止まっているかどうかの取得
playbackRate:再生速度の取得と設定(1.0で通常)
イベント
timeupdate:動画の再生時間が進んだとき。再生中は適当な間隔で繰り返し発生する。このイベントでcurrentTimeを取得し、B時間を過ぎたらAに戻すという作業をやっている
seeking:動画をシーク中(マウスドラッグなどで再生位置を変更中)。このイベント中にcurrentTimeを取得し、A-Bの間から抜けたらenable_loopというフラグをオフにする(リピートさせない)
その他、video/audio要素のイベントやプロパティ、メソッドについては以下のサイトが分かりやすかったです。
audio要素/video要素のプロパティ、メソッド、イベント一覧 _ GRAYCODE JavaScript
速度変更も追加したバージョン
再生速度の変更や数秒スキップ機能を追加したバージョンも作ってみました。デモページの下の方にあります。
私は以前から「Video Speed Controller」という機能拡張を使っているので速度変更機能は不要なのですが、試しにブックマークレットでも同様の機能を再現してみました。
ショートカットはVideo Speed Controllerと同じくSとDで速度変更、ZとXで数秒スキップ、Rでリセットです。
当然のことながら、この機能拡張の入ったChromeでブックマークレットを起動すると機能が二重に働いてしまいます。
ブックマークレットのコード(ブックマークのURLにコピペ)
bookmarkletjavascript:const videoElement=document.querySelector('video');let repeatTime_A=0;let repeatTime_B=videoElement.duration;let enable_loop=true;console.log('A-B repeat bookmarklet is on');document.addEventListener('keydown',function(e){const now=videoElement.currentTime;console.log(e.key);if(e.shiftKey||e.ctrlKey||e.metaKey||e.altKey||e.isComposing)return;switch(e.code){case'KeyA':repeatTime_A=now;if(now>repeatTime_B)repeatTime_B=videoElement.duration;enable_loop=true;console.log('repeat',repeatTime_A,repeatTime_B);break;case'KeyB':repeatTime_B=now<1?1:now;if(now<=repeatTime_A)repeatTime_A=0;enable_loop=true;console.log('repeat',repeatTime_A,repeatTime_B);break;case'KeyN':enable_loop=!enable_loop;console.log('repeat',enable_loop);break;case'KeyD':videoElement.playbackRate+=0.1;console.log('playback rate',videoElement.playbackRate);break;case'KeyS':videoElement.playbackRate-=0.1;console.log('playback rate',videoElement.playbackRate);break;case'KeyR':videoElement.playbackRate=1;console.log('playback rate',videoElement.playbackRate);break;case'KeyX':videoElement.currentTime+=10;console.log('+10sec');break;case'KeyZ':videoElement.currentTime-=5;console.log('-5sec');break}});videoElement.addEventListener('seeking',(e)=>{const now=videoElement.currentTime;if(now>repeatTime_B||now<repeatTime_A){enable_loop=false;console.log('repeat is off')}else{enable_loop=true;console.log('repeat is on')}});videoElement.addEventListener('timeupdate',(e)=>{const now=videoElement.currentTime;if(enable_loop){if(now>repeatTime_B){videoElement.currentTime=repeatTime_A}else if(now<repeatTime_A){videoElement.currentTime=repeatTime_A}}});void(0);
ソースコード
const videoElement = document.querySelector("video"); let repeatTime_A = 0; let repeatTime_B = videoElement.duration; let enable_loop = true; console.log("A-B repeat bookmarklet is on"); document.addEventListener("keydown", function (e) { const now = videoElement.currentTime; console.log(e.key); if (e.shiftKey || e.ctrlKey || e.metaKey || e.altKey || e.isComposing) return; switch (e.code) { case 'KeyA': repeatTime_A = now; if (now > repeatTime_B) repeatTime_B = videoElement.duration; enable_loop = true; console.log("repeat", repeatTime_A, repeatTime_B); break; case 'KeyB': repeatTime_B = now < 1 ? 1 : now; if (now <= repeatTime_A) repeatTime_A = 0; enable_loop = true; console.log("repeat", repeatTime_A, repeatTime_B); break; case 'KeyN': enable_loop = !enable_loop; console.log("repeat", enable_loop); break; case 'KeyD': videoElement.playbackRate += 0.1; console.log("playback rate", videoElement.playbackRate); break; case 'KeyS': videoElement.playbackRate -= 0.1; console.log("playback rate", videoElement.playbackRate); break; case 'KeyR': videoElement.playbackRate = 1; console.log("playback rate", videoElement.playbackRate); break; case 'KeyX': videoElement.currentTime += 10; console.log("+10sec"); break; case 'KeyZ': videoElement.currentTime -= 5; console.log("-5sec"); break; } }); videoElement.addEventListener("seeking", (e) => { const now = videoElement.currentTime; if (now > repeatTime_B || now < repeatTime_A) { enable_loop = false; console.log("repeat is off"); } else { enable_loop = true; console.log("repeat is on"); } }); videoElement.addEventListener("timeupdate", (e) => { const now = videoElement.currentTime; if (enable_loop) { if (now > repeatTime_B) { videoElement.currentTime = repeatTime_A; } else if (now < repeatTime_A) { videoElement.currentTime = repeatTime_A; } } });
ただし、Video Speed ControllerのほうがUIもちゃんとしているし、文字入力の影響も受けません。
ソースコードも公開されています。
Video Speed Controller(github)