Media Source Extensions API

1. Overview

Media Source Extensions API (以下, MSE) は, W3C によって標準化されている HTTP ダウンロードを利用してストリーミング再生するために作られた JavaScript API です. この記事では, MSE を利用して, 簡単な MPEG-DASH コンテンツを再生するための実装を紹介します.

2. Create resources

再生するコンテンツのリソースを生成します. 生成には, ffmpeg と MP4Box が必要になるので, brew などでインストールしておきます.

$ brew install ffmpeg
$ brew install MP4Box

次に, 対象となる MP4 動画を ffmpeg でエンコードします.

ffmpeg -i ./input.mp4 \
  -vcodec libx264 \
  -vb 500k \
  -r 30 \
  -x264opts no-scenecut \
  -g 15 \
  -acodec aac \
  -ac 2 \
  -ab 128k \
  -frag_duration 5000000 \
  -movflags frag_keyframe+empty_moov \
  ./encoded.mp4

そして, MP4Boxを利用して, 動画を分割し, セグメントファイルとプレイリストを生成します.

MP4Box -frag 4000 \            
  -dash 4000 \
  -rap \
  -segment-name sample \
  -out ./output.mp4 \
  ./encoded.mp4

以上で, プレイリストである output.mpd と output.m4s と連番になったセグメントファイルが生成されます. MPD (Media Presentation Description) ファイルの実体は, セグメントファイルをどの順番で再生するかが記述された XML ファイルです.

3. Media Source Extensions API

リソースの準備ができたので, MSE を利用した JavaScript の実装を紹介します.

'use strict';

class MSE {
    constructor(video, file) {
        this.getDescription(video, file);
        this.segmentIndex = 0;
        this.mediaAppended = false;
    }

    // MPD ファイルを取得し, MIME タイプやコーデックの情報を取得する
    getDescription(video, file) {
        const xhr = new XMLHttpRequest();

        xhr.open('GET', file, true);
        xhr.responseType = 'document';

        xhr.onload = () => {
            this.mpd = xhr.response;

            const representation = this.mpd.querySelector('Representation');
            const mimeType       = representation.getAttribute('mimeType');
            const codecs         = representation.getAttribute('codecs');

            this.type = `${mimeType}; codecs="${codecs}"`;

            this.initVideo(video);
        };

        xhr.send(null);
    }

    // セグメントを追加できるようにする
    initVideo(video) {
        this.mediaSource = new MediaSource();
        this.mediaSource.addEventListener('sourceopen', this.initSourceBuffer.bind(this), false);

        video.src = window.URL.createObjectURL(this.mediaSource);
    }

    // 初期化情報が入ったセグメント (初期化セグメント) とメディア本体のセグメントを追加できるようにする
    initSourceBuffer() {
        this.sourceBuffer = this.mediaSource.addSourceBuffer(this.type);
        this.sourceBuffer.addEventListener('updateend', this.appendMediaSegment.bind(this), false);
        this.appendInitSegment();
    }

    // セグメントファイルのロードが完了したタイミングで, SourceBuffer に追加する
    appendSegment(event) {
        this.sourceBuffer.appendBuffer(event.currentTarget.response);

        if (this.mediaAppended) {
            this.sourceBuffer.removeEventListener('updateend', this.appendMediaSegment.bind(this), false);
        }
    }

    // 初期化セグメントが SourceBuffer に追加されて更新されたら, メディア本体のセグメントファイル (メディアセグメント) を取得して, SourceBuffer に追加する. この処理を, メディアセグメントの数だけ繰り返す.
    appendMediaSegment() {
        const xhr = new XMLHttpRequest();

        const segmentURL = this.mpd.querySelectorAll('SegmentURL')[this.segmentIndex++];

        if (!segmentURL) {
            this.mediaAppended = true;
            return;
        }

        xhr.open('GET', segmentURL.getAttribute('media'), true);
        xhr.responseType = 'arraybuffer';
        xhr.onload = this.appendSegment.bind(this);
        xhr.send(null);
    }

    // MDP から セグメントファイルの URL を取得する
    appendInitSegment() {
        const xhr = new XMLHttpRequest();

        xhr.open('GET', this.mpd.querySelector('Initialization').getAttribute('sourceURL'), true);
        xhr.responseType = 'arraybuffer';
        xhr.onload = this.appendSegment.bind(this);
        xhr.send(null);
    }
}

new MSE(document.querySelector('video'), 'output.mpd');

これで, MPEG-DASH コンテンツを再生するための最小限の実装ができました.

Media Source Extensions API リポジトリ (Safari のみ)

参考 : フロントエンドエンジニアのための動画ストリーミング技術基礎