▶️

大学の動画プレイヤーをYoutube風に改造してみた

2023/06/07に公開

はじめに

Youtube風にしてみました。

動画はサンプルです

デモ

デモはcodepenにアップしています。(音が出ます)
https://codepen.io/rrisland/pen/yLQLBpP

作るきっかけ

通信制大学に通っていると教材はすべて動画。
それなのに大学指定の動画プレイヤーが使い辛過ぎる。
Youtubeに慣れた現代っ子には耐えられない。

なら、改造してYoutubeにすればいいじゃん。

実装の流れ

JSチョットワカル。という人はここだけ見てもOK。

  1. Video.js<video>Custom Elementにする<videojs-video>をクラス継承。
  2. 埋め込みURLから動画情報をパースする。
  3. config.jsonをフェッチしてポリシーキーを抽出。
  4. Brightcove PlaybackAPIから動画のソースを取得する
  5. ソースをthis.srcに挿入して継承元のload関数を実行する。
  6. Media Chromeという<video>の装飾用ライブラリでラップ。
  7. Media Chrome用のYoutubeテーマをGithubからjsDelivrを通して取得・適用。
  8. 1~5をnpm<brightcove-video>という名前でライブラリ化。

実装

STEP0. 改造のためのリサーチ

とても長いので見たい人だけどうぞ

大学で使われている動画プレイヤー

大学で使われている動画プレイヤーはBrightcove Playerという。
あまり知られていないが、Brightcoveはビジネス向けの動画パブリッシング/ストリーミングサービスを提供している会社でVideo.jsのスポンサーらしくBrightcove Playerの内部実装はVideo.jsだと宣伝されていた。
Brightcove Playerは「動画をアップロードした人が設定した通りの動作しかしない」が、著者の通う大学ではほぼ未設定の状態だった。
PlayerAPIから設定できる部分はChrome拡張を自作して利便性は上がったが、見た目が致命的にダサい。やはり、根本から変えるしかない。

Video.jsの改造計画

Brightcove PlayerVideo.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 schemeMicro interactionなど、細かい部分の知識ばかりが増えていく。

Media Chromeの発見

原点回帰してVideo.jsを調べ始め、Muxという会社にたどり着いた。
どうやらVideo.jsのメインデベロッパー達が新しくMux Playerというモダンな動画プレイヤーを作っているらしい。めちゃくちゃ良さそう。
期待しながらドキュメントを見るとMuxにアップロードした動画にしか対応していない
まただめなのか...

ん?何かGithubのリンクあるな...

内部実装オープンソースじゃん!!!!

Youtubeテーマある!!!これだーーーー!!!

STEP1. Video.js Custom Elementを拡張する

https://github.com/luwes/videojs-video-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をパースする

https://player.support.brightcove.com/plugins/video-seo-schema-generator-plugin.html#Plugin_options
動画の埋め込み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をフェッチしてポリシーキーの抽出をする

https://player.support.brightcove.com/references/player-configuration-guide.html#View_configuration
動画情報を取得するためにポリシーキーが必要なので、アカウントの設定を覗きたい。
パースした情報を以下のように構成すれば、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から動画ソースを取得する

https://apis.support.brightcove.com/playback/getting-started/quick-start-playback-api.html
動画情報をPlaybackAPIにリクエストするには、ヘッダーの'Accept'に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でラップする

https://www.media-chrome.org/docs/en/get-started

ドキュメントを参考に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から引っ張ってくる

https://github.com/muxinc/media-chrome/blob/main/src/js/themes/
上記のGithubからyoutube.jsを引っ張ってきたいけれど、直リンクでimportはダメだしそもそもESMではない。
https://www.jsdelivr.com/?docs=gh
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になったぞ!

STEP8. 1~5をnpmでbrightcove-video-elementとして公開する

とても長いので見たい人だけどうぞ

著者はJS初心者かつ今回が初めてのnpmでのライブラリパブリッシュ
その上、今回のプロジェクトはVanilla JSでPure ESM packageとかいう特殊な状況。
案の定、資料はTypeScriptのものしかなく、手探りで進むことに...
https://zenn.dev/mr_ozin/articles/6504541653fcb2
npmやパッケージの作り方について無知の状態で上記の記事にたどり着いた。
必死に読み込んでいるとTypeScriptはJSに変換してからパブリッシュしているということやCJSやESMの違いを知る。
https://dev.classmethod.jp/articles/introduction-to-github-packages/
悪戦苦闘している最中にGithub Packagesというnpm publishを管理してくれるサービスがあると知り、Github Actionsを勉強しながら動かすと無事パブリッシュが完了。

やった、これで誰でも使えるようになった...あれ、npm installできないぞ

Scoped Packageなことが原因と思っていたが、エラーを見るとパーソナルアクセストークンが必要と書かれている。
ここでGithub Packagesって普通のnpmとは違うんだと悟り、数時間絶望の淵に...
https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
Github Actionsのドキュメントと数時間格闘の後、CI/CDが無事に通った!やった!

あ...Readme空のままパブリッシュしちゃった。

major, minor, patchバージョンの違いが分からず「機能的に変わっていないけど、バージョンって変えて良いの?」と大混乱しながら何とかpatchで更新。

などと様々なミスをしながらも無事?パブリッシュは完了。
https://www.npmjs.com/package/brightcove-video-element

あとがき

Media Chromeは様々なテーマがあるので、好きなプラットフォーム風に改造できそう
字幕や再生速度、進む戻るなどのプラグインを入れないといけない機能が標準搭載されている上、ソースごとに違う見た目を統一できてとても便利。

皆さんも使いづらい動画プレイヤーに悩んだらMedia Chromeで改造してみましょう!

宣伝

Discussion