Simplify player view and enable drop-to-play
This commit is contained in:
parent
5cc42c9ca0
commit
d5458f7050
|
|
@ -61,3 +61,9 @@ func HandleCompare(files []string) {
|
||||||
logging.Debug(logging.CatModule, "compare handler invoked with %v", files)
|
logging.Debug(logging.CatModule, "compare handler invoked with %v", files)
|
||||||
fmt.Println("compare", files)
|
fmt.Println("compare", files)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandlePlayer handles the player module
|
||||||
|
func HandlePlayer(files []string) {
|
||||||
|
logging.Debug(logging.CatModule, "player handler invoked with %v", files)
|
||||||
|
fmt.Println("player", files)
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,13 +25,20 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro
|
||||||
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
||||||
title.TextSize = 28
|
title.TextSize = 28
|
||||||
|
|
||||||
queueTile := buildQueueTile(queueCompleted, queueTotal, queueColor, textColor, onQueueClick)
|
var header fyne.CanvasObject
|
||||||
|
if onQueueClick != nil {
|
||||||
header := container.New(layout.NewHBoxLayout(),
|
queueTile := buildQueueTile(queueCompleted, queueTotal, queueColor, textColor, onQueueClick)
|
||||||
title,
|
header = container.New(layout.NewHBoxLayout(),
|
||||||
layout.NewSpacer(),
|
title,
|
||||||
queueTile,
|
layout.NewSpacer(),
|
||||||
)
|
queueTile,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
header = container.New(layout.NewHBoxLayout(),
|
||||||
|
title,
|
||||||
|
layout.NewSpacer(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var tileObjects []fyne.CanvasObject
|
var tileObjects []fyne.CanvasObject
|
||||||
for _, mod := range modules {
|
for _, mod := range modules {
|
||||||
|
|
|
||||||
791
main.go
791
main.go
|
|
@ -60,15 +60,7 @@ var (
|
||||||
queueColor = utils.MustHex("#5961FF")
|
queueColor = utils.MustHex("#5961FF")
|
||||||
|
|
||||||
modulesList = []Module{
|
modulesList = []Module{
|
||||||
{"convert", "Convert", utils.MustHex("#8B44FF"), modules.HandleConvert}, // Violet
|
{"player", "Player", utils.MustHex("#4CE870"), modules.HandlePlayer},
|
||||||
{"merge", "Merge", utils.MustHex("#4488FF"), modules.HandleMerge}, // Blue
|
|
||||||
{"trim", "Trim", utils.MustHex("#44DDFF"), modules.HandleTrim}, // Cyan
|
|
||||||
{"filters", "Filters", utils.MustHex("#44FF88"), modules.HandleFilters}, // Green
|
|
||||||
{"upscale", "Upscale", utils.MustHex("#AAFF44"), modules.HandleUpscale}, // Yellow-Green
|
|
||||||
{"audio", "Audio", utils.MustHex("#FFD744"), modules.HandleAudio}, // Yellow
|
|
||||||
{"thumb", "Thumb", utils.MustHex("#FF8844"), modules.HandleThumb}, // Orange
|
|
||||||
{"compare", "Compare", utils.MustHex("#FF44AA"), modules.HandleCompare}, // Pink
|
|
||||||
{"inspect", "Inspect", utils.MustHex("#FF4444"), modules.HandleInspect}, // Red
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -117,33 +109,33 @@ type convertConfig struct {
|
||||||
Mode string // Simple or Advanced
|
Mode string // Simple or Advanced
|
||||||
|
|
||||||
// Video encoding settings
|
// Video encoding settings
|
||||||
VideoCodec string // H.264, H.265, VP9, AV1, Copy
|
VideoCodec string // H.264, H.265, VP9, AV1, Copy
|
||||||
EncoderPreset string // ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow
|
EncoderPreset string // ultrafast, superfast, veryfast, faster, fast, medium, slow, slower, veryslow
|
||||||
CRF string // Manual CRF value (0-51, or empty to use Quality preset)
|
CRF string // Manual CRF value (0-51, or empty to use Quality preset)
|
||||||
BitrateMode string // CRF, CBR, VBR, "Target Size"
|
BitrateMode string // CRF, CBR, VBR, "Target Size"
|
||||||
VideoBitrate string // For CBR/VBR modes (e.g., "5000k")
|
VideoBitrate string // For CBR/VBR modes (e.g., "5000k")
|
||||||
TargetFileSize string // Target file size (e.g., "25MB", "100MB") - requires BitrateMode="Target Size"
|
TargetFileSize string // Target file size (e.g., "25MB", "100MB") - requires BitrateMode="Target Size"
|
||||||
TargetResolution string // Source, 720p, 1080p, 1440p, 4K, or custom
|
TargetResolution string // Source, 720p, 1080p, 1440p, 4K, or custom
|
||||||
FrameRate string // Source, 24, 30, 60, or custom
|
FrameRate string // Source, 24, 30, 60, or custom
|
||||||
PixelFormat string // yuv420p, yuv422p, yuv444p
|
PixelFormat string // yuv420p, yuv422p, yuv444p
|
||||||
HardwareAccel string // none, nvenc, vaapi, qsv, videotoolbox
|
HardwareAccel string // none, nvenc, vaapi, qsv, videotoolbox
|
||||||
TwoPass bool // Enable two-pass encoding for VBR
|
TwoPass bool // Enable two-pass encoding for VBR
|
||||||
H264Profile string // baseline, main, high (for H.264 compatibility)
|
H264Profile string // baseline, main, high (for H.264 compatibility)
|
||||||
H264Level string // 3.0, 3.1, 4.0, 4.1, 5.0, 5.1 (for H.264 compatibility)
|
H264Level string // 3.0, 3.1, 4.0, 4.1, 5.0, 5.1 (for H.264 compatibility)
|
||||||
Deinterlace string // Auto, Force, Off
|
Deinterlace string // Auto, Force, Off
|
||||||
DeinterlaceMethod string // yadif, bwdif (bwdif is higher quality but slower)
|
DeinterlaceMethod string // yadif, bwdif (bwdif is higher quality but slower)
|
||||||
AutoCrop bool // Auto-detect and remove black bars
|
AutoCrop bool // Auto-detect and remove black bars
|
||||||
CropWidth string // Manual crop width (empty = use auto-detect)
|
CropWidth string // Manual crop width (empty = use auto-detect)
|
||||||
CropHeight string // Manual crop height (empty = use auto-detect)
|
CropHeight string // Manual crop height (empty = use auto-detect)
|
||||||
CropX string // Manual crop X offset (empty = use auto-detect)
|
CropX string // Manual crop X offset (empty = use auto-detect)
|
||||||
CropY string // Manual crop Y offset (empty = use auto-detect)
|
CropY string // Manual crop Y offset (empty = use auto-detect)
|
||||||
|
|
||||||
// Audio encoding settings
|
// Audio encoding settings
|
||||||
AudioCodec string // AAC, Opus, MP3, FLAC, Copy
|
AudioCodec string // AAC, Opus, MP3, FLAC, Copy
|
||||||
AudioBitrate string // 128k, 192k, 256k, 320k
|
AudioBitrate string // 128k, 192k, 256k, 320k
|
||||||
AudioChannels string // Source, Mono, Stereo, 5.1
|
AudioChannels string // Source, Mono, Stereo, 5.1
|
||||||
AudioSampleRate string // Source, 44100, 48000
|
AudioSampleRate string // Source, 44100, 48000
|
||||||
NormalizeAudio bool // Force stereo + 48kHz for compatibility
|
NormalizeAudio bool // Force stereo + 48kHz for compatibility
|
||||||
|
|
||||||
// Other settings
|
// Other settings
|
||||||
InverseTelecine bool
|
InverseTelecine bool
|
||||||
|
|
@ -419,46 +411,265 @@ func (s *appState) showErrorWithCopy(title string, err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) showMainMenu() {
|
func (s *appState) showMainMenu() {
|
||||||
|
// Minimal entry point: go straight to the player view.
|
||||||
|
s.showPlayerView()
|
||||||
|
}
|
||||||
|
|
||||||
|
// showPlayerView renders the player-focused UI with a lightweight playlist.
|
||||||
|
func (s *appState) showPlayerView() {
|
||||||
s.stopPreview()
|
s.stopPreview()
|
||||||
s.stopPlayer()
|
s.stopPlayer()
|
||||||
s.active = ""
|
s.active = "player"
|
||||||
|
|
||||||
// Convert Module slice to ui.ModuleInfo slice
|
header := widget.NewLabelWithStyle("VT Player", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||||
var mods []ui.ModuleInfo
|
|
||||||
for _, m := range modulesList {
|
// Helper to refresh the view after selection/loads.
|
||||||
mods = append(mods, ui.ModuleInfo{
|
refresh := func() {
|
||||||
ID: m.ID,
|
s.showPlayerView()
|
||||||
Label: m.Label,
|
}
|
||||||
Color: m.Color,
|
|
||||||
Enabled: m.ID == "convert" || m.ID == "compare", // Convert and compare modules are functional
|
openFile := widget.NewButton("Open File…", func() {
|
||||||
|
dlg := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := r.URI().Path()
|
||||||
|
r.Close()
|
||||||
|
go s.loadVideo(path)
|
||||||
|
}, s.window)
|
||||||
|
dlg.Resize(fyne.NewSize(700, 480))
|
||||||
|
dlg.Show()
|
||||||
|
})
|
||||||
|
|
||||||
|
addFolder := widget.NewButton("Add Folder…", func() {
|
||||||
|
dlg := dialog.NewFolderOpen(func(l fyne.ListableURI, err error) {
|
||||||
|
if err != nil || l == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
paths := s.findVideoFiles(l.Path())
|
||||||
|
if len(paths) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go s.loadVideos(paths)
|
||||||
|
}, s.window)
|
||||||
|
dlg.Resize(fyne.NewSize(700, 480))
|
||||||
|
dlg.Show()
|
||||||
|
})
|
||||||
|
|
||||||
|
clearList := widget.NewButton("Clear Playlist", func() {
|
||||||
|
s.clearVideo()
|
||||||
|
refresh()
|
||||||
|
})
|
||||||
|
clearList.Importance = widget.LowImportance
|
||||||
|
|
||||||
|
controlsBar := container.NewHBox(openFile, addFolder, clearList, layout.NewSpacer())
|
||||||
|
|
||||||
|
// Player area
|
||||||
|
var playerArea fyne.CanvasObject
|
||||||
|
if s.source == nil {
|
||||||
|
bg := canvas.NewRectangle(utils.MustHex("#05070C"))
|
||||||
|
bg.SetMinSize(fyne.NewSize(960, 540))
|
||||||
|
|
||||||
|
// Minimal play icon stack
|
||||||
|
outer := canvas.NewCircle(utils.MustHex("#2A1540"))
|
||||||
|
outer.Resize(fyne.NewSize(120, 120))
|
||||||
|
middle := canvas.NewCircle(utils.MustHex("#5A2E7A"))
|
||||||
|
middle.Resize(fyne.NewSize(96, 96))
|
||||||
|
inner := canvas.NewCircle(utils.MustHex("#7F45B0"))
|
||||||
|
inner.Resize(fyne.NewSize(74, 74))
|
||||||
|
playTri := canvas.NewPolygon(3, color.NRGBA{R: 232, G: 239, B: 255, A: 255})
|
||||||
|
playTri.StrokeColor = color.Transparent
|
||||||
|
playTri.Resize(fyne.NewSize(36, 36))
|
||||||
|
|
||||||
|
icon := container.NewMax(
|
||||||
|
outer,
|
||||||
|
container.NewCenter(middle),
|
||||||
|
container.NewCenter(inner),
|
||||||
|
container.NewCenter(playTri),
|
||||||
|
)
|
||||||
|
|
||||||
|
loadBtn := widget.NewButton("Load Video", func() {
|
||||||
|
dlg := dialog.NewFileOpen(func(r fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || r == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := r.URI().Path()
|
||||||
|
r.Close()
|
||||||
|
go s.loadVideo(path)
|
||||||
|
}, s.window)
|
||||||
|
dlg.Resize(fyne.NewSize(700, 480))
|
||||||
|
dlg.Show()
|
||||||
})
|
})
|
||||||
|
loadBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
hint := widget.NewLabel("Drop files or URLs to play here.")
|
||||||
|
hint.Alignment = fyne.TextAlignCenter
|
||||||
|
|
||||||
|
centerStack := container.NewVBox(
|
||||||
|
container.NewCenter(icon),
|
||||||
|
layout.NewSpacer(),
|
||||||
|
container.NewCenter(loadBtn),
|
||||||
|
container.NewCenter(hint),
|
||||||
|
)
|
||||||
|
|
||||||
|
playerArea = container.NewMax(bg, container.NewCenter(centerStack))
|
||||||
|
} else {
|
||||||
|
// Playlist panel (only when we have media loaded)
|
||||||
|
playlist := widget.NewList(
|
||||||
|
func() int { return len(s.loadedVideos) },
|
||||||
|
func() fyne.CanvasObject { return widget.NewLabel("item") },
|
||||||
|
func(id widget.ListItemID, o fyne.CanvasObject) {
|
||||||
|
if id >= 0 && id < len(s.loadedVideos) {
|
||||||
|
o.(*widget.Label).SetText(filepath.Base(s.loadedVideos[id].Path))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
playlist.OnSelected = func(id widget.ListItemID) {
|
||||||
|
if id >= 0 && id < len(s.loadedVideos) {
|
||||||
|
s.switchToVideo(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(s.loadedVideos) > 0 && s.currentIndex < len(s.loadedVideos) {
|
||||||
|
playlist.Select(s.currentIndex)
|
||||||
|
}
|
||||||
|
playlistPanel := container.NewBorder(
|
||||||
|
widget.NewLabelWithStyle("Playlist", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
|
nil, nil, nil,
|
||||||
|
playlist,
|
||||||
|
)
|
||||||
|
|
||||||
|
src := s.source
|
||||||
|
// Image surface
|
||||||
|
stageBG := canvas.NewRectangle(utils.MustHex("#0F1529"))
|
||||||
|
stageBG.SetMinSize(fyne.NewSize(960, 540))
|
||||||
|
videoImg := canvas.NewImageFromResource(nil)
|
||||||
|
videoImg.FillMode = canvas.ImageFillContain
|
||||||
|
stage := container.NewMax(stageBG, videoImg)
|
||||||
|
|
||||||
|
currentTime := widget.NewLabel("0:00")
|
||||||
|
totalTime := widget.NewLabel(src.DurationString())
|
||||||
|
totalTime.Alignment = fyne.TextAlignTrailing
|
||||||
|
slider := widget.NewSlider(0, math.Max(1, src.Duration))
|
||||||
|
slider.Step = 0.5
|
||||||
|
var updatingProgress bool
|
||||||
|
|
||||||
|
updateProgress := func(val float64) {
|
||||||
|
fyne.Do(func() {
|
||||||
|
updatingProgress = true
|
||||||
|
currentTime.SetText(formatClock(val))
|
||||||
|
slider.SetValue(val)
|
||||||
|
updatingProgress = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var ensureSession func() bool
|
||||||
|
ensureSession = func() bool {
|
||||||
|
if s.playSess == nil {
|
||||||
|
s.playSess = newPlaySession(src.Path, src.Width, src.Height, src.FrameRate, 960, 540, updateProgress, videoImg)
|
||||||
|
s.playSess.SetVolume(s.playerVolume)
|
||||||
|
s.playerPaused = true
|
||||||
|
}
|
||||||
|
return s.playSess != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
slider.OnChanged = func(val float64) {
|
||||||
|
if updatingProgress {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateProgress(val)
|
||||||
|
if ensureSession() {
|
||||||
|
s.playSess.Seek(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
playBtn := utils.MakeIconButton("▶/⏸", "Play/Pause", func() {
|
||||||
|
if !ensureSession() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.playerPaused {
|
||||||
|
s.playSess.Play()
|
||||||
|
s.playerPaused = false
|
||||||
|
} else {
|
||||||
|
s.playSess.Pause()
|
||||||
|
s.playerPaused = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
prevBtn := utils.MakeIconButton("⏮", "Previous", func() {
|
||||||
|
s.prevVideo()
|
||||||
|
})
|
||||||
|
nextBtn := utils.MakeIconButton("⏭", "Next", func() {
|
||||||
|
s.nextVideo()
|
||||||
|
})
|
||||||
|
|
||||||
|
var volIcon *widget.Button
|
||||||
|
volIcon = utils.MakeIconButton("🔊", "Mute/Unmute", func() {
|
||||||
|
if !ensureSession() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.playerMuted {
|
||||||
|
target := s.lastVolume
|
||||||
|
if target <= 0 {
|
||||||
|
target = 50
|
||||||
|
}
|
||||||
|
s.playerVolume = target
|
||||||
|
s.playerMuted = false
|
||||||
|
s.playSess.SetVolume(target)
|
||||||
|
} else {
|
||||||
|
s.lastVolume = s.playerVolume
|
||||||
|
s.playerVolume = 0
|
||||||
|
s.playerMuted = true
|
||||||
|
s.playSess.SetVolume(0)
|
||||||
|
}
|
||||||
|
if s.playerMuted || s.playerVolume <= 0 {
|
||||||
|
volIcon.SetText("🔇")
|
||||||
|
} else {
|
||||||
|
volIcon.SetText("🔊")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
volSlider := widget.NewSlider(0, 100)
|
||||||
|
volSlider.Step = 1
|
||||||
|
volSlider.Value = s.playerVolume
|
||||||
|
volSlider.OnChanged = func(val float64) {
|
||||||
|
s.playerVolume = val
|
||||||
|
if val > 0 {
|
||||||
|
s.lastVolume = val
|
||||||
|
s.playerMuted = false
|
||||||
|
} else {
|
||||||
|
s.playerMuted = true
|
||||||
|
}
|
||||||
|
if ensureSession() {
|
||||||
|
s.playSess.SetVolume(val)
|
||||||
|
}
|
||||||
|
if s.playerMuted || s.playerVolume <= 0 {
|
||||||
|
volIcon.SetText("🔇")
|
||||||
|
} else {
|
||||||
|
volIcon.SetText("🔊")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
progressBar := container.NewBorder(nil, nil, currentTime, totalTime, container.NewMax(slider))
|
||||||
|
controlRow := container.NewHBox(prevBtn, playBtn, nextBtn, layout.NewSpacer(), volIcon, container.NewMax(volSlider))
|
||||||
|
|
||||||
|
playerArea = container.NewBorder(
|
||||||
|
nil,
|
||||||
|
container.NewVBox(container.NewPadded(progressBar), container.NewPadded(controlRow)),
|
||||||
|
playlistPanel,
|
||||||
|
nil,
|
||||||
|
container.NewPadded(stage),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
titleColor := utils.MustHex("#4CE870")
|
mainPanel := container.NewBorder(
|
||||||
|
container.NewVBox(header, controlsBar, widget.NewSeparator()),
|
||||||
// Get queue stats - show completed jobs out of total
|
nil,
|
||||||
var queueCompleted, queueTotal int
|
nil,
|
||||||
if s.jobQueue != nil {
|
nil,
|
||||||
_, _, completed, _ := s.jobQueue.Stats()
|
playerArea,
|
||||||
queueCompleted = completed
|
|
||||||
queueTotal = len(s.jobQueue.List())
|
|
||||||
}
|
|
||||||
|
|
||||||
menu := ui.BuildMainMenu(mods, s.showModule, s.handleModuleDrop, s.showQueue, titleColor, queueColor, textColor, queueCompleted, queueTotal)
|
|
||||||
|
|
||||||
// Update stats bar
|
|
||||||
s.updateStatsBar()
|
|
||||||
|
|
||||||
// Add stats bar at the bottom of the menu
|
|
||||||
content := container.NewBorder(
|
|
||||||
nil, // top
|
|
||||||
s.statsBar, // bottom
|
|
||||||
nil, // left
|
|
||||||
nil, // right
|
|
||||||
container.NewPadded(menu), // center
|
|
||||||
)
|
)
|
||||||
|
|
||||||
s.setContent(content)
|
s.setContent(mainPanel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) showQueue() {
|
func (s *appState) showQueue() {
|
||||||
|
|
@ -597,45 +808,45 @@ func (s *appState) addConvertToQueue() error {
|
||||||
|
|
||||||
// Create job config map
|
// Create job config map
|
||||||
config := map[string]interface{}{
|
config := map[string]interface{}{
|
||||||
"inputPath": src.Path,
|
"inputPath": src.Path,
|
||||||
"outputPath": outPath,
|
"outputPath": outPath,
|
||||||
"outputBase": cfg.OutputBase,
|
"outputBase": cfg.OutputBase,
|
||||||
"selectedFormat": cfg.SelectedFormat,
|
"selectedFormat": cfg.SelectedFormat,
|
||||||
"quality": cfg.Quality,
|
"quality": cfg.Quality,
|
||||||
"mode": cfg.Mode,
|
"mode": cfg.Mode,
|
||||||
"videoCodec": cfg.VideoCodec,
|
"videoCodec": cfg.VideoCodec,
|
||||||
"encoderPreset": cfg.EncoderPreset,
|
"encoderPreset": cfg.EncoderPreset,
|
||||||
"crf": cfg.CRF,
|
"crf": cfg.CRF,
|
||||||
"bitrateMode": cfg.BitrateMode,
|
"bitrateMode": cfg.BitrateMode,
|
||||||
"videoBitrate": cfg.VideoBitrate,
|
"videoBitrate": cfg.VideoBitrate,
|
||||||
"targetFileSize": cfg.TargetFileSize,
|
"targetFileSize": cfg.TargetFileSize,
|
||||||
"targetResolution": cfg.TargetResolution,
|
"targetResolution": cfg.TargetResolution,
|
||||||
"frameRate": cfg.FrameRate,
|
"frameRate": cfg.FrameRate,
|
||||||
"pixelFormat": cfg.PixelFormat,
|
"pixelFormat": cfg.PixelFormat,
|
||||||
"hardwareAccel": cfg.HardwareAccel,
|
"hardwareAccel": cfg.HardwareAccel,
|
||||||
"twoPass": cfg.TwoPass,
|
"twoPass": cfg.TwoPass,
|
||||||
"h264Profile": cfg.H264Profile,
|
"h264Profile": cfg.H264Profile,
|
||||||
"h264Level": cfg.H264Level,
|
"h264Level": cfg.H264Level,
|
||||||
"deinterlace": cfg.Deinterlace,
|
"deinterlace": cfg.Deinterlace,
|
||||||
"deinterlaceMethod": cfg.DeinterlaceMethod,
|
"deinterlaceMethod": cfg.DeinterlaceMethod,
|
||||||
"autoCrop": cfg.AutoCrop,
|
"autoCrop": cfg.AutoCrop,
|
||||||
"cropWidth": cfg.CropWidth,
|
"cropWidth": cfg.CropWidth,
|
||||||
"cropHeight": cfg.CropHeight,
|
"cropHeight": cfg.CropHeight,
|
||||||
"cropX": cfg.CropX,
|
"cropX": cfg.CropX,
|
||||||
"cropY": cfg.CropY,
|
"cropY": cfg.CropY,
|
||||||
"audioCodec": cfg.AudioCodec,
|
"audioCodec": cfg.AudioCodec,
|
||||||
"audioBitrate": cfg.AudioBitrate,
|
"audioBitrate": cfg.AudioBitrate,
|
||||||
"audioChannels": cfg.AudioChannels,
|
"audioChannels": cfg.AudioChannels,
|
||||||
"audioSampleRate": cfg.AudioSampleRate,
|
"audioSampleRate": cfg.AudioSampleRate,
|
||||||
"normalizeAudio": cfg.NormalizeAudio,
|
"normalizeAudio": cfg.NormalizeAudio,
|
||||||
"inverseTelecine": cfg.InverseTelecine,
|
"inverseTelecine": cfg.InverseTelecine,
|
||||||
"coverArtPath": cfg.CoverArtPath,
|
"coverArtPath": cfg.CoverArtPath,
|
||||||
"aspectHandling": cfg.AspectHandling,
|
"aspectHandling": cfg.AspectHandling,
|
||||||
"outputAspect": cfg.OutputAspect,
|
"outputAspect": cfg.OutputAspect,
|
||||||
"sourceWidth": src.Width,
|
"sourceWidth": src.Width,
|
||||||
"sourceHeight": src.Height,
|
"sourceHeight": src.Height,
|
||||||
"sourceDuration": src.Duration,
|
"sourceDuration": src.Duration,
|
||||||
"fieldOrder": src.FieldOrder,
|
"fieldOrder": src.FieldOrder,
|
||||||
}
|
}
|
||||||
|
|
||||||
job := &queue.Job{
|
job := &queue.Job{
|
||||||
|
|
@ -655,10 +866,8 @@ func (s *appState) addConvertToQueue() error {
|
||||||
|
|
||||||
func (s *appState) showModule(id string) {
|
func (s *appState) showModule(id string) {
|
||||||
switch id {
|
switch id {
|
||||||
case "convert":
|
case "player":
|
||||||
s.showConvertView(nil)
|
s.showPlayerView()
|
||||||
case "compare":
|
|
||||||
s.showCompareView()
|
|
||||||
default:
|
default:
|
||||||
logging.Debug(logging.CatUI, "UI module %s not wired yet", id)
|
logging.Debug(logging.CatUI, "UI module %s not wired yet", id)
|
||||||
}
|
}
|
||||||
|
|
@ -697,70 +906,15 @@ func (s *appState) handleModuleDrop(moduleID string, items []fyne.URI) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If convert module and multiple files, add all to queue
|
// Player: if multiple files, load as playlist; otherwise load single.
|
||||||
if moduleID == "convert" && len(videoPaths) > 1 {
|
if len(videoPaths) > 1 {
|
||||||
go s.batchAddToQueue(videoPaths)
|
go s.loadVideos(videoPaths)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If compare module, load up to 2 videos into compare slots
|
|
||||||
if moduleID == "compare" {
|
|
||||||
go func() {
|
|
||||||
// Load first video
|
|
||||||
src1, err := probeVideo(videoPaths[0])
|
|
||||||
if err != nil {
|
|
||||||
logging.Debug(logging.CatModule, "failed to load first video for compare: %v", err)
|
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
||||||
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), s.window)
|
|
||||||
}, false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load second video if available
|
|
||||||
var src2 *videoSource
|
|
||||||
if len(videoPaths) >= 2 {
|
|
||||||
src2, err = probeVideo(videoPaths[1])
|
|
||||||
if err != nil {
|
|
||||||
logging.Debug(logging.CatModule, "failed to load second video for compare: %v", err)
|
|
||||||
// Continue with just first video
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show dialog if more than 2 videos
|
|
||||||
if len(videoPaths) > 2 {
|
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
||||||
dialog.ShowInformation("Compare Videos",
|
|
||||||
fmt.Sprintf("You dropped %d videos. Only the first two will be loaded for comparison.", len(videoPaths)),
|
|
||||||
s.window)
|
|
||||||
}, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update state and show module (with small delay to allow flash animation to be seen)
|
|
||||||
time.Sleep(350 * time.Millisecond)
|
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
||||||
s.compareFile1 = src1
|
|
||||||
s.compareFile2 = src2
|
|
||||||
s.showModule(moduleID)
|
|
||||||
logging.Debug(logging.CatModule, "loaded %d video(s) for compare module", len(videoPaths))
|
|
||||||
}, false)
|
|
||||||
}()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single file or non-convert module: load first video and show module
|
|
||||||
path := videoPaths[0]
|
path := videoPaths[0]
|
||||||
logging.Debug(logging.CatModule, "drop on module %s path=%s - starting load", moduleID, path)
|
logging.Debug(logging.CatModule, "drop on module %s path=%s - starting load", moduleID, path)
|
||||||
|
go s.loadVideo(path)
|
||||||
go func() {
|
|
||||||
logging.Debug(logging.CatModule, "loading video in goroutine")
|
|
||||||
s.loadVideo(path)
|
|
||||||
// After loading, switch to the module (with small delay to allow flash animation)
|
|
||||||
time.Sleep(350 * time.Millisecond)
|
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
||||||
logging.Debug(logging.CatModule, "showing module %s after load", moduleID)
|
|
||||||
s.showModule(moduleID)
|
|
||||||
}, false)
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isVideoFile checks if a file has a video extension
|
// isVideoFile checks if a file has a video extension
|
||||||
|
|
@ -827,39 +981,39 @@ func (s *appState) batchAddToQueue(paths []string) {
|
||||||
outPath := filepath.Join(outDir, outName)
|
outPath := filepath.Join(outDir, outName)
|
||||||
|
|
||||||
config := map[string]interface{}{
|
config := map[string]interface{}{
|
||||||
"inputPath": path,
|
"inputPath": path,
|
||||||
"outputPath": outPath,
|
"outputPath": outPath,
|
||||||
"outputBase": baseName + "-converted",
|
"outputBase": baseName + "-converted",
|
||||||
"selectedFormat": s.convert.SelectedFormat,
|
"selectedFormat": s.convert.SelectedFormat,
|
||||||
"quality": s.convert.Quality,
|
"quality": s.convert.Quality,
|
||||||
"mode": s.convert.Mode,
|
"mode": s.convert.Mode,
|
||||||
"videoCodec": s.convert.VideoCodec,
|
"videoCodec": s.convert.VideoCodec,
|
||||||
"encoderPreset": s.convert.EncoderPreset,
|
"encoderPreset": s.convert.EncoderPreset,
|
||||||
"crf": s.convert.CRF,
|
"crf": s.convert.CRF,
|
||||||
"bitrateMode": s.convert.BitrateMode,
|
"bitrateMode": s.convert.BitrateMode,
|
||||||
"videoBitrate": s.convert.VideoBitrate,
|
"videoBitrate": s.convert.VideoBitrate,
|
||||||
"targetResolution": s.convert.TargetResolution,
|
"targetResolution": s.convert.TargetResolution,
|
||||||
"frameRate": s.convert.FrameRate,
|
"frameRate": s.convert.FrameRate,
|
||||||
"pixelFormat": s.convert.PixelFormat,
|
"pixelFormat": s.convert.PixelFormat,
|
||||||
"hardwareAccel": s.convert.HardwareAccel,
|
"hardwareAccel": s.convert.HardwareAccel,
|
||||||
"twoPass": s.convert.TwoPass,
|
"twoPass": s.convert.TwoPass,
|
||||||
"h264Profile": s.convert.H264Profile,
|
"h264Profile": s.convert.H264Profile,
|
||||||
"h264Level": s.convert.H264Level,
|
"h264Level": s.convert.H264Level,
|
||||||
"deinterlace": s.convert.Deinterlace,
|
"deinterlace": s.convert.Deinterlace,
|
||||||
"deinterlaceMethod": s.convert.DeinterlaceMethod,
|
"deinterlaceMethod": s.convert.DeinterlaceMethod,
|
||||||
"audioCodec": s.convert.AudioCodec,
|
"audioCodec": s.convert.AudioCodec,
|
||||||
"audioBitrate": s.convert.AudioBitrate,
|
"audioBitrate": s.convert.AudioBitrate,
|
||||||
"audioChannels": s.convert.AudioChannels,
|
"audioChannels": s.convert.AudioChannels,
|
||||||
"audioSampleRate": s.convert.AudioSampleRate,
|
"audioSampleRate": s.convert.AudioSampleRate,
|
||||||
"normalizeAudio": s.convert.NormalizeAudio,
|
"normalizeAudio": s.convert.NormalizeAudio,
|
||||||
"inverseTelecine": s.convert.InverseTelecine,
|
"inverseTelecine": s.convert.InverseTelecine,
|
||||||
"coverArtPath": "",
|
"coverArtPath": "",
|
||||||
"aspectHandling": s.convert.AspectHandling,
|
"aspectHandling": s.convert.AspectHandling,
|
||||||
"outputAspect": s.convert.OutputAspect,
|
"outputAspect": s.convert.OutputAspect,
|
||||||
"sourceWidth": src.Width,
|
"sourceWidth": src.Width,
|
||||||
"sourceHeight": src.Height,
|
"sourceHeight": src.Height,
|
||||||
"sourceDuration": src.Duration,
|
"sourceDuration": src.Duration,
|
||||||
"fieldOrder": src.FieldOrder,
|
"fieldOrder": src.FieldOrder,
|
||||||
}
|
}
|
||||||
|
|
||||||
job := &queue.Job{
|
job := &queue.Job{
|
||||||
|
|
@ -1398,10 +1552,10 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
||||||
isHardwareFailure := strings.Contains(stderrOutput, "No capable devices found") ||
|
isHardwareFailure := strings.Contains(stderrOutput, "No capable devices found") ||
|
||||||
strings.Contains(stderrOutput, "Cannot load") ||
|
strings.Contains(stderrOutput, "Cannot load") ||
|
||||||
strings.Contains(stderrOutput, "not available") &&
|
strings.Contains(stderrOutput, "not available") &&
|
||||||
(strings.Contains(stderrOutput, "nvenc") ||
|
(strings.Contains(stderrOutput, "nvenc") ||
|
||||||
strings.Contains(stderrOutput, "qsv") ||
|
strings.Contains(stderrOutput, "qsv") ||
|
||||||
strings.Contains(stderrOutput, "vaapi") ||
|
strings.Contains(stderrOutput, "vaapi") ||
|
||||||
strings.Contains(stderrOutput, "videotoolbox"))
|
strings.Contains(stderrOutput, "videotoolbox"))
|
||||||
|
|
||||||
if isHardwareFailure && hardwareAccel != "none" && hardwareAccel != "" {
|
if isHardwareFailure && hardwareAccel != "none" && hardwareAccel != "" {
|
||||||
logging.Debug(logging.CatFFMPEG, "hardware encoding failed, will suggest software fallback")
|
logging.Debug(logging.CatFFMPEG, "hardware encoding failed, will suggest software fallback")
|
||||||
|
|
@ -1525,28 +1679,28 @@ func runGUI() {
|
||||||
Mode: "Simple",
|
Mode: "Simple",
|
||||||
|
|
||||||
// Video encoding defaults
|
// Video encoding defaults
|
||||||
VideoCodec: "H.264",
|
VideoCodec: "H.264",
|
||||||
EncoderPreset: "medium",
|
EncoderPreset: "medium",
|
||||||
CRF: "", // Empty means use Quality preset
|
CRF: "", // Empty means use Quality preset
|
||||||
BitrateMode: "CRF",
|
BitrateMode: "CRF",
|
||||||
VideoBitrate: "5000k",
|
VideoBitrate: "5000k",
|
||||||
TargetResolution: "Source",
|
TargetResolution: "Source",
|
||||||
FrameRate: "Source",
|
FrameRate: "Source",
|
||||||
PixelFormat: "yuv420p",
|
PixelFormat: "yuv420p",
|
||||||
HardwareAccel: "none",
|
HardwareAccel: "none",
|
||||||
TwoPass: false,
|
TwoPass: false,
|
||||||
H264Profile: "main",
|
H264Profile: "main",
|
||||||
H264Level: "4.0",
|
H264Level: "4.0",
|
||||||
Deinterlace: "Auto",
|
Deinterlace: "Auto",
|
||||||
DeinterlaceMethod: "bwdif",
|
DeinterlaceMethod: "bwdif",
|
||||||
AutoCrop: false,
|
AutoCrop: false,
|
||||||
|
|
||||||
// Audio encoding defaults
|
// Audio encoding defaults
|
||||||
AudioCodec: "AAC",
|
AudioCodec: "AAC",
|
||||||
AudioBitrate: "192k",
|
AudioBitrate: "192k",
|
||||||
AudioChannels: "Source",
|
AudioChannels: "Source",
|
||||||
AudioSampleRate: "Source",
|
AudioSampleRate: "Source",
|
||||||
NormalizeAudio: false,
|
NormalizeAudio: false,
|
||||||
|
|
||||||
// Other defaults
|
// Other defaults
|
||||||
InverseTelecine: true,
|
InverseTelecine: true,
|
||||||
|
|
@ -1585,7 +1739,7 @@ func runGUI() {
|
||||||
|
|
||||||
defer state.shutdown()
|
defer state.shutdown()
|
||||||
w.SetOnDropped(func(pos fyne.Position, items []fyne.URI) {
|
w.SetOnDropped(func(pos fyne.Position, items []fyne.URI) {
|
||||||
state.handleDrop(pos, items)
|
state.handleDropPlayer(items)
|
||||||
})
|
})
|
||||||
state.showMainMenu()
|
state.showMainMenu()
|
||||||
logging.Debug(logging.CatUI, "main menu rendered with %d modules", len(modulesList))
|
logging.Debug(logging.CatUI, "main menu rendered with %d modules", len(modulesList))
|
||||||
|
|
@ -3616,159 +3770,38 @@ func (s *appState) importCoverImage(path string) (string, error) {
|
||||||
return dest, nil
|
return dest, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) handleDrop(pos fyne.Position, items []fyne.URI) {
|
// handleDropPlayer accepts dropped files/folders anywhere and loads them into the playlist/player.
|
||||||
|
func (s *appState) handleDropPlayer(items []fyne.URI) {
|
||||||
if len(items) == 0 {
|
if len(items) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If on main menu, detect which module tile was dropped on
|
var videoPaths []string
|
||||||
if s.active == "" {
|
for _, uri := range items {
|
||||||
moduleID := s.detectModuleTileAtPosition(pos)
|
if uri.Scheme() != "file" {
|
||||||
if moduleID != "" {
|
continue
|
||||||
logging.Debug(logging.CatUI, "drop on main menu tile=%s", moduleID)
|
|
||||||
s.handleModuleDrop(moduleID, items)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
logging.Debug(logging.CatUI, "drop on main menu but not over any module tile")
|
path := uri.Path()
|
||||||
|
logging.Debug(logging.CatModule, "drop received path=%s", path)
|
||||||
|
|
||||||
|
if info, err := os.Stat(path); err == nil && info.IsDir() {
|
||||||
|
videos := s.findVideoFiles(path)
|
||||||
|
videoPaths = append(videoPaths, videos...)
|
||||||
|
} else if s.isVideoFile(path) {
|
||||||
|
videoPaths = append(videoPaths, path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(videoPaths) == 0 {
|
||||||
|
logging.Debug(logging.CatUI, "no valid video files in dropped items")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If in convert module, handle all files
|
if len(videoPaths) > 1 {
|
||||||
if s.active == "convert" {
|
go s.loadVideos(videoPaths)
|
||||||
// Collect all video files from the dropped items
|
} else {
|
||||||
var videoPaths []string
|
go s.loadVideo(videoPaths[0])
|
||||||
for _, uri := range items {
|
|
||||||
if uri.Scheme() != "file" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
path := uri.Path()
|
|
||||||
logging.Debug(logging.CatModule, "drop received path=%s", path)
|
|
||||||
|
|
||||||
// Check if it's a directory
|
|
||||||
if info, err := os.Stat(path); err == nil && info.IsDir() {
|
|
||||||
logging.Debug(logging.CatModule, "processing directory: %s", path)
|
|
||||||
videos := s.findVideoFiles(path)
|
|
||||||
videoPaths = append(videoPaths, videos...)
|
|
||||||
} else if s.isVideoFile(path) {
|
|
||||||
videoPaths = append(videoPaths, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(videoPaths) == 0 {
|
|
||||||
logging.Debug(logging.CatUI, "no valid video files in dropped items")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If multiple videos, add all to queue
|
|
||||||
if len(videoPaths) > 1 {
|
|
||||||
logging.Debug(logging.CatUI, "multiple videos dropped in convert module; adding all to queue")
|
|
||||||
go s.batchAddToQueue(videoPaths)
|
|
||||||
} else {
|
|
||||||
// Single video: load it
|
|
||||||
logging.Debug(logging.CatUI, "single video dropped in convert module; loading: %s", videoPaths[0])
|
|
||||||
go s.loadVideo(videoPaths[0])
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If in compare module, handle up to 2 video files
|
|
||||||
if s.active == "compare" {
|
|
||||||
// Collect all video files from the dropped items
|
|
||||||
var videoPaths []string
|
|
||||||
for _, uri := range items {
|
|
||||||
if uri.Scheme() != "file" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
path := uri.Path()
|
|
||||||
logging.Debug(logging.CatModule, "drop received path=%s", path)
|
|
||||||
|
|
||||||
// Only accept video files (not directories)
|
|
||||||
if s.isVideoFile(path) {
|
|
||||||
videoPaths = append(videoPaths, path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(videoPaths) == 0 {
|
|
||||||
logging.Debug(logging.CatUI, "no valid video files in dropped items")
|
|
||||||
dialog.ShowInformation("Compare Videos", "No video files found in dropped items.", s.window)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show message if more than 2 videos dropped
|
|
||||||
if len(videoPaths) > 2 {
|
|
||||||
dialog.ShowInformation("Compare Videos",
|
|
||||||
fmt.Sprintf("You dropped %d videos. Only the first two will be loaded for comparison.", len(videoPaths)),
|
|
||||||
s.window)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load videos sequentially to avoid race conditions
|
|
||||||
go func() {
|
|
||||||
if len(videoPaths) == 1 {
|
|
||||||
// Single video dropped - fill first empty slot
|
|
||||||
src, err := probeVideo(videoPaths[0])
|
|
||||||
if err != nil {
|
|
||||||
logging.Debug(logging.CatModule, "failed to load video: %v", err)
|
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
||||||
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), s.window)
|
|
||||||
}, false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
||||||
// Fill first empty slot
|
|
||||||
if s.compareFile1 == nil {
|
|
||||||
s.compareFile1 = src
|
|
||||||
logging.Debug(logging.CatModule, "loaded video into slot 1")
|
|
||||||
} else if s.compareFile2 == nil {
|
|
||||||
s.compareFile2 = src
|
|
||||||
logging.Debug(logging.CatModule, "loaded video into slot 2")
|
|
||||||
} else {
|
|
||||||
// Both slots full - ask which to replace
|
|
||||||
dialog.ShowInformation("Both Slots Full",
|
|
||||||
"Both comparison slots are full. Use the Clear button to empty a slot first.",
|
|
||||||
s.window)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.showCompareView()
|
|
||||||
}, false)
|
|
||||||
} else {
|
|
||||||
// Multiple videos dropped - load into both slots
|
|
||||||
src1, err := probeVideo(videoPaths[0])
|
|
||||||
if err != nil {
|
|
||||||
logging.Debug(logging.CatModule, "failed to load first video: %v", err)
|
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
||||||
dialog.ShowError(fmt.Errorf("failed to load video 1: %w", err), s.window)
|
|
||||||
}, false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var src2 *videoSource
|
|
||||||
if len(videoPaths) >= 2 {
|
|
||||||
src2, err = probeVideo(videoPaths[1])
|
|
||||||
if err != nil {
|
|
||||||
logging.Debug(logging.CatModule, "failed to load second video: %v", err)
|
|
||||||
// Continue with just first video
|
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
||||||
dialog.ShowError(fmt.Errorf("failed to load video 2: %w", err), s.window)
|
|
||||||
}, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update both slots and refresh view once
|
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
||||||
s.compareFile1 = src1
|
|
||||||
s.compareFile2 = src2
|
|
||||||
s.showCompareView()
|
|
||||||
logging.Debug(logging.CatModule, "loaded %d video(s) into both slots", len(videoPaths))
|
|
||||||
}, false)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other modules don't handle file drops yet
|
|
||||||
logging.Debug(logging.CatUI, "drop ignored; module %s cannot handle files", s.active)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// detectModuleTileAtPosition calculates which module tile is at the given position
|
// detectModuleTileAtPosition calculates which module tile is at the given position
|
||||||
|
|
@ -3897,7 +3930,7 @@ func (s *appState) loadVideo(path string) {
|
||||||
|
|
||||||
logging.Debug(logging.CatModule, "video loaded %+v", src)
|
logging.Debug(logging.CatModule, "video loaded %+v", src)
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
s.showConvertView(src)
|
s.showPlayerView()
|
||||||
}, false)
|
}, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3915,7 +3948,7 @@ func (s *appState) clearVideo() {
|
||||||
s.convert.AspectHandling = "Auto"
|
s.convert.AspectHandling = "Auto"
|
||||||
s.convert.OutputAspect = "Source"
|
s.convert.OutputAspect = "Source"
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
s.showConvertView(nil)
|
s.showPlayerView()
|
||||||
}, false)
|
}, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4056,7 +4089,7 @@ func (s *appState) switchToVideo(index int) {
|
||||||
s.playerPaused = true
|
s.playerPaused = true
|
||||||
|
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
s.showConvertView(src)
|
s.showPlayerView()
|
||||||
}, false)
|
}, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4685,10 +4718,10 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
||||||
isHardwareFailure := strings.Contains(stderrOutput, "No capable devices found") ||
|
isHardwareFailure := strings.Contains(stderrOutput, "No capable devices found") ||
|
||||||
strings.Contains(stderrOutput, "Cannot load") ||
|
strings.Contains(stderrOutput, "Cannot load") ||
|
||||||
strings.Contains(stderrOutput, "not available") &&
|
strings.Contains(stderrOutput, "not available") &&
|
||||||
(strings.Contains(stderrOutput, "nvenc") ||
|
(strings.Contains(stderrOutput, "nvenc") ||
|
||||||
strings.Contains(stderrOutput, "qsv") ||
|
strings.Contains(stderrOutput, "qsv") ||
|
||||||
strings.Contains(stderrOutput, "vaapi") ||
|
strings.Contains(stderrOutput, "vaapi") ||
|
||||||
strings.Contains(stderrOutput, "videotoolbox"))
|
strings.Contains(stderrOutput, "videotoolbox"))
|
||||||
|
|
||||||
if isHardwareFailure && s.convert.HardwareAccel != "none" && s.convert.HardwareAccel != "" {
|
if isHardwareFailure && s.convert.HardwareAccel != "none" && s.convert.HardwareAccel != "" {
|
||||||
errorMsg = fmt.Errorf("Hardware encoding (%s) failed - no compatible hardware found.\n\nPlease disable hardware acceleration in the conversion settings and try again with software encoding.\n\nFFmpeg output:\n%s", s.convert.HardwareAccel, stderrOutput)
|
errorMsg = fmt.Errorf("Hardware encoding (%s) failed - no compatible hardware found.\n\nPlease disable hardware acceleration in the conversion settings and try again with software encoding.\n\nFFmpeg output:\n%s", s.convert.HardwareAccel, stderrOutput)
|
||||||
|
|
@ -5076,8 +5109,8 @@ type videoSource struct {
|
||||||
Duration float64
|
Duration float64
|
||||||
VideoCodec string
|
VideoCodec string
|
||||||
AudioCodec string
|
AudioCodec string
|
||||||
Bitrate int // Video bitrate in bits per second
|
Bitrate int // Video bitrate in bits per second
|
||||||
AudioBitrate int // Audio bitrate in bits per second
|
AudioBitrate int // Audio bitrate in bits per second
|
||||||
FrameRate float64
|
FrameRate float64
|
||||||
PixelFormat string
|
PixelFormat string
|
||||||
AudioRate int
|
AudioRate int
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user