hls.jsの流れを追ってみる
おさらい
前回の記事では、hls.jsをこのように利用していました。
var hls = new Hls({
"debug": true
});
hls.loadSource("playlist.m3u8");
hls.attachMedia(video);
それではプレイリストファイルが読み込まれて動画が再生されるまでの流れを追ってみましょう。
といっても1行ずつ追っていくのは本質的ではないので、映像データが読み込まれていく流れをポイントごとに解説していきます。
基本的には、前回ffmpegを使ってmp4ファイルからm3u8ファイルを生成した流れの逆を辿っていくことになります。
プレイリストファイルのパース
プレイリストファイルがloadSourceされると、MANIFEST_LOADINGイベントが発火されます。hls.js内ではイベント駆動で処理が行われていくので、それを念頭に置くと流れが追いやすいです。
playlist-loaderのonManifestLoadingによってマスタープレイリストがパースされることで、levelsという名前でABRの対象となるストリームの一覧と、各ストリームの情報(ビットレートやコーデック情報)が得られます。
この時にチャンクに関する情報が記載されたプレイリスト(levelPlaylist)のurlも得られるので、マスタープレイリストとほぼ同様の手順でパースされていき、チャンクファイルのURLが得られます。
private load(context: PlaylistLoaderContext): void {
(中略)
// マスタープレイリストか、チャンクに関する情報が記載されたプレイリストかを判定
if (
M3U8Parser.isMediaPlaylist(string) ||
context.type !== PlaylistContextType.MANIFEST
) {
this.handleTrackOrLevelPlaylist(
response,
stats,
context,
networkDetails || null,
loader,
);
} else {
this.handleMasterPlaylist(response, stats, context, networkDetails);
}
(中略)
private handleMasterPlaylist(
response: LoaderResponse,
stats: LoaderStats,
context: PlaylistLoaderContext,
networkDetails: any,
): void {
const hls = this.hls;
const string = response.data as string;
const url = getResponseUrl(response, context);
const parsedResult = M3U8Parser.parseMasterPlaylist(string, url);
(中略)
// マスタープレイリストをパースした結果が得られる
const {
contentSteering,
levels,
sessionData,
sessionKeys,
startTimeOffset,
variableList,
} = parsedResult;
トランスマックス
チャンクファイルがロードされると、トランスマックスという処理が走ります。コンテナ形式を移し替える処理のことで、今回はMPEG-TS形式のチャンクをfMP4形式にしています。その結果、MSEのSourceBufferで受け付けられるようになります。
protected _handleFragmentLoadProgress(data: FragLoadedData) {
(中略)
const transmuxer = (this.transmuxer =
this.transmuxer ||
new TransmuxerInterface(
this.hls,
PlaylistLevelType.MAIN,
this._handleTransmuxComplete.bind(this),
this._handleTransmuxerFlush.bind(this),
));
(中略)
transmuxer.push(
payload,
initSegmentData,
audioCodec,
videoCodec,
frag,
part,
details.totalduration,
accurateTimeOffset,
chunkMeta,
initPTS,
);
}
そして出来たfmp4(arrayBuffer型)をMSEのSourcebufferにappendBufferすることで、再生に必要なチャンクのデータが集まりきります。これによって、MediaSourceオブジェクトとして動画再生の準備が整います。
private appendExecutor(data: Uint8Array, type: SourceBufferName) {
(中略)
// sb は音声や映像などのtrackごとのSourceBufferとして定義されています
sb.appendBuffer(data);
}
MediaSourceとvideo要素の紐付け
HTML上で動画を再生させるために、チャンクが格納されたSourceBufferとvideo要素が紐づいている必要があります。
まずは、受け取ったvideo要素からmediaSourceを取得します。そのmediaSourceにaddSourceBufferすることで、紐づいたSourceBufferが得られます。
先ほどappendBufferしてチャンクを格納したのは、ここで得られたSourceBufferに対してだったというわけですね。
private onMediaAttaching(
event: Events.MEDIA_ATTACHING,
data: MediaAttachingData,
) {
(中略)
// hls.attachMedia(video); で渡したmedia要素からmediaSourceを取得
const ms = (this.mediaSource = data.mediaSource || new MediaSource());
(中略)
private createSourceBuffers() {
(中略)
const sb = mediaSource.addSourceBuffer(
mimeType,
) as ExtendedSourceBuffer;
(中略)
// 先述した音声や映像などのtrackごとのSourceBufferとして持っておく
track.buffer = sb;
これで、hls.jsを介してMSEを用いた動画再生が始まります。
