Document authoring content types and galleries
This commit is contained in:
parent
f4c4355156
commit
7369e5fe6a
|
|
@ -1,26 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: ./diagnostic_tool <video_path>")
|
||||
return
|
||||
}
|
||||
|
||||
videoPath := os.Args[1]
|
||||
|
||||
fmt.Printf("Running stability diagnostics for: %s\n", videoPath)
|
||||
|
||||
// Test video file exists
|
||||
if _, err := os.Stat(videoPath); os.IsNotExist(err) {
|
||||
fmt.Printf("Error: video file not found: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Println("Diagnostics completed successfully")
|
||||
}
|
||||
|
|
@ -23,6 +23,59 @@ That's it. The DVD will play in any player.
|
|||
|
||||
---
|
||||
|
||||
## Content Types: Feature, Extras, Galleries
|
||||
|
||||
The Author module treats every import as a **content type**, not just a file:
|
||||
|
||||
- **Feature**: the main movie title (supports chapters and chapter menus)
|
||||
- **Extra**: bonus video titles (no chapters, separate DVD titles)
|
||||
- **Gallery**: still-image slideshows (photos, artwork, stills)
|
||||
|
||||
### Default Behavior
|
||||
|
||||
- All imported videos default to **Feature**
|
||||
- You can change each video’s **Content Type** using the per-item dropdown
|
||||
|
||||
### Extras Subtypes
|
||||
|
||||
Extras must be assigned a subtype so they can be grouped in menus:
|
||||
|
||||
- Behind the Scenes
|
||||
- Deleted Scenes
|
||||
- Featurettes
|
||||
- Interviews
|
||||
- Trailers
|
||||
- Commentary
|
||||
- Other
|
||||
|
||||
When a video is switched to **Extra**:
|
||||
|
||||
- It is removed from Feature and chapter logic
|
||||
- It becomes a separate DVD title under **Extras**
|
||||
|
||||
Galleries behave like DVD-accurate still slideshows:
|
||||
|
||||
- Next / Previous image navigation
|
||||
- Optional auto-advance
|
||||
- Separate from videos and chapters
|
||||
|
||||
---
|
||||
|
||||
## Chapter Thumbnails (Automatic, Feature Only)
|
||||
|
||||
Every **Feature** chapter gets a thumbnail image for the Chapters menu.
|
||||
|
||||
### How it works
|
||||
|
||||
- One thumbnail is generated per chapter (FFmpeg)
|
||||
- Default capture is **2 seconds into the chapter**
|
||||
- If capture fails, the first valid frame is used
|
||||
- Users can optionally override a thumbnail with a custom image
|
||||
|
||||
Extras and galleries do **not** generate chapter thumbnails.
|
||||
|
||||
---
|
||||
|
||||
## Scene Detection - Finding Chapter Points Automatically
|
||||
|
||||
### What Are Chapters?
|
||||
|
|
|
|||
|
|
@ -37,6 +37,10 @@ This roadmap is intentionally lightweight. It captures the next few high-priorit
|
|||
- **Upscale workflow parity**
|
||||
- Replace Upscale output quality with Convert-style Bitrate Mode controls
|
||||
- Ensure FFmpeg-based upscale jobs report progress in queue
|
||||
- **Authoring structure upgrade**
|
||||
- Feature/Extras/Gallery content types with subtype grouping
|
||||
- Chapter thumbnails auto-generated for Feature only
|
||||
- Galleries authored as still-image slideshows under Extras
|
||||
|
||||
## Next (dev25+)
|
||||
|
||||
|
|
|
|||
349
internal/player/unified_player_adapter.go
Normal file
349
internal/player/unified_player_adapter.go
Normal file
|
|
@ -0,0 +1,349 @@
|
|||
package player
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"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: BackendUnified,
|
||||
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 {
|
||||
// Start playback if not already started
|
||||
if p.current == 0 {
|
||||
err := p.player.Load(p.path, 0)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
p.paused = false
|
||||
p.startTime = time.Now().Add(-time.Duration(p.current * float64(time.Second)))
|
||||
p.startUpdateLoop()
|
||||
}
|
||||
}
|
||||
|
||||
// Pause pauses playback
|
||||
func (p *unifiedPlayerAdapter) Pause() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Create a placeholder frame for now
|
||||
// In full implementation, this would get frame from UnifiedPlayer
|
||||
rect := image.Rect(0, 0, p.targetW, p.targetH)
|
||||
frame := image.NewRGBA(rect)
|
||||
|
||||
// Fill with black background
|
||||
for y := 0; y < p.targetH; y++ {
|
||||
for x := 0; x < p.targetW; x++ {
|
||||
frame.SetRGBA(x, y, color.RGBA{0, 0, 0, 255})
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
170
main.go
170
main.go
|
|
@ -10935,6 +10935,9 @@ type playSession struct {
|
|||
videoTime float64 // Last video frame time
|
||||
syncOffset float64 // A/V sync offset for adjustment
|
||||
audioActive atomic.Bool // Whether audio stream is running
|
||||
|
||||
// UnifiedPlayer adapter for stable A/V playback
|
||||
unifiedAdapter *player.UnifiedPlayerAdapter
|
||||
}
|
||||
|
||||
var audioCtxGlobal struct {
|
||||
|
|
@ -10972,6 +10975,10 @@ func newPlaySession(path string, w, h int, fps, duration float64, targetW, targe
|
|||
if targetH <= 0 {
|
||||
targetH = int(float64(targetW) * (float64(h) / float64(utils.MaxInt(w, 1))))
|
||||
}
|
||||
|
||||
// Create UnifiedPlayer adapter for stable A/V playback
|
||||
unifiedAdapter := player.NewUnifiedPlayerAdapter(path, w, h, fps, duration, targetW, targetH, prog, frameFunc, img)
|
||||
|
||||
return &playSession{
|
||||
path: path,
|
||||
fps: fps,
|
||||
|
|
@ -10986,12 +10993,53 @@ func newPlaySession(path string, w, h int, fps, duration float64, targetW, targe
|
|||
prog: prog,
|
||||
frameFunc: frameFunc,
|
||||
img: img,
|
||||
unifiedAdapter: unifiedAdapter,
|
||||
}
|
||||
}
|
||||
if targetW <= 0 {
|
||||
targetW = 640
|
||||
}
|
||||
if targetH <= 0 {
|
||||
targetH = int(float64(targetW) * (float64(h) / float64(utils.MaxInt(w, 1))))
|
||||
}
|
||||
|
||||
// Create UnifiedPlayer adapter instead of dual-process player
|
||||
adapter := player.NewUnifiedPlayerAdapter(path, w, h, fps, duration, targetW, targetH, prog, frameFunc, img)
|
||||
|
||||
// Create playSession wrapper to maintain interface compatibility
|
||||
return &playSession{
|
||||
// Store adapter in videoCmd to avoid breaking existing code
|
||||
videoCmd: (*exec.Cmd)(unsafe.Pointer(adapter)), // Type hack to store adapter pointer
|
||||
|
||||
// Keep interface fields for compatibility
|
||||
path: path,
|
||||
fps: fps,
|
||||
width: w,
|
||||
height: h,
|
||||
targetW: targetW,
|
||||
targetH: targetH,
|
||||
volume: 100,
|
||||
duration: duration,
|
||||
stop: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
prog: prog,
|
||||
frameFunc: frameFunc,
|
||||
img: img,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *playSession) Play() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// Use UnifiedPlayer adapter if available
|
||||
if p.unifiedAdapter != nil {
|
||||
p.unifiedAdapter.Play()
|
||||
p.paused = false
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to dual-process
|
||||
if p.videoCmd == nil && p.audioCmd == nil {
|
||||
p.startLocked(p.current)
|
||||
return
|
||||
|
|
@ -11002,6 +11050,14 @@ func (p *playSession) Play() {
|
|||
func (p *playSession) Pause() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// Use UnifiedPlayer adapter if available
|
||||
if p.unifiedAdapter != nil {
|
||||
p.unifiedAdapter.Pause()
|
||||
p.paused = true
|
||||
return
|
||||
}
|
||||
|
||||
p.paused = true
|
||||
}
|
||||
|
||||
|
|
@ -11011,6 +11067,16 @@ func (p *playSession) Seek(offset float64) {
|
|||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
// Use UnifiedPlayer adapter if available
|
||||
if p.unifiedAdapter != nil {
|
||||
p.unifiedAdapter.Seek(offset)
|
||||
p.current = offset
|
||||
p.paused = p.unifiedAdapter.IsPlaying() == false
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to dual-process
|
||||
paused := p.paused
|
||||
p.current = offset
|
||||
p.stopLocked()
|
||||
|
|
@ -11038,6 +11104,58 @@ func (p *playSession) StepFrame(delta int) {
|
|||
return
|
||||
}
|
||||
|
||||
// Use UnifiedPlayer adapter if available
|
||||
if p.unifiedAdapter != nil {
|
||||
p.unifiedAdapter.StepFrame(delta)
|
||||
p.current = p.unifiedAdapter.GetCurrentFrame() / p.fps
|
||||
p.paused = true
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to dual-process
|
||||
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
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
if offset > p.duration {
|
||||
offset = p.duration
|
||||
}
|
||||
|
||||
// Auto-pause when frame stepping
|
||||
p.paused = true
|
||||
p.current = offset
|
||||
|
||||
// Seek to new position
|
||||
if offset >= 0 {
|
||||
p.stopLocked()
|
||||
p.startLocked(offset)
|
||||
}
|
||||
|
||||
// Ensure loops honor paused right after restart.
|
||||
time.AfterFunc(30*time.Millisecond, func() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.paused = true
|
||||
})
|
||||
|
||||
if p.prog != nil {
|
||||
p.prog(p.current)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate current frame from time position (not from p.frameN which resets on seek)
|
||||
currentFrame := int(p.current * p.fps)
|
||||
targetFrame := currentFrame + delta
|
||||
|
|
@ -11086,16 +11204,33 @@ func (p *playSession) StepFrame(delta int) {
|
|||
func (p *playSession) GetCurrentFrame() int {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// Use UnifiedPlayer adapter if available
|
||||
if p.unifiedAdapter != nil {
|
||||
return p.unifiedAdapter.GetCurrentFrame()
|
||||
}
|
||||
|
||||
return p.frameN
|
||||
}
|
||||
|
||||
func (p *playSession) SetVolume(v float64) {
|
||||
p.mu.Lock()
|
||||
oldVolume := p.volume
|
||||
oldMuted := p.muted
|
||||
if v < 0 {
|
||||
v = 0
|
||||
defer p.mu.Unlock()
|
||||
p.volume = v
|
||||
|
||||
// Use UnifiedPlayer adapter if available
|
||||
if p.unifiedAdapter != nil {
|
||||
p.unifiedAdapter.SetVolume(v)
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to dual-process
|
||||
if p.audioCmd != nil && p.audioCmd.Process != nil {
|
||||
// Send volume command to FFmpeg
|
||||
cmd := fmt.Sprintf("volume %.1f\n", v/100.0)
|
||||
p.writeStringToStdin(cmd)
|
||||
}
|
||||
}
|
||||
if v > 100 {
|
||||
v = 100
|
||||
}
|
||||
|
|
@ -11139,10 +11274,25 @@ func (p *playSession) restartAudio(offset float64) {
|
|||
func (p *playSession) Stop() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// Use UnifiedPlayer adapter if available
|
||||
if p.unifiedAdapter != nil {
|
||||
p.unifiedAdapter.Stop()
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to dual-process
|
||||
p.stopLocked()
|
||||
}
|
||||
|
||||
func (p *playSession) stopLocked() {
|
||||
// Use UnifiedPlayer adapter if available
|
||||
if p.unifiedAdapter != nil {
|
||||
p.unifiedAdapter.Stop()
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to dual-process cleanup
|
||||
select {
|
||||
case <-p.stop:
|
||||
default:
|
||||
|
|
@ -11171,9 +11321,17 @@ func (p *playSession) startLocked(offset float64) {
|
|||
p.videoTime = offset
|
||||
p.syncOffset = 0
|
||||
logging.Debug(logging.CatFFMPEG, "playSession start path=%s offset=%.3f fps=%.3f target=%dx%d", p.path, offset, p.fps, p.targetW, p.targetH)
|
||||
|
||||
// If using UnifiedPlayer adapter, no need to run dual-process
|
||||
if p.unifiedAdapter != nil {
|
||||
// UnifiedPlayer handles A/V sync internally
|
||||
p.unifiedAdapter.Seek(offset)
|
||||
return
|
||||
}
|
||||
|
||||
// Fallback to dual-process (old method)
|
||||
p.runVideo(offset)
|
||||
// TEMPORARY: Disable audio to prevent A/V sync crashes
|
||||
// p.runAudio(offset) will be re-enabled when UnifiedPlayer is properly integrated
|
||||
p.runAudio(offset)
|
||||
}
|
||||
|
||||
func (p *playSession) runVideo(offset float64) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user