From 251eac4e987e241798b12f271f0cd4af24173d3e Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Mon, 18 Sep 2017 20:59:09 -0700 Subject: Add remote VLC player backend --- README.rst | 25 +++++++++++++++++++++++++ pydora/audio_backend.py | 42 +++++++++++++++++++++++++++++++++++++++++- pydora/player.py | 28 ++++++++++++++++++++++++++-- 3 files changed, 92 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 8eab71d..f317900 100644 --- a/README.rst +++ b/README.rst @@ -55,6 +55,31 @@ The ``pydora`` player will try to auto-detect whatever player exists on your system, prefering VLC, and will use that audio output backend. If you notice a lot of skipping in a playlist consider installing VLC. +Remote VLC Backend +------------------ +It is also possible to remotely control a copy of VLC running on another +machine if you're unable or unwilling to install Pydora on your playback +machine. To do this start VLC on the remote machine with the ``rc-host`` option +set. For example:: + + vlc -I rc --advanced --rc-host=0.0.0.0:1234 + +Once VLC is running start Pydora with the ``vlc-net`` option and specify the +remote host and port that VLC is listening on. For example:: + + pydora --vlc-net 192.168.0.12:1234 + +Pydora will now send all audio playback requests to the remote VLC. It does +this using a text control protocol; all audio data is streamed directly from +the internet to VLC and is not passed over the pydora control channel. Because +of this it is possible for the control channel to run over a very low bandwidth +connection. + +**Note**: VLC doesn't provide any security so anyone on the network will be +able to control VLC. It is generally safer to bind VLC to ``127.0.0.1`` and use +something like SSH forwarding to securely forward the port to a remote host but +that's outside of the scope of this README. + Simple Player ============= Included is ``pydora``, a simple Pandora stream player that runs at the command diff --git a/pydora/audio_backend.py b/pydora/audio_backend.py index 9599771..f92998a 100644 --- a/pydora/audio_backend.py +++ b/pydora/audio_backend.py @@ -2,6 +2,7 @@ import os import time import fcntl import select +import socket from pandora.py2compat import which from .utils import iterate_forever, SilentPopen @@ -141,6 +142,13 @@ class BasePlayer(object): self._process = SilentPopen(self._cmd) self._post_start() + def _get_select_readers(self): + """Return a list of file-like objects for reading + + Will be passed to select() to poll for readers. + """ + return [self._control_channel, self._process.stdout] + def play(self, song): """Play a new song from a Pandora model @@ -159,7 +167,7 @@ class BasePlayer(object): self._loop_hook() readers, _, _ = select.select( - [self._control_channel, self._process.stdout], [], [], 1) + self._get_select_readers(), [], [], 1) for handle in readers: if handle.fileno() == self._control_fd: @@ -272,3 +280,35 @@ class VLCPlayer(BasePlayer): if (time.time() - self._last_poll) >= self.POLL_INTERVAL: self._send_cmd("status") self._last_poll = time.time() + + +class RemoteVLC(VLCPlayer): + + def __init__(self, host, port, callbacks, control_channel): + self._connect_to = (host, int(port)) + self._control_sock = None + super(RemoteVLC, self).__init__(callbacks, control_channel) + + def _get_select_readers(self): + return [self._control_channel, self._control_sock] + + def _send_cmd(self, cmd): + self._control_sock.sendall("{}\n".format(cmd).encode("utf-8")) + + def _read_from_process(self, handle): + return handle.recv(self.CHUNK_SIZE).strip() + + def _ensure_started(self): + if not self._control_sock: + self._control_sock = socket.create_connection(self._connect_to) + self._control_sock.setblocking(False) + + def _find_path(self): + try: + self._ensure_started() + except socket.error: + raise PlayerUnusable("Unable to connect to VLC") + + def _post_start(self): + # This is a NOOP for network VLC + pass diff --git a/pydora/player.py b/pydora/player.py index 991468a..128674b 100644 --- a/pydora/player.py +++ b/pydora/player.py @@ -10,9 +10,11 @@ from __future__ import print_function import os import sys +import argparse from pandora import clientbuilder from .utils import Colors, Screen +from .audio_backend import RemoteVLC from .audio_backend import MPG123Player, VLCPlayer from .audio_backend import UnsupportedEncoding, PlayerUnusable @@ -66,7 +68,20 @@ class PlayerApp(object): self.client = None self.screen = Screen() - def get_player(self): + def get_player(self, vlc_net=None): + # The user must explicitly request network VLC so we should always + # honor that request, to this end we try network first and fail hard + # if that isn't available. + if vlc_net: + try: + host, port = vlc_net.split(":") + player = RemoteVLC(host, port, self, sys.stdin) + Screen.print_success("Using Remote VLC") + return player + except PlayerUnusable: + Screen.print_error("Unable to connect to vlc") + raise + try: player = VLCPlayer(self, sys.stdin) self.screen.print_success("Using VLC") @@ -232,8 +247,16 @@ class PlayerApp(object): "your config file before continuing.")) sys.exit(1) + def _parse_args(self): + parser = argparse.ArgumentParser( + description="command line Pandora player") + parser.add_argument( + "--vlc-net", dest="vlc_net", + help="connect to VLC over the network (host:port)") + return parser.parse_args() + def run(self): - self.player = self.get_player() + self.player = self.get_player(self._parse_args().vlc_net) self.player.start() self.client = self.get_client() @@ -258,6 +281,7 @@ class PlayerApp(object): except UnsupportedEncoding as ex: error = str(ex) except KeyboardInterrupt: + self.player.stop() sys.exit(0) -- cgit v1.2.3