代码初始化

This commit is contained in:
2026-05-19 14:33:06 +08:00
commit 219b7e39c7
18 changed files with 3311 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
package audio
import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/gogf/gf/v2/frame/g"
)
// AudioExtractService 音频提取服务
type AudioExtractService struct{}
// AudioExtract 音频提取服务单例
var AudioExtract = new(AudioExtractService)
// ExtractAudioReq 提取音频请求
type ExtractAudioReq struct {
VideoPath string // 视频文件路径
Format string // 输出音频格式,默认 mp3
}
// ExtractAudioRes 提取音频响应
type ExtractAudioRes struct {
AudioPath string // 提取后的音频文件路径
Duration string // 音频时长
Size int64 // 音频文件大小(bytes)
}
// Extract 从视频中提取音频
func (s *AudioExtractService) Extract(ctx context.Context, req *ExtractAudioReq) (res *ExtractAudioRes, err error) {
// 1. 校验视频文件存在
if _, err = os.Stat(req.VideoPath); os.IsNotExist(err) {
return nil, fmt.Errorf("视频文件不存在: %s", req.VideoPath)
}
// 2. 校验 ffmpeg 是否可用
ffmpegPath, err := s.getFFmpegPath()
if err != nil {
return nil, err
}
// 3. 确定输出格式
format := req.Format
if format == "" {
format = "mp3"
}
format = strings.TrimLeft(format, ".")
// 4. 生成输出文件路径
outputDir := filepath.Dir(req.VideoPath)
baseName := strings.TrimSuffix(filepath.Base(req.VideoPath), filepath.Ext(req.VideoPath))
timestamp := time.Now().UnixMilli()
outputName := fmt.Sprintf("%s_audio_%d.%s", baseName, timestamp, format)
outputPath := filepath.Join(outputDir, outputName)
g.Log().Infof(ctx, "开始提取音频: video=%s, output=%s", req.VideoPath, outputPath)
// 5. 构建 ffmpeg 命令
// 提取音频并转换为指定格式
args := []string{
"-i", req.VideoPath,
"-vn", // 去掉视频流
"-acodec", "libmp3lame", // 使用 mp3 编码器mp3格式
"-ab", "192k", // 音频比特率
"-ar", "44100", // 采样率
"-ac", "2", // 双声道
"-y", // 覆盖输出文件
outputPath,
}
// 如果输出不是 mp3调整编码器
switch format {
case "aac":
args[4] = "aac"
case "wav":
args[4] = "pcm_s16le"
args[5] = "-vn"
args = args[:8] // wav 不需要指定比特率等参数
args = append(args, outputPath)
case "ogg":
args[4] = "libvorbis"
case "flac":
args[4] = "flac"
}
cmd := exec.CommandContext(ctx, ffmpegPath, args...)
// 捕获输出用于调试
output, execErr := cmd.CombinedOutput()
if execErr != nil {
g.Log().Errorf(ctx, "ffmpeg 执行失败: %v, output: %s", execErr, string(output))
return nil, fmt.Errorf("音频提取失败: %v", execErr)
}
// 6. 验证输出文件
stat, statErr := os.Stat(outputPath)
if statErr != nil {
return nil, fmt.Errorf("音频文件生成失败: %v", statErr)
}
// 7. 获取音频时长(通过 ffprobe
duration, _ := s.getAudioDuration(ctx, ffmpegPath, outputPath)
g.Log().Infof(ctx, "音频提取成功: path=%s, size=%d, duration=%s", outputPath, stat.Size(), duration)
res = &ExtractAudioRes{
AudioPath: outputPath,
Duration: duration,
Size: stat.Size(),
}
return
}
// getFFmpegPath 获取 ffmpeg 可执行路径
func (s *AudioExtractService) getFFmpegPath() (string, error) {
// 1. 优先从配置读取
ffmpegPath := g.Cfg().MustGet(context.Background(), "ffmpeg.path", "").String()
if ffmpegPath != "" {
if _, err := os.Stat(ffmpegPath); err == nil {
return ffmpegPath, nil
}
}
// 2. 从 PATH 中查找
path, err := exec.LookPath("ffmpeg")
if err != nil {
return "", fmt.Errorf("未找到 ffmpeg请确保已安装 ffmpeg 或在配置中指定路径")
}
return path, nil
}
// getAudioDuration 获取音频时长
func (s *AudioExtractService) getAudioDuration(ctx context.Context, ffmpegPath string, audioPath string) (string, error) {
// 使用 ffprobe 获取时长
// 先尝试查找 ffprobe
ffprobePath := filepath.Join(filepath.Dir(ffmpegPath), "ffprobe")
if _, err := os.Stat(ffprobePath); os.IsNotExist(err) {
ffprobePath = "ffprobe"
}
cmd := exec.CommandContext(ctx, ffprobePath,
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
audioPath,
)
output, err := cmd.Output()
if err != nil {
return "", err
}
durationStr := strings.TrimSpace(string(output))
// 转换为人类可读格式: 秒 -> HH:MM:SS
var seconds float64
fmt.Sscanf(durationStr, "%f", &seconds)
hours := int(seconds) / 3600
minutes := (int(seconds) % 3600) / 60
secs := int(seconds) % 60
return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, secs), nil
}
// ExtractAndCleanup 提取音频并清理临时视频文件
func (s *AudioExtractService) ExtractAndCleanup(ctx context.Context, req *ExtractAudioReq) (res *ExtractAudioRes, err error) {
res, err = s.Extract(ctx, req)
if err != nil {
return nil, err
}
// 尝试删除原始视频文件
if req.VideoPath != "" {
if removeErr := os.Remove(req.VideoPath); removeErr != nil {
g.Log().Warningf(ctx, "删除临时视频文件失败: %v", removeErr)
}
}
return
}