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. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
501e2622dc
commit
ca48de7dc6
278
main.go
278
main.go
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user