From 9b5769747af4a924a6446b4c5eb0551f32446e0e Mon Sep 17 00:00:00 2001 From: byte <byteturtle@byteturtle.eu> Date: Fri, 7 Mar 2025 16:42:18 +0000 Subject: [PATCH] initial commit --- README.md | 1 + stream.py | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 README.md create mode 100644 stream.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..bc9c1c9 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +currently only supports a ``--debug``` flag diff --git a/stream.py b/stream.py new file mode 100644 index 0000000..9374059 --- /dev/null +++ b/stream.py @@ -0,0 +1,222 @@ +from flask import Flask, Response, request +import subprocess +import os +import argparse +import time +import threading + +app = Flask(__name__) + +BASE_STORAGE_PATH = "/vPath" # Root directory for MKV files +DEBUG_MODE = True # Default: Debugging on + + +@app.route('/convert/vpath/<path:filepath>.webm') +def convert(filepath): + # Log the incoming request + if DEBUG_MODE: + print(f"[DEBUG] Received request: {request.url}") + print(f"[DEBUG] Mapped file path: {os.path.join(BASE_STORAGE_PATH, filepath + '.mkv')}") + + mkv_file = os.path.join(BASE_STORAGE_PATH, filepath + ".mkv") + + # Check if the file exists before proceeding + if not os.path.isfile(mkv_file): + if DEBUG_MODE: + print(f"[ERROR] File not found: {mkv_file}") + return "File not found", 404 + + def generate(): + # Use ffprobe to get the audio codec info + probe_cmd = ["ffprobe", "-v", "error", "-select_streams", "a:0", "-show_entries", "stream=codec_name", "-of", "default=nw=1:nk=1", mkv_file] + try: + audio_codec_info = subprocess.check_output(probe_cmd, stderr=subprocess.PIPE).decode('utf-8').strip() + if DEBUG_MODE: + print(f"[DEBUG] Audio codec: {audio_codec_info}") + except subprocess.CalledProcessError as e: + if DEBUG_MODE: + print(f"[ERROR] Audio probe failed: {e}") + audio_codec_info = "unknown" + + # Use ffprobe to get the video information + probe_cmd = ["ffprobe", "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=codec_name", "-of", "default=nw=1:nk=1", mkv_file] + try: + video_codec_info = subprocess.check_output(probe_cmd, stderr=subprocess.PIPE).decode('utf-8').strip() + if DEBUG_MODE: + print(f"[DEBUG] Video codec: {video_codec_info}") + except subprocess.CalledProcessError as e: + if DEBUG_MODE: + print(f"[ERROR] Video probe failed: {e}") + video_codec_info = "unknown" + + # Get available encoders in this FFmpeg installation + available_encoders = [] + try: + encoders_output = subprocess.check_output(["ffmpeg", "-encoders"], stderr=subprocess.PIPE).decode('utf-8') + if DEBUG_MODE: + print("[DEBUG] Checking available encoders") + for line in encoders_output.split('\n'): + if ' V' in line or ' A' in line: # Video or Audio encoder + parts = line.split() + if len(parts) > 2: + available_encoders.append(parts[1]) + if DEBUG_MODE: + print(f"[DEBUG] Found encoders: {', '.join(available_encoders)}") + except Exception as e: + if DEBUG_MODE: + print(f"[ERROR] Failed to get encoder list: {str(e)}") + + # Build FFmpeg command with fallback options + ffmpeg_cmd = ["ffmpeg", "-i", mkv_file] + + # Add video options based on codec and available encoders + if video_codec_info in ["vp8", "vp9"]: + ffmpeg_cmd.extend(["-c:v", "copy"]) + elif "libvpx" in available_encoders: + ffmpeg_cmd.extend(["-c:v", "libvpx", "-b:v", "1M"]) + elif "vp8" in available_encoders: + ffmpeg_cmd.extend(["-c:v", "vp8", "-b:v", "1M"]) + else: + # Last resort fallback + ffmpeg_cmd.extend(["-c:v", "libx264", "-b:v", "1M"]) + + # Add audio options based on codec and available encoders + if audio_codec_info in ["opus", "vorbis"]: + ffmpeg_cmd.extend(["-c:a", "copy"]) + elif "libvorbis" in available_encoders: + ffmpeg_cmd.extend(["-c:a", "libvorbis", "-b:a", "128k"]) + elif "vorbis" in available_encoders: + ffmpeg_cmd.extend(["-c:a", "vorbis", "-b:a", "128k"]) + elif "aac" in available_encoders: + ffmpeg_cmd.extend(["-c:a", "aac", "-b:a", "128k"]) + else: + # Try to copy audio as a last resort + ffmpeg_cmd.extend(["-c:a", "copy"]) + + # Add output format and destination - simplified with just essential options + ffmpeg_cmd.extend([ + "-f", "webm", + "-y", # Overwrite output without asking + "pipe:1" + ]) + + if DEBUG_MODE: + print(f"[DEBUG] Executing command: {' '.join(ffmpeg_cmd)}") + + # Start FFmpeg process + process = subprocess.Popen( + ffmpeg_cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + bufsize=1024*1024 # 1MB buffer + ) + + # Create a flag to track if we're actually getting video data + received_data = False + error_output = [] + + # Create a thread to capture and log stderr + def log_stderr(): + for line in iter(process.stderr.readline, b''): + line_text = line.decode('utf-8', errors='replace').strip() + error_output.append(line_text) + if DEBUG_MODE: + print(f"[FFMPEG] {line_text}") + + stderr_thread = threading.Thread(target=log_stderr) + stderr_thread.daemon = True + stderr_thread.start() + + try: + # Stream output to client + start_time = time.time() + byte_count = 0 + + while True: + chunk = process.stdout.read(65536) # 64KB chunk size + + # Check if we got any data + if chunk: + byte_count += len(chunk) + received_data = True + if byte_count == len(chunk): # First chunk + if DEBUG_MODE: + print(f"[DEBUG] First chunk received after {time.time() - start_time:.2f} seconds, size: {len(chunk)} bytes") + yield chunk + else: + # Check if process has ended + if process.poll() is not None: + if DEBUG_MODE: + print(f"[DEBUG] FFmpeg process completed with code: {process.returncode}") + print(f"[DEBUG] Total bytes streamed: {byte_count}") + + # If we never got any data and process exited with error + if not received_data and process.returncode != 0: + if DEBUG_MODE: + print("[ERROR] FFmpeg failed to produce any output") + if error_output: + print(f"[ERROR] Last errors: {error_output[-10:]}") + break + + # Short wait before trying again + time.sleep(0.1) + + # Safety timeout - if no data after 30 seconds, abort + if not received_data and (time.time() - start_time) > 30: + if DEBUG_MODE: + print("[ERROR] Timed out waiting for FFmpeg output") + break + + except Exception as e: + if DEBUG_MODE: + print(f"[ERROR] Streaming exception: {str(e)}") + + finally: + try: + if process.poll() is None: + process.terminate() + time.sleep(0.5) + if process.poll() is None: + process.kill() + + # Wait for stderr thread to finish + stderr_thread.join(timeout=1.0) + + except Exception as e: + if DEBUG_MODE: + print(f"[ERROR] Process cleanup error: {str(e)}") + + if DEBUG_MODE: + print(f"[DEBUG] Streaming ended for: {mkv_file}") + + # Set proper headers for streaming + headers = { + 'Content-Type': 'video/webm', + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'X-Content-Type-Options': 'nosniff' + } + + return Response( + generate(), + headers=headers, + direct_passthrough=True + ) + + +@app.route('/test') +def test(): + """Simple test endpoint to verify the server is running""" + return "Server is running" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Run Flask video streaming server") + parser.add_argument("--debug", action="store_true", help="Enable debug mode") + args = parser.parse_args() + + if args.debug: + DEBUG_MODE = True + print("[DEBUG] Debug mode enabled") + + app.run(host="0.0.0.0", port=8080, debug=DEBUG_MODE, threaded=True)