rtc_backend/docs/opus-preconvert-plan.md
repair-agent 134ccb70f3
All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 5m41s
fix 音频并发优化
2026-03-03 17:21:46 +08:00

5.9 KiB
Raw Permalink Blame History

故事音频预转码方案 — MP3 → Opus 预处理

创建时间2026-03-03 状态:待实施

Context

问题:当前 hw_service_go 每次播放故事都实时执行 MP3下载 → ffmpeg转码 → Opus编码ffmpeg 是 CPU 密集型操作,压测显示 0.5 核 CPU 下 5 个并发就首帧延迟 4.5s。

方案:在 TTS 生成 MP3 后,立即预转码为 Opus 帧数据JSON 格式)并上传 OSS。hw_service_go 播放时直接下载预处理好的 Opus 数据,跳过 ffmpeg首帧延迟从秒级降到毫秒级。

预期效果

  • hw_service_go 播放时 零 CPU 转码开销
  • 首帧延迟从 ~2s 降到 ~200ms
  • 并发播放容量从 5-10 个提升到 100+(瓶颈变为网络/内存)

压测数据参考(单 Pod, 0.5 核 CPU, 512Mi

并发故事数 首帧延迟 帧数/故事 错误
2 2.0s 796 0
5 4.5s 796 0
10 8.7s 796 0
20 17.4s 796 0

详见 压测报告


改动概览

改动范围 文件 改动大小
DjangoStory 模型 apps/stories/models.py 小(加 1 个字段)
DjangoTTS 服务 apps/stories/services/tts_service.py 中(加预转码逻辑)
Django故事 API apps/devices/views.py 小(返回新字段)
Django迁移文件 apps/stories/migrations/ 自动生成
GoAPI 响应结构体 hw_service_go/internal/rtcclient/client.go
Go播放处理器 hw_service_go/internal/handler/story.go 中(分支逻辑)
Go新增 Opus 下载 hw_service_go/internal/audio/ 中(新函数)

总改动量:中等偏小,核心改动集中在 3 个文件。


详细方案

Step 1: Story 模型加字段

文件apps/stories/models.py

# 在 Story 模型中新增
opus_url = models.URLField('Opus音频URL', max_length=500, blank=True, default='')

opus_url 存储预转码后的 Opus JSON 文件地址。为空表示未转码(兼容旧数据)。

然后 makemigrations + migrate

Step 2: TTS 服务中增加预转码

文件apps/stories/services/tts_service.py

在 MP3 上传 OSS 成功后(第 88 行 story.save 之前),增加:

  1. 调用 ffmpeg 将 MP3 bytes 转为 PCM16kHz, mono, s16le
  2. 用 Python opuslib或 subprocess 调 ffmpeg 直出 opus编码为 60ms 帧
  3. 将帧列表序列化为紧凑格式上传 OSS
  4. 保存 story.opus_url

Opus 数据格式JSON + base64

{
  "sample_rate": 16000,
  "channels": 1,
  "frame_duration_ms": 60,
  "frames": ["<base64帧1>", "<base64帧2>", ...]
}

一个 5 分钟故事约 5000 帧 × ~300 bytes/帧 ≈ 1.5MB JSON压缩后 ~1MB对 OSS 存储无压力。

转码实现subprocess 调 ffmpeg + opuslib

import subprocess, base64, json, opuslib

def convert_mp3_to_opus_frames(mp3_bytes):
    """MP3 → PCM → Opus 帧列表"""
    # ffmpeg: MP3 → PCM
    proc = subprocess.run(
        ['ffmpeg', '-i', 'pipe:0', '-ar', '16000', '-ac', '1', '-f', 's16le', 'pipe:1'],
        input=mp3_bytes, capture_output=True
    )
    pcm = proc.stdout

    # Opus 编码:每帧 960 samples (60ms @ 16kHz)
    encoder = opuslib.Encoder(16000, 1, opuslib.APPLICATION_AUDIO)
    frame_size = 960
    frames = []
    for i in range(0, len(pcm) // 2 - frame_size + 1, frame_size):
        chunk = pcm[i*2 : (i+frame_size)*2]
        opus_frame = encoder.encode(chunk, frame_size)
        frames.append(base64.b64encode(opus_frame).decode())

    return json.dumps({
        "sample_rate": 16000,
        "channels": 1,
        "frame_duration_ms": 60,
        "frames": frames
    })

上传路径:stories/audio-opus/YYYYMMDD/{uuid}.json

Step 3: Django API 返回 opus_url

文件apps/devices/views.pystories_by_mac 方法)

return success(data={
    'title': story.title,
    'audio_url': story.audio_url,
    'opus_url': story.opus_url,  # 新增
})

Step 4: Go 服务适配

文件hw_service_go/internal/rtcclient/client.go

type StoryInfo struct {
    Title    string `json:"title"`
    AudioURL string `json:"audio_url"`
    OpusURL  string `json:"opus_url"`  // 新增
}

文件hw_service_go/internal/audio/ — 新增函数

// FetchOpusFrames 从 OSS 下载预转码的 Opus JSON 文件,解析为帧列表
func FetchOpusFrames(ctx context.Context, opusURL string) ([][]byte, error)

文件hw_service_go/internal/handler/story.go — 修改播放逻辑

// 优先使用预转码 Opus
var frames [][]byte
if story.OpusURL != "" {
    frames, err = audio.FetchOpusFrames(ctx, story.OpusURL)
} else {
    // 兜底:旧数据无预转码,走实时转码
    frames, err = audio.MP3URLToOpusFrames(ctx, story.AudioURL)
}

Step 5: 历史数据迁移(可选)

写一个 management command 批量转码已有故事:

python manage.py convert_stories_to_opus

兼容性

  • 旧故事opus_url 为空hw_service_go 自动 fallback 到实时 ffmpeg 转码,无影响
  • 新故事TTS 生成时自动预转码hw_service_go 直接下载 Opus 数据
  • App 端:无任何改动,audio_urlMP3仍然存在供 App 播放器使用

依赖

  • Django 端需安装 opuslibPython Opus 绑定):pip install opuslib
  • Django 服务器需有 ffmpeg(已有,用于 TTS 后处理等)
  • 如果不想引入 opuslib 依赖,可以用 ffmpeg -c:a libopus 直接输出 opus但需要自行按 60ms 分帧

验证方法

  1. 本地创建一个故事 + TTS → 检查 opus_url 是否生成
  2. curl /api/v1/devices/stories/?mac_address=... 确认返回含 opus_url
  3. hw_service_go 本地启动,连接测试页面触发故事 → 确认跳过 ffmpeg
  4. 压测对比:相同并发下首帧延迟应从秒级降到百毫秒级