package audio import ( "context" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "time" ) // opusJSON 是 Django 预转码上传的 Opus JSON 文件结构。 type opusJSON struct { SampleRate int `json:"sample_rate"` Channels int `json:"channels"` FrameDurationMs int `json:"frame_duration_ms"` Frames []string `json:"frames"` // base64 编码的 Opus 帧 } // FetchOpusFrames 从 OSS 下载预转码的 Opus JSON 文件,解析为原始帧列表。 // 跳过 ffmpeg 实时转码,大幅降低 CPU 消耗和首帧延迟。 func FetchOpusFrames(ctx context.Context, opusURL string) ([][]byte, error) { httpCtx, cancel := context.WithTimeout(ctx, 60*time.Second) defer cancel() req, err := http.NewRequestWithContext(httpCtx, http.MethodGet, opusURL, nil) if err != nil { return nil, fmt.Errorf("audio: build opus request: %w", err) } resp, err := http.DefaultClient.Do(req) if err != nil { return nil, fmt.Errorf("audio: download opus json: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("audio: opus json status %d", resp.StatusCode) } body, err := io.ReadAll(io.LimitReader(resp.Body, 50*1024*1024)) // 50MB 上限 if err != nil { return nil, fmt.Errorf("audio: read opus json: %w", err) } var data opusJSON if err := json.Unmarshal(body, &data); err != nil { return nil, fmt.Errorf("audio: parse opus json: %w", err) } if len(data.Frames) == 0 { return nil, fmt.Errorf("audio: opus json has no frames") } frames := make([][]byte, 0, len(data.Frames)) for i, b64 := range data.Frames { raw, err := base64.StdEncoding.DecodeString(b64) if err != nil { return nil, fmt.Errorf("audio: decode frame %d: %w", i, err) } frames = append(frames, raw) } return frames, nil }