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
audioActive atomic.Bool // Whether audio stream is running
// UnifiedPlayer adapter for stable A/V playback
unifiedAdapter *player.UnifiedPlayerAdapter
// GStreamer player for stable A/V playback
gstPlayer *player.GStreamerPlayer
}
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))))
}
// Create UnifiedPlayer adapter for stable A/V playback
unifiedAdapter := player.NewUnifiedPlayerAdapter(path, w, h, fps, duration, targetW, targetH, prog, frameFunc, img)
// Create GStreamer player for stable A/V playback
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,
fps: fps,
width: w,
height: h,
targetW: targetW,
targetH: targetH,
volume: 100,
duration: duration,
stop: make(chan struct{}),
done: make(chan struct{}),
prog: prog,
frameFunc: frameFunc,
img: img,
unifiedAdapter: unifiedAdapter,
volume: 100,
duration: duration,
stop: make(chan struct{}),
done: make(chan struct{}),
prog: prog,
frameFunc: frameFunc,
img: img,
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() {
p.mu.Lock()
defer p.mu.Unlock()
// Use UnifiedPlayer adapter if available
if p.unifiedAdapter != nil {
p.unifiedAdapter.Play()
// Use GStreamer player
if p.gstPlayer != nil {
p.gstPlayer.Play()
p.paused = false
logging.Debug(logging.CatPlayer, "playSession: Play called")
return
}
// Fallback to dual-process
if p.videoCmd == nil && p.audioCmd == nil {
p.startLocked(p.current)
return
}
p.paused = false
logging.Error(logging.CatPlayer, "playSession: GStreamer player not available")
}
func (p *playSession) Pause() {
p.mu.Lock()
defer p.mu.Unlock()
// Use UnifiedPlayer adapter if available
if p.unifiedAdapter != nil {
p.unifiedAdapter.Pause()
// Use GStreamer player
if p.gstPlayer != nil {
p.gstPlayer.Pause()
p.paused = true
logging.Debug(logging.CatPlayer, "playSession: Pause called")
return
}
p.paused = true
logging.Error(logging.CatPlayer, "playSession: GStreamer player not available")
}
func (p *playSession) Seek(offset float64) {
@ -11264,31 +11290,18 @@ func (p *playSession) Seek(offset float64) {
offset = 0
}
// Use UnifiedPlayer adapter if available
if p.unifiedAdapter != nil {
p.unifiedAdapter.Seek(offset)
// Use GStreamer player
if p.gstPlayer != nil {
p.gstPlayer.SeekToTime(time.Duration(offset * float64(time.Second)))
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
}
// Fallback to dual-process
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)
}
logging.Error(logging.CatPlayer, "playSession: GStreamer player not available")
}
// StepFrame moves forward or backward by a specific number of frames.
@ -11300,68 +11313,106 @@ func (p *playSession) StepFrame(delta int) {
return
}
// Use UnifiedPlayer adapter if available
if p.unifiedAdapter != nil {
p.unifiedAdapter.StepFrame(delta)
p.current = float64(p.unifiedAdapter.GetCurrentFrame()) / p.fps
// Use GStreamer player
if p.gstPlayer != nil {
currentFrame := int(p.current * 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.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
}
// Fallback to dual-process
currentFrame := int(p.current * p.fps)
targetFrame := currentFrame + delta
logging.Error(logging.CatPlayer, "playSession: GStreamer player not available")
}
// Clamp to valid range
if targetFrame < 0 {
targetFrame = 0
}
maxFrame := int(p.duration * p.fps)
if targetFrame > maxFrame {
targetFrame = maxFrame
// frameDisplayLoop continuously pulls frames from GStreamer and updates the UI
func (p *playSession) frameDisplayLoop() {
if p.fps <= 0 {
p.fps = 24
}
frameDuration := time.Second / time.Duration(p.fps)
ticker := time.NewTicker(frameDuration)
defer ticker.Stop()
// Convert to time offset
offset := float64(targetFrame) / p.fps
if offset < 0 {
offset = 0
}
if offset > p.duration {
offset = p.duration
}
frameCount := 0
logging.Debug(logging.CatPlayer, "playSession: frameDisplayLoop started (fps=%.2f)", p.fps)
// Auto-pause when frame stepping
p.paused = true
p.current = offset
for {
select {
case <-p.stop:
logging.Debug(logging.CatPlayer, "playSession: frameDisplayLoop stopped")
return
// Seek to new position
if offset >= 0 {
p.stopLocked()
p.startLocked(offset)
}
case <-ticker.C:
if p.gstPlayer == nil {
continue
}
// Ensure loops honor paused right after restart.
time.AfterFunc(30*time.Millisecond, func() {
p.mu.Lock()
defer p.mu.Unlock()
p.paused = true
})
// Skip frame updates when paused
if p.paused {
continue
}
if p.prog != nil {
p.prog(p.current)
// Get current frame from GStreamer
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 {
p.mu.Lock()
defer p.mu.Unlock()
// Use UnifiedPlayer adapter if available
if p.unifiedAdapter != nil {
return p.unifiedAdapter.GetCurrentFrame()
}
return p.frameN
}
@ -11370,17 +11421,10 @@ func (p *playSession) SetVolume(v float64) {
defer p.mu.Unlock()
p.volume = v
// Use UnifiedPlayer adapter if available
if p.unifiedAdapter != nil {
p.unifiedAdapter.SetVolume(v)
return
}
// 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)
// Use GStreamer player (volume is 0.0-1.0 range)
if p.gstPlayer != nil {
p.gstPlayer.SetVolume(v / 100.0)
logging.Debug(logging.CatPlayer, "playSession: SetVolume to %.1f%%", v)
}
}
@ -11408,9 +11452,11 @@ func (p *playSession) Stop() {
p.mu.Lock()
defer p.mu.Unlock()
// Use UnifiedPlayer adapter if available
if p.unifiedAdapter != nil {
p.unifiedAdapter.Stop()
// Use GStreamer player
if p.gstPlayer != nil {
p.gstPlayer.Stop()
close(p.stop)
logging.Debug(logging.CatPlayer, "playSession: Stop called")
return
}
@ -11419,10 +11465,9 @@ func (p *playSession) Stop() {
}
func (p *playSession) stopLocked() {
// Use UnifiedPlayer adapter if available
if p.unifiedAdapter != nil {
p.unifiedAdapter.Stop()
return
// Stop GStreamer player
if p.gstPlayer != nil {
p.gstPlayer.Stop()
}
// Fallback to dual-process cleanup
@ -11453,18 +11498,15 @@ func (p *playSession) startLocked(offset float64) {
p.audioTime.Store(offset)
p.videoTime = offset
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
if p.unifiedAdapter != nil {
// UnifiedPlayer handles A/V sync internally
p.unifiedAdapter.Seek(offset)
// GStreamer handles playback internally - just seek to position
if p.gstPlayer != nil {
p.gstPlayer.SeekToTime(time.Duration(offset * float64(time.Second)))
return
}
// Fallback to dual-process (old method)
p.runVideo(offset)
p.runAudio(offset)
logging.Error(logging.CatPlayer, "playSession: GStreamer player not available in startLocked")
}
func (p *playSession) runVideo(offset float64) {