From c1c108e63245d471fb637bfbb6c756aae04161f9 Mon Sep 17 00:00:00 2001 From: duoaohui <928970622@qq.com> Date: Tue, 19 May 2026 15:28:36 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A7=86=E9=A2=91=E4=BF=9D=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/meeting_recording_service.go | 24 ++++++++++++++++--- backend/go-service/app/provider/wire_gen.go | 4 ++-- backend/go-service/config/config.dev.yaml | 5 +++- backend/go-service/config/config.docker.yaml | 3 +++ backend/go-service/config/config.go | 7 ++++-- 5 files changed, 35 insertions(+), 8 deletions(-) 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 f12eba0..90be079 100644 --- a/backend/go-service/app/meeting/service/meeting_recording_service.go +++ b/backend/go-service/app/meeting/service/meeting_recording_service.go @@ -47,6 +47,7 @@ type MeetingRecordingService struct { minioClient *minio.Client minioCfg *config.MinioConfig + meetingCfg *config.MeetingConfig } // NewMeetingRecordingService Wire Provider @@ -59,6 +60,7 @@ func NewMeetingRecordingService( mediaOrchestrator MediaOrchestrator, minioClient *minio.Client, minioCfg *config.MinioConfig, + meetingCfg *config.MeetingConfig, ) *MeetingRecordingService { return &MeetingRecordingService{ roomDAO: roomDAO, @@ -69,6 +71,7 @@ func NewMeetingRecordingService( mediaOrchestrator: mediaOrchestrator, minioClient: minioClient, minioCfg: minioCfg, + meetingCfg: meetingCfg, } } @@ -510,6 +513,11 @@ func (s *MeetingRecordingService) prepareUploadArtifact(ctx context.Context, web if webmPath == "" { return webmPath, ".webm", "video/webm", "" } + if s.meetingCfg == nil || !s.meetingCfg.RecordingTranscodeEnabled { + logs.Info(ctx, funcName, "录制转码未启用,直接上传 webm", + zap.Int64("recording_id", recordingID), zap.String("webm_path", webmPath)) + return webmPath, ".webm", "video/webm", "" + } mp4Path, err := s.transcodeWebmToMp4(ctx, webmPath) if err != nil { @@ -535,16 +543,26 @@ func (s *MeetingRecordingService) prepareUploadArtifact(ctx context.Context, web // - -movflags +faststart:moov 前置,HTTP Range 即点即播 // - -y:覆盖目标文件,避免重复 stop 时 EEXIST func (s *MeetingRecordingService) transcodeWebmToMp4(ctx context.Context, webmPath string) (string, error) { - if _, err := exec.LookPath("ffmpeg"); err != nil { + ffmpegPath := "ffmpeg" + timeoutSec := 600 + if s.meetingCfg != nil { + if strings.TrimSpace(s.meetingCfg.RecordingTranscodeFFmpegPath) != "" { + ffmpegPath = strings.TrimSpace(s.meetingCfg.RecordingTranscodeFFmpegPath) + } + if s.meetingCfg.RecordingTranscodeTimeoutSecond > 0 { + timeoutSec = s.meetingCfg.RecordingTranscodeTimeoutSecond + } + } + if _, err := exec.LookPath(ffmpegPath); err != nil { return "", fmt.Errorf("ffmpeg 未安装或不在 PATH: %w", err) } mp4Path := strings.TrimSuffix(webmPath, filepath.Ext(webmPath)) + ".mp4" // 转码上限:单文件最长容忍 10 分钟 CPU 时间,超时则视为失败回退 - tctx, cancel := context.WithTimeout(ctx, 10*time.Minute) + tctx, cancel := context.WithTimeout(ctx, time.Duration(timeoutSec)*time.Second) defer cancel() - cmd := exec.CommandContext(tctx, "ffmpeg", + cmd := exec.CommandContext(tctx, ffmpegPath, "-y", "-i", webmPath, "-c:v", "libx264", "-preset", "veryfast", "-crf", "23", diff --git a/backend/go-service/app/provider/wire_gen.go b/backend/go-service/app/provider/wire_gen.go index 5fc5a37..edd750d 100644 --- a/backend/go-service/app/provider/wire_gen.go +++ b/backend/go-service/app/provider/wire_gen.go @@ -111,15 +111,15 @@ func InitializeApp(cfg *config.Config) (*App, error) { meetingRecordingDAO := dao6.NewMeetingRecordingDAO(gormDB) meetingBroadcaster := service7.NewMeetingBroadcaster(meetingParticipantDAO, pubSub) httpMediaOrchestrator := service7.NewHTTPMediaOrchestrator(cfg, client) + meetingConfig := provideMeetingConfig(cfg) meetingLifecycleService := service7.NewMeetingLifecycleService(meetingRoomDAO, meetingParticipantDAO, client, meetingBroadcaster, httpMediaOrchestrator, cfg) meetingService := service7.NewMeetingService(meetingRoomDAO, meetingParticipantDAO, meetingChatDAO, gormDB, client, meetingBroadcaster, notifyService, friendshipDAO, onlineService, httpMediaOrchestrator, meetingLifecycleService) meetingSignalService := service7.NewMeetingSignalService(meetingRoomDAO, meetingParticipantDAO, client, meetingBroadcaster, httpMediaOrchestrator, meetingLifecycleService) - meetingRecordingService := service7.NewMeetingRecordingService(meetingRoomDAO, meetingParticipantDAO, meetingRecordingDAO, client, meetingBroadcaster, httpMediaOrchestrator, minioClient, minioConfig) + meetingRecordingService := service7.NewMeetingRecordingService(meetingRoomDAO, meetingParticipantDAO, meetingRecordingDAO, client, meetingBroadcaster, httpMediaOrchestrator, minioClient, minioConfig, meetingConfig) // admin 会议管理服务在 recording service 构造后再创建,从而支持"强制结束录制"兜底 meetingManageService := service2.NewMeetingManageService(meetingManageDAO, meetingRecordingService) meetingManageController := controller2.NewMeetingManageController(meetingManageService) meetingController := controller7.NewMeetingController(meetingService) - meetingConfig := provideMeetingConfig(cfg) meetingRecordingController := controller7.NewMeetingRecordingController(meetingRecordingService, meetingConfig) meetingWSHandler := controller7.NewMeetingWSHandler(meetingSignalService, hub) meetingCleanupTask := task2.NewMeetingCleanupTask(meetingLifecycleService, meetingRoomDAO, meetingChatDAO) diff --git a/backend/go-service/config/config.dev.yaml b/backend/go-service/config/config.dev.yaml index c05cdea..c198f47 100644 --- a/backend/go-service/config/config.dev.yaml +++ b/backend/go-service/config/config.dev.yaml @@ -90,4 +90,7 @@ meeting: stale_room_hours: 4 # 活跃超过此小时且无活跃成员视为 stale,强制结束 # media-server 失败回调共享密钥:必须与 media-server 端 INTERNAL_WEBHOOK_SECRET 环境变量一致 # 留空时 /internal/meeting/recordings/failure 全部 403(默认安全) - internal_webhook_secret: internal-webhook-12xqs \ No newline at end of file + internal_webhook_secret: internal-webhook-12xqs + recording_transcode_enabled: true + recording_transcode_ffmpeg_path: /home/ykf/jenkins/yuyin/ffmpeg/ffmpeg-7.0.2-amd64-static/ffmpeg + recording_transcode_timeout_seconds: 600 \ No newline at end of file diff --git a/backend/go-service/config/config.docker.yaml b/backend/go-service/config/config.docker.yaml index b7ca305..b8a22e0 100644 --- a/backend/go-service/config/config.docker.yaml +++ b/backend/go-service/config/config.docker.yaml @@ -82,3 +82,6 @@ meeting: # media-server 失败回调共享密钥:必须与 docker-compose 中 media-server 服务的 INTERNAL_WEBHOOK_SECRET 一致 # 生产部署请通过环境变量 ECHOCHAT_MEETING_INTERNAL_WEBHOOK_SECRET 覆盖(viper.AutomaticEnv 已开启) internal_webhook_secret: internal-webhook-12xqs + recording_transcode_enabled: true + recording_transcode_ffmpeg_path: /home/ykf/jenkins/yuyin/ffmpeg/ffmpeg-7.0.2-amd64-static/ffmpeg + recording_transcode_timeout_seconds: 600 diff --git a/backend/go-service/config/config.go b/backend/go-service/config/config.go index 7fe4951..c31a539 100644 --- a/backend/go-service/config/config.go +++ b/backend/go-service/config/config.go @@ -78,8 +78,11 @@ type MeetingConfig struct { CleanupIntervalSeconds int `mapstructure:"cleanup_interval_seconds"` // 兜底扫描周期秒数,默认 30 // Phase B:media-server 失败回调 webhook 共享密钥,必须与 media-server 端 INTERNAL_WEBHOOK_SECRET 一致 // 留空时 /internal/meeting/recordings/failure 全部直接 403(默认安全) - InternalWebhookSecret string `mapstructure:"internal_webhook_secret"` - StaleRoomHours int `mapstructure:"stale_room_hours"` // 活跃超过此小时且无成员视为 stale,默认 4 + InternalWebhookSecret string `mapstructure:"internal_webhook_secret"` + RecordingTranscodeEnabled bool `mapstructure:"recording_transcode_enabled"` + RecordingTranscodeFFmpegPath string `mapstructure:"recording_transcode_ffmpeg_path"` + RecordingTranscodeTimeoutSecond int `mapstructure:"recording_transcode_timeout_seconds"` + StaleRoomHours int `mapstructure:"stale_room_hours"` // 活跃超过此小时且无成员视为 stale,默认 4 } // MediaServerConfig Node media-server 接入配置(Phase 2e-2 Task 7)