Enhancement: Adds support for UDP streams. Closes #617

This commit is contained in:
SergeantPanda 2025-11-11 18:30:59 -06:00
parent a7f449f746
commit b9e819e343
6 changed files with 54 additions and 18 deletions

View file

@ -29,6 +29,7 @@ from core.models import CoreSettings, UserAgent
from asgiref.sync import async_to_sync
from core.xtream_codes import Client as XCClient
from core.utils import send_websocket_update
from .utils import normalize_stream_url
logger = logging.getLogger(__name__)
@ -219,10 +220,10 @@ def fetch_m3u_lines(account, use_cache=False):
# Has HTTP URLs, might be a simple M3U without headers
is_valid_m3u = True
logger.info("Content validated as M3U: contains HTTP URLs")
elif any(line.strip().startswith('rtsp') for line in content_lines):
# Has HTTP URLs, might be a simple M3U without headers
elif any(line.strip().startswith(('rtsp', 'rtp', 'udp')) for line in content_lines):
# Has RTSP/RTP/UDP URLs, might be a simple M3U without headers
is_valid_m3u = True
logger.info("Content validated as M3U: contains RTSP URLs")
logger.info("Content validated as M3U: contains RTSP/RTP/UDP URLs")
if not is_valid_m3u:
# Log what we actually received for debugging
@ -1403,10 +1404,12 @@ def refresh_m3u_groups(account_id, use_cache=False, full_refresh=False):
)
problematic_lines.append((line_index + 1, line[:200]))
elif extinf_data and (line.startswith("http") or line.startswith("rtsp")):
elif extinf_data and (line.startswith("http") or line.startswith("rtsp") or line.startswith("rtp") or line.startswith("udp")):
url_count += 1
# Normalize UDP URLs only (e.g., remove VLC-specific @ prefix)
normalized_url = normalize_stream_url(line) if line.startswith("udp") else line
# Associate URL with the last EXTINF line
extinf_data[-1]["url"] = line
extinf_data[-1]["url"] = normalized_url
valid_stream_count += 1
# Periodically log progress for large files

View file

@ -8,6 +8,34 @@ lock = threading.Lock()
active_streams_map = {}
logger = logging.getLogger(__name__)
def normalize_stream_url(url):
"""
Normalize stream URLs for compatibility with FFmpeg.
Handles VLC-specific syntax like udp://@239.0.0.1:1234 by removing the @ symbol.
FFmpeg doesn't recognize the @ prefix for multicast addresses.
Args:
url (str): The stream URL to normalize
Returns:
str: The normalized URL
"""
if not url:
return url
# Handle VLC-style UDP multicast URLs: udp://@239.0.0.1:1234 -> udp://239.0.0.1:1234
# The @ symbol in VLC means "listen on all interfaces" but FFmpeg doesn't use this syntax
if url.startswith('udp://@'):
normalized = url.replace('udp://@', 'udp://', 1)
logger.debug(f"Normalized VLC-style UDP URL: {url} -> {normalized}")
return normalized
# Could add other normalizations here in the future (rtp://@, etc.)
return url
def increment_stream_count(account):
with lock:
current_usage = active_streams_map.get(account.id, 0)

View file

@ -34,6 +34,7 @@ class EventType:
class StreamType:
HLS = "hls"
RTSP = "rtsp"
UDP = "udp"
TS = "ts"
UNKNOWN = "unknown"

View file

