Projects
Multimedia
pulseaudio-dlna
Sign Up
Log In
Username
Password
We truncated the diff of some files because they were too big. If you want to see the full diff for every file,
click here
.
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" + } + ] + }
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)
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' +
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
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,
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
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(
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
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
.