Bilibili動画ダウンローダーの裏側:DASHプロトコル解析、WBI署名、そしてWebAssemblyによるメディア処理の全貌
-
はじめに
動画配信プラットフォームの解析は、Webエンジニアリングの粋を集めた挑戦です。特に Bilibili(哔哩哔哩)は、独自の WBI 署名アルゴリズム、DASH (Dynamic Adaptive Streaming over HTTP) による映像・音声の分離配信など、高度な技術スタックを採用しています。
本記事では、Bilibili 動画ダウンローダー twittervideodownloaderx.com/bilibili_downloader_ja の実装を例に、ブラウザベースでどのように動画ストリームを解析・取得し、最終的に MP4 として再構築するのか、その技術的裏側を徹底解説します。 -
Bilibili のストリーミングアーキテクチャ解析
現代の Bilibili は、従来の FLV 形式から DASH 形式へ完全に移行しています。
2.1 DASH (Dynamic Adaptive Streaming over HTTP) とは
DASH では、動画が「映像トラック」と「音声トラック」に分かれて配信されます。さらに、それぞれのトラックは数秒単位のセグメントに分割されており、ネットワーク帯域に応じて動的にビットレートを切り替えることが可能です。
ダウンローダーを構築する場合、以下のステップが必要になります: -
動画メタデータ(CID, BVID)の取得。
-
映像と音声それぞれの playurl(M4Sファイル)の特定。
-
セパレートされたファイルをバイナリとして取得。
-
クライアントサイドでのマージ(Muxing)。
Bilibili動画ダウンロード -
難関:WBI (Web-based Interface) 署名アルゴリズムの突破
Bilibili の API(特に playurl)を叩く際、最も困難なのが WBI 署名 です。これはリクエストパラメータに w_rid と wts を付与し、サーバー側で改ざんを検知する仕組みです。
3.1 署名の生成プロセス -
img_key と sub_key の取得: /x/web-interface/nav API のレスポンスに含まれる画像 URL から抽出します。
-
キーのミキシング: 特定の固定テーブル(インデックス配列)に基づき、キーを並べ替えます。
-
署名計算: パラメータを辞書順にソートし、MD5 ハッシュを生成します。
3.2 TypeScript による実装例
TypeScript
const mixinKeyTable = [
46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
36, 20, 34, 44, 52
];
function getMixinKey(orig: string): string {
return mixinKeyTable.map(n => orig[n]).join('').slice(0, 32);
}
// 署名生成のコアロジック
export function encWbi(params: Record, imgKey: string, subKey: string) {
const mixinKey = getMixinKey(imgKey + subKey);
const currTime = Math.round(Date.now() / 1000);
const newParams = { ...params, wts: currTime };
// パラメータのソートとフィルタリング
const query = Object.keys(newParams)
.sort()
.map(key => {
const value = newParams[key].toString().replace(/[!'()*]/g, '');
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
})
.join('&');
const w_rid = md5(query + mixinKey);
return query + `&w_rid=${w_rid}`;
}
-
高速ダウンロード:HTTP Range リクエストと並列化
大容量の動画(4Kなど)をダウンロードする場合、単一のスレッドでは速度が低下します。ここでは HTTP Range ヘッダーを利用したマルチスレッド・ダウンロードを実装します。
4.1 チャンク分割アルゴリズム -
サーバーに対して HEAD リクエストを送り、Content-Length を確認。
-
ファイルを 5MB 程度のブロックに分割。
-
Promise.all を使用し、各ブロックを並列取得。
TypeScript
async function downloadChunk(url: string, start: number, end: number) {
const response = await fetch(url, {
headers: { 'Range':bytes=${start}-${end}}
});
return await response.arrayBuffer();
} -
WebAssembly (FFmpeg.wasm) によるブラウザ上での動画合成
取得した映像(M4S)と音声(M4S)は別々のため、そのままでは再生できません。サーバーサイドで合成すると転送コストと CPU 負荷が膨大になるため、Bilibili Downloader では FFmpeg.wasm を活用しています。
5.1 ブラウザ内 Muxing の実装
JavaScript
import { createFFmpeg, fetchFile } from '@ffmpeg/ffmpeg';
const ffmpeg = createFFmpeg({ log: true });
async function muxMedia(videoData: Uint8Array, audioData: Uint8Array) {
await ffmpeg.load();
ffmpeg.FS('writeFile', 'input_video.m4s', videoData);
ffmpeg.FS('writeFile', 'input_audio.m4s', audioData);
// 再エンコードなしでコピー(高速処理)
await ffmpeg.run('-i', 'input_video.m4s', '-i', 'input_audio.m4s', '-c', 'copy', 'output.mp4');
const data = ffmpeg.FS('readFile', 'output.mp4');
return new Blob([data.buffer], { type: 'video/mp4' });
}
注意: SharedArrayBuffer の制限により、適切な COOP/COEP ヘッダーの設定が必要です。
-
フロントエンド:Next.js + Tailwind CSS によるモダン UI
ユーザー体験(UX)を最適化するため、以下の機能を実装しています。
• Web Workers: 署名計算やバイナリ処理をメインスレッドから分離。
• SWR / React Query: API 状態の管理。
• Progress API: リアルタイムなダウンロード進捗表示。 -
法的考察と技術エチケット
技術者として、ツールの開発には責任が伴います。 -
負荷軽減: 指数バックオフ(Exponential Backoff)の実装。
-
規約遵守: 私的利用の範囲内での使用をユーザーに推奨。
-
著作権: 権利者の利益を損なわない設計。
-
まとめ
Bilibili 動画ダウンロードツールの開発は、最新の Web テクノロジーの集大成です。
• WBI 署名 によるセキュリティ解析。
• DASH 形式のストリーム処理。
• WebAssembly によるクライアントサイドでの重いバイナリ操作。
これらの知見は、動画配信サービスだけでなく、様々な Web 解析や最適化に応用可能です。さらなる詳細や実際の動作については、こちら の実装例を確認してください。