みなさん、こんにちは!動画をJavaScriptでコマ送りしたいと思ったことはありませんか?実は、これって結構面白い機能なんですよ。動画編集や詳細な分析に役立つだけでなく、ユーザー体験を大幅に向上させることができるんです。今回は、その実装方法をステップバイステップで解説していきますね。難しそうに聞こえるかもしれませんが、一緒に頑張っていきましょう!
動画のコマ送り機能実装に必要なJavaScript基礎知識
さて、本格的な実装に入る前に、いくつか押さえておきたいポイントがあります。JavaScriptを使って動画をコマ送りするには、HTML5のVideo APIやrequestAnimationFrame、そしてCanvas要素についての基本的な理解が必要になってきます。でも心配しないでくださいね。これらは難しそうに聞こえますが、実際に使ってみると意外と簡単なんですよ。それでは、一つずつ見ていきましょう。
HTML5 Video APIの基本と操作方法
HTML5のVideo APIって聞いたことありますか?これ、実はすごく便利なツールなんです。動画の再生、一時停止、シーク(特定の時間に移動すること)などの基本的な操作が、JavaScriptを使って簡単にできるようになるんですよ。
まずは、HTMLで動画要素を追加するところから始めましょう。こんな感じです:
<video id="myVideo" src="path/to/your/video.mp4"></video>
これで、ページに動画が表示されるようになりました。次に、JavaScriptでこの動画を操作していきます。
const video = document.getElementById('myVideo');
// 再生
video.play();
// 一時停止
video.pause();
// 特定の時間(秒)に移動
video.currentTime = 10; // 10秒目に移動
どうですか?思ったより簡単でしょう?これだけで、動画の基本的な操作ができるようになりました。でも、コマ送りにはもう少し工夫が必要です。次は、そのための重要な要素を見ていきましょう。
requestAnimationFrameを使用したスムーズな再生制御
さて、次は少し変わった名前の関数が出てきます。「requestAnimationFrame」。長い名前ですが、これがコマ送りをスムーズにする鍵になるんです。
この関数、簡単に言うと「次の描画タイミングで指定した関数を呼んでね」というお願いをブラウザにするものです。動画のコマ送りでこれを使うと、画面の更新に合わせて次のフレームを表示できるので、スムーズな動きが実現できるんです。
使い方はこんな感じです:
function nextFrame() {
if (video.paused || video.ended) return;
// 次のフレームに進む
video.currentTime += 1 / 30; // 30fpsの場合
requestAnimationFrame(nextFrame);
}
// コマ送り開始
requestAnimationFrame(nextFrame);
この例では、30fps(1秒間に30フレーム)の動画を想定しています。video.currentTime += 1 / 30
で、1フレーム分だけ時間を進めているんです。
ここでのポイントは、requestAnimationFrame
が自分自身を呼び出していることです。これにより、連続的にフレームを進められるんですね。ただし、動画が一時停止中や終了している場合は、この処理を止めるようにしています。
この方法を使えば、カクカクしないスムーズなコマ送りが実現できます。でも、もっと細かい制御をしたい場合は、次に紹介するCanvas要素を使う方法も覚えておくと良いでしょう。
キャンバス要素を活用した動画フレームの描画テクニック
Canvas要素って知っていますか?これ、ウェブページ上に絵を描くことができる特別な要素なんです。動画のコマ送りでこれを使うと、さらに細かい制御ができるようになります。
まず、HTMLにCanvas要素を追加しましょう:
<canvas id="myCanvas"></canvas>
そして、JavaScriptでこのCanvas上に動画のフレームを描画していきます:
const canvas = document.getElementById('myCanvas');
const ctx = canvas.getContext('2d');
function drawFrame() {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
}
function nextFrame() {
if (video.paused || video.ended) return;
video.currentTime += 1 / 30; // 30fpsの場合
drawFrame();
requestAnimationFrame(nextFrame);
}
// コマ送り開始
video.addEventListener('loadedmetadata', () => {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
requestAnimationFrame(nextFrame);
});
このコードでは、動画の各フレームをCanvasに描画しています。drawImage
メソッドを使うことで、動画の現在のフレームをCanvas上に表示できるんです。
Canvas要素を使うメリットは、フレームごとに画像処理を適用できることです。例えば、フレームに特殊効果を加えたり、フレーム間の差分を検出したりといった高度な処理も可能になります。
ここまでくれば、基本的なコマ送り機能の実装はほぼ完了です。でも、まだまだ改善の余地はありますよ。次は、より使いやすい機能を追加していきましょう。
ステップバイステップで学ぶJavaScriptコマ送り実装手順
基礎知識を押さえたところで、いよいよ実際の実装に入っていきましょう。コマ送り機能を段階的に作っていくことで、複雑な処理も理解しやすくなりますよ。まずは動画要素の取得から始めて、徐々に機能を追加していきます。最終的には、キーボード操作でコマ送りができるところまで持っていきますので、楽しみにしていてくださいね。
動画要素の取得とイベントリスナーの設定方法
さあ、いよいよ本格的な実装に入っていきますよ。まずは、HTMLの動画要素をJavaScriptで操作できるようにしましょう。
const video = document.getElementById('myVideo');
このコードで、HTML上のid="myVideo"
がついた動画要素を取得できます。
次に、動画の状態変化を監視するイベントリスナーを設定します。これが重要なポイントになってきますよ。
video.addEventListener('loadedmetadata', () => {
console.log('動画のメタデータがロードされました');
// ここに動画の準備が整った後の処理を書きます
});
video.addEventListener('play', () => {
console.log('動画が再生開始しました');
});
video.addEventListener('pause', () => {
console.log('動画が一時停止しました');
});
video.addEventListener('ended', () => {
console.log('動画が終了しました');
});
これらのイベントリスナーを設定することで、動画の状態変化に応じて特定の処理を実行できるようになります。例えば、再生が始まったときにコマ送りの処理を開始したり、一時停止したときにコマ送りを止めたりといった具合です。
ここでのポイントは、loadedmetadata
イベントをしっかり押さえることです。このイベントが発火したタイミングで、動画の長さやサイズなどの情報が取得可能になるので、コマ送りの準備をするのに最適なタイミングなんです。
これで基本的な準備は整いました。次は、実際にコマ送りの機能を実装していきましょう。
再生・一時停止・コマ送り機能の実装手順
準備ができたところで、いよいよコマ送りの核心部分に迫っていきますよ。ここでは、再生、一時停止、そしてコマ送りの機能を順番に実装していきます。
まずは、再生と一時停止のボタンを作りましょう。HTMLにこんな感じでボタンを追加します:
<button id="playPauseBtn">再生/一時停止</button>
<button id="nextFrameBtn">次のフレーム</button>
そして、JavaScriptでこれらのボタンに機能を持たせます:
const playPauseBtn = document.getElementById('playPauseBtn');
const nextFrameBtn = document.getElementById('nextFrameBtn');
playPauseBtn.addEventListener('click', () => {
if (video.paused) {
video.play();
} else {
video.pause();
}
});
nextFrameBtn.addEventListener('click', () => {
if (video.paused) {
video.currentTime += 1 / 30; // 30fpsの場合
}
});
この実装では、再生/一時停止ボタンをクリックすると動画の再生状態が切り替わり、次のフレームボタンをクリックすると1フレーム分だけ動画が進むようになっています。
でも、まだちょっと物足りないですよね。もう少し滑らかなコマ送りを実現するために、requestAnimationFrame
を使った方法を導入してみましょう:
let isFrameByFrame = false;
function nextFrame() {
if (!isFrameByFrame || video.ended) return;
video.currentTime += 1 / 30; // 30fpsの場合
requestAnimationFrame(nextFrame);
}
nextFrameBtn.addEventListener('click', () => {
if (video.paused) {
isFrameByFrame = !isFrameByFrame;
if (isFrameByFrame) {
requestAnimationFrame(nextFrame);
}
}
});
この新しい実装では、「次のフレーム」ボタンを押すとコマ送りモードがオンになり、連続的にフレームが進むようになります。もう一度押すとコマ送りモードがオフになります。
これで基本的なコマ送り機能が実装できました。でも、まだまだ改善の余地はありますよ。例えば、キーボード操作でコマ送りができるようにすれば、より使いやすくなりそうですね。次はそれに挑戦してみましょう!
キーボード操作によるコマ送り制御の追加方法
さて、ボタンでのコマ送りはできるようになりましたが、キーボードで操作できたらもっと便利ですよね。例えば、右矢印キーで次のフレーム、左矢印キーで前のフレーム、スペースキーで再生/一時停止ができるようにしてみましょう。
まずは、キーボードイベントをリッスンするコードを追加します:
document.addEventListener('keydown', (event) => {
switch(event.code) {
case 'Space':
if (video.paused) {
video.play();
} else {
video.pause();
}
break;
case 'ArrowRight':
if (video.paused) {
video.currentTime += 1 / 30; // 30fpsの場合
}
break;
case 'ArrowLeft':
if (video.paused) {
video.currentTime -= 1 / 30; // 30fpsの場合
}
break;
}
});
このコードを追加することで、キーボードでの操作が可能になります。スペースキーで再生/一時停止、右矢印キーで1フレーム進む、左矢印キーで1フレーム戻るという具合です。
ただし、このままだと高速で連続してキーを押したときに、思ったより遅くコマ送りされる可能性があります。そこで、キーを押し続けたときに連続的にコマ送りされるようにしてみましょう:
let isKeyPressed = false;
let keyPressInterval;
document.addEventListener('keydown', (event) => {
if (isKeyPressed) return;
isKeyPressed = true;
const frameMove = (direction) => {
if (video.paused) {
video.currentTime += direction * (1 / 30);
}
};
const handleKeyPress = () => {
switch(event.code) {
case 'Space':
video.paused ? video.play() : video.pause();
break;
case 'ArrowRight':
frameMove(1);
break;
case 'ArrowLeft':
frameMove(-1);
break;
}
};
handleKeyPress();
keyPressInterval = setInterval(handleKeyPress, 100);
});
document.addEventListener('keyup', () => {
isKeyPressed = false;
clearInterval(keyPressInterval);
});
このコードでは、キーを押し続けると100ミリ秒ごとにフレームが移動するようになっています。キーを離すと、その動作が停止します。
これで、よりインタラクティブなコマ送り機能が実現できました。キーボード操作を加えることで、ユーザーはよりスムーズに動画を操作できるようになりますね。
ここまでくれば、基本的なコマ送り機能はほぼ完成したと言えるでしょう。でも、まだまだ改善の余地はありますよ。例えば、パフォーマンスの最適化や、より高度な機能の追加なんかも考えられます。それじゃあ、さらに一歩進んだ話題に移っていきましょう。
パフォーマンス最適化:スムーズなコマ送り動画再生のコツ
基本的な機能は実装できましたが、実際に使ってみると少しもたつきを感じるかもしれません。特に、長い動画や高解像度の動画を扱う場合は顕著です。そこで、パフォーマンスを最適化するテクニックをいくつか紹介しましょう。バッファリング、フレームキャッシュ、GPUアクセラレーションなど、ちょっと難しそうな言葉が出てきますが、一つずつ理解していけば大丈夫です。これらの技術を使えば、よりスムーズなコマ送り体験を提供できるようになりますよ。
バッファリングを活用した先読み処理の実装
「バッファリング」って聞いたことありますか?動画を見ているときに、時々「読み込み中…」って表示が出るアレです。実は、このバッファリングをうまく活用すると、コマ送りの性能を大幅に向上させることができるんです。
基本的な考え方は、「先の部分をあらかじめ読み込んでおく」というものです。これにより、ユーザーがコマ送りしたときに、すぐに次のフレームを表示できるようになります。
実装方法はこんな感じです:
const bufferTime = 5; // 5秒分先読みする
video.addEventListener('timeupdate', () => {
if (!video.paused) {
const end = video.currentTime + bufferTime;
if (end > video.duration) {
video.preload = 'auto';
} else {
video.setMediaKeySystemConfiguration([{
startTime: video.currentTime,
endTime: end
}]);
}
}
});
このコードでは、現在の再生位置から5秒先までを常にバッファリングするようにしています。setMediaKeySystemConfiguration
メソッドを使って、バッファリングする範囲を指定しているんです。
ただし、注意点もあります。あまり先まで読み込みすぎると、メモリを大量に消費してしまう可能性があります。動画の長さやユーザーの操作頻度に応じて、適切なバッファ時間を設定することが大切です。
また、ネットワーク環境によっては、バッファリングに時間がかかってしまうこともあります。その場合は、ユーザーに進捗状況を表示するのも良いアイデアですね。例えば、こんな感じです:
video.addEventListener('progress', () => {
const buffered = video.buffered;
if (buffered.length > 0) {
const loadedPercentage = (buffered.end(buffered.length - 1) / video.duration) * 100;
console.log(`${loadedPercentage.toFixed(2)}% loaded`);
}
});
このコードを追加すれば、コンソールに読み込み状況が表示されます。実際のアプリケーションでは、これをプログレスバーなどで視覚的に表現するとよいでしょう。
バッファリングを活用することで、ユーザーはよりスムーズなコマ送り体験を得られるはずです。でも、まだまだパフォーマンスを向上させる方法はありますよ。次は、メモリ使用量を抑えるテクニックを見ていきましょう。
メモリ使用量を抑えるフレームキャッシュ技術
さて、次はメモリ使用量の最適化について話しましょう。コマ送り機能を実装すると、意外とメモリを食うんですよね。特に長時間の動画や高解像度の動画を扱う場合は顕著です。そこで役立つのが「フレームキャッシュ」という技術です。
フレームキャッシュの基本的な考え方は、「よく使うフレームは記憶しておく、でも全部は覚えない」というものです。これにより、メモリ使用量を抑えつつ、高速なコマ送りを実現できるんです。
実装例を見てみましょう:
class FrameCache {
constructor(maxSize = 30) {
this.maxSize = maxSize;
this.frames = new Map();
}
add(time, imageData) {
if (this.frames.size >= this.maxSize) {
const oldestKey = this.frames.keys().next().value;
this.frames.delete(oldestKey);
}
this.frames.set(time, imageData);
}
get(time) {
return this.frames.get(time);
}
}
const frameCache = new FrameCache();
function drawFrame() {
const cachedFrame = frameCache.get(video.currentTime);
if (cachedFrame) {
ctx.putImageData(cachedFrame, 0, 0);
} else {
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
frameCache.add(video.currentTime, imageData);
}
}
このコードでは、FrameCache
というクラスを作成し、最大30フレームまでキャッシュするようにしています。フレームを描画する際、まずキャッシュを確認し、あればそれを使用。なければ新たに描画してキャッシュに追加します。
これにより、頻繁にアクセスするフレームの読み込み速度が向上し、全体的なパフォーマンスが改善されます。ただし、キャッシュサイズは慎重に選ぶ必要がありますね。大きすぎるとメモリを圧迫しますし、小さすぎると効果が薄くなってしまいます。
また、動的にキャッシュサイズを調整する方法も考えられます:
class AdaptiveFrameCache {
constructor(initialSize = 30, maxSize = 100) {
this.frames = new Map();
this.initialSize = initialSize;
this.maxSize = maxSize;
this.hitCount = 0;
this.missCount = 0;
}
add(time, imageData) {
if (this.frames.size >= this.getCurrentMaxSize()) {
const oldestKey = this.frames.keys().next().value;
this.frames.delete(oldestKey);
}
this.frames.set(time, imageData);
}
get(time) {
const frame = this.frames.get(time);
if (frame) {
this.hitCount++;
} else {
this.missCount++;
}
this.adjustSize();
return frame;
}
getCurrentMaxSize() {
const hitRate = this.hitCount / (this.hitCount + this.missCount);
return Math.min(this.maxSize, Math.max(this.initialSize, Math.floor(this.frames.size * (1 + hitRate))));
}
adjustSize() {
if (this.hitCount + this.missCount > 100) {
const currentSize = this.getCurrentMaxSize();
while (this.frames.size > currentSize) {
const oldestKey = this.frames.keys().next().value;
this.frames.delete(oldestKey);
}
this.hitCount = 0;
this.missCount = 0;
}
}
}
このAdaptiveFrameCache
クラスは、キャッシュヒット率に基づいてサイズを動的に調整します。使用頻度の高いフレームが多い場合はキャッシュを拡大し、そうでない場合は縮小します。
フレームキャッシュを利用することで、メモリ使用量を抑えつつ、スムーズなコマ送りを実現できます。ただし、キャッシュの管理には細心の注意を払う必要がありますね。次は、さらなるパフォーマンス向上のために、GPUの力を借りる方法を見ていきましょう。
GPUアクセラレーションを利用した描画処理の高速化
さて、ここまでCPUでの処理を中心に見てきましたが、実はGPU(Graphics Processing Unit)を活用することで、さらなるパフォーマンスの向上が見込めるんです。特に、高解像度の動画や複雑な効果を適用する場合には、GPUの力が大いに役立ちます。
GPUアクセラレーションを利用するには、主にWebGLというテクノロジーを使います。WebGLを使うと、ブラウザ上で直接GPUにアクセスして高速な描画処理が可能になるんです。
まずは、基本的なWebGLのセットアップから見ていきましょう:
const canvas = document.getElementById('myCanvas');
const gl = canvas.getContext('webgl');
if (!gl) {
console.error('WebGLがサポートされていません');
return;
}
// 頂点シェーダー
const vertexShaderSource = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0, 1);
v_texCoord = a_texCoord;
}
`;
// フラグメントシェーダー
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_image;
varying vec2 v_texCoord;
void main() {
gl_FragColor = texture2D(u_image, v_texCoord);
}
`;
// シェーダーのコンパイルと関連付け
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
これで、WebGLの基本的なセットアップが完了です。次に、動画のフレームをテクスチャとしてGPUに送り、描画する処理を実装します:
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
0.0, 0.0,
1.0, 0.0,
0.0, 1.0,
0.0, 1.0,
1.0, 0.0,
1.0, 1.0,
]), gl.STATIC_DRAW);
const texCoordLocation = gl.getAttribLocation(program, "a_texCoord");
gl.enableVertexAttribArray(texCoordLocation);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
function updateTexture() {
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
}
function render() {
updateTexture();
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
function nextFrame() {
if (video.paused || video.ended) return;
video.currentTime += 1 / 30; // 30fpsの場合
render();
requestAnimationFrame(nextFrame);
}
このコードでは、動画の各フレームをGPUのテクスチャとして送り、シェーダーを使って描画しています。GPUを使うことで、高速な描画処理が可能になります。
さらに、GPUを使えば複雑な効果も簡単に適用できます。例えば、フレーム間の補間処理を行って、よりスムーズなスロー再生を実現することも可能です:
// フラグメントシェーダーを修正
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_image0;
uniform sampler2D u_image1;
uniform float u_mixture;
varying vec2 v_texCoord;
void main() {
vec4 color0 = texture2D(u_image0, v_texCoord);
vec4 color1 = texture2D(u_image1, v_texCoord);
gl_FragColor = mix(color0, color1, u_mixture);
}
`;
// 2つのテクスチャを用意
const texture0 = gl.createTexture();
const texture1 = gl.createTexture();
function updateTextures() {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture0);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
video.currentTime += 1 / 30;
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texture1);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
}
let mixture = 0;
function render() {
updateTextures();
gl.uniform1f(gl.getUniformLocation(program, "u_mixture"), mixture);
gl.drawArrays(gl.TRIANGLES, 0, 6);
mixture += 0.1;
if (mixture > 1) mixture = 0;
requestAnimationFrame(render);
}
この例では、2つの連続するフレーム間を補間することで、よりなめらかな動きを実現しています。
GPUアクセラレーションを利用することで、高解像度の動画でも滑らかなコマ送りが可能になります。ただし、WebGLの利用には一定の学習コストがかかりますし、すべてのブラウザでサポートされているわけではないので、使用する際はターゲットユーザーのブラウザ環境をしっかり確認する必要がありますね。
ここまでの最適化テクニックを組み合わせることで、非常にスムーズで高性能なコマ送り機能を実現できるはずです。次は、これらの基本機能をベースに、さらに高度な機能を追加する方法を見ていきましょう。
応用編:高度なコマ送り機能の実装テクニック
基本的なコマ送り機能ができたら、次は一歩進んだ機能を追加してみましょう。ユーザビリティを向上させたり、より多様なニーズに応えたりするための機能です。ここでは、可変速再生、複数動画の同期再生、そしてフレーム補間について見ていきます。これらの機能を追加することで、よりプロフェッショナルな動画編集ツールに近づけることができますよ。
可変速再生機能の追加方法とユーザビリティ向上のポイント
コマ送り機能があれば、次は速度を自由に変えられるようにしたいですよね。可変速再生機能を追加することで、ユーザーはスローモーションや倍速再生を楽しむことができます。
まずは、速度を調整するためのUIを追加しましょう:
<input type="range" id="speedSlider" min="0.1" max="2" step="0.1" value="1">
<span id="speedDisplay">1x</span>
そして、JavaScriptで速度調整の処理を実装します:
const speedSlider = document.getElementById('speedSlider');
const speedDisplay = document.getElementById('speedDisplay');
let playbackRate = 1;
speedSlider.addEventListener('input', function() {
playbackRate = parseFloat(this.value);
speedDisplay.textContent = playbackRate.toFixed(1) + 'x';
video.playbackRate = playbackRate;
});
function nextFrame() {
if (video.paused || video.ended) return;
video.currentTime += (1 / 30) / playbackRate; // 30fpsの場合
requestAnimationFrame(nextFrame);
}
この実装では、スライダーの値に応じてplaybackRate
を変更し、nextFrame
関数内でフレームの進み具合を調整しています。
ユーザビリティ向上のポイントとしては、以下のようなものが考えられます:
- キーボードショートカットの追加(例:「+」キーで速度アップ、「-」キーで速度ダウン)
- プリセット速度ボタンの追加(0.5x、1x、1.5x、2xなど)
- 現在の再生速度を動画上に表示
- 速度変更時に音声ピッチを維持するオプション
例えば、キーボードショートカットはこんな感じで実装できます:
document.addEventListener('keydown', (event) => {
switch(event.code) {
case 'Equal': // '+'キー
playbackRate = Math.min(playbackRate + 0.1, 2);
break;
case 'Minus': // '-'キー
playbackRate = Math.max(playbackRate - 0.1, 0.1);
break;
}
speedSlider.value = playbackRate;
speedDisplay.textContent = playbackRate.toFixed(1) + 'x';
video.playbackRate = playbackRate;
});
これらの機能を追加することで、ユーザーはより直感的に、そして細かく再生速度を調整できるようになります。
複数動画の同期再生を実現するコマ送り制御
次は、複数の動画を同時にコマ送りする機能について考えてみましょう。この機能は、例えば異なるアングルで撮影された動画を比較する際などに非常に有用です。
まず、HTMLに複数の動画要素を追加します:
<video id="video1" src="video1.mp4"></video>
<video id="video2" src="video2.mp4"></video>
<button id="syncPlayBtn">同期再生</button>
<button id="syncNextFrameBtn">同期コマ送り</button>
そして、JavaScriptで同期再生の処理を実装します:
const video1 = document.getElementById('video1');
const video2 = document.getElementById('video2');
const syncPlayBtn = document.getElementById('syncPlayBtn');
const syncNextFrameBtn = document.getElementById('syncNextFrameBtn');
function syncPlay() {
video1.currentTime = 0;
video2.currentTime = 0;
video1.play();
video2.play();
}
function syncNextFrame() {
if (video1.paused && video2.paused) {
video1.currentTime += 1 / 30;
video2.currentTime += 1 / 30;
}
}
syncPlayBtn.addEventListener('click', syncPlay);
syncNextFrameBtn.addEventListener('click', syncNextFrame);
// 一方の動画が一時停止されたら、もう一方も一時停止
video1.addEventListener('pause', () => video2.pause());
video2.addEventListener('pause', () => video1.pause());
この実装では、2つの動画を同時に再生開始したり、同時にコマ送りしたりすることができます。さらに、一方の動画が一時停止されたら、もう一方も自動的に一時停止するようになっています。
より高度な同期制御を行いたい場合は、動画の再生状態を常に監視し、ずれが生じた場合に自動的に補正する機能を追加するとよいでしょう。
WebGL技術を駆使した高品質フレーム補間手法
最後に、WebGLを使ってフレーム補間を行う高度な手法を見てみましょう。これにより、元の動画よりもさらに滑らかなスロー再生を実現できます。
まず、フレーム補間用のシェーダーを用意します:
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_image0;
uniform sampler2D u_image1;
uniform float u_mixture;
varying vec2 v_texCoord;
vec4 cubic(float v) {
vec4 n = vec4(1.0, 2.0, 3.0, 4.0) - v;
vec4 s = n * n * n;
float x = s.x;
float y = s.y - 4.0 * s.x;
float z = s.z - 4.0 * s.y + 6.0 * s.x;
float w = 6.0 - x - y - z;
return vec4(x, y, z, w) * (1.0/6.0);
}
vec4 textureBicubic(sampler2D sampler, vec2 texCoords) {
vec2 texSize = vec2(textureSize(sampler, 0));
vec2 invTexSize = 1.0 / texSize;
texCoords = texCoords * texSize - 0.5;
vec2 fxy = fract(texCoords);
texCoords -= fxy;
vec4 xcubic = cubic(fxy.x);
vec4 ycubic = cubic(fxy.y);
vec4 c = texCoords.xxyy + vec2(-0.5, +1.5).xyxy;
vec4 s = vec4(xcubic.xz + xcubic.yw, ycubic.xz + ycubic.yw);
vec4 offset = c + vec4(xcubic.yw, ycubic.yw) / s;
offset *= invTexSize.xxyy;
vec4 sample0 = texture2D(sampler, offset.xz);
vec4 sample1 = texture2D(sampler, offset.yz);
vec4 sample2 = texture2D(sampler, offset.xw);
vec4 sample3 = texture2D(sampler, offset.yw);
float sx = s.x / (s.x + s.y);
float sy = s.z / (s.z + s.w);
return mix(mix(sample3, sample2, sx), mix(sample1, sample0, sx), sy);
}
void main() {
vec4 color0 = textureBicubic(u_image0, v_texCoord);
vec4 color1 = textureBicubic(u_image1, v_texCoord);
gl_FragColor = mix(color0, color1, u_mixture);
}
`;
このシェーダーでは、バイキュービック補間を使用して高品質なフレーム補間を行っています。
次に、このシェーダーを使ってフレーム補間を行う関数を実装します:
function interpolateFrames(mixture) {
gl.uniform1f(mixtureLoc, mixture);
gl.drawArrays(gl.TRIANGLES, 0, 6);
}
function render() {
if (video.paused || video.ended) return;
const frameTime = 1 / 30; // 30fpsの場合
const desiredTime = video.currentTime + frameTime * playbackRate;
while (video.currentTime < desiredTime) {
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture0);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
video.currentTime += frameTime;
gl.activeTexture(gl.TEXTURE1);
gl.bindTexture(gl.TEXTURE_2D, texture1);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video);
const subFrames = 4; // 補間するフレーム数
for (let i = 0; i < subFrames; i++) {
interpolateFrames(i / subFrames);
// ここで描画されたフレームを表示または保存
}
}
requestAnimationFrame(render);
}
この実装では、2つの連続するフレーム間に4つの中間フレームを生成しています。これにより、元の動画よりも滑らかな動きを実現できます。
以上で、高度なコマ送り機能の実装テクニックの紹介を終わります。これらの機能を組み合わせることで、プロフェッショナルレベルの動画編集ツールに近い機能を持つWebアプリケーションを作ることができます。
JavaScriptで動画をコマ送りする方法について、基礎から応用まで幅広く解説してきました。初心者の方には少し難しい部分もあったかもしれませんが、一つずつ理解していけば、きっと素晴らしい動画ツールが作れるはずです。
最後に、こういった高度な機能を実装する際は、ブラウザの互換性やパフォーマンスの問題にも注意を払う必要があります。ユーザーの環境に応じて適切な機能を提供できるよう、十分なテストとフォールバック処理の実装をお忘れなく。
それでは、楽しいコーディングライフを!何か質問があれば、いつでも聞いてくださいね。