mcpField
T
ttdl

ttdl

github.com/monotykamary创意与媒体462 次安装2 天前更新
代码仓库

关于 ttdl

ttdl 是通过浏览器播放捕获 TikTok 视频的命令行工具,适合需要保存视频或仅提取音频的用户。它记录播放器实际获得的媒体流,并用 ffmpeg 合并、裁剪而不重新编码。

主要功能

  1. 01下载 TikTok 视频
  2. 02仅保存音频为 M4A
  3. 03显示标题、作者、时长和分辨率
  4. 04支持短链接及纯数字视频 ID
  5. 05合并并裁剪媒体流而不重新编码

适用场景

  1. 01保存 TikTok 播放器播放的视频
  2. 02提取视频中的音频
  3. 03检查视频信息后决定是否下载

安装

复制下面的命令,在终端中运行。

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

安装后在 Claude Code 中输入/ttdl确认已加载。

附带文件scripts/setupscripts/ttdl

详细介绍

ttdl — 浏览器原生 TikTok 下载器

一个精简的 browser-harness-js heredoc,与 gsearch/xsearch/ytdl 完全相同。 没有 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.* 音频),因此需要将它们合并成一个文件。
  • 裁剪: 原始 MSE 媒体比播放器播放的内容更长(参见“为何要按时长裁剪”)。
  • 快速启动: 捕获的流是分片 MP4;-movflags +faststart 会将 moov 移到文件开头,使文件可在任何地方跳转和流式播放。

输出保留 TikTok 自身的编解码器,纯净无损——HEVC 视频 + AAC 音频。这种组合兼容 QuickTime/iOS(iOS 可原生解码 HEVC+AAC),因此不同于 ytdl 的 AV1/Opus 输出,无需为 Apple 设备重新编码。

工作原理

全部写在一个 browser-harness-js <<EOF heredoc 中,采用 ytdl 风格:

  1. 连接到浏览器共享的 CDP 会话(或调用 session.connect())。
  2. Page.addScriptToEvaluateOnNewDocument — 在任何页面 JS 运行之前注入 MSE hook:
    • 包装 MediaSource.prototype.addSourceBuffer(通过 Object.defineProperty),为每个 SourceBuffer 标记其 编解码器和派生的类型。
    • 在每个 SourceBuffer 上定义一个自有的 appendBuffer 属性,在调用原始方法之前记录字节(解复用后的 ISO-BMFF 片段)。必须使用自有属性——SourceBuffer.prototype.appendBuffer 是不可写的原生方法,因此重新赋值原型会静默无效。
    • 类型根据 codecs= 的 fourcc 派生,而非容器 MIME(见“陷阱”——这是与 ytdl 相比,TikTok 特有的最大差异)。
  3. 将 Target.createTarget 置于前台 → 对观看 URL 执行 Page.navigate → 等待 networkIdle,然后轮询 <video>;如果遇到验证/验证码拦截页则退出。必须置于前台:后台标签页的自动播放不稳定,而且如果播放器始终未启动,就不会向 MediaSource 输送数据。
  4. 从最终的(重定向后)location.href 读取作者和视频 ID——短链接(vm.tiktok.com/…、tiktok.com/t/…)此时已完成重定向。
  5. 从 currentTime = 0 开始静音播放。与 ytdl 不同(16×,因为 YouTube 的 SABR 会在播放时获取数据,且播放速率会限制捕获速度),TikTok 的捕获由追加操作驱动:播放器会预先获取并追加整个 fMP4,因此在 currentTime 开始移动之前,buffered.end 就会达到完整时长。所以播放速率对捕获来说只是表面设置——而 TikTok 的播放器在标签页处于前台时会将 playbackRate 重置为 1×,与之对抗毫无意义。ttdl 不设置 playbackRate。不过它会在每次覆盖范围检查时重新设置 muted:播放器可能在切换画质或重新初始化时取消静音,导致捕获过程中突然播放声音。
  6. 等待已观看部分完成缓冲,且追加活动趋于静止(buffered.end >= duration-0.5,并且捕获的总字节数连续 2 次检查未变),然后原子地冻结捕获并暂停。__capDone 标志会在覆盖范围完成的瞬间,让 hook 继续传递追加操作(停止录制),避免 TikTok 自动循环(会重新向 MSE 输送数据)污染捕获的缓冲区。
  7. 选取最大的 video 缓冲区和最大的 audio 缓冲区。 播放器通常会创建两个 MediaSource 实例——一个初始的低画质实例,随后在画质提升时创建一个更高画质的实例——从而产生 4 个缓冲区(2 个 video、2 个 audio)。每种类型中最大的缓冲区才是真正捕获的内容;较小的缓冲区属于已弃用的低画质实例。
  8. 分块写入磁盘,每块 256 KB(字节数可被 3 整除,因此每个 base64 分片都可独立解码):页面端的 __pullBuffer(i,offset,len) 返回 base64;REPL 使用 Buffer.from(b64,'base64') 解码,并通过 fs.appendFileSync 追加写入。使用 256 KB,而不是 4 MB——见下文“陷阱”:第一次调用时,携带 4 MB 分片 base64 的单个 returnByValue 帧会关闭 Dia 的调试 WebSocket。
  9. ffmpeg -i video -i audio -t <duration> -c copy -movflags +faststart out.mp4(heredoc 返回临时文件路径后,在 bash 中执行)。
  10. 在 try/finally 中以 fire-and-forget 方式调用 closeTab——与 ytdl 的清理流程完全一致。

