暗号化されたHLSストリームをブラウザのHAR経由で保存する

実はWebブラウザの開発ツールには大抵HTTPリクエストとレスポンスをHAR(HTTP Archive)と呼ばれる.jsonファイルとして保存する機能がある。これが超クッソ便利で、色々と応用が効く。

今回はストリーミング動画/音声をHARとして保存し、そこからデータを抽出してみる。

スクリプト( `in.har` から鍵データと `.m3u8` 、`.ts`を抽出する ) : https://github.com/okuoku/striphls/blob/5c592c735e79f3fc604c5b6da4d8cf3cd66cc664/run.js

# やること

今回は個人的なプロジェクトで採用している(いた)Amazon Elastic TranscoderとHLS AES暗号化、HLS.jsプレイヤの組合せを想定している。 **世間の動画配信サイトでは通常もっと真面目な暗号化/転送方式を採用しているのでこの方法は使えない。**

HLS(HTTP Live Streaming)はストリーミングプロトコルの1つで、広範なクライアント/サーバのサポートが既に存在することと、全てがHTTPで完結することが特徴となっている。... そして、Webブラウザには、HAR形式で通信内容をエクスポートする機能が備わっている。

つまり、

1. 開発者ツールを開く(F12)
2. 再生ボタンを押す
3. 再生が終わったら、HTTP通信内容をHARで保存する
4. 保存したHARからストリームを取り出す

ということが行える。全てがHTTPで完結しているということは、再生に必要なものの全てがHTTPのログに載っているということになる。

# HARの保存

HARの保存は開発者ツールの中にある:

![SnapCrab_NoName_2019-9-22_0-38-25_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/67446/2b6276b4-b5c7-260c-1f32-bea9590fa06c.png)

このスクリーンショットではFirefoxを使っているが、ChromeやIE11(!)でも同じようにして保存できる。

Firefoxの場合、 **デフォルトではHAR中のデータは1MiBを超えた分は省略される** 。これを回避するには、`about:config`で `devtools.netmonitor.responseBodyLimit` をゼロにセットする。

# HARのアクセス

HARはただの.jsonになっているため、JSONを扱えるプログラミング言語ならなんでも使える。今回は普通にNode.jsを使った。

```js
const har = JSON.parse(fs.readFileSync(path.resolve(__dirname, "in.har")).toString("utf8"));
```

あとは `forEach` でファイルをBufferなりなんなりに変換しておく:

```js
const entries = har.log.entries; // ★ ファイルの log.entries にリクエストの一覧がある

const urls = {}; // ★ URLをキーにした辞書
const files = []; // ★ 全ファイルを含む配列

entries.forEach(e => {
    let entry = false;
    const url = new URL(e.request.url);
    const name = url.pathname;
    const basename = path.basename(url.pathname);
    if(e.response.content.text){
        if(e.response.content.encoding == "base64"){
            entry = new Buffer.from(e.response.content.text, "base64");
        }else{
            entry = e.response.content.text;
        }
    }
    files.push({name: name, ext: path.extname(name), 
               basename: basename,
               blob: entry});
    urls[url] = entry;
});
```

# `.m3u8` に含まれる鍵URLの処理

HLSでは、メディアファイルを細かいファイルに分割して扱う。その分割したファイルのURLは `.m3u8` という拡張子のファイルに列挙され、さらに、分割ファイルを復号するための鍵ファイルのURLもそこに含まれる。

保存したHARには、`.m3u8`ファイルに指定された鍵URLのアクセスという形で鍵も保存されているはずなので、

1. HARから`.m3u8`ファイルを探す
2. `.m3u8`ファイルに含まれる鍵URLを抽出
3. HARから鍵URLのアクセスを探してファイルに保存
4. 保存したファイルを指すように`.m3u8`ファイルを加工して保存

というステップで鍵を抽出でき、さらに抽出した鍵を使う`.m3u8`を生成できる。

