Help us understand the problem. What is going on with this article?

暗号化された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

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

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

HARのアクセス

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

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

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

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を生成できる。

// 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 の保存

特記事項なし。

// 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を求めたりするケースもそれなりにあるので、もうちょっと便利に取得できるように何かブラウザ拡張とか作っておいた方が良いのかもしれない。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away