Author menu sections and menu options
This commit is contained in:
parent
514f1a0475
commit
68738cf1a5
256
author_module.go
256
author_module.go
|
|
@ -8,6 +8,7 @@ import (
|
|||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image/color"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
|
|
@ -46,6 +47,9 @@ type authorConfig struct {
|
|||
MenuLogoPosition string `json:"menuLogoPosition"`
|
||||
MenuLogoScale float64 `json:"menuLogoScale"`
|
||||
MenuLogoMargin int `json:"menuLogoMargin"`
|
||||
MenuStructure string `json:"menuStructure"`
|
||||
MenuExtrasEnabled bool `json:"menuExtrasEnabled"`
|
||||
MenuChapterThumbSrc string `json:"menuChapterThumbSrc"`
|
||||
TreatAsChapters bool `json:"treatAsChapters"`
|
||||
SceneThreshold float64 `json:"sceneThreshold"`
|
||||
}
|
||||
|
|
@ -66,6 +70,9 @@ func defaultAuthorConfig() authorConfig {
|
|||
MenuLogoPosition: "Top Right",
|
||||
MenuLogoScale: 1.0,
|
||||
MenuLogoMargin: 24,
|
||||
MenuStructure: "Feature + Chapters",
|
||||
MenuExtrasEnabled: false,
|
||||
MenuChapterThumbSrc: "Auto",
|
||||
TreatAsChapters: false,
|
||||
SceneThreshold: 0.3,
|
||||
}
|
||||
|
|
@ -108,6 +115,12 @@ func loadPersistedAuthorConfig() (authorConfig, error) {
|
|||
if cfg.MenuLogoMargin == 0 {
|
||||
cfg.MenuLogoMargin = 24
|
||||
}
|
||||
if cfg.MenuStructure == "" {
|
||||
cfg.MenuStructure = "Feature + Chapters"
|
||||
}
|
||||
if cfg.MenuChapterThumbSrc == "" {
|
||||
cfg.MenuChapterThumbSrc = "Auto"
|
||||
}
|
||||
if cfg.SceneThreshold <= 0 {
|
||||
cfg.SceneThreshold = 0.3
|
||||
}
|
||||
|
|
@ -141,6 +154,9 @@ func (s *appState) applyAuthorConfig(cfg authorConfig) {
|
|||
s.authorMenuLogoPosition = cfg.MenuLogoPosition
|
||||
s.authorMenuLogoScale = cfg.MenuLogoScale
|
||||
s.authorMenuLogoMargin = cfg.MenuLogoMargin
|
||||
s.authorMenuStructure = cfg.MenuStructure
|
||||
s.authorMenuExtrasEnabled = cfg.MenuExtrasEnabled
|
||||
s.authorMenuChapterThumbSrc = cfg.MenuChapterThumbSrc
|
||||
s.authorTreatAsChapters = cfg.TreatAsChapters
|
||||
s.authorSceneThreshold = cfg.SceneThreshold
|
||||
}
|
||||
|
|
@ -161,6 +177,9 @@ func (s *appState) persistAuthorConfig() {
|
|||
MenuLogoPosition: s.authorMenuLogoPosition,
|
||||
MenuLogoScale: s.authorMenuLogoScale,
|
||||
MenuLogoMargin: s.authorMenuLogoMargin,
|
||||
MenuStructure: s.authorMenuStructure,
|
||||
MenuExtrasEnabled: s.authorMenuExtrasEnabled,
|
||||
MenuChapterThumbSrc: s.authorMenuChapterThumbSrc,
|
||||
TreatAsChapters: s.authorTreatAsChapters,
|
||||
SceneThreshold: s.authorSceneThreshold,
|
||||
}
|
||||
|
|
@ -205,6 +224,12 @@ func buildAuthorView(state *appState) fyne.CanvasObject {
|
|||
if state.authorMenuLogoMargin == 0 {
|
||||
state.authorMenuLogoMargin = 24
|
||||
}
|
||||
if state.authorMenuStructure == "" {
|
||||
state.authorMenuStructure = "Feature + Chapters"
|
||||
}
|
||||
if state.authorMenuChapterThumbSrc == "" {
|
||||
state.authorMenuChapterThumbSrc = "Auto"
|
||||
}
|
||||
|
||||
authorColor := moduleColor("author")
|
||||
|
||||
|
|
@ -849,6 +874,9 @@ func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
|
|||
MenuLogoPosition: state.authorMenuLogoPosition,
|
||||
MenuLogoScale: state.authorMenuLogoScale,
|
||||
MenuLogoMargin: state.authorMenuLogoMargin,
|
||||
MenuStructure: state.authorMenuStructure,
|
||||
MenuExtrasEnabled: state.authorMenuExtrasEnabled,
|
||||
MenuChapterThumbSrc: state.authorMenuChapterThumbSrc,
|
||||
TreatAsChapters: state.authorTreatAsChapters,
|
||||
SceneThreshold: state.authorSceneThreshold,
|
||||
}
|
||||
|
|
@ -893,12 +921,41 @@ func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
|
|||
}
|
||||
|
||||
func buildAuthorMenuTab(state *appState) fyne.CanvasObject {
|
||||
navyBlue := utils.MustHex("#191F35")
|
||||
boxAccent := gridColor
|
||||
var updateMenuControls func(bool)
|
||||
|
||||
buildMenuBox := func(title string, content fyne.CanvasObject) fyne.CanvasObject {
|
||||
bg := canvas.NewRectangle(navyBlue)
|
||||
bg.CornerRadius = 10
|
||||
bg.StrokeColor = boxAccent
|
||||
bg.StrokeWidth = 1
|
||||
body := container.NewVBox(
|
||||
widget.NewLabelWithStyle(title, fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
widget.NewSeparator(),
|
||||
content,
|
||||
)
|
||||
return container.NewMax(bg, container.NewPadded(body))
|
||||
}
|
||||
|
||||
sectionGap := func() fyne.CanvasObject {
|
||||
gap := canvas.NewRectangle(color.Transparent)
|
||||
gap.SetMinSize(fyne.NewSize(0, 10))
|
||||
return gap
|
||||
}
|
||||
|
||||
createMenuCheck := widget.NewCheck("Enable DVD Menus", func(checked bool) {
|
||||
state.authorCreateMenu = checked
|
||||
state.updateAuthorSummary()
|
||||
state.persistAuthorConfig()
|
||||
if updateMenuControls != nil {
|
||||
updateMenuControls(checked)
|
||||
}
|
||||
})
|
||||
createMenuCheck.SetChecked(state.authorCreateMenu)
|
||||
menuDisabledNote := widget.NewLabel("DVD menus will not be generated unless enabled.")
|
||||
menuDisabledNote.TextStyle = fyne.TextStyle{Italic: true}
|
||||
menuDisabledNote.Wrapping = fyne.TextWrapWord
|
||||
|
||||
menuThemeSelect := widget.NewSelect([]string{"VideoTools"}, func(value string) {
|
||||
state.authorMenuTheme = value
|
||||
|
|
@ -910,15 +967,36 @@ func buildAuthorMenuTab(state *appState) fyne.CanvasObject {
|
|||
}
|
||||
menuThemeSelect.SetSelected(state.authorMenuTheme)
|
||||
|
||||
menuTemplateSelect := widget.NewSelect([]string{"Simple", "Dark", "Poster"}, func(value string) {
|
||||
state.authorMenuTemplate = value
|
||||
templateOptions := []string{
|
||||
"Simple (Title + Buttons)",
|
||||
"Poster (Grid Thumbnails)",
|
||||
"Classic (Title + Buttons)",
|
||||
}
|
||||
templateValueByLabel := map[string]string{
|
||||
"Simple (Title + Buttons)": "Simple",
|
||||
"Poster (Grid Thumbnails)": "Poster",
|
||||
"Classic (Title + Buttons)": "Dark",
|
||||
}
|
||||
templateLabelByValue := map[string]string{
|
||||
"Simple": "Simple (Title + Buttons)",
|
||||
"Poster": "Poster (Grid Thumbnails)",
|
||||
"Dark": "Classic (Title + Buttons)",
|
||||
}
|
||||
|
||||
menuTemplateSelect := widget.NewSelect(templateOptions, func(value string) {
|
||||
state.authorMenuTemplate = templateValueByLabel[value]
|
||||
state.updateAuthorSummary()
|
||||
state.persistAuthorConfig()
|
||||
})
|
||||
if state.authorMenuTemplate == "" {
|
||||
state.authorMenuTemplate = "Simple"
|
||||
}
|
||||
menuTemplateSelect.SetSelected(state.authorMenuTemplate)
|
||||
templateLabel := templateLabelByValue[state.authorMenuTemplate]
|
||||
if templateLabel == "" {
|
||||
templateLabel = templateOptions[0]
|
||||
state.authorMenuTemplate = templateValueByLabel[templateLabel]
|
||||
}
|
||||
menuTemplateSelect.SetSelected(templateLabel)
|
||||
|
||||
bgImageLabel := widget.NewLabel(state.authorMenuBackgroundImage)
|
||||
bgImageLabel.Wrapping = fyne.TextWrapWord
|
||||
|
|
@ -939,19 +1017,15 @@ func buildAuthorMenuTab(state *appState) fyne.CanvasObject {
|
|||
bgImageLabel.Hidden = state.authorMenuTemplate != "Poster"
|
||||
|
||||
menuTemplateSelect.OnChanged = func(value string) {
|
||||
state.authorMenuTemplate = value
|
||||
showPoster := value == "Poster"
|
||||
state.authorMenuTemplate = templateValueByLabel[value]
|
||||
showPoster := state.authorMenuTemplate == "Poster"
|
||||
bgImageButton.Hidden = !showPoster
|
||||
bgImageLabel.Hidden = !showPoster
|
||||
state.updateAuthorSummary()
|
||||
state.persistAuthorConfig()
|
||||
}
|
||||
|
||||
logoEnableCheck := widget.NewCheck("Embed Logo", func(checked bool) {
|
||||
state.authorMenuLogoEnabled = checked
|
||||
state.updateAuthorSummary()
|
||||
state.persistAuthorConfig()
|
||||
})
|
||||
logoEnableCheck := widget.NewCheck("Embed Logo", nil)
|
||||
logoEnableCheck.SetChecked(state.authorMenuLogoEnabled)
|
||||
|
||||
logoLabel := widget.NewLabel(state.authorMenuLogoPath)
|
||||
|
|
@ -985,18 +1059,38 @@ func buildAuthorMenuTab(state *appState) fyne.CanvasObject {
|
|||
}
|
||||
logoPositionSelect.SetSelected(state.authorMenuLogoPosition)
|
||||
|
||||
scaleOptions := []string{"50%", "75%", "100%", "125%", "150%", "200%"}
|
||||
scaleValueByLabel := map[string]float64{
|
||||
"50%": 0.5,
|
||||
"75%": 0.75,
|
||||
"100%": 1.0,
|
||||
"125%": 1.25,
|
||||
"150%": 1.5,
|
||||
"200%": 2.0,
|
||||
}
|
||||
scaleLabelByValue := map[float64]string{
|
||||
0.5: "50%",
|
||||
0.75: "75%",
|
||||
1.0: "100%",
|
||||
1.25: "125%",
|
||||
1.5: "150%",
|
||||
2.0: "200%",
|
||||
}
|
||||
if state.authorMenuLogoScale == 0 {
|
||||
state.authorMenuLogoScale = 1.0
|
||||
}
|
||||
scaleLabel := widget.NewLabel(fmt.Sprintf("Logo Scale: %.0f%%", state.authorMenuLogoScale*100))
|
||||
scaleSlider := widget.NewSlider(0.2, 2.0)
|
||||
scaleSlider.Step = 0.05
|
||||
scaleSlider.Value = state.authorMenuLogoScale
|
||||
scaleSlider.OnChanged = func(v float64) {
|
||||
state.authorMenuLogoScale = v
|
||||
scaleLabel.SetText(fmt.Sprintf("Logo Scale: %.0f%%", v*100))
|
||||
state.persistAuthorConfig()
|
||||
logoScaleSelect := widget.NewSelect(scaleOptions, func(value string) {
|
||||
if scale, ok := scaleValueByLabel[value]; ok {
|
||||
state.authorMenuLogoScale = scale
|
||||
state.persistAuthorConfig()
|
||||
}
|
||||
})
|
||||
scaleLabel := scaleLabelByValue[state.authorMenuLogoScale]
|
||||
if scaleLabel == "" {
|
||||
scaleLabel = "100%"
|
||||
state.authorMenuLogoScale = 1.0
|
||||
}
|
||||
logoScaleSelect.SetSelected(scaleLabel)
|
||||
|
||||
if state.authorMenuLogoMargin == 0 {
|
||||
state.authorMenuLogoMargin = 24
|
||||
|
|
@ -1011,34 +1105,141 @@ func buildAuthorMenuTab(state *appState) fyne.CanvasObject {
|
|||
state.persistAuthorConfig()
|
||||
}
|
||||
|
||||
info := widget.NewLabel("DVD menus use the VideoTools theme and IBM Plex Mono. Menu settings apply only to disc authoring.")
|
||||
safeAreaNote := widget.NewLabel("Logos are constrained to DVD safe areas.")
|
||||
safeAreaNote.TextStyle = fyne.TextStyle{Italic: true}
|
||||
safeAreaNote.Wrapping = fyne.TextWrapWord
|
||||
|
||||
menuStructureSelect := widget.NewSelect([]string{
|
||||
"Feature Only",
|
||||
"Feature + Chapters",
|
||||
"Feature + Extras",
|
||||
"Feature + Chapters + Extras",
|
||||
}, func(value string) {
|
||||
state.authorMenuStructure = value
|
||||
state.persistAuthorConfig()
|
||||
})
|
||||
if state.authorMenuStructure == "" {
|
||||
state.authorMenuStructure = "Feature + Chapters"
|
||||
}
|
||||
menuStructureSelect.SetSelected(state.authorMenuStructure)
|
||||
|
||||
extrasMenuCheck := widget.NewCheck("Enable Extras Menu", func(checked bool) {
|
||||
state.authorMenuExtrasEnabled = checked
|
||||
state.persistAuthorConfig()
|
||||
})
|
||||
extrasMenuCheck.SetChecked(state.authorMenuExtrasEnabled)
|
||||
extrasNote := widget.NewLabel("Videos marked as Extras appear in a separate menu.")
|
||||
extrasNote.Wrapping = fyne.TextWrapWord
|
||||
|
||||
thumbSourceSelect := widget.NewSelect([]string{
|
||||
"Auto",
|
||||
"First Frame",
|
||||
"Midpoint",
|
||||
"Custom (Advanced)",
|
||||
}, func(value string) {
|
||||
state.authorMenuChapterThumbSrc = value
|
||||
state.persistAuthorConfig()
|
||||
})
|
||||
if state.authorMenuChapterThumbSrc == "" {
|
||||
state.authorMenuChapterThumbSrc = "Auto"
|
||||
}
|
||||
thumbSourceSelect.SetSelected(state.authorMenuChapterThumbSrc)
|
||||
|
||||
info := widget.NewLabel("DVD menus are generated using the VideoTools theme and IBM Plex Mono. Menu settings apply only to disc authoring.")
|
||||
info.Wrapping = fyne.TextWrapWord
|
||||
|
||||
controls := container.NewVBox(
|
||||
widget.NewLabel("DVD Menu Settings:"),
|
||||
widget.NewSeparator(),
|
||||
previewBox := buildMenuBox("Preview", widget.NewLabel("Menu preview is generated during authoring."))
|
||||
|
||||
menuCore := buildMenuBox("Menu Core", container.NewVBox(
|
||||
createMenuCheck,
|
||||
menuDisabledNote,
|
||||
widget.NewLabel("Theme:"),
|
||||
menuThemeSelect,
|
||||
widget.NewLabel("Template:"),
|
||||
menuTemplateSelect,
|
||||
bgImageLabel,
|
||||
bgImageButton,
|
||||
widget.NewSeparator(),
|
||||
widget.NewLabel("Menu Structure:"),
|
||||
menuStructureSelect,
|
||||
))
|
||||
|
||||
branding := buildMenuBox("Branding", container.NewVBox(
|
||||
logoEnableCheck,
|
||||
widget.NewLabel("Logo Path:"),
|
||||
logoLabel,
|
||||
logoPickButton,
|
||||
widget.NewLabel("Logo Position:"),
|
||||
logoPositionSelect,
|
||||
scaleLabel,
|
||||
scaleSlider,
|
||||
widget.NewLabel("Logo Scale:"),
|
||||
logoScaleSelect,
|
||||
marginLabel,
|
||||
marginSlider,
|
||||
widget.NewSeparator(),
|
||||
safeAreaNote,
|
||||
))
|
||||
|
||||
navigation := buildMenuBox("Navigation", container.NewVBox(
|
||||
extrasMenuCheck,
|
||||
extrasNote,
|
||||
widget.NewLabel("Chapter Thumbnail Source:"),
|
||||
thumbSourceSelect,
|
||||
))
|
||||
|
||||
controls := container.NewVBox(
|
||||
menuCore,
|
||||
sectionGap(),
|
||||
branding,
|
||||
sectionGap(),
|
||||
navigation,
|
||||
sectionGap(),
|
||||
previewBox,
|
||||
sectionGap(),
|
||||
info,
|
||||
)
|
||||
|
||||
updateMenuControls = func(enabled bool) {
|
||||
menuDisabledNote.Hidden = enabled
|
||||
setEnabled := func(on bool, items ...fyne.Disableable) {
|
||||
for _, item := range items {
|
||||
if on {
|
||||
item.Enable()
|
||||
} else {
|
||||
item.Disable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setEnabled(enabled,
|
||||
menuThemeSelect,
|
||||
menuTemplateSelect,
|
||||
bgImageButton,
|
||||
menuStructureSelect,
|
||||
logoEnableCheck,
|
||||
logoPickButton,
|
||||
logoPositionSelect,
|
||||
logoScaleSelect,
|
||||
marginSlider,
|
||||
extrasMenuCheck,
|
||||
thumbSourceSelect,
|
||||
)
|
||||
|
||||
logoControlsEnabled := enabled && state.authorMenuLogoEnabled
|
||||
setEnabled(logoControlsEnabled,
|
||||
logoPickButton,
|
||||
logoPositionSelect,
|
||||
logoScaleSelect,
|
||||
marginSlider,
|
||||
)
|
||||
}
|
||||
|
||||
logoEnableCheck.OnChanged = func(checked bool) {
|
||||
state.authorMenuLogoEnabled = checked
|
||||
updateMenuControls(state.authorCreateMenu)
|
||||
state.updateAuthorSummary()
|
||||
state.persistAuthorConfig()
|
||||
}
|
||||
|
||||
updateMenuControls(state.authorCreateMenu)
|
||||
|
||||
return container.NewPadded(controls)
|
||||
}
|
||||
|
||||
|
|
@ -1962,6 +2163,9 @@ func (s *appState) addAuthorToQueue(paths []string, region, aspect, title, outpu
|
|||
"menuLogoPosition": s.authorMenuLogoPosition,
|
||||
"menuLogoScale": s.authorMenuLogoScale,
|
||||
"menuLogoMargin": s.authorMenuLogoMargin,
|
||||
"menuStructure": s.authorMenuStructure,
|
||||
"menuExtrasEnabled": s.authorMenuExtrasEnabled,
|
||||
"menuChapterThumbSrc": s.authorMenuChapterThumbSrc,
|
||||
}
|
||||
|
||||
titleLabel := title
|
||||
|
|
|
|||
|
|
@ -156,12 +156,14 @@ You filmed stuff on your phone and want to give it to relatives who don't use co
|
|||
- Output Type: DVD
|
||||
- Region: NTSC
|
||||
- Aspect Ratio: 16:9
|
||||
5. **Generate Tab**:
|
||||
5. **Menu Tab** (optional):
|
||||
- Enable DVD Menus if you want a playable menu
|
||||
6. **Generate Tab**:
|
||||
- Title: "Birthday 2024"
|
||||
- Pick where to save it
|
||||
- Click Generate
|
||||
6. When done, burn the .iso file to a DVD-R
|
||||
7. Hand it to grandma - it'll just work in her DVD player
|
||||
7. When done, burn the .iso file to a DVD-R
|
||||
8. Hand it to grandma - it'll just work in her DVD player
|
||||
|
||||
### Scenario 2: Multiple Episodes on One Disc
|
||||
|
||||
|
|
@ -172,9 +174,11 @@ You downloaded 3 episodes of a show and want them on one disc with a menu.
|
|||
3. **Settings Tab**:
|
||||
- Output Type: DVD
|
||||
- Region: NTSC
|
||||
- Create Menu: YES (important!)
|
||||
4. **Generate Tab** → Generate the disc
|
||||
5. The DVD will have a menu where you can pick which episode to watch
|
||||
4. **Menu Tab**:
|
||||
- Enable DVD Menus
|
||||
- Menu Structure: Feature + Extras
|
||||
5. **Generate Tab** → Generate the disc
|
||||
6. The DVD will have a menu where you can pick which episode to watch
|
||||
|
||||
### Scenario 3: Concert Video with Song Chapters
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/ebitengine/oto/v3"
|
||||
|
||||
"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
|
||||
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
|
||||
)
|
||||
|
|
@ -35,6 +37,10 @@ type UnifiedPlayer struct {
|
|||
audioPipeReader *io.PipeReader
|
||||
audioPipeWriter *io.PipeWriter
|
||||
|
||||
// Audio output
|
||||
audioContext *oto.Context
|
||||
audioPlayer *oto.Player
|
||||
|
||||
// State tracking
|
||||
currentPath string
|
||||
currentTime time.Duration
|
||||
|
|
@ -137,8 +143,40 @@ func (p *UnifiedPlayer) Load(path string, offset time.Duration) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Initialize audio context for playback
|
||||
sampleRate := 48000
|
||||
channels := 2
|
||||
bytesPerSample := 2 // 16-bit = 2 bytes
|
||||
|
||||
ctx, ready, err := oto.NewContext(&oto.NewContextOptions{
|
||||
SampleRate: sampleRate,
|
||||
ChannelCount: channels,
|
||||
Format: oto.FormatSignedInt16LE,
|
||||
BufferSize: 4096, // 85ms chunks for smooth playback
|
||||
})
|
||||
if err != nil {
|
||||
logging.Error(logging.CatPlayer, "Failed to create audio context: %v", err)
|
||||
return err
|
||||
}
|
||||
if ready != nil {
|
||||
<-ready
|
||||
}
|
||||
|
||||
p.audioContext = ctx
|
||||
|
||||
// Initialize audio buffer
|
||||
p.audioBuffer = make([]byte, 0, 0) // Will grow as needed
|
||||
|
||||
// Start FFmpeg process for unified A/V output
|
||||
return p.startVideoProcess()
|
||||
err = p.startVideoProcess()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start audio stream processing
|
||||
go p.readAudioStream()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SeekToTime seeks to a specific time without restarting processes
|
||||
|
|
@ -396,6 +434,15 @@ func (p *UnifiedPlayer) Close() {
|
|||
|
||||
p.frameBuffer = nil
|
||||
p.audioBuffer = nil
|
||||
|
||||
// Close audio context and player
|
||||
if p.audioContext != nil {
|
||||
p.audioContext = nil
|
||||
}
|
||||
if p.audioPlayer != nil {
|
||||
p.audioPlayer.Close()
|
||||
p.audioPlayer = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Stop halts playback and tears down the FFmpeg process.
|
||||
|
|
@ -585,14 +632,25 @@ func (p *UnifiedPlayer) readAudioStream() {
|
|||
continue
|
||||
}
|
||||
|
||||
// Apply volume if not muted
|
||||
if !p.muted && p.volume > 0 {
|
||||
p.applyVolumeToBuffer(buffer[:n])
|
||||
// Initialize audio player if needed
|
||||
if p.audioPlayer == nil && p.audioContext != nil {
|
||||
player, err := p.audioContext.NewPlayer(p.audioPipeReader)
|
||||
if err != nil {
|
||||
logging.Error(logging.CatPlayer, "Failed to create audio player: %v", err)
|
||||
return
|
||||
}
|
||||
p.audioPlayer = player
|
||||
}
|
||||
|
||||
// Send to audio output (this would connect to audio system)
|
||||
// For now, we'll store in buffer for playback sync monitoring
|
||||
p.audioBuffer = append(p.audioBuffer, buffer[:n]...)
|
||||
// Write audio data to player buffer
|
||||
if p.audioPlayer != nil {
|
||||
p.audioPlayer.Write(buffer[:n])
|
||||
}
|
||||
|
||||
// Buffer for sync monitoring (keep small to avoid memory issues)
|
||||
if len(p.audioBuffer) > 32768 { // Max 1 second at 48kHz
|
||||
p.audioBuffer = p.audioBuffer[len(p.audioBuffer)-16384:] // Keep half
|
||||
}
|
||||
|
||||
// Simple audio sync timing
|
||||
p.updateAVSync()
|
||||
|
|
@ -611,7 +669,12 @@ func (p *UnifiedPlayer) readVideoFrame() (*image.RGBA, error) {
|
|||
frameSize := p.windowW * p.windowH * 3 // RGB24 = 3 bytes per pixel
|
||||
frameData := make([]byte, frameSize)
|
||||
|
||||
// Read full frame - io.ReadFull ensures we get the complete frame
|
||||
// Check for paused state before reading
|
||||
if p.paused {
|
||||
return nil, fmt.Errorf("player is paused")
|
||||
}
|
||||
|
||||
// Read full frame - io.ReadFull ensures we get complete frame
|
||||
n, err := io.ReadFull(p.videoPipeReader, frameData)
|
||||
if err != nil {
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
|
|
|
|||
3
main.go
3
main.go
|
|
@ -1157,6 +1157,9 @@ type appState struct {
|
|||
authorMenuLogoPosition string // "Top Left", "Top Right", "Bottom Left", "Bottom Right", "Center"
|
||||
authorMenuLogoScale float64
|
||||
authorMenuLogoMargin int
|
||||
authorMenuStructure string // Feature only, Chapters, Extras
|
||||
authorMenuExtrasEnabled bool // Show extras menu
|
||||
authorMenuChapterThumbSrc string // Auto, First Frame, Midpoint, Custom
|
||||
authorTitle string // DVD title
|
||||
authorSubtitles []string // Subtitle file paths
|
||||
authorAudioTracks []string // Additional audio tracks
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user