```js
// Extract key, modified .m3u8
files.forEach(e => {
    const reKeyUrl = RegExp('#EXT-X-KEY:METHOD=AES-128,URI="([^"]+)"'); // ★ 鍵URLの検出
    if(e.ext == ".m3u8"){
        if(e.blob instanceof Buffer){
            e.blob = e.blob.toString("utf8");
        }
        const keyUrlArr = reKeyUrl.exec(e.blob);
        if(reKeyUrl.exec(e.blob)){
            const keyUrl = keyUrlArr[1];
            if(urls[keyUrl]){
                console.log(keyUrl);
                console.log(urls[keyUrl]);
                const out = // ★ .m3u8の加工
                    e.blob.replace(reKeyUrl, "#EXT-X-KEY:METHOD=AES-128,URI=zzzKEY");
                fs.writeFileSync("out.m3u8", out); // ★ .m3u8の保存
                fs.writeFileSync("zzzKey", urls[keyUrl]); // ★ 鍵データの保存
            }else{
                console.log("Not found, skip:", keyUrl);
            }
        }
    }
});
```

Amazon Elastic Transcoderが生成する`.m3u8`は鍵のURLを `#EXT-X-KEY:METHOD=AES-128,URI=` から始める。というわけで、これを探して、ローカルに保存する鍵データを指すように書き換える。(設定にも依るが鍵は通常自動的に生成されるため、鍵データもHARから入手する方が簡単)

# `.ts` の保存

特記事項なし。

```js
// Extract *.ts
files.forEach(e => {
    if(e.ext == ".ts"){
        fs.writeFileSync(e.basename, e.blob);
    }
});
```

メディアファイルは複数の `.ts` に分割されるため、この拡張子を持つファイルを探して保存すれば良い。

# ファイルの結合

...ここでHLS.jsを使いたいんだけど上手くいってないのでとりあえず `ffmpeg` でお茶を濁す。

```
ffmpeg.exe -allowed_extensions ALL -i out.m3u8 -c copy out.ts
```

出力された `.ts` はVLCなりなんなりで正常に再生できる(あたり前だ)

# ToDo、かんそう

そもそもご予算の都合で動画機能自体撤廃したのでToDoもクソも無いんだけど、この方式で保存できないサイトの実装方式が結構興味深い。ログが残ってしまうHTTPを避け、カスタムURIスキームを用意してクライアント側のJavaScriptで出力を加工する方式( https://medium.com/@onetdev/custom-key-acquisition-for-encrypted-hls-in-videojs-59e495f78e52 )が有る。

今年のWWDCでlow-latency拡張が提案される( https://developer.apple.com/documentation/http_live_streaming/protocol_extension_for_low-latency_hls_preliminary_specification )等、HLS自体も拡張は続いているが、プロトコル自体のJavaScript実装を可能にするMediaSourceExtensionはiPadOS止まりと、絶妙な状況が続いているように見える。

まぁPlayReadyの開発元なんだし当然という気もするけど、AWSよりAzureの方がサービスやドキュメントが充実している。"真面目なDRM"としてAzureはメジャーなDRM全て(Widevine、PlayReady、FairPlay)をサポートしていて、それらを同時にサポートするためのドキュメント https://docs.microsoft.com/ja-jp/azure/media-services/latest/design-multi-drm-system-with-access-control も提供している。

HARは興味深いフォーマットだが、例えばブラウザの描画タイミングとか、スクリプトから打ったパフォーマンスマーカーといった情報は残せないため、現代的にはちょっと不満がある。もちろん今回のようなスクレイピング / デバッグ用途には便利に使えるが。。バグレポートにHARを求めたりするケースもそれなりにあるので、もうちょっと便利に取得できるように何かブラウザ拡張とか作っておいた方が良いのかもしれない。

## Fiddler Classic でやる

せっかくなので(今さら)実サイトでも試してみた。あんまり真面目に試行していないので何とも言えないが、 **実際のサイトではDeveloper toolを開いているとストリーミングを止める** ようだ。かしこい。

こういう場合はFiddlerのHAR 1.2エクスポートを使うのが良い。FiddlerのHARエクスポートもFirefox同様容量制限があるので、 `fiddler.importexport.HTTPArchiveJSON.MaxBinaryBodyLength` を View → Tabs → Preferences で適当な値に設定してやる必要があった。

FiddlerのHAR保存にはBOMが付いてしまうので手で削る必要がある。また、chunked encodingで送ってくるファイルは手動でデコード指定する必要があった。