新增获取视频时长接口

This commit is contained in:
lmk
2026-07-01 16:52:33 +08:00
parent 09a3637b56
commit b740e400c9
5 changed files with 192 additions and 6 deletions
@@ -0,0 +1,26 @@
package video
import (
"context"
dto "media/model/dto/video"
service "media/service/video"
"github.com/gogf/gf/v2/frame/g"
)
// GetDuration 获取视频时长 POST /video/duration
// 方法挂载在 video struct 上,利用 Concat 已注册的 `/video` 组前缀
func (c *video) GetDuration(ctx context.Context, req *dto.VideoDurationReq) (res *dto.VideoDurationRes, err error) {
ctx = withUser(ctx)
g.Log().Infof(ctx, "[视频时长] 收到请求: video_urls=%v", req.VideoURLs)
res, err = service.VideoDuration.GetVideoDurations(ctx, req.VideoURLs)
if err != nil {
g.Log().Errorf(ctx, "[视频时长] 获取失败: %v", err)
return nil, err
}
g.Log().Infof(ctx, "[视频时长] 响应: count=%d, total=%s", res.Count, res.TotalDurationStr)
return res, nil
}
+25
View File
@@ -0,0 +1,25 @@
package video
import "github.com/gogf/gf/v2/frame/g"
// VideoDurationReq 视频时长查询请求
type VideoDurationReq struct {
g.Meta `path:"/duration" method:"post" tags:"视频时长" summary:"获取视频时长" dc:"传入一个或多个视频URL,返回每个视频的时长及总时长"`
VideoURLs []string `json:"video_urls" v:"required#视频URL列表不能为空" dc:"视频URL列表"`
}
// VideoInfo 单个视频时长信息
type VideoInfo struct {
Index int `json:"index" dc:"序号"`
VideoURL string `json:"videoUrl" dc:"视频URL"`
Duration float64 `json:"duration" dc:"时长(秒)"`
DurationStr string `json:"durationStr" dc:"可读时长(HH:MM:SS)"`
}
// VideoDurationRes 视频时长查询响应
type VideoDurationRes struct {
Videos []VideoInfo `json:"videos" dc:"视频时长列表"`
Count int `json:"count" dc:"视频数量"`
TotalDuration float64 `json:"totalDuration" dc:"总时长(秒)"`
TotalDurationStr string `json:"totalDurationStr" dc:"可读总时长(HH:MM:SS)"`
}
+6 -6
View File
@@ -603,8 +603,8 @@ func (s *captionService) concatVideos(ctx context.Context, videoPaths []string,
// getVideoRealDuration 用 ffprobe 获取视频时长(秒,float)
func getVideoRealDuration(ctx context.Context, videoPath string) float64 {
ffprobePath, err := exec.LookPath("ffprobe")
if err != nil {
ffprobePath := lookupFFprobePath()
if ffprobePath == "" {
g.Log().Warningf(ctx, "[ffprobe] ⚠ 未找到,无法获取视频时长")
return 0
}
@@ -637,8 +637,8 @@ func getVideoRealDuration(ctx context.Context, videoPath string) float64 {
// getVideoDurationStr 获取视频时长可读字符串
func getVideoDurationStr(ctx context.Context, videoPath string) string {
ffprobePath, err := exec.LookPath("ffprobe")
if err != nil {
ffprobePath := lookupFFprobePath()
if ffprobePath == "" {
g.Log().Warningf(ctx, "[ffprobe] ⚠ 未找到,无法获取视频时长字符串")
return ""
}
@@ -676,8 +676,8 @@ func getVideoDurationStr(ctx context.Context, videoPath string) string {
// getVideoResolution 使用 ffprobe 获取视频分辨率
func getVideoResolution(ctx context.Context, videoPath string) (width, height int) {
ffprobePath, err := exec.LookPath("ffprobe")
if err != nil {
ffprobePath := lookupFFprobePath()
if ffprobePath == "" {
g.Log().Warningf(ctx, "[ffprobe] ⚠ 未找到,使用默认分辨率 1080x1920")
return 1080, 1920 // 默认竖屏
}
+27
View File
@@ -452,6 +452,33 @@ func cleanupFiles(paths []string) {
}
}
// lookupFFprobePath 查找 ffprobe 可执行文件路径
// 优先查找 ffmpeg.path 配置同目录下的 ffprobe,回退到系统 PATH
func lookupFFprobePath() string {
ctx := context.Background()
ffmpegCfgPath := g.Cfg().MustGet(ctx, "ffmpeg.path", "").String()
if ffmpegCfgPath != "" {
// 尝试在 ffmpeg 同目录下找 ffprobe
ffprobeCandidate := filepath.Join(filepath.Dir(ffmpegCfgPath), "ffprobe")
if _, err := os.Stat(ffprobeCandidate); err == nil {
g.Log().Infof(ctx, "[ffprobe] ✔ 使用 ffmpeg.path 同目录下的 ffprobe: %s", ffprobeCandidate)
return ffprobeCandidate
}
// Windows: 尝试加 .exe
ffprobeCandidateExe := ffprobeCandidate + ".exe"
if _, err := os.Stat(ffprobeCandidateExe); err == nil {
g.Log().Infof(ctx, "[ffprobe] ✔ 使用 ffmpeg.path 同目录下的 ffprobe.exe: %s", ffprobeCandidateExe)
return ffprobeCandidateExe
}
}
path, err := exec.LookPath("ffprobe")
if err == nil {
return path
}
g.Log().Warningf(ctx, "[ffprobe] ⚠ 未找到 ffprobe")
return ""
}
// lookupFFmpegPath 查找 ffmpeg 可执行文件路径
func lookupFFmpegPath() (string, error) {
ctx := context.Background()
+108
View File
@@ -0,0 +1,108 @@
package video
import (
"context"
"fmt"
"os"
"path/filepath"
"sync"
"time"
dto "media/model/dto/video"
"github.com/gogf/gf/v2/frame/g"
)
type videoDurationService struct{}
// VideoDuration 视频时长服务单例
var VideoDuration = new(videoDurationService)
// GetVideoDurations 并发获取多个视频的时长,返回每个视频的时长信息及总时长
func (s *videoDurationService) GetVideoDurations(ctx context.Context, urls []string) (*dto.VideoDurationRes, error) {
if len(urls) == 0 {
return nil, fmt.Errorf("视频URL列表不能为空")
}
tempDir := g.Cfg().MustGet(ctx, "ffmpeg.temp_dir", "resource/temp").String()
if tempDir == "" {
tempDir = "resource/temp"
}
if !filepath.IsAbs(tempDir) {
absDir, _ := filepath.Abs(tempDir)
tempDir = absDir
}
os.MkdirAll(tempDir, 0755)
g.Log().Infof(ctx, "[视频时长] 开始获取 %d 个视频时长", len(urls))
type itemResult struct {
index int
url string
duration float64
durationStr string
err error
}
results := make([]itemResult, len(urls))
sem := make(chan struct{}, 5) // 最大并发数 5
var wg sync.WaitGroup
for i, u := range urls {
wg.Add(1)
i, u := i, u
go func() {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
// 使用独立 context(带超时),不受请求 context 生命周期影响
dlCtx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
localPath, dlErr := downloadFile(dlCtx, u, tempDir)
if dlErr != nil {
g.Log().Warningf(ctx, "[视频时长] 下载失败 index=%d, url=%s, err=%v", i+1, u, dlErr)
results[i] = itemResult{index: i, url: u, err: dlErr}
return
}
duration := getVideoRealDuration(ctx, localPath)
durationStr := formatDuration(duration)
os.Remove(localPath)
results[i] = itemResult{index: i, url: u, duration: duration, durationStr: durationStr}
}()
}
wg.Wait()
var videos []dto.VideoInfo
var total float64
for _, r := range results {
if r.err != nil {
continue
}
videos = append(videos, dto.VideoInfo{
Index: r.index + 1,
VideoURL: r.url,
Duration: r.duration,
DurationStr: r.durationStr,
})
total += r.duration
}
if len(videos) == 0 {
return nil, fmt.Errorf("所有视频下载或时长获取失败")
}
totalStr := formatDuration(total)
g.Log().Infof(ctx, "[视频时长] 完成: 成功=%d, 总时长=%s", len(videos), totalStr)
return &dto.VideoDurationRes{
Videos: videos,
Count: len(videos),
TotalDuration: total,
TotalDurationStr: totalStr,
}, nil
}