VideoTools/docs/PHASE2_INTEGRATION_PLAN.md
Stu Leak 501e2622dc feat(player): integrate GStreamer for stable video playback
- 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>
2026-01-09 03:43:34 -05:00

322 lines
6.9 KiB
Markdown

# Phase 2: GStreamer Integration Plan
## Current State Analysis
### Two Player Systems Found:
1. **Player Module** (`main.go:6609`)
- Uses: `player.Controller` interface
- Implementation: `ffplayController` (uses external ffplay window)
- File: `internal/player/controller_linux.go`
- Problem: External window, not embedded in Fyne UI
2. **Convert Preview** (`main.go:11132`)
- Uses: `playSession` struct
- Implementation: `UnifiedPlayerAdapter` (broken FFmpeg pipes)
- File: Defined in `main.go`
- Problem: Uses the UnifiedPlayer we're deleting
## 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
//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:**
```go
type playSession struct {
// ...
unifiedAdapter *player.UnifiedPlayerAdapter
}
func newPlaySession(...) *playSession {
unifiedAdapter := player.NewUnifiedPlayerAdapter(...)
return &playSession{
unifiedAdapter: unifiedAdapter,
// ...
}
}
```
**AFTER:**
```go
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`:
```go
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:**
```go
// 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
1. Install GStreamer (user runs command)
2. Create `controller_gstreamer.go`
3. Update `playSession` in `main.go`
4. Build with `./scripts/build.sh`
5. Test Player module
6. Test Convert preview
7. 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:
```bash
# 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)