ズーム機能付きビデオプレイヤーを作成するためのアイデア
こんにちは!
ALH開発エンジニアのY.Nです。
はじめに
webページに動画を埋め込むためのライブラリは有名どころはvideo.js、react-player、plyrから他にも色々ありますが、どれも再生停止、秒送り、再生速度などを操作する機能のみのものがほとんどです。※一般的な使用にはそれで十分だとは思いますが...
任意の箇所を拡大して見たいというのは意外と需要がある気がしますが、意外と動画をズームして見るためのライブラリや方法の紹介等がありませんでしたので自分なりに思いついた方法を解説してみます。
※今回作成するものは、ほとんどネット記事の組み合わせなので独自性は皆無ですが
職務の関係上ソースコードの公開はせずに、私が参考にした記事の解説に留めます。ご了承ください。
仕様
以下、二つの仕様を満たす動画プレイヤを実装します。
デモはこんな感じ↓
実装方法の検討
webページに動画を埋め込むにはvideoタグを使用しますが、当然videoタグには拡大縮小の機能はないので何らかの処理を自作して搭載する必要があります。
私が検討した限りでは実装方法は二つほどありました。
今回は1の方法で進めますが、結論からいいますと2の方が処理の重たさ等含めて優秀な方法だと思われます。
私の場合は1の方がイメージがしやすく実装が容易かったので2は捨てました。
CSSの知識に自信がなかったのでcanvasのように一つの要素にできる方が後々レスポンシブ対応する際に便利だと考えたのですがおそらくそんなことはない気がします。
以下でも言及されていますが、ズーム機能のためだけにcanvasを使用するのは大がかりすぎるとのこと。耳が痛い...
上記の記事ではtranslate(), scale()を使っていますが今はmatrix()で全て賄えるのでそちらを使うのがよいかと。
1と2どちらを採用するにしてもマウスの座標位置の取得やホイールでのイベント発火、表示領域の座標計算は同じようになるはずですので興味がある方は是非CSSの方(↓)も挑戦してみてください。
実装方法の解説
では本題です。
canvasはグラフィックやアニメーションを描画するためのHTML要素ですがdrawImage()メソッドを使ってHTMLのImage要素やvideo要素を画像としてキャンバスに張り付けることができます。
drawImage()メソッドは最大9個の引数を取り、要素の切り取り位置や縮尺を指定することができます。
canvas: グラフィックキャンバス要素
context . drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
よって
ことで動画を拡大縮小しているように見せることができそうです。
では上記の手順を具体的に解説してみます。
切り取り領域の位置、範囲、縮尺の計算
まず、drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)メソッドについて仕様を確認します。
つまり今回の場合、video要素をHTMLから取得し、動画の解像度が1920×1080なら1920×1080のcanvas(実際表示する大きさはCSSで指定できる)を生成し
dx,dyは(0,0)、dw, dhは(1920,1080)固定で
倍率1倍なら、drawImage(video, 0, 0, 1920, 1080, 0, 0, 1920, 1080)
中心に向かって倍率を2倍ならdrawImage(video, 480, 270, 960, 540, 0, 0, 1920, 1080)
これで2倍拡大を実現できます。
実際の実装ではマウスポインタの座標を中心にマウスホイールを回した際に差分の計算を行うので
マウスホイール1回:+0.2倍とすると
倍率1倍(初期値)からホイール1回
→
sx += canvas上のマウスポインタx座標 * 0.2 / (1 + 1 * 0.2) * ((1 + 1 * 0.2) - 0.2)
shも同様
となります。正直何が何やらですが仮の値を色々入れてみるとイメージつきやすいかと思います。
また拡大した動画のドラッグ移動は
クリックしたマウスの移動差分を保持してその分sx,shから引いてあげれば実現できます。
この座標計算は以下サイトを基にしています。
今回の実装は以下参考にすればほぼ完了です。
上記サイトについていくつか補足説明します。
HTML要素の取得、処理タイミング
基本的な事ですが「document.getElementById」などは画面が描画されてから初めて取得できるのでreact等使用している場合は、componentDidMount、useEffectでマウント後に処理が走るようする必要があります。
切り取り領域の限界値
最後のcodepen70行目などの
zoomLeft = Math.max(0, Math.min(canvas.width - zoomWidth, zoomLeft));
は切り取り領域が元のソースからはみ出ないようにするための記述です。
切り取り領域x座標の左端(zoomLeft)が必ず 0 < canvas.width - zoomWidth の範囲になります。
レスポンシブ対応
canvas.addEventListener('mousemove', mouseMove);
で引数として渡されるeからは
let rect = e.target.getBoundingClientRect();
とすることで
レスポンシブ対応などでキャンバスの表示上の大きさが変わる場合
マウス位置やドラッグ距離を計算している箇所
mouseX = e.clientX - rect.left;
などでは実際の座標と値がずれてしまうため
(生成する際に指定したcanvasサイズ / 表示上のcanvasサイズ)を掛けて縮尺を反映させてあげる必要があります。
canvas.addEventListener('mousewheel' は非推奨
細かいですが、addEventListenerのキーである'mousewheel'は現在非推奨となっているため'wheel'にリファクタした方が無難です。
以上で任意の点を中心に、画像や動画を拡大・縮小する方法についての補足説明終わりです。これで画像なら仕様を満たすことはできました。
rawImageメソッドは画像も動画も同じように画像に切り取ってcanvasに張り付ける仕様なのでdrawImageメソッドの第一引数をgetElementByIdしたvideo要素にすると処理の瞬間の画像として適用されます。あとは動画として見せるためにパラパラ漫画のように処理を連続させるだけです。
drawImage()メソッドを秒間30~60回走らせる
videoタグの動画をcanvasに描写する方法は以下が参考になります。
ズバリ方法は1秒間に60回drawImage()する、です。
対象メソッドを連続で処理し続けるためにはsetInterval()メソッドを使用します。使い方は
setInterval({対象のメソッド}, {処理の間隔})
で処理の間隔にはミリ秒単位で指定できます。例えば30フレームなら30/1000、60フレームなら60/1000となります。
60フレームにしておけば問題ないですが、処理の重さが気になる場合は30フレームに下げると良さそうです。
画面上に二つの動画が...
ここまで来ればもう一息です。
今の状態では素のvideoタグ
と動画を描画しているcanvasタグ
2つの動画が画面に表示されてしまっていると思います。なのでcanvasの方をvideoタグの上に重ねる必要があります。
要素を重ねる方法はCSS「position:relative」と「position:absolute」で画像や文字を重ねる方法等を参考にvideoタグの方をposition: "absolute"にしてzIndexを-1等すれば実現できます。
おわり
以上で、ズーム機能のある動画プレイヤを実装することができたと思います。
よくある機能かと思いきや意外と記事が少なくて苦戦しました...
だいぶごり押し感のある実装でしたが、一つの方法として見てもらえればと思います。
canvasちょこちょこ触って思いましたが、落書き機能のある動画プレイヤとかも面白そうですね、それならわざわざcanvasに描画した意味もあるのか...?とも思いましたが結局レイヤーのように透過させたcanvasをもう一枚上に重ねる必要がありそうなのでやっぱり関係ないですね。
ちなみに
今回は静的なmp.4ファイルを配置しvideoタグに読み込みましたがvideoタグに描写ができればどんな形式でもズーム機能が適用できるのでhls.jsなどを使えば.m3u8ファイルを読み込んでズーム機能付き配信プレイヤーなんていうのも可能です。
では、読んでいただきありがとうございました。
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ 採用サイトはこちら ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ALHについてはこちら ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ もっとALHについて知りたい? ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