Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions backend/stream_check_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,12 @@ def get_stream_info_and_bitrate(url: str, duration: int = 30, timeout: int = 30,
'fps': 0,
'bitrate_kbps': None,
'status': 'OK',
'elapsed_time': 0
'elapsed_time': 0,
'video_profile': None,
'video_bit_depth': None,
'audio_language': None,
'sample_rate': None,
'audio_channels': None,
}

# Add buffer to timeout to account for ffmpeg startup, network latency, and shutdown overhead
Expand Down Expand Up @@ -435,6 +440,25 @@ def get_stream_info_and_bitrate(url: str, duration: int = 30, timeout: int = 30,
if fps_match:
result_data['fps'] = round(float(fps_match.group(1)), 2)
logger.debug(f" → Detected FPS: {result_data['fps']}")

# Extract video profile — e.g. "h264 (High)" or "hevc (Main 10)"
# Skip if it looks like a codec alias (contains / or 0x)
profile_match = re.search(r'Video:\s+\w+\s+\(([^)]+)\)', line)
if profile_match:
candidate = profile_match.group(1).strip()
if '/' not in candidate and '0x' not in candidate:
result_data['video_profile'] = candidate
logger.debug(f" → Detected video profile: {candidate}")

# Extract bit depth from pixel format
# e.g. yuv420p10le → 10, yuv420p12le → 12, yuv420p → 8
depth_match = re.search(r'yuv[j\d]+p(\d{2})', line)
if depth_match:
result_data['video_bit_depth'] = int(depth_match.group(1))
logger.debug(f" → Detected bit depth: {result_data['video_bit_depth']}")
elif re.search(r'yuv[j\d]+p\b', line):
result_data['video_bit_depth'] = 8
logger.debug(f" → Detected bit depth: 8 (standard yuv)")
except (ValueError, AttributeError) as e:
logger.debug(f" → Error parsing video stream line: {e}")

Expand All @@ -454,6 +478,32 @@ def get_stream_info_and_bitrate(url: str, duration: int = 30, timeout: int = 30,
if audio_codec != 'N/A':
result_data['audio_codec'] = audio_codec
logger.debug(f" → Final audio codec: {result_data['audio_codec']}")

# Extract audio language from stream specifier e.g. "Stream #0:1(eng):"
lang_match = re.search(r'Stream #\d+:\d+\((\w+)\):', line)
if lang_match:
lang = lang_match.group(1)
if lang.lower() not in ('und', 'unknown'):
result_data['audio_language'] = lang
logger.debug(f" → Detected audio language: {lang}")

# Extract sample rate e.g. "48000 Hz"
sr_match = re.search(r'(\d+)\s+Hz', line)
if sr_match:
result_data['sample_rate'] = int(sr_match.group(1))
logger.debug(f" → Detected sample rate: {result_data['sample_rate']}")

# Extract channel layout e.g. "stereo", "5.1(side)", "7.1"
# Strip trailing (side)/(back) qualifiers so "5.1(side)" → "5.1"
ch_match = re.search(r'\d+\s+Hz,\s+([^,\s]+)', line)
if ch_match:
layout = ch_match.group(1)
paren = layout.find('(')
if paren > 0:
layout = layout[:paren]
if layout:
result_data['audio_channels'] = layout.strip()
logger.debug(f" → Detected audio channels: {result_data['audio_channels']}")
except (ValueError, AttributeError) as e:
logger.debug(f" → Error parsing audio stream line: {e}")

Expand Down Expand Up @@ -537,6 +587,7 @@ def get_stream_info_and_bitrate(url: str, duration: int = 30, timeout: int = 30,
return result_data



def get_stream_bitrate(url: str, duration: int = 30, timeout: int = 30, user_agent: str = 'VLC/3.0.14', stream_startup_buffer: int = 10) -> Tuple[Optional[float], str, float]:
"""
Get stream bitrate using ffmpeg to analyze actual stream data.
Expand Down Expand Up @@ -762,7 +813,7 @@ def analyze_stream(
stream_startup_buffer=stream_startup_buffer
)

# Build result dictionary with metadata
# Build result dictionary
result = {
'stream_id': stream_id,
'stream_name': stream_name,
Expand All @@ -773,7 +824,12 @@ def analyze_stream(
'resolution': result_data['resolution'],
'fps': result_data['fps'],
'bitrate_kbps': result_data['bitrate_kbps'],
'status': result_data['status']
'status': result_data['status'],
'video_profile': result_data.get('video_profile'),
'video_bit_depth': result_data.get('video_bit_depth'),
'audio_language': result_data.get('audio_language'),
'sample_rate': result_data.get('sample_rate'),
'audio_channels': result_data.get('audio_channels'),
}

# Log results
Expand Down
28 changes: 22 additions & 6 deletions backend/stream_checker_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -1460,15 +1460,23 @@ def _update_stream_stats(self, stream_data: Dict) -> bool:
"video_codec": stream_data.get("video_codec"),
"audio_codec": stream_data.get("audio_codec"),
"ffmpeg_output_bitrate": int(stream_data.get("bitrate_kbps")) if stream_data.get("bitrate_kbps") not in ["N/A", None] else None,
# Extended stream metadata
"video_profile": stream_data.get("video_profile"),
"video_level": stream_data.get("video_level"),
"video_bit_depth": stream_data.get("video_bit_depth"),
"video_ref_frames": stream_data.get("video_ref_frames"),
"audio_language": stream_data.get("audio_language"),
"sample_rate": stream_data.get("sample_rate"),
"audio_channels": stream_data.get("audio_channels"),
}

# Clean up the payload, removing any None values or N/A values
stream_stats_payload = {k: v for k, v in stream_stats_payload.items() if v not in [None, "N/A"]}

if not stream_stats_payload:
logger.debug(f"No data to update for stream {stream_id}. Skipping.")
return False

# Construct the URL for the specific stream
stream_url = f"{base_url}/api/channels/streams/{int(stream_id)}/"

Expand Down Expand Up @@ -1535,15 +1543,23 @@ def _prepare_stream_stats_for_batch(self, stream_data: Dict) -> Optional[Dict[st
"video_codec": stream_data.get("video_codec"),
"audio_codec": stream_data.get("audio_codec"),
"ffmpeg_output_bitrate": int(stream_data.get("bitrate_kbps")) if stream_data.get("bitrate_kbps") not in ["N/A", None] else None,
# Extended stream metadata
"video_profile": stream_data.get("video_profile"),
"video_level": stream_data.get("video_level"),
"video_bit_depth": stream_data.get("video_bit_depth"),
"video_ref_frames": stream_data.get("video_ref_frames"),
"audio_language": stream_data.get("audio_language"),
"sample_rate": stream_data.get("sample_rate"),
"audio_channels": stream_data.get("audio_channels"),
}

# Clean up the payload, removing any None values or N/A values
stream_stats_payload = {k: v for k, v in stream_stats_payload.items() if v not in [None, "N/A"]}

if not stream_stats_payload:
logger.debug(f"No data to update for stream {stream_id}. Skipping.")
return None

return {
'stream_id': stream_id,
'stream_stats': stream_stats_payload
Expand Down