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
+66
View File
@@ -7,6 +7,7 @@ from __future__ import annotations
from typing import Callable, List, Optional from typing import Callable, List, Optional
import random import random
import time import time
import logging
from PySide6.QtCore import QObject, QUrl, Signal from PySide6.QtCore import QObject, QUrl, Signal
from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer from PySide6.QtMultimedia import QAudioOutput, QMediaPlayer
@@ -29,6 +30,7 @@ class PlaybackManager(QObject):
def __init__(self, parent: Optional[QObject] = None): def __init__(self, parent: Optional[QObject] = None):
super().__init__(parent) super().__init__(parent)
self.logger = logging.getLogger("navipy.playback")
self.player = QMediaPlayer() self.player = QMediaPlayer()
self.audioOutput = QAudioOutput() self.audioOutput = QAudioOutput()
self.player.setAudioOutput(self.audioOutput) self.player.setAudioOutput(self.audioOutput)
@@ -38,6 +40,8 @@ class PlaybackManager(QObject):
self.streamResolver: Optional[Callable[[Song], str]] = None self.streamResolver: Optional[Callable[[Song], str]] = None
self.shuffleEnabled = False self.shuffleEnabled = False
self.repeatMode = "none" # none, one, all self.repeatMode = "none" # none, one, all
self._maxRetriesPerTrack = 2
self._retryAttemptsForTrack = 0
# Shuffle state: shuffleOrder is a list of queue indices in shuffled order # Shuffle state: shuffleOrder is a list of queue indices in shuffled order
# shufflePosition tracks where we are in that order # shufflePosition tracks where we are in that order
@@ -97,6 +101,7 @@ class PlaybackManager(QObject):
self.currentIndex = -1 self.currentIndex = -1
self._shuffleOrder = [] self._shuffleOrder = []
self._shufflePosition = -1 self._shufflePosition = -1
self._retryAttemptsForTrack = 0
self.player.stop() self.player.stop()
self.queueChanged.emit(self.queue.copy()) self.queueChanged.emit(self.queue.copy())
self.playbackStateChanged.emit("stopped") self.playbackStateChanged.emit("stopped")
@@ -110,6 +115,7 @@ class PlaybackManager(QObject):
return return
self.currentIndex = index self.currentIndex = index
self._retryAttemptsForTrack = 0
# Sync shuffle position to match the played index # Sync shuffle position to match the played index
self._syncShufflePosition(index) self._syncShufflePosition(index)
song = self.queue[index] song = self.queue[index]
@@ -328,6 +334,10 @@ class PlaybackManager(QObject):
description = self.player.errorString() or str(error) description = self.player.errorString() or str(error)
currentTime = time.monotonic() 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 # Debounce: skip if same error within debounce window
if (description == self._lastErrorMessage and if (description == self._lastErrorMessage and
currentTime - self._lastErrorTime < self._errorDebounceSeconds): currentTime - self._lastErrorTime < self._errorDebounceSeconds):
@@ -339,4 +349,60 @@ class PlaybackManager(QObject):
# Stop playback to prevent further cascading errors # Stop playback to prevent further cascading errors
self.player.stop() 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) 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