feat(app): add internal components batch 6
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user