视频保存
This commit is contained in:
@@ -255,7 +255,9 @@ func (s *MeetingRecordingService) StopRecording(ctx context.Context, userID int6
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 上传到 MinIO
|
// 上传到 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)
|
fileURL, sizeBytes, uploadErr := s.uploadRecording(ctx, outputPath, objectKey)
|
||||||
if uploadErr != nil {
|
if uploadErr != nil {
|
||||||
logs.Error(ctx, funcName, "录制文件上传 MinIO 失败",
|
logs.Error(ctx, funcName, "录制文件上传 MinIO 失败",
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ func (c *OpenAICompatibleClient) Transcribe(ctx context.Context, cfg *dao.STTCon
|
|||||||
// downloadAudio 从给定 URL 拉取音频内容,返回字节流 + 推断的文件名
|
// downloadAudio 从给定 URL 拉取音频内容,返回字节流 + 推断的文件名
|
||||||
//
|
//
|
||||||
// 文件名仅作为 multipart 的 filename 参数,主要决定 Content-Type 推断;
|
// 文件名仅作为 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) {
|
func (c *OpenAICompatibleClient) downloadAudio(ctx context.Context, fileURL string) ([]byte, string, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*
|
*
|
||||||
* 录制管线:
|
* 录制管线:
|
||||||
* ┌─────────────┐ RTP/RTCP ┌──────────────┐ SDP+stdin ┌────────┐ fs ┌─────────┐
|
* ┌─────────────┐ RTP/RTCP ┌──────────────┐ SDP+stdin ┌────────┐ fs ┌─────────┐
|
||||||
* │ Producer X │ ───────────> │ PlainTransport│ ───────────────> │ ffmpeg │ ──────> │ *.mp4 │
|
* │ Producer X │ ───────────> │ PlainTransport│ ───────────────> │ ffmpeg │ ──────> │ *.webm │
|
||||||
* │ (audio/video)│ │ + Consumer │ (UDP loopback) │ (子进程)│ └─────────┘
|
* │ (audio/video)│ │ + Consumer │ (UDP loopback) │ (子进程)│ └─────────┘
|
||||||
* └─────────────┘ └──────────────┘ └────────┘
|
* └─────────────┘ └──────────────┘ └────────┘
|
||||||
*
|
*
|
||||||
@@ -95,7 +95,7 @@ interface RecordingEntry {
|
|||||||
ffmpeg: ChildProcess;
|
ffmpeg: ChildProcess;
|
||||||
/** SDP 临时文件 */
|
/** SDP 临时文件 */
|
||||||
sdpFilePath: string;
|
sdpFilePath: string;
|
||||||
/** 输出 mp4 文件 */
|
/** 输出 webm 文件(VP8 + Opus 原生容器,-c copy 零重编码) */
|
||||||
outputPath: string;
|
outputPath: string;
|
||||||
/** ffmpeg 退出 promise,stop 时 await */
|
/** ffmpeg 退出 promise,stop 时 await */
|
||||||
ffmpegExited: Promise<void>;
|
ffmpegExited: Promise<void>;
|
||||||
@@ -339,7 +339,10 @@ function buildSdp(tracks: RecordingTrack[], localIp: string): string {
|
|||||||
* -use_wallclock_as_timestamps 1:避免 RTP 时间戳跳变导致 mp4 时长异常
|
* -use_wallclock_as_timestamps 1:避免 RTP 时间戳跳变导致 mp4 时长异常
|
||||||
* -c copy:不重编码,直接复用 RTP 载荷
|
* -c copy:不重编码,直接复用 RTP 载荷
|
||||||
* - 优点:CPU 占用极低
|
* - 优点: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 碰撞)
|
* -y:覆盖已存在文件(防止 recordingId 碰撞)
|
||||||
*/
|
*/
|
||||||
function buildFfmpegArgs(sdpFilePath: string, outputPath: string): string[] {
|
function buildFfmpegArgs(sdpFilePath: string, outputPath: string): string[] {
|
||||||
@@ -360,6 +363,8 @@ function buildFfmpegArgs(sdpFilePath: string, outputPath: string): string[] {
|
|||||||
'0:v?',
|
'0:v?',
|
||||||
'-c',
|
'-c',
|
||||||
'copy',
|
'copy',
|
||||||
|
'-f',
|
||||||
|
'webm',
|
||||||
'-y',
|
'-y',
|
||||||
outputPath,
|
outputPath,
|
||||||
];
|
];
|
||||||
@@ -404,7 +409,8 @@ export async function startRecording(input: StartRecordingInput): Promise<StartR
|
|||||||
const id = recordingId();
|
const id = recordingId();
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const localIp = '127.0.0.1';
|
const localIp = '127.0.0.1';
|
||||||
const outputPath = join(OUTPUT_DIR, `${id}.mp4`);
|
// 输出后缀 .webm:VP8 不能装进 mp4,webm 是 VP8+Opus 的原生容器,-c copy 零重编码
|
||||||
|
const outputPath = join(OUTPUT_DIR, `${id}.webm`);
|
||||||
const sdpFilePath = join(OUTPUT_DIR, `${id}.sdp`);
|
const sdpFilePath = join(OUTPUT_DIR, `${id}.sdp`);
|
||||||
|
|
||||||
// 已分配资源,用于失败时回滚
|
// 已分配资源,用于失败时回滚
|
||||||
|
|||||||
Reference in New Issue
Block a user