mcpField
T
ttdl

ttdl

github.com/monotykamaryクリエイティブ・メディア462 回インストール2 日前に更新
リポジトリ

ttdl について

ttdl はブラウザーでの再生を記録する TikTok 向けコマンドラインツールです。動画や音声を保存したいユーザー向けで、再生ストリームを取得し、ffmpeg で再エンコードせずに結合・トリミングします。

主な機能

  1. 01TikTok 動画のダウンロード
  2. 02音声のみを M4A で保存
  3. 03タイトル、作者、長さ、解像度の表示
  4. 04短縮リンクと数値の動画 ID に対応
  5. 05再エンコードせずに結合・トリミング

活用シーン

  1. 01TikTok で再生される動画の保存
  2. 02TikTok 動画から音声を抽出
  3. 03ダウンロード前の動画情報確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/monotykamary/browser-harness-js --skill ttdl

インストール後、Claude Code で/ttdlを入力して読み込まれたか確認します。

同梱ファイルscripts/setupscripts/ttdl

詳細

ttdl — ブラウザーで動作する TikTok ダウンローダー

gsearch/xsearch/ytdl とまったく同じ、browser-harness-js の薄い heredoc です。 Bun プログラムも、同梱の署名処理も、HTTP クライアントの偽装もありません。 動画を再生するために TikTok が行う複雑な処理――署名付き CDN URL、アダプティブな画質の引き上げ、編集リスト――は、再生時にページがすでに処理しています。ttdl はその結果を記録するだけです。キャプチャするのは、クリーンでウォーターマークのない再生ストリームです。TikTok アプリ内の「ダウンロード」ボタンが提供するのは、別途レンダリングされたウォーターマーク付きファイルですが、MediaSource を記録すれば、プレーヤーが視聴者に表示するものをそのまま取得できます。

ttdl "https://www.tiktok.com/@user/video/7642721752497310989"   # best → ~/Downloads
ttdl "https://www.tiktok.com/@user/video/7642721752497310989" -q audio   # audio only (.m4a)
ttdl "https://www.tiktok.com/@user/video/7642721752497310989" --info      # title / author / duration / resolution
ttdl "https://vm.tiktok.com/ZMxxxxx/"                            # short link (browser resolves the redirect)
ttdl 7642721752497310989                                         # bare numeric ID
ttdl "https://www.tiktok.com/@user/video/..." -o Name -d ~/Videos

画質の目標

-q動作ffmpeg が必要か
bestプレーヤーに任せます(提供される最高画質まで自動的に段階的に引き上げます)。キャプチャ、結合、トリミング、高速再生用の最適化を行いますはい
audio音声バッファーだけを保持します → .m4aはい

意図的に -q 720p/1080p は用意していません。YouTube とは異なり、TikTok のウェブプレーヤーには明確な画質 API がなく、接続状況に応じて画質を自動選択し、段階的に引き上げます(再生中に新しい高画質の MediaSource を作成します。「仕組み」を参照)。ttdl は各種類のうち最大のものをキャプチャするため、常にプレーヤーが到達した最高画質を取得できます。特定の解像度を強制することは、確実にはできません。

ここでの ffmpeg は結合とトリミングに使い、再エンコードには使いません(-c copy)。

  • 結合: TikTok は音声と動画を別々の SourceBuffer として配信します(例:HEVC hvc1.* 動画と AAC mp4a.* 音声)。そのため、1 つのファイルにまとめる必要があります。
  • トリミング: 生の MSE メディアは、プレーヤーで再生される長さよりも長くなります(「再生時間に合わせてトリミングする理由」を参照)。
  • 高速再生用の最適化: キャプチャしたストリームはフラグメント化 MP4 です。-movflags +faststart は moov を先頭に並べ替え、どこでもシークやストリーミングをできるようにします。

出力は、TikTok 独自のコーデックを使った純粋な(ロスレスの)形式、すなわち HEVC 動画と AAC 音声のままです。この組み合わせは QuickTime/iOS と相性がよく(iOS は HEVC+AAC をネイティブにデコードします)、ytdl の AV1/Opus 出力とは異なり、Apple デバイス向けに再エンコードする必要はありません。

