Files
web/server/handlers/promotion_transcode.go

198 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"context"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"go.mongodb.org/mongo-driver/v2/bson"
"yh_web/server/config"
)
func skipPromotionTranscode() bool {
v := strings.TrimSpace(os.Getenv("SKIP_PROMOTION_TRANSCODE"))
return v == "1" || strings.EqualFold(v, "true")
}
func ffmpegAvailable() bool {
_, err := exec.LookPath("ffmpeg")
return err == nil
}
func isMOVUnderPromotion(relPath string, ext string) bool {
if strings.ToLower(ext) != ".mov" {
return false
}
return strings.Contains(filepath.ToSlash(relPath), "/promotion/")
}
func mp4PathForMOV(movPath string) string {
return strings.TrimSuffix(movPath, filepath.Ext(movPath)) + ".mp4"
}
func needsTranscode(movPath, mp4Path string) bool {
mi, err1 := os.Stat(movPath)
if err1 != nil || mi.IsDir() {
return false
}
pi, err2 := os.Stat(mp4Path)
if err2 != nil {
return true
}
return mi.ModTime().After(pi.ModTime())
}
// runFFmpegMOVToMP4 将 mov 转为浏览器通用 mp4与前端/脚本参数一致)
func runFFmpegMOVToMP4(ctx context.Context, movPath, mp4Path string) error {
cmd := exec.CommandContext(ctx, "ffmpeg",
"-y", "-i", movPath,
"-c:v", "libx264", "-profile:v", "high", "-pix_fmt", "yuv420p",
"-c:a", "aac", "-b:a", "128k",
"-movflags", "+faststart",
mp4Path,
)
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("%w: %s", err, string(out))
}
return nil
}
func relPathFromUploadRoot(uploadRoot, fullPath string) (string, error) {
r, err := filepath.Rel(uploadRoot, fullPath)
if err != nil {
return "", err
}
return filepath.ToSlash(r), nil
}
// replaceMOVWithMP4InDB 将 site_assets 中对应 .mov 记录更新为 .mp4转码成功后调用
func replaceMOVWithMP4InDB(siteID, oldRelPath, mp4FullPath string, insertedID any) {
if config.MongoClient == nil {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
defer cancel()
coll := config.GetDB(config.DBName).Collection("site_assets")
newRel := strings.TrimSuffix(oldRelPath, filepath.Ext(oldRelPath)) + ".mp4"
fi, err := os.Stat(mp4FullPath)
if err != nil {
log.Printf("[promotion-transcode] stat mp4: %v", err)
return
}
set := bson.M{
"file_path": newRel,
"name": filepath.Base(newRel),
"size": fi.Size(),
"content_type": "video/mp4",
}
filter := bson.M{"site_id": siteID, "file_path": oldRelPath}
if oid, ok := insertedID.(bson.ObjectID); ok && !oid.IsZero() {
filter = bson.M{"_id": oid, "site_id": siteID}
}
_, err = coll.UpdateOne(ctx, filter, bson.M{"$set": set})
if err != nil {
log.Printf("[promotion-transcode] 更新数据库失败: %v", err)
}
}
// ScheduleTranscodeAfterUpload 上传保存成功后异步promotion 下 .mov -> .mp4并更新本条 site_assets
func ScheduleTranscodeAfterUpload(siteID, relPath, movFullPath string, insertedID any) {
if skipPromotionTranscode() || !isMOVUnderPromotion(relPath, filepath.Ext(movFullPath)) {
return
}
go func() {
if !ffmpegAvailable() {
log.Printf("[promotion-transcode] 已上传 .mov 但未安装 ffmpeg无法转码: %s", relPath)
return
}
mp4Full := mp4PathForMOV(movFullPath)
if !needsTranscode(movFullPath, mp4Full) {
return
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
defer cancel()
log.Printf("[promotion-transcode] 开始转码: %s -> %s", movFullPath, mp4Full)
if err := runFFmpegMOVToMP4(ctx, movFullPath, mp4Full); err != nil {
log.Printf("[promotion-transcode] 转码失败 %s: %v", relPath, err)
return
}
if err := os.Remove(movFullPath); err != nil {
log.Printf("[promotion-transcode] 删除原 .mov 失败(可手动删): %v", err)
}
replaceMOVWithMP4InDB(siteID, relPath, mp4Full, insertedID)
log.Printf("[promotion-transcode] 完成: %s", newRelLog(relPath))
}()
}
func newRelLog(oldRel string) string {
return strings.TrimSuffix(oldRel, filepath.Ext(oldRel)) + ".mp4"
}
// SweepPromotionTranscodeOnStartup 扫描 uploads/sites/*/promotion/**.mov补转码并同步数据库已有文件
func SweepPromotionTranscodeOnStartup() {
time.Sleep(3 * time.Second)
if skipPromotionTranscode() {
log.Println("[promotion-transcode] 启动扫描已跳过 SKIP_PROMOTION_TRANSCODE=1")
return
}
if !ffmpegAvailable() {
log.Println("[promotion-transcode] 启动扫描跳过:未找到 ffmpeg安装后可重启服务")
return
}
root := getUploadDir()
sitesDir := filepath.Join(root, "sites")
fi, err := os.Stat(sitesDir)
if err != nil || !fi.IsDir() {
return
}
entries, err := os.ReadDir(sitesDir)
if err != nil {
return
}
for _, e := range entries {
if !e.IsDir() {
continue
}
siteID := e.Name()
promoRoot := filepath.Join(sitesDir, siteID, "promotion")
_ = filepath.WalkDir(promoRoot, func(path string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
if strings.ToLower(filepath.Ext(path)) != ".mov" {
return nil
}
mp4Full := mp4PathForMOV(path)
if !needsTranscode(path, mp4Full) {
return nil
}
rel, err := relPathFromUploadRoot(root, path)
if err != nil {
return nil
}
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Hour)
log.Printf("[promotion-transcode] [启动补转] %s", rel)
err = runFFmpegMOVToMP4(ctx, path, mp4Full)
cancel()
if err != nil {
log.Printf("[promotion-transcode] [启动补转] 失败 %s: %v", rel, err)
return nil
}
_ = os.Remove(path)
replaceMOVWithMP4InDB(siteID, rel, mp4Full, nil)
log.Printf("[promotion-transcode] [启动补转] 完成 %s", newRelLog(rel))
return nil
})
}
}