All checks were successful
Build and Deploy Backend / build-and-deploy (push) Successful in 5m41s
67 lines
1.8 KiB
Go
67 lines
1.8 KiB
Go
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
|
|
}
|