refactor(player): remove legacy UnifiedPlayer, GStreamer now mandatory
- Removed unified_ffmpeg_player.go and unified_player_adapter.go - Updated frame_player_gstreamer.go to remove UnifiedPlayer fallback - Updated frame_player_default.go to return error when GStreamer unavailable - Updated PROJECT_STATUS.md: Player module now fully implemented with GStreamer - Removed critical issues note about Player A/V sync problems GStreamer is now the sole playback backend, providing stable A/V synchronization and frame-accurate seeking. The broken FFmpeg pipe-based UnifiedPlayer has been completely removed.
This commit is contained in:
parent
e1fecbba06
commit
749bdc6bf9
|
|
@ -8,7 +8,7 @@ VideoTools is a modular application for video processing. While many features ar
|
||||||
|
|
||||||
## 🚨 Critical Known Issues
|
## 🚨 Critical Known Issues
|
||||||
|
|
||||||
* **Player Module:** The core player has fundamental A/V synchronization and frame-accurate seeking issues. This blocks the development of several planned features that depend on it (e.g., Trim, Filters). A major rework of the player is a critical priority.
|
*None currently*
|
||||||
|
|
||||||
## Module Implementation Status
|
## Module Implementation Status
|
||||||
|
|
||||||
|
|
@ -16,11 +16,11 @@ VideoTools is a modular application for video processing. While many features ar
|
||||||
|
|
||||||
| Module | Status | Notes |
|
| Module | Status | Notes |
|
||||||
| :------ | :-------------------------- | :--------------------------------------------------------------------- |
|
| :------ | :-------------------------- | :--------------------------------------------------------------------- |
|
||||||
| Player | 🟡 **Partial / Buggy** | Core playback works, but critical bugs block further development. |
|
| Player | ✅ **Implemented** | GStreamer-based player with stable A/V sync and frame-accurate seeking. |
|
||||||
| Convert | ✅ **Implemented** | Fully implemented with DVD encoding and professional validation. |
|
| Convert | ✅ **Implemented** | Fully implemented with DVD encoding and professional validation. |
|
||||||
| Merge | 🔄 **Planned** | Planned for a future release. |
|
| Merge | 🔄 **Planned** | Planned for a future release. |
|
||||||
| Trim | 🔄 **Planned** | Planned. Depends on Player module fixes. |
|
| Trim | 🔄 **Planned** | Planned for a future release. |
|
||||||
| Filters | 🔄 **Planned** | Planned. Depends on Player module fixes. |
|
| Filters | 🔄 **Planned** | Planned for a future release. |
|
||||||
| Upscale | 🟡 **Partial** | AI-based upscaling (Real-ESRGAN) is integrated. |
|
| Upscale | 🟡 **Partial** | AI-based upscaling (Real-ESRGAN) is integrated. |
|
||||||
| Audio | 🔄 **Planned** | Planned for a future release. |
|
| Audio | 🔄 **Planned** | Planned for a future release. |
|
||||||
| Thumb | 🔄 **Planned** | Planned for a future release. |
|
| Thumb | 🔄 **Planned** | Planned for a future release. |
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
package player
|
package player
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
func newFramePlayer(config Config) (framePlayer, error) {
|
func newFramePlayer(config Config) (framePlayer, error) {
|
||||||
return NewUnifiedPlayer(config), nil
|
return nil, errors.New("GStreamer is required but not available - build with -tags gstreamer")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,5 @@
|
||||||
package player
|
package player
|
||||||
|
|
||||||
func newFramePlayer(config Config) (framePlayer, error) {
|
func newFramePlayer(config Config) (framePlayer, error) {
|
||||||
if gstPlayer, err := NewGStreamerPlayer(config); err == nil {
|
return NewGStreamerPlayer(config)
|
||||||
return gstPlayer, nil
|
|
||||||
}
|
|
||||||
return NewUnifiedPlayer(config), nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,887 +0,0 @@
|
||||||
package player
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"io"
|
|
||||||
"os/exec"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/ebitengine/oto/v3"
|
|
||||||
|
|
||||||
"git.leaktechnologies.dev/stu/VideoTools/internal/logging"
|
|
||||||
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
|
|
||||||
)
|
|
||||||
|
|
||||||
// UnifiedPlayer implements rock-solid video playback with proper A/V synchronization
|
|
||||||
// and frame-accurate seeking using a single FFmpeg process
|
|
||||||
type UnifiedPlayer struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
|
|
||||||
// FFmpeg process
|
|
||||||
cmd *exec.Cmd
|
|
||||||
stdin *bufio.Writer
|
|
||||||
stdout *bufio.Reader
|
|
||||||
stderr *bufio.Reader
|
|
||||||
|
|
||||||
// Video output pipes
|
|
||||||
videoPipeReader *io.PipeReader
|
|
||||||
videoPipeWriter *io.PipeWriter
|
|
||||||
audioPipeReader *io.PipeReader
|
|
||||||
audioPipeWriter *io.PipeWriter
|
|
||||||
|
|
||||||
// Audio output
|
|
||||||
audioContext *oto.Context
|
|
||||||
audioPlayer *oto.Player
|
|
||||||
|
|
||||||
// State tracking
|
|
||||||
currentPath string
|
|
||||||
currentTime time.Duration
|
|
||||||
currentFrame int64
|
|
||||||
duration time.Duration
|
|
||||||
frameRate float64
|
|
||||||
state PlayerState
|
|
||||||
volume float64
|
|
||||||
speed float64
|
|
||||||
muted bool
|
|
||||||
fullscreen bool
|
|
||||||
previewMode bool
|
|
||||||
paused bool // Playback paused state
|
|
||||||
|
|
||||||
// Video info
|
|
||||||
videoInfo *VideoInfo
|
|
||||||
|
|
||||||
// Synchronization
|
|
||||||
syncClock time.Time
|
|
||||||
videoPTS int64
|
|
||||||
audioPTS int64
|
|
||||||
ptsOffset int64
|
|
||||||
|
|
||||||
// Buffer management
|
|
||||||
frameBuffer *sync.Pool
|
|
||||||
videoBuffer []byte
|
|
||||||
audioBuffer []byte
|
|
||||||
audioBufferSize int
|
|
||||||
|
|
||||||
// Window state
|
|
||||||
windowX, windowY int
|
|
||||||
windowW, windowH int
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
timeCallback func(time.Duration)
|
|
||||||
frameCallback func(int64)
|
|
||||||
stateCallback func(PlayerState)
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
config Config
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewUnifiedPlayer creates a new unified player with proper A/V synchronization
|
|
||||||
func NewUnifiedPlayer(config Config) *UnifiedPlayer {
|
|
||||||
player := &UnifiedPlayer{
|
|
||||||
config: config,
|
|
||||||
frameBuffer: &sync.Pool{
|
|
||||||
New: func() interface{} {
|
|
||||||
return &image.RGBA{
|
|
||||||
Pix: make([]uint8, 0),
|
|
||||||
Stride: 0,
|
|
||||||
Rect: image.Rect(0, 0, 0, 0),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
audioBufferSize: 32768, // 170ms at 48kHz for smooth playback
|
|
||||||
}
|
|
||||||
player.previewMode = config.PreviewMode
|
|
||||||
if config.WindowWidth > 0 {
|
|
||||||
player.windowW = config.WindowWidth
|
|
||||||
}
|
|
||||||
if config.WindowHeight > 0 {
|
|
||||||
player.windowH = config.WindowHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
player.ctx = ctx
|
|
||||||
player.cancel = cancel
|
|
||||||
|
|
||||||
return player
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load loads a video file and initializes playback
|
|
||||||
func (p *UnifiedPlayer) Load(path string, offset time.Duration) error {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
// Special handling for our test file
|
|
||||||
if strings.Contains(path, "bbb_sunflower_2160p_60fps_normal.mp4") {
|
|
||||||
logging.Debug(logging.CatPlayer, "Loading test video: Big Buck Bunny (%s)", path)
|
|
||||||
}
|
|
||||||
|
|
||||||
p.currentPath = path
|
|
||||||
p.state = StateLoading
|
|
||||||
|
|
||||||
// Add panic recovery for crash safety
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
logging.Crash(logging.CatPlayer, "Panic in Load(): %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Create pipes for FFmpeg communication
|
|
||||||
p.videoPipeReader, p.videoPipeWriter = io.Pipe()
|
|
||||||
if !p.previewMode {
|
|
||||||
p.audioPipeReader, p.audioPipeWriter = io.Pipe()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build FFmpeg command - focus on video first
|
|
||||||
args := []string{
|
|
||||||
"-hide_banner", "-loglevel", "error",
|
|
||||||
"-ss", fmt.Sprintf("%.3f", offset.Seconds()),
|
|
||||||
"-i", path,
|
|
||||||
"-map", "0:v:0",
|
|
||||||
"-f", "rawvideo",
|
|
||||||
"-pix_fmt", "rgb24",
|
|
||||||
"-r", "24",
|
|
||||||
"pipe:1",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Disable audio for now to get basic video working
|
|
||||||
args = append(args, "-an")
|
|
||||||
|
|
||||||
// Add hardware acceleration if available
|
|
||||||
if p.config.HardwareAccel {
|
|
||||||
if args = p.addHardwareAcceleration(args); args != nil {
|
|
||||||
logging.Debug(logging.CatPlayer, "Hardware acceleration enabled: %v", args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !p.previewMode {
|
|
||||||
// Initialize audio context for playback
|
|
||||||
sampleRate := 48000
|
|
||||||
channels := 2
|
|
||||||
|
|
||||||
ctx, ready, err := oto.NewContext(&oto.NewContextOptions{
|
|
||||||
SampleRate: sampleRate,
|
|
||||||
ChannelCount: channels,
|
|
||||||
Format: oto.FormatSignedInt16LE,
|
|
||||||
BufferSize: 4096, // 85ms chunks for smooth playback
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
logging.Error(logging.CatPlayer, "Failed to create audio context: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if ready != nil {
|
|
||||||
<-ready
|
|
||||||
}
|
|
||||||
|
|
||||||
p.audioContext = ctx
|
|
||||||
logging.Info(logging.CatPlayer, "Audio context initialized successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start FFmpeg process for unified A/V output
|
|
||||||
if err := p.startVideoProcess(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start audio stream processing
|
|
||||||
if !p.previewMode {
|
|
||||||
go p.readAudioStream()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SeekToTime seeks to a specific time without restarting processes
|
|
||||||
func (p *UnifiedPlayer) SeekToTime(offset time.Duration) error {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
if offset < 0 {
|
|
||||||
offset = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
seekTime := offset.Seconds()
|
|
||||||
logging.Debug(logging.CatPlayer, "Seeking to time: %.3f seconds", seekTime)
|
|
||||||
p.writeStringToStdin(fmt.Sprintf("seek %.3f\n", seekTime))
|
|
||||||
|
|
||||||
p.currentTime = offset
|
|
||||||
if p.frameRate > 0 {
|
|
||||||
p.currentFrame = int64(seekTime * p.frameRate)
|
|
||||||
}
|
|
||||||
p.syncClock = time.Now()
|
|
||||||
|
|
||||||
if p.timeCallback != nil {
|
|
||||||
p.timeCallback(offset)
|
|
||||||
}
|
|
||||||
if p.frameCallback != nil {
|
|
||||||
p.frameCallback(p.currentFrame)
|
|
||||||
}
|
|
||||||
|
|
||||||
logging.Debug(logging.CatPlayer, "Seek completed to %.3f seconds", seekTime)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SeekToFrame seeks to a specific frame without restarting processes
|
|
||||||
func (p *UnifiedPlayer) SeekToFrame(frame int64) error {
|
|
||||||
if p.frameRate <= 0 {
|
|
||||||
return fmt.Errorf("invalid frame rate: %f", p.frameRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert frame number to time
|
|
||||||
frameTime := time.Duration(float64(frame) * float64(time.Second) / p.frameRate)
|
|
||||||
return p.SeekToTime(frameTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentTime returns the current playback time
|
|
||||||
func (p *UnifiedPlayer) GetCurrentTime() time.Duration {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
return p.currentTime
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCurrentFrame returns the current frame number
|
|
||||||
func (p *UnifiedPlayer) GetCurrentFrame() int64 {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
if p.frameRate > 0 {
|
|
||||||
return int64(p.currentTime.Seconds() * p.frameRate)
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDuration returns the total video duration
|
|
||||||
func (p *UnifiedPlayer) GetDuration() time.Duration {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
return p.duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFrameImage reads and returns the current video frame as an RGBA image
|
|
||||||
// This is the main method for getting video frames to display in the UI
|
|
||||||
func (p *UnifiedPlayer) GetFrameImage() (*image.RGBA, error) {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
// Allow frame reading even when paused for UI updates
|
|
||||||
if p.state == StateStopped {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.readVideoFrame()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetFrameRate returns the video frame rate
|
|
||||||
func (p *UnifiedPlayer) GetFrameRate() float64 {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
return p.frameRate
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetVideoInfo returns video metadata
|
|
||||||
func (p *UnifiedPlayer) GetVideoInfo() *VideoInfo {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
if p.videoInfo == nil {
|
|
||||||
return &VideoInfo{}
|
|
||||||
}
|
|
||||||
return p.videoInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWindow sets the window position and size
|
|
||||||
func (p *UnifiedPlayer) SetWindow(x, y, w, h int) {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
p.windowX, p.windowY, p.windowW, p.windowH = x, y, w, h
|
|
||||||
|
|
||||||
// Send window command to FFmpeg
|
|
||||||
p.writeStringToStdin(fmt.Sprintf("window %d %d %d %d\n", x, y, w, h))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFullScreen toggles fullscreen mode
|
|
||||||
func (p *UnifiedPlayer) SetFullScreen(fullscreen bool) error {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
p.fullscreen = fullscreen
|
|
||||||
|
|
||||||
// Send fullscreen command to FFmpeg
|
|
||||||
var cmd string
|
|
||||||
if fullscreen {
|
|
||||||
cmd = "fullscreen"
|
|
||||||
} else {
|
|
||||||
cmd = "windowed"
|
|
||||||
}
|
|
||||||
|
|
||||||
p.writeStringToStdin(fmt.Sprintf("%s\n", cmd))
|
|
||||||
|
|
||||||
logging.Debug(logging.CatPlayer, "Fullscreen set to: %v", fullscreen)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWindowSize returns current window dimensions
|
|
||||||
func (p *UnifiedPlayer) GetWindowSize() (x, y, w, h int) {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
return p.windowX, p.windowY, p.windowW, p.windowH
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetVolume sets the audio volume (0.0-1.0)
|
|
||||||
func (p *UnifiedPlayer) SetVolume(level float64) error {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
// Clamp volume to valid range
|
|
||||||
if level < 0 {
|
|
||||||
level = 0
|
|
||||||
} else if level > 1 {
|
|
||||||
level = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
p.volume = level
|
|
||||||
|
|
||||||
// Send volume command to FFmpeg
|
|
||||||
p.writeStringToStdin(fmt.Sprintf("volume %.3f\n", level))
|
|
||||||
|
|
||||||
logging.Debug(logging.CatPlayer, "Volume set to: %.3f", level)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetVolume returns current volume level
|
|
||||||
func (p *UnifiedPlayer) GetVolume() float64 {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
return p.volume
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMuted sets the mute state
|
|
||||||
func (p *UnifiedPlayer) SetMuted(muted bool) {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
p.muted = muted
|
|
||||||
|
|
||||||
// Send mute command to FFmpeg
|
|
||||||
var cmd string
|
|
||||||
if muted {
|
|
||||||
cmd = "mute"
|
|
||||||
} else {
|
|
||||||
cmd = "unmute"
|
|
||||||
}
|
|
||||||
|
|
||||||
p.writeStringToStdin(fmt.Sprintf("%s\n", cmd))
|
|
||||||
|
|
||||||
logging.Debug(logging.CatPlayer, "Mute set to: %v", muted)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsMuted returns current mute state
|
|
||||||
func (p *UnifiedPlayer) IsMuted() bool {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
return p.muted
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSpeed sets playback speed
|
|
||||||
func (p *UnifiedPlayer) SetSpeed(speed float64) error {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
p.speed = speed
|
|
||||||
|
|
||||||
// Send speed command to FFmpeg
|
|
||||||
p.writeStringToStdin(fmt.Sprintf("speed %.2f\n", speed))
|
|
||||||
|
|
||||||
logging.Debug(logging.CatPlayer, "Speed set to: %.2f", speed)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSpeed returns current playback speed
|
|
||||||
func (p *UnifiedPlayer) GetSpeed() float64 {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
return p.speed
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetTimeCallback sets the time update callback
|
|
||||||
func (p *UnifiedPlayer) SetTimeCallback(callback func(time.Duration)) {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
p.timeCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFrameCallback sets the frame update callback
|
|
||||||
func (p *UnifiedPlayer) SetFrameCallback(callback func(int64)) {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
p.frameCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetStateCallback sets the state change callback
|
|
||||||
func (p *UnifiedPlayer) SetStateCallback(callback func(PlayerState)) {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
p.stateCallback = callback
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnablePreviewMode enables or disables preview mode
|
|
||||||
func (p *UnifiedPlayer) EnablePreviewMode(enabled bool) {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
p.previewMode = enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsPreviewMode returns current preview mode state
|
|
||||||
func (p *UnifiedPlayer) IsPreviewMode() bool {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
return p.previewMode
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close shuts down the player and cleans up resources
|
|
||||||
func (p *UnifiedPlayer) Close() {
|
|
||||||
p.Stop()
|
|
||||||
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
p.frameBuffer = nil
|
|
||||||
p.audioBuffer = nil
|
|
||||||
|
|
||||||
// Close audio context and player
|
|
||||||
if p.audioContext != nil {
|
|
||||||
p.audioContext = nil
|
|
||||||
}
|
|
||||||
if p.audioPlayer != nil {
|
|
||||||
p.audioPlayer.Close()
|
|
||||||
p.audioPlayer = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop halts playback and tears down the FFmpeg process.
|
|
||||||
func (p *UnifiedPlayer) Stop() error {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
if p.cancel != nil {
|
|
||||||
p.cancel()
|
|
||||||
}
|
|
||||||
if p.cmd != nil && p.cmd.Process != nil {
|
|
||||||
_ = p.cmd.Process.Kill()
|
|
||||||
}
|
|
||||||
p.state = StateStopped
|
|
||||||
p.paused = false
|
|
||||||
if p.stateCallback != nil {
|
|
||||||
p.stateCallback(p.state)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Play starts or resumes video playback
|
|
||||||
func (p *UnifiedPlayer) Play() error {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
// Add panic recovery for crash safety
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
logging.Crash(logging.CatPlayer, "Panic in Play(): %v", r)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if p.state == StateStopped {
|
|
||||||
// Need to load first
|
|
||||||
return fmt.Errorf("no video loaded")
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.state == StateLoading {
|
|
||||||
// Still loading, wait
|
|
||||||
return fmt.Errorf("video still loading")
|
|
||||||
}
|
|
||||||
|
|
||||||
p.paused = false
|
|
||||||
p.state = StatePlaying
|
|
||||||
p.syncClock = time.Now()
|
|
||||||
|
|
||||||
logging.Debug(logging.CatPlayer, "UnifiedPlayer: Play() called, state=%v", p.state)
|
|
||||||
|
|
||||||
// Start FFmpeg process if not already running
|
|
||||||
if p.cmd == nil || p.cmd.Process == nil {
|
|
||||||
if err := p.startVideoProcess(); err != nil {
|
|
||||||
p.state = StateStopped
|
|
||||||
return fmt.Errorf("failed to start video process: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.stateCallback != nil {
|
|
||||||
p.stateCallback(p.state)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pause pauses video playback
|
|
||||||
func (p *UnifiedPlayer) Pause() error {
|
|
||||||
p.mu.Lock()
|
|
||||||
defer p.mu.Unlock()
|
|
||||||
|
|
||||||
if p.state != StatePlaying {
|
|
||||||
return nil // Already paused or stopped
|
|
||||||
}
|
|
||||||
|
|
||||||
p.paused = true
|
|
||||||
p.state = StatePaused
|
|
||||||
|
|
||||||
logging.Debug(logging.CatPlayer, "UnifiedPlayer: Pause() called, state=%v", p.state)
|
|
||||||
|
|
||||||
if p.stateCallback != nil {
|
|
||||||
p.stateCallback(p.state)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsPaused returns whether playback is paused
|
|
||||||
func (p *UnifiedPlayer) IsPaused() bool {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
return p.paused
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsPlaying returns whether playback is active
|
|
||||||
func (p *UnifiedPlayer) IsPlaying() bool {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
return p.state == StatePlaying && !p.paused
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper methods
|
|
||||||
|
|
||||||
// startVideoProcess starts the video processing goroutine and FFmpeg process
|
|
||||||
func (p *UnifiedPlayer) startVideoProcess() error {
|
|
||||||
// Build FFmpeg command for unified A/V output
|
|
||||||
args := []string{
|
|
||||||
"-hide_banner", "-loglevel", "error",
|
|
||||||
"-ss", fmt.Sprintf("%.3f", p.currentTime.Seconds()),
|
|
||||||
"-i", p.currentPath,
|
|
||||||
}
|
|
||||||
if p.previewMode {
|
|
||||||
args = append(args,
|
|
||||||
"-map", "0:v:0",
|
|
||||||
"-an",
|
|
||||||
"-f", "rawvideo",
|
|
||||||
"-pix_fmt", "rgb24",
|
|
||||||
"-r", "24",
|
|
||||||
"pipe:1",
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
args = append(args,
|
|
||||||
// Video stream to pipe 4
|
|
||||||
"-map", "0:v:0",
|
|
||||||
"-f", "rawvideo",
|
|
||||||
"-pix_fmt", "rgb24",
|
|
||||||
"-r", "24", // We'll detect actual framerate
|
|
||||||
"pipe:4",
|
|
||||||
// Audio stream to pipe 5
|
|
||||||
"-map", "0:a:0",
|
|
||||||
"-ac", "2",
|
|
||||||
"-ar", "48000",
|
|
||||||
"-f", "s16le",
|
|
||||||
"pipe:5",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add hardware acceleration if available
|
|
||||||
if p.config.HardwareAccel {
|
|
||||||
if args = p.addHardwareAcceleration(args); args != nil {
|
|
||||||
logging.Debug(logging.CatPlayer, "Hardware acceleration enabled: %v", args)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create FFmpeg command
|
|
||||||
cmd := utils.CreateCommandRaw(utils.GetFFmpegPath(), args...)
|
|
||||||
cmd.Stdin = nil
|
|
||||||
cmd.Stdout = p.videoPipeWriter
|
|
||||||
cmd.Stderr = nil // We'll handle errors through logging
|
|
||||||
|
|
||||||
// Start FFmpeg process
|
|
||||||
if err := cmd.Start(); err != nil {
|
|
||||||
logging.Error(logging.CatPlayer, "Failed to start FFmpeg: %v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store command reference
|
|
||||||
p.cmd = cmd
|
|
||||||
|
|
||||||
// Start video frame reading goroutine
|
|
||||||
if !p.previewMode {
|
|
||||||
go func() {
|
|
||||||
rate := p.frameRate
|
|
||||||
if rate <= 0 {
|
|
||||||
rate = 24
|
|
||||||
logging.Debug(logging.CatPlayer, "Frame rate unavailable; defaulting to %.0f fps", rate)
|
|
||||||
}
|
|
||||||
frameDuration := time.Second / time.Duration(rate)
|
|
||||||
frameTime := p.syncClock
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-p.ctx.Done():
|
|
||||||
logging.Debug(logging.CatPlayer, "Video processing goroutine stopped")
|
|
||||||
return
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Read frame from video pipe
|
|
||||||
frame, err := p.readVideoFrame()
|
|
||||||
if err != nil {
|
|
||||||
logging.Error(logging.CatPlayer, "Failed to read video frame: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if frame == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update timing
|
|
||||||
p.currentTime = frameTime.Sub(p.syncClock)
|
|
||||||
frameTime = frameTime.Add(frameDuration)
|
|
||||||
p.syncClock = time.Now()
|
|
||||||
|
|
||||||
// Notify callback
|
|
||||||
if p.frameCallback != nil {
|
|
||||||
p.frameCallback(p.GetCurrentFrame())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sleep until next frame time
|
|
||||||
sleepTime := frameTime.Sub(time.Now())
|
|
||||||
if sleepTime > 0 {
|
|
||||||
time.Sleep(sleepTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readAudioStream reads and processes audio from the audio pipe
|
|
||||||
func (p *UnifiedPlayer) readAudioStream() {
|
|
||||||
// Add panic recovery for crash safety
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
logging.Crash(logging.CatPlayer, "Panic in readAudioStream(): %v", r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if p.audioContext == nil {
|
|
||||||
logging.Error(logging.CatPlayer, "Audio context is not initialized")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
p.mu.Lock()
|
|
||||||
if p.audioPlayer == nil {
|
|
||||||
p.audioPlayer = p.audioContext.NewPlayer(p.audioPipeReader)
|
|
||||||
p.audioPlayer.Play()
|
|
||||||
logging.Info(logging.CatPlayer, "Audio player created successfully")
|
|
||||||
}
|
|
||||||
p.mu.Unlock()
|
|
||||||
|
|
||||||
<-p.ctx.Done()
|
|
||||||
logging.Debug(logging.CatPlayer, "Audio reading goroutine stopped")
|
|
||||||
}
|
|
||||||
|
|
||||||
// readVideoStream reads video frames from the video pipe
|
|
||||||
func (p *UnifiedPlayer) readVideoFrame() (*image.RGBA, error) {
|
|
||||||
// Allow frame reading when paused for UI updates
|
|
||||||
// but don't advance frame counter if paused
|
|
||||||
wasPaused := p.paused
|
|
||||||
|
|
||||||
// Read RGB24 frame data from FFmpeg pipe
|
|
||||||
frameSize := p.windowW * p.windowH * 3 // RGB24 = 3 bytes per pixel
|
|
||||||
if len(p.videoBuffer) != frameSize {
|
|
||||||
p.videoBuffer = make([]byte, frameSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For non-blocking read when paused, use peek
|
|
||||||
if wasPaused {
|
|
||||||
// Return last known frame when paused (create placeholder if none)
|
|
||||||
img := p.frameBuffer.Get().(*image.RGBA)
|
|
||||||
if img.Rect.Dx() != p.windowW || img.Rect.Dy() != p.windowH {
|
|
||||||
img.Rect = image.Rect(0, 0, p.windowW, p.windowH)
|
|
||||||
img.Stride = p.windowW * 4
|
|
||||||
img.Pix = make([]uint8, p.windowW*p.windowH*4)
|
|
||||||
}
|
|
||||||
return img, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read full frame - io.ReadFull ensures we get complete frame
|
|
||||||
n, err := io.ReadFull(p.videoPipeReader, p.videoBuffer)
|
|
||||||
if err != nil {
|
|
||||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
|
||||||
return nil, nil // End of stream
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("video read error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if n != frameSize {
|
|
||||||
return nil, fmt.Errorf("incomplete frame: got %d bytes, expected %d", n, frameSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create RGBA image (Fyne requires RGBA, not RGB), reuse buffer.
|
|
||||||
img := p.frameBuffer.Get().(*image.RGBA)
|
|
||||||
if img.Rect.Dx() != p.windowW || img.Rect.Dy() != p.windowH {
|
|
||||||
img.Rect = image.Rect(0, 0, p.windowW, p.windowH)
|
|
||||||
img.Stride = p.windowW * 4
|
|
||||||
img.Pix = make([]uint8, p.windowW*p.windowH*4)
|
|
||||||
}
|
|
||||||
utils.CopyRGBToRGBA(img.Pix, p.videoBuffer)
|
|
||||||
|
|
||||||
// Update frame counter only when not paused
|
|
||||||
if !wasPaused {
|
|
||||||
p.currentFrame++
|
|
||||||
// Notify time callback
|
|
||||||
if p.timeCallback != nil {
|
|
||||||
p.timeCallback(p.currentTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return img, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// detectVideoProperties analyzes the video to determine properties
|
|
||||||
func (p *UnifiedPlayer) detectVideoProperties() error {
|
|
||||||
// Use ffprobe to get video information
|
|
||||||
cmd := exec.Command(utils.GetFFprobePath(),
|
|
||||||
"-v", "error",
|
|
||||||
"-select_streams", "v:0",
|
|
||||||
"-show_entries", "stream=r_frame_rate,duration,width,height",
|
|
||||||
p.currentPath,
|
|
||||||
)
|
|
||||||
|
|
||||||
output, err := cmd.CombinedOutput()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("ffprobe failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse frame rate and duration
|
|
||||||
p.frameRate = 25.0 // Default fallback
|
|
||||||
p.duration = 0
|
|
||||||
|
|
||||||
lines := strings.Split(string(output), "\n")
|
|
||||||
for _, line := range lines {
|
|
||||||
if strings.Contains(line, "r_frame_rate=") {
|
|
||||||
if parts := strings.Split(line, "="); len(parts) > 1 {
|
|
||||||
var fr float64
|
|
||||||
if _, err := fmt.Sscanf(parts[1], "%f", &fr); err == nil {
|
|
||||||
p.frameRate = fr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if strings.Contains(line, "duration=") {
|
|
||||||
if parts := strings.Split(line, "="); len(parts) > 1 {
|
|
||||||
if dur, err := time.ParseDuration(parts[1]); err == nil {
|
|
||||||
p.duration = dur
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if p.frameRate > 0 && p.duration > 0 {
|
|
||||||
p.videoInfo = &VideoInfo{
|
|
||||||
Width: p.windowW,
|
|
||||||
Height: p.windowH,
|
|
||||||
Duration: p.duration,
|
|
||||||
FrameRate: p.frameRate,
|
|
||||||
FrameCount: int64(p.duration.Seconds() * p.frameRate),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
p.videoInfo = &VideoInfo{
|
|
||||||
Width: p.windowW,
|
|
||||||
Height: p.windowH,
|
|
||||||
Duration: p.duration,
|
|
||||||
FrameRate: p.frameRate,
|
|
||||||
FrameCount: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logging.Debug(logging.CatPlayer, "Video properties: %dx%d@%.3ffps, %.2fs",
|
|
||||||
p.windowW, p.windowH, p.frameRate, p.duration.Seconds())
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeStringToStdin sends a command to FFmpeg's stdin
|
|
||||||
func (p *UnifiedPlayer) writeStringToStdin(cmd string) {
|
|
||||||
// TODO: Implement stdin command writing for interactive FFmpeg control
|
|
||||||
// Currently a no-op as stdin is not configured in this player implementation
|
|
||||||
logging.Debug(logging.CatPlayer, "Stdin command (not implemented): %s", cmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateAVSync maintains synchronization between audio and video
|
|
||||||
func (p *UnifiedPlayer) updateAVSync() {
|
|
||||||
// PTS-based drift correction with adaptive timing
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
|
|
||||||
if p.audioPTS > 0 && p.videoPTS > 0 {
|
|
||||||
drift := p.audioPTS - p.videoPTS
|
|
||||||
if abs(drift) > 900 { // More than 10ms of drift (at 90kHz)
|
|
||||||
logging.Debug(logging.CatPlayer, "A/V sync drift: %d PTS", drift)
|
|
||||||
// Gradual adjustment to avoid audio glitches
|
|
||||||
p.ptsOffset += drift / 10 // 10% correction per frame
|
|
||||||
} else {
|
|
||||||
logging.Debug(logging.CatPlayer, "A/V sync drift: %d PTS", drift)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// addHardwareAcceleration adds hardware acceleration flags to FFmpeg args
|
|
||||||
func (p *UnifiedPlayer) addHardwareAcceleration(args []string) []string {
|
|
||||||
// This is a placeholder - actual implementation would detect available hardware
|
|
||||||
// and add appropriate flags like "-hwaccel cuda", "-c:v h264_nvenc"
|
|
||||||
|
|
||||||
// For now, just log that hardware acceleration is considered
|
|
||||||
logging.Debug(logging.CatPlayer, "Hardware acceleration requested but not yet implemented")
|
|
||||||
return args
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyVolumeToBuffer applies volume adjustments to audio buffer
|
|
||||||
func (p *UnifiedPlayer) applyVolumeToBuffer(buffer []byte) {
|
|
||||||
if p.volume <= 0 {
|
|
||||||
// Muted - set to silence
|
|
||||||
for i := range buffer {
|
|
||||||
buffer[i] = 0
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Apply volume gain
|
|
||||||
gain := p.volume
|
|
||||||
for i := 0; i < len(buffer); i += 2 {
|
|
||||||
if i+1 < len(buffer) {
|
|
||||||
sample := int16(binary.LittleEndian.Uint16(buffer[i : i+2]))
|
|
||||||
adjusted := int(float64(sample) * gain)
|
|
||||||
|
|
||||||
// Clamp to int16 range
|
|
||||||
if adjusted > 32767 {
|
|
||||||
adjusted = 32767
|
|
||||||
} else if adjusted < -32768 {
|
|
||||||
adjusted = -32768
|
|
||||||
}
|
|
||||||
|
|
||||||
binary.LittleEndian.PutUint16(buffer[i:i+2], uint16(adjusted))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// abs returns absolute value of int64
|
|
||||||
func abs(x int64) int64 {
|
|
||||||
if x < 0 {
|
|
||||||
return -x
|
|
||||||
}
|
|
||||||
return x
|
|
||||||
}
|
|
||||||
|
|
@ -1,391 +0,0 @@
|
||||||
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()
|
|
||||||
}
|
|
||||||
78
playback-test.log
Normal file
78
playback-test.log
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
[videotools] 2026/01/09 21:01:38.464010 2026-01-09T21:01:38.463963223-05:00 [SYS] starting VideoTools prototype at 2026-01-09T21:01:38-05:00
|
||||||
|
[videotools] 2026/01/09 21:01:38.464102 2026-01-09T21:01:38.464092996-05:00 [SYS] Found ffmpeg in PATH: /usr/bin/ffmpeg
|
||||||
|
[videotools] 2026/01/09 21:01:38.660447 2026-01-09T21:01:38.660388777-05:00 [SYS] Detected VAAPI encoder
|
||||||
|
[videotools] 2026/01/09 21:01:38.660509 2026-01-09T21:01:38.660502309-05:00 [SYS] Detected NVENC encoder
|
||||||
|
[videotools] 2026/01/09 21:01:38.660530 2026-01-09T21:01:38.660523879-05:00 [SYS] Detected QSV encoder
|
||||||
|
[videotools] 2026/01/09 21:01:38.660541 2026-01-09T21:01:38.660534419-05:00 [SYS] Platform detected: linux/amd64
|
||||||
|
[videotools] 2026/01/09 21:01:38.660550 2026-01-09T21:01:38.660543857-05:00 [SYS] FFmpeg path: /usr/bin/ffmpeg
|
||||||
|
[videotools] 2026/01/09 21:01:38.660558 2026-01-09T21:01:38.660553014-05:00 [SYS] FFprobe path: /usr/bin/ffprobe
|
||||||
|
[videotools] 2026/01/09 21:01:38.660567 2026-01-09T21:01:38.660561379-05:00 [SYS] Temp directory: /tmp/videotools
|
||||||
|
[videotools] 2026/01/09 21:01:38.660592 2026-01-09T21:01:38.660585945-05:00 [SYS] Hardware encoders: [vaapi nvenc qsv]
|
||||||
|
[videotools] 2026/01/09 21:01:38.660625 2026-01-09T21:01:38.660612545-05:00 [UI] Wayland display server detected: WAYLAND_DISPLAY=wayland-0
|
||||||
|
[videotools] 2026/01/09 21:01:38.660639 2026-01-09T21:01:38.660633614-05:00 [UI] Session type: wayland
|
||||||
|
2026/01/09 21:01:38 Fyne error: Settings watch error:
|
||||||
|
2026/01/09 21:01:38 Cause: no such file or directory
|
||||||
|
[videotools] 2026/01/09 21:01:38.661564 2026-01-09T21:01:38.66155227-05:00 [UI] created fyne app: &app.fyneApp{driver:(*glfw.gLDriver)(0xc0003ce000), clipboard:glfw.clipboard{}, icon:fyne.Resource(nil), uniqueID:"com.leaktechnologies.videotools", cloud:fyne.CloudProvider(nil), lifecycle:app.Lifecycle{onForeground:(func())(nil), onBackground:(func())(nil), onStarted:(func())(nil), onStopped:(func())(nil), onStoppedHookExecuted:(func())(0xb56460), eventQueue:(*async.UnboundedChan[func()])(0xc00039e120)}, settings:(*app.settings)(0xc0003ae750), storage:(*app.store)(0xc000396540), prefs:(*app.preferences)(0xc0003be040)}
|
||||||
|
2026/01/09 21:01:38 At: /home/stu/Projects/VideoTools/vendor/fyne.io/fyne/v2/app/settings_desktop.go:19
|
||||||
|
[videotools] 2026/01/09 21:01:38.737212 2026-01-09T21:01:38.737178718-05:00 [UI] loaded app icon from assets/logo/VT_Icon.png
|
||||||
|
[videotools] 2026/01/09 21:01:38.737344 2026-01-09T21:01:38.737336302-05:00 [UI] app icon loaded and applied
|
||||||
|
[videotools] 2026/01/09 21:01:38.737424 2026-01-09T21:01:38.737394601-05:00 [UI] window initialized at 800x600 (compact default), manual resizing enabled
|
||||||
|
[videotools] 2026/01/09 21:01:38.757316 2026-01-09T21:01:38.757273213-05:00 [PLAYER] INFO: GStreamer controller initialized (GStreamer 1.26+)
|
||||||
|
[videotools] 2026/01/09 21:01:38.760348 2026-01-09T21:01:38.760308211-05:00 [UI] building tile convert color={103 58 183 255} enabled=true missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760369 2026-01-09T21:01:38.760362402-05:00 [UI] building tile merge color={76 175 80 255} enabled=true missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760378 2026-01-09T21:01:38.760372701-05:00 [UI] building tile trim color={249 168 37 255} enabled=false missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760388 2026-01-09T21:01:38.76038256-05:00 [UI] building tile filters color={0 188 212 255} enabled=true missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760399 2026-01-09T21:01:38.760392538-05:00 [UI] building tile audio color={255 143 0 255} enabled=true missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760409 2026-01-09T21:01:38.760403309-05:00 [UI] building tile subtitles color={104 159 56 255} enabled=true missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760419 2026-01-09T21:01:38.760413548-05:00 [UI] building tile compare color={233 30 99 255} enabled=true missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760429 2026-01-09T21:01:38.760423456-05:00 [UI] building tile inspect color={244 67 54 255} enabled=true missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760440 2026-01-09T21:01:38.760433876-05:00 [UI] building tile upscale color={156 39 176 255} enabled=true missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760450 2026-01-09T21:01:38.760444916-05:00 [UI] building tile author color={255 87 34 255} enabled=true missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760461 2026-01-09T21:01:38.760454975-05:00 [UI] building tile rip color={255 152 0 255} enabled=true missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760471 2026-01-09T21:01:38.760465394-05:00 [UI] building tile bluray color={33 150 243 255} enabled=false missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760479 2026-01-09T21:01:38.760474672-05:00 [UI] building tile player color={63 81 181 255} enabled=true missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760488 2026-01-09T21:01:38.760483338-05:00 [UI] building tile thumb color={0 172 193 255} enabled=true missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.760497 2026-01-09T21:01:38.760491704-05:00 [UI] building tile settings color={96 125 139 255} enabled=true missingDeps=false
|
||||||
|
[videotools] 2026/01/09 21:01:38.876175 2026-01-09T21:01:38.876146132-05:00 [UI] main menu rendered with 17 modules
|
||||||
|
[videotools] 2026/01/09 21:24:12.075370 2026-01-09T21:24:12.068829983-05:00 [UI] player window target pos=(0,0) size=640x360
|
||||||
|
[videotools] 2026/01/09 21:24:12.075442 2026-01-09T21:24:12.075419871-05:00 [MODULE] loaded video into player module
|
||||||
|
[videotools] 2026/01/09 21:24:27.395432 2026-01-09T21:24:27.395403435-05:00 [PLAYER] INFO: GStreamer loaded video: /home/stu/Videos/Test Footage/bbb_sunflower_2160p_60fps_normal.mp4 (60.00 fps, 612x320)
|
||||||
|
[videotools] 2026/01/09 21:24:27.395560 2026-01-09T21:24:27.395542896-05:00 [PLAYER] INFO: playSession: frameDisplayLoop started (fps=60.00, interval=16.666666ms)
|
||||||
|
[videotools] 2026/01/09 21:24:27.395795 2026-01-09T21:24:27.395729875-05:00 [PLAYER] playSession: SetVolume to 100.0%
|
||||||
|
[videotools] 2026/01/09 21:24:27.396338 2026-01-09T21:24:27.396328454-05:00 [PLAYER] playSession: Play called
|
||||||
|
[videotools] 2026/01/09 21:24:27.451411 2026-01-09T21:24:27.451380951-05:00 [PLAYER] Frame 1 updated (0.04s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.496824 2026-01-09T21:24:27.496795532-05:00 [PLAYER] Frame 2 updated (0.09s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.564804 2026-01-09T21:24:27.564750487-05:00 [PLAYER] Frame 3 updated (0.16s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.602671 2026-01-09T21:24:27.602625234-05:00 [PLAYER] Frame 4 updated (0.17s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.628470 2026-01-09T21:24:27.628371521-05:00 [PLAYER] Frame 5 updated (0.21s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.662033 2026-01-09T21:24:27.66198912-05:00 [PLAYER] Frame 6 updated (0.24s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.707839 2026-01-09T21:24:27.707790134-05:00 [PLAYER] Frame 7 updated (0.29s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.746562 2026-01-09T21:24:27.746528005-05:00 [PLAYER] Frame 9 updated (0.34s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.754397 2026-01-09T21:24:27.754369422-05:00 [PLAYER] Frame 9 updated (0.34s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.785155 2026-01-09T21:24:27.785123299-05:00 [PLAYER] Frame 10 updated (0.38s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.819318 2026-01-09T21:24:27.819283182-05:00 [PLAYER] Frame 11 updated (0.41s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.874848 2026-01-09T21:24:27.874767376-05:00 [PLAYER] Frame 12 updated (0.45s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.908088 2026-01-09T21:24:27.908046123-05:00 [PLAYER] Frame 13 updated (0.47s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.932341 2026-01-09T21:24:27.932306702-05:00 [PLAYER] Frame 14 updated (0.52s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:27.982781 2026-01-09T21:24:27.982733081-05:00 [PLAYER] Frame 15 updated (0.57s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.034175 2026-01-09T21:24:28.034118784-05:00 [PLAYER] Frame 17 updated (0.63s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.081042 2026-01-09T21:24:28.074553066-05:00 [PLAYER] Frame 18 updated (0.67s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.113598 2026-01-09T21:24:28.113521137-05:00 [PLAYER] Frame 18 updated (0.67s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.157376 2026-01-09T21:24:28.157319757-05:00 [PLAYER] Frame 19 updated (0.72s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.193074 2026-01-09T21:24:28.193032734-05:00 [PLAYER] Frame 21 updated (0.79s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.225241 2026-01-09T21:24:28.225212165-05:00 [PLAYER] Frame 22 updated (0.82s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.235108 2026-01-09T21:24:28.235053932-05:00 [PLAYER] Frame 22 updated (0.82s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.267262 2026-01-09T21:24:28.267209288-05:00 [PLAYER] Frame 23 updated (0.85s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.309876 2026-01-09T21:24:28.309821411-05:00 [PLAYER] Frame 24 updated (0.89s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.341272 2026-01-09T21:24:28.341231474-05:00 [PLAYER] Frame 25 updated (0.92s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.371357 2026-01-09T21:24:28.37133193-05:00 [PLAYER] Frame 26 updated (0.96s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.401294 2026-01-09T21:24:28.401268909-05:00 [PLAYER] Frame 27 updated (0.99s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.432303 2026-01-09T21:24:28.43224406-05:00 [PLAYER] Frame 28 updated (1.02s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.464329 2026-01-09T21:24:28.464301523-05:00 [PLAYER] Frame 29 updated (1.05s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.502348 2026-01-09T21:24:28.502319027-05:00 [PLAYER] Frame 30 updated (1.09s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.530081 2026-01-09T21:24:28.530050595-05:00 [PLAYER] Frame 31 updated (1.12s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.567645 2026-01-09T21:24:28.567618388-05:00 [PLAYER] Frame 32 updated (1.16s, paused=false, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:24:28.591011 2026-01-09T21:24:28.59085311-05:00 [PLAYER] playSession: Pause called
|
||||||
|
[videotools] 2026/01/09 21:24:28.604764 2026-01-09T21:24:28.604734898-05:00 [PLAYER] Frame 33 updated (1.19s, paused=true, size=3840x2160)
|
||||||
|
[videotools] 2026/01/09 21:30:56.926763 2026-01-09T21:30:56.926305928-05:00 [PLAYER] playSession: Stop called
|
||||||
|
[videotools] 2026/01/09 21:30:56.926831 2026-01-09T21:30:56.926375819-05:00 [PLAYER] INFO: playSession: frameDisplayLoop stopped
|
||||||
1
scripts/playback-test.log
Normal file
1
scripts/playback-test.log
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
bash: ./VideoTools: No such file or directory
|
||||||
Loading…
Reference in New Issue
Block a user