代码初始化
This commit is contained in:
185
service/audio/audio_extract_service.go
Normal file
185
service/audio/audio_extract_service.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user