Projects
Multimedia
pulseaudio-dlna
Sign Up
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 8
View file
pulseaudio-dlna.changes
Changed
@@ -1,4 +1,33 @@ ------------------------------------------------------------------- +Sun Sep 20 15:45:37 UTC 2015 - antoine.belvire@laposte.net + +- Update to 0.4.5: + * Exceptions while updating sink and device information from + pulseaudio are now handled better + * Change --fake-http10-content-length flag to --fake-http-content-length + to also support HTTP 1.1 requests + * Fix a bug where the supported device mime types could not get + parsed correctly + * Fix a bug where the device UUID was not parsed correctly + * Fix a bug where just mime types beginning with audio/ were + accepted, but not e.g. application/ogg + * The stream server will now respond with 206 when receiving + requests with range header + * UPNP control commands have now a timeout of 10 seconds + * Fix a bug where the wrong stream was removed from the stream + manager + * Fix several bugs caused by purely relying on stopping actions + for the devices idle state + * Add L16 Encoder + * The encoder option can now handle multiple options separated by + comma + * Add the --create-device-config flag + * Fix a bug where the dbus session was bound from the wrong + process + * Fix a bug where the wrong device UDN was retrieved from XML + documents containing multiple devices + +------------------------------------------------------------------- Sat Aug 8 19:26:41 UTC 2015 - antoine.belvire@laposte.net - Update to 0.4.4:
View file
pulseaudio-dlna.spec
Changed
@@ -17,7 +17,7 @@ Name: pulseaudio-dlna -Version: 0.4.4 +Version: 0.4.5 Release: 0 Summary: A DLNA server which brings DLNA/UPnP support to PulseAudio License: GPL-3.0
View file
pulseaudio-dlna-0.4.4.tar.gz/pulseaudio_dlna/common.py
Deleted
@@ -1,47 +0,0 @@ -#!/usr/bin/python - -# This file is part of pulseaudio-dlna. - -# pulseaudio-dlna is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. - -# pulseaudio-dlna is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. - -# You should have received a copy of the GNU General Public License -# along with pulseaudio-dlna. If not, see <http://www.gnu.org/licenses/>. - -from __future__ import unicode_literals - -import inspect - -import encoders - -supported_encoders = [] - - -def load_encoders(): - for (name, _type) in inspect.getmembers(encoders): - forbidden_members = [ - '__builtins__', - '__doc__', - '__file__', - '__name__', - '__package__', - 'unicode_literals' - ] - if name not in forbidden_members: - try: - encoder = _type() - except: - continue - if name != 'BaseEncoder' and \ - isinstance(_type(), encoders.BaseEncoder): - supported_encoders.append(encoder) - supported_encoders.sort(reverse=True) - -load_encoders()
View file
pulseaudio-dlna-0.4.4.tar.gz/README.md -> pulseaudio-dlna-0.4.5.tar.gz/README.md
Changed
@@ -36,6 +36,22 @@ ## Changelog ## + * __0.4.5__ - (_2015-09-20_) + - Exceptions while updating sink and device information from pulseaudio are now handled better + - Changed `--fake-http10-content-length` flag to `--fake-http-content-length` to also support HTTP 1.1 requests + - Fixed a bug where the supported device mime types could not get parsed correctly + - Fixed a bug where the device UUID was not parsed correctly + - Fixed a bug where just mime types beginning with `audio/` where accepted, but not e.g. `application/ogg` + - The stream server will now respond with 206 when receiving requests with `range` header + - UPNP control commands have now a timeout of 10 seconds + - Fixed a bug where the wrong stream was removed from the stream manager + - Fixed several bugs caused by purely relying on stopping actions for the devices idle state + - Added L16 Encoder + - The encoder option can now handle multiple options separated by comma + - Added the `--create-device-config` flag + - Fixed a bug where the dbus session was bound from the wrong process + - Fix a bug where the wrong device UDN was retrieved from XML documents containing multiple devices + * __0.4.4__ - (_2015-08-07_) - Added `--disable-ssdp-listener` option - Fixed a bug with applications which remove and re-add streams all the time @@ -271,26 +287,37 @@ ### CLI ### Usage: - pulseaudio-dlna [--host <host>] [--port <port>] [--encoder <encoder>] [--bit-rate=<rate>] [--filter-device=<filter-device>] [--renderer-urls <urls>] [--debug] [--fake-http10-content-length] [--disable-switchback] [--disable-ssdp-listener] + pulseaudio-dlna [--host <host>] [--port <port>] [--encoder <encoders>] [--bit-rate=<rate>] [--filter-device=<filter-device>] [--renderer-urls <urls>] [--debug] [--fake-http10-content-length] [--fake-http-content-length] [--disable-switchback] [--disable-ssdp-listener] + pulseaudio-dlna [--create-device-config] pulseaudio-dlna [-h | --help | --version] Options: + --create-device-config Discovers all devices in your network and write a config for them. + That config can be editied manually to adjust various settings. + You can set: + - Device name + - Codec order (The first one is used if the encoder binary is available on your system) + - Various codec settings such as the mime type, specific rules or + the bit rate (depends on the codec) + A written config is loaded by default if the --encoder and --bit-rate options are not used. --host=<host> Set the server ip. -p --port=<port> Set the server port [default: 8080]. - -e --encoder=<encoder> Set the audio encoder. + -e --encoder=<encoders> Set the audio encoder. Possible encoders are: - mp3 MPEG Audio Layer III (MP3) - - ogg Ogg Vorbis + - ogg Ogg Vorbis (OGG) - flac Free Lossless Audio Codec (FLAC) - wav Waveform Audio File Format (WAV) - opus Opus Interactive Audio Codec (OPUS) + - aac Advanced Audio Coding (AAC) + - l16 Linear PCM (L16) -b --bit-rate=<rate> Set the audio encoder's bitrate. --filter-device=<filter-device> Set a name filter for devices which should be added. Devices which get discovered, but won't match the filter text will be skipped. --renderer-urls=<urls> Set the renderer urls yourself. no discovery will commence. --debug enables detailed debug messages. - --fake-http10-content-length If set, the content-length of HTTP 1.0 requests will be set to 100 GB. + --fake-http-content-length If set, the content-length of HTTP requests will be set to 100 GB. --disable-switchback If set, streams won't switched back to the default sink if a device disconnects. --disable-ssdp-listener If set, the application won't bind to the port 1900 and therefore the automatic discovery of new devices won't work. -v --version Show the version. @@ -315,49 +342,230 @@ UDP multicast packages won't work (most times) over VPN connections this is very useful if you ever plan to stream to a UPNP device over VPN. +### Device configuration rules + +Most times the automatic discovery of supported device codecs and their +prioritization works pretty good. But in the case of a device which does work +out of the box or if you don't like the used codec you can adjust the settings +with a _device configuration_. + +If you want to create a specific configuration for your devices you can do +that via the `--create-device-config` flag. It will search for devices on +your network and write a config for them. It will look for / write them at: + +- `~/.local/share/pulseaudio-dlna/devices.json` (prioritized) +- `/etc/pulseaudio-dlna/devices.json` + +The purpose of this is that the application should do the most work for the +user. You just have to edit the file instead of writing it completely on +your own. + +Let's make an example: +I started the application via `pulseaudio-dlna --create-device-config` and +that is what was discovered: + +```json + "uuid:e4572d54-c2c7-d491-1eb3-9cf17cf5fe01": { + "flavour": "DLNA", + "name": "Device name", + "codecs": [ + { + "rules": [], + "bit_rate": null, + "identifier": "mp3", + "mime_type": "audio/mpeg" + }, + { + "rules": [], + "identifier": "flac", + "mime_type": "audio/flac" + }, + { + "channels": 2, + "rules": [], + "identifier": "l16", + "sample_rate": 48000, + "mime_type": "audio/L16;rate=48000;channels=2" + }, + { + "channels": 2, + "rules": [], + "identifier": "l16", + "sample_rate": 44100, + "mime_type": "audio/L16;rate=44100;channels=2" + }, + { + "channels": 1, + "rules": [], + "identifier": "l16", + "sample_rate": 44100, + "mime_type": "audio/L16;rate=44100;channels=1" + } + ] + } +``` + +It was detected that the device supports the following codecs: + +- `audio/mp3` +- `audio/flac` +- `audio/L16;rate=48000;channels=2` +- `audio/L16;rate=44100;channels=2` +- `audio/L16;rate=44100;channels=1` + +If you don't change the configuration at all, it means that the next time +you start _pulseaudio-dlna_ it will automatically use those codecs for that +device. The order of the list also defines the priority. It will take the +first codec and use it if the appropriate encoder binary is installed on your +system. If the binary is missing it will take the next one. So here the +_mp3_ codec would be used, if the _lame_ binary is installed. + +You can also change the name of the device, adjust the mime type or set the +bit rate. A `null` value means _default_, for bit rates this +is set to 192 Kbit/s. + +In that case I want to rename my device to "Living Room". Besides that +I don't want the L16 codecs, so i simply remove them and i want my _mp3_ to +be encoded in 256 Kbit/s. + +```json + "uuid:e4572d54-c2c7-d491-1eb3-9cf17cf5fe01": { + "flavour": "DLNA", + "name": "Living Room", + "codecs": [ + { + "rules": [], + "bit_rate": 256, + "identifier": "mp3", + "mime_type": "audio/mpeg" + }, + { + "rules": [], + "identifier": "flac", + "mime_type": "audio/flac" + } + ] + } +``` +But as it turns out this device has a problem with playing the _mp3_ stream +when you don't specify the `--fake-http-content-length` flag. Let's say _flac_ +works without the flag. So, you can add a rule for that to that device. + +```json + "uuid:e4572d54-c2c7-d491-1eb3-9cf17cf5fe01": { + "flavour": "DLNA", + "name": "Living Room", + "codecs": [ + { + "rules": [ + { + "name": "FAKE_HTTP_CONTENT_LENGTH" + } + ], + "bit_rate": 256, + "identifier": "mp3", + "mime_type": "audio/mpeg" + }, + { + "rules": [], + "identifier": "flac", + "mime_type": "audio/flac" + } + ] + } +``` + +That's it. _pulseaudio-dlna_ will automatically use that config if you don't +use the `--encoder` or `--bit-rate` options. + +## Known Issues ## + +- **Distorted sound** + + If you experience distorted sound, try to pause / unpause the playback or + changing / adjusting the volume. Some encoders handle volume changes + better than others. The _lame_ encoder handles this by far better than + most of the other ones. + +- **There is a delay about a few seconds** + + Since there is HTTP streaming used for the audio data to transport, + there is always a buffer involved. This device buffer ensures that even + if you suffer from a slow network (e.g. weak wifi) small interruptions + won't affect your playback. On the other hand devices will first start + to play when this buffer is filled. Most devices do this based on the + received amount of data. Therefore inefficient codecs such as _wav_ fill + that buffer much faster than efficient codecs do. The result is a + noticeable shorter delay in contrast to e.g. _mp3_ or others. Note, that + in this case your network should be pretty stable, otherwise your device + will quickly run out of data and stop playing. This is normally not a + problem with cable connections. E.g. I have a delay about 1-2 seconds + with _wav_ and a delay of about 5 seconds with _mp3_ with the same + cable connected device. You can decrease the delay when using _wav_ or + using high bit rates, but you won't get rid of it completely. + My advice: If you have a reliable network, use _wav_. It is lossless + and you will get a short delay. If you have not, use another encoder + which does not require that much bandwidth to make sure your device + will keep playing. Of course you will be affected from a higher delay. + ## Troubleshooting ## Some devices do not stick to the HTTP 1.0/1.1 standard. Since most devices do, _pulseaudio-dlna_ must be instructed by CLI flags to act in a non-standard way. -- `--fake-http10-content-length` +- `--fake-http-content-length` - Adds a faked HTTP Content-Length to HTTP 1.0 responses. The length is set + Adds a faked HTTP Content-Length to HTTP 1.0/1.1 responses. The length is set to 100 GB and ensures that the device would keep playing for months. - This is necessary for the _Hame Soundrouter_. + This is e.g. necessary for the _Hame Soundrouter_ and depending on the used + encoder for _Sonos_ devices. ## Tested devices ## -_pulseaudio-dlna_ was successfully tested on the following devices / applications: - -- D-Link DCH-M225/E -- [Cocy UPNP media renderer](https://github.com/mnlipp/CoCy) -- BubbleUPnP (Android App) -- Samsung Smart TV LED60 (UE60F6300) -- Samsung Smart TV LED40 (UE40ES6100) -- Xbmc / Kodi -- Philips Streamium NP2500 Network Player -- Yamaha RX-475 (AV Receiver) -- Majik DSM -- [Pi MusicBox](http://www.woutervanwijk.nl/pimusicbox/) -- Google Chromecast -- Sonos PLAY:1 -- Sonos PLAY:3 -- Hame Soundrouter -- [Raumfeld Speaker M](http://raumfeld.com) -- Pioneer VSX-824 (AV Receiver) -- [ROCKI](http://www.myrocki.com/) -- Sony STR-DN1050 (AV Receiver) -- Pure Jongo S3 -- [Volumio](http://volumio.org) +A listed entry means that it was successfully tested, even if there is no specific +codec information available. + +Device | mp3 | wav | ogg | flac | aac | opus | l16 +------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- | ------------- +D-Link DCH-M225/E | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: +[Cocy UPNP media renderer](https://github.com/mnlipp/CoCy) | :white_check_mark: | :no_entry_sign: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: +BubbleUPnP (Android App) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :white_check_mark: +Samsung Smart TV LED60 (UE60F6300) | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Samsung Smart TV LED40 (UE40ES6100) | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Samsung Smart TV LED48 (UE48JU6560) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_circle:<sup>2</sup> | :no_entry_sign: | :no_entry_sign: +Xbmc / Kodi | :white_check_mark: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :white_circle:<sup>2</sup> | :white_circle:<sup>2</sup> | :white_check_mark: +Philips Streamium NP2500 Network Player | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Yamaha RX-475 (AV Receiver) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Majik DSM | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +[Pi MusicBox](http://www.woutervanwijk.nl/pimusicbox/) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Google Chromecast | :white_check_mark: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: +Sonos PLAY:1 | :white_check_mark:<sup>3</sup> | :white_check_mark: | :white_check_mark:<sup>3</sup> | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :grey_question: +Sonos PLAY:3 | :white_check_mark:<sup>3</sup> | :white_check_mark: | :white_check_mark:<sup>3</sup> | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :grey_question: +Hame Soundrouter | :white_check_mark:<sup>1</sup> | :no_entry_sign: | :no_entry_sign: | :white_check_mark:<sup>1</sup> | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: +[Raumfeld Speaker M](http://raumfeld.com) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Pioneer VSX-824 (AV Receiver) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +[ROCKI](http://www.myrocki.com/) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Sony STR-DN1050 (AV Receiver) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Pure Jongo S3 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +[Volumio](http://volumio.org) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Logitech Media Server | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: + +<sup>1</sup>) Works when specifing the `--fake-http-content-length` flag + +<sup>2</sup>) Is capable of playing the codec, but does not specifiy the correct mime type + +<sup>3</sup>) Works since _0.4.5_ (`--fake-http-content-length` is added automatic) ## Supported encoders ## -_pulseaudio-dlna_ supports the following encoders: - -- __lame__ MPEG Audio Layer III (MP3) -- __ogg__ Ogg Vorbis -- __flac__ Free Lossless Audio Codec (FLAC) -- __wav__ Waveform Audio File Format (WAV) -- __opus__ Opus Interactive Audio Codec (OPUS) -- __aac__ Advanced Audio Coding (AAC) +Encoder | Description | Identifier +------------- | ------------- | ------------- +lame | MPEG Audio Layer III | mp3 +oggenc | Ogg Vorbis | ogg +flac | Free Lossless Audio Codec | flac +sox | Waveform Audio File Format | wav +opusenc | Opus Interactive Audio Codec | opus +faac | Advanced Audio Coding | aac +sox | Linear PCM | l16 + +You can select a specific codec using the `--encoder` flag followed by its identifier.
View file
pulseaudio-dlna-0.4.4.tar.gz/debian/changelog -> pulseaudio-dlna-0.4.5.tar.gz/debian/changelog
Changed
@@ -1,3 +1,30 @@ +pulseaudio-dlna (0.4.5) trusty; urgency=low + + * Exceptions while updating sink and device information from pulseaudio + are now handled better + * Changed --fake-http10-content-length flag to --fake-http-content-length + to also support HTTP 1.1 requests + * Fixed a bug where the supported device mime types could not get parsed + correctly + * Fixed a bug where the device UUID was not parsed correctly + * Fixed a bug where just mime types beginning with audio/ where + accepted, but not e.g. application/ogg + * The stream server will now respond with 206 when receiving requests + with range header + * UPNP control commands have now a timeout of 10 seconds + * Fixed a bug where the wrong stream was removed from the stream manager + * Fixed several bugs caused by purely relying on stopping actions for + the devices idle state + * Added L16 Encoder + * The encoder option can now handle multiple options separated by comma + * Added the --create-device-config flag + * Fixed a bug where the dbus session was bound from the wrong process + * Fix a bug where the wrong device UDN was retrieved from XML documents + containing multiple devices + + -- Massimo Mund <mo@lancode.de> Sun, 20 Sep 2015 15:47:52 +0100 + + pulseaudio-dlna (0.4.4) trusty; urgency=low * Added '--disable-ssdp-listener' option
View file
pulseaudio-dlna-0.4.4.tar.gz/debian/pulseaudio-dlna.1 -> pulseaudio-dlna-0.4.5.tar.gz/debian/pulseaudio-dlna.1
Changed
@@ -1,17 +1,27 @@ .\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.44.1. -.TH PULSEAUDIO-DLNA "1" "August 2015" "pulseaudio-dlna 0.4.4" "User Commands" +.TH PULSEAUDIO-DLNA "1" "September 2015" "pulseaudio-dlna 0.4.5" "User Commands" .SH NAME -pulseaudio-dlna \- manual page for pulseaudio-dlna 0.4.4 +pulseaudio-dlna \- manual page for pulseaudio-dlna 0.4.5 .SH DESCRIPTION .SS "Usage:" .IP -pulseaudio\-dlna [\-\-host <host>] [\-\-port <port>] [\-\-encoder <encoder>] [\-\-bit\-rate=<rate>] [\-\-filter\-device=<filter\-device>] [\-\-renderer\-urls <urls>] [\-\-debug] [\-\-fake\-http10\-content\-length] [\-\-disable\-switchback] [\-\-disable\-ssdp\-listener] +pulseaudio\-dlna [\-\-host <host>] [\-\-port <port>] [\-\-encoder <encoders>] [\-\-bit\-rate=<rate>] [\-\-filter\-device=<filter\-device>] [\-\-renderer\-urls <urls>] [\-\-debug] [\-\-fake\-http10\-content\-length] [\-\-fake\-http\-content\-length] [\-\-disable\-switchback] [\-\-disable\-ssdp\-listener] +pulseaudio\-dlna [\-\-create\-device\-config] pulseaudio\-dlna [\-h | \fB\-\-help\fR | \fB\-\-version]\fR +.SH OPTIONS +.TP +\fB\-\-create\-device\-config\fR +Discovers all devices in your network and write a config for them. +That config can be editied manually to adjust various settings. +You can set: .IP -Note that _pulseaudio\-dlna_ has to run all the time while you are listening to your music. If you stop _pulseaudio\-dlna_ it will cleanly remove the created UPNP devices from PulseAudio and your UPNP devices will stop playing. +\- Device name +\- Codec order (The first one is used if the encoder binary is available on your system) +\- Various codec settings such as the mime type, specific rules or .IP -Since 0.4, new devices are automatically discovered as they appear on the network. -.SH OPTIONS +the bit rate (depends on the codec) +.IP +A written config is loaded by default if the \fB\-\-encoder\fR and \fB\-\-bit\-rate\fR options are not used. .TP \fB\-\-host=\fR<host> Set the server ip. @@ -19,7 +29,7 @@ \fB\-p\fR \fB\-\-port=\fR<port> Set the server port [default: 8080]. .TP -\fB\-e\fR \fB\-\-encoder=\fR<encoder> +\fB\-e\fR \fB\-\-encoder=\fR<encoders> Set the audio encoder. Possible encoders are: .TP @@ -27,7 +37,7 @@ MPEG Audio Layer III (MP3) .TP \- ogg -Ogg Vorbis +Ogg Vorbis (OGG) .TP \- flac Free Lossless Audio Codec (FLAC) @@ -39,7 +49,10 @@ Opus Interactive Audio Codec (OPUS) .TP \- aac -Advanced Audio Coding (FAAC) +Advanced Audio Coding (AAC) +.TP +\- l16 +Linear PCM (L16) .TP \fB\-b\fR \fB\-\-bit\-rate=\fR<rate> Set the audio encoder's bitrate. @@ -55,8 +68,8 @@ \fB\-\-debug\fR enables detailed debug messages. .TP -\fB\-\-fake\-http10\-content\-length\fR -If set, the content\-length of HTTP 1.0 requests will be set to 100 GB. +\fB\-\-fake\-http\-content\-length\fR +If set, the content\-length of HTTP requests will be set to 100 GB. .TP \fB\-\-disable\-switchback\fR If set, streams won't switched back to the default sink if a device disconnects.
View file
pulseaudio-dlna-0.4.4.tar.gz/pulseaudio_dlna/__main__.py -> pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/__main__.py
Changed
@@ -17,31 +17,37 @@ ''' Usage: - pulseaudio-dlna [--host <host>] [--port <port>] [--encoder <encoder>] [--bit-rate=<rate>] [--filter-device=<filter-device>] [--renderer-urls <urls>] [--debug] [--fake-http10-content-length] [--disable-switchback] [--disable-ssdp-listener] + pulseaudio-dlna [--host <host>] [--port <port>] [--encoder <encoders>] [--bit-rate=<rate>] [--filter-device=<filter-device>] [--renderer-urls <urls>] [--debug] [--fake-http10-content-length] [--fake-http-content-length] [--disable-switchback] [--disable-ssdp-listener] + pulseaudio-dlna [--create-device-config] pulseaudio-dlna [-h | --help | --version] - Note that _pulseaudio-dlna_ has to run all the time while you are listening to your music. If you stop _pulseaudio-dlna_ it will cleanly remove the created UPNP devices from PulseAudio and your UPNP devices will stop playing. - - Since 0.4, new devices are automatically discovered as they appear on the network. - Options: + --create-device-config Discovers all devices in your network and write a config for them. + That config can be editied manually to adjust various settings. + You can set: + - Device name + - Codec order (The first one is used if the encoder binary is available on your system) + - Various codec settings such as the mime type, specific rules or + the bit rate (depends on the codec) + A written config is loaded by default if the --encoder and --bit-rate options are not used. --host=<host> Set the server ip. -p --port=<port> Set the server port [default: 8080]. - -e --encoder=<encoder> Set the audio encoder. + -e --encoder=<encoders> Set the audio encoder. Possible encoders are: - mp3 MPEG Audio Layer III (MP3) - - ogg Ogg Vorbis + - ogg Ogg Vorbis (OGG) - flac Free Lossless Audio Codec (FLAC) - wav Waveform Audio File Format (WAV) - opus Opus Interactive Audio Codec (OPUS) - - aac Advanced Audio Coding (FAAC) + - aac Advanced Audio Coding (AAC) + - l16 Linear PCM (L16) -b --bit-rate=<rate> Set the audio encoder's bitrate. --filter-device=<filter-device> Set a name filter for devices which should be added. Devices which get discovered, but won't match the filter text will be skipped. --renderer-urls=<urls> Set the renderer urls yourself. no discovery will commence. --debug enables detailed debug messages. - --fake-http10-content-length If set, the content-length of HTTP 1.0 requests will be set to 100 GB. + --fake-http-content-length If set, the content-length of HTTP requests will be set to 100 GB. --disable-switchback If set, streams won't switched back to the default sink if a device disconnects. --disable-ssdp-listener If set, the application won't bind to the port 1900 and therefore the automatic discovery of new devices won't work. -v --version Show the version.
View file
pulseaudio-dlna-0.4.4.tar.gz/pulseaudio_dlna/application.py -> pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/application.py
Changed
@@ -17,17 +17,16 @@ from __future__ import unicode_literals -import dbus -import dbus.mainloop.glib import multiprocessing import signal import setproctitle import logging import sys import socket +import json +import os import pulseaudio_dlna -import pulseaudio_dlna.common import pulseaudio_dlna.listener import pulseaudio_dlna.plugins.upnp import pulseaudio_dlna.plugins.chromecast @@ -35,11 +34,24 @@ import pulseaudio_dlna.streamserver import pulseaudio_dlna.pulseaudio import pulseaudio_dlna.utils.network +import pulseaudio_dlna.rules logger = logging.getLogger('pulseaudio_dlna.application') class Application(object): + + ENCODING = 'utf-8' + DEVICE_CONFIG_PATHS = [ + os.path.expanduser('~/.local/share/pulseaudio-dlna'), + '/etc/pulseaudio-dlna', + ] + DEVICE_CONFIG = 'devices.json' + PLUGINS = [ + pulseaudio_dlna.plugins.upnp.DLNAPlugin(), + pulseaudio_dlna.plugins.chromecast.ChromecastPlugin(), + ] + def __init__(self): self.processes = [] @@ -57,6 +69,8 @@ def run(self, options): + logger.info('Using version: {}'.format(pulseaudio_dlna.__version__)) + if not options['--host']: host = pulseaudio_dlna.utils.network.default_ipv4() if host is None: @@ -72,52 +86,73 @@ logger.info('Using localhost: {host}:{port}'.format( host=host, port=port)) - plugins = [ - pulseaudio_dlna.plugins.upnp.DLNAPlugin(), - pulseaudio_dlna.plugins.chromecast.ChromecastPlugin(), - ] + if options['--create-device-config']: + self.create_device_config() + sys.exit(0) + + device_config = None + if not options['--encoder'] and not options['--bit-rate']: + device_config = self.read_device_config() if options['--encoder']: - for encoder in pulseaudio_dlna.common.supported_encoders: - if encoder.suffix == options['--encoder']: - pulseaudio_dlna.common.supported_encoders = [encoder] - break - if len(pulseaudio_dlna.common.supported_encoders) != 1: - logger.error('You specified an unknown encoder! ' - 'Application terminates.') - sys.exit(1) + for identifier, _type in pulseaudio_dlna.codecs.CODECS.iteritems(): + _type.ENABLED = False + for identifier in options['--encoder'].split(','): + try: + pulseaudio_dlna.codecs.CODECS[identifier].ENABLED = True + continue + except KeyError: + logger.error('You specified an unknown codec! ' + 'Application terminates.') + sys.exit(1) if options['--bit-rate']: - for encoder in pulseaudio_dlna.common.supported_encoders: - try: - encoder.bit_rate = options['--bit-rate'] - except pulseaudio_dlna.encoders.UnsupportedBitrateException: - if len(encoder.bit_rates) > 0: + try: + bit_rate = int(options['--bit-rate']) + except ValueError: + logger.error('Bit rates must be specified as integers!') + sys.exit(0) + for _type in pulseaudio_dlna.encoders.ENCODERS: + if hasattr(_type, 'DEFAULT_BIT_RATE') and \ + hasattr(_type, 'SUPPORTED_BIT_RATES'): + if bit_rate in _type.SUPPORTED_BIT_RATES: + _type.DEFAULT_BIT_RATE = bit_rate + else: logger.error( 'You specified an invalid bit rate ' - 'for the encoder! Supported bit rates ' + 'for the {encoder}!' + ' Supported bit rates ' 'are "{bit_rates}"! ' 'Application terminates.'.format( + encoder=_type().__class__.__name__, bit_rates=','.join( - str(e) for e in encoder.bit_rates))) - else: - logger.error('You selected encoder does not support ' - 'setting a specific bit rate! ' - 'Application terminates.') - sys.exit(1) + str(e) for e in _type.SUPPORTED_BIT_RATES + ))) + sys.exit(0) - logger.info('Loaded encoders:') - for encoder in pulseaudio_dlna.common.supported_encoders: + logger.info('Encoder settings:') + for _type in pulseaudio_dlna.encoders.ENCODERS: + encoder = _type() encoder.validate() - logger.info(encoder) + logger.info(' {}'.format(encoder)) + + logger.info('Codec settings:') + for identifier, _type in pulseaudio_dlna.codecs.CODECS.iteritems(): + codec = _type() + logger.info(' {}'.format(codec)) manager = multiprocessing.Manager() message_queue = multiprocessing.Queue() bridges = manager.list() - fake_http10_content_length = False + fake_http_content_length = False + if options['--fake-http-content-length']: + fake_http_content_length = True if options['--fake-http10-content-length']: - fake_http10_content_length = True + logger.warning( + 'The option "--fake-http10-content-length" is deprecated. ' + 'Please use "--fake-http-content-length" instead.') + fake_http_content_length = True disable_switchback = False if options['--disable-switchback']: @@ -130,8 +165,7 @@ try: stream_server = pulseaudio_dlna.streamserver.ThreadedStreamServer( host, port, bridges, message_queue, - fake_http10_content_length=fake_http10_content_length, - disable_switchback=disable_switchback, + fake_http_content_length=fake_http_content_length, ) except socket.error: logger.error( @@ -140,8 +174,10 @@ 'terminates.'.format(port=port)) sys.exit(1) - dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) - pulse = pulseaudio_dlna.pulseaudio.PulseWatcher(bridges, message_queue) + pulse = pulseaudio_dlna.pulseaudio.PulseWatcher( + bridges, message_queue, + disable_switchback=disable_switchback, + ) device_filter = None if options['--filter-device']: @@ -154,14 +190,14 @@ try: stream_server_address = stream_server.ip, stream_server.port ssdp_listener = pulseaudio_dlna.listener.ThreadedSSDPListener( - stream_server_address, message_queue, plugins, - device_filter, locations, disable_ssdp_listener) + stream_server_address, message_queue, self.PLUGINS, + device_filter, device_config, locations, disable_ssdp_listener) except socket.error: logger.error( 'The SSDP listener could not bind to the port 1900/UDP. ' - 'Perhaps this is already in use? Application terminates. ' - 'You can disable this feature with the ' - '"--disable-ssdp-listener" flag.') + 'Probably the port is in use by another application. ' + 'Terminate the application which is using the port or run this ' + 'application with the "--disable-ssdp-listener" flag.') sys.exit(1) self.run_process(target=stream_server.run) @@ -175,3 +211,79 @@ for process in self.processes: process.join() + + def create_device_config(self): + holder = pulseaudio_dlna.renderers.RendererHolder( + ('', 0), multiprocessing.Queue(), self.PLUGINS) + discover = pulseaudio_dlna.discover.RendererDiscover(holder) + discover.search() + + def device_filter(obj): + if hasattr(obj, 'to_json'): + return obj.to_json() + else: + return obj.__dict__ + + def obj_to_dict(obj): + json_text = json.dumps(obj, default=device_filter) + return json.loads(json_text) + + existing_config = self.read_device_config() + if existing_config: + new_config = obj_to_dict(holder.renderers) + new_config.update(existing_config) + else: + new_config = obj_to_dict(holder.renderers) + json_text = json.dumps(new_config, indent=4) + + for config_path in reversed(self.DEVICE_CONFIG_PATHS): + config_file = os.path.join(config_path, self.DEVICE_CONFIG) + if not os.path.exists(config_path): + try: + os.makedirs(config_path) + except (OSError, IOError): + continue + try: + with open(config_file, 'w') as h: + h.write(json_text.encode(self.ENCODING)) + logger.info('Found the following devices:') + for device in holder.renderers.values(): + logger.info('{name} ({flavour})'.format( + name=device.name, flavour=device.flavour)) + for codec in device.codecs: + logger.info(' - {}'.format( + codec.__class__.__name__)) + logger.info( + 'Your config was successfully written to "{}"'.format( + config_file)) + return + except (OSError, IOError): + continue + + logger.error( + 'Your device config could not be written to any of the ' + 'locations "{}"'.format(','.join(self.DEVICE_CONFIG_PATHS))) + + def read_device_config(self): + for config_path in self.DEVICE_CONFIG_PATHS: + config_file = os.path.join(config_path, self.DEVICE_CONFIG) + if os.path.isfile(config_file) and \ + os.access(config_file, os.R_OK): + with open(config_file, 'r') as h: + json_text = h.read().decode(self.ENCODING) + logger.debug( + 'Device configuration:\n{}'.format( + json_text)) + json_text = json_text.replace('\n', '') + try: + device_config = json.loads(json_text) + logger.info( + 'Loaded device config "{}"'.format( + config_file)) + return device_config + except ValueError: + logger.error( + 'Unable to parse "{}"! ' + 'Check the file for syntax errors ...'.format( + config_file)) + return None
View file
pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/codecs.py
Added
@@ -0,0 +1,271 @@ +#!/usr/bin/python + +# This file is part of pulseaudio-dlna. + +# pulseaudio-dlna is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# pulseaudio-dlna is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with pulseaudio-dlna. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import unicode_literals + +import functools +import logging +import re +import inspect +import sys + +import pulseaudio_dlna.encoders +import pulseaudio_dlna.rules + +logger = logging.getLogger('pulseaudio_dlna.codecs') + +CODECS = {} + + +@functools.total_ordering +class BaseCodec(object): + + ENABLED = True + IDENTIFIER = None + + def __init__(self): + self.mime_type = None + self.suffix = None + self.priority = None + self.rules = pulseaudio_dlna.rules.Rules() + + @property + def enabled(self): + return type(self).ENABLED + + @enabled.setter + def enabled(self, value): + type(self).ENABLED = value + + @property + def specific_mime_type(self): + return self.mime_type + + @classmethod + def accepts(cls, mime_type): + for accepted_mime_type in cls.SUPPORTED_MIME_TYPES: + if mime_type.lower().startswith(accepted_mime_type.lower()): + return True + return False + + def get_recorder(self, monitor): + return pulseaudio_dlna.recorders.PulseaudioRecorder(monitor) + + def __eq__(self, other): + return type(self) is type(other) + + def __gt__(self, other): + return type(self) is type(other) + + def __str__(self, detailed=False): + return '<{} enabled="{}" priority="{}" mime_type="{}">{}{}'.format( + self.__class__.__name__, + self.enabled, + self.priority, + self.specific_mime_type, + ('\n' if len(self.rules) > 0 else '') + '\n'.join( + [' - ' + str(rule) for rule in self.rules] + ) if detailed else '', + '\n ' + str(self.encoder) if detailed else '', + ) + + def to_json(self): + attributes = ['priority', 'suffix', 'mime_type'] + d = { + k: v for k, v in self.__dict__.iteritems() + if k not in attributes + } + d['mime_type'] = self.specific_mime_type + d['identifier'] = self.IDENTIFIER + return d + + +class BitRateMixin(object): + def __eq__(self, other): + return type(self) is type(other) and self.bit_rate == other.bit_rate + + def __gt__(self, other): + return type(self) is type(other) and self.bit_rate > other.bit_rate + + +@functools.total_ordering +class Mp3Codec(BitRateMixin, BaseCodec): + + SUPPORTED_MIME_TYPES = ['audio/mpeg', 'audio/mp3'] + IDENTIFIER = 'mp3' + + def __init__(self, mime_string=None): + BaseCodec.__init__(self) + self.priority = 18 + self.suffix = 'mp3' + self.mime_type = mime_string or 'audio/mp3' + + self.bit_rate = None + + @property + def encoder(self): + return pulseaudio_dlna.encoders.LameEncoder(self.bit_rate) + + +class WavCodec(BaseCodec): + + SUPPORTED_MIME_TYPES = ['audio/wav', 'audio/x-wav'] + IDENTIFIER = 'wav' + + def __init__(self, mime_string=None): + BaseCodec.__init__(self) + self.priority = 15 + self.suffix = 'wav' + self.mime_type = mime_string or 'audio/wav' + + @property + def encoder(self): + return pulseaudio_dlna.encoders.WavEncoder() + + +class L16Codec(BaseCodec): + + SUPPORTED_MIME_TYPES = ['audio/l16'] + IDENTIFIER = 'l16' + + def __init__(self, mime_string=None): + BaseCodec.__init__(self) + self.priority = 0 + self.suffix = 'pcm16' + self.mime_type = 'audio/L16' + + self.sample_rate = None + self.channels = None + + if mime_string: + match = re.match( + '(.*?)(?P<mime_type>.*?);' + '(.*?)rate=(?P<sample_rate>.*?);' + '(.*?)channels=(?P<channels>\d)', mime_string) + if match: + self.mime_type = match.group('mime_type') + self.sample_rate = int(match.group('sample_rate')) + self.channels = int(match.group('channels')) + + @property + def specific_mime_type(self): + if self.sample_rate and self.channels: + return '{};rate={};channels={}'.format( + self.mime_type, self.sample_rate, self.channels) + else: + return self.mime_type + + @property + def encoder(self): + return pulseaudio_dlna.encoders.L16Encoder( + self.sample_rate, self.channels) + + def __eq__(self, other): + return type(self) is type(other) and ( + self.sample_rate == other.sample_rate and + self.channels == other.channels) + + def __gt__(self, other): + return type(self) is type(other) and ( + self.sample_rate > other.sample_rate and + self.channels > other.channels) + + +@functools.total_ordering +class AacCodec(BitRateMixin, BaseCodec): + + SUPPORTED_MIME_TYPES = ['audio/aac', 'audio/x-aac'] + IDENTIFIER = 'aac' + + def __init__(self, mime_string=None): + BaseCodec.__init__(self) + self.priority = 12 + self.suffix = 'aac' + self.mime_type = mime_string or 'audio/aac' + + self.bit_rate = None + + @property + def encoder(self): + return pulseaudio_dlna.encoders.AacEncoder(self.bit_rate) + + +@functools.total_ordering +class OggCodec(BitRateMixin, BaseCodec): + + SUPPORTED_MIME_TYPES = ['audio/ogg', 'audio/x-ogg', 'application/ogg'] + IDENTIFIER = 'ogg' + + def __init__(self, mime_string=None): + BaseCodec.__init__(self) + self.priority = 6 + self.suffix = 'ogg' + self.mime_type = mime_string or 'audio/ogg' + + self.bit_rate = None + + @property + def encoder(self): + return pulseaudio_dlna.encoders.OggEncoder(self.bit_rate) + + +class FlacCodec(BaseCodec): + + SUPPORTED_MIME_TYPES = ['audio/flac', 'audio/x-flac'] + IDENTIFIER = 'flac' + + def __init__(self, mime_string=None): + BaseCodec.__init__(self) + self.priority = 9 + self.suffix = 'flac' + self.mime_type = mime_string or 'audio/flac' + + @property + def encoder(self): + return pulseaudio_dlna.encoders.FlacEncoder() + + +@functools.total_ordering +class OpusCodec(BitRateMixin, BaseCodec): + + SUPPORTED_MIME_TYPES = ['audio/opus', 'audio/x-opus'] + IDENTIFIER = 'opus' + + def __init__(self, mime_string=None): + BaseCodec.__init__(self) + self.priority = 3 + self.suffix = 'opus' + self.mime_type = mime_string or 'audio/opus' + + self.bit_rate = None + + @property + def encoder(self): + return pulseaudio_dlna.encoders.OpusEncoder(self.bit_rate) + + +def load_codecs(): + if len(CODECS) == 0: + logger.debug('Loaded codecs:') + for name, _type in inspect.getmembers(sys.modules[__name__]): + if inspect.isclass(_type) and issubclass(_type, BaseCodec): + if _type is not BaseCodec: + logger.debug(' {} = {}'.format(_type.IDENTIFIER, _type)) + CODECS[_type.IDENTIFIER] = _type + return None + +load_codecs()
View file
pulseaudio-dlna-0.4.4.tar.gz/pulseaudio_dlna/discover.py -> pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/discover.py
Changed
@@ -67,4 +67,6 @@ BaseUpnpMediaRendererDiscover.search(self, ttl, timeout, times) def _header_received(self, header, address): + logger.debug('Recieved the following SSDP header: \n{header}'.format( + header=header)) self.renderer_holder.add_from_search(header)
View file
pulseaudio-dlna-0.4.4.tar.gz/pulseaudio_dlna/encoders.py -> pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/encoders.py
Changed
@@ -17,8 +17,18 @@ from __future__ import unicode_literals -import functools import distutils.spawn +import inspect +import sys +import logging + +logger = logging.getLogger('pulseaudio_dlna.encoder') + +ENCODERS = [] + + +class InvalidBitrateException(): + pass class UnsupportedBitrateException(): @@ -29,19 +39,14 @@ pass -@functools.total_ordering class BaseEncoder(object): + + AVAILABLE = False + def __init__(self): self._binary = None - self._command = '' - self._mime_type = 'undefined' - self._mime_types = [] - self._suffix = 'undefined' + self._command = [] self._bit_rate = None - self._bit_rates = [] - self._priority = 0 - self._state = False - self._enabled = False @property def binary(self): @@ -49,26 +54,33 @@ @property def command(self): - return self._command.format(binary=self.binary) + return [self.binary] + self._command @property - def mime_type(self): - return self._mime_type + def available(self): + return type(self).AVAILABLE - @mime_type.setter - def mime_type(self, value): - if value in self._mime_types: - self._mime_type = value - else: - raise UnsupportedMimeTypeException() + def validate(self): + if not type(self).AVAILABLE: + result = distutils.spawn.find_executable(self.binary) + if result is not None and result.endswith(self.binary): + type(self).AVAILABLE = True + return type(self).AVAILABLE @property - def mime_types(self): - return self._mime_types + def supported_bit_rates(self): + raise UnsupportedBitrateException() - @property - def suffix(self): - return self._suffix + def __str__(self): + return '<{} available="{}">'.format( + self.__class__.__name__, + unicode(self.available), + ) + + +class BitRateMixin(object): + + DEFAULT_BIT_RATE = 192 @property def bit_rate(self): @@ -76,196 +88,185 @@ @bit_rate.setter def bit_rate(self, value): - if int(value) in self.bit_rates: + if int(value) in self.SUPPORTED_BIT_RATES: self._bit_rate = value else: raise UnsupportedBitrateException() @property - def bit_rates(self): - return self._bit_rates + def supported_bit_rates(self): + return self.SUPPORTED_BIT_RATES - @property - def priority(self): - return self._priority + def __str__(self): + return '<{} available="{}" bit-rate="{}">'.format( + self.__class__.__name__, + unicode(self.available), + unicode(self.bit_rate), + ) - @property - def state(self): - if self._enabled: - return self._state - return False - @property - def enabled(self): - return self._enabled +class NullEncoder(BaseEncoder): + + def __init__(self): + BaseEncoder.__init__(self) + self._binary = 'cat' + self._command = [] - @enabled.setter - def enabled(self, value): - self._enabled = value - def validate(self): - result = distutils.spawn.find_executable(self.binary) - if result is not None and result.endswith(self.binary): - self._state = True - return self._state +class LameEncoder(BitRateMixin, BaseEncoder): - def __eq__(self, other): - return self.priority == other.priority + SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, + 128, 160, 192, 224, 256, 320] - def __gt__(self, other): - return self.priority > other.priority + def __init__(self, bit_rate=None): + BaseEncoder.__init__(self) + self.bit_rate = bit_rate or LameEncoder.DEFAULT_BIT_RATE - def __str__(self): - return '<{} bit-rate="{}" state="{}" enabled="{}" mime-types="{}">'.format( - self.__class__.__name__, - unicode(self.bit_rate), - unicode(self.state), - unicode(self.enabled), - ','.join(self.mime_types), - ) + self._binary = 'lame' + self._command = ['-r', '-'] + + @property + def command(self): + if self.bit_rate is None: + return super(LameEncoder, self).command + else: + return [self.binary] + ['-b', str(self.bit_rate)] + self._command class WavEncoder(BaseEncoder): def __init__(self): BaseEncoder.__init__(self) self._binary = 'sox' - self._command = ('{binary} -t raw -b 16 -e signed -c 2 -r 44100 - -t wav ' - '-r 44100 -b 16 -L -e signed -c 2 -') - self._mime_type = 'audio/wav' - self._suffix = 'wav' - self._mime_types = ['audio/wav', 'audio/x-wav'] - self._bit_rate = None - self._bit_rates = [] - self._priority = 15 - self._enabled = True + self._command = ['-t', 'raw', '-b', '16', '-e', 'signed', '-c', '2', + '-r', '44100', '-', + '-t', 'wav', '-b', '16', '-e', 'signed', '-c', '2', + '-r', '44100', + '-L', '-', + ] + + +class L16Encoder(BaseEncoder): + def __init__(self, sample_rate=None, channels=None): + BaseEncoder.__init__(self) + self._sample_rate = sample_rate or 44100 + self._channels = channels or 2 + + self._binary = 'sox' + self._command = ['-t', 'raw', '-b', '16', '-e', 'signed', '-c', '2', + '-r', '44100', '-', + '-t', 'wav', '-b', '16', '-e', 'signed', + '-c', str(self.channels), + '-r', '44100', + '-B', '-', + 'rate', str(self.sample_rate), + ] @property - def bit_rate(self): - return self._bit_rate + def sample_rate(self): + return self._sample_rate - @bit_rate.setter - def bit_rate(self, value): - raise UnsupportedBitrateException() + @sample_rate.setter + def sample_rate(self, value): + self._sample_rate = int(value) + @property + def channels(self): + return self._channels -class LameEncoder(BaseEncoder): - def __init__(self): - BaseEncoder.__init__(self) - self._binary = 'lame' - self._command = '{binary} {bit_rate} -r -' - self._mime_type = 'audio/mpeg' - self._suffix = 'mp3' - self._mime_types = ['audio/mpeg', 'audio/mp3'] - self._bit_rate = 192 - self._bit_rates = [32, 40, 48, 56, 64, 80, 96, 112, + @channels.setter + def channels(self, value): + self._channels = int(value) + + def __str__(self): + return '<{} available="{}" sample-rate="{}" channels="{}">'.format( + self.__class__.__name__, + unicode(self.available), + unicode(self.sample_rate), + unicode(self.channels), + ) + + +class AacEncoder(BitRateMixin, BaseEncoder): + + SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] - self._priority = 18 - self._enabled = True + + def __init__(self, bit_rate=None): + BaseEncoder.__init__(self) + self.bit_rate = bit_rate or AacEncoder.DEFAULT_BIT_RATE + + self._binary = 'faac' + self._command = ['-X', '-P', '-o', '-', '-'] @property def command(self): if self.bit_rate is None: - return self._command.format(binary=self.binary, bit_rate='') + return super(AacEncoder, self).command else: - return self._command.format( - binary=self.binary, bit_rate='-b ' + str(self.bit_rate)) + return [self.binary] + ['-b', str(self.bit_rate)] + self._command -class AacEncoder(BaseEncoder): - def __init__(self): - BaseEncoder.__init__(self) - self._binary = 'faac' - self._command = '{binary} {bit_rate} -X -P -o - -' - self._mime_type = 'audio/aac' - self._suffix = 'aac' - self._mime_types = ['audio/aac', 'audio/x-aac'] - self._bit_rates = [32, 40, 48, 56, 64, 80, 96, 112, +class OggEncoder(BitRateMixin, BaseEncoder): + + SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320] - self._bit_rate = 192 - self._priority = 12 - self._enabled = True + + def __init__(self, bit_rate=None): + BaseEncoder.__init__(self) + self.bit_rate = bit_rate or OggEncoder.DEFAULT_BIT_RATE + + self._binary = 'oggenc' + self._command = ['-Q', '-r', '--ignorelength', '-'] @property def command(self): if self.bit_rate is None: - return self._command.format(binary=self.binary, bit_rate='') + return super(OggEncoder, self).command else: - return self._command.format( - binary=self.binary, bit_rate='-b ' + str(self.bit_rate)) + return [self.binary] + ['-b', str(self.bit_rate)] + self._command class FlacEncoder(BaseEncoder): - def __init__(self): + + def __init__(self, bit_rate=None): BaseEncoder.__init__(self) self._binary = 'flac' - self._command = ('{binary} - -c --channels 2 --bps 16 ' - '--sample-rate 44100 ' - '--endian little --sign signed -s') - self._mime_type = 'audio/flac' - self._suffix = 'flac' - self._mime_types = ['audio/flac', 'audio/x-flac'] - self._bit_rate = None - self._bit_rates = [] - self._priority = 9 - self._enabled = True + self._command = ['-', '-c', '--channels', '2', '--bps', '16', + '--sample-rate', '44100', + '--endian', 'little', '--sign', 'signed', '-s'] - @property - def bit_rate(self): - return self._bit_rate - @bit_rate.setter - def bit_rate(self, value): - raise UnsupportedBitrateException() +class OpusEncoder(BitRateMixin, BaseEncoder): + SUPPORTED_BIT_RATES = [i for i in range(6, 257)] -class OggEncoder(BaseEncoder): - def __init__(self): + def __init__(self, bit_rate=None): BaseEncoder.__init__(self) - self._binary = 'oggenc' - self._command = '{binary} {bit_rate} -Q -r -k --ignorelength -' - self._mime_type = 'audio/ogg' - self._suffix = 'ogg' - self._mime_types = ['audio/ogg', 'audio/x-ogg', 'application/ogg'] - self._bit_rate = 192 - self._priority = 6 - self._enabled = True - - @property - def bit_rate(self): - return self._bit_rate + self.bit_rate = bit_rate or OpusEncoder.DEFAULT_BIT_RATE - @bit_rate.setter - def bit_rate(self, value): - self._bit_rate = int(value) + self._binary = 'opusenc' + self._command = ['--padding', '0', '--max-delay', '0', + '--expect-loss', '1', '--framesize', '2.5', + '--raw-rate', '44100', + '--raw', '-', '-'] @property def command(self): if self.bit_rate is None: - return self._command.format(binary=self.binary, bit_rate='') + return super(OpusEncoder, self).command else: - return self._command.format( - binary=self.binary, bit_rate='-b ' + str(self.bit_rate)) + return [self.binary] + \ + ['--bitrate', str(self.bit_rate)] + self._command -class OpusEncoder(BaseEncoder): - def __init__(self): - BaseEncoder.__init__(self) - self._binary = 'opusenc' - self._command = ('{binary} {bit_rate} --padding 0 --max-delay 0 ' - '--expect-loss 1 --framesize 2.5 --raw-rate 44100 ' - '--raw --bitrate 64 - -') - self._mime_type = 'audio/opus' - self._suffix = 'opus' - self._mime_types = ['audio/opus', 'audio/x-opus'] - self._bit_rate = 192 - self._bit_rates = [i for i in range(6, 257)] - self._priority = 3 - self._enabled = True +def load_encoders(): + if len(ENCODERS) == 0: + logger.debug('Loaded encoders:') + for name, _type in inspect.getmembers(sys.modules[__name__]): + if inspect.isclass(_type) and issubclass(_type, BaseEncoder): + if _type is not BaseEncoder: + logger.debug(' {}'.format(_type)) + ENCODERS.append(_type) + return None - @property - def command(self): - if self.bit_rate is None: - return self._command.format(binary=self.binary, bit_rate='') - else: - return self._command.format( - binary=self.binary, bit_rate='--bitrate ' + str(self.bit_rate)) +load_encoders()
View file
pulseaudio-dlna-0.4.4.tar.gz/pulseaudio_dlna/listener.py -> pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/listener.py
Changed
@@ -46,12 +46,13 @@ class SSDPListener(SocketServer.UDPServer): def __init__( self, stream_server_address, message_queue, plugins, - device_filter=None, renderer_urls=None, + device_filter=None, device_config=None, renderer_urls=None, disable_ssdp_listener=False): self.disable_ssdp_listener = disable_ssdp_listener self.renderer_urls = renderer_urls self.renderers_holder = RendererHolder( - stream_server_address, message_queue, plugins, device_filter) + stream_server_address, message_queue, plugins, device_filter, + device_config) if not self.disable_ssdp_listener: SocketServer.UDPServer.__init__( self, ('', 1900), SSDPRequestHandler)
View file
pulseaudio-dlna-0.4.4.tar.gz/pulseaudio_dlna/plugins/chromecast/renderer.py -> pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/plugins/chromecast/renderer.py
Changed
@@ -26,25 +26,33 @@ import pycastv2 import pulseaudio_dlna.plugins.renderer +import pulseaudio_dlna.codecs logger = logging.getLogger('pulseaudio_dlna.plugins.chromecast.renderer') class ChromecastRenderer(pulseaudio_dlna.plugins.renderer.BaseRenderer): - def __init__(self, name, ip): - pulseaudio_dlna.plugins.renderer.BaseRenderer.__init__(self) + def __init__(self, name, ip, udn, model_name, model_number, manufacturer): + pulseaudio_dlna.plugins.renderer.BaseRenderer.__init__( + self, udn, model_name, model_number, manufacturer) self.flavour = 'Chromecast' self.name = name self.ip = ip self.port = 8009 self.state = self.IDLE - self.protocols = [ - 'audio/mp3', - 'audio/mp4', - 'audio/ogg', - 'audio/wav', - ] + self.codecs = [] + + def activate(self, config): + if config: + self.set_codecs_from_config(config) + else: + self.codecs = [ + pulseaudio_dlna.codecs.Mp3Codec(), + pulseaudio_dlna.codecs.AacCodec(), + pulseaudio_dlna.codecs.OggCodec(), + pulseaudio_dlna.codecs.WavCodec(), + ] def _get_media_player(self): try: @@ -63,7 +71,7 @@ if cast is None: return 500 try: - if cast.load(url, self.encoder.mime_type) is True: + if cast.load(url, self.codec.mime_type) is True: self.state = self.PLAYING return 200 return 500 @@ -89,9 +97,10 @@ class CoinedChromecastRenderer( pulseaudio_dlna.plugins.renderer.CoinedBaseRendererMixin, ChromecastRenderer): - def play(self): + def play(self, url=None, codec=None): try: - return ChromecastRenderer.play(self, self.get_stream_url()) + stream_url = url or self.get_stream_url() + return ChromecastRenderer.play(self, stream_url) except pulseaudio_dlna.plugins.renderer.NoSuitableEncoderFoundException: return 500 @@ -122,7 +131,11 @@ return None cast_device = type_( soup.root.device.friendlyname.text, - ip) + ip, + soup.root.device.udn.text, + soup.root.device.modelname.text, + None, + soup.root.device.manufacturer.text) return cast_device except AttributeError: logger.error(
View file
pulseaudio-dlna-0.4.4.tar.gz/pulseaudio_dlna/plugins/renderer.py -> pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/plugins/renderer.py
Changed
@@ -20,10 +20,11 @@ import re import random import urlparse +import urllib import functools import logging +import base64 -import pulseaudio_dlna.common import pulseaudio_dlna.pulseaudio logger = logging.getLogger('pulseaudio_dlna.plugins.renderer') @@ -41,7 +42,13 @@ PAUSE = 'paused' STOP = 'stopped' - def __init__(self): + def __init__(self, udn, model_name=None, model_number=None, + manufacturer=None): + self._udn = udn + self._model_name = model_name + self._model_number = model_number + self._manufacturer = manufacturer + self._name = None self._short_name = None self._label = None @@ -50,7 +57,39 @@ self._state = None self._encoder = None self._flavour = None - self._protocols = [] + self._codecs = [] + + @property + def udn(self): + return self._udn + + @udn.setter + def udn(self, value): + self._udn = value + + @property + def model_name(self): + return self._model_name + + @model_name.setter + def model_name(self, value): + self._model_name = value + + @property + def model_number(self): + return self._model_number + + @model_number.setter + def model_number(self, value): + self._model_number = value + + @property + def manufacturer(self): + return self._manufacturer + + @manufacturer.setter + def manufacturer(self, value): + self._manufacturer = value @property def name(self): @@ -101,25 +140,16 @@ self._state = value @property - def encoder(self): - if self._encoder is None: - for encoder in pulseaudio_dlna.common.supported_encoders: - if encoder.state is False: - continue - for mime_type in encoder.mime_types: - if mime_type in self.protocols: - return encoder - logger.info('There was no suitable encoder found for "{name}". ' - 'The device can play "{protocols}"'.format( - name=self.label, - protocols=','.join(self.protocols))) - raise NoSuitableEncoderFoundException() - else: - return self._encoder - - @encoder.setter - def encoder(self, value): - self._encoder = value + def codec(self): + for codec in self.codecs: + if codec.enabled and codec.encoder.available: + return codec + logger.info('There was no suitable codec found for "{name}". ' + 'The device can play "{codecs}"'.format( + name=self.label, + codecs=','.join( + [codec.mime_type for codec in self.codecs]))) + raise NoSuitableEncoderFoundException() @property def flavour(self): @@ -130,12 +160,12 @@ self._flavour = value @property - def protocols(self): - return self._protocols + def codecs(self): + return self._codecs - @protocols.setter - def protocols(self, value): - self._protocols = value + @codecs.setter + def codecs(self, value): + self._codecs = value def activate(self): pass @@ -149,6 +179,59 @@ def stop(self): raise NotImplementedError() + def add_mime_type(self, mime_type): + for identifier, _type in pulseaudio_dlna.codecs.CODECS.iteritems(): + if _type.accepts(mime_type): + codec = _type(mime_type) + if codec not in self.codecs: + self.codecs.append(codec) + + def prioritize_codecs(self): + + def sorting_algorithm(codec): + if isinstance(codec, pulseaudio_dlna.codecs.L16Codec): + value = codec.priority * 100000 + if codec.sample_rate: + value += codec.sample_rate / 1000 + if codec.channels: + value *= codec.channels + return value + else: + return codec.priority * 100000 + + self.codecs.sort(key=sorting_algorithm, reverse=True) + + def check_for_device_rules(self): + if self.manufacturer == 'Sonos, Inc.': + for codec in self.codecs: + if type(codec) in [ + pulseaudio_dlna.codecs.Mp3Codec, + pulseaudio_dlna.codecs.OggCodec]: + codec.rules.append( + pulseaudio_dlna.rules.FAKE_HTTP_CONTENT_LENGTH()) + if self.model_name == 'Kodi': + for codec in self.codecs: + if type(codec) is pulseaudio_dlna.codecs.WavCodec: + codec.mime_type = 'audio/mpeg' + + def set_codecs_from_config(self, config): + self.name = config['name'] + for codec_properties in config.get('codecs', []): + codec_type = pulseaudio_dlna.codecs.CODECS[ + codec_properties['identifier']] + codec = codec_type(codec_properties['mime_type']) + for k, v in codec_properties.iteritems(): + forbidden_attributes = ['mime_type', 'identifier', 'rules'] + if hasattr(codec, k) and k not in forbidden_attributes: + setattr(codec, k, v) + for rule in codec_properties.get('rules', []): + codec.rules.append(rule) + self.codecs.append(codec) + logger.debug( + 'Loaded the following device configuration:\n{}'.format( + self.__str__(True))) + return True + def __eq__(self, other): if isinstance(other, BaseRenderer): return self.short_name == other.short_name @@ -161,15 +244,30 @@ if isinstance(other, pulseaudio_dlna.pulseaudio.PulseBridge): return self.short_name > other.device.short_name - def __str__(self): - return '<{} name="{}" short="{}" state="{}" protocols="{}">'.format( - self.__class__.__name__, - self.name, - self.short_name, - self.state, - ','.join(self.protocols), + def __str__(self, detailed=False): + return ( + '<{} name="{}" short="{}" state="{}" udn="{}" model_name="{}" ' + 'model_number="{}" manufacturer="{}">{}').format( + self.__class__.__name__, + self.name, + self.short_name, + self.state, + self.udn, + self.model_name, + self.model_number, + self.manufacturer, + '\n' + '\n'.join([ + ' ' + codec.__str__(detailed) for codec in self.codecs + ]) if detailed else '', ) + def to_json(self): + return { + 'name': self.name, + 'flavour': self.flavour, + 'codecs': self.codecs, + } + class CoinedBaseRendererMixin(): @@ -185,9 +283,14 @@ ip=self.server_ip, port=self.server_port, ) - stream_name = '/{stream_name}.{suffix}'.format( - stream_name=self.short_name, - suffix=self.encoder.suffix, + settings = { + 'udn': self.udn, + } + data_string = ','.join( + ['{}={}'.format(k, v) for k, v in settings.iteritems()]) + stream_name = '/{base_string}/stream.{suffix}'.format( + base_string=urllib.quote(base64.b64encode(data_string)), + suffix=self.codec.suffix, ) return urlparse.urljoin(server_url, stream_name)
View file
pulseaudio-dlna-0.4.4.tar.gz/pulseaudio_dlna/plugins/upnp/renderer.py -> pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/plugins/upnp/renderer.py
Changed
@@ -26,7 +26,6 @@ import BeautifulSoup import pulseaudio_dlna.pulseaudio import pulseaudio_dlna.encoders -import pulseaudio_dlna.common import pulseaudio_dlna.plugins.renderer logger = logging.getLogger('pulseaudio_dlna.plugins.upnp.renderer') @@ -126,18 +125,20 @@ class UpnpMediaRenderer(pulseaudio_dlna.plugins.renderer.BaseRenderer): ENCODING = 'utf-8' + REQUEST_TIMEOUT = 10 - def __init__(self, name, ip, port, udn, services, encoder=None): - pulseaudio_dlna.plugins.renderer.BaseRenderer.__init__(self) + def __init__( + self, name, ip, port, udn, model_name, model_number, manufacturer, + services): + pulseaudio_dlna.plugins.renderer.BaseRenderer.__init__( + self, udn, model_name, model_number, manufacturer) self.flavour = 'DLNA' self.name = name self.ip = ip self.port = port self.state = self.IDLE - self.encoder = encoder - self.protocols = [] + self.codecs = [] - self.udn = udn self.xml = self._load_xml_files() self.service_transport = None self.service_connection = None @@ -152,8 +153,11 @@ if service.type == UpnpService.SERVICE_RENDERING: self.service_rendering = service - def activate(self): - self.get_protocol_info() + def activate(self, config): + if config: + self.set_codecs_from_config(config) + else: + self.get_protocol_info() def _load_xml_files(self): content = {} @@ -185,8 +189,9 @@ status_code=response.status_code, result=response.text)) - def register(self, stream_url): + def register(self, stream_url, codec=None): url = self.service_transport.control_url + codec = codec or self.codec headers = { 'Content-Type': 'text/xml; charset="{encoding}"'.format( @@ -202,9 +207,6 @@ UpnpContentFlags.CONNECTION_STALLING_SUPPORTED, UpnpContentFlags.DLNA_VERSION_15_SUPPORTED ]) - mime_type = self.encoder.mime_type - if isinstance(self.encoder, pulseaudio_dlna.encoders.WavEncoder): - mime_type = 'audio/mpeg' metadata = self.xml['register_metadata'].format( stream_url=stream_url, title='Live Audio', @@ -212,7 +214,7 @@ creator='PulseAudio', album='Stream', encoding=self.ENCODING, - mime_type=mime_type, + mime_type=codec.mime_type, content_features=str(content_features), ) data = self.xml['register'].format( @@ -221,10 +223,17 @@ encoding=self.ENCODING, service_type=self.service_transport.service_type, ) - response = requests.post( - url, data=data.encode(self.ENCODING), headers=headers) - self._debug('register', url, headers, data, response) - return response.status_code + try: + response = requests.post( + url, data=data.encode(self.ENCODING), + headers=headers, timeout=self.REQUEST_TIMEOUT) + self._debug('register', url, headers, data, response) + return response.status_code + except requests.exceptions.Timeout: + logger.error( + 'REGISTER command - Could no connect to {url}. ' + 'Connection timeout.'.format(url=url)) + return 408 def get_protocol_info(self): url = self.service_connection.control_url @@ -239,21 +248,32 @@ encoding=self.ENCODING, service_type=self.service_connection.service_type, ) - response = requests.post( - url, data=data.encode(self.ENCODING), headers=headers) - if response.status_code == 200: - soup = BeautifulSoup.BeautifulSoup(response.content) - try: - self.protocols = [] - sinks = soup('sink')[0].text - for sink in sinks.split(','): - http_get, w1, mime_type, w2 = sink.strip().split(':') - if mime_type.startswith('audio/'): - self.protocols.append(mime_type) - except IndexError: - logger.error( - 'IndexError: No valid XML returned from {url}.'.format( - url=url)) + try: + response = requests.post( + url, data=data.encode(self.ENCODING), + headers=headers, timeout=self.REQUEST_TIMEOUT) + if response.status_code == 200: + soup = BeautifulSoup.BeautifulSoup(response.content) + try: + self.codecs = [] + sinks = soup('sink')[0].text + logger.debug('Got the following mime types: "{}"'.format( + sinks)) + for sink in sinks.split(','): + attributes = sink.strip().split(':') + if len(attributes) >= 4: + self.add_mime_type(attributes[2]) + self.check_for_device_rules() + self.prioritize_codecs() + except IndexError: + logger.error( + 'IndexError: No valid XML returned from {url}.'.format( + url=url)) + except requests.exceptions.Timeout: + logger.error( + 'PROTOCOL_INFO command - Could no connect to {url}. ' + 'Connection timeout.'.format(url=url)) + return 408 self._debug('get_protocol_info', url, headers, data, response) return response.status_code @@ -271,12 +291,19 @@ encoding=self.ENCODING, service_type=self.service_transport.service_type, ) - response = requests.post( - url, data=data.encode(self.ENCODING), headers=headers) - if response.status_code == 200: - self.state = self.PLAYING - self._debug('play', url, headers, data, response) - return response.status_code + try: + response = requests.post( + url, data=data.encode(self.ENCODING), + headers=headers, timeout=self.REQUEST_TIMEOUT) + if response.status_code == 200: + self.state = self.PLAYING + self._debug('play', url, headers, data, response) + return response.status_code + except requests.exceptions.Timeout: + logger.error( + 'PLAY command - Could no connect to {url}. ' + 'Connection timeout.'.format(url=url)) + return 408 def stop(self): url = self.service_transport.control_url @@ -291,12 +318,19 @@ encoding=self.ENCODING, service_type=self.service_transport.service_type, ) - response = requests.post( - url, data=data.encode(self.ENCODING), headers=headers) - if response.status_code == 200: - self.state = self.IDLE - self._debug('stop', url, headers, data, response) - return response.status_code + try: + response = requests.post( + url, data=data.encode(self.ENCODING), + headers=headers, timeout=self.REQUEST_TIMEOUT) + if response.status_code == 200: + self.state = self.IDLE + self._debug('stop', url, headers, data, response) + return response.status_code + except requests.exceptions.Timeout: + logger.error( + 'STOP command - Could no connect to {url}. ' + 'Connection timeout.'.format(url=url)) + return 408 def pause(self): url = self.service_transport.control_url @@ -311,21 +345,28 @@ encoding=self.ENCODING, service_type=self.service_transport.service_type, ) - response = requests.post( - url, data=data.encode(self.ENCODING), headers=headers) - if response.status_code == 200: - self.state = self.PAUSE - self._debug('pause', url, headers, data, response) - return response.status_code + try: + response = requests.post( + url, data=data.encode(self.ENCODING), + headers=headers, timeout=self.REQUEST_TIMEOUT) + if response.status_code == 200: + self.state = self.PAUSE + self._debug('pause', url, headers, data, response) + return response.status_code + except requests.exceptions.Timeout: + logger.error( + 'PAUSE command - Could no connect to {url}. ' + 'Connection timeout.'.format(url=url)) + return 408 class CoinedUpnpMediaRenderer( pulseaudio_dlna.plugins.renderer.CoinedBaseRendererMixin, UpnpMediaRenderer): - def play(self): + def play(self, url=None, codec=None): try: - stream_url = self.get_stream_url() - if UpnpMediaRenderer.register(self, stream_url) == 200: + stream_url = url or self.get_stream_url() + if UpnpMediaRenderer.register(self, stream_url, codec) == 200: return UpnpMediaRenderer.play(self) else: logger.error('"{}" registering failed!'.format(self.name)) @@ -344,9 +385,14 @@ @classmethod def from_url(self, url, type_=UpnpMediaRenderer): try: - response = requests.get(url) + response = requests.get(url, timeout=5) logger.debug('Response from UPNP device ({url})\n' '{response}'.format(url=url, response=response.text)) + except requests.exceptions.Timeout: + logger.info( + 'Could no connect to {url}. ' + 'Connection timeout.'.format(url=url)) + return None except requests.exceptions.ConnectionError: logger.info( 'Could no connect to {url}. ' @@ -370,15 +416,19 @@ } services.append(service) upnp_device = type_( - soup.root.device.friendlyname.text, + device.friendlyname.text, ip, port, - soup.root.device.udn.text, + device.udn.text, + device.modelname.text if device.modelname else None, + device.modelnumber.text if device.modelnumber else None, + device.manufacturer.text if device.manufacturer else None, services) return upnp_device except AttributeError: logger.error( 'No valid XML returned from {url}.'.format(url=url)) + logger.info(response.content) return None @classmethod
View file
pulseaudio-dlna-0.4.4.tar.gz/pulseaudio_dlna/pulseaudio.py -> pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/pulseaudio.py
Changed
@@ -20,6 +20,7 @@ import sys import locale import dbus +import dbus.mainloop.glib import os import struct import subprocess @@ -29,6 +30,7 @@ import functools import copy import signal +import concurrent.futures import pulseaudio_dlna.plugins.renderer import pulseaudio_dlna.notification @@ -101,24 +103,41 @@ sys.exit(1) def update(self): - self.update_playback_streams() - self.update_sinks() - for stream in self.streams: - for sink in self.sinks: - if sink.object_path == stream.device: - sink.streams.append(stream) + def retry_on_fail(method, tries=5): + count = 1 + while not method(): + if count > tries: + return False + count += 1 + return True + + if retry_on_fail(self.update_playback_streams) and \ + retry_on_fail(self.update_sinks): + for stream in self.streams: + for sink in self.sinks: + if sink.object_path == stream.device: + sink.streams.append(stream) + else: + logger.error( + 'Could not update sinks and streams. This normally indicates a ' + 'problem with pulseaudio\'s dbus module. Try restarting ' + 'pulseaudio if the problem persists.') def update_playback_streams(self): - stream_paths = self.core.Get( - 'org.PulseAudio.Core1', 'PlaybackStreams', - dbus_interface='org.freedesktop.DBus.Properties') + try: + stream_paths = self.core.Get( + 'org.PulseAudio.Core1', 'PlaybackStreams', + dbus_interface='org.freedesktop.DBus.Properties') - self.streams = [] - for stream_path in stream_paths: - stream = PulseStreamFactory.new(self.bus, stream_path) - if stream: - self.streams.append(stream) + self.streams = [] + for stream_path in stream_paths: + stream = PulseStreamFactory.new(self.bus, stream_path) + if stream: + self.streams.append(stream) + return True + except dbus.exceptions.DBusException: + return False def update_sinks(self): try: @@ -132,11 +151,9 @@ if sink: sink.fallback_sink = self.fallback_sink self.sinks.append(sink) + return True except dbus.exceptions.DBusException: - logger.error( - 'Could not update sinks. This normally indicates a problem ' - 'with pulseaudio\'s dbus module. Try restarting pulseaudio ' - 'if the problem persists.') + return False def create_null_sink(self, sink_name, sink_description): cmd = [ @@ -374,7 +391,10 @@ class PulseWatcher(PulseAudio): - def __init__(self, bridges_shared, message_queue): + + ASYNC_EXECUTION = True + + def __init__(self, bridges_shared, message_queue, disable_switchback=False): PulseAudio.__init__(self) self.bridges = [] @@ -385,6 +405,18 @@ self.blocked_devices = [] self.signal_timers = {} + self.disable_switchback = disable_switchback + + def terminate(self, signal_number=None, frame=None): + self.cleanup() + sys.exit(0) + + def run(self): + signal.signal(signal.SIGINT, self.terminate) + signal.signal(signal.SIGTERM, self.terminate) + setproctitle.setproctitle('pulse_watcher') + + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) signals = ( ('NewPlaybackStream', 'org.PulseAudio.Core1.{}', self.on_new_playback_stream), @@ -399,14 +431,8 @@ self.update() self.default_sink = self.fallback_sink - def terminate(self, signal_number=None, frame=None): - self.cleanup() - sys.exit(0) + self.thread_pool = concurrent.futures.ThreadPoolExecutor(max_workers=1) - def run(self): - signal.signal(signal.SIGINT, self.terminate) - signal.signal(signal.SIGTERM, self.terminate) - setproctitle.setproctitle('pulse_watcher') mainloop = gobject.MainLoop() gobject.timeout_add(500, self._check_message_queue) try: @@ -433,15 +459,10 @@ def _unblock_device_handling(self, object_path): self.blocked_devices.remove(object_path) - def set_devices(self, devices): - self.devices = devices - self.update_bridges() - self.share_bridges() - def share_bridges(self): + bridges_copy = [bridge for bridge in copy.deepcopy(self.bridges)] del self.bridges_shared[:] - for bridge in copy.deepcopy(self.bridges): - self.bridges_shared.append(bridge) + self.bridges_shared.extend(bridges_copy) def update_bridges(self): for device in self.devices: @@ -450,14 +471,11 @@ device.short_name, device.label) self.bridges.append(PulseBridge(sink, device)) - def update(self): - PulseAudio.update(self) - self.share_bridges() - def cleanup(self): for bridge in self.bridges: logger.info('Remove "{}" sink ...'.format(bridge.sink.name)) self.delete_null_sink(bridge.sink.module.index) + self.bridges = [] def _was_stream_moved(self, moved_stream, ignore_sink): for sink in self.system_sinks: @@ -494,16 +512,24 @@ if sink == stopped_bridge.sink: stopped_bridge.sink = sink break + for bridge in self.bridges: + if bridge.device == stopped_bridge.device: + stopped_bridge.device = bridge.device + break - reason = 'The device disconnected' - if len(stopped_bridge.sink.streams) > 1: - self.switch_back(stopped_bridge, reason) - elif len(stopped_bridge.sink.streams) == 1: - stream = stopped_bridge.sink.streams[0] - if not self._was_stream_moved(stream, stopped_bridge.sink): + stopped_bridge.device.state = \ + pulseaudio_dlna.plugins.renderer.BaseRenderer.IDLE + + if not self.disable_switchback: + reason = 'The device disconnected' + if len(stopped_bridge.sink.streams) > 1: self.switch_back(stopped_bridge, reason) - elif len(stopped_bridge.sink.streams) == 0: - pass + elif len(stopped_bridge.sink.streams) == 1: + stream = stopped_bridge.sink.streams[0] + if not self._was_stream_moved(stream, stopped_bridge.sink): + self.switch_back(stopped_bridge, reason) + elif len(stopped_bridge.sink.streams) == 0: + pass def on_device_updated(self, sink_path): logger.info('on_device_updated "{path}"'.format( @@ -539,10 +565,24 @@ if self.signal_timers.get(sink_path, None): gobject.source_remove(self.signal_timers[sink_path]) self.signal_timers[sink_path] = gobject.timeout_add( - 500, self._handle_sink_update, sink_path) + 1000, self._handle_sink_update, sink_path) def _handle_sink_update(self, sink_path): - logger.info('_handle_sink_update {}'.format(sink_path)) + if not self.ASYNC_EXECUTION: + logger.info('_sync_handle_sink_update {}'.format(sink_path)) + result = self.__handle_sink_update(sink_path) + logger.info( + '_sync_handle_sink_update {} finished!'.format(sink_path)) + else: + logger.info('_async_handle_sink_update {}'.format(sink_path)) + future = self.thread_pool.submit( + self.__handle_sink_update, sink_path) + result = future.result() + logger.info( + '_async_handle_sink_update {} finished!'.format(sink_path)) + return result + + def __handle_sink_update(self, sink_path): if sink_path in self.signal_timers: del self.signal_timers[sink_path] @@ -551,17 +591,21 @@ return for bridge in self.bridges: + logger.debug('\n{}'.format(bridge)) if bridge.device.state == bridge.device.PLAYING: if len(bridge.sink.streams) == 0: logger.info( 'Instructing the device "{}" to stop ...'.format( bridge.device.label)) - if bridge.device.stop() == 200: + return_code = bridge.device.stop() + if return_code == 200: logger.info('The device "{}" was stopped.'.format( bridge.device.label)) else: - logger.error('The device "{}" failed to stop!'.format( - bridge.device.label)) + logger.error( + 'The device "{}" failed to stop! ({})'.format( + bridge.device.label, + return_code)) continue if bridge.sink.object_path == sink_path: if bridge.device.state == bridge.device.IDLE or \ @@ -569,14 +613,19 @@ logger.info( 'Instructing the device "{}" to play ...'.format( bridge.device.label)) - if bridge.device.play() == 200: + return_code = bridge.device.play() + if return_code == 200: logger.info('The device "{}" is playing.'.format( bridge.device.label)) else: - logger.error('The device "{}" failed to play!'.format( - bridge.device.label)) + logger.error( + 'The device "{}" failed to play! ({})'.format( + bridge.device.label, + return_code)) self.switch_back( - bridge, 'The device failed to started.') + bridge, + 'The device failed to start playing. ({})'.format( + return_code)) return False def add_device(self, device): @@ -585,6 +634,7 @@ device.short_name, device.label) self.bridges.append(PulseBridge(sink, device)) self.update() + self.share_bridges() logger.info('Added the device "{name} ({flavour})".'.format( name=device.name, flavour=device.flavour)) @@ -600,5 +650,6 @@ if bridge_index_to_remove is not None: self.bridges.pop(bridge_index_to_remove) self.update() + self.share_bridges() logger.info('Removed the device "{name}".'.format( name=device.name))
View file
pulseaudio-dlna-0.4.4.tar.gz/pulseaudio_dlna/recorders.py -> pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/recorders.py
Changed
@@ -20,7 +20,7 @@ class BaseRecorder(object): def __init__(self): - self._command = '' + self._command = [] @property def command(self): @@ -28,11 +28,24 @@ class PulseaudioRecorder(BaseRecorder): - def __init__(self, sink_path): + def __init__(self, monitor, _format=None): BaseRecorder.__init__(self) - self._command = 'parec --format=s16le -d {sink_path}' - self._sink_path = sink_path + self._monitor = monitor + self._format = _format + self._command = ['parec', '--format=s16le'] + + @property + def monitor(self): + return self._monitor + + @property + def format(self): + return self._format @property def command(self): - return self._command.format(sink_path=self._sink_path) + if not self.format: + return super(PulseaudioRecorder, self).command + ['-d', self.monitor] + else: + return super(PulseaudioRecorder, self).command + [ + '-d', self.monitor, '--file-format=' + self.format]
View file
pulseaudio-dlna-0.4.4.tar.gz/pulseaudio_dlna/renderers.py -> pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/renderers.py
Changed
@@ -31,11 +31,12 @@ def __init__( self, stream_server_address, message_queue, plugins, - device_filter=None): + device_filter=None, device_config=None): self.renderers = {} self.registered = {} self.stream_server_address = stream_server_address self.device_filter = device_filter + self.device_config = device_config or {} self.message_queue = message_queue self.lock = threading.Lock() for plugin in plugins: @@ -50,14 +51,14 @@ self.registered[identifier] = _type def _retrieve_header_map(self, header): - header = re.findall(r"(?P<name>.*?): (?P<value>.*?)\r\n", header) - header = {k.lower(): v for k, v in dict(header).items()} + header = re.findall(r"(?P<name>.*?): (?P<value>.*?)\n", header) + header = {k.lower(): v.strip() for k, v in dict(header).items()} return header def _retrieve_device_id(self, header): if 'usn' in header: match = re.search( - "(uuid:[0-9a-f\-]+)::.*", header['usn'], re.IGNORECASE) + "(uuid:.*?)::(.*)", header['usn'], re.IGNORECASE) if match: return match.group(1) return None @@ -70,7 +71,11 @@ name=device.name)) def _add_renderer(self, device_id, device): - device.activate() + config = self.device_config.get(device.udn, None) + device.activate(config) + if config: + logger.info( + 'Using device configuration:\n' + device.__str__(True)) ip, port = self.stream_server_address device.set_server_location(ip, port) self.renderers[device_id] = device
View file
pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/rules.py
Added
@@ -0,0 +1,137 @@ +#!/usr/bin/python + +# This file is part of pulseaudio-dlna. + +# pulseaudio-dlna is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# pulseaudio-dlna is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with pulseaudio-dlna. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import unicode_literals + +import functools +import logging +import inspect +import sys + +logger = logging.getLogger('pulseaudio_dlna.rules') + +RULES = {} + + +class RuleNotFoundException(Exception): + def __init__(self, identifier): + Exception.__init__( + self, + 'You specified an invalid rule identifier "{}"!'.format(identifier) + ) + + +@functools.total_ordering +class BaseRule(object): + def __str__(self): + return self.__class__.__name__ + + def __eq__(self, other): + if type(other) is type: + return type(self) is other + try: + if isinstance(other, basestring): + return type(self) is RULES[other] + except: + raise RuleNotFoundException(other) + return type(self) is type(other) + + def __gt__(self, other): + if type(other) is type: + return type(self) > other + try: + if isinstance(other, basestring): + return type(self) > RULES[other] + except: + raise RuleNotFoundException() + return type(self) > type(other) + + def to_json(self): + attributes = [] + d = { + k: v for k, v in self.__dict__.iteritems() + if k not in attributes + } + d['name'] = str(self) + return d + + +class FAKE_HTTP_CONTENT_LENGTH(BaseRule): + pass + + +# class EXAMPLE_PROPERTIES_RULE(BaseRule): +# def __init__(self, prop1=None, prop2=None): +# self.prop1 = prop1 or 'abc' +# self.prop2 = prop2 or 'def' + +# def __str__(self): +# return '{} (prop1="{}",prop2="{}")'.format( +# self.__class__.__name__, self.prop1, self.prop2) + + +class Rules(list): + def __init__(self, *args, **kwargs): + list.__init__(self, ()) + self.append(*args) + + def append(self, *args): + for arg in args: + if type(arg) is list: + for value in arg: + self.append(value) + elif type(arg) is dict: + try: + name = arg.get('name', 'missing') + rule = RULES[name]() + except KeyError: + raise RuleNotFoundException(name) + attributes = ['name'] + for k, v in arg.iteritems(): + if hasattr(rule, k) and k not in attributes: + setattr(rule, k, v) + self._add_rule(rule) + elif isinstance(arg, basestring): + try: + rule = RULES[arg]() + self._add_rule(rule) + except KeyError: + raise RuleNotFoundException(arg) + elif isinstance(arg, BaseRule): + self._add_rule(arg) + else: + raise RuleNotFoundException('?') + + def _add_rule(self, rule): + if rule not in self: + list.append(self, rule) + + def to_json(self): + return [rule.to_json() for rule in self] + + +def load_rules(): + if len(RULES) == 0: + logger.debug('Loaded rules:') + for name, _type in inspect.getmembers(sys.modules[__name__]): + if inspect.isclass(_type) and issubclass(_type, BaseRule): + if _type is not BaseRule: + logger.debug(' {} = {}'.format(name, _type)) + RULES[name] = _type + return None + +load_rules()
View file
pulseaudio-dlna-0.4.4.tar.gz/pulseaudio_dlna/streamserver.py -> pulseaudio-dlna-0.4.5.tar.gz/pulseaudio_dlna/streamserver.py
Changed
@@ -25,9 +25,12 @@ import time import socket import select +import sys import gobject import functools import atexit +import base64 +import urllib import json import os import signal @@ -35,8 +38,9 @@ import SocketServer import pulseaudio_dlna.encoders +import pulseaudio_dlna.codecs import pulseaudio_dlna.recorders -import pulseaudio_dlna.common +import pulseaudio_dlna.rules from pulseaudio_dlna.plugins.upnp.renderer import ( UpnpContentFeatures, UpnpContentFlags) @@ -51,6 +55,7 @@ class RemoteDevice(object): def __init__(self, bridge, sock): self.bridge = bridge + self.sock = sock try: self.ip, self.port = sock.getpeername() except: @@ -69,10 +74,19 @@ return self.ip > other.ip raise NotImplementedError + def __str__(self): + return '<{} socket="{}" ip="{}" port="{}">'.format( + self.__class__.__name__, + str(self.sock), + self.ip, + self.port, + ) + @functools.total_ordering class ProcessStream(object): def __init__(self, path, recorder, encoder, manager): + self.id = hex(id(self)) self.path = path self.recorder = recorder self.encoder = encoder @@ -114,16 +128,29 @@ def stop(self): self.do_stop = True - self.resume() + if self.is_running is False: + self.is_running = True + self.lock.release() def pause(self): self.is_running = False def resume(self): + if self.do_stop: + logger.error('Trying to resume a stopped thread!') if self.is_running is False: self.is_running = True self.lock.release() + @property + def state(self): + if self.do_stop: + return 'stopped' + if self.is_running: + return 'running' + else: + return 'paused' + self.update_thread = UpdateThread(self) self.update_thread.daemon = True self.update_thread.start() @@ -225,6 +252,18 @@ except socket.error: self.unregister(sock, lock_override=True, method=2) + for sock in r: + if sock in self.sockets: + try: + data = sock.recv(1024) + logger.info( + 'Read data from socket "{}"'.format(data)) + if len(data) == 0: + self.unregister(sock, lock_override=True, method=3) + except socket.error: + logger.error( + 'Error while reading from socket ...') + finally: self.lock.release() @@ -262,13 +301,13 @@ if self.reinitialize_count < 3: self.reinitialize_count += 1 logger.debug('Starting processes "{recorder} | {encoder}"'.format( - recorder=self.recorder.command, - encoder=self.encoder.command)) + recorder=' '.join(self.recorder.command), + encoder=' '.join(self.encoder.command))) self.recorder_process = subprocess.Popen( - self.recorder.command.split(' '), + self.recorder.command, stdout=subprocess.PIPE) self.encoder_process = subprocess.Popen( - self.encoder.command.split(' '), + self.encoder.command, stdin=self.recorder_process.stdout, stdout=subprocess.PIPE) self.recorder_process.stdout.close() @@ -282,6 +321,7 @@ self.update_thread.stop() for sock in self.sockets.keys(): sock.close() + logger.info('Thread exited for "{}".'.format(self.path)) def __eq__(self, other): if isinstance(other, ProcessStream): @@ -293,6 +333,15 @@ return self.path > other.path raise NotImplementedError + def __str__(self): + return '<{} id="{}" path="{}" state="{}">\n{}'.format( + self.__class__.__name__, + self.id, + self.path, + self.update_thread.state, + '\n'.join([' ' + str(device) for device in self.sockets.values()]), + ) + class StreamManager(object): def __init__(self, server): @@ -300,7 +349,7 @@ self.shared_streams = {} self.server = server - def _on_device_disconnect(self, device, stream): + def _on_device_disconnect(self, remote_device, stream): def _send_bridge_disconnected(bridge): logger.info('Device "{}" disconnected.'.format(bridge.device.name)) @@ -309,41 +358,52 @@ 'stopped_bridge': bridge, }) - if isinstance(stream.encoder, pulseaudio_dlna.encoders.WavEncoder): - self.single_streams.remove(stream) - if not self.server.disable_switchback: - if stream not in self.single_streams: - _send_bridge_disconnected(device.bridge) + if isinstance( + remote_device.bridge.device.codec, + pulseaudio_dlna.codecs.WavCodec): + self.single_streams = [ + s for s in self.single_streams if stream.id != s.id] + + if stream not in self.single_streams: + _send_bridge_disconnected(remote_device.bridge) stream.shutdown() else: - if not self.server.disable_switchback: - if device not in stream.sockets.values(): - _send_bridge_disconnected(device.bridge) - - def _create_stream(self, path, bridge, encoder): - recorder = pulseaudio_dlna.recorders.PulseaudioRecorder( - bridge.sink.monitor) - stream = ProcessStream(path, recorder, encoder, self) - return stream - - def get_stream(self, path, bridge, encoder): - if isinstance(encoder, pulseaudio_dlna.encoders.WavEncoder): - # always create a seperate process stream for wav encoders + if remote_device not in stream.sockets.values(): + _send_bridge_disconnected(remote_device.bridge) + + def _create_stream(self, path, bridge): + return ProcessStream( + path, + bridge.device.codec.get_recorder(bridge.sink.monitor), + bridge.device.codec.encoder, + self, + ) + + def get_stream(self, path, bridge): + if isinstance(bridge.device.codec, pulseaudio_dlna.codecs.WavCodec): + # always create a seperate process stream for wav codecs # since the client devices require the wav header which is # just send at the beginning of each encoding process - stream = self._create_stream(path, bridge, encoder) + stream = self._create_stream(path, bridge) self.single_streams.append(stream) return stream else: - # all other encoders can share a process stream depending + # all other codecs can share a process stream depending # on their path if path not in self.shared_streams: - stream = self._create_stream(path, bridge, encoder) + stream = self._create_stream(path, bridge) self.shared_streams[path] = stream return stream else: return self.shared_streams[path] + def __str__(self): + return '<{}>\n single:\n{}\n shared:\n{}\n'.format( + self.__class__.__name__, + '\n'.join([' ' + str(stream) for stream in self.single_streams]), + '\n'.join([' ' + str(stream) for stream in self.shared_streams.values()]), + ) + class StreamRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler): def __init__(self, *args): @@ -360,10 +420,9 @@ def do_GET(self): logger.debug('Got the following GET request:\n{header}'.format( header=json.dumps(self.headers.items(), indent=2))) - bridge, encoder = self.handle_headers() - if bridge and encoder: - stream = self.server.stream_manager.get_stream( - self.path, bridge, encoder) + bridge = self.handle_headers() + if bridge: + stream = self.server.stream_manager.get_stream(self.path, bridge) stream.register(bridge, self.request) self.keep_connection_alive() @@ -380,19 +439,30 @@ time.sleep(1) def handle_headers(self): - bridge, encoder = self.chop_request_path(self.path) - if encoder and bridge: - self.send_response(200) + bridge = self.chop_request_path(self.path) + if bridge: + response_code = 200 headers = { - 'Content-Type': encoder.mime_type, + 'Content-Type': bridge.device.codec.specific_mime_type, } - if self.request_version == PROTOCOL_VERSION_V10: - if self.server.fake_http10_content_length: - gb_in_bytes = 1073741824 - headers['Content-Length'] = gb_in_bytes * 100 - elif self.request_version == PROTOCOL_VERSION_V11: - headers['Connection'] = 'close' + if self.server.fake_http_content_length or \ + pulseaudio_dlna.rules.FAKE_HTTP_CONTENT_LENGTH in bridge.device.codec.rules: + gb_in_bytes = pow(1024, 3) + headers['Content-Length'] = gb_in_bytes * 100 + else: + if self.request_version == PROTOCOL_VERSION_V10: + pass + elif self.request_version == PROTOCOL_VERSION_V11: + headers['Connection'] = 'close' + + if self.headers.get('range'): + match = re.search( + 'bytes=(\d+)-(\d+)?', self.headers['range'], re.IGNORECASE) + if match: + start_range = int(match.group(1)) + if start_range != 0: + response_code = 206 if isinstance( bridge.device, @@ -408,43 +478,38 @@ headers['Ext'] = '' headers['transferMode.dlna.org'] = 'Streaming' - logger.debug('Sending header:\n{header}'.format( - header=json.dumps(headers, indent=2))) + logger.debug('Sending header ({response_code}):\n{header}'.format( + response_code=response_code, + header=json.dumps(headers, indent=2), + )) + self.send_response(response_code) for name, value in headers.items(): self.send_header(name, value) self.end_headers() - return bridge, encoder + return bridge else: logger.info('Error 404: File not found "{}"'.format(self.path)) self.send_error(404, 'File not found: %s' % self.path) - return None, None + return None def chop_request_path(self, path): - logger.info( - 'Requested streaming URL was: {path} ({version})'.format( - path=path, - version=self.request_version)) try: - short_name, suffix = re.findall(r"/(.*?)\.(.*)", path)[0] - - choosen_encoder = None - for encoder in pulseaudio_dlna.common.supported_encoders: - if encoder.suffix == suffix: - choosen_encoder = encoder - break - - choosen_bridge = None + data_quoted, suffix = re.findall(r'/(.*?)/stream\.(.*)', path)[0] + data_string = base64.b64decode(urllib.unquote(data_quoted)) + settings = { + k: v for k, v in + [pair.split('=') for pair in data_string.split(',')] + } + logger.info( + 'URL settings: {path} ({data_string})'.format( + path=path, + data_string=data_string)) for bridge in self.server.bridges: - if short_name == bridge.device.short_name: - choosen_bridge = bridge - break - - if choosen_bridge is not None and choosen_encoder is not None: - return bridge, encoder - + if settings.get('udn') == bridge.device.udn: + return bridge except (TypeError, ValueError, IndexError): pass - return None, None + return None def log_message(self, format, *args): args = [unicode(arg) for arg in args] @@ -458,7 +523,7 @@ def __init__( self, ip, port, bridges, message_queue, - fake_http10_content_length=False, disable_switchback=False, *args): + fake_http_content_length=False, *args): SocketServer.TCPServer.allow_reuse_address = True SocketServer.TCPServer.__init__( self, ('', port), StreamRequestHandler, *args) @@ -468,8 +533,7 @@ self.bridges = bridges self.message_queue = message_queue self.stream_manager = StreamManager(self) - self.fake_http10_content_length = fake_http10_content_length - self.disable_switchback = disable_switchback + self.fake_http_content_length = fake_http_content_length def get_server_url(self): return 'http://{ip}:{port}'.format(
View file
pulseaudio-dlna-0.4.5.tar.gz/scripts
Added
+(directory)
View file
pulseaudio-dlna-0.4.5.tar.gz/scripts/radio.py
Added
@@ -0,0 +1,137 @@ +#!/usr/bin/python + +# This file is part of pulseaudio-dlna. + +# pulseaudio-dlna is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# pulseaudio-dlna is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with pulseaudio-dlna. If not, see <http://www.gnu.org/licenses/>. + +from __future__ import unicode_literals + +import multiprocessing +import requests +import logging +import sys + +level = logging.INFO +logging.getLogger('requests').setLevel(logging.WARNING) +logging.getLogger('urllib3').setLevel(logging.WARNING) + +logging.basicConfig( + level=level, + format='%(asctime)s %(name)-46s %(levelname)-8s %(message)s', + datefmt='%m-%d %H:%M:%S') +logger = logging.getLogger('radio') + +import pulseaudio_dlna +import pulseaudio_dlna.renderers +import pulseaudio_dlna.discover +import pulseaudio_dlna.plugins.upnp +import pulseaudio_dlna.plugins.chromecast +import pulseaudio_dlna.codecs + + +class RadioLauncher(): + + PLUGINS = [ + pulseaudio_dlna.plugins.upnp.DLNAPlugin(), + pulseaudio_dlna.plugins.chromecast.ChromecastPlugin(), + ] + + def __init__(self): + self.devices = self._discover_devices() + + def stop(self, name, flavour=None): + device = self._get_device(name, flavour) + if device: + return_code = device.stop() + if return_code == 200: + logger.info( + 'The device "{name}" was instructed to stop'.format( + name=device.label)) + else: + logger.info( + 'The device "{name}" failed to stop ({code})'.format( + name=device.label, code=return_code)) + + def play(self, url, name, flavour=None): + if url.lower().endswith('.m3u'): + url = self._get_playlist_url(url) + codec = self._get_codec(url) + device = self._get_device(name, flavour) + if device: + return_code = device.play(url, codec) + if return_code == 200: + logger.info( + 'The device "{name}" was instructed to play'.format( + name=device.label)) + else: + logger.info( + 'The device "{name}" failed to play ({code})'.format( + name=device.label, code=return_code)) + + def _get_device(self, name, flavour=None): + for device in self.devices: + if flavour: + if device.name == name and device.flavour == flavour: + return device + else: + if device.name == name: + return device + return None + + def _get_codec(self, url): + for identifier, _type in pulseaudio_dlna.codecs.CODECS.iteritems(): + codec = _type() + if url.endswith(codec.suffix): + return codec + return pulseaudio_dlna.codecs.Mp3Codec() + + def _get_playlist_url(self, url): + response = requests.get(url=url) + for line in response.content.split('\n'): + if line.lower().startswith('http://'): + return line + return None + + def _discover_devices(self): + holder = pulseaudio_dlna.renderers.RendererHolder( + ('', 0), multiprocessing.Queue(), self.PLUGINS) + discover = pulseaudio_dlna.discover.RendererDiscover(holder) + discover.search() + logger.info('Found the following devices:') + for udn, device in holder.renderers.iteritems(): + logger.info(' - "{name}" ({flavour})'.format( + name=device.name, flavour=device.flavour)) + return holder.renderers.values() + +# Local pulseaudio-dlna installations running in a virutalenv should run this +# script as module: +# python -m scripts/radio [--list | --stop] + +args = sys.argv[1:] +rl = RadioLauncher() + +if len(args) > 0 and args[0] == '--list': + sys.exit(0) + +devices = [ + ('Wohnzimmer', 'DLNA'), + ('Kueche', 'DLNA'), +] + +for device in devices: + name, flavour = device + if len(args) > 0 and args[0] == '--stop': + rl.stop(name, flavour) + else: + rl.play('http://www.wdr.de/wdrlive/media/einslive.m3u', name, flavour)
Locations
Projects
Search
Status Monitor
Help
Open Build Service
OBS Manuals
API Documentation
OBS Portal
Reporting a Bug
Contact
Mailing List
Forums
Chat (IRC)
Twitter
Open Build Service (OBS)
is an
openSUSE project
.