feat(player): replace UnifiedPlayerAdapter with GStreamer in playSession

- Replace unifiedAdapter with gstPlayer in playSession struct
- Update all playSession methods to use GStreamerPlayer:
  - Play(), Pause(), Seek(), StepFrame()
  - SetVolume(), Stop(), stopLocked(), startLocked()
  - GetCurrentFrame()
- Add frameDisplayLoop() to continuously pull frames from GStreamer
- Connect GStreamer frame output to Fyne canvas for display
- Remove all UnifiedPlayerAdapter dependencies

This completes the GStreamer integration for both Player module
and Convert preview system. Both now use the same stable GStreamer
backend for video playback.
This commit is contained in:
Stu Leak 2026-01-09 03:50:32 -05:00
parent 57eecf96df
commit 00df0b3b31

278
main.go
View File

@ -11163,8 +11163,8 @@ type playSession struct {
syncOffset float64 // A/V sync offset for adjustment syncOffset float64 // A/V sync offset for adjustment
audioActive atomic.Bool // Whether audio stream is running audioActive atomic.Bool // Whether audio stream is running
// UnifiedPlayer adapter for stable A/V playback // GStreamer player for stable A/V playback
unifiedAdapter *player.UnifiedPlayerAdapter gstPlayer *player.GStreamerPlayer
} }
var audioCtxGlobal struct { var audioCtxGlobal struct {
@ -11203,58 +11203,84 @@ func newPlaySession(path string, w, h int, fps, duration float64, targetW, targe
targetH = int(float64(targetW) * (float64(h) / float64(utils.MaxInt(w, 1)))) targetH = int(float64(targetW) * (float64(h) / float64(utils.MaxInt(w, 1))))
} }
// Create UnifiedPlayer adapter for stable A/V playback // Create GStreamer player for stable A/V playback
unifiedAdapter := player.NewUnifiedPlayerAdapter(path, w, h, fps, duration, targetW, targetH, prog, frameFunc, img) gstPlayer, err := player.NewGStreamerPlayer(player.Config{
Backend: player.BackendAuto,
WindowWidth: targetW,
WindowHeight: targetH,
Volume: 1.0,
Muted: false,
AutoPlay: false,
HardwareAccel: false,
PreviewMode: false, // Full playback with audio
AudioOutput: "auto",
VideoOutput: "rgb24",
CacheEnabled: true,
CacheSize: 64 * 1024 * 1024,
LogLevel: player.LogInfo,
})
if err != nil {
logging.Error(logging.CatPlayer, "Failed to create GStreamer player for playback: %v", err)
return nil
}
return &playSession{ sess := &playSession{
path: path, path: path,
fps: fps, fps: fps,
width: w, width: w,
height: h, height: h,
targetW: targetW, targetW: targetW,
targetH: targetH, targetH: targetH,
volume: 100, volume: 100,
duration: duration, duration: duration,
stop: make(chan struct{}), stop: make(chan struct{}),
done: make(chan struct{}), done: make(chan struct{}),
prog: prog, prog: prog,
frameFunc: frameFunc, frameFunc: frameFunc,
img: img, img: img,
unifiedAdapter: unifiedAdapter, gstPlayer: gstPlayer,
} }
// Load the video in GStreamer
if err := gstPlayer.Load(path, 0); err != nil {
logging.Error(logging.CatPlayer, "Failed to load video in GStreamer: %v", err)
return nil
}
// Start frame display loop
go sess.frameDisplayLoop()
return sess
} }
func (p *playSession) Play() { func (p *playSession) Play() {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
// Use UnifiedPlayer adapter if available // Use GStreamer player
if p.unifiedAdapter != nil { if p.gstPlayer != nil {
p.unifiedAdapter.Play() p.gstPlayer.Play()
p.paused = false p.paused = false
logging.Debug(logging.CatPlayer, "playSession: Play called")
return return
} }
// Fallback to dual-process logging.Error(logging.CatPlayer, "playSession: GStreamer player not available")
if p.videoCmd == nil && p.audioCmd == nil {
p.startLocked(p.current)
return
}
p.paused = false
} }
func (p *playSession) Pause() { func (p *playSession) Pause() {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
// Use UnifiedPlayer adapter if available // Use GStreamer player
if p.unifiedAdapter != nil { if p.gstPlayer != nil {
p.unifiedAdapter.Pause() p.gstPlayer.Pause()
p.paused = true p.paused = true
logging.Debug(logging.CatPlayer, "playSession: Pause called")
return return
} }
p.paused = true logging.Error(logging.CatPlayer, "playSession: GStreamer player not available")
} }
func (p *playSession) Seek(offset float64) { func (p *playSession) Seek(offset float64) {
@ -11264,31 +11290,18 @@ func (p *playSession) Seek(offset float64) {
offset = 0 offset = 0
} }
// Use UnifiedPlayer adapter if available // Use GStreamer player
if p.unifiedAdapter != nil { if p.gstPlayer != nil {
p.unifiedAdapter.Seek(offset) p.gstPlayer.SeekToTime(time.Duration(offset * float64(time.Second)))
p.current = offset p.current = offset
p.paused = p.unifiedAdapter.IsPlaying() == false logging.Debug(logging.CatPlayer, "playSession: Seek to %.2fs", offset)
if p.prog != nil {
p.prog(p.current)
}
return return
} }
// Fallback to dual-process logging.Error(logging.CatPlayer, "playSession: GStreamer player not available")
paused := p.paused
p.current = offset
p.stopLocked()
p.startLocked(p.current)
p.paused = paused
if p.paused {
// Ensure loops honor paused right after restart.
time.AfterFunc(30*time.Millisecond, func() {
p.mu.Lock()
defer p.mu.Unlock()
p.paused = true
})
}
if p.prog != nil {
p.prog(p.current)
}
} }
// StepFrame moves forward or backward by a specific number of frames. // StepFrame moves forward or backward by a specific number of frames.
@ -11300,68 +11313,106 @@ func (p *playSession) StepFrame(delta int) {
return return
} }
// Use UnifiedPlayer adapter if available // Use GStreamer player
if p.unifiedAdapter != nil { if p.gstPlayer != nil {
p.unifiedAdapter.StepFrame(delta) currentFrame := int(p.current * p.fps)
p.current = float64(p.unifiedAdapter.GetCurrentFrame()) / p.fps targetFrame := currentFrame + delta
// Clamp to valid range
if targetFrame < 0 {
targetFrame = 0
}
maxFrame := int(p.duration * p.fps)
if targetFrame > maxFrame {
targetFrame = maxFrame
}
// Seek to target frame
p.gstPlayer.SeekToFrame(int64(targetFrame))
p.current = float64(targetFrame) / p.fps
p.paused = true p.paused = true
p.frameN = targetFrame
if p.frameFunc != nil {
p.frameFunc(targetFrame)
}
if p.prog != nil {
p.prog(p.current)
}
logging.Debug(logging.CatPlayer, "playSession: StepFrame delta=%d to frame %d", delta, targetFrame)
return return
} }
// Fallback to dual-process logging.Error(logging.CatPlayer, "playSession: GStreamer player not available")
currentFrame := int(p.current * p.fps) }
targetFrame := currentFrame + delta
// Clamp to valid range // frameDisplayLoop continuously pulls frames from GStreamer and updates the UI
if targetFrame < 0 { func (p *playSession) frameDisplayLoop() {
targetFrame = 0 if p.fps <= 0 {
} p.fps = 24
maxFrame := int(p.duration * p.fps)
if targetFrame > maxFrame {
targetFrame = maxFrame
} }
frameDuration := time.Second / time.Duration(p.fps)
ticker := time.NewTicker(frameDuration)
defer ticker.Stop()
// Convert to time offset frameCount := 0
offset := float64(targetFrame) / p.fps logging.Debug(logging.CatPlayer, "playSession: frameDisplayLoop started (fps=%.2f)", p.fps)
if offset < 0 {
offset = 0
}
if offset > p.duration {
offset = p.duration
}
// Auto-pause when frame stepping for {
p.paused = true select {
p.current = offset case <-p.stop:
logging.Debug(logging.CatPlayer, "playSession: frameDisplayLoop stopped")
return
// Seek to new position case <-ticker.C:
if offset >= 0 { if p.gstPlayer == nil {
p.stopLocked() continue
p.startLocked(offset) }
}
// Ensure loops honor paused right after restart. // Skip frame updates when paused
time.AfterFunc(30*time.Millisecond, func() { if p.paused {
p.mu.Lock() continue
defer p.mu.Unlock() }
p.paused = true
})
if p.prog != nil { // Get current frame from GStreamer
p.prog(p.current) frame, err := p.gstPlayer.GetFrameImage()
if err != nil {
logging.Debug(logging.CatPlayer, "Frame read error: %v", err)
continue
}
if frame == nil {
continue
}
// Update frame counter
frameCount++
p.mu.Lock()
p.frameN = frameCount
currentTime := p.gstPlayer.GetCurrentTime()
p.current = currentTime.Seconds()
p.mu.Unlock()
// Update UI on main thread
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
if p.img != nil {
p.img.Image = frame
p.img.Refresh()
}
if p.prog != nil {
p.prog(p.current)
}
if p.frameFunc != nil {
p.frameFunc(frameCount)
}
}, false)
}
} }
} }
// GetCurrentFrame returns the current frame number
func (p *playSession) GetCurrentFrame() int { func (p *playSession) GetCurrentFrame() int {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
// Use UnifiedPlayer adapter if available
if p.unifiedAdapter != nil {
return p.unifiedAdapter.GetCurrentFrame()
}
return p.frameN return p.frameN
} }
@ -11370,17 +11421,10 @@ func (p *playSession) SetVolume(v float64) {
defer p.mu.Unlock() defer p.mu.Unlock()
p.volume = v p.volume = v
// Use UnifiedPlayer adapter if available // Use GStreamer player (volume is 0.0-1.0 range)
if p.unifiedAdapter != nil { if p.gstPlayer != nil {
p.unifiedAdapter.SetVolume(v) p.gstPlayer.SetVolume(v / 100.0)
return logging.Debug(logging.CatPlayer, "playSession: SetVolume to %.1f%%", v)
}
// Fallback to dual-process
if p.audioCmd != nil && p.audioCmd.Process != nil {
// Send volume command to FFmpeg
cmd := fmt.Sprintf("volume %.1f\n", v/100.0)
p.writeStringToStdin(cmd)
} }
} }
@ -11408,9 +11452,11 @@ func (p *playSession) Stop() {
p.mu.Lock() p.mu.Lock()
defer p.mu.Unlock() defer p.mu.Unlock()
// Use UnifiedPlayer adapter if available // Use GStreamer player
if p.unifiedAdapter != nil { if p.gstPlayer != nil {
p.unifiedAdapter.Stop() p.gstPlayer.Stop()
close(p.stop)
logging.Debug(logging.CatPlayer, "playSession: Stop called")
return return
} }
@ -11419,10 +11465,9 @@ func (p *playSession) Stop() {
} }
func (p *playSession) stopLocked() { func (p *playSession) stopLocked() {
// Use UnifiedPlayer adapter if available // Stop GStreamer player
if p.unifiedAdapter != nil { if p.gstPlayer != nil {
p.unifiedAdapter.Stop() p.gstPlayer.Stop()
return
} }
// Fallback to dual-process cleanup // Fallback to dual-process cleanup
@ -11453,18 +11498,15 @@ func (p *playSession) startLocked(offset float64) {
p.audioTime.Store(offset) p.audioTime.Store(offset)
p.videoTime = offset p.videoTime = offset
p.syncOffset = 0 p.syncOffset = 0
logging.Debug(logging.CatFFMPEG, "playSession start path=%s offset=%.3f fps=%.3f target=%dx%d", p.path, offset, p.fps, p.targetW, p.targetH) logging.Debug(logging.CatPlayer, "playSession start path=%s offset=%.3f fps=%.3f target=%dx%d", p.path, offset, p.fps, p.targetW, p.targetH)
// If using UnifiedPlayer adapter, no need to run dual-process // GStreamer handles playback internally - just seek to position
if p.unifiedAdapter != nil { if p.gstPlayer != nil {
// UnifiedPlayer handles A/V sync internally p.gstPlayer.SeekToTime(time.Duration(offset * float64(time.Second)))
p.unifiedAdapter.Seek(offset)
return return
} }
// Fallback to dual-process (old method) logging.Error(logging.CatPlayer, "playSession: GStreamer player not available in startLocked")
p.runVideo(offset)
p.runAudio(offset)
} }
func (p *playSession) runVideo(offset float64) { func (p *playSession) runVideo(offset float64) {