diff --git a/nanodlna/cli.py b/nanodlna/cli.py
index 7667932..776534d 100755
--- a/nanodlna/cli.py
+++ b/nanodlna/cli.py
@@ -6,6 +6,7 @@
import json
import os
import sys
+import signal
import datetime
import tempfile
@@ -52,34 +53,15 @@ def list_devices(args):
set_logs(args)
logging.info("Scanning devices...")
- my_devices = devices.get_devices(args.timeout)
+ my_devices = devices.get_devices(args.timeout, args.local_host)
logging.info("Number of devices found: {}".format(len(my_devices)))
for i, device in enumerate(my_devices, 1):
print("Device {0}:\n{1}\n\n".format(i, json.dumps(device, indent=4)))
-def play(args):
-
- set_logs(args)
-
- logging.info("Starting to play")
-
- # Get video and subtitle file names
-
- files = {"file_video": args.file_video}
-
- if args.use_subtitle:
-
- if not args.file_subtitle:
- args.file_subtitle = get_subtitle(args.file_video)
-
- if args.file_subtitle:
- files["file_subtitle"] = args.file_subtitle
-
- logging.info("Media files: {}".format(json.dumps(files)))
+def find_device(args):
- # Select device to play
logging.info("Selecting device to play")
device = None
@@ -88,7 +70,7 @@ def play(args):
logging.info("Select device by URL")
device = devices.register_device(args.device_url)
else:
- my_devices = devices.get_devices(args.timeout)
+ my_devices = devices.get_devices(args.timeout, args.local_host)
if len(my_devices) > 0:
if args.device_query:
@@ -100,6 +82,30 @@ def play(args):
logging.info("Select first device")
device = my_devices[0]
+ return device
+
+
+def play(args):
+
+ set_logs(args)
+
+ logging.info("Starting to play")
+
+ # Get video and subtitle file names
+
+ files = {"file_video": args.file_video}
+
+ if args.use_subtitle:
+
+ if not args.file_subtitle:
+ args.file_subtitle = get_subtitle(args.file_video)
+
+ if args.file_subtitle:
+ files["file_subtitle"] = args.file_subtitle
+
+ logging.info("Media files: {}".format(json.dumps(files)))
+
+ device = find_device(args)
if not device:
sys.exit("No devices found.")
@@ -117,16 +123,64 @@ def play(args):
logging.info("Streaming server ready")
+ # Register handler if interrupt signal is received
+ signal.signal(signal.SIGINT, build_handler_stop(device))
+
# Play the video through DLNA protocol
logging.info("Sending play command")
dlna.play(files_urls, device)
+def build_handler_stop(device):
+ def signal_handler(sig, frame):
+
+ logging.info("Interrupt signal detected")
+
+ logging.info("Sending stop command to render device")
+ dlna.stop(device)
+
+ logging.info("Stopping streaming server")
+ streaming.stop_server()
+
+ sys.exit(
+ "Interrupt signal detected. "
+ "Sent stop command to render device and "
+ "stopped streaming. "
+ "nano-dlna will exit now!"
+ )
+ return signal_handler
+
+
+def pause(args):
+
+ set_logs(args)
+
+ logging.info("Selecting device to pause")
+ device = find_device(args)
+
+ # Pause through DLNA protocol
+ logging.info("Sending pause command")
+ dlna.pause(device)
+
+
+def stop(args):
+
+ set_logs(args)
+
+ logging.info("Selecting device to stop")
+ device = find_device(args)
+
+ # Stop through DLNA protocol
+ logging.info("Sending stop command")
+ dlna.stop(device)
+
+
def run():
parser = argparse.ArgumentParser(
description="A minimal UPnP/DLNA media streamer.")
parser.set_defaults(func=lambda args: parser.print_help())
+ parser.add_argument("-H", "--host", dest="local_host")
parser.add_argument("-t", "--timeout", type=float, default=5)
parser.add_argument("-b", "--debug",
dest="debug_activated", action="store_true")
@@ -137,7 +191,6 @@ def run():
p_play = subparsers.add_parser('play')
p_play.add_argument("-d", "--device", dest="device_url")
- p_play.add_argument("-H", "--host", dest="local_host")
p_play.add_argument("-q", "--query-device", dest="device_query")
p_play.add_argument("-s", "--subtitle", dest="file_subtitle")
p_play.add_argument("-n", "--no-subtitle",
@@ -145,6 +198,16 @@ def run():
p_play.add_argument("file_video")
p_play.set_defaults(func=play)
+ p_pause = subparsers.add_parser('pause')
+ p_pause.add_argument("-d", "--device", dest="device_url")
+ p_pause.add_argument("-q", "--query-device", dest="device_query")
+ p_pause.set_defaults(func=pause)
+
+ p_stop = subparsers.add_parser('stop')
+ p_stop.add_argument("-d", "--device", dest="device_url")
+ p_stop.add_argument("-q", "--query-device", dest="device_query")
+ p_stop.set_defaults(func=stop)
+
args = parser.parse_args()
args.func(args)
diff --git a/nanodlna/devices.py b/nanodlna/devices.py
index 0c2bc85..c7547ea 100644
--- a/nanodlna/devices.py
+++ b/nanodlna/devices.py
@@ -3,6 +3,7 @@
import re
import socket
+import struct
import sys
import xml.etree.ElementTree as ET
@@ -25,37 +26,67 @@
"MAN: \"ssdp:discover\"", "MX: 10", "ST: ssdp:all", "", ""]
SSDP_BROADCAST_MSG = "\r\n".join(SSDP_BROADCAST_PARAMS)
-UPNP_DEFAULT_SERVICE_TYPE = "urn:schemas-upnp-org:service:AVTransport:1"
+UPNP_DEVICE_TYPE = "urn:schemas-upnp-org:device:MediaRenderer:1"
+UPNP_SERVICE_TYPE = "urn:schemas-upnp-org:service:AVTransport:1"
+
+
+def get_xml_field_text(xml_root, query):
+ result = None
+ if xml_root:
+ node = xml_root.find(query)
+ result = node.text if node is not None else None
+ return result
def register_device(location_url):
- xml = urllibreq.urlopen(location_url).read().decode("UTF-8")
- xml = re.sub(" xmlns=\"[^\"]+\"", "", xml, count=1)
+ xml_raw = urllibreq.urlopen(location_url).read().decode("UTF-8")
+ logging.debug(
+ "Device to be registered: {}".format(
+ json.dumps({
+ "location_url": location_url,
+ "xml_raw": xml_raw
+ })
+ )
+ )
+
+ xml = re.sub(r"""\s(xmlns="[^"]+"|xmlns='[^']+')""", '', xml_raw, count=1)
info = ET.fromstring(xml)
location = urllibparse.urlparse(location_url)
hostname = location.hostname
- friendly_name = info.find("./device/friendlyName").text
-
- try:
- path = info.find(
- "./device/serviceList/service/"
- "[serviceType='{0}']/controlURL".format(
- UPNP_DEFAULT_SERVICE_TYPE
+ device_root = info.find("./device")
+ if not device_root:
+ device_root = info.find(
+ "./device/deviceList/device/"
+ "[deviceType='{0}']".format(
+ UPNP_DEVICE_TYPE
)
- ).text
- action_url = urllibparse.urljoin(location_url, path)
- except AttributeError:
+ )
+
+ friendly_name = get_xml_field_text(device_root, "./friendlyName")
+ manufacturer = get_xml_field_text(device_root, "./manufacturer")
+ action_url_path = get_xml_field_text(
+ device_root,
+ "./serviceList/service/"
+ "[serviceType='{0}']/controlURL".format(
+ UPNP_SERVICE_TYPE
+ )
+ )
+
+ if action_url_path is not None:
+ action_url = urllibparse.urljoin(location_url, action_url_path)
+ else:
action_url = None
device = {
"location": location_url,
"hostname": hostname,
+ "manufacturer": manufacturer,
"friendly_name": friendly_name,
"action_url": action_url,
- "st": UPNP_DEFAULT_SERVICE_TYPE
+ "st": UPNP_SERVICE_TYPE
}
logging.debug(
@@ -70,12 +101,31 @@ def register_device(location_url):
return device
-def get_devices(timeout=3.0):
+def remove_duplicates(devices):
+ seen = set()
+ result_devices = []
+ for device in devices:
+ device_str = str(device)
+ if device_str not in seen:
+ result_devices.append(device)
+ seen.add(device_str)
+ return result_devices
+
+
+def get_devices(timeout=3.0, host=None):
+
+ if not host:
+ host = "0.0.0.0"
+ logging.debug("Searching for devices on {}".format(host))
logging.debug("Configuring broadcast message")
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP)
- s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 4)
- s.bind(("", SSDP_BROADCAST_PORT + 10))
+
+ # OpenBSD needs the ttl for the IP_MULTICAST_TTL as an unsigned char
+ ttl = struct.pack("B", 4)
+ s.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)
+
+ s.bind((host, 0))
logging.debug("Sending broadcast message")
s.sendto(SSDP_BROADCAST_MSG.encode("UTF-8"), (SSDP_BROADCAST_ADDR,
@@ -109,9 +159,19 @@ def get_devices(timeout=3.0):
except Exception:
pass
- devices_urls = [dev["location"]
- for dev in devices if "AVTransport" in dev["st"]]
- devices = [register_device(location_url) for location_url in devices_urls]
+ devices_urls = [
+ dev["location"]
+ for dev in devices
+ if "st" in dev and
+ "AVTransport" in dev["st"]
+ ]
+
+ devices = [
+ register_device(location_url)
+ for location_url in devices_urls
+ ]
+
+ devices = remove_duplicates(devices)
return devices
@@ -120,7 +180,7 @@ def get_devices(timeout=3.0):
timeout = int(sys.argv[1]) if len(sys.argv) >= 2 else 5
- devices = get_devices(timeout)
+ devices = get_devices(timeout, "0.0.0.0")
for i, device in enumerate(devices, 1):
print("Device {0}:\n{1}\n\n".format(i, json.dumps(device, indent=4)))
diff --git a/nanodlna/dlna.py b/nanodlna/dlna.py
index a6f2111..109db6d 100644
--- a/nanodlna/dlna.py
+++ b/nanodlna/dlna.py
@@ -28,7 +28,9 @@ def send_dlna_action(device, data, action):
action_data = pkgutil.get_data(
"nanodlna", "templates/action-{0}.xml".format(action)).decode("UTF-8")
- action_data = action_data.format(**data).encode("UTF-8")
+ if data:
+ action_data = action_data.format(**data)
+ action_data = action_data.encode("UTF-8")
headers = {
"Content-Type": "text/xml; charset=\"utf-8\"",
@@ -95,3 +97,21 @@ def play(files_urls, device):
send_dlna_action(device, video_data, "SetAVTransportURI")
logging.debug("Playing video")
send_dlna_action(device, video_data, "Play")
+
+
+def pause(device):
+ logging.debug("Pausing device: {}".format(
+ json.dumps({
+ "device": device
+ })
+ ))
+ send_dlna_action(device, None, "Pause")
+
+
+def stop(device):
+ logging.debug("Stopping device: {}".format(
+ json.dumps({
+ "device": device
+ })
+ ))
+ send_dlna_action(device, None, "Stop")
diff --git a/nanodlna/streaming.py b/nanodlna/streaming.py
index b9447c0..169c20b 100644
--- a/nanodlna/streaming.py
+++ b/nanodlna/streaming.py
@@ -88,6 +88,10 @@ def start_server(files, serve_ip, serve_port=9000):
return files_urls
+def stop_server():
+ reactor.stop()
+
+
def get_serve_ip(target_ip, target_port=80):
logging.debug("Identifying server IP")
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
diff --git a/nanodlna/templates/action-Pause.xml b/nanodlna/templates/action-Pause.xml
new file mode 100644
index 0000000..838fca7
--- /dev/null
+++ b/nanodlna/templates/action-Pause.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ 0
+
+
+
diff --git a/nanodlna/templates/action-Stop.xml b/nanodlna/templates/action-Stop.xml
new file mode 100644
index 0000000..7026ce1
--- /dev/null
+++ b/nanodlna/templates/action-Stop.xml
@@ -0,0 +1,8 @@
+
+
+
+
+ 0
+
+
+
diff --git a/setup.py b/setup.py
index 0f96f74..b36faa5 100644
--- a/setup.py
+++ b/setup.py
@@ -13,7 +13,7 @@
setup(
name='nanodlna',
- version='0.2.1',
+ version='0.3.0',
description='A minimal UPnP/DLNA media streamer',
long_description="""nano-dlna is a command line tool that allows you to
play a local video file in your TV (or any other DLNA compatible device)""",