- Add GStreamer as mandatory core dependency in install.sh - Create controller_gstreamer.go wrapping GStreamerPlayer - Add missing methods to GStreamerPlayer (SetWindow, Stop, SetFullScreen) - Fix GstSeekFlags type casting issue - Update build scripts to always use -tags gstreamer - Update controller_linux.go build tag to exclude when gstreamer enabled - Add comprehensive migration documentation GStreamer replaces the broken FFmpeg pipe-based UnifiedPlayer. GStreamer 1.26+ provides frame-accurate seeking and reliable A/V sync. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
6.9 KiB
6.9 KiB
Phase 2: GStreamer Integration Plan
Current State Analysis
Two Player Systems Found:
-
Player Module (
main.go:6609)- Uses:
player.Controllerinterface - Implementation:
ffplayController(uses external ffplay window) - File:
internal/player/controller_linux.go - Problem: External window, not embedded in Fyne UI
- Uses:
-
Convert Preview (
main.go:11132)- Uses:
playSessionstruct - Implementation:
UnifiedPlayerAdapter(broken FFmpeg pipes) - File: Defined in
main.go - Problem: Uses the UnifiedPlayer we're deleting
- Uses:
Integration Strategy
Option A: Unified Approach (RECOMMENDED)
Replace both systems with a single GStreamer-based player:
GStreamerPlayer (internal/player/gstreamer_player.go)
↓
├──> Player Module (embedded playback)
└──> Convert Preview (embedded preview)
Benefits:
- Single code path
- Easier to maintain
- Both use same solid GStreamer backend
Option B: Hybrid Approach
Keep Controller interface, but make it use GStreamer internally:
Controller interface
↓
GStreamerController (wraps GStreamerPlayer)
↓
GStreamerPlayer
Benefits:
- Minimal changes to main.go
- Controller interface stays the same
We'll use Option A - cleaner, simpler.
Implementation Steps
Step 1: Create GStreamer-Based Controller
File: internal/player/controller_gstreamer.go
//go:build gstreamer
package player
import (
"fmt"
"time"
)
func newController() Controller {
return &gstreamerController{
player: NewGStreamerPlayer(Config{
PreviewMode: false,
WindowWidth: 640,
WindowHeight: 360,
}),
}
}
type gstreamerController struct {
player *GStreamerPlayer
}
func (c *gstreamerController) Load(path string, offset float64) error {
return c.player.Load(path, time.Duration(offset*float64(time.Second)))
}
func (c *gstreamerController) SetWindow(x, y, w, h int) {
c.player.SetWindow(x, y, w, h)
}
func (c *gstreamerController) Play() error {
return c.player.Play()
}
func (c *gstreamerController) Pause() error {
return c.player.Pause()
}
func (c *gstreamerController) Seek(offset float64) error {
return c.player.SeekToTime(time.Duration(offset * float64(time.Second)))
}
func (c *gstreamerController) SetVolume(level float64) error {
// Controller uses 0-100, GStreamer uses 0.0-1.0
return c.player.SetVolume(level / 100.0)
}
func (c *gstreamerController) FullScreen() error {
return c.player.SetFullScreen(true)
}
func (c *gstreamerController) Stop() error {
return c.player.Stop()
}
func (c *gstreamerController) Close() {
c.player.Close()
}
Step 2: Update playSession to Use GStreamer
File: main.go (around line 11132)
BEFORE:
type playSession struct {
// ...
unifiedAdapter *player.UnifiedPlayerAdapter
}
func newPlaySession(...) *playSession {
unifiedAdapter := player.NewUnifiedPlayerAdapter(...)
return &playSession{
unifiedAdapter: unifiedAdapter,
// ...
}
}
AFTER:
type playSession struct {
// ...
gstPlayer *player.GStreamerPlayer
}
func newPlaySession(...) *playSession {
gstPlayer, err := player.NewGStreamerPlayer(player.Config{
PreviewMode: true,
WindowWidth: targetW,
WindowHeight: targetH,
Volume: 1.0,
})
if err != nil {
// Handle error
}
return &playSession{
gstPlayer: gstPlayer,
// ...
}
}
Step 3: Update playSession Methods
Replace all unifiedAdapter calls with gstPlayer:
func (p *playSession) Play() {
p.mu.Lock()
defer p.mu.Unlock()
if p.gstPlayer != nil {
p.gstPlayer.Play()
}
p.paused = false
}
func (p *playSession) Pause() {
p.mu.Lock()
defer p.mu.Unlock()
if p.gstPlayer != nil {
p.gstPlayer.Pause()
}
p.paused = true
}
func (p *playSession) Seek(offset float64) {
p.mu.Lock()
defer p.mu.Unlock()
if p.gstPlayer != nil {
p.gstPlayer.SeekToTime(time.Duration(offset * float64(time.Second)))
}
p.current = offset
// ...
}
func (p *playSession) Stop() {
p.mu.Lock()
defer p.mu.Unlock()
if p.gstPlayer != nil {
p.gstPlayer.Stop()
}
p.stopLocked()
}
Step 4: Connect GStreamer Frames to Fyne UI
The key challenge: GStreamer produces RGBA frames, Fyne needs to display them.
In playSession:
// Start frame display loop
go func() {
ticker := time.NewTicker(time.Second / time.Duration(fps))
defer ticker.Stop()
for {
select {
case <-p.stop:
return
case <-ticker.C:
if p.gstPlayer != nil {
frame, err := p.gstPlayer.GetFrameImage()
if err == nil && frame != nil {
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
p.img.Image = frame
p.img.Refresh()
}, false)
}
}
}
}
}()
Module Integration Points
Modules Using Player:
| Module | Usage | Status | Notes |
|---|---|---|---|
| Player | Main playback | ✅ Ready | Uses Controller interface |
| Convert | Preview pane | ✅ Ready | Uses playSession |
| Trim | Not implemented | ⏳ Waiting | Blocked by player |
| Filters | Not implemented | ⏳ Waiting | Blocked by player |
After GStreamer Integration:
- ✅ Player module: Works with GStreamerController
- ✅ Convert preview: Works with GStreamerPlayer directly
- ✅ Trim module: Can be implemented (player stable)
- ✅ Filters module: Can be implemented (player stable)
Build Order
- Install GStreamer (user runs command)
- Create
controller_gstreamer.go - Update
playSessioninmain.go - Build with
./scripts/build.sh - Test Player module
- Test Convert preview
- Verify no crashes
Testing Checklist
Player Module Tests:
- Load video file
- Play button works
- Pause button works
- Seek bar works
- Volume control works
- Frame stepping works (if implemented)
Convert Preview Tests:
- Load video in Convert module
- Preview pane shows video
- Playback works in preview
- Seek works in preview
- Preview updates when converting
Rollback If Needed
If GStreamer integration has issues:
# Revert controller
git checkout HEAD -- internal/player/controller_gstreamer.go
# Revert playSession changes
git checkout HEAD -- main.go
# Rebuild without GStreamer
GOFLAGS="" ./scripts/build.sh
Success Criteria
Phase 2 is complete when:
- ✅ GStreamer installed on system
- ✅ VideoTools builds with
-tags gstreamer - ✅ Player module loads and plays videos
- ✅ Convert preview shows video frames
- ✅ No crashes during basic playback
- ✅ Both systems use GStreamerPlayer backend
Estimated Time: 1-2 hours (mostly testing)