diff --git a/apps/channels/api_views.py b/apps/channels/api_views.py
index 38cb5bd4..3ffb98af 100644
--- a/apps/channels/api_views.py
+++ b/apps/channels/api_views.py
@@ -432,10 +432,13 @@ class ChannelViewSet(viewsets.ModelViewSet):
"name": name,
"tvg_id": stream.tvg_id,
"tvc_guide_stationid": tvc_guide_stationid,
- "channel_group_id": channel_group.id,
"streams": [stream_id],
}
+ # Only add channel_group_id if the stream has a channel group
+ if channel_group:
+ channel_data["channel_group_id"] = channel_group.id
+
if stream.logo_url:
logo, _ = Logo.objects.get_or_create(
url=stream.logo_url, defaults={"name": stream.name or stream.tvg_id}
@@ -584,9 +587,12 @@ class ChannelViewSet(viewsets.ModelViewSet):
"name": name,
"tvc_guide_stationid": tvc_guide_stationid,
"tvg_id": stream.tvg_id,
- "channel_group_id": channel_group.id,
}
+ # Only add channel_group_id if the stream has a channel group
+ if channel_group:
+ channel_data["channel_group_id"] = channel_group.id
+
# Attempt to find existing EPGs with the same tvg-id
epgs = EPGData.objects.filter(tvg_id=stream.tvg_id)
if epgs:
diff --git a/apps/m3u/tasks.py b/apps/m3u/tasks.py
index ce46a2ec..d6e0755b 100644
--- a/apps/m3u/tasks.py
+++ b/apps/m3u/tasks.py
@@ -172,6 +172,13 @@ def fetch_m3u_lines(account, use_cache=False):
send_m3u_update(account.id, "downloading", 100, status="error", error=error_msg)
return [], False
+def get_case_insensitive_attr(attributes, key, default=""):
+ """Get attribute value using case-insensitive key lookup."""
+ for attr_key, attr_value in attributes.items():
+ if attr_key.lower() == key.lower():
+ return attr_value
+ return default
+
def parse_extinf_line(line: str) -> dict:
"""
Parse an EXTINF line from an M3U file.
@@ -193,7 +200,7 @@ def parse_extinf_line(line: str) -> dict:
attributes_part, display_name = parts[0], parts[1].strip()
attrs = dict(re.findall(r'([^\s]+)=["\']([^"\']+)["\']', attributes_part))
# Use tvg-name attribute if available; otherwise, use the display name.
- name = attrs.get('tvg-name', display_name)
+ name = get_case_insensitive_attr(attrs, 'tvg-name', display_name)
return {
'attributes': attrs,
'display_name': display_name,
@@ -409,8 +416,8 @@ def process_m3u_batch(account_id, batch, groups, hash_keys):
for stream_info in batch:
try:
name, url = stream_info["name"], stream_info["url"]
- tvg_id, tvg_logo = stream_info["attributes"].get("tvg-id", ""), stream_info["attributes"].get("tvg-logo", "")
- group_title = stream_info["attributes"].get("group-title", "Default Group")
+ tvg_id, tvg_logo = get_case_insensitive_attr(stream_info["attributes"], "tvg-id", ""), get_case_insensitive_attr(stream_info["attributes"], "tvg-logo", "")
+ group_title = get_case_insensitive_attr(stream_info["attributes"], "group-title", "Default Group")
# Filter out disabled groups for this account
if group_title not in groups:
@@ -712,8 +719,9 @@ def refresh_m3u_groups(account_id, use_cache=False, full_refresh=False):
extinf_count += 1
parsed = parse_extinf_line(line)
if parsed:
- if "group-title" in parsed["attributes"]:
- group_name = parsed["attributes"]["group-title"]
+ group_title_attr = get_case_insensitive_attr(parsed["attributes"], "group-title", "")
+ if group_title_attr:
+ group_name = group_title_attr
# Log new groups as they're discovered
if group_name not in groups:
logger.debug(f"Found new group for M3U account {account_id}: '{group_name}'")
diff --git a/apps/proxy/config.py b/apps/proxy/config.py
index b00bd224..b369a92f 100644
--- a/apps/proxy/config.py
+++ b/apps/proxy/config.py
@@ -11,6 +11,8 @@ class BaseConfig:
BUFFER_CHUNK_SIZE = 188 * 1361 # ~256KB
# Redis settings
REDIS_CHUNK_TTL = 60 # Number in seconds - Chunks expire after 1 minute
+ BUFFERING_TIMEOUT = 15 # Seconds to wait for buffering before switching streams
+ BUFFER_SPEED = 1 # What speed to condsider the stream buffering, 1x is normal speed, 2x is double speed, etc.
class HLSConfig(BaseConfig):
MIN_SEGMENTS = 12
diff --git a/apps/proxy/ts_proxy/channel_status.py b/apps/proxy/ts_proxy/channel_status.py
index 864ddac8..8f1d0649 100644
--- a/apps/proxy/ts_proxy/channel_status.py
+++ b/apps/proxy/ts_proxy/channel_status.py
@@ -317,6 +317,9 @@ class ChannelStatus:
ffmpeg_bitrate = metadata.get(ChannelMetadataField.FFMPEG_BITRATE.encode('utf-8'))
if ffmpeg_bitrate:
info['ffmpeg_bitrate'] = float(ffmpeg_bitrate.decode('utf-8'))
+ stream_type = metadata.get(ChannelMetadataField.STREAM_TYPE.encode('utf-8'))
+ if stream_type:
+ info['stream_type'] = stream_type.decode('utf-8')
return info
@@ -494,6 +497,12 @@ class ChannelStatus:
audio_codec = metadata.get(ChannelMetadataField.AUDIO_CODEC.encode('utf-8'))
if audio_codec:
info['audio_codec'] = audio_codec.decode('utf-8')
+ audio_channels = metadata.get(ChannelMetadataField.AUDIO_CHANNELS.encode('utf-8'))
+ if audio_channels:
+ info['audio_channels'] = audio_channels.decode('utf-8')
+ stream_type = metadata.get(ChannelMetadataField.STREAM_TYPE.encode('utf-8'))
+ if stream_type:
+ info['stream_type'] = stream_type.decode('utf-8')
return info
except Exception as e:
diff --git a/apps/proxy/ts_proxy/config_helper.py b/apps/proxy/ts_proxy/config_helper.py
index 773ab378..4057a2d5 100644
--- a/apps/proxy/ts_proxy/config_helper.py
+++ b/apps/proxy/ts_proxy/config_helper.py
@@ -85,3 +85,11 @@ class ConfigHelper:
def failover_grace_period():
"""Get extra time (in seconds) to allow for stream switching before disconnecting clients"""
return ConfigHelper.get('FAILOVER_GRACE_PERIOD', 20) # Default to 20 seconds
+ @staticmethod
+ def buffering_timeout():
+ """Get buffering timeout in seconds"""
+ return ConfigHelper.get('BUFFERING_TIMEOUT', 15) # Default to 15 seconds
+ @staticmethod
+ def buffering_speed():
+ """Get buffering speed in bytes per second"""
+ return ConfigHelper.get('BUFFERING_SPEED',1) # Default to 1x
diff --git a/apps/proxy/ts_proxy/constants.py b/apps/proxy/ts_proxy/constants.py
index daaf7bb3..55d6e006 100644
--- a/apps/proxy/ts_proxy/constants.py
+++ b/apps/proxy/ts_proxy/constants.py
@@ -18,6 +18,7 @@ class ChannelState:
ERROR = "error"
STOPPING = "stopping"
STOPPED = "stopped"
+ BUFFERING = "buffering"
# Event types
class EventType:
@@ -85,6 +86,8 @@ class ChannelMetadataField:
AUDIO_CHANNELS = "audio_channels"
AUDIO_BITRATE = "audio_bitrate"
+ # Stream format info
+ STREAM_TYPE = "stream_type"
# Stream info timestamp
STREAM_INFO_UPDATED = "stream_info_updated"
diff --git a/apps/proxy/ts_proxy/services/channel_service.py b/apps/proxy/ts_proxy/services/channel_service.py
index 761d56ac..2bf26364 100644
--- a/apps/proxy/ts_proxy/services/channel_service.py
+++ b/apps/proxy/ts_proxy/services/channel_service.py
@@ -420,7 +420,22 @@ class ChannelService:
def parse_and_store_stream_info(channel_id, stream_info_line, stream_type="video"):
"""Parse FFmpeg stream info line and store in Redis metadata"""
try:
- if stream_type == "video":
+ if stream_type == "input":
+ # Example lines:
+ # Input #0, mpegts, from 'http://example.com/stream.ts':
+ # Input #0, hls, from 'http://example.com/stream.m3u8':
+
+ # Extract input format (e.g., "mpegts", "hls", "flv", etc.)
+ input_match = re.search(r'Input #\d+,\s*([^,]+)', stream_info_line)
+ input_format = input_match.group(1).strip() if input_match else None
+
+ # Store in Redis if we have valid data
+ if input_format:
+ ChannelService._update_stream_info_in_redis(channel_id, None, None, None, None, None, None, None, None, None, None, None, input_format)
+
+ logger.debug(f"Input format info - Format: {input_format} for channel {channel_id}")
+
+ elif stream_type == "video":
# Example line:
# Stream #0:0: Video: h264 (Main), yuv420p(tv, progressive), 1280x720 [SAR 1:1 DAR 16:9], q=2-31, 2000 kb/s, 29.97 fps, 90k tbn
@@ -428,12 +443,17 @@ class ChannelService:
codec_match = re.search(r'Video:\s*([a-zA-Z0-9_]+)', stream_info_line)
video_codec = codec_match.group(1) if codec_match else None
- # Extract resolution (e.g., "1280x720")
- resolution_match = re.search(r'(\d+)x(\d+)', stream_info_line)
+ # Extract resolution (e.g., "1280x720") - be more specific to avoid hex values
+ # Look for resolution patterns that are realistic video dimensions
+ resolution_match = re.search(r'\b(\d{3,5})x(\d{3,5})\b', stream_info_line)
if resolution_match:
width = int(resolution_match.group(1))
height = int(resolution_match.group(2))
- resolution = f"{width}x{height}"
+ # Validate that these look like reasonable video dimensions
+ if 100 <= width <= 10000 and 100 <= height <= 10000:
+ resolution = f"{width}x{height}"
+ else:
+ width = height = resolution = None
else:
width = height = resolution = None
@@ -459,7 +479,7 @@ class ChannelService:
# Store in Redis if we have valid data
if any(x is not None for x in [video_codec, resolution, source_fps, pixel_format, video_bitrate]):
- ChannelService._update_stream_info_in_redis(channel_id, video_codec, resolution, width, height, source_fps, pixel_format, video_bitrate, None, None, None, None)
+ ChannelService._update_stream_info_in_redis(channel_id, video_codec, resolution, width, height, source_fps, pixel_format, video_bitrate, None, None, None, None, None)
logger.info(f"Video stream info - Codec: {video_codec}, Resolution: {resolution}, "
f"Source FPS: {source_fps}, Pixel Format: {pixel_format}, "
@@ -490,7 +510,7 @@ class ChannelService:
# Store in Redis if we have valid data
if any(x is not None for x in [audio_codec, sample_rate, channels, audio_bitrate]):
- ChannelService._update_stream_info_in_redis(channel_id, None, None, None, None, None, None, None, audio_codec, sample_rate, channels, audio_bitrate)
+ ChannelService._update_stream_info_in_redis(channel_id, None, None, None, None, None, None, None, audio_codec, sample_rate, channels, audio_bitrate, None)
logger.info(f"Audio stream info - Codec: {audio_codec}, Sample Rate: {sample_rate} Hz, "
f"Channels: {channels}, Audio Bitrate: {audio_bitrate} kb/s")
@@ -499,7 +519,7 @@ class ChannelService:
logger.debug(f"Error parsing FFmpeg {stream_type} stream info: {e}")
@staticmethod
- def _update_stream_info_in_redis(channel_id, codec, resolution, width, height, fps, pixel_format, video_bitrate, audio_codec=None, sample_rate=None, channels=None, audio_bitrate=None):
+ def _update_stream_info_in_redis(channel_id, codec, resolution, width, height, fps, pixel_format, video_bitrate, audio_codec=None, sample_rate=None, channels=None, audio_bitrate=None, input_format=None):
"""Update stream info in Redis metadata"""
try:
proxy_server = ProxyServer.get_instance()
@@ -545,6 +565,8 @@ class ChannelService:
if audio_bitrate is not None:
update_data[ChannelMetadataField.AUDIO_BITRATE] = str(round(audio_bitrate, 1))
+ if input_format is not None:
+ update_data[ChannelMetadataField.STREAM_TYPE] = str(input_format)
proxy_server.redis_client.hset(metadata_key, mapping=update_data)
return True
diff --git a/apps/proxy/ts_proxy/stream_manager.py b/apps/proxy/ts_proxy/stream_manager.py
index 7f81e29e..f8a7323b 100644
--- a/apps/proxy/ts_proxy/stream_manager.py
+++ b/apps/proxy/ts_proxy/stream_manager.py
@@ -6,8 +6,8 @@ import time
import socket
import requests
import subprocess
-import gevent # Add this import
-import re # Add this import at the top
+import gevent
+import re
from typing import Optional, List
from django.shortcuts import get_object_or_404
from apps.proxy.config import TSConfig as Config
@@ -40,6 +40,10 @@ class StreamManager:
self.url_switching = False
self.url_switch_start_time = 0
self.url_switch_timeout = ConfigHelper.url_switch_timeout()
+ self.buffering = False
+ self.buffering_timeout = ConfigHelper.buffering_timeout()
+ self.buffering_speed = ConfigHelper.buffering_speed()
+ self.buffering_start_time = None
# Store worker_id for ownership checks
self.worker_id = worker_id
@@ -502,6 +506,10 @@ class StreamManager:
elif any(keyword in content_lower for keyword in ['input', 'output', 'stream', 'video', 'audio']):
# Stream info - log at info level
logger.info(f"FFmpeg info: {content}")
+ if content.startswith('Input #0'):
+ # If it's input 0, parse stream info
+ from .services.channel_service import ChannelService
+ ChannelService.parse_and_store_stream_info(self.channel_id, content, "input")
else:
# Everything else at debug level
logger.debug(f"FFmpeg stderr: {content}")
@@ -541,7 +549,6 @@ class StreamManager:
actual_fps = None
if ffmpeg_fps is not None and ffmpeg_speed is not None and ffmpeg_speed > 0:
actual_fps = ffmpeg_fps / ffmpeg_speed
-
# Store in Redis if we have valid data
if any(x is not None for x in [ffmpeg_speed, ffmpeg_fps, actual_fps, ffmpeg_bitrate]):
self._update_ffmpeg_stats_in_redis(ffmpeg_speed, ffmpeg_fps, actual_fps, ffmpeg_bitrate)
@@ -549,10 +556,51 @@ class StreamManager:
# Fix the f-string formatting
actual_fps_str = f"{actual_fps:.1f}" if actual_fps is not None else "N/A"
ffmpeg_bitrate_str = f"{ffmpeg_bitrate:.1f}" if ffmpeg_bitrate is not None else "N/A"
-
+ # Log the stats
logger.debug(f"FFmpeg stats - Speed: {ffmpeg_speed}x, FFmpeg FPS: {ffmpeg_fps}, "
f"Actual FPS: {actual_fps_str}, "
f"Bitrate: {ffmpeg_bitrate_str} kbps")
+ # If we have a valid speed, check for buffering
+ if ffmpeg_speed is not None and ffmpeg_speed < self.buffering_speed:
+ if self.buffering:
+ # Buffering is still ongoing, check for how long
+ if self.buffering_start_time is None:
+ self.buffering_start_time = time.time()
+ else:
+ buffering_duration = time.time() - self.buffering_start_time
+ if buffering_duration > self.buffering_timeout:
+ # Buffering timeout reached, log error and try next stream
+ logger.error(f"Buffering timeout reached for channel {self.channel_id} after {buffering_duration:.1f} seconds")
+ # Send next stream request
+ if self._try_next_stream():
+ logger.info(f"Switched to next stream for channel {self.channel_id} after buffering timeout")
+ # Reset buffering state
+ self.buffering = False
+ self.buffering_start_time = None
+ else:
+ logger.error(f"Failed to switch to next stream for channel {self.channel_id} after buffering timeout")
+ else:
+ # Buffering just started, set the flag and start timer
+ self.buffering = True
+ self.buffering_start_time = time.time()
+ logger.warning(f"Buffering started for channel {self.channel_id} - speed: {ffmpeg_speed}x")
+ # Log buffering warning
+ logger.debug(f"FFmpeg speed on channel {self.channel_id} is below {self.buffering_speed} ({ffmpeg_speed}x) - buffering detected")
+ # Set channel state to buffering
+ if hasattr(self.buffer, 'redis_client') and self.buffer.redis_client:
+ metadata_key = RedisKeys.channel_metadata(self.channel_id)
+ self.buffer.redis_client.hset(metadata_key, ChannelMetadataField.STATE, ChannelState.BUFFERING)
+ elif ffmpeg_speed is not None and ffmpeg_speed >= self.buffering_speed:
+ # Speed is good, check if we were buffering
+ if self.buffering:
+ # Reset buffering state
+ logger.info(f"Buffering ended for channel {self.channel_id} - speed: {ffmpeg_speed}x")
+ self.buffering = False
+ self.buffering_start_time = None
+ # Set channel state to active if speed is good
+ if hasattr(self.buffer, 'redis_client') and self.buffer.redis_client:
+ metadata_key = RedisKeys.channel_metadata(self.channel_id)
+ self.buffer.redis_client.hset(metadata_key, ChannelMetadataField.STATE, ChannelState.ACTIVE)
except Exception as e:
logger.debug(f"Error parsing FFmpeg stats: {e}")
diff --git a/apps/proxy/ts_proxy/url_utils.py b/apps/proxy/ts_proxy/url_utils.py
index 33a87057..a60e0ba3 100644
--- a/apps/proxy/ts_proxy/url_utils.py
+++ b/apps/proxy/ts_proxy/url_utils.py
@@ -171,7 +171,7 @@ def get_stream_info_for_switch(channel_id: str, target_stream_id: Optional[int]
# Get transcode info from the channel's stream profile
stream_profile = channel.get_stream_profile()
transcode = not (stream_profile.is_proxy() or stream_profile is None)
- profile_value = str(stream_profile)
+ profile_value = stream_profile.id
return {
'url': stream_url,
diff --git a/core/api_views.py b/core/api_views.py
index 2f01b503..cc0363f2 100644
--- a/core/api_views.py
+++ b/core/api_views.py
@@ -2,6 +2,7 @@
import json
import ipaddress
+import logging
from rest_framework import viewsets, status
from rest_framework.response import Response
from django.shortcuts import get_object_or_404
@@ -29,6 +30,9 @@ from apps.accounts.permissions import (
from dispatcharr.utils import get_client_ip
+logger = logging.getLogger(__name__)
+
+
class UserAgentViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows user agents to be viewed, created, edited, or deleted.
@@ -137,14 +141,32 @@ def environment(request):
except Exception as e:
local_ip = f"Error: {e}"
- # 3) If we got a valid public_ip, fetch geo info from ipapi.co
+ # 3) If we got a valid public_ip, fetch geo info from ipapi.co or ip-api.com
if public_ip and "Error" not in public_ip:
try:
- geo = requests.get(f"https://ipapi.co/{public_ip}/json/", timeout=5).json()
- # ipapi returns fields like country_code, country_name, etc.
- country_code = geo.get("country_code", "") # e.g. "US"
- country_name = geo.get("country_name", "") # e.g. "United States"
- except requests.RequestException as e:
+ # Attempt to get geo information from ipapi.co first
+ r = requests.get(f"https://ipapi.co/{public_ip}/json/", timeout=5)
+
+ if r.status_code == requests.codes.ok:
+ geo = r.json()
+ country_code = geo.get("country_code") # e.g. "US"
+ country_name = geo.get("country_name") # e.g. "United States"
+
+ else:
+ # If ipapi.co fails, fallback to ip-api.com
+ # only supports http requests for free tier
+ r = requests.get("http://ip-api.com/json/", timeout=5)
+
+ if r.status_code == requests.codes.ok:
+ geo = r.json()
+ country_code = geo.get("countryCode") # e.g. "US"
+ country_name = geo.get("country") # e.g. "United States"
+
+ else:
+ raise Exception("Geo lookup failed with both services")
+
+ except Exception as e:
+ logger.error(f"Error during geo lookup: {e}")
country_code = None
country_name = None
diff --git a/frontend/src/components/FloatingVideo.jsx b/frontend/src/components/FloatingVideo.jsx
index 46c191eb..7f1e1c53 100644
--- a/frontend/src/components/FloatingVideo.jsx
+++ b/frontend/src/components/FloatingVideo.jsx
@@ -73,72 +73,109 @@ export default function FloatingVideo() {
console.log("Attempting to play stream:", streamUrl);
try {
- // If the browser supports MSE for live playback, initialize mpegts.js
- if (mpegts.getFeatureList().mseLivePlayback) {
- // Set loading flag
- setIsLoading(true);
-
- const player = mpegts.createPlayer({
- type: 'mpegts', // MPEG-TS format
- url: streamUrl,
- isLive: true,
- enableWorker: true,
- enableStashBuffer: false, // Try disabling stash buffer for live streams
- liveBufferLatencyChasing: true,
- liveSync: true,
- cors: true, // Enable CORS for cross-domain requests
- // Add error recovery options
- autoCleanupSourceBuffer: true,
- autoCleanupMaxBackwardDuration: 10,
- autoCleanupMinBackwardDuration: 5,
- reuseRedirectedURL: true,
- });
-
- player.attachMediaElement(videoRef.current);
-
- // Add events to track loading state
- player.on(mpegts.Events.LOADING_COMPLETE, () => {
- setIsLoading(false);
- });
-
- player.on(mpegts.Events.METADATA_ARRIVED, () => {
- setIsLoading(false);
- });
-
- // Add error event handler
- player.on(mpegts.Events.ERROR, (errorType, errorDetail) => {
- setIsLoading(false);
-
- // Filter out aborted errors
- if (errorType !== 'NetworkError' || !errorDetail?.includes('aborted')) {
- console.error('Player error:', errorType, errorDetail);
- setLoadError(`Error: ${errorType}${errorDetail ? ` - ${errorDetail}` : ''}`);
- }
- });
-
- player.load();
-
- // Don't auto-play until we've loaded properly
- player.on(mpegts.Events.MEDIA_INFO, () => {
- setIsLoading(false);
- try {
- player.play().catch(e => {
- console.log("Auto-play prevented:", e);
- setLoadError("Auto-play was prevented. Click play to start.");
- });
- } catch (e) {
- console.log("Error during play:", e);
- setLoadError(`Playback error: ${e.message}`);
- }
- });
-
- // Store player instance so we can clean up later
- playerRef.current = player;
+ // Check for MSE support first
+ if (!mpegts.getFeatureList().mseLivePlayback) {
+ setIsLoading(false);
+ setLoadError("Your browser doesn't support live video streaming. Please try Chrome or Edge.");
+ return;
}
+
+ // Check for basic codec support
+ const video = document.createElement('video');
+ const h264Support = video.canPlayType('video/mp4; codecs="avc1.42E01E"');
+ const aacSupport = video.canPlayType('audio/mp4; codecs="mp4a.40.2"');
+
+ console.log("Browser codec support - H264:", h264Support, "AAC:", aacSupport);
+
+ // If the browser supports MSE for live playback, initialize mpegts.js
+ setIsLoading(true);
+
+ const player = mpegts.createPlayer({
+ type: 'mpegts',
+ url: streamUrl,
+ isLive: true,
+ enableWorker: true,
+ enableStashBuffer: false,
+ liveBufferLatencyChasing: true,
+ liveSync: true,
+ cors: true,
+ autoCleanupSourceBuffer: true,
+ autoCleanupMaxBackwardDuration: 10,
+ autoCleanupMinBackwardDuration: 5,
+ reuseRedirectedURL: true,
+ });
+
+ player.attachMediaElement(videoRef.current);
+
+ // Add events to track loading state
+ player.on(mpegts.Events.LOADING_COMPLETE, () => {
+ setIsLoading(false);
+ });
+
+ player.on(mpegts.Events.METADATA_ARRIVED, () => {
+ setIsLoading(false);
+ });
+
+ // Enhanced error event handler with codec-specific messages
+ player.on(mpegts.Events.ERROR, (errorType, errorDetail) => {
+ setIsLoading(false);
+
+ // Filter out aborted errors
+ if (errorType !== 'NetworkError' || !errorDetail?.includes('aborted')) {
+ console.error('Player error:', errorType, errorDetail);
+
+ // Provide specific error messages based on error type
+ let errorMessage = `Error: ${errorType}`;
+
+ if (errorType === 'MediaError') {
+ // Try to determine if it's an audio or video codec issue
+ const errorString = errorDetail?.toLowerCase() || '';
+
+ if (errorString.includes('audio') || errorString.includes('ac3') || errorString.includes('ac-3')) {
+ errorMessage = "Audio codec not supported by your browser. Try Chrome or Edge for better audio codec support.";
+ } else if (errorString.includes('video') || errorString.includes('h264') || errorString.includes('h.264')) {
+ errorMessage = "Video codec not supported by your browser. Try Chrome or Edge for better video codec support.";
+ } else if (errorString.includes('mse')) {
+ errorMessage = "Your browser doesn't support the codecs used in this stream. Try Chrome or Edge for better compatibility.";
+ } else {
+ errorMessage = "Media codec not supported by your browser. This may be due to unsupported audio (AC3) or video codecs. Try Chrome or Edge.";
+ }
+ } else if (errorDetail) {
+ errorMessage += ` - ${errorDetail}`;
+ }
+
+ setLoadError(errorMessage);
+ }
+ });
+
+ player.load();
+
+ // Don't auto-play until we've loaded properly
+ player.on(mpegts.Events.MEDIA_INFO, () => {
+ setIsLoading(false);
+ try {
+ player.play().catch(e => {
+ console.log("Auto-play prevented:", e);
+ setLoadError("Auto-play was prevented. Click play to start.");
+ });
+ } catch (e) {
+ console.log("Error during play:", e);
+ setLoadError(`Playback error: ${e.message}`);
+ }
+ });
+
+ // Store player instance so we can clean up later
+ playerRef.current = player;
} catch (error) {
setIsLoading(false);
- setLoadError(`Initialization error: ${error.message}`);
console.error("Error initializing player:", error);
+
+ // Provide helpful error message based on the error
+ if (error.message?.includes('codec') || error.message?.includes('format')) {
+ setLoadError("Codec not supported by your browser. Please try a different browser (Chrome/Edge recommended).");
+ } else {
+ setLoadError(`Initialization error: ${error.message}`);
+ }
}
// Cleanup when component unmounts or streamUrl changes
@@ -191,7 +228,7 @@ export default function FloatingVideo() {
style={{ width: '100%', height: '180px', backgroundColor: '#000' }}
/>
- {/* Loading overlay */}
+ {/* Loading overlay - only show when loading */}
{isLoading && (
)}
-
- {/* Error message overlay */}
- {!isLoading && loadError && (
-
-
- {loadError}
-
-
- )}
+
+ {/* Error message below video - doesn't block controls */}
+ {!isLoading && loadError && (
+
+
+ {loadError}
+
+
+ )}
);
diff --git a/frontend/src/pages/Stats.jsx b/frontend/src/pages/Stats.jsx
index 150ac66b..ba9829fe 100644
--- a/frontend/src/pages/Stats.jsx
+++ b/frontend/src/pages/Stats.jsx
@@ -570,28 +570,50 @@ const ChannelCard = ({
{/* Add stream information badges */}
- {channel.video_codec && (
-
- {channel.video_codec.toUpperCase()}
-
- )}
{channel.resolution && (
-
- {channel.resolution}
-
+
+
+ {channel.resolution}
+
+
)}
{channel.source_fps && (
-
- {channel.source_fps} FPS
-
+
+
+ {channel.source_fps} FPS
+
+
+ )}
+ {channel.video_codec && (
+
+
+ {channel.video_codec.toUpperCase()}
+
+
)}
{channel.audio_codec && (
-
- {channel.audio_codec.toUpperCase()}
-
+
+
+ {channel.audio_codec.toUpperCase()}
+
+
+ )}
+ {channel.audio_channels && (
+
+
+ {channel.audio_channels}
+
+
+ )}
+ {channel.stream_type && (
+
+
+ {channel.stream_type.toUpperCase()}
+
+
)}
{channel.ffmpeg_speed && (
-
+