视频保存

This commit is contained in:
duoaohui
2026-05-19 18:08:52 +08:00
parent 6784616411
commit 44c75c9c64
4 changed files with 58 additions and 22 deletions

View File

@@ -19,6 +19,7 @@ import (
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"github.com/echochat/backend/config" "github.com/echochat/backend/config"
"github.com/echochat/backend/pkg/logs" "github.com/echochat/backend/pkg/logs"
"github.com/echochat/backend/pkg/storage"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
"go.uber.org/zap" "go.uber.org/zap"
@@ -259,12 +260,5 @@ func (s *FileService) UploadVoice(ctx context.Context, fileHeader *multipart.Fil
// buildURL 根据 MinIO 配置构建文件访问 URL // buildURL 根据 MinIO 配置构建文件访问 URL
func (s *FileService) buildURL(objectName string) string { func (s *FileService) buildURL(objectName string) string {
if base := strings.TrimRight(strings.TrimSpace(s.minioCfg.PublicBaseURL), "/"); base != "" { return storage.BuildObjectURL(s.minioCfg, objectName)
return fmt.Sprintf("%s/%s", base, objectName)
}
scheme := "http"
if s.minioCfg.UseSSL {
scheme = "https"
}
return fmt.Sprintf("%s://%s/%s/%s", scheme, s.minioCfg.Endpoint, s.minioCfg.Bucket, objectName)
} }

View File

@@ -16,6 +16,7 @@ import (
"github.com/echochat/backend/app/meeting/model" "github.com/echochat/backend/app/meeting/model"
"github.com/echochat/backend/config" "github.com/echochat/backend/config"
"github.com/echochat/backend/pkg/logs" "github.com/echochat/backend/pkg/logs"
"github.com/echochat/backend/pkg/storage"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"go.uber.org/zap" "go.uber.org/zap"
@@ -503,14 +504,7 @@ func (s *MeetingRecordingService) uploadRecording(ctx context.Context, localPath
} }
func (s *MeetingRecordingService) buildRecordingURL(objectKey string) string { func (s *MeetingRecordingService) buildRecordingURL(objectKey string) string {
if base := strings.TrimRight(strings.TrimSpace(s.minioCfg.PublicBaseURL), "/"); base != "" { return storage.BuildObjectURL(s.minioCfg, objectKey)
return fmt.Sprintf("%s/%s", base, objectKey)
}
scheme := "http"
if s.minioCfg.UseSSL {
scheme = "https"
}
return fmt.Sprintf("%s://%s/%s/%s", scheme, s.minioCfg.Endpoint, s.minioCfg.Bucket, objectKey)
} }
// prepareUploadArtifact 决定最终上传到 MinIO 的产物 // prepareUploadArtifact 决定最终上传到 MinIO 的产物
@@ -542,8 +536,30 @@ func (s *MeetingRecordingService) prepareUploadArtifact(ctx context.Context, web
zap.Int64("recording_id", recordingID), zap.String("webm_path", webmPath), zap.Error(err)) zap.Int64("recording_id", recordingID), zap.String("webm_path", webmPath), zap.Error(err))
return webmPath, ".webm", "video/webm", "" return webmPath, ".webm", "video/webm", ""
} }
webmStat, webmStatErr := os.Stat(webmPath)
mp4Stat, mp4StatErr := os.Stat(mp4Path)
if webmStatErr == nil && mp4StatErr == nil {
if mp4Stat.Size() >= webmStat.Size() {
logs.Warn(ctx, funcName, "MP4 转码产物未变小,回退上传原始 webm",
zap.Int64("recording_id", recordingID),
zap.String("webm_path", webmPath),
zap.Int64("webm_size_bytes", webmStat.Size()),
zap.String("mp4_path", mp4Path),
zap.Int64("mp4_size_bytes", mp4Stat.Size()))
return webmPath, ".webm", "video/webm", mp4Path
}
logs.Info(ctx, funcName, "WebM→MP4 转码完成",
zap.Int64("recording_id", recordingID),
zap.String("mp4_path", mp4Path),
zap.Int64("webm_size_bytes", webmStat.Size()),
zap.Int64("mp4_size_bytes", mp4Stat.Size()))
return mp4Path, ".mp4", "video/mp4", mp4Path
}
logs.Info(ctx, funcName, "WebM→MP4 转码完成", logs.Info(ctx, funcName, "WebM→MP4 转码完成",
zap.Int64("recording_id", recordingID), zap.String("mp4_path", mp4Path)) zap.Int64("recording_id", recordingID),
zap.String("mp4_path", mp4Path),
zap.NamedError("webm_stat_error", webmStatErr),
zap.NamedError("mp4_stat_error", mp4StatErr))
return mp4Path, ".mp4", "video/mp4", mp4Path return mp4Path, ".mp4", "video/mp4", mp4Path
} }
@@ -555,8 +571,8 @@ func (s *MeetingRecordingService) prepareUploadArtifact(ctx context.Context, web
// - 转码产物不存在或大小为 0 // - 转码产物不存在或大小为 0
// //
// 参数选型: // 参数选型:
// - -c:v libx264 -preset veryfast -crf 23常见 1080p 短会议清晰度/CPU 折中 // - -c:v libx264 -preset medium -crf 30压缩优先避免 WebM 转 MP4 后体积变大
// - -c:a aac -b:a 128k覆盖绝大多数浏览器原生播放 // - -c:a aac -b:a 64k会议语音场景的体积/清晰度折中
// - -movflags +faststartmoov 前置HTTP Range 即点即播 // - -movflags +faststartmoov 前置HTTP Range 即点即播
// - -y覆盖目标文件避免重复 stop 时 EEXIST // - -y覆盖目标文件避免重复 stop 时 EEXIST
func (s *MeetingRecordingService) transcodeWebmToMp4(ctx context.Context, webmPath string) (string, error) { func (s *MeetingRecordingService) transcodeWebmToMp4(ctx context.Context, webmPath string) (string, error) {
@@ -587,8 +603,8 @@ func (s *MeetingRecordingService) transcodeWebmToMp4(ctx context.Context, webmPa
cmd := exec.CommandContext(tctx, ffmpegPath, cmd := exec.CommandContext(tctx, ffmpegPath,
"-y", "-y",
"-i", webmPath, "-i", webmPath,
"-c:v", "libx264", "-preset", "veryfast", "-crf", "23", "-c:v", "libx264", "-preset", "medium", "-crf", "30",
"-c:a", "aac", "-b:a", "128k", "-c:a", "aac", "-b:a", "64k",
"-movflags", "+faststart", "-movflags", "+faststart",
mp4Path, mp4Path,
) )

View File

@@ -44,7 +44,7 @@ log:
minio: minio:
endpoint: "minio:9000" endpoint: "minio:9000"
access_key: "echochat" access_key: "echochat"
secret_key: "echochat123456" secret_key: "echochat_2026-1s32dswW@#"
bucket: "echochat" bucket: "echochat"
use_ssl: false use_ssl: false
public_base_url: "http://yl.z64.cn/echochat" public_base_url: "http://yl.z64.cn/echochat"

View File

@@ -5,6 +5,7 @@ package storage
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
"github.com/echochat/backend/config" "github.com/echochat/backend/config"
"github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7"
@@ -61,3 +62,28 @@ func ensureBucketPublicRead(ctx context.Context, client *minio.Client, bucket st
}`, bucket) }`, bucket)
return client.SetBucketPolicy(ctx, bucket, policy) return client.SetBucketPolicy(ctx, bucket, policy)
} }
func BuildObjectURL(cfg *config.MinioConfig, objectName string) string {
objectName = strings.TrimLeft(strings.TrimSpace(objectName), "/")
if cfg == nil {
return objectName
}
bucket := strings.Trim(strings.TrimSpace(cfg.Bucket), "/")
if base := strings.TrimRight(strings.TrimSpace(cfg.PublicBaseURL), "/"); base != "" {
if bucket != "" && !strings.HasSuffix(base, "/"+bucket) {
base = fmt.Sprintf("%s/%s", base, bucket)
}
if objectName == "" {
return base
}
return fmt.Sprintf("%s/%s", base, objectName)
}
scheme := "http"
if cfg.UseSSL {
scheme = "https"
}
if objectName == "" {
return fmt.Sprintf("%s://%s/%s", scheme, cfg.Endpoint, bucket)
}
return fmt.Sprintf("%s://%s/%s/%s", scheme, cfg.Endpoint, bucket, objectName)
}