仕組み

すべてを browser-harness-js <<EOF のヒアドキュメント内に記述します。ytdl 方式です。

  1. ブラウザーの共有 CDP セッションに接続します(または session.connect() を使用します)。
  2. Page.addScriptToEvaluateOnNewDocument — ページ上の JS が実行される 前に MSE フックを注入します。
    • MediaSource.prototype.addSourceBuffer をラップします(Object.defineProperty を使用)。各 SourceBuffer に コーデックと派生した 種別をタグ付けします。
    • 各 SourceBuffer に、元のメソッドを呼び出す前にバイト列(デマックス済みの ISO-BMFF フラグメント)を記録する OWN appendBuffer プロパティを定義します。独自プロパティが必要です。SourceBuffer.prototype.appendBuffer は書き込み不可のネイティブメソッドなので、プロトタイプへの再代入は何もせずに終了します。
    • 種別はコンテナの MIME ではなく、codecs= の fourcc から判定します(Traps を参照してください。ここが ytdl との最大の TikTok 固有の違いです)。
  3. Target.createTarget でフォアグラウンドを設定 → Page.navigate で視聴 URL に移動 → networkIdle を待ち、<video> をポーリングします。確認/CAPTCHA のインタースティシャルが表示されたら中止します。フォアグラウンドが必要です。バックグラウンドタブでは自動再生が不安定で、プレーヤーが再生を開始しないと MediaSource にデータが供給されないためです。
  4. 作成者と動画 ID を最終的な(リダイレクト後の)location.href から読み取ります。短縮リンク(vm.tiktok.com/…、tiktok.com/t/…)は、この時点でリダイレクト済みです。
  5. currentTime = 0 からミュート再生します。ytdl は、YouTube の SABR が再生しながら取得し、再生速度がキャプチャ範囲を制限するため 16× にしますが、TikTok のキャプチャは append 駆動です。プレーヤーは fMP4 全体を先に取得して追加するため、currentTime が少しも進む前に buffered.end が全再生時間に達します。そのため、キャプチャに対して再生速度は 見かけだけです。また、タブがフォアグラウンドにある間、TikTok のプレーヤーは playbackRate を 1× に戻すため、変更を試みても意味がありません。ttdl は playbackRate を設定しません。カバレッジを確認するたびに muted を再設定します。画質の切り替え/再初期化時にプレーヤーがミュートを解除し、キャプチャ中に突然音声が再生されることがあるためです。
  6. 視聴部分がバッファーに格納され、さらに append の動作が落ち着くまで待ちます(buffered.end >= duration-0.5 かつ キャプチャしたバイト数の合計が 2 ティック連続で変化しない状態)。その後、キャプチャの停止と一時停止をアトミックに行います。__capDone フラグによって、カバレッジが完了した瞬間にフックは append を通過させ(記録は停止し)、TikTok の自動ループ(MSE にデータを再供給します)がキャプチャ済みバッファーを汚染しないようにします。
  7. 動画バッファーと音声バッファーをそれぞれ最大のものから選びます。 プレーヤーは一般的に 2 つの MediaSource インスタンスを作成します。最初に低画質のものを作成し、その後、品質が上がると高画質のものを作成するため、バッファーは 4 つ(動画 2 つ、音声 2 つ)になります。それぞれ最大のものが実際のキャプチャで、小さいものは破棄された低画質側のペアです。
  8. それぞれを 256 KB 単位でディスクに書き出します(バイト数を 3 で割り切れる値にすることで、各 base64 スライスを個別にデコードできます)。ページ側の __pullBuffer(i,offset,len) は base64 を返し、REPL は Buffer.from(b64,'base64') でデコードして fs.appendFileSync で追記します。4 MB ではなく 256 KB にしてください。下記の Traps を参照してください。4 MB のスライスを base64 化したデータを含む returnByValue フレームを 1 つ返すと、最初の呼び出しで Dia のデバッグ WebSocket が切断されます。
  9. ffmpeg -i video -i audio -t <duration> -c copy -movflags +faststart out.mp4(ヒアドキュメントの終了後、bash で一時ファイルのパスを渡します)。
  10. try/finally で closeTab を呼び出します。結果を待たずに実行し、ytdl と同じ方法でクリーンアップします。

