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
|
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) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user