VideoTools/internal/player/unified_player_adapter.go

393 lines
8.1 KiB
Go

package player
import (
"image"
"image/color"
"sync"
"time"
"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 {
// Core UnifiedPlayer
player *UnifiedPlayer
// 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 UnifiedPlayer 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: false,
AudioOutput: "auto",
VideoOutput: "rgb24",
CacheEnabled: true,
CacheSize: 64 * 1024 * 1024, // 64MB
LogLevel: 3, // Debug
}
adapter.player = NewUnifiedPlayer(config)
// Set up callbacks for progress and frame updates
adapter.player.SetTimeCallback(func(d time.Duration) {
seconds := d.Seconds()
adapter.current = seconds
if adapter.prog != nil {
adapter.prog(seconds)
}
})
adapter.player.SetFrameCallback(func(frame int64) {
adapter.frameN = int(frame)
if adapter.frameFunc != nil {
adapter.frameFunc(int(frame))
}
})
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 {
// Get current time from UnifiedPlayer
currentTime := p.player.GetCurrentTime()
p.current = currentTime.Seconds()
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
frameDuration := time.Second / time.Duration(p.fps)
ticker := time.NewTicker(frameDuration)
defer ticker.Stop()
for {
select {
case <-p.stop:
return
case <-ticker.C:
p.mu.Lock()
if !p.paused && p.player != nil {
// Get frame from UnifiedPlayer
frame, err := p.player.GetFrameImage()
if err == nil && frame != nil {
// Update the Fyne canvas image
p.img.Image = frame
p.img.Refresh()
}
}
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()
}