CDN の URL ではなく MediaSource でキャプチャする理由

TikTok は各動画を、ページで生成されセッションに紐づいた署名付きCDN URL (v16-webapp-prime.tiktok.com/video/tos/…?a=…&bti=…&ft=…&x-expires=…&x-signature=…) から配信します。署名は URL に含まれているため、ページが有効な間なら単純な fetch(url) でも動作する可能性はあります。しかし、URL の有効期間は短く、レスポンスの範囲が制限されることがあり、URL を見つけるには TikTok のプレーヤーをリバースエンジニアリングする必要があります。SourceBuffer.appendBuffer でキャプチャすれば、こうした問題をすべて回避できます。プレーヤーが何を取得し、どのように署名したかにかかわらず、多重化解除されたバイト列は MSE を通ってクリアな状態で流れるからです。これは認証方式、署名方式、画質を問いません。ページがすべて処理し、その出力を記録します。これは文字どおり「視聴して、記録する」という、ytdl と同じモデルです。

再生時間に合わせてトリミングする理由

TikTok が配信する fMP4 は、プレーヤーが再生する長さより長くなっています。<video>.duration(および seekable/buffered の各範囲)が示すのは、たとえば 27.8 秒といった編集リストに基づく再生時間ですが、MSE の生メディアは約 30.4 秒あります。プレーヤーは 0 秒から 27.8 秒まで再生した後、0 秒に戻ってループ再生します。末尾の約 2.6 秒は実際の映像ですが、プレーヤーがフィードから意図的に除外しています。MSE のバイト列をそのまますべてキャプチャすると(ytdl のモデル)、ユーザーが視聴した映像より約 10%長くなり、ユーザーが見ていない末尾部分も含まれてしまいます。

そのため、ttdl は多重化した出力を <video>.duration に合わせてトリミングします(ffmpeg -t <dur> -c copy)。出力はユーザーが実際に視聴した内容と一致します。カバレッジラッチの静止状態ガード(ステップ 6)により、末尾をすべて取得できたかどうかにかかわらず、この処理を安全に行えます。buffered.end >= dur-0.5 であれば、末尾がどの程度届いたかにかかわらず、視聴部分がすべて追加されたことが保証されます。トリミングによって、その後ろにある末尾部分は破棄されます。

ファイル

