Files
4h_tool/stream_m3u_to_rtmp.py
T

256 lines
8.0 KiB
Python

#!/usr/bin/env python3
"""
Stream M3U playlist files to RTMP server using ffmpeg without re-encoding.
"""
import subprocess
import sys
import os
import argparse
def parse_m3u_playlist(m3u_path):
"""
Parse M3U playlist file and extract video file paths.
Args:
m3u_path: Path to M3U playlist file
Returns:
List of video file paths
"""
videos = []
try:
with open(m3u_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
current_name = None
for line in lines:
line = line.strip()
if not line or line.startswith('#EXTM3U'):
continue
if line.startswith('#EXTINF'):
# Extract name from #EXTINF line: #EXTINF:-1,Video Name
parts = line.split(',', 1)
if len(parts) > 1:
current_name = parts[1].strip()
else:
current_name = None
elif line and not line.startswith('#'):
# This is a file path or URL
video_path = line.strip()
# Check if it's a URL (http, https, rtmp, etc.)
is_url = (video_path.startswith('http://') or
video_path.startswith('https://') or
video_path.startswith('rtmp://') or
video_path.startswith('rtsp://') or
video_path.startswith('udp://') or
video_path.startswith('tcp://'))
# Only resolve relative paths for local files, not URLs
if not is_url and not os.path.isabs(video_path):
m3u_dir = os.path.dirname(os.path.abspath(m3u_path))
video_path = os.path.join(m3u_dir, video_path)
video_path = os.path.normpath(video_path)
# For URLs, use the URL as-is. For local files, check existence later
videos.append({
'name': current_name or (os.path.basename(video_path) if not is_url else video_path),
'path': video_path,
'is_url': is_url
})
current_name = None
except Exception as e:
print(f"Error parsing M3U file: {e}")
return []
return videos
def stream_video_to_rtmp(video_path, rtmp_url, ffmpeg_path='ffmpeg', loop=False, is_url=False):
"""
Stream a single video file or URL to RTMP server without re-encoding.
Args:
video_path: Path to video file or URL
rtmp_url: RTMP server URL
ffmpeg_path: Path to ffmpeg executable
loop: Whether to loop the video
is_url: Whether video_path is a URL (not a local file)
Returns:
True if successful, False otherwise
"""
# Only check file existence for local files, not URLs
if not is_url and not os.path.exists(video_path):
print(f"Error: Video file not found: {video_path}")
return False
ffmpeg_cmd = [
ffmpeg_path,
'-re', # Read input at native frame rate
'-i', video_path,
'-c', 'copy', # Copy all streams without re-encoding
'-f', 'flv', # RTMP requires FLV format
'-flvflags', 'no_duration_filesize',
rtmp_url
]
if loop:
ffmpeg_cmd.insert(2, '-stream_loop')
ffmpeg_cmd.insert(3, '-1')
try:
process = subprocess.Popen(
ffmpeg_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
bufsize=1
)
# Print ffmpeg output in real-time
def print_output(pipe):
for line in iter(pipe.readline, ''):
if line:
print(line.strip())
import threading
stdout_thread = threading.Thread(target=print_output, args=(process.stdout,))
stderr_thread = threading.Thread(target=print_output, args=(process.stderr,))
stdout_thread.daemon = True
stderr_thread.daemon = True
stdout_thread.start()
stderr_thread.start()
process.wait()
if process.returncode != 0:
print(f"\nError: FFmpeg exited with code {process.returncode}")
return False
return True
except KeyboardInterrupt:
print("\n\nStopping stream...")
if process:
process.terminate()
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
process.kill()
print("Stream stopped")
return False
except Exception as e:
print(f"Error: {e}")
return False
def stream_m3u_to_rtmp(m3u_path, rtmp_url, ffmpeg_path='ffmpeg', loop_playlist=False, loop_video=False):
"""
Stream M3U playlist to RTMP server without re-encoding.
Args:
m3u_path: Path to M3U playlist file
rtmp_url: RTMP server URL (e.g., rtmp://server/live/stream_key)
ffmpeg_path: Path to ffmpeg executable (default: 'ffmpeg')
loop_playlist: Loop through the entire playlist when finished
loop_video: Loop each individual video
"""
# Convert to absolute path
m3u_path = os.path.abspath(m3u_path)
if not os.path.exists(m3u_path):
print(f"Error: M3U playlist file not found: {m3u_path}")
return False
print(f"Parsing M3U playlist: {m3u_path}")
videos = parse_m3u_playlist(m3u_path)
if not videos:
print("Error: No videos found in M3U playlist")
return False
print(f"Found {len(videos)} video(s) in playlist:")
for i, video in enumerate(videos, 1):
print(f" {i}. {video['name']} ({video['path']})")
print(f"\nStreaming to: {rtmp_url}")
print("Press Ctrl+C to stop streaming...\n")
try:
while True:
for i, video in enumerate(videos, 1):
print(f"\n{'='*60}")
print(f"Streaming video {i}/{len(videos)}: {video['name']}")
print(f"Source: {video['path']}")
print(f"{'='*60}\n")
success = stream_video_to_rtmp(
video['path'],
rtmp_url,
ffmpeg_path,
loop_video,
video.get('is_url', False)
)
if not success:
print(f"Failed to stream: {video['name']}")
if not loop_playlist:
return False
continue
if not loop_playlist:
print("\nPlaylist finished streaming")
return True
print("\nRestarting playlist...")
except KeyboardInterrupt:
print("\n\nStreaming stopped by user")
return False
except Exception as e:
print(f"Error: {e}")
return False
def main():
parser = argparse.ArgumentParser(
description='Stream M3U playlist files to RTMP server without re-encoding'
)
parser.add_argument(
'm3u_file',
help='Path to M3U playlist file'
)
parser.add_argument(
'rtmp_url',
help='RTMP server URL (e.g., rtmp://server/live/stream_key)'
)
parser.add_argument(
'--ffmpeg',
default='ffmpeg',
help='Path to ffmpeg executable (default: ffmpeg)'
)
parser.add_argument(
'--loop-playlist',
action='store_true',
help='Loop through the entire playlist when finished'
)
parser.add_argument(
'--loop-video',
action='store_true',
help='Loop each individual video file'
)
args = parser.parse_args()
success = stream_m3u_to_rtmp(
args.m3u_file,
args.rtmp_url,
args.ffmpeg,
args.loop_playlist,
args.loop_video
)
sys.exit(0 if success else 1)
if __name__ == '__main__':
main()