add cast-tesla: UPnP/DLNA stream caster for TESLA TE-300
This commit is contained in:
Executable
+202
@@ -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"""<?xml version="1.0" encoding="utf-8"?>
|
||||
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
|
||||
<s:Body>
|
||||
<u:{action} xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
|
||||
{body_xml}
|
||||
</u:{action}>
|
||||
</s:Body>
|
||||
</s:Envelope>"""
|
||||
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"<InstanceID>0</InstanceID><CurrentURI>{url}</CurrentURI><CurrentURIMetaData></CurrentURIMetaData>"
|
||||
send_soap("SetAVTransportURI", set_xml)
|
||||
play_xml = "<InstanceID>0</InstanceID><Speed>1</Speed>"
|
||||
send_soap("Play", play_xml)
|
||||
print("Playback started on TV!")
|
||||
|
||||
def stop():
|
||||
print("Stopping playback on TESLA TE-300...")
|
||||
send_soap("Stop", "<InstanceID>0</InstanceID>")
|
||||
print("Stopped.")
|
||||
|
||||
def pause():
|
||||
print("Pausing playback...")
|
||||
send_soap("Pause", "<InstanceID>0</InstanceID>")
|
||||
print("Paused.")
|
||||
|
||||
def status():
|
||||
xml = send_soap("GetTransportInfo", "<InstanceID>0</InstanceID>")
|
||||
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 <file | url | stop | pause | status>")
|
||||
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()
|
||||
Reference in New Issue
Block a user