代码初始化
This commit is contained in:
@@ -1,27 +1,34 @@
|
||||
package video
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
commonHttp "gitea.com/red-future/common/http"
|
||||
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/glog"
|
||||
)
|
||||
|
||||
// ConcatService 视频拼接服务
|
||||
type ConcatService struct{}
|
||||
type concatService struct{}
|
||||
|
||||
// Concat 视频拼接服务单例
|
||||
var Concat = new(ConcatService)
|
||||
var Concat = new(concatService)
|
||||
|
||||
// ConcatReq 视频拼接请求
|
||||
type ConcatReq struct {
|
||||
VideoPaths []string // 视频文件路径列表(按此顺序拼接)
|
||||
OutputPath string // 输出视频文件路径,空则自动生成
|
||||
Method string // 拼接方式: auto/fast/reencode,默认 auto
|
||||
Upload bool // 是否上传到MinIO
|
||||
}
|
||||
|
||||
// ConcatRes 视频拼接响应
|
||||
@@ -32,10 +39,11 @@ type ConcatRes struct {
|
||||
DurationStr string `json:"durationStr"` // 可读时长
|
||||
MethodUsed string `json:"methodUsed"` // 实际使用的拼接方式
|
||||
InputFiles int `json:"inputFiles"` // 输入文件数
|
||||
FileURL string `json:"fileURL"` // MinIO访问地址(上传后返回)
|
||||
}
|
||||
|
||||
// Concat 拼接多个视频为一个
|
||||
func (s *ConcatService) Concat(ctx context.Context, req *ConcatReq) (res *ConcatRes, err error) {
|
||||
func (s *concatService) Concat(ctx context.Context, req *ConcatReq) (res *ConcatRes, err error) {
|
||||
if len(req.VideoPaths) < 2 {
|
||||
return nil, fmt.Errorf("至少需要2个视频才能拼接")
|
||||
}
|
||||
@@ -98,11 +106,21 @@ func (s *ConcatService) Concat(ctx context.Context, req *ConcatReq) (res *Concat
|
||||
MethodUsed: methodUsed,
|
||||
InputFiles: len(req.VideoPaths),
|
||||
}
|
||||
|
||||
// 如果需要上传到 MinIO
|
||||
if req.Upload {
|
||||
uploadRes, uploadErr := s.UploadToMinIO(ctx, outputPath)
|
||||
if uploadErr != nil {
|
||||
return nil, fmt.Errorf("上传到MinIO失败: %v", uploadErr)
|
||||
}
|
||||
res.FileURL = uploadRes.FileURL
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// concatByDemuxer 使用 concat demuxer 无损拼接(要求同编码参数)
|
||||
func (s *ConcatService) concatByDemuxer(ctx context.Context, ffmpegPath string, inputs []string, output string) error {
|
||||
func (s *concatService) concatByDemuxer(ctx context.Context, ffmpegPath string, inputs []string, output string) error {
|
||||
// 创建文件列表
|
||||
fileListPath := filepath.Join(filepath.Dir(output), "concat_list.txt")
|
||||
var lines []string
|
||||
@@ -132,7 +150,7 @@ func (s *ConcatService) concatByDemuxer(ctx context.Context, ffmpegPath string,
|
||||
}
|
||||
|
||||
// concatByFilter 使用 concat filter 重编码拼接(自动归一化分辨率/音频参数)
|
||||
func (s *ConcatService) concatByFilter(ctx context.Context, ffmpegPath string, inputs []string, output string) error {
|
||||
func (s *concatService) concatByFilter(ctx context.Context, ffmpegPath string, inputs []string, output string) error {
|
||||
n := len(inputs)
|
||||
|
||||
// 1. 获取所有视频的分辨率,确定统一输出尺寸
|
||||
@@ -211,7 +229,7 @@ func (s *ConcatService) concatByFilter(ctx context.Context, ffmpegPath string, i
|
||||
}
|
||||
|
||||
// getVideoResolution 获取视频分辨率
|
||||
func (s *ConcatService) getVideoResolution(ctx context.Context, ffmpegPath, videoPath string) (width, height int, err error) {
|
||||
func (s *concatService) getVideoResolution(ctx context.Context, ffmpegPath, videoPath string) (width, height int, err error) {
|
||||
ffprobePath := filepath.Join(filepath.Dir(ffmpegPath), "ffprobe")
|
||||
if _, err := os.Stat(ffprobePath); os.IsNotExist(err) {
|
||||
ffprobePath = "ffprobe"
|
||||
@@ -233,7 +251,7 @@ func (s *ConcatService) getVideoResolution(ctx context.Context, ffmpegPath, vide
|
||||
}
|
||||
|
||||
// getVideoDuration 获取视频时长
|
||||
func (s *ConcatService) getVideoDuration(ctx context.Context, ffmpegPath, videoPath string) (float64, error) {
|
||||
func (s *concatService) getVideoDuration(ctx context.Context, ffmpegPath, videoPath string) (float64, error) {
|
||||
ffprobePath := filepath.Join(filepath.Dir(ffmpegPath), "ffprobe")
|
||||
if _, err := os.Stat(ffprobePath); os.IsNotExist(err) {
|
||||
ffprobePath = "ffprobe"
|
||||
@@ -256,7 +274,7 @@ func (s *ConcatService) getVideoDuration(ctx context.Context, ffmpegPath, videoP
|
||||
return duration, nil
|
||||
}
|
||||
|
||||
func (s *ConcatService) getFFmpegPath() (string, error) {
|
||||
func (s *concatService) getFFmpegPath() (string, error) {
|
||||
ffmpegPath := g.Cfg().MustGet(context.Background(), "ffmpeg.path", "").String()
|
||||
if ffmpegPath != "" {
|
||||
if _, err := os.Stat(ffmpegPath); err == nil {
|
||||
@@ -277,6 +295,84 @@ func formatDuration(seconds float64) string {
|
||||
return fmt.Sprintf("%02d:%02d:%02d", h, m, s)
|
||||
}
|
||||
|
||||
// uploadFileRes 上传文件响应
|
||||
type uploadFileRes struct {
|
||||
FileURL string `json:"fileURL" dc:"上传地址"`
|
||||
FileSize int `json:"fileSize" dc:"文件大小"`
|
||||
FileName string `json:"fileName" dc:"文件名称"`
|
||||
FileFormat string `json:"fileFormat" dc:"文件格式"`
|
||||
FileAddressPrefix string `json:"fileAddressPrefix"`
|
||||
}
|
||||
|
||||
// UploadToMinIO 通过 OSS 微服务的 multipart 文件上传接口上传到 MinIO
|
||||
func (s *concatService) UploadToMinIO(ctx context.Context, localFilePath string) (*uploadFileRes, error) {
|
||||
// 手动构建 multipart/form-data 表单
|
||||
var buf bytes.Buffer
|
||||
mw := multipart.NewWriter(&buf)
|
||||
|
||||
file, err := os.Open(localFilePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("打开文件失败: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fw, err := mw.CreateFormFile("file", filepath.Base(localFilePath))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建表单文件字段失败: %v", err)
|
||||
}
|
||||
if _, err = io.Copy(fw, file); err != nil {
|
||||
return nil, fmt.Errorf("写入文件内容失败: %v", err)
|
||||
}
|
||||
mw.Close()
|
||||
|
||||
client := commonHttp.Httpclient.Clone()
|
||||
|
||||
// 透传认证 headers
|
||||
if r := g.RequestFromCtx(ctx); r != nil {
|
||||
for k, v := range r.Header {
|
||||
client.SetHeader(k, v[0])
|
||||
}
|
||||
}
|
||||
// 设置 multipart Content-Type(含 boundary)
|
||||
contentType := mw.FormDataContentType()
|
||||
g.Log().Debugf(ctx, "[UploadToMinIO] Content-Type: %s", contentType)
|
||||
client.SetHeader("Content-Type", contentType)
|
||||
|
||||
// 打印请求信息
|
||||
postBytes := buf.Bytes()
|
||||
g.Log().Debugf(ctx, "[UploadToMinIO] 请求URL: oss/file/uploadFile, 文件: %s, Body大小: %d bytes, Boundary: %s",
|
||||
localFilePath, len(postBytes), mw.Boundary())
|
||||
|
||||
response, err := client.Post(ctx, "oss/file/uploadFile", postBytes)
|
||||
if err != nil {
|
||||
glog.Error(ctx, err)
|
||||
return nil, fmt.Errorf("调用OSS上传服务失败: %v", err)
|
||||
}
|
||||
defer response.Close()
|
||||
|
||||
body := response.ReadAll()
|
||||
|
||||
// 调试:打印原始响应
|
||||
g.Log().Debugf(ctx, "[UploadToMinIO] OSS原始响应: %s", string(body))
|
||||
|
||||
// 解析标准 GoFrame 响应格式 {code, message, data}
|
||||
var apiResp struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Data *uploadFileRes `json:"data"`
|
||||
}
|
||||
if err = json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("响应解析失败: %v", err)
|
||||
}
|
||||
|
||||
if apiResp.Code != 200 && apiResp.Code != 0 {
|
||||
return nil, fmt.Errorf("OSS上传失败: %s", apiResp.Message)
|
||||
}
|
||||
|
||||
g.Log().Infof(ctx, "[UploadToMinIO] 上传成功 fileURL=%s size=%d", apiResp.Data.FileURL, apiResp.Data.FileSize)
|
||||
return apiResp.Data, nil
|
||||
}
|
||||
|
||||
// CleanupConcat 清理输入视频文件
|
||||
func CleanupConcat(paths []string) {
|
||||
for _, p := range paths {
|
||||
|
||||
Reference in New Issue
Block a user