新增获取视频时长接口
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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)"`
|
||||
}
|
||||
@@ -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 // 默认竖屏
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user