diff options
author | Mike Crute <mike@crute.us> | 2017-09-18 20:59:09 -0700 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2017-09-30 23:09:18 +0000 |
commit | 251eac4e987e241798b12f271f0cd4af24173d3e (patch) | |
tree | 9fed127b0aba8a35b62c4411831eebc35617c72f | |
parent | 095351609ff0bbf9abdbf7a86b09cb6afc8f83b0 (diff) | |
download | pydora-251eac4e987e241798b12f271f0cd4af24173d3e.tar.bz2 pydora-251eac4e987e241798b12f271f0cd4af24173d3e.tar.xz pydora-251eac4e987e241798b12f271f0cd4af24173d3e.zip |
Add remote VLC player backend
-rw-r--r-- | README.rst | 25 | ||||
-rw-r--r-- | pydora/audio_backend.py | 42 | ||||
-rw-r--r-- | pydora/player.py | 28 |
3 files changed, 92 insertions, 3 deletions
@@ -55,6 +55,31 @@ The ``pydora`` player will try to auto-detect whatever player exists on your | |||
55 | system, prefering VLC, and will use that audio output backend. If you notice a | 55 | system, prefering VLC, and will use that audio output backend. If you notice a |
56 | lot of skipping in a playlist consider installing VLC. | 56 | lot of skipping in a playlist consider installing VLC. |
57 | 57 | ||
58 | Remote VLC Backend | ||
59 | ------------------ | ||
60 | It is also possible to remotely control a copy of VLC running on another | ||
61 | machine if you're unable or unwilling to install Pydora on your playback | ||
62 | machine. To do this start VLC on the remote machine with the ``rc-host`` option | ||
63 | set. For example:: | ||
64 | |||
65 | vlc -I rc --advanced --rc-host=0.0.0.0:1234 | ||
66 | |||
67 | Once VLC is running start Pydora with the ``vlc-net`` option and specify the | ||
68 | remote host and port that VLC is listening on. For example:: | ||
69 | |||
70 | pydora --vlc-net 192.168.0.12:1234 | ||
71 | |||
72 | Pydora will now send all audio playback requests to the remote VLC. It does | ||
73 | this using a text control protocol; all audio data is streamed directly from | ||
74 | the internet to VLC and is not passed over the pydora control channel. Because | ||
75 | of this it is possible for the control channel to run over a very low bandwidth | ||
76 | connection. | ||
77 | |||
78 | **Note**: VLC doesn't provide any security so anyone on the network will be | ||
79 | able to control VLC. It is generally safer to bind VLC to ``127.0.0.1`` and use | ||
80 | something like SSH forwarding to securely forward the port to a remote host but | ||
81 | that's outside of the scope of this README. | ||
82 | |||
58 | Simple Player | 83 | Simple Player |
59 | ============= | 84 | ============= |
60 | Included is ``pydora``, a simple Pandora stream player that runs at the command | 85 | 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 | |||
2 | import time | 2 | import time |
3 | import fcntl | 3 | import fcntl |
4 | import select | 4 | import select |
5 | import socket | ||
5 | 6 | ||
6 | from pandora.py2compat import which | 7 | from pandora.py2compat import which |
7 | from .utils import iterate_forever, SilentPopen | 8 | from .utils import iterate_forever, SilentPopen |
@@ -141,6 +142,13 @@ class BasePlayer(object): | |||
141 | self._process = SilentPopen(self._cmd) | 142 | self._process = SilentPopen(self._cmd) |
142 | self._post_start() | 143 | self._post_start() |
143 | 144 | ||
145 | def _get_select_readers(self): | ||
146 | """Return a list of file-like objects for reading | ||
147 | |||
148 | Will be passed to select() to poll for readers. | ||
149 | """ | ||
150 | return [self._control_channel, self._process.stdout] | ||
151 | |||
144 | def play(self, song): | 152 | def play(self, song): |
145 | """Play a new song from a Pandora model | 153 | """Play a new song from a Pandora model |
146 | 154 | ||
@@ -159,7 +167,7 @@ class BasePlayer(object): | |||
159 | self._loop_hook() | 167 | self._loop_hook() |
160 | 168 | ||
161 | readers, _, _ = select.select( | 169 | readers, _, _ = select.select( |
162 | [self._control_channel, self._process.stdout], [], [], 1) | 170 | self._get_select_readers(), [], [], 1) |
163 | 171 | ||
164 | for handle in readers: | 172 | for handle in readers: |
165 | if handle.fileno() == self._control_fd: | 173 | if handle.fileno() == self._control_fd: |
@@ -272,3 +280,35 @@ class VLCPlayer(BasePlayer): | |||
272 | if (time.time() - self._last_poll) >= self.POLL_INTERVAL: | 280 | if (time.time() - self._last_poll) >= self.POLL_INTERVAL: |
273 | self._send_cmd("status") | 281 | self._send_cmd("status") |
274 | self._last_poll = time.time() | 282 | self._last_poll = time.time() |
283 | |||
284 | |||
285 | class RemoteVLC(VLCPlayer): | ||
286 | |||
287 | def __init__(self, host, port, callbacks, control_channel): | ||
288 | self._connect_to = (host, int(port)) | ||
289 | self._control_sock = None | ||
290 | super(RemoteVLC, self).__init__(callbacks, control_channel) | ||
291 | |||
292 | def _get_select_readers(self): | ||
293 | return [self._control_channel, self._control_sock] | ||
294 | |||
295 | def _send_cmd(self, cmd): | ||
296 | self._control_sock.sendall("{}\n".format(cmd).encode("utf-8")) | ||
297 | |||
298 | def _read_from_process(self, handle): | ||
299 | return handle.recv(self.CHUNK_SIZE).strip() | ||
300 | |||
301 | def _ensure_started(self): | ||
302 | if not self._control_sock: | ||
303 | self._control_sock = socket.create_connection(self._connect_to) | ||
304 | self._control_sock.setblocking(False) | ||
305 | |||
306 | def _find_path(self): | ||
307 | try: | ||
308 | self._ensure_started() | ||
309 | except socket.error: | ||
310 | raise PlayerUnusable("Unable to connect to VLC") | ||
311 | |||
312 | def _post_start(self): | ||
313 | # This is a NOOP for network VLC | ||
314 | 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 | |||
10 | 10 | ||
11 | import os | 11 | import os |
12 | import sys | 12 | import sys |
13 | import argparse | ||
13 | from pandora import clientbuilder | 14 | from pandora import clientbuilder |
14 | 15 | ||
15 | from .utils import Colors, Screen | 16 | from .utils import Colors, Screen |
17 | from .audio_backend import RemoteVLC | ||
16 | from .audio_backend import MPG123Player, VLCPlayer | 18 | from .audio_backend import MPG123Player, VLCPlayer |
17 | from .audio_backend import UnsupportedEncoding, PlayerUnusable | 19 | from .audio_backend import UnsupportedEncoding, PlayerUnusable |
18 | 20 | ||
@@ -66,7 +68,20 @@ class PlayerApp(object): | |||
66 | self.client = None | 68 | self.client = None |
67 | self.screen = Screen() | 69 | self.screen = Screen() |
68 | 70 | ||
69 | def get_player(self): | 71 | def get_player(self, vlc_net=None): |
72 | # The user must explicitly request network VLC so we should always | ||
73 | # honor that request, to this end we try network first and fail hard | ||
74 | # if that isn't available. | ||
75 | if vlc_net: | ||
76 | try: | ||
77 | host, port = vlc_net.split(":") | ||
78 | player = RemoteVLC(host, port, self, sys.stdin) | ||
79 | Screen.print_success("Using Remote VLC") | ||
80 | return player | ||
81 | except PlayerUnusable: | ||
82 | Screen.print_error("Unable to connect to vlc") | ||
83 | raise | ||
84 | |||
70 | try: | 85 | try: |
71 | player = VLCPlayer(self, sys.stdin) | 86 | player = VLCPlayer(self, sys.stdin) |
72 | self.screen.print_success("Using VLC") | 87 | self.screen.print_success("Using VLC") |
@@ -232,8 +247,16 @@ class PlayerApp(object): | |||
232 | "your config file before continuing.")) | 247 | "your config file before continuing.")) |
233 | sys.exit(1) | 248 | sys.exit(1) |
234 | 249 | ||
250 | def _parse_args(self): | ||
251 | parser = argparse.ArgumentParser( | ||
252 | description="command line Pandora player") | ||
253 | parser.add_argument( | ||
254 | "--vlc-net", dest="vlc_net", | ||
255 | help="connect to VLC over the network (host:port)") | ||
256 | return parser.parse_args() | ||
257 | |||
235 | def run(self): | 258 | def run(self): |
236 | self.player = self.get_player() | 259 | self.player = self.get_player(self._parse_args().vlc_net) |
237 | self.player.start() | 260 | self.player.start() |
238 | 261 | ||
239 | self.client = self.get_client() | 262 | self.client = self.get_client() |
@@ -258,6 +281,7 @@ class PlayerApp(object): | |||
258 | except UnsupportedEncoding as ex: | 281 | except UnsupportedEncoding as ex: |
259 | error = str(ex) | 282 | error = str(ex) |
260 | except KeyboardInterrupt: | 283 | except KeyboardInterrupt: |
284 | self.player.stop() | ||
261 | sys.exit(0) | 285 | sys.exit(0) |
262 | 286 | ||
263 | 287 | ||