Compare commits
4 Commits
f62b64b0d5
...
fefe3ddd50
| Author | SHA1 | Date | |
|---|---|---|---|
| fefe3ddd50 | |||
| 610e75df33 | |||
| e5d1ecfc06 | |||
| 6f82641018 |
221
main.go
221
main.go
|
|
@ -655,7 +655,8 @@ type appState struct {
|
|||
upscaleFilterChain []string // Transferred filters from Filters module
|
||||
|
||||
// Snippet settings
|
||||
snippetLength int // Length of snippet in seconds (default: 20)
|
||||
snippetLength int // Length of snippet in seconds (default: 20)
|
||||
snippetSourceFormat bool // true = source format, false = conversion format (default: true)
|
||||
|
||||
// Interlacing detection state
|
||||
interlaceResult *interlace.DetectionResult
|
||||
|
|
@ -3339,6 +3340,12 @@ func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progre
|
|||
snippetLength = int(lengthVal)
|
||||
}
|
||||
|
||||
// Get snippet mode, default to source format (true)
|
||||
useSourceFormat := true
|
||||
if modeVal, ok := cfg["useSourceFormat"].(bool); ok {
|
||||
useSourceFormat = modeVal
|
||||
}
|
||||
|
||||
// Probe video to get duration
|
||||
src, err := probeVideo(inputPath)
|
||||
if err != nil {
|
||||
|
|
@ -3354,20 +3361,118 @@ func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progre
|
|||
progressCallback(0)
|
||||
}
|
||||
|
||||
// Re-encode for precise duration control (stream copy can only cut at keyframes)
|
||||
args := []string{
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-loglevel", "error",
|
||||
"-ss", start,
|
||||
"-i", inputPath,
|
||||
"-t", fmt.Sprintf("%d", snippetLength),
|
||||
"-c:v", "libx264", // Re-encode video for frame-accurate cutting
|
||||
"-preset", "ultrafast", // Fast encoding for snippets
|
||||
"-crf", "18", // High quality
|
||||
"-c:a", "copy", // Copy audio without re-encoding
|
||||
"-map", "0", // Include all streams
|
||||
outputPath,
|
||||
var args []string
|
||||
|
||||
if useSourceFormat {
|
||||
// Source format mode: Use stream copy for clean extraction
|
||||
// Note: This uses keyframe cutting, so duration may not be frame-perfect
|
||||
args = []string{
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-loglevel", "error",
|
||||
"-ss", start,
|
||||
"-i", inputPath,
|
||||
"-t", fmt.Sprintf("%d", snippetLength),
|
||||
"-c", "copy", // Stream copy - no re-encoding
|
||||
"-map", "0", // Include all streams
|
||||
outputPath,
|
||||
}
|
||||
} else {
|
||||
// Conversion format mode: Use configured conversion settings
|
||||
// This allows previewing what the final converted output will look like
|
||||
conv := s.convert
|
||||
|
||||
args = []string{
|
||||
"-y",
|
||||
"-hide_banner",
|
||||
"-loglevel", "error",
|
||||
"-ss", start,
|
||||
"-i", inputPath,
|
||||
"-t", fmt.Sprintf("%d", snippetLength),
|
||||
}
|
||||
|
||||
// Apply video codec settings
|
||||
videoCodec := strings.ToLower(conv.VideoCodec)
|
||||
switch videoCodec {
|
||||
case "h.264", "":
|
||||
args = append(args, "-c:v", "libx264")
|
||||
if conv.EncoderPreset != "" {
|
||||
args = append(args, "-preset", conv.EncoderPreset)
|
||||
} else {
|
||||
args = append(args, "-preset", "medium")
|
||||
}
|
||||
if conv.CRF != "" {
|
||||
args = append(args, "-crf", conv.CRF)
|
||||
} else {
|
||||
args = append(args, "-crf", "23")
|
||||
}
|
||||
case "h.265":
|
||||
args = append(args, "-c:v", "libx265")
|
||||
if conv.EncoderPreset != "" {
|
||||
args = append(args, "-preset", conv.EncoderPreset)
|
||||
} else {
|
||||
args = append(args, "-preset", "medium")
|
||||
}
|
||||
if conv.CRF != "" {
|
||||
args = append(args, "-crf", conv.CRF)
|
||||
} else {
|
||||
args = append(args, "-crf", "28")
|
||||
}
|
||||
case "vp9":
|
||||
args = append(args, "-c:v", "libvpx-vp9")
|
||||
if conv.CRF != "" {
|
||||
args = append(args, "-crf", conv.CRF)
|
||||
} else {
|
||||
args = append(args, "-crf", "31")
|
||||
}
|
||||
case "av1":
|
||||
args = append(args, "-c:v", "libsvtav1")
|
||||
if conv.CRF != "" {
|
||||
args = append(args, "-crf", conv.CRF)
|
||||
} else {
|
||||
args = append(args, "-crf", "35")
|
||||
}
|
||||
case "copy":
|
||||
args = append(args, "-c:v", "copy")
|
||||
default:
|
||||
// Fallback to h264
|
||||
args = append(args, "-c:v", "libx264", "-preset", "medium", "-crf", "23")
|
||||
}
|
||||
|
||||
// Apply audio codec settings
|
||||
audioCodec := strings.ToLower(conv.AudioCodec)
|
||||
switch audioCodec {
|
||||
case "aac", "":
|
||||
args = append(args, "-c:a", "aac")
|
||||
if conv.AudioBitrate != "" {
|
||||
args = append(args, "-b:a", conv.AudioBitrate)
|
||||
} else {
|
||||
args = append(args, "-b:a", "192k")
|
||||
}
|
||||
case "opus":
|
||||
args = append(args, "-c:a", "libopus")
|
||||
if conv.AudioBitrate != "" {
|
||||
args = append(args, "-b:a", conv.AudioBitrate)
|
||||
} else {
|
||||
args = append(args, "-b:a", "128k")
|
||||
}
|
||||
case "mp3":
|
||||
args = append(args, "-c:a", "libmp3lame")
|
||||
if conv.AudioBitrate != "" {
|
||||
args = append(args, "-b:a", conv.AudioBitrate)
|
||||
} else {
|
||||
args = append(args, "-b:a", "192k")
|
||||
}
|
||||
case "flac":
|
||||
args = append(args, "-c:a", "flac")
|
||||
case "copy":
|
||||
args = append(args, "-c:a", "copy")
|
||||
default:
|
||||
// Fallback to AAC
|
||||
args = append(args, "-c:a", "aac", "-b:a", "192k")
|
||||
}
|
||||
|
||||
args = append(args, outputPath)
|
||||
}
|
||||
|
||||
logFile, logPath, _ := createConversionLog(inputPath, outputPath, args)
|
||||
|
|
@ -5181,10 +5286,14 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
optionsRect.StrokeWidth = 1
|
||||
optionsPanel := container.NewMax(optionsRect, container.NewPadded(tabs))
|
||||
|
||||
// Initialize snippet length default
|
||||
// Initialize snippet settings defaults
|
||||
if state.snippetLength == 0 {
|
||||
state.snippetLength = 20 // Default to 20 seconds
|
||||
}
|
||||
// Default to source format if not set
|
||||
if !state.snippetSourceFormat {
|
||||
state.snippetSourceFormat = true
|
||||
}
|
||||
|
||||
// Snippet length configuration
|
||||
snippetLengthLabel := widget.NewLabel(fmt.Sprintf("Snippet Length: %d seconds", state.snippetLength))
|
||||
|
|
@ -5196,9 +5305,22 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
snippetLengthLabel.SetText(fmt.Sprintf("Snippet Length: %d seconds", state.snippetLength))
|
||||
}
|
||||
|
||||
// Snippet output mode
|
||||
snippetModeLabel := widget.NewLabel("Snippet Output:")
|
||||
snippetModeCheck := widget.NewCheck("Snippet to Default Format (preserves source quality)", func(checked bool) {
|
||||
state.snippetSourceFormat = checked
|
||||
})
|
||||
snippetModeCheck.SetChecked(state.snippetSourceFormat)
|
||||
snippetModeHint := widget.NewLabel("Unchecked = Snippet to Output Format (uses conversion settings)")
|
||||
snippetModeHint.TextStyle = fyne.TextStyle{Italic: true}
|
||||
|
||||
snippetConfigRow := container.NewVBox(
|
||||
snippetLengthLabel,
|
||||
snippetLengthSlider,
|
||||
widget.NewSeparator(),
|
||||
snippetModeLabel,
|
||||
snippetModeCheck,
|
||||
snippetModeHint,
|
||||
)
|
||||
|
||||
snippetBtn := widget.NewButton("Generate Snippet", func() {
|
||||
|
|
@ -5211,24 +5333,42 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
return
|
||||
}
|
||||
src := state.source
|
||||
// Use same extension as source file since we're using stream copy
|
||||
ext := filepath.Ext(src.Path)
|
||||
if ext == "" {
|
||||
ext = ".mp4"
|
||||
|
||||
// Determine output extension based on mode
|
||||
var ext string
|
||||
if state.snippetSourceFormat {
|
||||
// Source format: use source extension
|
||||
ext = filepath.Ext(src.Path)
|
||||
if ext == "" {
|
||||
ext = ".mp4"
|
||||
}
|
||||
} else {
|
||||
// Conversion format: use configured output format
|
||||
ext = state.convert.SelectedFormat.Ext
|
||||
if ext == "" {
|
||||
ext = ".mp4"
|
||||
}
|
||||
}
|
||||
|
||||
outName := fmt.Sprintf("%s-snippet-%d%s", strings.TrimSuffix(src.DisplayName, filepath.Ext(src.DisplayName)), time.Now().Unix(), ext)
|
||||
outPath := filepath.Join(filepath.Dir(src.Path), outName)
|
||||
|
||||
modeDesc := "conversion settings"
|
||||
if state.snippetSourceFormat {
|
||||
modeDesc = "source format"
|
||||
}
|
||||
|
||||
job := &queue.Job{
|
||||
Type: queue.JobTypeSnippet,
|
||||
Title: "Snippet: " + filepath.Base(src.Path),
|
||||
Description: fmt.Sprintf("%ds snippet centred on midpoint (source settings)", state.snippetLength),
|
||||
Description: fmt.Sprintf("%ds snippet centred on midpoint (%s)", state.snippetLength, modeDesc),
|
||||
InputFile: src.Path,
|
||||
OutputFile: outPath,
|
||||
Config: map[string]interface{}{
|
||||
"inputPath": src.Path,
|
||||
"outputPath": outPath,
|
||||
"snippetLength": float64(state.snippetLength),
|
||||
"inputPath": src.Path,
|
||||
"outputPath": outPath,
|
||||
"snippetLength": float64(state.snippetLength),
|
||||
"useSourceFormat": state.snippetSourceFormat,
|
||||
},
|
||||
}
|
||||
state.jobQueue.Add(job)
|
||||
|
|
@ -5254,29 +5394,46 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
timestamp := time.Now().Unix()
|
||||
jobsAdded := 0
|
||||
|
||||
modeDesc := "conversion settings"
|
||||
if state.snippetSourceFormat {
|
||||
modeDesc = "source format"
|
||||
}
|
||||
|
||||
for _, src := range state.loadedVideos {
|
||||
if src == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Use same extension as source file since we're using stream copy
|
||||
ext := filepath.Ext(src.Path)
|
||||
if ext == "" {
|
||||
ext = ".mp4"
|
||||
// Determine output extension based on mode
|
||||
var ext string
|
||||
if state.snippetSourceFormat {
|
||||
// Source format: use source extension
|
||||
ext = filepath.Ext(src.Path)
|
||||
if ext == "" {
|
||||
ext = ".mp4"
|
||||
}
|
||||
} else {
|
||||
// Conversion format: use configured output format
|
||||
ext = state.convert.SelectedFormat.Ext
|
||||
if ext == "" {
|
||||
ext = ".mp4"
|
||||
}
|
||||
}
|
||||
|
||||
outName := fmt.Sprintf("%s-snippet-%d%s", strings.TrimSuffix(src.DisplayName, filepath.Ext(src.DisplayName)), timestamp, ext)
|
||||
outPath := filepath.Join(filepath.Dir(src.Path), outName)
|
||||
|
||||
job := &queue.Job{
|
||||
Type: queue.JobTypeSnippet,
|
||||
Title: "Snippet: " + filepath.Base(src.Path),
|
||||
Description: fmt.Sprintf("%ds snippet centred on midpoint (source settings)", state.snippetLength),
|
||||
Description: fmt.Sprintf("%ds snippet centred on midpoint (%s)", state.snippetLength, modeDesc),
|
||||
InputFile: src.Path,
|
||||
OutputFile: outPath,
|
||||
Config: map[string]interface{}{
|
||||
"inputPath": src.Path,
|
||||
"outputPath": outPath,
|
||||
"snippetLength": float64(state.snippetLength),
|
||||
"inputPath": src.Path,
|
||||
"outputPath": outPath,
|
||||
"snippetLength": float64(state.snippetLength),
|
||||
"useSourceFormat": state.snippetSourceFormat,
|
||||
},
|
||||
}
|
||||
state.jobQueue.Add(job)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user