0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Bilibili動画のダウンロード技術を解剖する:DASHストリーム解析から実装の勘所まで

0
Posted at

Bilibili動画のダウンロード技術を解剖する:DASHストリーム解析から実装の勘所まで

※本記事は技術的な学習・研究を目的としており、著作権で保護されたコンテンツの無断利用を推奨するものではありません。利用の際は必ず各プラットフォームの利用規約および著作権法を遵守してください。

はじめに:「なぜダウンロードできないのか」から始まった技術探求

こんにちは、普段はバックエンド開発をメインにしているエンジニアです。

先日、Bilibiliで素晴らしい技術解説シリーズを見つけたんですが、「通勤中にオフラインで見たい」「資料としてローカルに保存したい」と思った瞬間、あの「公式ダウンロード機能が制限されている」という現実に直面しました。

「APIを叩けば取れるでしょ?」と思った方もいるかもしれません。実際、数年前までは比較的簡単に動画データにアクセスできました。しかし、2024年現在、Bilibiliの配信アーキテクチャはDASH(Dynamic Adaptive Streaming over HTTP)を基盤とした複雑な構造へと進化しており、単純なURL直叩きでは対応できなくなっています。

かといって、サードパーティ製ツールにアカウント情報を渡すのも抵抗がある…。

そんな「技術的には可能なのに、手軽に安全に使える手段がない」というギャップを埋めたくて、今回ご紹介する bilibili_downloader_ja の技術アーキテクチャを、開発者の視点から分解してみようと思います。

本記事では、単なる「使い方の紹介」ではなく、「BilibiliのDASHストリームをどう解析し、どう再構築しているか」 という技術的な中身に焦点を当てます。コード例も交えつつ、実装の勘所を共有できれば幸いです。


Bilibili動画配信の技術的構造:DASHとm4sの正体

1. DASHストリームとは何か

Bilibiliでは、高画質・高フレームレートの動画を提供するため、動画と音声を別々のストリームとして配信するDASH方式を採用しています。

ブラウザで動画を再生する際、実際には以下のようなフローで処理が行われています:

1. 動画ページ読み込み
2. playurl API を叩いてメタデータ取得
3. video.m4s(映像)と audio.m4s(音声)を並列でダウンロード
4. ブラウザのMedia Source Extensions (MSE) で同期再生

つまり、私たちが「1つの動画ファイル」として認識しているものも、技術的には複数の断片として配信されているのです。

2. Networkタブで覗いてみる

開発者ツール(F12)→ Networkタブ → 「media」または「xhr」でフィルターをかけ、Bilibiliの動画を再生してみてください。以下のようなリクエストが確認できるはずです:

https://api.bilibili.com/x/player/playurl?cid=123456789&bvid=BV1xx411c7mD&qn=80&type=&otype=json&fourk=1&fnver=0&fnval=4048

このplayurlエンドポイントが、動画・音声ストリームの実際の取得URLを返す重要なAPIです。返されるJSONレスポンスには、以下のような情報が含まれています:

{
  "code": 0,
  "data": {
    "dash": {
      "video": [
        {
          "id": 80,
          "baseUrl": "https://xxx.bilivideo.com/xxx/video.m4s",
          "bandwidth": 1200000,
          "mimeType": "video/mp4",
          "width": 1920,
          "height": 1080
        }
      ],
      "audio": [
        {
          "id": 30280,
          "baseUrl": "https://xxx.bilivideo.com/xxx/audio.m4s",
          "bandwidth": 132000,
          "mimeType": "audio/mp4"
        }
      ]
    }
  }
}

💡 補足:fnval=4048 のようなパラメータは、対応フォーマット(DASH, HDR, 4K等)を指定するフラグです。値の意味は非公開ですが、公式アプリのリクエストをリバースエンジニアリングすることで推測可能です。
2low.png


簡易実装:Pythonでメタデータ取得からダウンロードまで

以下は、学習目的のサンプルコードです。実際の運用では、Cookie管理、署名検証、レート制限など、より多くの配慮が必要になります。

import requests
import json

def get_playurl(bvid: str, cid: str) -> dict:
    """Bilibili playurl API から動画メタデータを取得"""
    url = "https://api.bilibili.com/x/player/playurl"
    params = {
        "bvid": bvid,
        "cid": cid,
        "qn": 80,          # 画質指定(80=1080p)
        "fnval": 4048,    # DASH形式を要求
        "otype": "json"
    }
    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
        "Referer": "https://www.bilibili.com"
    }
    
    resp = requests.get(url, params=params, headers=headers)
    return resp.json()