@ -228,11 +228,11 @@ class StreamManager:
# Check stream type before connecting
stream_type = detect_stream_type(self.url)
if self.transcode == False and stream_type in (StreamType.HLS, StreamType.RTSP):
stream_type_name = "HLS" if stream_type == StreamType.HLS else "RTSP/RTP"
if self.transcode == False and stream_type in (StreamType.HLS, StreamType.RTSP, StreamType.UDP):
stream_type_name = "HLS" if stream_type == StreamType.HLS else ("RTSP/RTP" if stream_type == StreamType.RTSP else "UDP")
logger.info(f"Detected {stream_type_name} stream: {self.url} for channel {self.channel_id}")
logger.info(f"{stream_type_name} streams require FFmpeg for channel {self.channel_id}")
# Enable transcoding for HLS and RTSP/RTP streams
# Enable transcoding for HLS, RTSP/RTP, and UDP streams
self.transcode = True
# We'll override the stream profile selection with ffmpeg in the transcoding section
self.force_ffmpeg = True
@ -421,7 +421,7 @@ class StreamManager:
from core.models import StreamProfile
try:
stream_profile = StreamProfile.objects.get(name='ffmpeg', locked=True)
logger.info("Using FFmpeg stream profile for HLS content")
logger.info("Using FFmpeg stream profile for unsupported proxy content (HLS/RTSP/UDP)")
except StreamProfile.DoesNotExist:
# Fall back to channel's profile if FFmpeg not found
stream_profile = channel.get_stream_profile()
@ -949,10 +949,10 @@ class StreamManager:
logger.debug(f"Updated m3u profile for channel {self.channel_id} to use profile from stream {stream_id}")
else:
logger.warning(f"Failed to update stream profile for channel {self.channel_id}")
except Exception as e:
logger.error(f"Error updating stream profile for channel {self.channel_id}: {e}")
finally:
# Always close database connection after profile update
try:

View file

@ -7,20 +7,24 @@ logger = logging.getLogger("ts_proxy")
def detect_stream_type(url):
"""
Detect if stream URL is HLS, RTSP/RTP, or TS format.
Detect if stream URL is HLS, RTSP/RTP, UDP, or TS format.
Args:
url (str): The stream URL to analyze
Returns:
str: 'hls', 'rtsp', or 'ts' depending on detected format
str: 'hls', 'rtsp', 'udp', or 'ts' depending on detected format
"""
if not url:
return 'unknown'
url_lower = url.lower()
# Check for RTSP/RTP streams first (requires FFmpeg)
# Check for UDP streams (requires FFmpeg)
if url_lower.startswith('udp://'):
return 'udp'
# Check for RTSP/RTP streams (requires FFmpeg)
if url_lower.startswith('rtsp://') or url_lower.startswith('rtp://'):
return 'rtsp'

View file

@ -377,13 +377,13 @@ def validate_flexible_url(value):
import re
# More flexible pattern for non-FQDN hostnames with paths
# Matches: http://hostname, https://hostname/, http://hostname:port/path/to/file.xml, rtp://192.168.2.1, rtsp://192.168.178.1
# Also matches FQDNs for rtsp/rtp protocols: rtsp://FQDN/path?query=value
non_fqdn_pattern = r'^(rts?p|https?)://([a-zA-Z0-9]([a-zA-Z0-9\-\.]{0,61}[a-zA-Z0-9])?|[0-9.]+)?(\:[0-9]+)?(/[^\s]*)?$'
# Matches: http://hostname, https://hostname/, http://hostname:port/path/to/file.xml, rtp://192.168.2.1, rtsp://192.168.178.1, udp://239.0.0.1:1234
# Also matches FQDNs for rtsp/rtp/udp protocols: rtsp://FQDN/path?query=value
non_fqdn_pattern = r'^(rts?p|https?|udp)://([a-zA-Z0-9]([a-zA-Z0-9\-\.]{0,61}[a-zA-Z0-9])?|[0-9.]+)?(\:[0-9]+)?(/[^\s]*)?$'
non_fqdn_match = re.match(non_fqdn_pattern, value)
if non_fqdn_match:
return # Accept non-FQDN hostnames and rtsp/rtp URLs
return # Accept non-FQDN hostnames and rtsp/rtp/udp URLs
# If it doesn't match our flexible patterns, raise the original error
raise ValidationError("Enter a valid URL.")