Major improvements to UnifiedPlayer: 1. GetFrameImage() now works when paused for responsive UI updates 2. Play() method properly starts FFmpeg process 3. Frame display loop runs continuously for smooth video display 4. Disabled audio temporarily to fix video playback fundamentals 5. Simplified FFmpeg command to focus on video stream only Player now: - Generates video frames correctly - Shows video when paused - Has responsive progress tracking - Starts playback properly Next steps: Re-enable audio playback once video is stable
392 lines
8.0 KiB
Go
392 lines
8.0 KiB
Go
package player
|
|
|
|
import (
|
|
"image"
|
|
"image/color"
|
|
"sync"
|
|
"time"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/canvas"
|
|
)
|
|
|
|
// UnifiedPlayerAdapter wraps UnifiedPlayer to provide playSession interface compatibility
|
|
// This allows seamless replacement of the dual-process player with UnifiedPlayer
|
|
type UnifiedPlayerAdapter struct {
|
|
// Frame-capable player backend
|
|
player framePlayer
|
|
|
|
// Interface compatibility fields (from playSession)
|
|
path string
|
|
fps float64
|
|
width int
|
|
height int
|
|
targetW int
|
|
targetH int
|
|
volume float64
|
|
muted bool
|
|
paused bool
|
|
current float64
|
|
stop chan struct{}
|
|
done chan struct{}
|
|
prog func(float64)
|
|
frameFunc func(int) // Callback for frame number updates
|
|
img *canvas.Image
|
|
mu sync.Mutex
|
|
frameN int
|
|
duration float64 // Total duration in seconds
|
|
startTime time.Time
|
|
|
|
// Adapter-specific state
|
|
lastUpdateTime time.Time
|
|
updateTicker *time.Ticker
|
|
}
|
|
|
|
// NewUnifiedPlayerAdapter creates a new adapter that wraps UnifiedPlayer
|
|
func NewUnifiedPlayerAdapter(path string, width, height int, fps, duration float64, targetW, targetH int, prog func(float64), frameFunc func(int), img *canvas.Image) *UnifiedPlayerAdapter {
|
|
adapter := &UnifiedPlayerAdapter{
|
|
path: path,
|
|
fps: fps,
|
|
width: width,
|
|
height: height,
|
|
targetW: targetW,
|
|
targetH: targetH,
|
|
volume: 100.0,
|
|
muted: false,
|
|
paused: true,
|
|
current: 0.0,
|
|
stop: make(chan struct{}),
|
|
done: make(chan struct{}),
|
|
prog: prog,
|
|
frameFunc: frameFunc,
|
|
img: img,
|
|
duration: duration,
|
|
startTime: time.Now(),
|
|
}
|
|
|
|
// Create frame-capable player with proper configuration
|
|
config := Config{
|
|
Backend: BackendAuto, // Use auto for UnifiedPlayer
|
|
WindowX: 0,
|
|
WindowY: 0,
|
|
WindowWidth: targetW,
|
|
WindowHeight: targetH,
|
|
Volume: 1.0, // Full volume
|
|
Muted: false,
|
|
AutoPlay: false,
|
|
HardwareAccel: false,
|
|
PreviewMode: true,
|
|
AudioOutput: "auto",
|
|
VideoOutput: "rgb24",
|
|
CacheEnabled: true,
|
|
CacheSize: 64 * 1024 * 1024, // 64MB
|
|
LogLevel: 3, // Debug
|
|
}
|
|
|
|
playerBackend, err := newFramePlayer(config)
|
|
if err == nil {
|
|
adapter.player = playerBackend
|
|
}
|
|
|
|
return adapter
|
|
}
|
|
|
|
// Play starts or resumes playback
|
|
func (p *UnifiedPlayerAdapter) Play() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
if p.player == nil {
|
|
return
|
|
}
|
|
|
|
if p.paused {
|
|
// Load video if not already loaded
|
|
if p.current == 0 {
|
|
err := p.player.Load(p.path, 0)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Start playback in UnifiedPlayer
|
|
if err := p.player.Play(); err != nil {
|
|
return
|
|
}
|
|
|
|
p.paused = false
|
|
p.startTime = time.Now().Add(-time.Duration(p.current * float64(time.Second)))
|
|
p.startUpdateLoop()
|
|
p.startFrameDisplayLoop()
|
|
}
|
|
}
|
|
|
|
// Pause pauses playback
|
|
func (p *UnifiedPlayerAdapter) Pause() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
if p.player != nil {
|
|
p.player.Pause()
|
|
}
|
|
p.paused = true
|
|
p.stopUpdateLoop()
|
|
}
|
|
|
|
// Seek seeks to the specified time offset
|
|
func (p *UnifiedPlayerAdapter) Seek(offset float64) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
if offset < 0 {
|
|
offset = 0
|
|
}
|
|
if offset > p.duration {
|
|
offset = p.duration
|
|
}
|
|
|
|
paused := p.paused
|
|
p.current = offset
|
|
p.frameN = int(offset * p.fps)
|
|
|
|
// Seek in UnifiedPlayer
|
|
if p.player != nil {
|
|
err := p.player.SeekToTime(time.Duration(offset * float64(time.Second)))
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
p.paused = paused
|
|
if p.prog != nil {
|
|
p.prog(p.current)
|
|
}
|
|
if p.frameFunc != nil {
|
|
p.frameFunc(p.frameN)
|
|
}
|
|
}
|
|
|
|
// StepFrame moves forward or backward by a specific number of frames
|
|
func (p *UnifiedPlayerAdapter) StepFrame(delta int) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
if p.fps <= 0 {
|
|
return
|
|
}
|
|
|
|
// Calculate current frame from time position
|
|
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
|
|
}
|
|
|
|
// Convert to time offset
|
|
offset := float64(targetFrame) / p.fps
|
|
|
|
// Seek to the new position
|
|
if p.player != nil {
|
|
err := p.player.SeekToFrame(int64(targetFrame))
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
p.current = offset
|
|
p.frameN = targetFrame
|
|
p.paused = true // Auto-pause when frame stepping
|
|
|
|
if p.prog != nil {
|
|
p.prog(p.current)
|
|
}
|
|
if p.frameFunc != nil {
|
|
p.frameFunc(p.frameN)
|
|
}
|
|
}
|
|
|
|
// GetCurrentFrame returns the current frame number
|
|
func (p *UnifiedPlayerAdapter) GetCurrentFrame() int {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return p.frameN
|
|
}
|
|
|
|
// SetVolume sets the audio volume (0-100)
|
|
func (p *UnifiedPlayerAdapter) SetVolume(v float64) {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
p.volume = v
|
|
if p.player != nil {
|
|
// Convert 0-100 to 0.0-1.0 range
|
|
volumeLevel := v / 100.0
|
|
err := p.player.SetVolume(volumeLevel)
|
|
if err != nil {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Stop stops playback and cleans up resources
|
|
func (p *UnifiedPlayerAdapter) Stop() {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
p.stopUpdateLoop()
|
|
|
|
if p.player != nil {
|
|
p.player.Close()
|
|
p.player = nil
|
|
}
|
|
|
|
// Close channels to signal completion
|
|
select {
|
|
case <-p.stop:
|
|
default:
|
|
close(p.stop)
|
|
}
|
|
}
|
|
|
|
// startUpdateLoop starts the update loop for progress tracking
|
|
func (p *UnifiedPlayerAdapter) startUpdateLoop() {
|
|
if p.updateTicker != nil {
|
|
return // Already running
|
|
}
|
|
|
|
// Update progress based on frame rate (30fps updates)
|
|
interval := time.Second / 30
|
|
p.updateTicker = time.NewTicker(interval)
|
|
|
|
go func() {
|
|
defer p.updateTicker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-p.stop:
|
|
return
|
|
case <-p.updateTicker.C:
|
|
p.mu.Lock()
|
|
if !p.paused && p.player != nil {
|
|
// Drive timeline locally to avoid fighting the frame reader.
|
|
elapsed := time.Since(p.startTime).Seconds()
|
|
if elapsed < 0 {
|
|
elapsed = 0
|
|
}
|
|
if p.duration > 0 && elapsed > p.duration {
|
|
elapsed = p.duration
|
|
}
|
|
p.current = elapsed
|
|
p.frameN = int(p.current * p.fps)
|
|
|
|
// Update UI callbacks
|
|
if p.prog != nil {
|
|
p.prog(p.current)
|
|
}
|
|
if p.frameFunc != nil {
|
|
p.frameFunc(p.frameN)
|
|
}
|
|
}
|
|
p.mu.Unlock()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// stopUpdateLoop stops the update loop
|
|
func (p *UnifiedPlayerAdapter) stopUpdateLoop() {
|
|
if p.updateTicker != nil {
|
|
p.updateTicker.Stop()
|
|
p.updateTicker = nil
|
|
}
|
|
}
|
|
|
|
// startFrameDisplayLoop starts the loop that reads frames and displays them
|
|
func (p *UnifiedPlayerAdapter) startFrameDisplayLoop() {
|
|
if p.player == nil || p.img == nil {
|
|
return
|
|
}
|
|
|
|
go func() {
|
|
// Display at frame rate
|
|
fps := p.fps
|
|
if fps <= 0 {
|
|
fps = 24
|
|
}
|
|
frameDuration := time.Second / time.Duration(fps)
|
|
ticker := time.NewTicker(frameDuration)
|
|
defer ticker.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-p.stop:
|
|
return
|
|
case <-ticker.C:
|
|
p.mu.Lock()
|
|
// Always try to get frames, even when paused for UI updates
|
|
if p.player != nil {
|
|
frame, err := p.player.GetFrameImage()
|
|
if err == nil && frame != nil {
|
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
p.img.Image = frame
|
|
p.img.Refresh()
|
|
}, false)
|
|
}
|
|
}
|
|
p.mu.Unlock()
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// GetVideoFrame returns the current video frame for display
|
|
func (p *UnifiedPlayerAdapter) GetVideoFrame() *image.RGBA {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
|
|
if p.player == nil {
|
|
return nil
|
|
}
|
|
|
|
// Get real frame from UnifiedPlayer
|
|
frame, err := p.player.GetFrameImage()
|
|
if err != nil || frame == nil {
|
|
// Return black frame on error
|
|
rect := image.Rect(0, 0, p.targetW, p.targetH)
|
|
blackFrame := image.NewRGBA(rect)
|
|
for y := 0; y < p.targetH; y++ {
|
|
for x := 0; x < p.targetW; x++ {
|
|
blackFrame.SetRGBA(x, y, color.RGBA{0, 0, 0, 255})
|
|
}
|
|
}
|
|
return blackFrame
|
|
}
|
|
|
|
return frame
|
|
}
|
|
|
|
// IsPlaying returns whether playback is active
|
|
func (p *UnifiedPlayerAdapter) IsPlaying() bool {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return !p.paused
|
|
}
|
|
|
|
// GetDuration returns the total duration in seconds
|
|
func (p *UnifiedPlayerAdapter) GetDuration() float64 {
|
|
p.mu.Lock()
|
|
defer p.mu.Unlock()
|
|
return p.duration
|
|
}
|
|
|
|
// Close closes the adapter and cleans up resources
|
|
func (p *UnifiedPlayerAdapter) Close() {
|
|
p.Stop()
|
|
}
|