def extract_best_stream(dash_data: dict) -> tuple[str, str]:
    """video/audio の最適なストリームURLを抽出"""
    video = max(dash_data["dash"]["video"], key=lambda x: x["bandwidth"])
    audio = max(dash_data["dash"]["audio"], key=lambda x: x["bandwidth"])
    return video["baseUrl"], audio["baseUrl"]

# 使用例(cidは動画ページのソースまたはAPIから取得需要)
bvid = "BV1xx411c7mD"
cid = "123456789"  # 実際にはページ解析等で取得

data = get_playurl(bvid, cid)
if data["code"] == 0:
    v_url, a_url = extract_best_stream(data["data"])
    print(f"Video: {v_url}")
    print(f"Audio: {a_url}")

⚠️ 注意点:Bilibiliは頻繁にAPI仕様や署名アルゴリズムを変更します。このコードは「その時点で動作する」保証はありません。本番環境では、wbi_sign(Web Interface Signature)の生成ロジックなど、より高度な対応が必要になります。


当サービスのアーキテクチャ:なぜ「サーバーに保存しない」設計なのか

bilibili_downloader_ja の最大の特徴は、「ユーザーのダウンロードファイルを一切サーバーにキャッシュしない」 という設計思想にあります。これには明確な技術的・法的な理由があります。

1. プライバシーとセキュリティの観点

  • ユーザーがダウンロードした動画のURL、IPアドレス、利用履歴などを記録しない
  • サーバー側にファイルを一時保存しないため、情報漏洩リスクが原理的に排除される
  • すべてクライアントサイドで完結するフロー(またはプロキシ経由のストリーミング転送)を採用

2. 著作権対応の設計

当サービスは「ダウンロード支援ツール」であり、「コンテンツの再配布プラットフォーム」ではありません。技術的には、以下のようなフローで実現しています:

[ユーザーブラウザ] 
    ↓ (1) 動画URLを送信
[当サーバー] 
    ↓ (2) Bilibili API からメタデータを取得(キャッシュなし)
    ↓ (3) 映像・音声ストリームの直接ダウンロードリンクを生成
[ユーザーブラウザ] 
    ↓ (4) Bilibili CDN からファイルを直接取得 & ffmpeg.wasm で結合

この「中継のみで保存しない」アーキテクチャにより、当サーバーには著作権対象ファイルが一切存在しない 状態を維持できています。これは法的リスクの低減だけでなく、ストレージコストの削減、スケーラビリティの向上にも寄与しています。

3. m4s結合処理の実装方針

DASHストリームの課題は、「映像と音声が別ファイル」という点です。これをユーザーに透過的に処理するため、当サービスでは以下の2つのアプローチを用意しています:

  1. クライアントサイド結合(ffmpeg.wasm)
    WebAssembly版ffmpegをブラウザで実行し、ダウンロード後に自動結合。サーバー負荷ゼロですが、端末性能に依存します。

  2. サーバーサイド・パイプライン結合(限定利用)
    高画質・長時間動画向けに、ストリーミング転送中にffmpegでマージ。ただし、一時ファイルはメモリ上で処理し、完了後即時破棄します。

// ffmpeg.wasm による簡易結合のイメージ
const { createFFmpeg, fetchFile } = require('@ffmpeg/ffmpeg');
const ffmpeg = createFFmpeg({ log: true });

async function mergeStreams(videoBlob, audioBlob, outputName) {
  await ffmpeg.load();
  ffmpeg.FS('writeFile', 'video.m4s', await fetchFile(videoBlob));
  ffmpeg.FS('writeFile', 'audio.m4s', await fetchFile(audioBlob));
  
  await ffmpeg.run('-i', 'video.m4s', '-i', 'audio.m4s', '-c', 'copy', outputName);
  
  const data = ffmpeg.FS('readFile', outputName);
  return new Blob([data.buffer], { type: 'video/mp4' });
}

実際の運用では、メモリ制限、処理タイムアウト、進捗通知など、プロダクションレベルの配慮が必要です。


実際の使い方:3ステップで完了するシンプル設計

技術的な中身は複雑でも、ユーザー体験はシンプルであるべき。当サービスのUIは以下の3ステップのみで構成されています。

  1. Bilibili動画のURLをコピー
    例:https://www.bilibili.com/video/BV1xx411c7mD

  2. 入力欄に貼り付けて「解析を開始」をクリック
    約3〜8秒でメタデータ解析完了。利用可能な画質・形式オプションが表示されます。

  3. 希望の形式(MP4 / MP3 / GIF)を選択して保存
    ブラウザの標準ダウンロード機能を使用するため、追加ソフト不要。

スマホでも同じ操作性

レスポンシブデザインを採用しているため、iPhone / Android のブラウザからも全く同じ手順で利用可能です。iOSの場合、ダウンロード後に「ファイル」アプリ経由での保存が必要になることがありますが、これはOSのセキュリティポリシーによるものです。


