mirror of
https://github.com/Dispatcharr/Dispatcharr.git
synced 2026-07-17 16:50:53 +00:00
Enhancement: Implement live proxy failover for VLC stream profile, ensuring proper handling of upstream URL failures. Update changelog to reflect changes in stream management and VLC profile parameters.
This commit is contained in:
parent
9c3ace6146
commit
96db4f92c7
6 changed files with 91 additions and 2 deletions
|
|
@ -46,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- **`ChannelUtils.requeryChannels()` returns the API promise again** so bulk channel delete, drag reorder, and inline channel-number saves await the table refetch before clearing selection or continuing. — Thanks [@nick4810](https://github.com/nick4810)
|
||||
- **VOD connection card Content Duration badge** shows human-readable lengths (`45m`, `2h 0m`) again on the Stats page instead of bare minute integers. — Thanks [@nick4810](https://github.com/nick4810)
|
||||
- **Frontend unit tests pass on Node 25+ again.** Node 25+ exposes a native `localStorage` stub on `globalThis` that lacks Storage API methods (`getItem`, `clear`, etc.), which shadows jsdom's implementation and breaks tests that touch `localStorage` (e.g. `TypeError: localStorage.clear is not a function`). `setupTests.js` now installs an in-memory shim on `globalThis` and `window` when those methods are missing; the shim is skipped automatically once Node provides a working implementation. — Thanks [@nick4810](https://github.com/nick4810)
|
||||
- **Live proxy failover works with the default VLC stream profile.** When `cvlc` could not open an upstream URL it often stayed running in dummy mode without writing TS data, so the stream manager never left its read loop and never exhausted retries to switch streams. The locked VLC profile now passes `--play-and-exit` (migration 0027 for existing installs), and `VLCLogParser` treats `unable to open the mrl` as a fatal input error so the connection is closed and the normal retry → failover cycle can proceed.
|
||||
|
||||
## [0.27.2] - 2026-06-30
|
||||
|
||||
|
|
|
|||
|
|
@ -934,7 +934,13 @@ class StreamManager:
|
|||
parser = LogParserFactory._parsers.get(self.parser_type)
|
||||
if parser:
|
||||
stream_type = parser.can_parse(content)
|
||||
if stream_type:
|
||||
if stream_type == 'vlc_input_failed':
|
||||
logger.warning(
|
||||
f"VLC could not open input for channel {self.channel_id}: {content}"
|
||||
)
|
||||
self.connected = False
|
||||
self._close_socket()
|
||||
elif stream_type:
|
||||
# Parser can handle this line, parse it directly
|
||||
parsed_data = LogParserFactory.parse(stream_type, content)
|
||||
if parsed_data:
|
||||
|
|
|
|||
|
|
@ -164,6 +164,9 @@ class VLCLogParser(BaseLogParser):
|
|||
"""Check if this is a VLC line we can parse"""
|
||||
lower = line.lower()
|
||||
|
||||
if 'unable to open the mrl' in lower:
|
||||
return 'vlc_input_failed'
|
||||
|
||||
# VLC TS demux codec detection
|
||||
if 'ts demux debug' in lower and 'type=' in lower:
|
||||
if 'video' in lower:
|
||||
|
|
|
|||
42
apps/proxy/live_proxy/tests/test_vlc_failover.py
Normal file
42
apps/proxy/live_proxy/tests/test_vlc_failover.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"""Tests for VLC input-failure detection used during stream failover."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from apps.proxy.live_proxy.input.manager import StreamManager
|
||||
from apps.proxy.live_proxy.services.log_parsers import VLCLogParser
|
||||
|
||||
|
||||
class VLCInputFailureParserTests(TestCase):
|
||||
def test_detects_unable_to_open_mrl(self):
|
||||
parser = VLCLogParser()
|
||||
line = (
|
||||
"main input error: VLC is unable to open the MRL "
|
||||
"'http://example.com/live/1/2/3.ts'. Check the log for details."
|
||||
)
|
||||
self.assertEqual(parser.can_parse(line), "vlc_input_failed")
|
||||
|
||||
def test_ignores_unrelated_vlc_lines(self):
|
||||
parser = VLCLogParser()
|
||||
line = "vlcpulse audio output error: PulseAudio server connection failure"
|
||||
self.assertIsNone(parser.can_parse(line))
|
||||
|
||||
|
||||
class VLCInputFailureHandlingTests(TestCase):
|
||||
def test_stderr_handler_closes_connection_on_input_failure(self):
|
||||
sm = StreamManager.__new__(StreamManager)
|
||||
sm.channel_id = "test-channel"
|
||||
sm.parser_type = "vlc"
|
||||
sm.ffmpeg_input_phase = True
|
||||
sm.bytes_processed = 0
|
||||
sm.connected = True
|
||||
sm.current_stream_id = 1
|
||||
|
||||
with patch.object(sm, "_close_socket") as close_socket:
|
||||
sm._log_stderr_content(
|
||||
"main input error: VLC is unable to open the MRL 'http://x'. "
|
||||
"Check the log for details."
|
||||
)
|
||||
|
||||
self.assertFalse(sm.connected)
|
||||
close_socket.assert_called_once()
|
||||
|
|
@ -21,7 +21,7 @@ def add_vlc_profile(apps, schema_editor):
|
|||
StreamProfile.objects.create(
|
||||
name="VLC",
|
||||
command="cvlc",
|
||||
parameters="-vv -I dummy --no-video-title-show --http-user-agent {userAgent} {streamUrl} --sout #standard{access=file,mux=ts,dst=-}",
|
||||
parameters="-vv -I dummy --no-video-title-show --play-and-exit --http-user-agent {userAgent} {streamUrl} --sout #standard{access=file,mux=ts,dst=-}",
|
||||
is_active=True,
|
||||
user_agent=tivimate_ua,
|
||||
locked=True, # Make it read-only like ffmpeg/streamlink
|
||||
|
|
|
|||
37
core/migrations/0027_vlc_play_and_exit.py
Normal file
37
core/migrations/0027_vlc_play_and_exit.py
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Add --play-and-exit so cvlc exits when the input cannot be opened.
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
_VLC_PARAMETERS = (
|
||||
"-vv -I dummy --no-video-title-show --play-and-exit "
|
||||
"--http-user-agent {userAgent} {streamUrl} "
|
||||
"--sout #standard{access=file,mux=ts,dst=-}"
|
||||
)
|
||||
|
||||
|
||||
def update_vlc_profile(apps, schema_editor):
|
||||
StreamProfile = apps.get_model("core", "StreamProfile")
|
||||
StreamProfile.objects.filter(name="VLC", locked=True).update(
|
||||
parameters=_VLC_PARAMETERS
|
||||
)
|
||||
|
||||
|
||||
def revert_vlc_profile(apps, schema_editor):
|
||||
StreamProfile = apps.get_model("core", "StreamProfile")
|
||||
StreamProfile.objects.filter(name="VLC", locked=True).update(
|
||||
parameters=(
|
||||
"-vv -I dummy --no-video-title-show --http-user-agent {userAgent} "
|
||||
"{streamUrl} --sout #standard{access=file,mux=ts,dst=-}"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("core", "0026_add_channel_client_wait_period"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(update_vlc_profile, revert_vlc_profile),
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue