diff --git a/backend/go-service/app/meeting/service/meeting_recording_service.go b/backend/go-service/app/meeting/service/meeting_recording_service.go index 32c36d6..7fd66c6 100644 --- a/backend/go-service/app/meeting/service/meeting_recording_service.go +++ b/backend/go-service/app/meeting/service/meeting_recording_service.go @@ -255,7 +255,9 @@ func (s *MeetingRecordingService) StopRecording(ctx context.Context, userID int6 } // 上传到 MinIO - objectKey := fmt.Sprintf("recordings/%s/%d-%s.mp4", strings.ToLower(code), rec.ID, rec.RemoteID) + // 后缀 .webm 与 media-server 实际产物保持一致 + // (VP8/Opus 不能装进 mp4,media-server 直接出 webm;后续如需 mp4 可异步转码) + objectKey := fmt.Sprintf("recordings/%s/%d-%s.webm", strings.ToLower(code), rec.ID, rec.RemoteID) fileURL, sizeBytes, uploadErr := s.uploadRecording(ctx, outputPath, objectKey) if uploadErr != nil { logs.Error(ctx, funcName, "录制文件上传 MinIO 失败", diff --git a/backend/go-service/app/transcribe/service/openai_compat_client.go b/backend/go-service/app/transcribe/service/openai_compat_client.go index eb11601..08d5efc 100644 --- a/backend/go-service/app/transcribe/service/openai_compat_client.go +++ b/backend/go-service/app/transcribe/service/openai_compat_client.go @@ -154,7 +154,7 @@ func (c *OpenAICompatibleClient) Transcribe(ctx context.Context, cfg *dao.STTCon // downloadAudio 从给定 URL 拉取音频内容,返回字节流 + 推断的文件名 // // 文件名仅作为 multipart 的 filename 参数,主要决定 Content-Type 推断; -// 取 URL path 末段,无后缀则默认 recording.mp4。 +// 取 URL path 末段,无后缀则默认 recording.webm(与 media-server 实际产物一致)。 func (c *OpenAICompatibleClient) downloadAudio(ctx context.Context, fileURL string) ([]byte, string, error) { req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil) if err != nil { diff --git a/media-server/src/services/recording.service.ts b/media-server/src/services/recording.service.ts index dc11a1b..2dfc129 100644 --- a/media-server/src/services/recording.service.ts +++ b/media-server/src/services/recording.service.ts @@ -3,7 +3,7 @@ * * 录制管线: * ┌─────────────┐ RTP/RTCP ┌──────────────┐ SDP+stdin ┌────────┐ fs ┌─────────┐ - * │ Producer X │ ───────────> │ PlainTransport│ ───────────────> │ ffmpeg │ ──────> │ *.mp4 │ + * │ Producer X │ ───────────> │ PlainTransport│ ───────────────> │ ffmpeg │ ──────> │ *.webm │ * │ (audio/video)│ │ + Consumer │ (UDP loopback) │ (子进程)│ └─────────┘ * └─────────────┘ └──────────────┘ └────────┘ * @@ -95,7 +95,7 @@ interface RecordingEntry { ffmpeg: ChildProcess; /** SDP 临时文件 */ sdpFilePath: string; - /** 输出 mp4 文件 */ + /** 输出 webm 文件(VP8 + Opus 原生容器,-c copy 零重编码) */ outputPath: string; /** ffmpeg 退出 promise,stop 时 await */ ffmpegExited: Promise; @@ -339,7 +339,10 @@ function buildSdp(tracks: RecordingTrack[], localIp: string): string { * -use_wallclock_as_timestamps 1:避免 RTP 时间戳跳变导致 mp4 时长异常 * -c copy:不重编码,直接复用 RTP 载荷 * - 优点:CPU 占用极低 - * - 缺点:浏览器侧 VP8 进 mp4 容器在某些播放器不支持;后续可改 libx264 + * - 限制:输出容器必须能装 VP8/Opus。mp4 muxer 不接受 VP8(ffmpeg 报 + * "Could not find tag for codec vp8" 后 exit code=1)。改用 webm 恶不必重编码 + * (VP8+Opus 是 webm 原生组合)。后续如需 mp4 可在 go-service 上传后异步 ffmpeg 转一次 + * -f webm:显式指定 webm muxer(依赖后缀推断也可,但显式更稳) * -y:覆盖已存在文件(防止 recordingId 碰撞) */ function buildFfmpegArgs(sdpFilePath: string, outputPath: string): string[] { @@ -360,6 +363,8 @@ function buildFfmpegArgs(sdpFilePath: string, outputPath: string): string[] { '0:v?', '-c', 'copy', + '-f', + 'webm', '-y', outputPath, ]; @@ -404,7 +409,8 @@ export async function startRecording(input: StartRecordingInput): Promise