技術的なメリット:なぜこのツールが「軽量で高速」なのか

✅ 1. ステートレスなAPI設計

各リクエストが独立して処理されるため、セッション管理やデータベース接続が不要。これにより、サーバーリソースの効率的な利用と、突発的なトラフィック増加への耐性を実現しています。

✅ 2. CDNフレンドリーな配信

動画ファイルはBilibiliのCDN(*.bilivideo.com)から直接取得するため、当サーバーの帯域幅を圧迫しません。変換処理が必要な場合も、中間キャッシュを最小限に抑える設計としています。

✅ 3. クライアントサイドでの進捗表示

解析・結合中のステータスは、WebSocketまたはServer-Sent Events(SSE)でリアルタイムにクライアントへ通知。ユーザーが「待たされている感」を感じないよう、技術的に配慮しています。

// SSEによる進捗通知の簡易例
const eventSource = new EventSource('/api/progress?job_id=xxx');
eventSource.onmessage = (e) => {
  const data = JSON.parse(e.data);
  // data: { step: "downloading", percent: 45, message: "映像ストリーム取得中..." }
  updateUI(data);
};

開発者としての想い:オープンな技術を、より多くの人に

私自身、長年メディア処理やAPI連携の開発に携わってきましたが、「技術的にできること」と「ユーザーが実際に使えるもの」の間には、常に大きなギャップがあると感じています。

このツールを作った理由はシンプルです:

「面白いコンテンツを、好きな時に、好きなデバイスで楽しみたい」

という当たり前の欲求を、技術でサポートしたかったからです。

もちろん、Bilibiliのプラットフォームポリシーや著作権法とのバランスは常に意識しています。本サービスは:

  • 公開動画のメタデータのみを処理
  • 会員限定・地域制限コンテンツには対応しない
  • 商用利用・再配布を目的とした利用を規約で禁止

といった制限を設けています。技術は中立ですが、その使い方は責任を持って設計すべきだと考えています。


Qiita読者への技術的プレゼント:自分で作ってみるためのヒント

もし「自分でも似たツールを作ってみたい」と思われた方に向けて、学習リソースをいくつかご紹介します。

🔍 参考になる技術スタック

用途 推奨技術
APIリクエスト requests (Python), axios (Node.js)
HTML/JSON解析 BeautifulSoup, lxml, cheerio
動画結合処理 ffmpeg, ffmpeg.wasm, fluent-ffmpeg
署名生成(wbi) 自前実装またはオープンソースライブラリ参照
フロントエンド Next.js + Tailwind, Streamlit(プロトタイプ用)
非同期ジョブ asyncio + aiohttp, BullMQ + Redis

📚 学習に役立つリソース

💡 実装時のアドバイス

  1. User-AgentとRefererの適切な設定:Bilibiliはボット対策が厳しめ。ブラウザと見なされるヘッダーが必須。
  2. wbi署名の扱い:2023年以降、多くのAPIエンドポイントでwbi(Web Interface Signature)によるリクエスト検証が導入されています。公開されているアルゴリズムを参考に、適切に実装しましょう。
  3. レート制限の尊重:連続リクエストは避け、time.sleep() などで間隔を空ける。412エラー(Precondition Failed)が出たら要注意。
  4. エラーハンドリングの充実:ネットワークエラー、404、地域制限、会員限定など、想定される失敗ケースを網羅し、ユーザーに分かりやすいメッセージを表示。

おわりに:技術は、誰かの「ちょっと嬉しい」を創れる

最後になりましたが、bilibili_downloader_ja は、個人開発者が「自分の不便を自分で解決した」ことから始まったプロジェクトです。

Qiitaのコミュニティでは、技術的な深掘りと実用性のバランスが取れた記事が好まれる傾向があります。本記事が、単なる「ツール紹介」ではなく、「技術的な仕組みへの理解」を深めるきっかけになれば幸いです。

もし実際に使ってみて、「ここが分かりにくい」「こういう機能が欲しい」といったフィードバックがありましたら、ぜひサイトの問い合わせフォームからご連絡ください。オープンな技術開発は、ユーザーとの対話から進化していくものと信じています。


📌 免責事項
本サービスは、哔哩哔哩(Bilibili)とは一切関係ありません。
ダウンロードしたコンテンツの利用は、必ず著作権法および各プラットフォームの利用規約に従ってください。
個人での視聴・学習目的以外の利用(再配布、商用利用、改変など)はご遠慮ください。


タグ: #Bilibili #動画ダウンロード #DASH #Webスクレイピング #Python #JavaScript #ffmpeg #API解析 #Qiita #技術記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?