#!/usr/bin/env python3 import sys import os import subprocess import urllib.request import urllib.parse import mimetypes import http.server import threading import time RENDERER_IP = os.environ.get("RENDERER_IP", "192.168.1.107") RENDERER_PORT = int(os.environ.get("RENDERER_PORT", "54321")) LOCAL_STREAM_PORT = 8201 def get_local_ip(target_ip): try: import socket s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) s.connect((target_ip, 80)) ip = s.getsockname()[0] s.close() return ip except Exception: return "192.168.1.100" LOCAL_IP = get_local_ip(RENDERER_IP) CONTROL_URL = f"http://{RENDERER_IP}:{RENDERER_PORT}/_urn:schemas-upnp-org:service:AVTransport_control" def send_soap(action, body_xml): soap = f""" {body_xml} """ headers = { "SOAPAction": f"\"urn:schemas-upnp-org:service:AVTransport:1#{action}\"", "Content-Type": "text/xml; charset=\"utf-8\"" } req = urllib.request.Request(CONTROL_URL, data=soap.encode("utf-8"), headers=headers) try: with urllib.request.urlopen(req, timeout=5) as resp: return resp.read().decode("utf-8") except Exception as e: print(f"Error executing {action}: {e}") return None def cast_url(url): print(f"Casting to TESLA TE-300 ({RENDERER_IP})...") print(f"Media URL: {url}") set_xml = f"0{url}" send_soap("SetAVTransportURI", set_xml) play_xml = "01" send_soap("Play", play_xml) print("Playback started on TV!") def stop(): print("Stopping playback on TESLA TE-300...") send_soap("Stop", "0") print("Stopped.") def pause(): print("Pausing playback...") send_soap("Pause", "0") print("Paused.") def status(): xml = send_soap("GetTransportInfo", "0") if xml: import xml.etree.ElementTree as ET root = ET.fromstring(xml) for state in root.iter("CurrentTransportState"): print(f"Current State: {state.text}") class RangeRequestHandler(http.server.BaseHTTPRequestHandler): target_file = None def do_HEAD(self): self.send_headers(head_only=True) def do_GET(self): self.send_headers(head_only=False) def send_headers(self, head_only=False): if not self.target_file or not os.path.exists(self.target_file): self.send_error(404, "File Not Found") return file_size = os.path.getsize(self.target_file) range_header = self.headers.get("Range") start = 0 end = file_size - 1 if range_header: try: bytes_range = range_header.strip().split("=")[1] s, e = bytes_range.split("-") start = int(s) if s else 0 end = int(e) if e else file_size - 1 except: pass if range_header: self.send_response(206) self.send_header("Content-Range", f"bytes {start}-{end}/{file_size}") else: self.send_response(200) length = (end - start) + 1 mime_type, _ = mimetypes.guess_type(self.target_file) if not mime_type: mime_type = "video/mp4" self.send_header("Content-Type", mime_type) self.send_header("Content-Length", str(length)) self.send_header("Accept-Ranges", "bytes") self.end_headers() if not head_only: with open(self.target_file, "rb") as f: f.seek(start) remaining = length while remaining > 0: chunk = f.read(min(remaining, 65536)) if not chunk: break try: self.wfile.write(chunk) except (BrokenPipeError, ConnectionResetError): break remaining -= len(chunk) def log_message(self, format, *args): # Quiet logger pass def serve_and_cast_local_file(filepath): abs_path = os.path.abspath(filepath) if not os.path.exists(abs_path): print(f"File not found: {abs_path}") sys.exit(1) RangeRequestHandler.target_file = abs_path # Run the streaming HTTP server daemon in background class ThreadingServer(http.server.ThreadingHTTPServer): daemon_threads = True try: server = ThreadingServer(("0.0.0.0", LOCAL_STREAM_PORT), RangeRequestHandler) except OSError: # Port already open, assume existing server is ready server = None if server: t = threading.Thread(target=server.serve_forever, daemon=True) t.start() stream_url = f"http://{LOCAL_IP}:{LOCAL_STREAM_PORT}/{urllib.parse.quote(os.path.basename(abs_path))}" cast_url(stream_url) if server: print("Serving stream in background. Press Ctrl+C to stop casting.") try: while True: time.sleep(1) except KeyboardInterrupt: print("\nStopping...") stop() def main(): if len(sys.argv) < 2: print("Usage: cast-tesla ") print("\nExamples on t520:") print(" cast-tesla /mnt/kof17/video/Video8_02_fix.mp4 (file on boxC)") print(" cast-tesla ~/Downloads/my_video.mp4 (local file on t520)") print(" cast-tesla https://ocko-live.ssl.cdn.cra.cz/channels/ocko/playlist.m3u8") print(" cast-tesla http://10.0.0.14:9000/radio.mp3") print(" cast-tesla status") print(" cast-tesla stop") sys.exit(1) arg = sys.argv[1] if arg == "stop": stop() elif arg == "pause": pause() elif cmd_status := (arg == "status"): status() elif arg.startswith("http://") or arg.startswith("https://"): cast_url(arg) elif arg.startswith("/mnt/kof17") or arg.startswith("boxC:"): clean_path = arg[5:] if arg.startswith("boxC:") else arg print(f"Delegating to boxC: {clean_path}") subprocess.run(["ssh", "boxC", f"cast-tesla '{clean_path}'"]) elif os.path.exists(arg): serve_and_cast_local_file(arg) else: # Check if it exists on boxC res = subprocess.run(["ssh", "boxC", f"test -e '{arg}'"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if res.returncode == 0: print(f"File found on boxC: {arg}") subprocess.run(["ssh", "boxC", f"cast-tesla '{arg}'"]) else: print(f"Error: Target '{arg}' not found locally or on boxC.") sys.exit(1) if __name__ == "__main__": main()