Attempt to recover from 'Demuxing failed' errors.

This commit is contained in:
Storm Dragon
2025-12-17 01:23:39 -05:00
parent 221224270b
commit d5545dadcd

View File

@@ -7,6 +7,7 @@ from __future__ import annotations
from typing import Callable, List, Optional
import random
import time
import logging
from PySide6.QtCore import QObject, QUrl, Signal
from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer
@@ -29,6 +30,7 @@ class PlaybackManager(QObject):
def __init__(self, parent: Optional[QObject] = None):
super().__init__(parent)
self.logger = logging.getLogger("navipy.playback")
self.player = QMediaPlayer()
self.audioOutput = QAudioOutput()
self.player.setAudioOutput(self.audioOutput)
@@ -38,6 +40,8 @@ class PlaybackManager(QObject):
self.streamResolver: Optional[Callable[[Song], str]] = None
self.shuffleEnabled = False
self.repeatMode = "none" # none, one, all
self._maxRetriesPerTrack = 2
self._retryAttemptsForTrack = 0
# Shuffle state: shuffleOrder is a list of queue indices in shuffled order
# shufflePosition tracks where we are in that order
@@ -97,6 +101,7 @@ class PlaybackManager(QObject):
self.currentIndex = -1
self._shuffleOrder = []
self._shufflePosition = -1
self._retryAttemptsForTrack = 0
self.player.stop()
self.queueChanged.emit(self.queue.copy())
self.playbackStateChanged.emit("stopped")
@@ -110,6 +115,7 @@ class PlaybackManager(QObject):
return
self.currentIndex = index
self._retryAttemptsForTrack = 0
# Sync shuffle position to match the played index
self._syncShufflePosition(index)
song = self.queue[index]
@@ -328,6 +334,10 @@ class PlaybackManager(QObject):
description = self.player.errorString() or str(error)
currentTime = time.monotonic()
# Try to recover from transient stream errors (e.g., "Demuxing failed")
if self._attemptRecovery(description):
return
# Debounce: skip if same error within debounce window
if (description == self._lastErrorMessage and
currentTime - self._lastErrorTime < self._errorDebounceSeconds):
@@ -339,4 +349,60 @@ class PlaybackManager(QObject):
# Stop playback to prevent further cascading errors
self.player.stop()
if self._retryAttemptsForTrack >= self._maxRetriesPerTrack and len(self.queue) > 1:
self.logger.error(
"Playback error persisted after %d retries: %s; skipping to next track",
self._retryAttemptsForTrack,
description
)
self.errorOccurred.emit(f"{description}. Skipping to next track.")
self.next(fromAuto=True)
return
self.errorOccurred.emit(description)
def _shouldRetry(self, description: str) -> bool:
"""
Decide if the current error is transient enough to retry the stream.
We only retry a limited number of times per track and for errors that
look network/demux related rather than permanent format issues.
"""
if self.currentIndex == -1 or not self.streamResolver:
return False
if self._retryAttemptsForTrack >= self._maxRetriesPerTrack:
return False
text = description.lower()
return "demux" in text or "network" in text or "timeout" in text
def _attemptRecovery(self, description: str) -> bool:
"""Retry the current stream before surfacing the error to the UI."""
if not self._shouldRetry(description):
return False
song = self.currentSong()
if not song:
return False
position = self.player.position()
streamUrl = self.streamResolver(song)
if not streamUrl:
return False
self._retryAttemptsForTrack += 1
self.logger.warning(
"Playback error '%s' on '%s' (%s); retrying (%d/%d) at %d ms",
description,
song.title,
song.id,
self._retryAttemptsForTrack,
self._maxRetriesPerTrack,
position
)
self.player.stop()
self.player.setSource(QUrl(streamUrl))
if position > 0:
self.player.setPosition(position)
self.player.play()
return True