大学の動画プレイヤーをYoutube風に改造してみた
はじめに
Youtube風にしてみました。
動画はサンプルです
デモ
デモはcodepenにアップしています。(音が出ます)
作るきっかけ
通信制大学に通っていると教材はすべて動画。
それなのに大学指定の動画プレイヤーが使い辛過ぎる。
Youtubeに慣れた現代っ子には耐えられない。
なら、改造してYoutubeにすればいいじゃん。
実装の流れ
JSチョットワカル。という人はここだけ見てもOK。
-
Video.jsを
<video>
のCustom Elementにする<videojs-video>
をクラス継承。 - 埋め込みURLから動画情報をパースする。
-
config.json
をフェッチしてポリシーキーを抽出。 - Brightcove PlaybackAPIから動画のソースを取得する
- ソースを
this.src
に挿入して継承元のload関数を実行する。 -
Media Chromeという
<video>
の装飾用ライブラリでラップ。 - Media Chrome用のYoutubeテーマをGithubからjsDelivrを通して取得・適用。
- 1~5をnpmで
<brightcove-video>
という名前でライブラリ化。
実装
STEP0. 改造のためのリサーチ
とても長いので見たい人だけどうぞ
大学で使われている動画プレイヤー
大学で使われている動画プレイヤーはBrightcove Playerという。
あまり知られていないが、Brightcoveはビジネス向けの動画パブリッシング/ストリーミングサービスを提供している会社でVideo.jsのスポンサーらしくBrightcove Playerの内部実装はVideo.jsだと宣伝されていた。
Brightcove Playerは「動画をアップロードした人が設定した通りの動作しかしない」が、著者の通う大学ではほぼ未設定の状態だった。
PlayerAPIから設定できる部分はChrome拡張を自作して利便性は上がったが、見た目が致命的にダサい。やはり、根本から変えるしかない。
Video.jsの改造計画
Brightcove PlayerはVideo.jsのラッパーなので、Video.jsのカスタムスキンが使える。
プラグインの一覧で探すもパッと見で良さげなものが見当たらない。
しかも、Brightcove Playerのスキンが混ざってちぐはぐな見た目になることもしばしば。
「Youtubeみたいにしたい」と調べvideojs-youtube
にたどり着くも、Youtubeの動画を再生させるだけで見た目は変わらない。
終わった...Video.jsはYoutubeにはならない...
新しいVideo Playerを探す旅へ
Video.jsに見切りをつけ、JSで使える動画プレイヤーを調べ始める。
要件は「Video.jsで標準サポートされていたm3u8やmpdに対応していてモダンな見た目なこと」
ライブラリ | 所感 |
---|---|
<video> |
シンプルで使い勝手は良いが、m3u8やmpdに未対応。 |
hls.js |
m3u8に対応しているが、mpdに未対応。 |
dash.js |
mpdに対応しているが、m3u8に未対応。 |
Youtube PlayerAPI |
ソースだけ差し替えられないか調べるも無理。 |
YoutubeのColor schemeやMicro interactionなど、細かい部分の知識ばかりが増えていく。
Media Chromeの発見
原点回帰してVideo.jsを調べ始め、Muxという会社にたどり着いた。
どうやらVideo.jsのメインデベロッパー達が新しくMux Playerというモダンな動画プレイヤーを作っているらしい。めちゃくちゃ良さそう。
期待しながらドキュメントを見るとMuxにアップロードした動画にしか対応していない。
まただめなのか...
ん?何かGithubのリンクあるな...
内部実装オープンソースじゃん!!!!
Youtubeテーマある!!!これだーーーー!!!
STEP1. Video.js Custom Elementを拡張する
VideojsVideoElement
クラスがVideo.jsを<video>
タグのCustom Elementしている。
拡張するにはVideojsVideoElement
を継承するのが楽そう。
// CDNからVideojsVideoElementをインポート(デフォルトなので名前を付ける)
import VideojsVideoElement from 'https://esm.run/videojs-video-element@1.0'
// 本体
class BrightcoveVideoElement extends VideojsVideoElement {
async load() {
// TODO: ここに処理を書いていく
super.load();
}
}
// HTML Custom Elementとして定義
if (!globalThis.customElements.get('brightcove-video')) {
globalThis.customElements.define('brightcove-video', BrightcoveVideoElement);
}
export default BrightcoveVideoElement;
STEP2. 埋め込みURLからアカウントやビデオのidをパースする
動画の埋め込みURLの構造は以下の通り。
https://players.brightcove.net/{accountId}/{playerId}_{embedId}/index.html?videoId={videoId}
this.src
に格納されたURLを以下のようにパースできる。
const MATCH_SRC = /players\.brightcove\.net\/(?<accountId>\d+)\/(?<playerId>\w+)_(?<embedId>\w+)\/index\.html\?videoId=(?<videoId>(\d+|ref:\w+))/;
const matches = this.src.match(MATCH_SRC);
const {accountId, playerId, embedId, videoId} = matches.groups;
クラスの実装状況
class BrightcoveVideoElement extends VideojsVideoElement {
async load() {
const matches = this.src.match(MATCH_SRC);
if (matches && matches.groups) {
// この下が追加分
const {accountId, playerId, embedId, videoId} = matches.groups;
console.log(matches.groups);
}
super.load();
}
}
STEP3. config.jsonをフェッチしてポリシーキーの抽出をする
パースした情報を以下のように構成すれば、config.json
にアクセスできる。
https://players.brightcove.net/{accountId}/{playerId}_{embedId}/config.json
config.json
をフェッチするための関数を用意。
ポリシーキーはconfig.video_cloud.policy_key
から取得できる。
const EMBED_BASE = 'https://players.brightcove.net/';
// エラー処理は外で頑張って
async function fetchJson(url, options = {}) {
const response = await fetch(url, options);
const json = await response.json();
return json;
}
async function fetchConfig(accountId, playerId, embedId) {
const url = `${EMBED_BASE}/${accountId}/${playerId}_${embedId}/config.json`;
return await fetchJson(url);
}
// 呼び出し側
const config = await fetchConfig(accountId, playerId, embedId);
クラスの実装状況
class BrightcoveVideoElement extends VideojsVideoElement {
async load() {
const matches = this.src.match(MATCH_SRC);
if (matches && matches.groups) {
const {accountId, playerId, embedId, videoId} = matches.groups;
// この下が追加分
const config = await fetchConfig(accountId, playerId, embedId);
console.log(config);
}
super.load();
}
}
STEP4. Brightcove PlaybackAPIから動画ソースを取得する
application/json;pk={policyKey}
を設定する必要がある。
const API_BASE = 'https://edge.api.brightcove.com/playback/v1';
async function fetchVideoInfo (accountId, videoId, policyKey) {
const requestUrl = `${API_BASE}/accounts/${accountId}/videos/${videoId}`;
const options = {
method: 'GET',
headers: {'Accept': `application/json;pk=${policyKey}`}
};
return await fetchJson(requestUrl, options);
}
// 呼び出し側
const videoInfo = await fetchVideoInfo(accountId, videoId, policyKey);
クラスの実装状況
class BrightcoveVideoElement extends VideojsVideoElement {
async load() {
const matches = this.src.match(MATCH_SRC);
if (matches && matches.groups) {
const {accountId, playerId, embedId, videoId} = matches.groups;
const config = await fetchConfig(accountId, playerId, embedId);
// この下が追加分
const policyKey = config.video_cloud.policy_key;
const videoInfo = await fetchVideoInfo(accountId, videoId, policyKey);
console.log(videoInfo);
}
super.load();
}
}
STEP5. 取得したソースを適用してCustom Elementは完成
Brightcove Videoはhttpとhttpsの両方をCDNに登録している。
重複する分を除外するためにstartsWith('https')
でフィルタをする。
this.srcに取得したソースを挿入して完成。
class BrightcoveVideoElement extends VideojsVideoElement {
async load() {
const matches = this.src.match(MATCH_SRC);
if (matches && matches.groups) {
const {accountId, playerId, embedId, videoId} = matches.groups;
const config = await fetchConfig(accountId, playerId, embedId);
const policyKey = config.video_cloud.policy_key;
const videoInfo = await fetchVideoInfo(accountId, videoId, policyKey);
// 以下を追加
const sources = videoInfo.sources.filter(x => x.src.startsWith('https'));
this.src = sources[0].src;
}
super.load();
}
}
スクリプトをimportして以下のコードが動けば拡張完了!
<!--動画はBrightcove Player公式のコードサンプルから拝借-->
<brightcove-video controls src="https://players.brightcove.net/1752604059001/Nynfq6Yde_default/index.html?videoId=4029697544001"></brightcove-video>
<!--標準だと小さすぎるので拡張-->
<style>
brightcove-video {
height: 480px;
aspect-ratio: 16 / 9;
}
</style>
デモ
STEP6. Media Chromeでラップする
ドキュメントを参考にmedia-chromeをimportした後、<media-contrller>
で<brightcove-video>
をラップして動いたら次のステップへ。
<script type="module" src="https://esm.run/media-chrome@1.0"></script>
<media-controller>
<brightcove-video
slot="media" controls src="https://players.brightcove.net/1752604059001/Nynfq6Yde_default/index.html?videoId=4029697544001"
></brightcove-video>
<!--media-control-bar 省略-->
</media-controller>
<!--標準だと小さすぎるので拡張-->
<style>
media-controller {
height: 480px;
aspect-ratio: 16 / 9;
}
</style>
デモ
STEP7. Youtube ThemeをGithubから引っ張ってくる
上記のGithubからyoutube.jsを引っ張ってきたいけれど、直リンクでimportはダメだしそもそもESMではない。 JsDelivrにGithubのコンテンツへの仲介をしてくれるサービスがある。今回はこれを使おう。
<script type="module" src="https://cdn.jsdelivr.net/gh/muxinc/media-chrome@1.0/src/js/themes/youtube.js"></script>
上記をimportして<media-theme-youtube>
で<brightcove-video>
をラップすれば...完成!
やった!Youtubeになったぞ!
brightcove-video-element
として公開する
STEP8. 1~5をnpmでとても長いので見たい人だけどうぞ
著者はJS初心者かつ今回が初めてのnpmでのライブラリパブリッシュ。
その上、今回のプロジェクトはVanilla JSでPure ESM packageとかいう特殊な状況。
案の定、資料はTypeScriptのものしかなく、手探りで進むことに...
npmやパッケージの作り方について無知の状態で上記の記事にたどり着いた。
必死に読み込んでいるとTypeScriptはJSに変換してからパブリッシュしているということやCJSやESMの違いを知る。
悪戦苦闘している最中にGithub Packagesというnpm publish
を管理してくれるサービスがあると知り、Github Actionsを勉強しながら動かすと無事パブリッシュが完了。
やった、これで誰でも使えるようになった...あれ、npm installできないぞ
Scoped Packageなことが原因と思っていたが、エラーを見るとパーソナルアクセストークンが必要と書かれている。
ここでGithub Packagesって普通のnpmとは違うんだと悟り、数時間絶望の淵に...
Github Actionsのドキュメントと数時間格闘の後、CI/CDが無事に通った!やった!
あ...Readme空のままパブリッシュしちゃった。
major, minor, patch
バージョンの違いが分からず「機能的に変わっていないけど、バージョンって変えて良いの?」と大混乱しながら何とかpatch
で更新。
などと様々なミスをしながらも無事?パブリッシュは完了。
あとがき
Media Chromeは様々なテーマがあるので、好きなプラットフォーム風に改造できそう。
字幕や再生速度、進む戻るなどのプラグインを入れないといけない機能が標準搭載されている上、ソースごとに違う見た目を統一できてとても便利。
皆さんも使いづらい動画プレイヤーに悩んだらMedia Chromeで改造してみましょう!
宣伝
- Githubで今回のプロジェクト公開しています。MITライセンスなので自由に改造してください。
https://github.com/rrisland/brightcove-video-element - Youtubeテーマは改造することができます。以下を参考にしてください。
https://codepen.io/rrisland/pen/PoyVxqo - Youtubeの字幕生成機能欲しくてFaster-Whisperで自作しました。ご自由にお使いください。
https://colab.research.google.com/drive/1r5Ob_-zzRLfpFsfPtwK510YlI5sCnlnW?usp=sharing - Brightcove Playerを便利にするChrome拡張作りました。
https://chrome.google.com/webstore/detail/brightcove-player-extensi/lggakkakbeebblbphpiplnoipfojclam
Discussion