视频保存
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user