すべてのパスは <skill-dir> からの相対パスです。

  • scripts/ttdl — bash CLI(browser-harness-js のヒアドキュメント。#!bun はありません)
  • scripts/setup — ttdl と browser-harness-js を PATH にシンボリックリンクします
  • (lib/ はありません — ソルバー/クライアントの足場コードはありません)

注意点

  • codecs= のコーデックに基づいて SourceBuffer を分類し、コンテナの MIME タイプでは分類しないでください。 TikTok はコンテナが video/mp4 の SourceBuffer に音声を格納します。音声であることが分かるのは、codecs="mp4a.40.29" パラメーターだけです。単純な mime.startsWith("video") ? "video" : "audio"(ytdl のパターン)では TikTok の両方のバッファーが video と判定され、最大のもの(実際の動画)を選んで、音声バッファーを「小さい動画」として破棄するため、キャプチャーは無音になります。ttdl は fourcc のプレフィックス(mp4a→音声、hvc1/avc1/av01→動画)を解析し、codecs= がない場合にのみコンテナの MIME タイプにフォールバックします。この処理は維持してください。TikTok のキャプチャーを機能させる要となる一行です。
  • MSE フックは、読み込み後の Runtime.evaluate ではなく、Page.addScriptToEvaluateOnNewDocument を使って挿入する必要があります。 プレーヤーは起動中に MediaSource/addSourceBuffer の参照を取得します。ページの JavaScript が実行される前に挿入することだけが、間に合うようにパッチを適用できる方法です。フックはプレーヤーが存在するメインワールドで実行します(worldName は指定しません)。
  • appendBuffer は各 SourceBuffer インスタンスの独自プロパティとしてパッチを適用し、プロトタイプには適用しないでください。 SourceBuffer.prototype.appendBuffer は書き込み不可のネイティブメソッドです。プロトタイプ上で sb.appendBuffer = fn としても何も起こらず、Object.defineProperty(SourceBuffer.prototype, 'appendBuffer', …) も効果がありません。インスタンスに独自プロパティを定義すると、プロトタイプのメソッドを正しくシャドーイングできます(addSourceBuffer は書き込み可能なため、defineProperty を使ってプロトタイプにパッチを適用できます)。
  • フォアグラウンドのタブを使ってください。 TikTok ではバックグラウンドタブでの自動再生が不安定です。プレーヤーが起動しないと MediaSource にデータが供給されず、キャプチャーは空になります。createTarget({ url: 'about:blank' }) を使います(background: true は指定しません)。
  • 出力を <video>.duration までトリミングしてください。 TikTok の MSE メディアの生データは、再生された(編集リストに記載された)長さよりも長くなります。-t <dur> がないと、ユーザーが見ていない末尾の部分が結合後のファイルに残ります(「再生時間までトリミングする理由」を参照)。静止状態のラッチにより、トリミング前に視聴部分がすべてキャプチャーされるため、常に安全です。
  • カバレッジは append に応じて進みますが、buffered.end だけでなく、静止状態でラッチしてください。 TikTok は fMP4 全体を最初に append するため、キャプチャーは数秒で完了します。しかし、生メディア(編集リストに記載された長さよりも長いもの)の append が続いていても、buffered.end は編集リストに記載された長さで上限に達します。end >= dur-0.5 だけでラッチすると、まだバイトが流れている間に発火することがあります。ttdl はさらに、処理を終了する前に、キャプチャー済みの総バイト数が 2 回連続のティック(500 ms)の間変化しないことを要求します。__capDone はラッチ時にキャプチャーを凍結し、自動ループによる再 append がバッファーに追加されないようにします。
  • 各種類のバッファーから最大のものを選んでください。プレーヤーは画質が上がると MediaSource を再初期化します。 TikTok は再生途中で画質を上げることがあり(例:HEVC の hvc1.1.6.L186 → hvc1.1.6.L150)、音声と動画それぞれの SourceBuffer を持つ 2 つ目の MediaSource が作られます。バッファーは 4 つ(動画 2 つ、音声 2 つ)確認できます。各種類で最大のものが完全なキャプチャーで、小さいほうのペアは破棄された低画質のものです。種類ごとにバッファーが 1 つだけだと思い込まないでください。
  • TikTok は自動ループするため、ended に頼らないでください。 <video> は ended を発火させる代わりに 0 秒へ戻るため、ended に基づくラッチは永遠に回り続けます。ループによる再 append でキャプチャーが二重にならないようにするのが __capDone フラグです(ループしないケースへの安全策として ended のチェックも残していますが、実際の終了条件は静止状態とカバレッジのラッチです)。
  • ポーリングのたびに muted を再設定してください(ただし playbackRate は再設定しないでください)。 画質の切り替えや再初期化の際に、TikTok のプレーヤーがミュートを解除することがあります。再生開始時に一度だけ muted = true にすると上書きされ、キャプチャー中に音声が聞こえてしまいます。カバレッジのポーリングでは、250 ms ごとに muted を再設定します。playbackRate は再設定しないでください。キャプチャーは append に応じて進み(再生が進む前にファイル全体がバッファリングされるため)、再生速度は見た目にしか影響しません。また、フォアグラウンドに移したタブでは TikTok のプレーヤーが playbackRate を 1× に戻すため、無理に変更しても意味がありません。YouTube のカバレッジは再生に依存する(SABR は再生に合わせて取得する)ため、ytdl は 16× を再設定します。ttdl はそうではないので、この違いは意図的です。
  • カバレッジを確認した後に、data-e2e=video-desc をフォールバックとしてタイトルを読み取ってください。 SRM/ハイドレーションが完了するまで(数秒かかります)、document.title は一般的な "TikTok - Make Your Day" のままです。networkIdle の直後に読み取ると、一般的なタイトルが取得されます。ttdl はカバレッジのループ完了後にタイトルを読み取り、まだ一般的なタイトルの場合は [data-e2e=video-desc] 要素のテキストを使います。
  • 「verify you are human」(人間であることを確認してください)/CAPTCHA のインタースティシャルを検出し、再試行を控えてください。 TikTok はボット検出を積極的に行います。ttdl はプレーヤーの準備完了を待つポーリング中に document.body.innerText を走査して verify|robot|human|captcha|unusual|slide to|puzzle を検出し、見つかった場合は明確なエラーを返します。ブラウザーで一度 URL を手動で開いて解除してから再試行してください。繰り返しアクセスしないでください。
  • アクセス制限のあるコンテンツでは、ログイン済みのタブを利用してください。 地域制限、年齢制限、フォロワー限定の動画には、TikTok へのログインが必要な場合があります。ttdl は独自のタブを開きますが、ブラウザーの Cookie 保存領域を再利用します。ブラウザーで視聴できる動画なら、ttdl で記録できます。
  • ffmpeg は常に必要です。 HD の結合にのみ ffmpeg が必要な ytdl とは異なり、ttdl ではすべてのダウンロードに必要です。TikTok は音声と動画を別々のバッファーで配信するため(結合)、編集リストの末尾をトリミングし、フラグメント化された MP4 をキャプチャーする(faststart)ために使います。ffmpeg が PATH にない場合、CLI は実行を拒否します。-y(上書き)を削除しないでください。これがないと ffmpeg の対話式の Overwrite? [y/N] プロンプトは非対話環境で失敗し、古いファイルが残ります。
  • バイト列は、256 KB(3 の倍数)のスライスにして、Runtime.evaluate の returnByValue 経由で CDP の境界を越える際に base64 で渡し、Buffer.from(b64,'base64') でデコードして append します。 スライスサイズを 3 の倍数にすると、各スライスの base64 を個別にデコードできます(途中に = パディングが入りません)。3 の倍数以外のオフセットでスライスすると、連結後のデータが正しくデコードされません。4 MB ではなく、256 KB にしてください。 4 MB のスライスを base64 化して returnByValue フレーム 1 つで送ると、1 つの CDP フレームで約 5.6 MB の JSON となり、最初の呼び出しで Dia のデバッグ WebSocket が切断されます(ytdl で再現済み。同じバッファーからの 256 KB スライスは成功しますが、4 MB スライスでは rs→3 が即座にドロップします)。TikTok のバッファーは通常 4 MB 未満なので、ttdl ではこれまで表面化していませんでした。しかし、長尺または高画質の HEVC 動画では 4 MB を超えるため、最初の取得スライスでソケットが切断されます。スライスは小さくしてください。このサイズではスループットは問題になりません。
  • 出力は HEVC + AAC で、そのまま QuickTime/iOS で利用できます。 TikTok は通常、HEVC(hvc1)の動画と AAC(mp4a)の音声を配信します。-c copy でこれらを保持すれば、iOS/macOS でネイティブ再生できるため、ytdl の AV1/Opus とは異なり、再エンコードは不要です。AV1/VP9 + Opus で取得され、QuickTime/iOS で使う必要がある場合は、次のように一度で再エンコードします。ffmpeg -y -i "out.mp4" -c:v libx264 -crf 18 -preset veryfast -pix_fmt yuv420p -c:a aac -b:a 192k "out-qt.mp4"。
  • ヒアドキュメントの本文では、バッククォートを一切使わないでください。 引用符で囲んでいない <<EOF では、単独のバッククォートが bash のコマンド置換を開始し、EOF の解析エラーが発生します。文字列連結('...' + var + '...')と二重引用符の JS 文字列を使い、テンプレートリテラルは使わないでください。同様に、JS 内の $ も避けてください(意図した ${bash_var} の展開だけは例外です)。また、引用符で囲んでいないヒアドキュメントでそのまま渡せるよう、URL の解析にはバックスラッシュ付き正規表現を使わず、indexOf/slice を使ってください。

コメント