feat(app): add internal components batch 6

This commit is contained in:
吴璨
2026-09-30 10:20:46 +08:00
parent 5bd126e026
commit baa1154cdd
10 changed files with 1731 additions and 0 deletions
+237
View File
@@ -0,0 +1,237 @@
package app
import (
"fmt"
"strconv"
"strings"
"time"
)
type mergeMediaPlan struct {
video, audio mergeMediaInfo
steps []mergeMediaStep
timescale int
transport bool
parameters bool
videoCount int
audioCount int
remuxCount int
audioFallback bool
}
type mergeMediaStep struct {
video, audio, remux bool
}
func (info mergeMediaInfo) videoKey() string {
key := fmt.Sprintf("%s|%s|%s|%dx%d|%s", info.videoCodec, info.videoProfile, info.pixelFormat, info.width, info.height, info.sar)
if info.videoCodec != "h264" && info.videoCodec != "hevc" {
key += "|" + info.videoExtra
}
return key
}
func (info mergeMediaInfo) audioKey() string {
return fmt.Sprintf("%s|%s|%d|%s|%s", info.audioCodec, info.audioProfile, info.sampleRate, info.layout, info.audioExtra)
}
func majorityMergeMedia(infos []mergeMediaInfo, key func(mergeMediaInfo) string, audio bool) mergeMediaInfo {
counts := make(map[string]int)
durations := make(map[string]time.Duration)
for _, info := range infos {
if audio && !info.audio {
continue
}
value := key(info)
counts[value]++
durations[value] += info.duration
}
var chosen mergeMediaInfo
bestCount, bestDuration := 0, time.Duration(0)
for _, info := range infos {
if audio && !info.audio {
continue
}
value := key(info)
if counts[value] > bestCount || counts[value] == bestCount && durations[value] > bestDuration {
chosen, bestCount, bestDuration = info, counts[value], durations[value]
}
}
return chosen
}
func planMergeMedia(infos []mergeMediaInfo) mergeMediaPlan {
plan := mergeMediaPlan{timescale: 90000}
plan.video = majorityMergeMedia(infos, mergeMediaInfo.videoKey, false)
var compatible []mergeMediaInfo
for _, info := range infos {
if info.videoKey() == plan.video.videoKey() {
compatible = append(compatible, info)
}
}
plan.video = majorityMergeMedia(compatible, func(info mergeMediaInfo) string { return info.videoTB }, false)
plan.audio = majorityMergeMedia(infos, mergeMediaInfo.audioKey, true)
if plan.audio.audio && plan.audio.audioCodec == "aac" && plan.audio.audioProfile != "" && plan.audio.audioProfile != "LC" {
for _, info := range infos {
if !info.audio || info.audioKey() != plan.audio.audioKey() {
plan.audioFallback = true
plan.audio.audioProfile, plan.audio.audioExtra = "LC", ""
break
}
}
}
if value, ok := strings.CutPrefix(plan.video.videoTB, "1/"); ok {
if scale, err := strconv.Atoi(value); err == nil && scale > 0 && scale <= 10000000 {
plan.timescale = scale
}
}
for _, info := range infos {
step := mergeMediaStep{video: info.videoKey() != plan.video.videoKey(), audio: plan.audio.audio && (plan.audioFallback || !info.audio || info.audioKey() != plan.audio.audioKey())}
if step.video || info.videoExtra != plan.video.videoExtra {
plan.parameters = true
}
step.remux = info.videoTB != fmt.Sprintf("1/%d", plan.timescale) || plan.audio.audio && info.audioTB != plan.audio.audioTB || info.reorder
plan.steps = append(plan.steps, step)
}
plan.transport = plan.video.videoCodec == "hevc" && plan.parameters
for i := range plan.steps {
step := &plan.steps[i]
step.remux = step.remux || plan.transport
if step.video {
plan.videoCount++
}
if step.audio {
plan.audioCount++
}
if step.remux && !step.video && !step.audio {
plan.remuxCount++
}
}
return plan
}
func (plan mergeMediaPlan) description() string {
parts := []string{}
if plan.videoCount > 0 {
parts = append(parts, fmt.Sprintf("仅转视频 %d/%d 集(%s %dx%d)", plan.videoCount, len(plan.steps), strings.ToUpper(plan.video.videoCodec), plan.video.width, plan.video.height))
}
if plan.audioCount > 0 {
if plan.audioFallback {
parts = append(parts, fmt.Sprintf("统一音轨为 AAC-LC %d/%d 集", plan.audioCount, len(plan.steps)))
} else {
parts = append(parts, fmt.Sprintf("调整音轨 %d/%d 集", plan.audioCount, len(plan.steps)))
}
}
if len(parts) == 0 {
if plan.remuxCount > 0 {
return "无损换封装合并(不转码)"
}
return "原编码快速合并(不转码)"
}
return strings.Join(parts, ";")
}
func mergeVideoEncoder(info mergeMediaInfo) ([]string, error) {
profile := strings.ToLower(info.videoProfile)
var args []string
switch info.videoCodec {
case "h264":
profiles := map[string]string{"constrained baseline": "baseline", "baseline": "baseline", "main": "main", "high": "high", "high 10": "high10", "high 4:2:2": "high422", "high 4:4:4 predictive": "high444"}
value, ok := profiles[profile]
if !ok {
return nil, fmt.Errorf("暂不能将少数分集转为多数使用的 H.264 %s", info.videoProfile)
}
args = []string{"-c:v", "libx264", "-preset", "veryfast", "-crf", "18", "-profile:v", value, "-threads:v", "2"}
case "hevc":
profiles := map[string]string{"main": "main", "main 10": "main10"}
value, ok := profiles[profile]
if !ok {
return nil, fmt.Errorf("暂不能将少数分集转为多数使用的 HEVC %s", info.videoProfile)
}
args = []string{"-c:v", "libx265", "-preset", "veryfast", "-crf", "18", "-profile:v", value, "-threads:v", "2", "-x265-params", "pools=2:frame-threads=2:log-level=error"}
default:
return nil, fmt.Errorf("暂不支持将少数分集转为多数使用的 %s 编码;未对整剧转码", info.videoCodec)
}
if info.pixelFormat == "" || info.width%2 != 0 || info.height%2 != 0 {
return nil, fmt.Errorf("多数分集的像素格式或尺寸无法安全转换")
}
return append(args, "-pix_fmt", info.pixelFormat), nil
}
func mergeAudioEncoder(info mergeMediaInfo) ([]string, error) {
encoder := info.audioCodec
switch encoder {
case "aac":
if info.audioProfile != "" && info.audioProfile != "LC" {
return nil, fmt.Errorf("暂不能将少数音轨转为多数使用的 AAC %s", info.audioProfile)
}
case "mp3":
encoder = "libmp3lame"
case "ac3", "eac3", "alac":
default:
return nil, fmt.Errorf("暂不支持将少数音轨转为多数使用的 %s", info.audioCodec)
}
if info.sampleRate <= 0 || info.layout == "" {
return nil, fmt.Errorf("多数音轨缺少采样率或声道布局,无法安全转换")
}
args := []string{"-c:a", encoder, "-ar", strconv.Itoa(info.sampleRate), "-channel_layout:a", info.layout}
if encoder == "aac" {
args = append(args, "-profile:a", "aac_low", "-b:a", "192k")
}
return args, nil
}
func (plan mergeMediaPlan) prepareArgs(input, output string, info mergeMediaInfo, step mergeMediaStep) ([]string, error) {
args := []string{"-xerror", "-threads", "2", "-protocol_whitelist", "file,pipe", "-i", input}
if plan.audio.audio && !info.audio {
args = append(args, "-f", "lavfi", "-i", fmt.Sprintf("anullsrc=channel_layout=%s:sample_rate=%d", plan.audio.layout, plan.audio.sampleRate))
}
args = append(args, "-map", "0:v:0", "-sn", "-dn", "-map_metadata", "-1", "-c:v", "copy")
if step.video {
encoder, err := mergeVideoEncoder(plan.video)
if err != nil {
return nil, err
}
sar := plan.video.sar
if sar == "" || sar == "0/1" {
sar = "1/1"
}
filter := "setpts=PTS-STARTPTS,"
if plan.video.frameRate > 0 {
filter += "fps=" + strconv.FormatFloat(plan.video.frameRate, 'f', -1, 64) + ","
}
filter += fmt.Sprintf("scale=w='max(2,trunc(min(%d,%d*dar/(%s))/2)*2)':h='max(2,trunc(min(%d,%d*(%s)/dar)/2)*2)',pad=%d:%d:(ow-iw)/2:(oh-ih)/2,setsar=%s", plan.video.width, plan.video.height, sar, plan.video.height, plan.video.width, sar, plan.video.width, plan.video.height, sar)
args = append(args, "-vf", filter)
args = append(args, encoder...)
}
if plan.audio.audio {
stream := "0:a:0"
if !info.audio {
stream = "1:a:0"
}
args = append(args, "-map", stream)
if step.audio {
encoder, err := mergeAudioEncoder(plan.audio)
if err != nil {
return nil, err
}
args = append(args, encoder...)
args = append(args, "-af", fmt.Sprintf("aresample=%d:async=1:first_pts=0,apad", plan.audio.sampleRate))
} else {
args = append(args, "-c:a", "copy")
}
} else {
args = append(args, "-an")
}
args = append(args, "-max_muxing_queue_size", "4096")
if step.video || step.audio {
args = append(args, "-t", strconv.FormatFloat(info.duration.Seconds(), 'f', 6, 64))
}
if plan.transport {
args = append(args, "-bsf:v", "hevc_mp4toannexb", "-f", "mpegts")
} else {
args = append(args, "-video_track_timescale", strconv.Itoa(plan.timescale), "-f", "mp4")
}
return append(args, "-y", output), nil
}