Add initial Merge module with chapters and queue support
This commit is contained in:
parent
b97182baac
commit
3e7583704b
462
main.go
462
main.go
|
|
@ -520,6 +520,19 @@ type appState struct {
|
||||||
compareFile2 *videoSource
|
compareFile2 *videoSource
|
||||||
inspectFile *videoSource
|
inspectFile *videoSource
|
||||||
autoCompare bool // Auto-load Compare module after conversion
|
autoCompare bool // Auto-load Compare module after conversion
|
||||||
|
|
||||||
|
// Merge state
|
||||||
|
mergeClips []mergeClip
|
||||||
|
mergeFormat string
|
||||||
|
mergeOutput string
|
||||||
|
mergeKeepAll bool
|
||||||
|
mergeCodecMode string
|
||||||
|
}
|
||||||
|
|
||||||
|
type mergeClip struct {
|
||||||
|
Path string
|
||||||
|
Chapter string
|
||||||
|
Duration float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) persistConvertConfig() {
|
func (s *appState) persistConvertConfig() {
|
||||||
|
|
@ -535,6 +548,35 @@ func (s *appState) stopPreview() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toString(v interface{}) string {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case string:
|
||||||
|
return t
|
||||||
|
case fmt.Stringer:
|
||||||
|
return t.String()
|
||||||
|
default:
|
||||||
|
return fmt.Sprintf("%v", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFloat(v interface{}) float64 {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return t
|
||||||
|
case float32:
|
||||||
|
return float64(t)
|
||||||
|
case int:
|
||||||
|
return float64(t)
|
||||||
|
case int64:
|
||||||
|
return float64(t)
|
||||||
|
case json.Number:
|
||||||
|
if f, err := t.Float64(); err == nil {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
func (s *appState) updateStatsBar() {
|
func (s *appState) updateStatsBar() {
|
||||||
if s.statsBar == nil || s.jobQueue == nil {
|
if s.statsBar == nil || s.jobQueue == nil {
|
||||||
return
|
return
|
||||||
|
|
@ -773,7 +815,7 @@ func (s *appState) showMainMenu() {
|
||||||
Label: m.Label,
|
Label: m.Label,
|
||||||
Color: m.Color,
|
Color: m.Color,
|
||||||
Category: m.Category,
|
Category: m.Category,
|
||||||
Enabled: m.ID == "convert" || m.ID == "compare" || m.ID == "inspect", // Convert, compare, and inspect modules are functional
|
Enabled: m.ID == "convert" || m.ID == "compare" || m.ID == "inspect" || m.ID == "merge", // Enabled modules
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1109,6 +1151,8 @@ func (s *appState) showModule(id string) {
|
||||||
switch id {
|
switch id {
|
||||||
case "convert":
|
case "convert":
|
||||||
s.showConvertView(nil)
|
s.showConvertView(nil)
|
||||||
|
case "merge":
|
||||||
|
s.showMergeView()
|
||||||
case "compare":
|
case "compare":
|
||||||
s.showCompareView()
|
s.showCompareView()
|
||||||
case "inspect":
|
case "inspect":
|
||||||
|
|
@ -1437,6 +1481,284 @@ func (s *appState) showInspectView() {
|
||||||
s.setContent(buildInspectView(s))
|
s.setContent(buildInspectView(s))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *appState) showMergeView() {
|
||||||
|
s.stopPreview()
|
||||||
|
s.lastModule = s.active
|
||||||
|
s.active = "merge"
|
||||||
|
|
||||||
|
if s.mergeFormat == "" {
|
||||||
|
s.mergeFormat = "mkv-copy"
|
||||||
|
}
|
||||||
|
|
||||||
|
listBox := container.NewVBox()
|
||||||
|
|
||||||
|
var buildList func()
|
||||||
|
buildList = func() {
|
||||||
|
listBox.Objects = nil
|
||||||
|
if len(s.mergeClips) == 0 {
|
||||||
|
empty := widget.NewLabel("Add at least two clips to merge.")
|
||||||
|
empty.Alignment = fyne.TextAlignCenter
|
||||||
|
listBox.Add(container.NewCenter(empty))
|
||||||
|
} else {
|
||||||
|
for i, c := range s.mergeClips {
|
||||||
|
idx := i
|
||||||
|
name := filepath.Base(c.Path)
|
||||||
|
label := widget.NewLabel(utils.ShortenMiddle(name, 50))
|
||||||
|
chEntry := widget.NewEntry()
|
||||||
|
chEntry.SetText(c.Chapter)
|
||||||
|
chEntry.SetPlaceHolder(fmt.Sprintf("Part %d", i+1))
|
||||||
|
chEntry.OnChanged = func(val string) {
|
||||||
|
s.mergeClips[idx].Chapter = val
|
||||||
|
}
|
||||||
|
upBtn := widget.NewButton("↑", func() {
|
||||||
|
if idx > 0 {
|
||||||
|
s.mergeClips[idx-1], s.mergeClips[idx] = s.mergeClips[idx], s.mergeClips[idx-1]
|
||||||
|
buildList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
downBtn := widget.NewButton("↓", func() {
|
||||||
|
if idx < len(s.mergeClips)-1 {
|
||||||
|
s.mergeClips[idx+1], s.mergeClips[idx] = s.mergeClips[idx], s.mergeClips[idx+1]
|
||||||
|
buildList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
delBtn := widget.NewButton("Remove", func() {
|
||||||
|
s.mergeClips = append(s.mergeClips[:idx], s.mergeClips[idx+1:]...)
|
||||||
|
buildList()
|
||||||
|
})
|
||||||
|
row := container.NewBorder(
|
||||||
|
nil, nil,
|
||||||
|
container.NewVBox(upBtn, downBtn),
|
||||||
|
delBtn,
|
||||||
|
container.NewVBox(label, chEntry),
|
||||||
|
)
|
||||||
|
cardBg := canvas.NewRectangle(utils.MustHex("#171C2A"))
|
||||||
|
cardBg.CornerRadius = 6
|
||||||
|
cardBg.SetMinSize(fyne.NewSize(0, label.MinSize().Height+chEntry.MinSize().Height+12))
|
||||||
|
listBox.Add(container.NewPadded(container.NewMax(cardBg, row)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
listBox.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
addFiles := func(paths []string) {
|
||||||
|
for _, p := range paths {
|
||||||
|
src, err := probeVideo(p)
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(fmt.Errorf("failed to probe %s: %w", p, err), s.window)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.mergeClips = append(s.mergeClips, mergeClip{
|
||||||
|
Path: p,
|
||||||
|
Chapter: strings.TrimSuffix(filepath.Base(p), filepath.Ext(p)),
|
||||||
|
Duration: src.Duration,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if len(s.mergeClips) >= 2 && s.mergeOutput == "" {
|
||||||
|
first := filepath.Dir(s.mergeClips[0].Path)
|
||||||
|
s.mergeOutput = filepath.Join(first, "merged.mkv")
|
||||||
|
}
|
||||||
|
buildList()
|
||||||
|
}
|
||||||
|
|
||||||
|
addBtn := widget.NewButton("Add Files…", func() {
|
||||||
|
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || reader == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := reader.URI().Path()
|
||||||
|
reader.Close()
|
||||||
|
addFiles([]string{path})
|
||||||
|
}, s.window)
|
||||||
|
})
|
||||||
|
clearBtn := widget.NewButton("Clear", func() {
|
||||||
|
s.mergeClips = nil
|
||||||
|
buildList()
|
||||||
|
})
|
||||||
|
|
||||||
|
formatMap := map[string]string{
|
||||||
|
"MKV (Copy if compatible)": "mkv-copy",
|
||||||
|
"MKV (Re-encode H.265)": "mkv-encode",
|
||||||
|
"DVD NTSC 16:9": "dvd-ntsc-169",
|
||||||
|
"DVD NTSC 4:3": "dvd-ntsc-43",
|
||||||
|
"DVD PAL 16:9": "dvd-pal-169",
|
||||||
|
"DVD PAL 4:3": "dvd-pal-43",
|
||||||
|
"Blu-ray (H.264, MKV container)": "bd-h264",
|
||||||
|
}
|
||||||
|
var formatKeys []string
|
||||||
|
for k := range formatMap {
|
||||||
|
formatKeys = append(formatKeys, k)
|
||||||
|
}
|
||||||
|
slices.Sort(formatKeys)
|
||||||
|
|
||||||
|
formatSelect := widget.NewSelect(formatKeys, func(val string) {
|
||||||
|
s.mergeFormat = formatMap[val]
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(s.mergeFormat, "dvd"):
|
||||||
|
s.mergeCodecMode = "encode"
|
||||||
|
if s.mergeOutput == "" && len(s.mergeClips) > 0 {
|
||||||
|
dir := filepath.Dir(s.mergeClips[0].Path)
|
||||||
|
s.mergeOutput = filepath.Join(dir, "merged-dvd.mpg")
|
||||||
|
}
|
||||||
|
case s.mergeFormat == "bd-h264":
|
||||||
|
s.mergeCodecMode = "encode"
|
||||||
|
if s.mergeOutput == "" && len(s.mergeClips) > 0 {
|
||||||
|
dir := filepath.Dir(s.mergeClips[0].Path)
|
||||||
|
s.mergeOutput = filepath.Join(dir, "merged-bd.mkv")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if s.mergeCodecMode == "" {
|
||||||
|
s.mergeCodecMode = "copy"
|
||||||
|
}
|
||||||
|
if s.mergeOutput == "" && len(s.mergeClips) > 0 {
|
||||||
|
dir := filepath.Dir(s.mergeClips[0].Path)
|
||||||
|
s.mergeOutput = filepath.Join(dir, "merged.mkv")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
for label, val := range formatMap {
|
||||||
|
if val == s.mergeFormat {
|
||||||
|
formatSelect.SetSelected(label)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keepAllCheck := widget.NewCheck("Keep all audio/subtitle tracks", func(v bool) {
|
||||||
|
s.mergeKeepAll = v
|
||||||
|
})
|
||||||
|
keepAllCheck.SetChecked(s.mergeKeepAll)
|
||||||
|
|
||||||
|
codecModeSelect := widget.NewSelect([]string{"Copy (if compatible)", "Re-encode (H.265)"}, func(val string) {
|
||||||
|
if strings.HasPrefix(val, "Copy") {
|
||||||
|
s.mergeCodecMode = "copy"
|
||||||
|
} else {
|
||||||
|
s.mergeCodecMode = "encode"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if s.mergeCodecMode == "" {
|
||||||
|
s.mergeCodecMode = "copy"
|
||||||
|
}
|
||||||
|
if s.mergeCodecMode == "encode" {
|
||||||
|
codecModeSelect.SetSelected("Re-encode (H.265)")
|
||||||
|
} else {
|
||||||
|
codecModeSelect.SetSelected("Copy (if compatible)")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputEntry := widget.NewEntry()
|
||||||
|
outputEntry.SetPlaceHolder("merged output path")
|
||||||
|
outputEntry.SetText(s.mergeOutput)
|
||||||
|
outputEntry.OnChanged = func(val string) {
|
||||||
|
s.mergeOutput = val
|
||||||
|
}
|
||||||
|
browseOut := widget.NewButton("Browse", func() {
|
||||||
|
dialog.ShowFileSave(func(writer fyne.URIWriteCloser, err error) {
|
||||||
|
if err != nil || writer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.mergeOutput = writer.URI().Path()
|
||||||
|
outputEntry.SetText(s.mergeOutput)
|
||||||
|
writer.Close()
|
||||||
|
}, s.window)
|
||||||
|
})
|
||||||
|
|
||||||
|
addQueueBtn := widget.NewButton("Add Merge to Queue", func() {
|
||||||
|
if err := s.addMergeToQueue(false); err != nil {
|
||||||
|
dialog.ShowError(err, s.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dialog.ShowInformation("Queue", "Merge job added to queue.", s.window)
|
||||||
|
if s.jobQueue != nil && !s.jobQueue.IsRunning() {
|
||||||
|
s.jobQueue.Start()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
runNowBtn := widget.NewButton("Merge Now", func() {
|
||||||
|
if err := s.addMergeToQueue(true); err != nil {
|
||||||
|
dialog.ShowError(err, s.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.jobQueue != nil && !s.jobQueue.IsRunning() {
|
||||||
|
s.jobQueue.Start()
|
||||||
|
}
|
||||||
|
dialog.ShowInformation("Merge", "Merge started! Track progress in Job Queue.", s.window)
|
||||||
|
})
|
||||||
|
if len(s.mergeClips) < 2 {
|
||||||
|
addQueueBtn.Disable()
|
||||||
|
runNowBtn.Disable()
|
||||||
|
}
|
||||||
|
|
||||||
|
listScroll := container.NewVScroll(listBox)
|
||||||
|
listScroll.SetMinSize(fyne.NewSize(400, 300))
|
||||||
|
|
||||||
|
left := container.NewVBox(
|
||||||
|
widget.NewLabelWithStyle("Clips to Merge", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
|
container.NewHBox(addBtn, clearBtn),
|
||||||
|
listScroll,
|
||||||
|
)
|
||||||
|
|
||||||
|
right := container.NewVBox(
|
||||||
|
widget.NewLabelWithStyle("Output Options", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
|
widget.NewLabel("Format"),
|
||||||
|
formatSelect,
|
||||||
|
codecModeSelect,
|
||||||
|
keepAllCheck,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
widget.NewLabel("Output Path"),
|
||||||
|
container.NewBorder(nil, nil, nil, browseOut, outputEntry),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
container.NewHBox(addQueueBtn, runNowBtn),
|
||||||
|
)
|
||||||
|
|
||||||
|
content := container.NewHSplit(left, right)
|
||||||
|
content.Offset = 0.55
|
||||||
|
s.setContent(container.NewBorder(nil, nil, nil, nil, content))
|
||||||
|
|
||||||
|
buildList()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *appState) addMergeToQueue(startNow bool) error {
|
||||||
|
if len(s.mergeClips) < 2 {
|
||||||
|
return fmt.Errorf("add at least two clips")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(s.mergeOutput) == "" {
|
||||||
|
firstDir := filepath.Dir(s.mergeClips[0].Path)
|
||||||
|
s.mergeOutput = filepath.Join(firstDir, "merged.mkv")
|
||||||
|
}
|
||||||
|
clips := make([]map[string]interface{}, 0, len(s.mergeClips))
|
||||||
|
for _, c := range s.mergeClips {
|
||||||
|
name := c.Chapter
|
||||||
|
if strings.TrimSpace(name) == "" {
|
||||||
|
name = strings.TrimSuffix(filepath.Base(c.Path), filepath.Ext(c.Path))
|
||||||
|
}
|
||||||
|
clips = append(clips, map[string]interface{}{
|
||||||
|
"path": c.Path,
|
||||||
|
"chapter": name,
|
||||||
|
"duration": c.Duration,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
config := map[string]interface{}{
|
||||||
|
"clips": clips,
|
||||||
|
"format": s.mergeFormat,
|
||||||
|
"keepAllStreams": s.mergeKeepAll,
|
||||||
|
"codecMode": s.mergeCodecMode,
|
||||||
|
"outputPath": s.mergeOutput,
|
||||||
|
}
|
||||||
|
|
||||||
|
job := &queue.Job{
|
||||||
|
Type: queue.JobTypeMerge,
|
||||||
|
Title: fmt.Sprintf("Merge %d clips", len(clips)),
|
||||||
|
Description: fmt.Sprintf("Output: %s", utils.ShortenMiddle(filepath.Base(s.mergeOutput), 40)),
|
||||||
|
InputFile: clips[0]["path"].(string),
|
||||||
|
OutputFile: s.mergeOutput,
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
s.jobQueue.Add(job)
|
||||||
|
if startNow && s.jobQueue != nil && !s.jobQueue.IsRunning() {
|
||||||
|
s.jobQueue.Start()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *appState) showCompareFullscreen() {
|
func (s *appState) showCompareFullscreen() {
|
||||||
s.stopPreview()
|
s.stopPreview()
|
||||||
s.lastModule = s.active
|
s.lastModule = s.active
|
||||||
|
|
@ -1452,7 +1774,7 @@ func (s *appState) jobExecutor(ctx context.Context, job *queue.Job, progressCall
|
||||||
case queue.JobTypeConvert:
|
case queue.JobTypeConvert:
|
||||||
return s.executeConvertJob(ctx, job, progressCallback)
|
return s.executeConvertJob(ctx, job, progressCallback)
|
||||||
case queue.JobTypeMerge:
|
case queue.JobTypeMerge:
|
||||||
return fmt.Errorf("merge jobs not yet implemented")
|
return s.executeMergeJob(ctx, job, progressCallback)
|
||||||
case queue.JobTypeTrim:
|
case queue.JobTypeTrim:
|
||||||
return fmt.Errorf("trim jobs not yet implemented")
|
return fmt.Errorf("trim jobs not yet implemented")
|
||||||
case queue.JobTypeFilter:
|
case queue.JobTypeFilter:
|
||||||
|
|
@ -1470,6 +1792,142 @@ func (s *appState) jobExecutor(ctx context.Context, job *queue.Job, progressCall
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *appState) executeMergeJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
|
||||||
|
cfg := job.Config
|
||||||
|
format, _ := cfg["format"].(string)
|
||||||
|
keepAll, _ := cfg["keepAllStreams"].(bool)
|
||||||
|
codecMode, _ := cfg["codecMode"].(string) // copy or encode
|
||||||
|
outputPath, _ := cfg["outputPath"].(string)
|
||||||
|
|
||||||
|
rawClips, _ := cfg["clips"].([]interface{})
|
||||||
|
var clips []mergeClip
|
||||||
|
for _, rc := range rawClips {
|
||||||
|
if m, ok := rc.(map[string]interface{}); ok {
|
||||||
|
clips = append(clips, mergeClip{
|
||||||
|
Path: toString(m["path"]),
|
||||||
|
Chapter: toString(m["chapter"]),
|
||||||
|
Duration: toFloat(m["duration"]),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(clips) < 2 {
|
||||||
|
return fmt.Errorf("need at least two clips to merge")
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpDir := os.TempDir()
|
||||||
|
listFile, err := os.CreateTemp(tmpDir, "vt-merge-list-*.txt")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(listFile.Name())
|
||||||
|
for _, c := range clips {
|
||||||
|
fmt.Fprintf(listFile, "file '%s'\n", strings.ReplaceAll(c.Path, "'", "'\\''"))
|
||||||
|
}
|
||||||
|
_ = listFile.Close()
|
||||||
|
|
||||||
|
// Build chapters metadata
|
||||||
|
chapterFile, err := os.CreateTemp(tmpDir, "vt-merge-chapters-*.txt")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var elapsed float64
|
||||||
|
fmt.Fprintln(chapterFile, ";FFMETADATA1")
|
||||||
|
for i, c := range clips {
|
||||||
|
startMs := int64(elapsed * 1000)
|
||||||
|
endMs := int64((elapsed + c.Duration) * 1000)
|
||||||
|
fmt.Fprintln(chapterFile, "[CHAPTER]")
|
||||||
|
fmt.Fprintln(chapterFile, "TIMEBASE=1/1000")
|
||||||
|
fmt.Fprintf(chapterFile, "START=%d\n", startMs)
|
||||||
|
fmt.Fprintf(chapterFile, "END=%d\n", endMs)
|
||||||
|
name := c.Chapter
|
||||||
|
if strings.TrimSpace(name) == "" {
|
||||||
|
name = fmt.Sprintf("Part %d", i+1)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(chapterFile, "title=%s\n", name)
|
||||||
|
elapsed += c.Duration
|
||||||
|
}
|
||||||
|
_ = chapterFile.Close()
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-y",
|
||||||
|
"-hide_banner",
|
||||||
|
"-loglevel", "error",
|
||||||
|
"-f", "concat",
|
||||||
|
"-safe", "0",
|
||||||
|
"-i", listFile.Name(),
|
||||||
|
"-i", chapterFile.Name(),
|
||||||
|
"-map_metadata", "1",
|
||||||
|
"-map_chapters", "1",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map streams
|
||||||
|
if keepAll {
|
||||||
|
args = append(args, "-map", "0")
|
||||||
|
} else {
|
||||||
|
args = append(args, "-map", "0:v:0", "-map", "0:a:0")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output profile
|
||||||
|
switch format {
|
||||||
|
case "dvd-ntsc-169", "dvd-ntsc-43", "dvd-pal-169", "dvd-pal-43":
|
||||||
|
// Force MPEG-2 / AC-3
|
||||||
|
args = append(args,
|
||||||
|
"-c:v", "mpeg2video",
|
||||||
|
"-c:a", "ac3",
|
||||||
|
"-b:a", "192k",
|
||||||
|
"-max_muxing_queue_size", "1024",
|
||||||
|
)
|
||||||
|
aspect := "16:9"
|
||||||
|
if strings.Contains(format, "43") {
|
||||||
|
aspect = "4:3"
|
||||||
|
}
|
||||||
|
if strings.Contains(format, "ntsc") {
|
||||||
|
args = append(args, "-vf", "scale=720:480,setsar=1", "-r", "30000/1001", "-pix_fmt", "yuv420p", "-aspect", aspect)
|
||||||
|
} else {
|
||||||
|
args = append(args, "-vf", "scale=720:576,setsar=1", "-r", "25", "-pix_fmt", "yuv420p", "-aspect", aspect)
|
||||||
|
}
|
||||||
|
args = append(args, "-target", "ntsc-dvd")
|
||||||
|
if strings.Contains(format, "pal") {
|
||||||
|
args[len(args)-1] = "pal-dvd"
|
||||||
|
}
|
||||||
|
case "bd-h264":
|
||||||
|
args = append(args,
|
||||||
|
"-c:v", "libx264",
|
||||||
|
"-preset", "slow",
|
||||||
|
"-crf", "18",
|
||||||
|
"-pix_fmt", "yuv420p",
|
||||||
|
"-c:a", "ac3",
|
||||||
|
"-b:a", "256k",
|
||||||
|
)
|
||||||
|
default:
|
||||||
|
if codecMode == "copy" {
|
||||||
|
args = append(args, "-c", "copy")
|
||||||
|
} else {
|
||||||
|
// Re-encode to H.265 by default
|
||||||
|
args = append(args, "-c:v", "libx265", "-crf", "20", "-c:a", "copy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, outputPath)
|
||||||
|
|
||||||
|
// Execute
|
||||||
|
cmd := exec.CommandContext(ctx, platformConfig.FFmpegPath, args...)
|
||||||
|
utils.ApplyNoWindow(cmd)
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
if progressCallback != nil {
|
||||||
|
progressCallback(0)
|
||||||
|
}
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("merge failed: %w\nFFmpeg output:\n%s", err, strings.TrimSpace(stderr.String()))
|
||||||
|
}
|
||||||
|
if progressCallback != nil {
|
||||||
|
progressCallback(100)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// executeConvertJob executes a conversion job from the queue
|
// executeConvertJob executes a conversion job from the queue
|
||||||
func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
|
func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
|
||||||
cfg := job.Config
|
cfg := job.Config
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user