feat(app): add internal components batch 6
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type mergeMediaInfo struct {
|
||||
width, height int
|
||||
audio bool
|
||||
duration time.Duration
|
||||
videoCodec, audioCodec string
|
||||
videoExtra, audioExtra string
|
||||
videoTB, audioTB string
|
||||
videoProfile, audioProfile string
|
||||
pixelFormat, sar, layout string
|
||||
sampleRate int
|
||||
frameRate float64
|
||||
reorder bool
|
||||
}
|
||||
|
||||
var mergeVideoDescription = regexp.MustCompile(`Video: [a-zA-Z0-9_]+(?: \(([^)]*)\))?`)
|
||||
var mergeAudioDescription = regexp.MustCompile(`Audio: [a-zA-Z0-9_]+(?: \(([^)]*)\))?`)
|
||||
var mergePixelFormat = regexp.MustCompile(`, ([a-zA-Z0-9_]+)(?:\([^)]*\))?, \d+x\d+`)
|
||||
var mergeFrameRate = regexp.MustCompile(`, ([0-9.]+) (?:fps|tbr)`)
|
||||
var mergeStreamIndex = regexp.MustCompile(`Stream #0:(\d+)`)
|
||||
|
||||
func probeMergeMedia(ctx context.Context, ffmpeg, path string) (mergeMediaInfo, error) {
|
||||
probeCtx, cancel := context.WithTimeout(ctx, 20*time.Second)
|
||||
defer cancel()
|
||||
command := exec.CommandContext(probeCtx, ffmpeg, "-hide_banner", "-nostdin", "-loglevel", "info",
|
||||
"-protocol_whitelist", "file,pipe", "-i", path, "-map", "0:v:0", "-map", "0:a:0?",
|
||||
"-c", "copy", "-t", "0", "-f", "framehash", "-hash", "sha256", "pipe:1")
|
||||
output := &cappedStringWriter{limit: 64 * 1024}
|
||||
diagnostics := &cappedStringWriter{limit: 64 * 1024}
|
||||
command.Stdout, command.Stderr = output, diagnostics
|
||||
if err := command.Run(); err != nil {
|
||||
if probeCtx.Err() != nil {
|
||||
return mergeMediaInfo{}, probeCtx.Err()
|
||||
}
|
||||
return mergeMediaInfo{}, fmt.Errorf("检测 %s 失败:%w %s", filepath.Base(path), err, truncate(diagnostics.String(), 1000))
|
||||
}
|
||||
var info mergeMediaInfo
|
||||
fields := make(map[string]string)
|
||||
for _, line := range strings.Split(output.String(), "\n") {
|
||||
if key, value, ok := strings.Cut(strings.TrimSpace(line), ":"); ok {
|
||||
fields[key] = strings.TrimSpace(value)
|
||||
}
|
||||
if strings.HasPrefix(line, "#extradata ") {
|
||||
parts := strings.Split(line, ",")
|
||||
if len(parts) == 3 {
|
||||
if strings.TrimSpace(parts[0]) == "#extradata 0" {
|
||||
info.videoExtra = strings.TrimSpace(parts[2])
|
||||
} else if strings.TrimSpace(parts[0]) == "#extradata 1" {
|
||||
info.audioExtra = strings.TrimSpace(parts[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_, _ = fmt.Sscanf(fields["#dimensions 0"], "%dx%d", &info.width, &info.height)
|
||||
info.audio = fields["#media_type 1"] == "audio"
|
||||
info.videoCodec, info.audioCodec = fields["#codec_id 0"], fields["#codec_id 1"]
|
||||
info.videoTB, info.audioTB = fields["#tb 0"], fields["#tb 1"]
|
||||
info.sar = fields["#sar 0"]
|
||||
if info.sar == "" || info.sar == "0/1" {
|
||||
info.sar = "1/1"
|
||||
}
|
||||
info.sampleRate, _ = strconv.Atoi(fields["#sample_rate 1"])
|
||||
info.layout = firstNonEmpty(fields["#channel_layout_name 1"], fields["#channel_layout 1"])
|
||||
body, _, _ := strings.Cut(diagnostics.String(), "Stream mapping:")
|
||||
if match := playbackDurationPattern.FindStringSubmatch(body); len(match) == 4 {
|
||||
hours, _ := strconv.ParseFloat(match[1], 64)
|
||||
minutes, _ := strconv.ParseFloat(match[2], 64)
|
||||
seconds, _ := strconv.ParseFloat(match[3], 64)
|
||||
info.duration = time.Duration((hours*3600 + minutes*60 + seconds) * float64(time.Second))
|
||||
}
|
||||
streams, video, audio := 0, false, false
|
||||
for _, line := range strings.Split(body, "\n") {
|
||||
index := mergeStreamIndex.FindStringSubmatch(line)
|
||||
if len(index) != 2 {
|
||||
continue
|
||||
}
|
||||
streams++
|
||||
if strings.Contains(line, "Video:") && !video {
|
||||
video = true
|
||||
info.reorder = info.reorder || index[1] != "0"
|
||||
if match := mergeVideoDescription.FindStringSubmatch(line); len(match) == 2 {
|
||||
info.videoProfile = match[1]
|
||||
}
|
||||
if match := mergePixelFormat.FindStringSubmatch(line); len(match) == 2 {
|
||||
info.pixelFormat = match[1]
|
||||
}
|
||||
if match := mergeFrameRate.FindStringSubmatch(line); len(match) == 2 {
|
||||
info.frameRate, _ = strconv.ParseFloat(match[1], 64)
|
||||
}
|
||||
} else if strings.Contains(line, "Audio:") && !audio {
|
||||
audio = true
|
||||
info.reorder = info.reorder || index[1] != "1"
|
||||
if match := mergeAudioDescription.FindStringSubmatch(line); len(match) == 2 {
|
||||
info.audioProfile = match[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
info.reorder = info.reorder || streams > 2
|
||||
if info.width <= 0 || info.height <= 0 || info.duration <= 0 || info.videoCodec == "" {
|
||||
return mergeMediaInfo{}, fmt.Errorf("%s 缺少有效的视频编码、尺寸或时长,无法安全合并", filepath.Base(path))
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
Reference in New Issue
Block a user