ttdl について
ttdl はブラウザーでの再生を記録する TikTok 向けコマンドラインツールです。動画や音声を保存したいユーザー向けで、再生ストリームを取得し、ffmpeg で再エンコードせずに結合・トリミングします。
主な機能
- 01TikTok 動画のダウンロード
- 02音声のみを M4A で保存
- 03タイトル、作者、長さ、解像度の表示
- 04短縮リンクと数値の動画 ID に対応
- 05再エンコードせずに結合・トリミング
活用シーン
- 01TikTok で再生される動画の保存
- 02TikTok 動画から音声を抽出
- 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.*動画と AACmp4a.*音声)。そのため、1 つのファイルにまとめる必要があります。 - トリミング: 生の MSE メディアは、プレーヤーで再生される長さよりも長くなります(「再生時間に合わせてトリミングする理由」を参照)。
- 高速再生用の最適化: キャプチャしたストリームはフラグメント化 MP4 です。
-movflags +faststartはmoovを先頭に並べ替え、どこでもシークやストリーミングをできるようにします。
出力は、TikTok 独自のコーデックを使った純粋な(ロスレスの)形式、すなわち HEVC 動画と AAC 音声のままです。この組み合わせは QuickTime/iOS と相性がよく(iOS は HEVC+AAC をネイティブにデコードします)、ytdl の AV1/Opus 出力とは異なり、Apple デバイス向けに再エンコードする必要はありません。
仕組み
すべてを browser-harness-js <<EOF のヒアドキュメント内に記述します。ytdl 方式です。
- ブラウザーの共有 CDP セッションに接続します(または
session.connect()を使用します)。 Page.addScriptToEvaluateOnNewDocument— ページ上の JS が実行される 前に MSE フックを注入します。MediaSource.prototype.addSourceBufferをラップします(Object.definePropertyを使用)。各SourceBufferに コーデックと派生した 種別をタグ付けします。- 各 SourceBuffer に、元のメソッドを呼び出す前にバイト列(デマックス済みの ISO-BMFF フラグメント)を記録する OWN
appendBufferプロパティを定義します。独自プロパティが必要です。SourceBuffer.prototype.appendBufferは書き込み不可のネイティブメソッドなので、プロトタイプへの再代入は何もせずに終了します。 - 種別はコンテナの MIME ではなく、
codecs=の fourcc から判定します(Traps を参照してください。ここが ytdl との最大の TikTok 固有の違いです)。
Target.createTargetでフォアグラウンドを設定 →Page.navigateで視聴 URL に移動 →networkIdleを待ち、<video>をポーリングします。確認/CAPTCHA のインタースティシャルが表示されたら中止します。フォアグラウンドが必要です。バックグラウンドタブでは自動再生が不安定で、プレーヤーが再生を開始しないと MediaSource にデータが供給されないためです。- 作成者と動画 ID を最終的な(リダイレクト後の)
location.hrefから読み取ります。短縮リンク(vm.tiktok.com/…、tiktok.com/t/…)は、この時点でリダイレクト済みです。 currentTime = 0からミュート再生します。ytdl は、YouTube の SABR が再生しながら取得し、再生速度がキャプチャ範囲を制限するため 16× にしますが、TikTok のキャプチャは append 駆動です。プレーヤーは fMP4 全体を先に取得して追加するため、currentTimeが少しも進む前にbuffered.endが全再生時間に達します。そのため、キャプチャに対して再生速度は 見かけだけです。また、タブがフォアグラウンドにある間、TikTok のプレーヤーはplaybackRateを 1× に戻すため、変更を試みても意味がありません。ttdl はplaybackRateを設定しません。カバレッジを確認するたびにmutedを再設定します。画質の切り替え/再初期化時にプレーヤーがミュートを解除し、キャプチャ中に突然音声が再生されることがあるためです。- 視聴部分がバッファーに格納され、さらに append の動作が落ち着くまで待ちます(
buffered.end >= duration-0.5かつ キャプチャしたバイト数の合計が 2 ティック連続で変化しない状態)。その後、キャプチャの停止と一時停止をアトミックに行います。__capDoneフラグによって、カバレッジが完了した瞬間にフックは append を通過させ(記録は停止し)、TikTok の自動ループ(MSE にデータを再供給します)がキャプチャ済みバッファーを汚染しないようにします。 - 動画バッファーと音声バッファーをそれぞれ最大のものから選びます。 プレーヤーは一般的に 2 つの MediaSource インスタンスを作成します。最初に低画質のものを作成し、その後、品質が上がると高画質のものを作成するため、バッファーは 4 つ(動画 2 つ、音声 2 つ)になります。それぞれ最大のものが実際のキャプチャで、小さいものは破棄された低画質側のペアです。
- それぞれを 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 が切断されます。 - ffmpeg
-i video -i audio -t <duration> -c copy -movflags +faststart out.mp4(ヒアドキュメントの終了後、bash で一時ファイルのパスを渡します)。 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を使ってください。