Store video and audio information in redis.

This commit is contained in:
SergeantPanda 2025-06-09 18:57:36 -05:00
parent 7d0c32ef3f
commit 7e25be0717
2 changed files with 163 additions and 4 deletions

View file

@ -70,6 +70,24 @@ class ChannelMetadataField:
FFMPEG_BITRATE = "ffmpeg_bitrate"
FFMPEG_STATS_UPDATED = "ffmpeg_stats_updated"
# Video stream info
VIDEO_CODEC = "video_codec"
RESOLUTION = "resolution"
WIDTH = "width"
HEIGHT = "height"
SOURCE_FPS = "source_fps"
PIXEL_FORMAT = "pixel_format"
VIDEO_BITRATE = "video_bitrate"
# Audio stream info
AUDIO_CODEC = "audio_codec"
SAMPLE_RATE = "sample_rate"
AUDIO_CHANNELS = "audio_channels"
AUDIO_BITRATE = "audio_bitrate"
# Stream info timestamp
STREAM_INFO_UPDATED = "stream_info_updated"
# Client metadata fields
CONNECTED_AT = "connected_at"
LAST_ACTIVE = "last_active"

View file

@ -483,14 +483,21 @@ class StreamManager:
# Convert to lowercase for easier matching
content_lower = content.lower()
# Check for stream info lines first
if "stream #" in content_lower and ("video:" in content_lower or "audio:" in content_lower):
if "video:" in content_lower:
self._parse_ffmpeg_stream_info(content, stream_type="video")
elif "audio:" in content_lower:
self._parse_ffmpeg_stream_info(content, stream_type="audio")
# Determine log level based on content
if any(keyword in content_lower for keyword in ['error', 'failed', 'cannot', 'invalid', 'corrupt']):
logger.error(f"FFmpeg stderr: {content}")
elif any(keyword in content_lower for keyword in ['warning', 'deprecated', 'ignoring']):
logger.warning(f"FFmpeg stderr: {content}")
elif content.startswith('frame=') or 'fps=' in content or 'speed=' in content:
# Stats lines - log at debug level to avoid spam
logger.debug(f"FFmpeg stats: {content}")
# Stats lines - log at trace level to avoid spam
logger.trace(f"FFmpeg stats: {content}")
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}")
@ -501,6 +508,136 @@ class StreamManager:
except Exception as e:
logger.error(f"Error logging stderr content: {e}")
def _parse_ffmpeg_stream_info(self, stream_info_line, stream_type="video"):
"""Parse FFmpeg stream info line to extract video/audio codec, resolution, and FPS"""
try:
if 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
# Extract video codec (e.g., "h264", "mpeg2video", etc.)
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)
if resolution_match:
width = int(resolution_match.group(1))
height = int(resolution_match.group(2))
resolution = f"{width}x{height}"
else:
width = height = resolution = None
# Extract source FPS (e.g., "29.97 fps")
fps_match = re.search(r'(\d+(?:\.\d+)?)\s*fps', stream_info_line)
source_fps = float(fps_match.group(1)) if fps_match else None
# Extract pixel format (e.g., "yuv420p")
pixel_format_match = re.search(r'Video:\s*[^,]+,\s*([^,(]+)', stream_info_line)
pixel_format = None
if pixel_format_match:
pf = pixel_format_match.group(1).strip()
# Clean up pixel format (remove extra info in parentheses)
if '(' in pf:
pf = pf.split('(')[0].strip()
pixel_format = pf
# Extract bitrate if present (e.g., "2000 kb/s")
video_bitrate = None
bitrate_match = re.search(r'(\d+(?:\.\d+)?)\s*kb/s', stream_info_line)
if bitrate_match:
video_bitrate = float(bitrate_match.group(1))
# 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]):
self._update_stream_info_in_redis(video_codec, resolution, width, height, source_fps, pixel_format, video_bitrate, None, None, None)
logger.info(f"Video stream info - Codec: {video_codec}, Resolution: {resolution}, "
f"Source FPS: {source_fps}, Pixel Format: {pixel_format}, "
f"Video Bitrate: {video_bitrate} kb/s")
elif stream_type == "audio":
# Example line:
# Stream #0:1[0x101]: Audio: aac (LC) ([15][0][0][0] / 0x000F), 48000 Hz, stereo, fltp, 64 kb/s
# Extract audio codec (e.g., "aac", "mp3", etc.)
codec_match = re.search(r'Audio:\s*([a-zA-Z0-9_]+)', stream_info_line)
audio_codec = codec_match.group(1) if codec_match else None
# Extract sample rate (e.g., "48000 Hz")
sample_rate_match = re.search(r'(\d+)\s*Hz', stream_info_line)
sample_rate = int(sample_rate_match.group(1)) if sample_rate_match else None
# Extract channel layout (e.g., "stereo", "5.1", "mono")
# Look for common channel layouts
channel_match = re.search(r'\b(mono|stereo|5\.1|7\.1|quad|2\.1)\b', stream_info_line, re.IGNORECASE)
channels = channel_match.group(1) if channel_match else None
# Extract audio bitrate if present (e.g., "64 kb/s")
audio_bitrate = None
bitrate_match = re.search(r'(\d+(?:\.\d+)?)\s*kb/s', stream_info_line)
if bitrate_match:
audio_bitrate = float(bitrate_match.group(1))
# Store in Redis if we have valid data
if any(x is not None for x in [audio_codec, sample_rate, channels, audio_bitrate]):
self._update_stream_info_in_redis(None, None, None, None, None, None, None, audio_codec, sample_rate, channels, audio_bitrate)
logger.info(f"Audio stream info - Codec: {audio_codec}, Sample Rate: {sample_rate} Hz, "
f"Channels: {channels}, Audio Bitrate: {audio_bitrate} kb/s")
except Exception as e:
logger.debug(f"Error parsing FFmpeg {stream_type} stream info: {e}")
def _update_stream_info_in_redis(self, codec, resolution, width, height, fps, pixel_format, video_bitrate, audio_codec=None, sample_rate=None, channels=None, audio_bitrate=None):
"""Update stream info in Redis metadata"""
try:
if hasattr(self.buffer, 'redis_client') and self.buffer.redis_client:
metadata_key = RedisKeys.channel_metadata(self.channel_id)
update_data = {
ChannelMetadataField.STREAM_INFO_UPDATED: str(time.time())
}
# Video info
if codec is not None:
update_data[ChannelMetadataField.VIDEO_CODEC] = str(codec)
if resolution is not None:
update_data[ChannelMetadataField.RESOLUTION] = str(resolution)
if width is not None:
update_data[ChannelMetadataField.WIDTH] = str(width)
if height is not None:
update_data[ChannelMetadataField.HEIGHT] = str(height)
if fps is not None:
update_data[ChannelMetadataField.SOURCE_FPS] = str(round(fps, 2))
if pixel_format is not None:
update_data[ChannelMetadataField.PIXEL_FORMAT] = str(pixel_format)
if video_bitrate is not None:
update_data[ChannelMetadataField.VIDEO_BITRATE] = str(round(video_bitrate, 1))
# Audio info
if audio_codec is not None:
update_data[ChannelMetadataField.AUDIO_CODEC] = str(audio_codec)
if sample_rate is not None:
update_data[ChannelMetadataField.SAMPLE_RATE] = str(sample_rate)
if channels is not None:
update_data[ChannelMetadataField.AUDIO_CHANNELS] = str(channels)
if audio_bitrate is not None:
update_data[ChannelMetadataField.AUDIO_BITRATE] = str(round(audio_bitrate, 1))
self.buffer.redis_client.hset(metadata_key, mapping=update_data)
except Exception as e:
logger.error(f"Error updating stream info in Redis: {e}")
def _parse_ffmpeg_stats(self, stats_line):
"""Parse FFmpeg stats line and extract speed, fps, and bitrate"""
try:
@ -538,9 +675,13 @@ class StreamManager:
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)
# 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"
logger.debug(f"FFmpeg stats - Speed: {ffmpeg_speed}x, FFmpeg FPS: {ffmpeg_fps}, "
f"Actual FPS: {actual_fps:.1f if actual_fps else 'N/A'}, "
f"Bitrate: {ffmpeg_bitrate:.1f if ffmpeg_bitrate else 'N/A'} kbps")
f"Actual FPS: {actual_fps_str}, "
f"Bitrate: {ffmpeg_bitrate_str} kbps")
except Exception as e:
logger.debug(f"Error parsing FFmpeg stats: {e}")