Compare commits
No commits in common. "ee67bffbd9c3e5df2733ec679da09df0f4443248" and "1da9317d734d955a00567464379d7de4680e8612" have entirely different histories.
ee67bffbd9
...
1da9317d73
|
|
@ -1,354 +0,0 @@
|
|||
# Player Module Performance Issues & Fixes
|
||||
|
||||
## Current Problems Causing Stuttering
|
||||
|
||||
### 1. **Separate Video & Audio Processes (No Sync)**
|
||||
**Location:** `main.go:9144` (runVideo) and `main.go:9233` (runAudio)
|
||||
|
||||
**Problem:**
|
||||
- Video and audio run in completely separate FFmpeg processes
|
||||
- No synchronization mechanism between them
|
||||
- They will inevitably drift apart, causing A/V desync and stuttering
|
||||
|
||||
**Current Implementation:**
|
||||
```go
|
||||
func (p *playSession) startLocked(offset float64) {
|
||||
p.runVideo(offset) // Separate process
|
||||
p.runAudio(offset) // Separate process
|
||||
}
|
||||
```
|
||||
|
||||
**Why It Stutters:**
|
||||
- If video frame processing takes too long → audio continues → desync
|
||||
- If audio buffer underruns → video continues → desync
|
||||
- No feedback loop to keep them in sync
|
||||
|
||||
---
|
||||
|
||||
### 2. **Audio Buffer Too Small**
|
||||
**Location:** `main.go:8960` (audio context) and `main.go:9274` (chunk size)
|
||||
|
||||
**Problem:**
|
||||
```go
|
||||
// Audio context with tiny buffer (42ms at 48kHz)
|
||||
audioCtxGlobal.ctx, audioCtxGlobal.err = oto.NewContext(sampleRate, channels, bytesPerSample, 2048)
|
||||
|
||||
// Tiny read chunks (21ms of audio)
|
||||
chunk := make([]byte, 4096)
|
||||
```
|
||||
|
||||
**Why It Stutters:**
|
||||
- 21ms chunks mean we need to read 47 times per second
|
||||
- Any delay > 21ms causes audio dropout/stuttering
|
||||
- 2048 sample buffer gives only 42ms protection against underruns
|
||||
- Modern systems need 100-200ms buffers for smooth playback
|
||||
|
||||
---
|
||||
|
||||
### 3. **Volume Processing in Hot Path**
|
||||
**Location:** `main.go:9294-9318`
|
||||
|
||||
**Problem:**
|
||||
```go
|
||||
// Processes volume on EVERY audio chunk read
|
||||
for i := 0; i+1 < n; i += 2 {
|
||||
sample := int16(binary.LittleEndian.Uint16(tmp[i:]))
|
||||
amp := int(float64(sample) * gain)
|
||||
// ... clamping ...
|
||||
binary.LittleEndian.PutUint16(tmp[i:], uint16(int16(amp)))
|
||||
}
|
||||
```
|
||||
|
||||
**Why It Stutters:**
|
||||
- CPU-intensive per-sample processing
|
||||
- Happens 47 times/second with tiny chunks
|
||||
- Blocks the audio read loop
|
||||
- Should use FFmpeg's volume filter or hardware mixing
|
||||
|
||||
---
|
||||
|
||||
### 4. **Video Frame Pacing Issues**
|
||||
**Location:** `main.go:9200-9203`
|
||||
|
||||
**Problem:**
|
||||
```go
|
||||
if delay := time.Until(nextFrameAt); delay > 0 {
|
||||
time.Sleep(delay)
|
||||
}
|
||||
nextFrameAt = nextFrameAt.Add(frameDur)
|
||||
```
|
||||
|
||||
**Why It Stutters:**
|
||||
- `time.Sleep()` is not precise (can wake up late)
|
||||
- Cumulative drift: if one frame is late, all future frames shift
|
||||
- No correction mechanism if we fall behind
|
||||
- UI thread delays from `DoFromGoroutine` can cause frame drops
|
||||
|
||||
---
|
||||
|
||||
### 5. **UI Thread Blocking**
|
||||
**Location:** `main.go:9207-9215`
|
||||
|
||||
**Problem:**
|
||||
```go
|
||||
// Every frame waits for UI thread to be available
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
p.img.Image = frame
|
||||
p.img.Refresh()
|
||||
}, false)
|
||||
```
|
||||
|
||||
**Why It Stutters:**
|
||||
- If UI thread is busy, frame updates queue up
|
||||
- Can cause video to appear choppy even if FFmpeg is delivering smoothly
|
||||
- No frame dropping mechanism if UI can't keep up
|
||||
|
||||
---
|
||||
|
||||
### 6. **Frame Allocation on Every Frame**
|
||||
**Location:** `main.go:9205-9206`
|
||||
|
||||
**Problem:**
|
||||
```go
|
||||
// Allocates new frame buffer 24-60 times per second
|
||||
frame := image.NewRGBA(image.Rect(0, 0, p.targetW, p.targetH))
|
||||
utils.CopyRGBToRGBA(frame.Pix, buf)
|
||||
```
|
||||
|
||||
**Why It Stutters:**
|
||||
- Memory allocation on every frame causes GC pressure
|
||||
- Extra copy operation adds latency
|
||||
- Could reuse buffers or use ring buffer
|
||||
|
||||
---
|
||||
|
||||
## Recommended Fixes (Priority Order)
|
||||
|
||||
### Priority 1: Increase Audio Buffers (Quick Fix)
|
||||
|
||||
**Change `main.go:8960`:**
|
||||
```go
|
||||
// OLD: 2048 samples = 42ms
|
||||
audioCtxGlobal.ctx, audioCtxGlobal.err = oto.NewContext(sampleRate, channels, bytesPerSample, 2048)
|
||||
|
||||
// NEW: 8192 samples = 170ms (more buffer = smoother playback)
|
||||
audioCtxGlobal.ctx, audioCtxGlobal.err = oto.NewContext(sampleRate, channels, bytesPerSample, 8192)
|
||||
```
|
||||
|
||||
**Change `main.go:9274`:**
|
||||
```go
|
||||
// OLD: 4096 bytes = 21ms
|
||||
chunk := make([]byte, 4096)
|
||||
|
||||
// NEW: 16384 bytes = 85ms per chunk
|
||||
chunk := make([]byte, 16384)
|
||||
```
|
||||
|
||||
**Expected Result:** Audio stuttering should improve significantly
|
||||
|
||||
---
|
||||
|
||||
### Priority 2: Use FFmpeg for Volume Control
|
||||
|
||||
**Change `main.go:9238-9247`:**
|
||||
```go
|
||||
// Add volume filter to FFmpeg command instead of processing in Go
|
||||
volumeFilter := ""
|
||||
if p.muted || p.volume <= 0 {
|
||||
volumeFilter = "-af volume=0"
|
||||
} else if math.Abs(p.volume - 100) > 0.1 {
|
||||
volumeFilter = fmt.Sprintf("-af volume=%.2f", p.volume/100.0)
|
||||
}
|
||||
|
||||
cmd := exec.Command(platformConfig.FFmpegPath,
|
||||
"-hide_banner", "-loglevel", "error",
|
||||
"-ss", fmt.Sprintf("%.3f", offset),
|
||||
"-i", p.path,
|
||||
"-vn",
|
||||
"-ac", fmt.Sprintf("%d", channels),
|
||||
"-ar", fmt.Sprintf("%d", sampleRate),
|
||||
volumeFilter, // Let FFmpeg handle volume
|
||||
"-f", "s16le",
|
||||
"-",
|
||||
)
|
||||
```
|
||||
|
||||
**Remove volume processing loop (lines 9294-9318):**
|
||||
```go
|
||||
// Simply write chunks directly
|
||||
localPlayer.Write(chunk[:n])
|
||||
```
|
||||
|
||||
**Expected Result:** Reduced CPU usage, smoother audio
|
||||
|
||||
---
|
||||
|
||||
### Priority 3: Use Single FFmpeg Process with A/V Sync
|
||||
|
||||
**Conceptual Change:**
|
||||
Instead of separate video/audio processes, use ONE FFmpeg process that:
|
||||
1. Outputs video frames to one pipe
|
||||
2. Outputs audio to another pipe (or use `-f matroska` with demuxing)
|
||||
3. Maintains sync internally
|
||||
|
||||
**Pseudocode:**
|
||||
```go
|
||||
cmd := exec.Command(platformConfig.FFmpegPath,
|
||||
"-ss", fmt.Sprintf("%.3f", offset),
|
||||
"-i", p.path,
|
||||
// Video stream
|
||||
"-map", "0:v:0",
|
||||
"-f", "rawvideo",
|
||||
"-pix_fmt", "rgb24",
|
||||
"-r", fmt.Sprintf("%.3f", p.fps),
|
||||
"pipe:4", // Video to fd 4
|
||||
// Audio stream
|
||||
"-map", "0:a:0",
|
||||
"-ac", "2",
|
||||
"-ar", "48000",
|
||||
"-f", "s16le",
|
||||
"pipe:5", // Audio to fd 5
|
||||
)
|
||||
```
|
||||
|
||||
**Expected Result:** Perfect A/V sync, no drift
|
||||
|
||||
---
|
||||
|
||||
### Priority 4: Frame Buffer Reuse
|
||||
|
||||
**Change `main.go:9205-9206`:**
|
||||
```go
|
||||
// Reuse frame buffers instead of allocating every frame
|
||||
type framePool struct {
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
func (p *framePool) get(w, h int) *image.RGBA {
|
||||
if img := p.pool.Get(); img != nil {
|
||||
return img.(*image.RGBA)
|
||||
}
|
||||
return image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
}
|
||||
|
||||
func (p *framePool) put(img *image.RGBA) {
|
||||
// Clear pixel data
|
||||
for i := range img.Pix {
|
||||
img.Pix[i] = 0
|
||||
}
|
||||
p.pool.Put(img)
|
||||
}
|
||||
|
||||
// In video loop:
|
||||
frame := framePool.get(p.targetW, p.targetH)
|
||||
utils.CopyRGBToRGBA(frame.Pix, buf)
|
||||
// ... use frame ...
|
||||
// Note: can't return to pool if UI is still using it
|
||||
```
|
||||
|
||||
**Expected Result:** Reduced GC pressure, smoother frame delivery
|
||||
|
||||
---
|
||||
|
||||
### Priority 5: Adaptive Frame Timing
|
||||
|
||||
**Change `main.go:9200-9203`:**
|
||||
```go
|
||||
// Track actual vs expected time to detect drift
|
||||
now := time.Now()
|
||||
behind := now.Sub(nextFrameAt)
|
||||
|
||||
if behind < 0 {
|
||||
// We're ahead, sleep until next frame
|
||||
time.Sleep(-behind)
|
||||
} else if behind > frameDur*2 {
|
||||
// We're way behind (>2 frames), skip this frame
|
||||
logging.Debug(logging.CatFFMPEG, "dropping frame, %.0fms behind", behind.Seconds()*1000)
|
||||
nextFrameAt = now
|
||||
continue
|
||||
} else {
|
||||
// We're slightly behind, catchup gradually
|
||||
nextFrameAt = now.Add(frameDur / 2)
|
||||
}
|
||||
|
||||
nextFrameAt = nextFrameAt.Add(frameDur)
|
||||
```
|
||||
|
||||
**Expected Result:** Better handling of temporary slowdowns, adaptive recovery
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
After each fix, test:
|
||||
|
||||
- [ ] 24fps video plays smoothly
|
||||
- [ ] 30fps video plays smoothly
|
||||
- [ ] 60fps video plays smoothly
|
||||
- [ ] Audio doesn't stutter
|
||||
- [ ] A/V sync maintained over 30+ seconds
|
||||
- [ ] Seeking doesn't cause prolonged stuttering
|
||||
- [ ] CPU usage is reasonable (<20% for playback)
|
||||
- [ ] Works on both Linux and Windows
|
||||
- [ ] Works with various codecs (H.264, H.265, VP9)
|
||||
- [ ] Volume control works smoothly
|
||||
- [ ] Pause/resume doesn't cause issues
|
||||
|
||||
---
|
||||
|
||||
## Performance Monitoring
|
||||
|
||||
Add instrumentation to measure:
|
||||
|
||||
```go
|
||||
// Video frame timing
|
||||
frameDeliveryTime := time.Since(frameReadStart)
|
||||
if frameDeliveryTime > frameDur*1.5 {
|
||||
logging.Debug(logging.CatFFMPEG, "slow frame delivery: %.1fms (target: %.1fms)",
|
||||
frameDeliveryTime.Seconds()*1000,
|
||||
frameDur.Seconds()*1000)
|
||||
}
|
||||
|
||||
// Audio buffer health
|
||||
if audioBufferFillLevel < 0.3 {
|
||||
logging.Debug(logging.CatFFMPEG, "audio buffer low: %.0f%%", audioBufferFillLevel*100)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Alternative: Use External Player Library
|
||||
|
||||
If these tweaks don't achieve smooth playback, consider:
|
||||
|
||||
1. **mpv library** (libmpv) - Industry standard, perfect A/V sync
|
||||
2. **FFmpeg's ffplay** code - Reference implementation
|
||||
3. **VLC libvlc** - Proven playback engine
|
||||
|
||||
These handle all the complex synchronization automatically.
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
**Root Causes:**
|
||||
1. Separate video/audio processes with no sync
|
||||
2. Tiny audio buffers causing underruns
|
||||
3. CPU waste on per-sample volume processing
|
||||
4. Frame timing drift with no correction
|
||||
5. UI thread blocking frame updates
|
||||
|
||||
**Quick Wins (30 min):**
|
||||
- Increase audio buffers (Priority 1)
|
||||
- Move volume to FFmpeg (Priority 2)
|
||||
|
||||
**Proper Fix (2-4 hours):**
|
||||
- Single FFmpeg process with A/V muxing (Priority 3)
|
||||
- Frame buffer pooling (Priority 4)
|
||||
- Adaptive timing (Priority 5)
|
||||
|
||||
**Expected Final Result:**
|
||||
- Smooth playback at all frame rates
|
||||
- Rock-solid A/V sync
|
||||
- Low CPU usage
|
||||
- No stuttering or dropouts
|
||||
212
author_module.go
212
author_module.go
|
|
@ -75,7 +75,6 @@ func buildAuthorView(state *appState) fyne.CanvasObject {
|
|||
}
|
||||
|
||||
func buildVideoClipsTab(state *appState) fyne.CanvasObject {
|
||||
state.authorVideoTSPath = strings.TrimSpace(state.authorVideoTSPath)
|
||||
list := container.NewVBox()
|
||||
listScroll := container.NewVScroll(list)
|
||||
|
||||
|
|
@ -153,7 +152,6 @@ func buildVideoClipsTab(state *appState) fyne.CanvasObject {
|
|||
state.authorClips = []authorClip{}
|
||||
state.authorChapters = nil
|
||||
state.authorChapterSource = ""
|
||||
state.authorVideoTSPath = ""
|
||||
rebuildList()
|
||||
state.updateAuthorSummary()
|
||||
})
|
||||
|
|
@ -598,9 +596,7 @@ func buildAuthorDiscTab(state *appState) fyne.CanvasObject {
|
|||
|
||||
func authorSummary(state *appState) string {
|
||||
summary := "Ready to generate:\n\n"
|
||||
if state.authorVideoTSPath != "" {
|
||||
summary += fmt.Sprintf("VIDEO_TS: %s\n", filepath.Base(filepath.Dir(state.authorVideoTSPath)))
|
||||
} else if len(state.authorClips) > 0 {
|
||||
if len(state.authorClips) > 0 {
|
||||
summary += fmt.Sprintf("Videos: %d\n", len(state.authorClips))
|
||||
for i, clip := range state.authorClips {
|
||||
summary += fmt.Sprintf(" %d. %s (%.2fs)\n", i+1, clip.DisplayName, clip.Duration)
|
||||
|
|
@ -624,9 +620,6 @@ func authorSummary(state *appState) string {
|
|||
summary += fmt.Sprintf("Disc Size: %s\n", state.authorDiscSize)
|
||||
summary += fmt.Sprintf("Region: %s\n", state.authorRegion)
|
||||
summary += fmt.Sprintf("Aspect Ratio: %s\n", state.authorAspectRatio)
|
||||
if outPath := authorDefaultOutputPath(state.authorOutputType, authorOutputTitle(state), authorSummaryPaths(state)); outPath != "" {
|
||||
summary += fmt.Sprintf("Output Path: %s\n", outPath)
|
||||
}
|
||||
if state.authorTitle != "" {
|
||||
summary += fmt.Sprintf("DVD Title: %s\n", state.authorTitle)
|
||||
}
|
||||
|
|
@ -703,34 +696,6 @@ func authorTotalDuration(state *appState) float64 {
|
|||
return 0
|
||||
}
|
||||
|
||||
func authorSummaryPaths(state *appState) []string {
|
||||
if state.authorVideoTSPath != "" {
|
||||
return []string{state.authorVideoTSPath}
|
||||
}
|
||||
if len(state.authorClips) > 0 {
|
||||
paths := make([]string, 0, len(state.authorClips))
|
||||
for _, clip := range state.authorClips {
|
||||
paths = append(paths, clip.Path)
|
||||
}
|
||||
return paths
|
||||
}
|
||||
if state.authorFile != nil {
|
||||
return []string{state.authorFile.Path}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func authorOutputTitle(state *appState) string {
|
||||
title := strings.TrimSpace(state.authorTitle)
|
||||
if title != "" {
|
||||
return title
|
||||
}
|
||||
if state.authorVideoTSPath != "" {
|
||||
return filepath.Base(filepath.Dir(state.authorVideoTSPath))
|
||||
}
|
||||
return defaultAuthorTitle(authorSummaryPaths(state))
|
||||
}
|
||||
|
||||
func authorTargetBitrateKbps(discSize string, totalSeconds float64) int {
|
||||
if totalSeconds <= 0 {
|
||||
return 0
|
||||
|
|
@ -1034,19 +999,6 @@ func (s *appState) setAuthorProgress(percent float64) {
|
|||
}
|
||||
|
||||
func (s *appState) startAuthorGeneration() {
|
||||
if s.authorVideoTSPath != "" {
|
||||
title := authorOutputTitle(s)
|
||||
outputPath := authorDefaultOutputPath("iso", title, []string{s.authorVideoTSPath})
|
||||
if outputPath == "" {
|
||||
dialog.ShowError(fmt.Errorf("failed to resolve output path"), s.window)
|
||||
return
|
||||
}
|
||||
if err := s.addAuthorVideoTSToQueue(s.authorVideoTSPath, title, outputPath, true); err != nil {
|
||||
dialog.ShowError(err, s.window)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
paths, primary, err := s.authorSourcePaths()
|
||||
if err != nil {
|
||||
dialog.ShowError(err, s.window)
|
||||
|
|
@ -1094,12 +1046,28 @@ func (s *appState) promptAuthorOutput(paths []string, region, aspect, title stri
|
|||
outputType = "dvd"
|
||||
}
|
||||
|
||||
outputPath := authorDefaultOutputPath(outputType, title, paths)
|
||||
if outputType == "iso" {
|
||||
s.generateAuthoring(paths, region, aspect, title, outputPath, true)
|
||||
dialog.ShowFileSave(func(writer fyne.URIWriteCloser, err error) {
|
||||
if err != nil || writer == nil {
|
||||
return
|
||||
}
|
||||
path := writer.URI().Path()
|
||||
writer.Close()
|
||||
if !strings.HasSuffix(strings.ToLower(path), ".iso") {
|
||||
path += ".iso"
|
||||
}
|
||||
s.generateAuthoring(paths, region, aspect, title, path, true)
|
||||
}, s.window)
|
||||
return
|
||||
}
|
||||
s.generateAuthoring(paths, region, aspect, title, outputPath, false)
|
||||
|
||||
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
|
||||
if err != nil || uri == nil {
|
||||
return
|
||||
}
|
||||
discRoot := filepath.Join(uri.Path(), authorOutputFolderName(title, paths))
|
||||
s.generateAuthoring(paths, region, aspect, title, discRoot, false)
|
||||
}, s.window)
|
||||
}
|
||||
|
||||
func authorWarnings(state *appState) []string {
|
||||
|
|
@ -1199,66 +1167,6 @@ func authorOutputFolderName(title string, paths []string) string {
|
|||
return name
|
||||
}
|
||||
|
||||
func authorDefaultOutputDir(outputType string) string {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil || home == "" {
|
||||
home = "."
|
||||
}
|
||||
dir := filepath.Join(home, "Videos")
|
||||
if strings.EqualFold(outputType, "iso") {
|
||||
return filepath.Join(dir, "ISO_Convert")
|
||||
}
|
||||
return filepath.Join(dir, "DVD_Convert")
|
||||
}
|
||||
|
||||
func authorDefaultOutputPath(outputType, title string, paths []string) string {
|
||||
outputType = strings.ToLower(strings.TrimSpace(outputType))
|
||||
if outputType == "" {
|
||||
outputType = "dvd"
|
||||
}
|
||||
baseDir := authorDefaultOutputDir(outputType)
|
||||
name := strings.TrimSpace(title)
|
||||
if name == "" {
|
||||
name = defaultAuthorTitle(paths)
|
||||
}
|
||||
name = sanitizeForPath(name)
|
||||
if name == "" {
|
||||
name = "dvd_output"
|
||||
}
|
||||
if outputType == "iso" {
|
||||
return uniqueFilePath(filepath.Join(baseDir, name+".iso"))
|
||||
}
|
||||
return uniqueFolderPath(filepath.Join(baseDir, name))
|
||||
}
|
||||
|
||||
func uniqueFolderPath(path string) string {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return path
|
||||
}
|
||||
for i := 1; i < 1000; i++ {
|
||||
tryPath := fmt.Sprintf("%s-%d", path, i)
|
||||
if _, err := os.Stat(tryPath); os.IsNotExist(err) {
|
||||
return tryPath
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s-%d", path, time.Now().Unix())
|
||||
}
|
||||
|
||||
func uniqueFilePath(path string) string {
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return path
|
||||
}
|
||||
ext := filepath.Ext(path)
|
||||
base := strings.TrimSuffix(path, ext)
|
||||
for i := 1; i < 1000; i++ {
|
||||
tryPath := fmt.Sprintf("%s-%d%s", base, i, ext)
|
||||
if _, err := os.Stat(tryPath); os.IsNotExist(err) {
|
||||
return tryPath
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("%s-%d%s", base, time.Now().Unix(), ext)
|
||||
}
|
||||
|
||||
func (s *appState) generateAuthoring(paths []string, region, aspect, title, outputPath string, makeISO bool) {
|
||||
if err := s.addAuthorToQueue(paths, region, aspect, title, outputPath, makeISO, true); err != nil {
|
||||
dialog.ShowError(err, s.window)
|
||||
|
|
@ -1331,34 +1239,6 @@ func (s *appState) addAuthorToQueue(paths []string, region, aspect, title, outpu
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *appState) addAuthorVideoTSToQueue(videoTSPath, title, outputPath string, startNow bool) error {
|
||||
if s.jobQueue == nil {
|
||||
return fmt.Errorf("queue not initialized")
|
||||
}
|
||||
job := &queue.Job{
|
||||
Type: queue.JobTypeAuthor,
|
||||
Title: fmt.Sprintf("Author ISO: %s", title),
|
||||
Description: fmt.Sprintf("VIDEO_TS -> %s", utils.ShortenMiddle(filepath.Base(outputPath), 40)),
|
||||
InputFile: videoTSPath,
|
||||
OutputFile: outputPath,
|
||||
Config: map[string]interface{}{
|
||||
"videoTSPath": videoTSPath,
|
||||
"outputPath": outputPath,
|
||||
"makeISO": true,
|
||||
"title": title,
|
||||
},
|
||||
}
|
||||
|
||||
s.resetAuthorLog()
|
||||
s.setAuthorStatus("Queued authoring job...")
|
||||
s.setAuthorProgress(0)
|
||||
s.jobQueue.Add(job)
|
||||
if startNow && !s.jobQueue.IsRunning() {
|
||||
s.jobQueue.Start()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, region, aspect, title, outputPath string, makeISO bool, clips []authorClip, chapters []authorChapter, treatAsChapters bool, logFn func(string), progressFn func(float64)) error {
|
||||
workDir, err := os.MkdirTemp(utils.TempDir(), "videotools-author-")
|
||||
if err != nil {
|
||||
|
|
@ -1496,58 +1376,6 @@ func (s *appState) executeAuthorJob(ctx context.Context, job *queue.Job, progres
|
|||
if cfg == nil {
|
||||
return fmt.Errorf("author job config missing")
|
||||
}
|
||||
if videoTSPath := strings.TrimSpace(toString(cfg["videoTSPath"])); videoTSPath != "" {
|
||||
outputPath := toString(cfg["outputPath"])
|
||||
title := toString(cfg["title"])
|
||||
if err := ensureAuthorDependencies(true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logFile, logPath, logErr := createAuthorLog([]string{videoTSPath}, outputPath, true, "", "", title)
|
||||
if logErr != nil {
|
||||
logging.Debug(logging.CatSystem, "author log open failed: %v", logErr)
|
||||
} else {
|
||||
job.LogPath = logPath
|
||||
defer logFile.Close()
|
||||
}
|
||||
|
||||
appendLog := func(line string) {
|
||||
if logFile != nil {
|
||||
fmt.Fprintln(logFile, line)
|
||||
}
|
||||
app := fyne.CurrentApp()
|
||||
if app != nil && app.Driver() != nil {
|
||||
app.Driver().DoFromGoroutine(func() {
|
||||
s.appendAuthorLog(line)
|
||||
}, false)
|
||||
}
|
||||
}
|
||||
|
||||
updateProgress := func(percent float64) {
|
||||
progressCallback(percent)
|
||||
app := fyne.CurrentApp()
|
||||
if app != nil && app.Driver() != nil {
|
||||
app.Driver().DoFromGoroutine(func() {
|
||||
s.setAuthorProgress(percent)
|
||||
}, false)
|
||||
}
|
||||
}
|
||||
|
||||
appendLog(fmt.Sprintf("Authoring ISO from VIDEO_TS: %s", videoTSPath))
|
||||
tool, args, err := buildISOCommand(outputPath, videoTSPath, title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
appendLog(fmt.Sprintf(">> %s %s", tool, strings.Join(args, " ")))
|
||||
updateProgress(10)
|
||||
if err := runCommandWithLogger(ctx, tool, args, appendLog); err != nil {
|
||||
return err
|
||||
}
|
||||
updateProgress(100)
|
||||
appendLog("ISO creation completed successfully.")
|
||||
return nil
|
||||
}
|
||||
|
||||
rawPaths, _ := cfg["paths"].([]interface{})
|
||||
var paths []string
|
||||
for _, p := range rawPaths {
|
||||
|
|
|
|||
31
main.go
31
main.go
|
|
@ -926,7 +926,6 @@ type appState struct {
|
|||
authorProgress float64
|
||||
authorProgressBar *widget.ProgressBar
|
||||
authorStatusLabel *widget.Label
|
||||
authorVideoTSPath string
|
||||
|
||||
// Subtitles module state
|
||||
subtitleVideoPath string
|
||||
|
|
@ -8958,9 +8957,7 @@ var audioCtxGlobal struct {
|
|||
|
||||
func getAudioContext(sampleRate, channels, bytesPerSample int) (*oto.Context, error) {
|
||||
audioCtxGlobal.once.Do(func() {
|
||||
// Increased from 2048 (42ms) to 8192 (170ms) for smoother playback
|
||||
// Larger buffer prevents audio stuttering and underruns
|
||||
audioCtxGlobal.ctx, audioCtxGlobal.err = oto.NewContext(sampleRate, channels, bytesPerSample, 8192)
|
||||
audioCtxGlobal.ctx, audioCtxGlobal.err = oto.NewContext(sampleRate, channels, bytesPerSample, 2048)
|
||||
})
|
||||
return audioCtxGlobal.ctx, audioCtxGlobal.err
|
||||
}
|
||||
|
|
@ -9274,10 +9271,8 @@ func (p *playSession) runAudio(offset float64) {
|
|||
go func() {
|
||||
defer cmd.Process.Kill()
|
||||
defer localPlayer.Close()
|
||||
// Increased from 4096 (21ms) to 16384 (85ms) for smoother playback
|
||||
// Larger chunks reduce read frequency and improve performance
|
||||
chunk := make([]byte, 16384)
|
||||
tmp := make([]byte, 16384)
|
||||
chunk := make([]byte, 4096)
|
||||
tmp := make([]byte, 4096)
|
||||
loggedFirst := false
|
||||
for {
|
||||
select {
|
||||
|
|
@ -9520,22 +9515,12 @@ func (s *appState) handleDrop(pos fyne.Position, items []fyne.URI) {
|
|||
// If in author module, add video clips
|
||||
if s.active == "author" {
|
||||
var videoPaths []string
|
||||
var videoTSPath string
|
||||
for _, uri := range items {
|
||||
if uri.Scheme() != "file" {
|
||||
continue
|
||||
}
|
||||
path := uri.Path()
|
||||
if info, err := os.Stat(path); err == nil && info.IsDir() {
|
||||
if strings.EqualFold(filepath.Base(path), "VIDEO_TS") {
|
||||
videoTSPath = path
|
||||
break
|
||||
}
|
||||
videoTSChild := filepath.Join(path, "VIDEO_TS")
|
||||
if info, err := os.Stat(videoTSChild); err == nil && info.IsDir() {
|
||||
videoTSPath = videoTSChild
|
||||
break
|
||||
}
|
||||
videos := s.findVideoFiles(path)
|
||||
videoPaths = append(videoPaths, videos...)
|
||||
} else if s.isVideoFile(path) {
|
||||
|
|
@ -9543,15 +9528,6 @@ func (s *appState) handleDrop(pos fyne.Position, items []fyne.URI) {
|
|||
}
|
||||
}
|
||||
|
||||
if videoTSPath != "" {
|
||||
s.authorVideoTSPath = videoTSPath
|
||||
s.authorClips = nil
|
||||
s.authorFile = nil
|
||||
s.authorOutputType = "iso"
|
||||
s.showAuthorView()
|
||||
return
|
||||
}
|
||||
|
||||
if len(videoPaths) == 0 {
|
||||
logging.Debug(logging.CatUI, "no valid video files in dropped items")
|
||||
return
|
||||
|
|
@ -12585,6 +12561,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|||
return container.NewBorder(topBar, bottomBar, nil, nil, content)
|
||||
}
|
||||
|
||||
|
||||
// buildThumbView creates the thumbnail generation UI
|
||||
func buildThumbView(state *appState) fyne.CanvasObject {
|
||||
thumbColor := moduleColor("thumb")
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user