第一章:Go语言视频课程下载全流程概述
获取高质量的Go语言学习资源是初学者和进阶开发者夯实基础的关键一步。本流程聚焦于合法、稳定且可复现的视频课程下载方式,适用于主流平台提供的加密或非加密流媒体内容,强调本地化存档与离线学习能力。
准备工作
确保系统已安装以下工具:
curl或wget(用于基础HTTP请求)yt-dlp(推荐替代 youtube-dl,支持更多平台及DRM绕过提示)ffmpeg(用于合并音视频流及格式转换)
验证安装:
# 检查核心工具版本(确保 yt-dlp ≥ 2023.10.13)
yt-dlp --version && ffmpeg -version | head -n1
若未安装,macOS用户可执行 brew install yt-dlp ffmpeg;Ubuntu/Debian用户运行 sudo apt update && sudo apt install ffmpeg && sudo pip3 install yt-dlp。
获取课程访问权限
多数正规课程平台(如Udemy、Coursera、极客时间)要求登录后生成临时会话凭证。需使用浏览器开发者工具(F12 → Network → Filter: m3u8 或 mpd)捕获播放页的媒体清单URL。注意:仅限个人学习用途,禁止批量爬取或二次分发。
下载与合成视频
对常见HLS(.m3u8)流,执行以下命令:
yt-dlp \
--cookies-from-browser chrome \
--format "bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]" \
--merge-output-format mp4 \
--output "go-course-%(title)s.%(ext)s" \
"https://example.com/playlist.m3u8"
其中 --cookies-from-browser chrome 自动复用浏览器登录态;--format 指定优先下载高清视频+独立音频并自动合并;输出文件按课程标题命名,便于归档。
验证与组织
| 下载完成后建议执行完整性校验: | 检查项 | 命令示例 |
|---|---|---|
| 视频时长是否合理 | ffprobe -v quiet -show_entries format=duration -of default=nw=1 file.mp4 |
|
| 音画是否同步 | ffplay -autoexit -t 5 file.mp4(播放前5秒预览) |
将所有视频按模块归入 ./go-course/01-basics/、./go-course/02-concurrency/ 等子目录,配合 README.md 记录对应章节大纲与知识点索引。
第二章:HTTP Header解析与网络请求构造
2.1 HTTP协议核心字段语义解析与Wireshark抓包验证
HTTP请求/响应头部字段承载关键语义,需结合协议规范与真实流量交叉验证。
关键字段语义对照
| 字段名 | 语义作用 | 常见取值示例 |
|---|---|---|
Content-Length |
声明消息体字节长度,影响连接复用判断 | 342 |
Connection |
控制连接生命周期 | keep-alive / close |
Transfer-Encoding |
指定分块编码等传输修饰 | chunked |
Wireshark抓包实证片段(HTTP/1.1 GET)
GET /api/users HTTP/1.1
Host: example.com
Accept: application/json
Connection: keep-alive
此请求中
Connection: keep-alive显式声明复用TCP连接;Wireshark可过滤http.request && http.host == "example.com"快速定位该流。若服务端未返回Connection: keep-alive或含Content-Length: 0,则连接可能立即关闭。
字段协同逻辑示意
graph TD
A[Client发送Request] --> B{Server检查Connection}
B -->|keep-alive| C[复用TCP连接]
B -->|close| D[发送响应后断开]
C --> E[后续请求复用同一socket]
2.2 Go net/http库深度实践:自定义Client与Header注入
自定义HTTP Client的必要性
默认 http.DefaultClient 共享连接池且缺乏超时控制,高并发场景易引发资源耗尽或阻塞。
构建带超时与复用能力的Client
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
},
}
Timeout:限制整个请求生命周期(含DNS、连接、TLS握手、读写);MaxIdleConnsPerHost:防止单域名连接泄漏;IdleConnTimeout:及时回收空闲连接,避免TIME_WAIT堆积。
Header注入的两种方式
- 全局注入(Transport级):适用于认证Token等固定头;
- 单次请求注入(Request级):适用于trace-id、用户上下文等动态头。
| 场景 | 推荐方式 | 可控粒度 |
|---|---|---|
| Bearer Token | Transport RoundTrip | 粗粒度 |
| X-Request-ID | req.Header.Set() | 精确到请求 |
动态Header注入示例
req, _ := http.NewRequest("GET", "https://api.example.com/v1/users", nil)
req.Header.Set("Authorization", "Bearer abc123")
req.Header.Set("X-Request-ID", uuid.New().String())
req.Header.Set("User-Agent", "MyApp/1.0")
此方式确保每次请求携带唯一追踪ID与合法凭据,兼顾安全性与可观测性。
2.3 Content-Type与Content-Disposition响应头提取实战
常见响应头语义解析
Content-Type 告知客户端资源的MIME类型(如 application/json; charset=utf-8);
Content-Disposition 控制浏览器是否内联显示或触发下载,并可指定文件名(如 attachment; filename="report.pdf")。
关键字段提取逻辑
需兼顾参数解析、编码处理与边界容错:
import re
def extract_headers(resp):
ct = resp.headers.get("Content-Type", "")
cd = resp.headers.get("Content-Disposition", "")
# 提取 MIME 主类型(忽略参数)
mime_type = ct.split(";")[0].strip() if ct else "text/plain"
# 解析 filename*(RFC 5987 编码)优先,fallback 到 filename
filename = ""
if 'filename*' in cd:
match = re.search(r"filename\*\s*=\s*([^;]+)", cd)
if match:
# 示例:UTF-8''report%20%E6%96%87%E4%BB%B6.pdf → 解码为“报告文件.pdf”
encoded = match.group(1).strip()
filename = re.sub(r"%([0-9A-Fa-f]{2})", lambda m: chr(int(m.group(1), 16)), encoded.split("''")[-1])
elif 'filename=' in cd:
filename = re.search(r'filename="([^"]+)"', cd)?.group(1) or ""
return {"mime": mime_type, "filename": filename}
逻辑说明:
mime_type截断分号后参数,确保主类型纯净;filename*优先匹配 RFC 5987 编码值(含UTF-8''前缀),需 URL 解码;filename双引号内值作兜底,避免未转义引号导致解析失败。
典型响应头组合对照表
| Content-Type | Content-Disposition | 行为意图 |
|---|---|---|
image/png |
inline; filename="logo.png" |
浏览器内联渲染 |
application/pdf |
attachment; filename*=UTF-8''%E6%8A%A5%E5%91%8A.pdf |
强制下载,中文名 |
text/csv; charset=utf-8 |
attachment; filename="data.csv" |
下载,ASCII 文件名 |
处理流程概览
graph TD
A[获取原始响应头] --> B{是否存在 Content-Disposition?}
B -->|是| C[优先解析 filename*]
B -->|否| D[设 filename = None]
C --> E[URL解码 + 字符集还原]
E --> F[提取 Content-Type 主类型]
F --> G[返回结构化元数据]
2.4 User-Agent指纹识别与服务端反爬策略逆向分析
现代反爬系统已不再仅校验 User-Agent 字符串格式,而是结合 TLS 指纹、HTTP/2 设置帧顺序、JavaScript 运行时特征等构建多维 UA 指纹。
常见服务端 UA 校验维度
- 浏览器厂商与内核版本一致性(如
Chrome/125但platform: Win32与webkit内核冲突) - TLS Client Hello 中的扩展顺序与 ALPN 协议列表
Sec-Ch-Ua与User-Agent的语义对齐度
TLS 指纹提取示例(ja3)
# 使用 python-ja3 库提取客户端指纹
from ja3 import get_ja3_from_raw
# raw_tls_handshake 是捕获的 Client Hello 原始字节流
ja3_hash = get_ja3_from_raw(raw_tls_handshake)
print(ja3_hash) # e.g., "7d969e82b044c80212b2035f86393c85"
该哈希由 TLS 版本、加密套件、扩展类型、椭圆曲线及格式化方式拼接后 MD5 得到,服务端可比对白名单 JA3 指纹库快速拦截非标准客户端。
主流浏览器 JA3 指纹特征对比
| 浏览器 | 典型 JA3 Hash 前缀 | 关键扩展顺序特征 |
|---|---|---|
| Chrome 125 | 7d969e82... |
sni,alpn,ec_point_formats |
| Firefox 127 | a3bf09af... |
sni,ec_point_formats,alpn |
| Requests/2.32 | d013b618... |
缺失 alpn,扩展顺序异常 |
graph TD A[Client Hello] –> B{TLS 解析层} B –> C[提取版本/套件/扩展] C –> D[按JA3规则标准化排序] D –> E[MD5生成指纹] E –> F[服务端比对白名单]
2.5 基于http.Transport的连接复用与超时控制调优
http.Transport 是 Go HTTP 客户端性能的核心枢纽,其连接复用与超时策略直接影响吞吐量与稳定性。
连接池关键参数
MaxIdleConns: 全局最大空闲连接数(默认 100)MaxIdleConnsPerHost: 每 Host 最大空闲连接数(默认 2)IdleConnTimeout: 空闲连接保活时长(默认 30s)
超时分层控制
transport := &http.Transport{
IdleConnTimeout: 90 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
ResponseHeaderTimeout: 5 * time.Second, // 仅等待 header
ExpectContinueTimeout: 1 * time.Second,
}
ResponseHeaderTimeout防止后端响应头迟迟不返回导致连接长期阻塞;TLSHandshakeTimeout避免 TLS 握手异常拖垮整个连接池。
调优对比表
| 场景 | 推荐值 | 影响 |
|---|---|---|
| 高频短请求 | MaxIdleConnsPerHost=20 | 提升复用率,降低建连开销 |
| 弱网/高延迟链路 | IdleConnTimeout=120s | 减少重复握手 |
graph TD
A[HTTP Client] --> B[Transport]
B --> C[连接池]
C --> D{空闲连接 > MaxIdleConnsPerHost?}
D -->|是| E[关闭最旧连接]
D -->|否| F[复用现有连接]
第三章:Referer伪造与会话上下文模拟
3.1 Referer机制原理与前端路由跳转链路还原
Referer 是 HTTP 请求头字段,由浏览器自动注入,标识当前请求的来源页面 URL。在单页应用(SPA)中,其值取决于导航触发方式而非当前地址栏 URL。
浏览器 Referer 生成规则
- 直接地址栏输入 → 无 Referer
<a href>点击 → 设置为当前页面 URLlocation.assign()/location.replace()→ 继承上一跳 Refererhistory.pushState()→ 不触发新请求,Referer 不更新
前端路由跳转链路还原难点
SPA 中 pushState 跳转不发请求,服务端无法通过 Referer 获取真实跳转路径。需结合客户端日志埋点还原:
// 在路由守卫中记录跳转链路
router.beforeEach((to, from) => {
console.log(`From: ${from.fullPath} → To: ${to.fullPath}`);
// 上报至分析平台,补充 referer 字段模拟
analytics.track('route-change', {
from: from.fullPath,
to: to.fullPath,
referrer: document.referrer // 仅对首次加载或外链进入有效
});
});
该代码捕获 Vue Router 的导航钩子,
from.fullPath和to.fullPath构成可追溯的路由路径节点;document.referrer在页面内跳转时恒为初始入口页,故需额外维护lastRoute状态实现链路拼接。
| 触发方式 | 是否更新 Referer | 是否触发 fetch/Navigation |
|---|---|---|
<a> 标签点击 |
✅ | ✅(完整页面导航) |
router.push() |
❌ | ❌(仅 DOM 更新) |
fetch() 请求 |
✅(继承当前页) | ✅ |
graph TD
A[用户点击链接] --> B{是否为 SPA 内部路由?}
B -->|是| C[调用 history.pushState<br>→ 不发新请求<br>→ Referer 不变]
B -->|否| D[触发完整页面导航<br>→ 浏览器注入 Referer]
C --> E[需前端主动上报跳转上下文]
3.2 Go中CookieJar持久化管理与Session状态同步
Go标准库net/http/cookiejar默认仅内存存储,无法跨进程维持登录态。需自定义Jar实现持久化。
持久化CookieJar实现
type PersistentJar struct {
cookies map[string][]*http.Cookie // domain → cookies
mu sync.RWMutex
file string
}
func (j *PersistentJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
j.mu.Lock()
defer j.mu.Unlock()
domain := u.Hostname()
j.cookies[domain] = append(j.cookies[domain], cookies...)
j.saveToFile() // 序列化到磁盘
}
SetCookies接收原始Cookie列表并按域名归类;saveToFile需调用json.Marshal写入文件,确保重启后可恢复。
Session同步关键点
- Cookie过期时间必须与后端Session TTL严格对齐
- 同一用户多设备登录时,需监听
/logout事件广播清除本地Jar缓存
| 同步机制 | 实时性 | 一致性保障 |
|---|---|---|
| 文件轮询 | 秒级 | 依赖fsnotify |
| Redis订阅 | 毫秒级 | 需额外中间件 |
数据同步机制
graph TD
A[HTTP Client] -->|Set-Cookie| B(PersistentJar)
B --> C[本地JSON文件]
C --> D[进程重启]
D --> E[Load Cookies]
E --> A
3.3 利用htmlquery解析页面DOM动态提取Referer依赖路径
在现代前端工程中,静态资源(如 JS/CSS)的 Referer 依赖关系常隐含于 HTML 的 <script>、<link> 及内联 fetch() 调用中,需动态解析 DOM 结构还原真实请求上下文。
核心解析策略
- 遍历所有
<script src>和<link rel="stylesheet" href>节点 - 提取
document.write()、eval()中潜在的动态 URL 拼接逻辑(需结合 AST 分析,此处聚焦 DOM 层) - 识别
fetch()/XMLHttpRequest的字符串字面量或变量引用(限 HTML 内联脚本)
htmlquery 实战示例
doc, _ := htmlquery.LoadDoc(htmlBytes)
scripts := htmlquery.Find(doc, "//script[@src]")
for _, s := range scripts {
src := htmlquery.SelectAttr(s, "src") // 获取相对路径
base := htmlquery.SelectAttr(s.Parent, "base") // 尝试获取 base 标签
absPath := resolveURL(base, src) // 基于 base + src 构建绝对 Referer 上下文路径
}
htmlquery.SelectAttr() 安全提取属性值;s.Parent 向上追溯 <head> 或 <body>,用于定位 <base href>;resolveURL() 实现 RFC 3986 规范的相对路径解析,确保 Referer 路径可被服务端准确匹配。
支持的 Referer 路径类型对比
| 类型 | 示例 | 是否被 htmlquery 直接捕获 |
|---|---|---|
<script src="/js/app.js"> |
/js/app.js |
✅ |
<link href="css/style.css"> |
css/style.css |
✅ |
<script>fetch("api/data")</script> |
api/data |
❌(需额外 JS 解析) |
graph TD
A[加载 HTML 文档] --> B[htmlquery.Parse]
B --> C{遍历 script/link 节点}
C --> D[提取 src/href 属性]
C --> E[查找 base 标签]
D & E --> F[合成绝对 Referer 路径]
第四章:Token刷新机制与鉴权绕过实现
4.1 JWT/Session Token生命周期分析与时间戳校验绕过
时间戳校验的常见实现缺陷
JWT 的 exp(过期)、nbf(生效前)和 iat(签发时间)均依赖服务端时钟。若未同步 NTP 或存在时钟漂移,将导致校验逻辑失效。
绕过 exp 校验的典型路径
- 服务端未校验
exp字段(开发调试遗留) - 使用弱签名算法(如
none算法)篡改 payload - 时钟回拨攻击(客户端伪造低精度
exp值)
关键代码片段分析
// ❌ 危险:手动解析并忽略 exp 校验
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
if (payload.exp > Date.now() / 1000) { /* 未处理溢出或负值 */ }
⚠️ Date.now() / 1000 截断为整数,但 payload.exp 可能为字符串或超大整数;未验证 exp 是否为数字类型,易被 "9999999999" 等字符串绕过。
安全校验建议对比
| 检查项 | 风险等级 | 推荐方式 |
|---|---|---|
exp 类型校验 |
高 | typeof payload.exp === 'number' && isFinite(payload.exp) |
| 时钟容差设置 | 中 | 允许 ±30s 同步误差(非禁用) |
graph TD
A[收到JWT] --> B{Base64解码Header/Payload}
B --> C[验证签名]
C --> D[解析exp/nbf/iat]
D --> E[类型+范围+时钟偏移校验]
E -->|全部通过| F[授权访问]
E -->|任一失败| G[拒绝]
4.2 Go实现自动化Token轮询刷新与并发安全令牌池
核心设计原则
- 基于
sync.Map实现无锁读多写少的令牌缓存 - 利用
time.Ticker触发周期性健康检查与预刷新 - 所有写操作经
sync.RWMutex保护,读操作优先尝试无锁路径
并发安全令牌池结构
type TokenPool struct {
tokens sync.Map // key: serviceID, value: *TokenEntry
mu sync.RWMutex
ticker *time.Ticker
}
type TokenEntry struct {
Token string `json:"token"`
ExpiresAt time.Time `json:"expires_at"`
Refreshed time.Time `json:"refreshed_at"`
}
逻辑分析:
sync.Map降低高频读开销;TokenEntry内嵌过期时间与最后刷新时间,支持双阈值判断(如ExpiresAt.Add(-30s)触发预刷新)。ticker独立协程驱动轮询,避免阻塞主业务流。
刷新策略对比
| 策略 | 延迟 | 可靠性 | 实现复杂度 |
|---|---|---|---|
| 被动触发 | 高 | 中 | 低 |
| 主动轮询 | 低 | 高 | 中 |
| 混合预热 | 最低 | 最高 | 高 |
流程示意
graph TD
A[启动Ticker] --> B{距过期<60s?}
B -->|是| C[异步刷新Token]
B -->|否| D[跳过]
C --> E[更新sync.Map & Refreshed时间]
4.3 XSRF-Token与CSRF-Token双因子鉴权模拟实战
在现代Web安全实践中,单一Token防护易被绕过。双因子鉴权通过时间维度隔离(XSRF-Token)与会话维度绑定(CSRF-Token)协同防御。
双Token生成逻辑
import secrets, time
from hashlib import sha256
def generate_dual_tokens(session_id: str) -> dict:
# CSRF-Token:绑定session_id + 随机盐,持久化存储
csrf = sha256((session_id + secrets.token_hex(16)).encode()).hexdigest()[:32]
# XSRF-Token:一次性+时效性(60s),不存服务端
xsrf = secrets.token_urlsafe(16)
timestamp = int(time.time())
return {
"csrf": csrf,
"xsrf": f"{xsrf}.{timestamp}",
"expires": timestamp + 60
}
逻辑分析:csrf确保请求源自合法会话;xsrf含时间戳,客户端每次请求需重新签名验证时效性,服务端仅校验时间窗口,无状态存储。
校验流程
graph TD
A[客户端提交请求] --> B{携带XSRF-Token与CSRF-Token}
B --> C[校验CSRF-Token是否匹配当前session]
C --> D[解析XSRF-Token时间戳]
D --> E{是否在60s有效期内?}
E -->|是| F[允许请求]
E -->|否| G[拒绝并返回403]
关键参数说明
| 参数 | 类型 | 作用 |
|---|---|---|
csrf |
32字符哈希 | 会话级身份锚点,防重放 |
xsrf |
URL-safe随机串+时间戳 | 请求级时效凭证,防捕获重放 |
- 双Token必须同时校验通过才放行;
- XSRF-Token有效期严格控制在60秒内,超时即失效。
4.4 基于gorilla/sessions的本地Token缓存与失效回退策略
核心设计思想
将 gorilla/sessions 的内存存储(cookiestore 或 memstore)与 Token 生命周期解耦,利用 session 的 MaxAge 控制本地缓存时效,并在验证失败时自动触发后端令牌校验回退。
回退流程示意
graph TD
A[HTTP 请求携带 Session Cookie] --> B{Session 解密成功?}
B -->|是| C[读取 token 字段]
B -->|否/过期| D[清除 session 并重定向登录]
C --> E{Token 未过期且签名有效?}
E -->|否| F[调用 /auth/verify 接口校验]
F --> G[校验通过则刷新 session]
缓存配置示例
// 使用 memstore 实现无持久化的本地 Token 缓存
store := sessions.NewCookieStore([]byte("secret-key"))
store.Options = &sessions.Options{
MaxAge: 300, // 5分钟,与 JWT 的 short-lived token 对齐
Path: "/",
HttpOnly: true,
Secure: true, // 生产环境启用
}
MaxAge=300 确保客户端 Cookie 自动过期,强制触发服务端回退校验;HttpOnly 防止 XSS 窃取 session ID;Secure 保障仅 HTTPS 传输。
失效策略对比
| 策略 | 缓存位置 | 回退触发条件 | 适用场景 |
|---|---|---|---|
| Cookie-only | 客户端 | Cookie 过期或篡改 | 轻量级应用 |
| MemStore + Redis | 内存 + 远程 | 本地缓存缺失或校验失败 | 高并发需强一致性 |
- ✅ 优势:降低 Redis QPS 压力,提升首屏响应速度
- ⚠️ 注意:
memstore不支持分布式部署,多实例需切换为redisstore
第五章:MP4视频流封装与本地文件生成
MP4容器结构核心要素
MP4(MPEG-4 Part 14)并非单纯“视频格式”,而是一个基于Box(Atom)层级结构的容器规范。关键Box包括ftyp(文件类型)、moov(媒体元数据,含mvhd、trak、mdia等子Box)、mdat(原始音视频帧数据)。moov通常位于文件头部(fast-start模式),但实时流式封装时可能需延迟写入或采用moov前置策略以保障播放兼容性。
FFmpeg命令行实战封装流程
以下为从H.264裸流(stream.h264)与AAC音频(audio.aac)合成MP4的可复现命令:
ffmpeg -f h264 -i stream.h264 \
-f aac -i audio.aac \
-c:v copy -c:a copy \
-movflags +faststart \
-y output.mp4
其中-movflags +faststart将moov Box移至文件开头,使HTTP渐进式下载可立即播放;-c:v copy启用零拷贝复用,避免解码重编码开销。
基于MP4Box的流式分片生成
当处理长时直播录制时,需按时间切片生成独立MP4文件。使用MP4Box实现5秒分片:
MP4Box -add input.h264:fps=30 -add input.aac -split 5 output.mp4
该命令自动插入sidx(Segment Index)Box,并生成output_00001.mp4、output_00002.mp4等序列文件,适配HLS/DASH工作流。
Python调用libav实现动态封装
借助av库(Python绑定FFmpeg)在内存中构建MP4:
import av
container = av.open('output.mp4', mode='w')
stream_v = container.add_stream('h264', rate=30)
stream_a = container.add_stream('aac', rate=44100)
# 写入帧逻辑(省略具体帧获取)
for frame in video_frames:
for packet in stream_v.encode(frame):
container.mux(packet)
container.close()
关键参数对播放兼容性的影响
| 参数 | 推荐值 | 影响说明 |
|---|---|---|
moov位置 |
+faststart |
避免Web端首帧加载延迟超10s |
keyframe interval |
≤2s(60帧@30fps) | 确保DASH/HLS分片边界对齐I帧 |
timescale |
视频流:30000,音频流:44100 | 保证PTS/DTS精度,规避音画不同步 |
封装异常诊断清单
- 文件无法播放:检查
ftyp中major_brand是否为isom或mp42(非avc1等编码标识); - 播放卡顿:验证
mdat内NALU是否完整(无截断的0x00000001起始码); - 元数据丢失:确认
moov.trak.mdia.minf.stbl.stsd中avcCBox包含SPS/PPS二进制数据。
实际案例:安防摄像头RTSP流本地归档
某海康威视IPC通过rtsp://admin:pass@192.168.1.100:554/Streaming/Channels/101拉流,使用以下脚本持续生成带时间戳的MP4:
while true; do
ts=$(date +"%Y%m%d_%H%M%S")
ffmpeg -timeout 10000000 -i "rtsp://..." \
-t 300 \
-c:v libx264 -crf 23 -preset fast \
-c:a aac -b:a 128k \
-movflags +faststart \
"/archive/${ts}.mp4"
done
该方案已稳定运行18个月,单日生成约288个5分钟MP4文件,平均文件大小1.2GB,经VLC/QuickTime/Chrome全平台验证可播。
时间戳同步陷阱与修复
H.264裸流常缺失dts,直接copy会导致MP4中dts=pts引发播放器解码队列溢出。解决方案:强制生成单调递增DTS:
ffmpeg -i input.h264 -vf "settb=1/30,setpts=N/TB" -vsync vfr -f mp4 output.mp4 