みなさんこんにちは、サイバーエージェントのFRESH!というサービスでフロントエンドの開発しています鈴木です。
FRESH!は生放送に特化した「映像配信プラットフォーム」で生放送というサービスの特性上、HTTP Live Streaming (HLS) というストリーミングプロトコルを用いて映像を配信しています。
そのFRESH!では、2016年8月頃から動画プレイヤーをFlashを用いない新しいプレイヤーにリプレイスしました。(Google Chrome, Firefox, Safari10, Edge, Opera, Vivaldi)
このエントリーでは、その移行とその後の得られた結果について紹介いたします。
経緯
2016.01 サービス開始当初
Video.js v4.x(videojs-contrib-hls v0.16.x Flashベース)でプレイヤーを提供していました。
2016.06
各ブラウザにおいて数ヶ月〜1年を目処にFlashの自動再生(実行)がデフォルトでオフになる旨のアナウンスが行われました。ブラウザベンダによる Flash 包囲網の現状メモ
当時の時点でGoogle Chromeに関して早ければ9月から上記の措置がとられるかもしれないということで、実装コストが低く安定してそうなインターフェスが同じVideo.js v5.x(videojs-contrib-hls v3.x) にアップデートしようと目論みました。
Video.jsを使ってHLSの動画を再生するにはvideojs-media-sources (Media Source Extensionsのシュミレート) とvideojs-contrib-hls (m3u8のプレイリストを再生可能にする) の2つのプラグインを使います。
Video.js v5.xの最大のメリットは下記のように引数に渡すオプションを変えることによってFlashを使いたいか否かを容易に切り分けれる点で、IE11などは引き続きFlashを使いながらの再生にするつもりでした。
// html5 for html hls
videojs(video, {html5: {
hls: {
withCredentials: true
}
}});
// flash for flash hls
videojs(video, {flash: {
hls: {
withCredentials: true
}
}});
2016.07 上旬
ところが、Video.js v5.xで検証を続けたところいくつかの問題が発生しました。
- Safari/EdgeではそれぞれでサポートしているネイティブのHLS再生方法以外で再生が行えない
- 一部の番組でtsファイルの暗号化に独自AESを用いたため、上記の理由によりSafari/Edgeの再生が不可能であった
- 特定のプレイリスト(.m3u8)だと音声が二重に再生されエコーがかかったような感じになってしまう
- 一定以上の長さの番組(12時間以上)の場合presentation time stamp(PTS)が狂ってしまい、ある地点から再生できなくなってしまう(この辺の計算が狂う)
2016.07 中旬
上記の問題が深刻でなおかつ解決の方法を見出だせなかったためVideo.js v5.x(videojs-contrib-hls v3.x)の導入を見送ることにしました。
代わりに当時安定性に多少懐疑的であったもののパフォーマンスの良いMSEベースのhls.jsをGoogle Chromeから導入していくことにしました。
2016.07 下旬
従来のVideo.js v4.xとhls.jsでの再生をクロスブラウザで行うためプレイヤーの制御を行うUIパーツの実装をすすめていきました。
このようなラッパーもあったのですが、FRESH内での独自UIパーツ(30秒スキップなど)があったため導入は見送り自前で組み込みを行いました。Reactのコンポーネント設計に関しては↓の26ページ〜に詳しく書かれています。
コントロールバーの実装例
// project/FreshVideo/index.js
'use strict';
import React from 'react';
import ControlBar from './ControlBar';
import Video from '../../ui/Video';
/**
* ui/Video のラッパー
*/
export default class FreshVideo {
static propTypes = {
/* 略 */
};
/**
* 再生
* 上位コンポーネントから Video コンポーネントの `play()` を実行するための橋渡し(上位コンポーネントへの公開用インターフェース)
*/
play() {
this.refs.video.play();
}
/**
* 一時停止
*/
pause() {
this.refs.video.pause();
}
// 他にも 再生位置、シーク位置、ボリューム・ミュート、バッファー、Duration(長さ)などの処理が入ります
render() {
return (
<div>
<Video {...props} />
{ /* ControlBarコンポーネント以外でも */ }
{ /* Videoの操作を行うためFreshVideoに操作系のメソッドを生やしている */ }
<Controlbar play={this.play}
pause={this.pause}
getposition="{this.getPosition}"></Controlbar>
{ /* 略 */ }
</div>
);
}
}
// ui/Video/index.js
'use strict';
import uaParser from 'ua-parser-js';
import { canUseMSE } from '../../../utils/CanUseMSE';
import VideoJs from './Video';
import HlsJs from './Hls';
export default canUseMSE(uaParser().browser) ? HlsJs : VideoJs;
// project/FreshVideo/ControlBar.js
'use strict';
import React from 'react';
import Button from '../../ui/Button';
import Icon from '../../ui/Icon';
/**
* ControlBar
*/
export default class ControlBar {
static propTypes = {
/* 略 */
};
render() {
return (
<div>
{ /* FreshVideoから渡ってきたpropsとDOMを組み合わせてVideoを操作できるようにする */ }
{ /* ex */ }
{
playerStatus === 'play'
? (
<Button onClick={this.props.pause}>
<Icon name="pause" size="xxl" color="white" />
</button>
) : (
<Button onClick={this.props.play}>
<Icon name="play" size="xxl" color="white" />
</button>
)
}
{ /* 以下略 */ }
</div>
);
}
}
テスト方法
プレイヤーの検証と同時に並行してエイジングテストを行っていきました。弊社にはsmaqという沖縄に拠点を置くテスト部隊があり、実装したプレイヤーをステージング環境にデプロイして長時間の動作確認をしてもらいました。
レベル | 基準 |
5 | ? 正常 |
4 | ? 映像がカクつくが許容できる |
3 | ? 映像がカクつくてイラっとくる |
2 | ? 映像が止まる(自動復帰) |
1 | ? 映像が止まる(永久停止) |
0 | ? 動かない |
動画の再生レベルを0〜5段階に設定し、2時間視聴した際にどのレベルになっているかを確認していきました。テストする番組は開発環境のものではなく、実際に配信者が配信している番組を使用し配信者が使うネットワークやPCで問題ないかを調査していきました。
また目視と並行してhls.jsのerrorイベントを拾って映像に異常があった場合にconsole上にエラーが表示されていなかったかどうかなども確認していきました。
2016.8中旬
これらの地道なテストを経てGoogle Chromeを対象に脱Flash化をしリリースを行いました。
2016.10
Google Chrome以外のモダンブラウザ(IE11とSafari9を除く)が脱Flash化されました。
- IE11はWindows7でMedia Source Extensionsが使えないため時代とともに去りゆくのを待っている状況
- Safari9では EXT-X-DISCONTINUITY で再生が切り替わらい状況が発生していたので原因調査中
結果
パフォーマンス
Flashベースのプレイヤーと比較してエンドユーザーにはどのようなメリットがあるのか把握したく、幾つか検証を行いました。
体感速度
少々アナログですが、それぞれのプレイヤーで画面ロード、SPAでのページ内遷移(2番組)、シークでの読み込みで読み込み速度の違いを比較しました。どの状態でもhls.jsでの速度の方が速く、Flashの初期化コストより低いことがわかります。
60fpsの動画が快適に見れる
上記URLの地点からの映像を1分間ほどのFrame Rateを計測しました。動画の通り画面左側のVideo.js v4.xではところどころFrame Valuesが落ちていて一瞬カクつくようなことがありますが、画面右側のhls.jsでは60fpsからほとんど落ちることなく滑らかに再生できています。
バッテリー
下記の表はフル充電の状態から1時間同じ番組を視聴し続けた後、バッテリー残量がどのくらいになっているかを表したものです。
機種 | hls.js | Video.js |
---|---|---|
MacBook Air 11inc | 54% | 41% |
Lenovo X230 | 82% | 72% |
視聴する番組など諸条件によって多少の増減はあると思いますが、CPUの使用率が低いhls.jsではバッテリーの減りがVideo.jsに比べて遅くより省エネネルギーで視聴できています。
hls.jsを使いだしてからの気づき
maxBuffer
hls.jsでbufferをどれだけ持っておくかのオプションにmaxBufferというパラメータがあります。この値がデフォルト値で600secと結構長く、アーカイブ番組を視聴するとどんどんtsファイルにリクエストされます。結果、Video.jsを使っていた頃より数倍アクセスされてしまい、tsファイルのCDNであるCloudFrontのご利用料金があガクンとあがってしまいました。
startLevel
HLSの仕様ではマスタープレイリストにリクエストすると、その中にビットレートごとに別れた各画質のプレイリストが返却されます。まず始めに一番低い画質のプレイリストが選択され、その後ユーザーのネットワーク速度に応じて最適なプレイリストが選ばれるようになっています。
$ curl https://movie.freshlive.tv/manifest/76151/live.m3u8 // マスタープレイリストの例
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=220000,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=256x144
/playlist/574204.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=730000,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=512x288
/playlist/574205.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1600000,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=768x432
/playlist/574206.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2760000,CODECS="avc1.77.31,mp4a.40.2",RESOLUTION=1024x576
/playlist/574207.m3u8
FRESH!の場合一番画質の低い574204.m3u8 (256×144)で、プレイリスト内のEXTINFタグが2なので、動画再生後最初の2秒間は、かなり映像がぼやけてしまいます。これではせっかく初期表示を早くしても体感的にあまり心地よく感じられません。
そこでstartLevelを2に設定し、最初に選択するプレイリストを574206.m3u8 (768×432) に初期表示の画質を向上させました。
最後に
今回プレイヤーのリプレイスを行うまでは、正直なところ未だにFlashを用いていてレガシーだなと思っていたのですが、いざ検証に入るとFlashの安定さは絶大だと感じました。
結果的にGoogle ChromeのFlashオプトイン方式化は2017年1月ごろから始まり当初思っていた時期よりだいぶずれ込みましたが、Flashを使う使わないに関わらず快適な動画プレイヤーを提供することができてよかったと思っています。