【直播切片工作台】第22章:素材下载与 URL 重写

6 阅读4分钟

第22章:素材下载与 URL 重写

本章分析 internal/pkg/utils/download.go 与 internal/pkg/utils/urlrewrite:文件下载器的实现要点(流式落盘、错误包装)、Host 精确映射的改写器(让公网 URL 走内网直连),以及 Rewriter 如何从配置构造并注入下载链路。素材无论进 ASR 还是进草稿流水线,第一步都是「把它下下来」,本章是所有流水线的公共第一步。

流程

下载链路在 main.go 装配:rewriter := cfg.Download.URLRewriter() 从下载配置构造 Rewriter → downloader := service.NewFileDownloader(logger, rewriter) → 注入 ASR 音频预处理器与草稿 Generator。执行时:调用方给出 URL 与目标路径 → 下载器先 rewriter.Rewrite(url) 尝试改写 Host → 用改写后(或原样)的 URL 发起 HTTP GET → 流式拷贝到本地文件 → 返回实际落盘路径。

URL 重写的动机是混合网络拓扑:素材的公网 URL(如对象存储的签名地址)对公网用户可访问,但服务器自己下载时走公网既慢又费流量。download.url_rewriter.exact 配置一张 Host 映射表(example.com → internal.example.com),下载器出站前把 Host 精确替换,path 与 query 原样保留——签名 URL 的 query 参数是跟 path 绑定的,任何多余改动都会让签名失效,这正是「只动 Host」规则的技术根源。

实现

urlrewrite.Rewriter 的实现特点:

  • 精确匹配而非通配/正则:exact map[string]string,只替换完全相等的 hostname。配置面小、行为可预测,不会出现「*.cdn.com 误伤业务域名」的事故。
  • hostname 与 port 分离处理:hostnameOnly 用 net.SplitHostPort 剥离端口取 hostname 查表,命中后 replaceHostname 把端口拼回去——cdn.example.com:8443 → cdn-internal.example.com:8443,端口语义保留。
  • normalizeHost 的宽容解析:配置里写 https://a.com/、a.com:80 都能归一化成 hostname(剥协议前缀、剥路径),配置容错强。
  • 三处防御性返回:无规则、URL 解析失败、Host 为空时都原样返回(return raw, false)——改写器失败永远不阻断下载,退化为公网直连,可用性优先。
  • Empty() 判定:调用方可以探测「是否配置了改写」,日志里可区分「走了内网/走了公网」。

FileDownloader(service/live_material_download.go 同族实现)的要点:

  • HTTP 响应流直接 io.Copy 到目标文件,不读进内存——直播回放动辄数 GB,全量缓冲会 OOM;
  • 错误包装带上下文(URL、目标路径),排障时第一现场完整;
  • 下载前确保父目录存在(staging 目录由调用方创建,下载器做兜底)。

配置侧:DownloadConfig(config/download.go)持有一组 from → to 映射并暴露 URLRewriter() 构造方法,download_test.go 覆盖了协议前缀、端口、无匹配、非法 URL 等分支——一个 89 行的包配了完整测试,因为它站在所有数据入口的关键路径上。

📌 设计决策

  • Host 精确映射而非透明代理:无需额外进程、无需 TLS 信任链变更;代价是每换一个源站要加一条配置。对源站数量有限(自家对象存储 + 少数直播平台)的场景是最优解。
  • 改写失败静默降级:重写器是优化器而非守门员,任何异常都不应让下载失败。
  • 只改写出站下载,不改写入站 API:入站请求的 Host 处理交给 nginx(第 50 章),职责分界清晰。

代码示例

Rewriter 的核心两跳(查表 + 端口保留):

// internal/pkg/utils/urlrewrite/rewrite.go(节选)
func (r *Rewriter) Rewrite(raw string) (string, bool) {
    if r == nil || r.Empty() {
        return raw, false
    }
    u, err := url.Parse(raw)
    if err != nil || u.Host == "" {
        return raw, false // 解析失败原样返回,不阻断下载
    }
    hostname := hostnameOnly(u.Host)          // 剥端口取 hostname
    mapped, ok := r.exact[hostname]           // 精确查表
    if !ok {
        return raw, false
    }
    u.Host = replaceHostname(u.Host, mapped)  // 端口拼回
    return u.String(), true
}

配置到下载器的装配(main.go):

// cmd/webserver/main.go(节选)
rewriter := cfg.Download.URLRewriter()          // exact: {公网Host: 内网Host}
downloader := service.NewFileDownloader(logger, rewriter)
// downloader 随后注入:
//   ASR 音频预处理器(抽音频前先拉源视频)
//   draft.Generator(prepare 流水线下载 source.mp4)

小结

  • 下载链路 = Rewriter(Host 精确映射,失败静默)+ FileDownloader(流式落盘)。
  • 只动 Host 不动 path/query 是签名 URL 兼容的硬约束。
  • 改写器是优化器:公网直连是永远的兜底路径。

思考题

  1. 若源站使用 IP 直连更优(无域名),Host 映射方案需要怎么扩展?
  2. 下载器如何补「断点续传」与「哈希校验」两个能力?各自的失败语义如何设计?

项目信息

  • GitHub仓库:Chyona/live-mixer