diff --git a/cast-tesla b/cast-tesla new file mode 100755 index 0000000..892b135 --- /dev/null +++ b/cast-tesla @@ -0,0 +1,202 @@ +#!/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 = "10.0.0.11" +RENDERER_PORT = 54321 +LOCAL_IP = "10.0.0.1" +LOCAL_STREAM_PORT = 8201 +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()