视频保存

This commit is contained in:
duoaohui
2026-05-19 15:28:36 +08:00
parent 8e5770eb5d
commit c1c108e632
5 changed files with 35 additions and 8 deletions

View File

@@ -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 +faststartmoov 前置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",

View File

@@ -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)

View File

@@ -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
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

View File

@@ -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

View File

@@ -78,8 +78,11 @@ type MeetingConfig struct {
CleanupIntervalSeconds int `mapstructure:"cleanup_interval_seconds"` // 兜底扫描周期秒数,默认 30
// Phase Bmedia-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