Attempt to recover from 'Demuxing failed' errors.
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user