为什么在 MediaSource 捕获,而不是从 CDN URL 下载

TikTok 会通过页面生成并与会话绑定的签名 CDN URL 提供每条视频 (v16-webapp-prime.tiktok.com/video/tos/…?a=…&bti=…&ft=…&x-expires=…&x-signature=…)。 签名就在 URL 中,因此简单的 fetch(url) 在页面仍然有效时可能可行,但 URL 有效期很短,响应可能受范围限制,而且你得逆向分析 TikTok 的播放器才能找到它。在 SourceBuffer.appendBuffer 处捕获可以绕过这些问题:无论播放器获取了什么,也无论它如何签名,解复用后的字节都会明文流经 MSE。这种方式不依赖身份验证、签名或画质——页面已经处理好这一切;我们只需记录输出。这其实就是“观看 → 录制”,与 ytdl 使用的模式相同。

为什么要按时长裁剪

TikTok 提供的 fMP4 长于播放器实际播放的内容。<video>.duration(以及 seekable/buffered 范围)反映的是剪辑列表中的播放时长,例如 27.8 s;而原始 MSE 媒体约为 30.4 s。播放器播放 0 → 27.8 s,然后循环回到 0;尾部约 2.6 s 是真实画面,但播放器会刻意将其排除在信息流之外。如果你原样捕获所有 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 heredoc,不含 #!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 钩子必须通过 Page.addScriptToEvaluateOnNewDocument 注入,而不是在页面加载后调用 Runtime.evaluate。 播放器启动时会获取 MediaSource/addSourceBuffer 的引用;只有在页面运行任何 JS 之前注入,才能及时对它们进行修补。钩子在主 world 中运行(不设置 worldName),播放器也在此处运行。
  • 将 appendBuffer 作为每个 SourceBuffer 实例的自有属性进行修补,绝不要修改原型。 SourceBuffer.prototype.appendBuffer 是不可写的原生方法——在原型上执行 sb.appendBuffer = fn 会静默无效;Object.defineProperty(SourceBuffer.prototype, 'appendBuffer', …) 也无法生效。在实例上定义自有属性可以正确遮蔽原型方法。(可以通过 defineProperty 修补 addSourceBuffer 的原型——它是可写的。)
  • 使用前台标签页。 TikTok 在后台标签页中的自动播放并不可靠;如果播放器没有启动,MediaSource 就不会收到数据,捕获结果也会是空的。使用 createTarget({ url: 'about:blank' })(不要设置 background: true)。
  • 将输出截取到 <video>.duration。 TikTok 原始 MSE 媒体的时长比实际播放(编辑列表所列)的时长更长;如果不使用 -t <dur>,封装后的文件末尾会多出一段用户从未看到的内容(参见“为何要截取到媒体时长”)。静默判定闩锁可确保截取前已完整捕获观看部分,因此这样做始终安全。
  • 覆盖范围由 append 驱动,但要在静默时锁存,不能只看 buffered.end。 TikTok 会预先追加整个 fMP4,因此捕获只需几秒即可完成——但即使更长的原始媒体仍在不断追加,buffered.end 也会封顶在编辑列表所列的时长。仅在 end >= dur-0.5 时锁存,可能会在字节仍持续流入时触发。ttdl 还要求已捕获字节总量连续 2 个检测周期(500 ms)保持不变,才会退出。锁存时冻结的 __capDone 可防止自动循环再次追加数据到缓冲区。
  • 每种类型都选择最大的缓冲区——播放器在画质提升时会重新初始化 MediaSource。 TikTok 会在播放中途提高画质(例如从 HEVC hvc1.1.6.L186 切换到 hvc1.1.6.L150),从而创建第二个 MediaSource,并带有自己的音频和视频 SourceBuffer。你会看到 4 个缓冲区(2 个视频、2 个音频);每种类型中最大的缓冲区才是真正完整的捕获内容,较小的一对属于被弃用的低画质版本。不要假定每种类型只有一个缓冲区。
  • TikTok 会自动循环播放;绝不要依赖 ended。 <video> 会循环回到 0,而不是触发 ended,因此基于 ended 的闩锁会无限运行。__capDone 标志负责阻止循环播放产生的重复追加,以免捕获内容翻倍。(为处理不循环的边缘情况,仍保留了 ended 检查作为安全措施,但真正的退出条件是静默加覆盖范围闩锁。)
  • 每次轮询都重新设置 muted(但不要设置 playbackRate)。 TikTok 播放器可能会在切换画质或重新初始化时取消静音;播放开始时只设置一次 muted = true 会被覆盖,导致捕获过程中听到声音。覆盖范围轮询每隔 250 ms 就会重新设置 muted。不要重新设置 playbackRate:捕获由 append 驱动(整个文件会在播放进度推进前就缓冲完成),因此播放速率只是表面设置;而且 TikTok 在标签页切回前台时会将 playbackRate 重置为 1×,与之对抗毫无意义。ytdl 会重新设置为 16×,因为 YouTube 的覆盖范围受播放进度限制(SABR 会边播放边抓取);ttdl 则不受此限制,因此两者有意采用不同做法。
  • 在覆盖范围达到后再读取标题,并设置 data-e2e=video-desc 作为回退。 在 SRM/页面水合完成之前(需要几秒),document.title 都是通用标题 "TikTok - Make Your Day"。如果在 networkIdle 后立即读取,得到的就是这个通用标题。ttdl 会在覆盖范围循环完成后读取标题;如果标题仍是通用标题,则回退到 [data-e2e=video-desc] 元素的文本。
  • 检测“验证你是人类”/验证码拦截页,并停止重试。 TikTok 对机器人检测十分严格。ttdl 会在播放器就绪轮询期间扫描 document.body.innerText 中是否包含 verify|robot|human|captcha|unusual|slide to|puzzle,如果命中,就抛出清晰的错误。先在浏览器中手动打开一次 URL 以解除拦截,再重试——不要反复轰炸。
  • 访问受限内容时,借用已登录的标签页。 地区锁定、年龄限制和仅限关注者观看的视频可能需要有效的 TikTok 登录。ttdl 会打开自己的标签页,但会复用浏览器的 Cookie 存储;如果你能在浏览器中观看,ttdl 就能录制。
  • 始终需要 ffmpeg。 与 ytdl(只有封装 HD 视频时才需要 ffmpeg)不同,ttdl 每次下载都需要它:TikTok 会分别提供音频和视频缓冲区(需要封装)、编辑列表尾部(需要截取),还要捕获分片 MP4(需要 faststart)。如果 ffmpeg 不在 PATH 中,CLI 会拒绝运行。不要删掉 -y(覆盖)——否则 ffmpeg 的交互式 Overwrite? [y/N] 提示会在非交互模式下失败,并留下过期文件。
  • 字节会通过 CDP 边界以 base64 形式传输:通过 Runtime.evaluate 的 returnByValue,以 256 KB(3 的倍数)为单位切片,使用 Buffer.from(b64,'base64') 解码后再追加。切片大小可被 3 整除,因此每个切片的 base64 都能单独解码(内部不会出现 = 填充)。不要从非 3 的倍数偏移处切片,否则拼接后的内容会解码为乱码。必须是 256 KB,不能是 4 MB: 单个 returnByValue 帧携带 4 MB 切片的 base64 时,一个 CDP 帧中的 JSON 大约有 5.6 MB,会在首次调用时关闭 Dia 的调试 WebSocket(已在 ytdl 中复现:同一缓冲区的 256 KB 切片可以正常传输,而 4 MB 切片会立即导致 rs→3)。这个潜在问题在 ttdl 中一直未暴露,因为 TikTok 缓冲区通常小于 4 MB;但较长或较高画质的 HEVC TikTok 视频会超过此大小,首次拉取切片就会导致套接字断开。请使用较小切片;在这些大小下,吞吐量不是问题。
  • 输出为 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"。
  • heredoc 正文中的任何位置都不能出现反引号。 在未引用的 <<EOF 中,单个反引号会开启 bash 命令替换并触发 EOF 解析错误。请使用字符串拼接('...' + var + '...')和双引号 JS 字符串,绝不要使用模板字面量。同样要避免在 JS 中使用 $(只允许有意使用 ${bash_var} 插值),并避免使用反斜杠正则解析 URL(改用 indexOf/slice),这样未引用的 heredoc 才会原样传递内容。

评论

支持 Markdown