Projects
Multimedia
pulseaudio-dlna
Sign Up
Log In
Username
Password
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 14
View file
pulseaudio-dlna.changes
Changed
@@ -1,4 +1,29 @@ ------------------------------------------------------------------- +Wed Mar 9 21:52:48 UTC 2016 - antoine.belvire@laposte.net + +- Update to 0.5.0.1: + * Set Yamaha devices to the appropriate mode before playing + (thanks to hlchau) (new dependency: python-lxml) + * Fix a bug where some SSDP messages could not get parsed + correctly + * Also support media renderers identifying as + urn:schemas-upnp-org:device:MediaRenderer:2 + * Add the --disable-workarounds flag + * Add the --auto-reconnect flag + * Add the --encoder-backend option (new optional dependency: + ffmpeg) + * Remove shared encoder processes + * Increase the default HTTP timeout to 15 seconds + * Fix a bug where manually added renderers could appear twice + * Add device state polling for devices which start playing on + their own + * Add the flac encoder for Google Chromecast + * Add support for Google Cast Groups (new dependency + python-zeroconf) + * Remove dependency python-beautifulsoup + * Fix a bug where bytes were not decoded properly to unicode + +------------------------------------------------------------------- Tue Jan 5 21:33:22 UTC 2016 - antoine.belvire@laposte.net - Add python-setuptools as requirement (boo#960622)
View file
pulseaudio-dlna.spec
Changed
@@ -17,7 +17,7 @@ Name: pulseaudio-dlna -Version: 0.4.7 +Version: 0.5.0.1 Release: 0 Summary: A DLNA server which brings DLNA/UPnP support to PulseAudio License: GPL-3.0 @@ -32,11 +32,11 @@ Requires: opus-tools >= 0.1.8 Requires: pulseaudio Requires: python == 2.7 -Requires: python-beautifulsoup >= 3.2.1 Requires: python-chardet >= 2.0.1 Requires: python-docopt >= 0.6.1 Requires: python-futures >= 2.1.6 Requires: python-gobject >= 3.12.0 +Requires: python-lxml >= 3 Requires: python-netifaces >= 0.8 Requires: python-notify2 >= 0.3 Requires: python-protobuf >= 2.5.0 @@ -44,8 +44,10 @@ Requires: python-requests >= 2.2.1 Requires: python-setproctitle >= 1.0.1 Requires: python-setuptools +Requires: python-zeroconf >= 0.17 Requires: sox >= 14.4.1 Requires: vorbis-tools >= 1.4.0 +Recommends: ffmpeg Suggests: python-cairo >= 1.8.8 Suggests: python-rsvg >= 2.32.0 Suggests: python-gtk >= 2.24.0 @@ -71,7 +73,7 @@ %files %defattr(-,root,root) -%doc debian/changelog README.md LICENSE +%doc README.md LICENSE %{_bindir}/%{name} %{python_sitelib}/pulseaudio_dlna-%{version}-py%{py_ver}.egg-info/ %{python_sitelib}/pulseaudio_dlna/
View file
pulseaudio-dlna-0.4.7.tar.gz/debian
Deleted
-(directory)
View file
pulseaudio-dlna-0.4.7.tar.gz/debian/changelog
Deleted
@@ -1,182 +0,0 @@ -pulseaudio-dlna (0.4.7) trusty; urgency=low - - * The application can now co-exist with other applications which are - using the port 1900/udp - * Fixed the daemon mode to support `psutil` 1.x and 2.x - * HTML entities in device descriptions are now converted automatically - * Faster and more reliable device discovery - * Added the --cover-mode option, one mode requires - (optional) dependencies gtk, cairo, rsvg - * L16 codecs are now selected better (e.g. needed for _XBox 360_) - * Fixed a bug where sometimes it was tried to remove sinks twice on cleanup - * Added the --update-device-config flag - * Added the --ssdp-ttl, --ssdp-mx, --ssdp-amount options - * Added the --msearch-port option - - -- Massimo Mund <mo@lancode.de> Wed, 18 Nov 2015 00:48:12 +0100 - - -pulseaudio-dlna (0.4.6) trusty; urgency=low - - * Added support for Google Chromecast Audio - * Fixed a bug where devices which does not specifiy control urls made the - application crash - * Added the --disable-device-stop flag - * Added the --request-timeout option - * You can now also add rules to renderers - (e.g. DISABLE_DEVICE_STOP, REQUEST_TIMEOUT) - * Fixed a bug where stream urls where not parsed correctly - * Fixed a bug which made a Chomecast Audio throwing exceptions while - stopping - * Fixed a bug where the system's default encoding could not be determined - when piping the applications output - - -- Massimo Mund <mo@lancode.de> Sat, 17 Oct 2015 10:49:13 +0100 - - -pulseaudio-dlna (0.4.5.2) trusty; urgency=low - - * Fixed a bug where the encoding of SSDP headers was not detected correctly - - -- Massimo Mund <mo@lancode.de> Mon, 21 Sep 2015 17:33:28 +0100 - - -pulseaudio-dlna (0.4.5.1) trusty; urgency=low - - * Added a missing dependency python-concurrent.futures - - -- Massimo Mund <mo@lancode.de> Sun, 20 Sep 2015 20:50:14 +0100 - - -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 - * Fixed a bug with applications which remove and re-add streams all the time - * Added a missing dependency python-psutil - - -- Massimo Mund <mo@lancode.de> Fri, 07 Aug 2015 20:31:16 +0100 - - -pulseaudio-dlna (0.4.3) trusty; urgency=low - - * Fixed a bug when trying to terminate an encoder process - * Catch exceptions when trying to update pulseaudio sinks - * Fixed a timing issue where the streamserver was not ready but devices were already instructed to play - - -- Massimo Mund <mo@lancode.de> Sun, 02 Aug 2015 15:06:22 +0100 - - -pulseaudio-dlna (0.4.2) trusty; urgency=low - - * The mp3 encoder is now prioritize over wav - * Added '--disable-switchback' option - * Wav encoders do not longer share their encoder process - - -- Massimo Mund <mo@lancode.de> Sun, 02 Aug 2015 13:35:12 +0100 - - -pulseaudio-dlna (0.4.1) trusty; urgency=low - - * Fixed Makefile for launchpad - - -- Massimo Mund <mo@lancode.de> Mon, 27 Jul 2015 11:40:37 +0100 - - -pulseaudio-dlna (0.4.0) trusty; urgency=low - - * Added the --fake-http10-content-length option - * The application can now run as root - * Catch pulseaudio exceptions for streams, sinks and modules when those are - gone - * Fixed a bug where a missing ssdp header field made the application crash - * New devices are added now during runtime - * Rewrite of the streaming server - * Upnp devices can now request their audio format based on their capabilities - * Added AAC encoder - * If a device stops playing, the streams currently playing on the - corresponding sink are switched back to the default sink - * If a device failes to start playing, streams currently playing on the - corresponding sink are switched back to the default sink - * Added Chromecast support - * Fixed a bug where the application crashed when there was no suitable - encoder found - * Added the --bit-rate option - * Added additional headers for DLNA devices - * Added switch back mode also for sinks, not just for streams - * Added better logging - * Validate encoders for installed dependencies - - -- Massimo Mund <mo@lancode.de> Mon, 27 Jul 2015 10:23:02 +0100 - - -pulseaudio-dlna (0.3.5) trusty; urgency=low - - * Fixed a bug where Sonos description XML could not get parsed correctly - - -- Massimo Mund <mo@lancode.de> Sun, 09 Apr 2015 19:41:21 +0100 - - -pulseaudio-dlna (0.3.4) trusty; urgency=low - - * Fixed Makefile for launchpad - - -- Massimo Mund <mo@lancode.de> Sun, 22 Mar 2015 21:55:33 +0100 - - -pulseaudio-dlna (0.3.3) trusty; urgency=low - - * Added the --filter-device option - * Send 2 SSDP packets by default for better UPNP device discovery - * Added virtualenv for local installation - - -- Massimo Mund <mo@lancode.de> Sun, 22 Mar 2015 20:34:12 +0100 - - -pulseaudio-dlna (0.3.2) trusty; urgency=low - - * Added the Opus Encoder - * Fixed a bug where an empty UPNP device name made the application crash - * Added a missing dependency python-gobject - - -- Massimo Mund <mo@lancode.de> Sat, 14 Mar 2015 11:58:31 +0100 - - -pulseaudio-dlna (0.3.1) trusty; urgency=low - - * Fixed a bug so that AVTransports other than 1 can be used - - -- Massimo Mund <mo@lancode.de> Fri, 13 Feb 2015 20:01:12 +0100 - - -pulseaudio-dlna (0.3.0) trusty; urgency=low - - * Initial release - - -- Massimo Mund <mo@lancode.de> Sun, 01 Feb 2015 14:19:51 +0100
View file
pulseaudio-dlna-0.4.7.tar.gz/debian/compat
Deleted
@@ -1,1 +0,0 @@ -9
View file
pulseaudio-dlna-0.4.7.tar.gz/debian/control
Deleted
@@ -1,49 +0,0 @@ -Source: pulseaudio-dlna -Maintainer: Massimo Mund <mo@lancode.de> -Section: python -Priority: optional -Build-Depends: python-all, - python-dev, - python-pip, - python-setuptools, - python-dbus, - python-virtualenv | virtualenv, - git-core, - ca-certificates, - debhelper (>=9), - help2man, -Standards-Version: 3.9.5 - -Package: pulseaudio-dlna -Architecture: all -Depends: python2.7, - python-dbus (>=1.2.0), - python-setuptools (>=3.3), - python-beautifulsoup (>=3.2.1), - python-docopt (>=0.6.1), - python-requests (>=2.2.1), - python-setproctitle (>=1.0.1), - python-gobject (>=3.12.0), - python-protobuf (>=2.5.0), - python-notify2 (>=0.3), - python-psutil (>=1.2.1), - python-concurrent.futures (>=2.1.6), - python-chardet (>=2.0.1), - python-netifaces (>=0.8), - vorbis-tools (>=1.4.0), - sox (>=14.4.1), - lame (>=3.99.0), - flac (>=1.3.0), - faac (>=1.28), - opus-tools (>=0.1.8), - ${misc:Depends} -Suggests: python-cairo (>=1.8.8), - python-rsvg (>=2.32.0), - python-gtk2 (>=2.24.0), -Homepage: https://github.com/masmu/pulseaudio-dlna -Description: Stream audio to DLNA devices and Chromecasts - Creates Pulseaudio sinks for DLNA devices or Chromecasts in your network - and streams the current playback to those. - . - It's main goals are: - easy to use, no configuration hassle, no big dependencies.
View file
pulseaudio-dlna-0.4.7.tar.gz/debian/copyright
Deleted
@@ -1,9 +0,0 @@ -Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Contact: Massimo Mund <mo@lancode.de> -Source: https://github.com/masmu/pulseaudio-dlna - -Files: * -Copyright: - Copyright (C) 2014 Massimo Mund <mo@lancode.de> -License: GPL-3+ - /usr/share/common-licenses/GPL-3
View file
pulseaudio-dlna-0.4.7.tar.gz/debian/pulseaudio-dlna.1
Deleted
@@ -1,168 +0,0 @@ -.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.2. -.TH PULSEAUDIO-DLNA "1" "November 2015" "pulseaudio-dlna 0.4.7" "User Commands" -.SH NAME -pulseaudio-dlna \- Stream audio to DLNA devices and Chromecasts -.SH DESCRIPTION -.SS "Usage:" -.TP -pulseaudio\-dlna [\-\-host <host>] [\-\-port <port>][\-\-encoder <encoders>] [\-\-bit\-rate=<rate>] -[\-\-filter\-device=<filter\-device>] -[\-\-renderer\-urls <urls>] -[\-\-request\-timeout <timeout>] -[\-\-msearch\-port=<msearch\-port>] [\-\-ssdp\-mx <ssdp\-mx>] [\-\-ssdp\-ttl <ssdp\-ttl>] [\-\-ssdp\-amount <ssdp\-amount>] -[\-\-cover\-mode <mode>] -[\-\-debug] -[\-\-fake\-http10\-content\-length] [\-\-fake\-http\-content\-length] -[\-\-disable\-switchback] [\-\-disable\-ssdp\-listener] [\-\-disable\-device\-stop] -.TP -pulseaudio\-dlna [\-\-host <host>] [\-\-create\-device\-config] [\-\-update\-device\-config] -[\-\-msearch\-port=<msearch\-port>] [\-\-ssdp\-mx <ssdp\-mx>] [\-\-ssdp\-ttl <ssdp\-ttl>] [\-\-ssdp\-amount <ssdp\-amount>] -.IP -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: -.TP -\- 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 -.TP -the bit rate (depends on the codec) -A written config is loaded by default if the \fB\-\-encoder\fR and \fB\-\-bit\-rate\fR options are not used. -.TP -\fB\-\-update\-device\-config\fR -Same as \fB\-\-create\-device\-config\fR but preserves your existing config from being overwritten -.TP -\fB\-\-host=\fR<host> -Set the server ip. -.TP -\fB\-p\fR \fB\-\-port=\fR<port> -Set the server port [default: 8080]. -.TP -\fB\-e\fR \fB\-\-encoder=\fR<encoders> -Set the audio encoder. -Possible encoders are: -.TP -\- mp3 -MPEG Audio Layer III (MP3) -.TP -\- ogg -Ogg Vorbis (OGG) -.TP -\- flac -Free Lossless Audio Codec (FLAC) -.TP -\- wav -Waveform Audio File Format (WAV) -.TP -\- opus -Opus Interactive Audio Codec (OPUS) -.TP -\- aac -Advanced Audio Coding (AAC) -.TP -\- l16 -Linear PCM (L16) -.TP -\fB\-b\fR \fB\-\-bit\-rate=\fR<rate> -Set the audio encoder's bitrate. -.TP -\fB\-\-filter\-device=\fR<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. -.TP -\fB\-\-renderer\-urls=\fR<urls> -Set the renderer urls yourself. no discovery will commence. -.TP -\fB\-\-request\-timeout=\fR<timeout> -Set the timeout for requests in seconds [default: 10]. -.TP -\fB\-\-ssdp\-ttl=\fR<ssdp\-ttl> -Set the SSDP socket's TTL [default: 10]. -.TP -\fB\-\-ssdp\-mx=\fR<ssdp\-mx> -Set the MX value of the SSDP discovery message [default: 3]. -.TP -\fB\-\-ssdp\-amount=\fR<ssdp\-amount> -Set the amount of SSDP discovery messages being sent [default: 5]. -.TP -\fB\-\-msearch\-port=\fR<msearch\-port> -Set the source port of the MSEARCH socket [default: random]. -.TP -\fB\-\-cover\-mode=\fR<mode> -Set the cover mode [default: default]. -Possible modes are: -.TP -\- disabled -No icon is shown -.TP -\- default -The application icon is shown -.TP -\- distribution -The icon of your distribution is shown -.TP -\- application -The audio application's icon is shown -.TP -\fB\-\-debug\fR -enables detailed debug messages. -.TP -\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. -.TP -\fB\-\-disable\-ssdp\-listener\fR -If set, the application won't bind to the port 1900 and therefore the automatic discovery of new devices won't work. -.TP -\fB\-\-disable\-device\-stop\fR -If set, the application won't send any stop commands to renderers at all -.TP -\fB\-v\fR \fB\-\-version\fR -Show the version. -.TP -\fB\-h\fR \fB\-\-help\fR -Show the help. -.SH EXAMPLES -.IP -\- pulseaudio\-dlna -.IP -will start pulseaudio\-dlna on port 8080 and stream your PulseAudio streams encoded with mp3. -.IP -\- pulseaudio\-dlna \-\-encoder ogg -.IP -will start pulseaudio\-dlna on port 8080 and stream your PulseAudio streams encoded with Ogg Vorbis. -.IP -\- pulseaudio\-dlna \-\-port 10291 \-\-encoder flac -.IP -will start pulseaudio\-dlna on port 10291 and stream your PulseAudio streams encoded with FLAC. -.IP -\- pulseaudio\-dlna \-\-filter\-device 'Nexus 5,TV' -.IP -will just use devices named Nexus 5 or TV even when more devices got discovered. -.IP -\- pulseaudio\-dlna \-\-renderer\-urls http://192.168.1.7:7676/smp_10_ -.IP -won't discover upnp devices by itself. Instead it will search for upnp renderers -at the specified locations. You can specify multiple locations via urls -separated by comma (,). Most users won't ever need this option, but since -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. -.SH "SEE ALSO" -The full documentation for -.B pulseaudio-dlna -is maintained as a Texinfo manual. If the -.B info -and -.B pulseaudio-dlna -programs are properly installed at your site, the command -.IP -.B info pulseaudio-dlna -.PP -should give you access to the complete manual.
View file
pulseaudio-dlna-0.4.7.tar.gz/debian/rules
Deleted
@@ -1,7 +0,0 @@ -#!/usr/bin/make -f - -%: - dh $@ - -override_dh_auto_build: - # do nothing \ No newline at end of file
View file
pulseaudio-dlna-0.4.7.tar.gz/debian/source
Deleted
-(directory)
View file
pulseaudio-dlna-0.4.7.tar.gz/debian/source/format
Deleted
@@ -1,1 +0,0 @@ -3.0 (native)
View file
pulseaudio-dlna-0.4.7.tar.gz/debian/source/options
Deleted
@@ -1,5 +0,0 @@ -tar-ignore = ".git" -tar-ignore = ".gitignore" -tar-ignore = ".codeintel" -tar-ignore = "*.sublime-*" -tar-ignore = "samples" \ No newline at end of file
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/discover.py
Deleted
@@ -1,120 +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 socket -import logging -import chardet -import threading - -import pulseaudio_dlna.utils.network - -logger = logging.getLogger('pulseaudio_dlna.discover') - - -class SSDPDiscover(object): - - SSDP_ADDRESS = '239.255.255.250' - SSDP_PORT = 1900 - SSDP_MX = 3 - SSDP_TTL = 10 - SSDP_AMOUNT = 5 - - MSEARCH_PORT = 0 - MSEARCH_MSG = '\r\n'.join([ - 'M-SEARCH * HTTP/1.1', - 'HOST: {host}:{port}', - 'MAN: "ssdp:discover"', - 'MX: {mx}', - 'ST: ssdp:all', - ]) + '\r\n' * 2 - - BUFFER_SIZE = 1024 - USE_SINGLE_SOCKET = True - - def search(self, ssdp_ttl=None, ssdp_mx=None, ssdp_amount=None): - ssdp_mx = ssdp_mx or self.SSDP_MX - ssdp_ttl = ssdp_ttl or self.SSDP_TTL - ssdp_amount = ssdp_amount or self.SSDP_AMOUNT - - if self.USE_SINGLE_SOCKET: - logger.debug('Binding socket to "{}" ...'.format('')) - self._search('', ssdp_ttl, ssdp_mx, ssdp_amount) - else: - ips = pulseaudio_dlna.utils.network.ipv4_addresses() - threads = [] - for ip in ips: - logger.debug('Binding socket to "{}" ...'.format(ip)) - thread = threading.Thread( - target=self._search, - args=[ip, ssdp_ttl, ssdp_mx, ssdp_amount]) - threads.append(thread) - for thread in threads: - thread.start() - for thread in threads: - thread.join() - - def _search(self, host, ssdp_ttl, ssdp_mx, ssdp_amount): - sock = socket.socket( - socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) - sock.settimeout(ssdp_mx + 2) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.setsockopt( - socket.IPPROTO_IP, - socket.IP_MULTICAST_TTL, - ssdp_ttl) - sock.bind((host, self.MSEARCH_PORT)) - - for i in range(1, ssdp_amount + 1): - t = threading.Timer( - float(i) / 2, self._send_discover, args=[sock, ssdp_mx]) - t.start() - - while True: - try: - header, address = sock.recvfrom(self.BUFFER_SIZE) - guess = chardet.detect(header) - self._header_received( - header.decode(guess['encoding']), address) - except socket.timeout: - break - sock.close() - - def _send_discover(self, sock, ssdp_mx): - msg = self.MSEARCH_MSG.format( - host=self.SSDP_ADDRESS, port=self.SSDP_PORT, mx=ssdp_mx) - sock.sendto(msg, (self.SSDP_ADDRESS, self.SSDP_PORT)) - - def _header_received(self, header, address): - pass - - -class RendererDiscover(SSDPDiscover): - - def __init__(self, renderer_holder): - SSDPDiscover.__init__(self) - self.renderer_holder = renderer_holder - - def search(self, *args, **kwargs): - self.renderers = [] - SSDPDiscover.search(self, *args, **kwargs) - - def _header_received(self, header, address): - logger.debug('Recieved the following SSDP header: \n{header}'.format( - header=header)) - self.renderer_holder.process_msearch_request(header)
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/encoders.py
Deleted
@@ -1,272 +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 distutils.spawn -import inspect -import sys -import logging - -logger = logging.getLogger('pulseaudio_dlna.encoder') - -ENCODERS = [] - - -class InvalidBitrateException(): - pass - - -class UnsupportedBitrateException(): - pass - - -class UnsupportedMimeTypeException(): - pass - - -class BaseEncoder(object): - - AVAILABLE = True - - def __init__(self): - self._binary = None - self._command = [] - self._bit_rate = None - - @property - def binary(self): - return self._binary - - @property - def command(self): - return [self.binary] + self._command - - @property - def available(self): - return type(self).AVAILABLE - - 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 supported_bit_rates(self): - raise UnsupportedBitrateException() - - def __str__(self): - return '<{} available="{}">'.format( - self.__class__.__name__, - unicode(self.available), - ) - - -class BitRateMixin(object): - - DEFAULT_BIT_RATE = 192 - - @property - def bit_rate(self): - return self._bit_rate - - @bit_rate.setter - def bit_rate(self, value): - if int(value) in self.SUPPORTED_BIT_RATES: - self._bit_rate = value - else: - raise UnsupportedBitrateException() - - @property - def supported_bit_rates(self): - return self.SUPPORTED_BIT_RATES - - def __str__(self): - return '<{} available="{}" bit-rate="{}">'.format( - self.__class__.__name__, - unicode(self.available), - unicode(self.bit_rate), - ) - - -class NullEncoder(BaseEncoder): - - def __init__(self): - BaseEncoder.__init__(self) - self._binary = 'cat' - self._command = [] - - -class LameEncoder(BitRateMixin, BaseEncoder): - - SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, - 128, 160, 192, 224, 256, 320] - - def __init__(self, bit_rate=None): - BaseEncoder.__init__(self) - self.bit_rate = bit_rate or LameEncoder.DEFAULT_BIT_RATE - - 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 = ['-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 sample_rate(self): - return self._sample_rate - - @sample_rate.setter - def sample_rate(self, value): - self._sample_rate = int(value) - - @property - def channels(self): - return self._channels - - @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] - - 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 super(AacEncoder, self).command - else: - return [self.binary] + ['-b', str(self.bit_rate)] + self._command - - -class OggEncoder(BitRateMixin, BaseEncoder): - - SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, - 128, 160, 192, 224, 256, 320] - - 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 super(OggEncoder, self).command - else: - return [self.binary] + ['-b', str(self.bit_rate)] + self._command - - -class FlacEncoder(BaseEncoder): - - def __init__(self, bit_rate=None): - BaseEncoder.__init__(self) - self._binary = 'flac' - self._command = ['-', '-c', '--channels', '2', '--bps', '16', - '--sample-rate', '44100', - '--endian', 'little', '--sign', 'signed', '-s'] - - -class OpusEncoder(BitRateMixin, BaseEncoder): - - SUPPORTED_BIT_RATES = [i for i in range(6, 257)] - - def __init__(self, bit_rate=None): - BaseEncoder.__init__(self) - self.bit_rate = bit_rate or OpusEncoder.DEFAULT_BIT_RATE - - 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 super(OpusEncoder, self).command - else: - return [self.binary] + \ - ['--bitrate', str(self.bit_rate)] + self._command - - -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 - -load_encoders()
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/listener.py
Deleted
@@ -1,148 +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 SocketServer -import logging -import socket -import struct -import setproctitle -import time -import gobject -import os -import sys -import chardet - -import pulseaudio_dlna.discover - -logger = logging.getLogger('pulseaudio_dlna.listener') - - -class SSDPRequestHandler(SocketServer.BaseRequestHandler): - - def handle(self): - packet = self._decode(self.request[0]) - lines = packet.splitlines() - if len(lines) > 0: - if self._is_notify_method(lines[0]): - logger.debug( - 'Recieved the following NOTIFY header: \n{header}'.format( - header=packet)) - if self.server.holder: - self.server.holder.process_notify_request(packet) - - def _decode(self, data): - guess = chardet.detect(data) - for encoding in [guess['encoding'], 'utf-8', 'ascii']: - try: - return data.decode(encoding) - except: - pass - logger.error('Could not decode SSDP packet.') - return '' - - def _is_notify_method(self, method_header): - method = self._get_method(method_header) - return method == 'NOTIFY' - - def _get_method(self, method_header): - return method_header.split(' ')[0] - - -class SSDPListener(SocketServer.UDPServer): - - SSDP_ADDRESS = '239.255.255.250' - SSDP_PORT = 1900 - SSDP_TTL = 10 - - def __init__( - self, holder=None, - ssdp_ttl=None, - disable_ssdp_listener=False, - disable_ssdp_search=False): - self.holder = holder - self.ssdp_ttl = ssdp_ttl or self.SSDP_TTL - self.disable_ssdp_listener = disable_ssdp_listener - self.disable_ssdp_search = disable_ssdp_search - - def run(self): - if not self.disable_ssdp_listener: - self.allow_reuse_address = True - SocketServer.UDPServer.__init__( - self, ('', self.SSDP_PORT), SSDPRequestHandler) - self.socket.setsockopt( - socket.IPPROTO_IP, - socket.IP_ADD_MEMBERSHIP, - self._multicast_struct(self.SSDP_ADDRESS)) - self.socket.setsockopt( - socket.IPPROTO_IP, - socket.IP_MULTICAST_TTL, - self.ssdp_ttl) - - if not self.disable_ssdp_search: - gobject.timeout_add(100, self.search) - - setproctitle.setproctitle('ssdp_listener') - self.serve_forever(self) - - def search(self): - discover = pulseaudio_dlna.discover.RendererDiscover(self.holder) - discover.search() - logger.info('Discovery complete.') - - def _multicast_struct(self, address): - return struct.pack( - '4sl', socket.inet_aton(address), socket.INADDR_ANY) - - -class GobjectMainLoopMixin: - - def serve_forever(self, poll_interval=0.5): - self.mainloop = gobject.MainLoop() - if hasattr(self, 'socket'): - gobject.io_add_watch( - self, gobject.IO_IN | gobject.IO_PRI, self._on_new_request) - context = self.mainloop.get_context() - while True: - try: - if context.pending(): - context.iteration(True) - else: - time.sleep(0.1) - except KeyboardInterrupt: - break - - def _on_new_request(self, sock, *args): - self._handle_request_noblock() - return True - - def shutdown(self, *args): - logger.debug( - 'SSDPListener GobjectMainLoopMixin.shutdown() pid: {}'.format( - os.getpid())) - try: - self.socket.shutdown(socket.SHUT_RDWR) - except socket.error: - pass - self.socket.close() - sys.exit(0) - - -class ThreadedSSDPListener( - GobjectMainLoopMixin, SocketServer.ThreadingMixIn, SSDPListener): - pass
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/renderers.py
Deleted
@@ -1,139 +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 re -import logging -import threading - -logger = logging.getLogger('pulseaudio_dlna.renderers') - - -class RendererHolder(object): - - SSDP_ALIVE = 'ssdp:alive' - SSDP_BYEBYE = 'ssdp:byebye' - - def __init__( - self, plugins, - stream_ip=None, stream_port=None, message_queue=None, - device_filter=None, device_config=None): - self.renderers = {} - self.registered = {} - self.stream_ip = stream_ip - self.stream_port = stream_port - self.device_filter = device_filter - self.device_config = device_config or {} - self.message_queue = message_queue - self.lock = threading.Lock() - for plugin in plugins: - self.registered[plugin.st_header] = plugin - - def _retrieve_header_map(self, header): - 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:.*?)::(.*)", header['usn'], re.IGNORECASE) - if match: - return match.group(1) - return None - - def process_locations(self, locations): - try: - self.lock.acquire() - for plugin in self.registered.values(): - for device in plugin.lookup(locations): - self._add_renderer(device.udn, device) - finally: - self.lock.release() - - def process_msearch_request(self, header): - header = self._retrieve_header_map(header) - device_id = self._retrieve_device_id(header) - - if device_id is None: - return - try: - self.lock.acquire() - st_header = header.get('st', None) - if st_header and st_header in self.registered: - if device_id not in self.renderers: - device = self.registered[st_header].create_device(header) - if device is not None: - self._add_renderer_with_filter_check(device_id, device) - finally: - self.lock.release() - - def process_notify_request(self, header): - header = self._retrieve_header_map(header) - device_id = self._retrieve_device_id(header) - - if device_id is None: - return - try: - self.lock.acquire() - nts_header = header.get('nts', None) - nt_header = header.get('nt', None) - if nt_header and nts_header and nt_header in self.registered: - if (nts_header == self.SSDP_ALIVE and - device_id not in self.renderers): - plugin = self.registered[nt_header] - device = plugin.create_device(header) - if device is not None: - self._add_renderer_with_filter_check(device_id, device) - elif (nts_header == self.SSDP_BYEBYE and - device_id in self.renderers): - self._remove_renderer_by_id(device_id) - finally: - self.lock.release() - - def _add_renderer_with_filter_check(self, device_id, device): - if self.device_filter is None or device.name in self.device_filter: - self._add_renderer(device_id, device) - else: - logger.info('Skipped the device "{name}" ...'.format( - name=device.label)) - - def _add_renderer(self, device_id, device): - if device.validate(): - config = self.device_config.get(device.udn, None) - device.activate(config) - if config: - logger.info( - 'Using device configuration:\n' + device.__str__(True)) - if self.stream_ip and self.stream_port: - device.set_server_location(self.stream_ip, self.stream_port) - self.renderers[device_id] = device - if self.message_queue: - self.message_queue.put({ - 'type': 'add_device', - 'device': device - }) - - def _remove_renderer_by_id(self, device_id): - device = self.renderers[device_id] - if self.message_queue: - self.message_queue.put({ - 'type': 'remove_device', - 'device': device - }) - del self.renderers[device_id]
View file
pulseaudio-dlna-0.4.7.tar.gz/Makefile -> pulseaudio-dlna-0.5.0.1.tar.gz/Makefile
Changed
@@ -43,11 +43,11 @@ lintian --pedantic dist/*.deb dist/*.dsc dist/*.changes sudo chown -R $(user) dist/ -manpage: debian/pulseaudio-dlna.1 +manpage: man/pulseaudio-dlna.1 -debian/pulseaudio-dlna.1: pulseaudio_dlna.egg-info +man/pulseaudio-dlna.1: pulseaudio_dlna.egg-info export USE_PKG_VERSION=1; help2man -n "Stream audio to DLNA devices and Chromecasts" "bin/pulseaudio-dlna" > /tmp/pulseaudio-dlna.1 - mv /tmp/pulseaudio-dlna.1 debian/pulseaudio-dlna.1 + mv /tmp/pulseaudio-dlna.1 man/pulseaudio-dlna.1 clean: rm -rf build dist $(shell find pulseaudio_dlna -name "__pycache__")
View file
pulseaudio-dlna-0.4.7.tar.gz/README.md -> pulseaudio-dlna-0.5.0.1.tar.gz/README.md
Changed
@@ -35,6 +35,25 @@ ## Changelog ## + * __0.5.0.1__ - (_2016-03-09_) + - Readded manpage + + * __0.5.0__ - (_2016-03-09_) + - Set Yamaha devices to the appropriate mode before playing (thanks to [hlchau](https://github.com/hlchau)) (new dependency: `python-lxml`) + - Fixed a bug where some SSDP messages could not get parsed correctly + - Also support media renderers identifying as `urn:schemas-upnp-org:device:MediaRenderer:2` + - Added the `--disable-workarounds` flag + - Added the `--auto-reconnect` flag + - Added the `--encoder-backend` option (new optional dependencies `ffmpeg`, `libav-tools`) + - Removed shared encoder processes + - Increased the default HTTP timeout to 15 seconds + - Fixed a bug where manually added renderers could appear twice + - Added device state polling for devices which start playing on their own + - Added the flac encoder for _Google Chromecast_ + - Added support for _Google Cast Groups_ (new dependency `python-zeroconf`) + - Removed dependency `python-beautifulsoup` + - Fixed a bug where bytes were not decoded properly to unicode + * __0.4.7__ - (_2015-11-18_) - The application can now co-exist with other applications which are using the port 1900/udp (thanks to [klaernie](https://github.com/klaernie)) - Fixed the daemon mode to support `psutil` 1.x and 2.x (thanks to [klaernie](https://github.com/klaernie)) @@ -163,7 +182,6 @@ Supported Ubuntu releases: - 15.10 (Wily Werewolf) -- 15.04 (Vivid Vervet) - 14.04.2 LTS (Trusty Tahr) Ubuntu users can install _pulseaudio-dlna_ via the following [repository](https://launchpad.net/~qos/+archive/ubuntu/pulseaudio-dlna). @@ -191,6 +209,8 @@ [http://packman.links2linux.de/package/pulseaudio-dlna](http://packman.links2linux.de/package/pulseaudio-dlna) - Fedora - RHEL - CentOS - EPEL [https://copr.fedoraproject.org/coprs/cygn/pulseaudio-dlna/](https://copr.fedoraproject.org/coprs/cygn/pulseaudio-dlna/) +- Debian + [https://packages.debian.org/sid/pulseaudio-dlna](https://packages.debian.org/sid/pulseaudio-dlna) ## Installation via git ## @@ -207,7 +227,6 @@ - python-pip - python-setuptools - python-dbus -- python-beautifulsoup - python-docopt - python-requests - python-setproctitle @@ -218,6 +237,8 @@ - python-concurrent.futures - python-chardet - python-netifaces +- python-lxml +- python-zeroconf - vorbis-tools - sox - lame @@ -227,7 +248,7 @@ You can install all the dependencies in Ubuntu via: - sudo apt-get install python2.7 python-pip python-setuptools python-dbus python-beautifulsoup python-docopt python-requests python-setproctitle python-gobject python-protobuf python-notify2 python-psutil python-concurrent.futures python-chardet python-netifaces vorbis-tools sox lame flac faac opus-tools + sudo apt-get install python2.7 python-pip python-setuptools python-dbus python-docopt python-requests python-setproctitle python-gobject python-protobuf python-notify2 python-psutil python-concurrent.futures python-chardet python-netifaces python-lxml python-zeroconf vorbis-tools sox lame flac faac opus-tools ### PulseAudio DBus module ### @@ -318,15 +339,17 @@ ### CLI ### Usage: - pulseaudio-dlna [--host <host>] [--port <port>][--encoder <encoders>] [--bit-rate=<rate>] + pulseaudio-dlna [--host <host>] [--port <port>][--encoder <encoders> | --codec <codec>] [--bit-rate=<rate>] + [--encoder-backend <encoder-backend>] [--filter-device=<filter-device>] [--renderer-urls <urls>] [--request-timeout <timeout>] [--msearch-port=<msearch-port>] [--ssdp-mx <ssdp-mx>] [--ssdp-ttl <ssdp-ttl>] [--ssdp-amount <ssdp-amount>] [--cover-mode <mode>] + [--auto-reconnect] [--debug] [--fake-http10-content-length] [--fake-http-content-length] - [--disable-switchback] [--disable-ssdp-listener] [--disable-device-stop] + [--disable-switchback] [--disable-ssdp-listener] [--disable-device-stop] [--disable-workarounds] pulseaudio-dlna [--host <host>] [--create-device-config] [--update-device-config] [--msearch-port=<msearch-port>] [--ssdp-mx <ssdp-mx>] [--ssdp-ttl <ssdp-ttl>] [--ssdp-amount <ssdp-amount>] pulseaudio-dlna [-h | --help | --version] @@ -343,8 +366,9 @@ --update-device-config Same as --create-device-config but preserves your existing config from being overwritten --host=<host> Set the server ip. -p --port=<port> Set the server port [default: 8080]. - -e --encoder=<encoders> Set the audio encoder. - Possible encoders are: + -e --encoder=<encoders> Deprecated alias for --codec + -c --codec=<codecs> Set the audio codec. + Possible codecs are: - mp3 MPEG Audio Layer III (MP3) - ogg Ogg Vorbis (OGG) - flac Free Lossless Audio Codec (FLAC) @@ -352,12 +376,17 @@ - opus Opus Interactive Audio Codec (OPUS) - aac Advanced Audio Coding (AAC) - l16 Linear PCM (L16) + --encoder-backend=<encoder-backend> Set the backend for all encoders. + Possible backends are: + - generic (default) + - ffmpeg + - avconv -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. - --request-timeout=<timeout> Set the timeout for requests in seconds [default: 10]. + --request-timeout=<timeout> Set the timeout for requests in seconds [default: 15]. --ssdp-ttl=<ssdp-ttl> Set the SSDP socket's TTL [default: 10]. --ssdp-mx=<ssdp-mx> Set the MX value of the SSDP discovery message [default: 3]. --ssdp-amount=<ssdp-amount> Set the amount of SSDP discovery messages being sent [default: 5]. @@ -369,10 +398,12 @@ - distribution The icon of your distribution is shown - application The audio application's icon is shown --debug enables detailed debug messages. + --auto-reconnect If set, the application tries to reconnect devices in case the stream collapsed --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. --disable-device-stop If set, the application won't send any stop commands to renderers at all + --disable-workarounds If set, the application won't apply any device workarounds -v --version Show the version. -h --help Show the help. @@ -603,40 +634,52 @@ 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: +BubbleUPnP (Android App) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :white_check_mark: [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: +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: +DAMAI Airmusic | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Denon AVR-3808 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Denon AVR-X4000 | :white_check_mark: | :grey_question: | :grey_question: | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: +Freebox Player Mini | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: [gmrender-resurrect](http://github.com/hzeller/gmrender-resurrect) | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: -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: -Yamaha RX-V573 (AV Receiver) <sup>6</sup> | :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: Google Chromecast Audio | :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: +LG BP550 | :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: -Panasonic TX-50CX680W | :white_check_mark: | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: -Yamaha CRX-N560D <sup>4</sup> | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: -RaidSonic IB-MP401Air | :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: Medion P85055 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Onkyo TX-8050 | :white_check_mark: | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :white_check_mark: Onkyo TX-NR509 | :grey_question: | :white_check_mark: | :grey_question: | :no_entry_sign: | :grey_question: | :grey_question: | :grey_question: -Denon AVR-3808 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: -DAMAI Airmusic | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Onkyo TX-NR616 <sup>7</sup> | :grey_question: | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Onkyo TX-NR646 | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Onkyo TX-NR727 <sup>7</sup> | :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: Panasonic TX-50CX680W | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Panasonic TX-50CX680W | :white_check_mark: | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Philips NP2500 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Philips NP2900 | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +Pioneer SC-LX76 (AV Receiver) | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :white_check_mark: +Pioneer VSX-824 (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: +[Raumfeld Speaker M](http://raumfeld.com) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +[Raumfeld Speaker S](http://raumfeld.com) | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :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: +[rygel](https://wiki.gnome.org/Projects/Rygel) | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +RaidSonic IB-MP401Air | :grey_question: | :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 LED46 (UE46ES6715) | :white_check_mark: | :no_entry_sign: | :white_check_mark: | :white_check_mark: | :no_entry_sign: | :grey_question: | :no_entry_sign: +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: +Samsung Smart TV LED60 (UE60F6300) | :white_check_mark: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: | :grey_question: +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: +Sony STR-DN1050 (AV Receiver) | :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: +Xbmc / Kodi | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_circle:<sup>2</sup> | :white_circle:<sup>2</sup> | :white_check_mark: Xbox 360 | :white_check_mark:<sup>5</sup> | :no_entry_sign: | :no_entry_sign: | :no_entry_sign: | :grey_question: | :no_entry_sign: | :white_check_mark: +Yamaha CRX-N560D <sup>4</sup> | :white_check_mark: | :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: +Yamaha RX-V573 (AV Receiver) <sup>6</sup> | :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 @@ -650,6 +693,8 @@ <sup>6</sup>) Was reported to have issues being discovered. Make sure you run the latest firmware +<sup>7</sup>) Reported to need a `--request-timeout` of 15 seconds to work. Since _0.5.0_ the timeout is set to that value. + ## Supported encoders ## Encoder | Description | Identifier
View file
pulseaudio-dlna-0.5.0.1.tar.gz/man
Added
+(directory)
View file
pulseaudio-dlna-0.5.0.1.tar.gz/man/pulseaudio-dlna.1
Added
@@ -0,0 +1,187 @@ +.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.47.2. +.TH PULSEAUDIO-DLNA "1" "March 2016" "pulseaudio-dlna 0.5.0.1" "User Commands" +.SH NAME +pulseaudio-dlna \- Stream audio to DLNA devices and Chromecasts +.SH DESCRIPTION +.SS "Usage:" +.TP +pulseaudio\-dlna pulseaudio\-dlna [\-\-host <host>] [\-\-port <port>][\-\-encoder <encoders> | \fB\-\-codec\fR <codec>] [\-\-bit\-rate=<rate>] +[\-\-encoder\-backend <encoder\-backend>] +[\-\-filter\-device=<filter\-device>] +[\-\-renderer\-urls <urls>] +[\-\-request\-timeout <timeout>] +[\-\-msearch\-port=<msearch\-port>] [\-\-ssdp\-mx <ssdp\-mx>] [\-\-ssdp\-ttl <ssdp\-ttl>] [\-\-ssdp\-amount <ssdp\-amount>] +[\-\-cover\-mode <mode>] +[\-\-auto\-reconnect] +[\-\-debug] +[\-\-fake\-http10\-content\-length] [\-\-fake\-http\-content\-length] +[\-\-disable\-switchback] [\-\-disable\-ssdp\-listener] [\-\-disable\-device\-stop] [\-\-disable\-workarounds] +.TP +pulseaudio\-dlna [\-\-host <host>] [\-\-create\-device\-config] [\-\-update\-device\-config] +[\-\-msearch\-port=<msearch\-port>] [\-\-ssdp\-mx <ssdp\-mx>] [\-\-ssdp\-ttl <ssdp\-ttl>] [\-\-ssdp\-amount <ssdp\-amount>] +.IP +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: +.TP +\- 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 +.TP +the bit rate (depends on the codec) +A written config is loaded by default if the \fB\-\-encoder\fR and \fB\-\-bit\-rate\fR options are not used. +.TP +\fB\-\-update\-device\-config\fR +Same as \fB\-\-create\-device\-config\fR but preserves your existing config from being overwritten +.TP +\fB\-\-host=\fR<host> +Set the server ip. +.TP +\fB\-p\fR \fB\-\-port=\fR<port> +Set the server port [default: 8080]. +.TP +\fB\-e\fR \fB\-\-encoder=\fR<encoders> +Deprecated alias for \fB\-\-codec\fR +.TP +\fB\-c\fR \fB\-\-codec=\fR<codecs> +Set the audio codec. +Possible codecs are: +.TP +\- mp3 +MPEG Audio Layer III (MP3) +.TP +\- ogg +Ogg Vorbis (OGG) +.TP +\- flac +Free Lossless Audio Codec (FLAC) +.TP +\- wav +Waveform Audio File Format (WAV) +.TP +\- opus +Opus Interactive Audio Codec (OPUS) +.TP +\- aac +Advanced Audio Coding (AAC) +.TP +\- l16 +Linear PCM (L16) +.TP +\fB\-\-encoder\-backend=\fR<encoder\-backend> +Set the backend for all encoders. +Possible backends are: +.TP +\- generic (default) +\- ffmpeg +\- avconv +.TP +\fB\-b\fR \fB\-\-bit\-rate=\fR<rate> +Set the audio encoder's bitrate. +.TP +\fB\-\-filter\-device=\fR<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. +.TP +\fB\-\-renderer\-urls=\fR<urls> +Set the renderer urls yourself. no discovery will commence. +.TP +\fB\-\-request\-timeout=\fR<timeout> +Set the timeout for requests in seconds [default: 15]. +.TP +\fB\-\-ssdp\-ttl=\fR<ssdp\-ttl> +Set the SSDP socket's TTL [default: 10]. +.TP +\fB\-\-ssdp\-mx=\fR<ssdp\-mx> +Set the MX value of the SSDP discovery message [default: 3]. +.TP +\fB\-\-ssdp\-amount=\fR<ssdp\-amount> +Set the amount of SSDP discovery messages being sent [default: 5]. +.TP +\fB\-\-msearch\-port=\fR<msearch\-port> +Set the source port of the MSEARCH socket [default: random]. +.TP +\fB\-\-cover\-mode=\fR<mode> +Set the cover mode [default: default]. +Possible modes are: +.TP +\- disabled +No icon is shown +.TP +\- default +The application icon is shown +.TP +\- distribution +The icon of your distribution is shown +.TP +\- application +The audio application's icon is shown +.TP +\fB\-\-debug\fR +enables detailed debug messages. +.TP +\fB\-\-auto\-reconnect\fR +If set, the application tries to reconnect devices in case the stream collapsed +.TP +\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. +.TP +\fB\-\-disable\-ssdp\-listener\fR +If set, the application won't bind to the port 1900 and therefore the automatic discovery of new devices won't work. +.TP +\fB\-\-disable\-device\-stop\fR +If set, the application won't send any stop commands to renderers at all +.TP +\fB\-\-disable\-workarounds\fR +If set, the application won't apply any device workarounds +.TP +\fB\-v\fR \fB\-\-version\fR +Show the version. +.TP +\fB\-h\fR \fB\-\-help\fR +Show the help. +.SH EXAMPLES +.IP +\- pulseaudio\-dlna +.IP +will start pulseaudio\-dlna on port 8080 and stream your PulseAudio streams encoded with mp3. +.IP +\- pulseaudio\-dlna \-\-encoder ogg +.IP +will start pulseaudio\-dlna on port 8080 and stream your PulseAudio streams encoded with Ogg Vorbis. +.IP +\- pulseaudio\-dlna \-\-port 10291 \-\-encoder flac +.IP +will start pulseaudio\-dlna on port 10291 and stream your PulseAudio streams encoded with FLAC. +.IP +\- pulseaudio\-dlna \-\-filter\-device 'Nexus 5,TV' +.IP +will just use devices named Nexus 5 or TV even when more devices got discovered. +.IP +\- pulseaudio\-dlna \-\-renderer\-urls http://192.168.1.7:7676/smp_10_ +.IP +won't discover upnp devices by itself. Instead it will search for upnp renderers +at the specified locations. You can specify multiple locations via urls +separated by comma (,). Most users won't ever need this option, but since +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. +.SH "SEE ALSO" +The full documentation for +.B pulseaudio-dlna +is maintained as a Texinfo manual. If the +.B info +and +.B pulseaudio-dlna +programs are properly installed at your site, the command +.IP +.B info pulseaudio-dlna +.PP +should give you access to the complete manual.
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/__main__.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/__main__.py
Changed
@@ -17,15 +17,17 @@ ''' Usage: - pulseaudio-dlna [--host <host>] [--port <port>][--encoder <encoders>] [--bit-rate=<rate>] + pulseaudio-dlna pulseaudio-dlna [--host <host>] [--port <port>][--encoder <encoders> | --codec <codec>] [--bit-rate=<rate>] + [--encoder-backend <encoder-backend>] [--filter-device=<filter-device>] [--renderer-urls <urls>] [--request-timeout <timeout>] [--msearch-port=<msearch-port>] [--ssdp-mx <ssdp-mx>] [--ssdp-ttl <ssdp-ttl>] [--ssdp-amount <ssdp-amount>] [--cover-mode <mode>] + [--auto-reconnect] [--debug] [--fake-http10-content-length] [--fake-http-content-length] - [--disable-switchback] [--disable-ssdp-listener] [--disable-device-stop] + [--disable-switchback] [--disable-ssdp-listener] [--disable-device-stop] [--disable-workarounds] pulseaudio-dlna [--host <host>] [--create-device-config] [--update-device-config] [--msearch-port=<msearch-port>] [--ssdp-mx <ssdp-mx>] [--ssdp-ttl <ssdp-ttl>] [--ssdp-amount <ssdp-amount>] pulseaudio-dlna [-h | --help | --version] @@ -42,8 +44,9 @@ --update-device-config Same as --create-device-config but preserves your existing config from being overwritten --host=<host> Set the server ip. -p --port=<port> Set the server port [default: 8080]. - -e --encoder=<encoders> Set the audio encoder. - Possible encoders are: + -e --encoder=<encoders> Deprecated alias for --codec + -c --codec=<codecs> Set the audio codec. + Possible codecs are: - mp3 MPEG Audio Layer III (MP3) - ogg Ogg Vorbis (OGG) - flac Free Lossless Audio Codec (FLAC) @@ -51,12 +54,17 @@ - opus Opus Interactive Audio Codec (OPUS) - aac Advanced Audio Coding (AAC) - l16 Linear PCM (L16) + --encoder-backend=<encoder-backend> Set the backend for all encoders. + Possible backends are: + - generic (default) + - ffmpeg + - avconv -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. - --request-timeout=<timeout> Set the timeout for requests in seconds [default: 10]. + --request-timeout=<timeout> Set the timeout for requests in seconds [default: 15]. --ssdp-ttl=<ssdp-ttl> Set the SSDP socket's TTL [default: 10]. --ssdp-mx=<ssdp-mx> Set the MX value of the SSDP discovery message [default: 3]. --ssdp-amount=<ssdp-amount> Set the amount of SSDP discovery messages being sent [default: 5]. @@ -68,10 +76,12 @@ - distribution The icon of your distribution is shown - application The audio application's icon is shown --debug enables detailed debug messages. + --auto-reconnect If set, the application tries to reconnect devices in case the stream collapsed --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. --disable-device-stop If set, the application won't send any stop commands to renderers at all + --disable-workarounds If set, the application won't apply any device workarounds -v --version Show the version. -h --help Show the help.
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/application.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/application.py
Changed
@@ -22,22 +22,24 @@ import setproctitle import logging import sys -import socket import json import os import pulseaudio_dlna -import pulseaudio_dlna.listener +import pulseaudio_dlna.holder import pulseaudio_dlna.plugins.upnp +import pulseaudio_dlna.plugins.upnp.ssdp +import pulseaudio_dlna.plugins.upnp.ssdp.listener +import pulseaudio_dlna.plugins.upnp.ssdp.discover import pulseaudio_dlna.plugins.chromecast +import pulseaudio_dlna.plugins.chromecast.mdns import pulseaudio_dlna.encoders import pulseaudio_dlna.covermodes import pulseaudio_dlna.streamserver import pulseaudio_dlna.pulseaudio import pulseaudio_dlna.utils.network import pulseaudio_dlna.rules -import pulseaudio_dlna.renderers -import pulseaudio_dlna.discover +import pulseaudio_dlna.workarounds logger = logging.getLogger('pulseaudio_dlna.application') @@ -65,8 +67,9 @@ process.terminate() sys.exit(0) - def run_process(self, target): - process = multiprocessing.Process(target=target) + def run_process(self, target, *args, **kwargs): + process = multiprocessing.Process( + target=target, args=args, kwargs=kwargs) self.processes.append(process) process.start() @@ -89,23 +92,34 @@ logger.info('Using localhost: {host}:{port}'.format( host=host, port=port)) + if options['--disable-workarounds']: + pulseaudio_dlna.workarounds.BaseWorkaround.ENABLED = False + + if options['--disable-ssdp-listener']: + pulseaudio_dlna.plugins.upnp.ssdp.listener.\ + SSDPListener.DISABLE_SSDP_LISTENER = True + if options['--ssdp-ttl']: ssdp_ttl = int(options['--ssdp-ttl']) - pulseaudio_dlna.discover.RendererDiscover.SSDP_TTL = ssdp_ttl - pulseaudio_dlna.listener.SSDPListener.SSDP_TTL = ssdp_ttl + pulseaudio_dlna.plugins.upnp.ssdp.discover.\ + SSDPDiscover.SSDP_TTL = ssdp_ttl + pulseaudio_dlna.plugins.upnp.ssdp.listener.\ + SSDPListener.SSDP_TTL = ssdp_ttl if options['--ssdp-mx']: ssdp_mx = int(options['--ssdp-mx']) - pulseaudio_dlna.discover.RendererDiscover.SSDP_MX = ssdp_mx + pulseaudio_dlna.plugins.upnp.ssdp.discover.\ + SSDPDiscover.SSDP_MX = ssdp_mx if options['--ssdp-amount']: ssdp_amount = int(options['--ssdp-amount']) - pulseaudio_dlna.discover.RendererDiscover.SSDP_AMOUNT = ssdp_amount + pulseaudio_dlna.plugins.upnp.ssdp.discover.\ + SSDPDiscover.SSDP_AMOUNT = ssdp_amount msearch_port = options.get('--msearch-port', None) if msearch_port != 'random': - pulseaudio_dlna.discover.RendererDiscover.MSEARCH_PORT = \ - int(msearch_port) + pulseaudio_dlna.plugins.upnp.ssdp.discover.\ + SSDPDiscover.MSEARCH_PORT = int(msearch_port) if options['--create-device-config']: self.create_device_config() @@ -119,46 +133,40 @@ if not options['--encoder'] and not options['--bit-rate']: device_config = self.read_device_config() + if options['--encoder-backend']: + try: + pulseaudio_dlna.codecs.set_backend( + options['--encoder-backend']) + except pulseaudio_dlna.codecs.UnknownBackendException as e: + logger.error(e) + sys.exit(1) + if options['--encoder']: - 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) + logger.warning( + 'The option "--encoder" is deprecated. ' + 'Please use "--codec" instead.') + codecs = (options['--encoder'] or options['--codec']) + if codecs: + try: + pulseaudio_dlna.codecs.set_codecs(codecs.split(',')) + except pulseaudio_dlna.codecs.UnknownCodecException as e: + logger.error(e) + sys.exit(1) - if options['--bit-rate']: + bit_rate = options['--bit-rate'] + if bit_rate: 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 ' - 'are "{bit_rates}"! ' - 'Application terminates.'.format( - encoder=_type().__class__.__name__, - bit_rates=','.join( - str(e) for e in _type.SUPPORTED_BIT_RATES - ))) - sys.exit(0) + pulseaudio_dlna.encoders.set_bit_rate(bit_rate) + except (pulseaudio_dlna.encoders.InvalidBitrateException, + pulseaudio_dlna.encoders.UnsupportedBitrateException) as e: + logger.error(e) + sys.exit(1) cover_mode = options['--cover-mode'] - if cover_mode not in pulseaudio_dlna.covermodes.MODES: - logger.info('You specified an unknown cover mode! ' - 'Application terminates.') + try: + pulseaudio_dlna.covermodes.validate(cover_mode) + except pulseaudio_dlna.covermodes.UnknownCoverModeException as e: + logger.error(e) sys.exit(1) logger.info('Encoder settings:') @@ -191,30 +199,24 @@ if options['--disable-switchback']: disable_switchback = True - disable_ssdp_listener = False - if options['--disable-ssdp-listener']: - disable_ssdp_listener = True - disable_device_stop = False if options['--disable-device-stop']: disable_device_stop = True - try: - stream_server = pulseaudio_dlna.streamserver.ThreadedStreamServer( - host, port, bridges, message_queue, - fake_http_content_length=fake_http_content_length, - ) - except socket.error: - logger.error( - 'The streaming server could not bind to your specified port ' - '({port}). Perhaps this is already in use? Application ' - 'terminates.'.format(port=port)) - sys.exit(1) + disable_auto_reconnect = True + if options['--auto-reconnect']: + disable_auto_reconnect = False + + stream_server = pulseaudio_dlna.streamserver.ThreadedStreamServer( + host, port, bridges, message_queue, + fake_http_content_length=fake_http_content_length, + ) pulse = pulseaudio_dlna.pulseaudio.PulseWatcher( bridges, message_queue, disable_switchback=disable_switchback, disable_device_stop=disable_device_stop, + disable_auto_reconnect=disable_auto_reconnect, cover_mode=cover_mode, ) @@ -223,10 +225,8 @@ device_filter = options['--filter-device'].split(',') locations = None - disable_ssdp_search = False if options['--renderer-urls']: locations = options['--renderer-urls'].split(',') - disable_ssdp_search = True if options['--request-timeout']: request_timeout = float(options['--request-timeout']) @@ -234,30 +234,21 @@ pulseaudio_dlna.plugins.renderer.BaseRenderer.REQUEST_TIMEOUT = \ request_timeout - holder = pulseaudio_dlna.renderers.RendererHolder( - self.PLUGINS, stream_server.ip, stream_server.port, message_queue, - device_filter, device_config) + holder = pulseaudio_dlna.holder.Holder( + plugins=self.PLUGINS, + stream_ip=stream_server.ip, + stream_port=stream_server.port, + message_queue=message_queue, + device_filter=device_filter, + device_config=device_config + ) + self.run_process(stream_server.run) + self.run_process(pulse.run) if locations: - holder.process_locations(locations) - - try: - ssdp_listener = pulseaudio_dlna.listener.ThreadedSSDPListener( - holder, - disable_ssdp_listener=disable_ssdp_listener, - disable_ssdp_search=disable_ssdp_search - ) - except socket.error: - logger.error( - 'The SSDP listener could not bind to the port 1900/UDP. ' - '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) - self.run_process(target=pulse.run) - self.run_process(target=ssdp_listener.run) + self.run_process(holder.lookup, locations) + else: + self.run_process(holder.search) setproctitle.setproctitle('pulseaudio-dlna') signal.signal(signal.SIGINT, self.shutdown) @@ -268,9 +259,10 @@ process.join() def create_device_config(self, update=False): - holder = pulseaudio_dlna.renderers.RendererHolder(self.PLUGINS) - discover = pulseaudio_dlna.discover.RendererDiscover(holder) - discover.search() + logger.info('Starting discovery ...') + holder = pulseaudio_dlna.holder.Holder(plugins=self.PLUGINS) + holder.search(ttl=5) + logger.info('Discovery complete.') def device_filter(obj): if hasattr(obj, 'to_json'): @@ -285,15 +277,16 @@ if update: existing_config = self.read_device_config() if existing_config: - new_config = obj_to_dict(holder.renderers) + new_config = obj_to_dict(holder.devices) new_config.update(existing_config) else: logger.error( 'Your device config could not be found at any of the ' - 'locations "{}"'.format(','.join(self.DEVICE_CONFIG_PATHS))) + 'locations "{}"'.format( + ','.join(self.DEVICE_CONFIG_PATHS))) sys.exit(1) else: - new_config = obj_to_dict(holder.renderers) + new_config = obj_to_dict(holder.devices) json_text = json.dumps(new_config, indent=4) for config_path in reversed(self.DEVICE_CONFIG_PATHS): @@ -307,7 +300,7 @@ 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(): + for device in holder.devices.values(): logger.info('{name} ({flavour})'.format( name=device.name, flavour=device.flavour)) for codec in device.codecs: @@ -331,15 +324,12 @@ 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)) + 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)) + 'Loaded device config "{}"'.format(config_file)) return device_config except ValueError: logger.error(
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/codecs.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/codecs.py
Changed
@@ -28,14 +28,49 @@ logger = logging.getLogger('pulseaudio_dlna.codecs') +BACKENDS = ['generic', 'ffmpeg', 'avconv'] CODECS = {} +class UnknownBackendException(Exception): + def __init__(self, backend): + Exception.__init__( + self, + 'You specified an unknown backend "{}"!'.format(backend) + ) + + +class UnknownCodecException(Exception): + def __init__(self, codec): + Exception.__init__( + self, + 'You specified an unknown codec "{}"!'.format(codec), + ) + + +def set_backend(backend): + if backend in BACKENDS: + BaseCodec.BACKEND = backend + return + raise UnknownBackendException(backend) + + +def set_codecs(identifiers): + for identifier, _type in CODECS.iteritems(): + _type.ENABLED = False + for identifier in identifiers: + try: + CODECS[identifier].ENABLED = True + except KeyError: + raise UnknownCodecException(identifier) + + @functools.total_ordering class BaseCodec(object): ENABLED = True IDENTIFIER = None + BACKEND = 'generic' def __init__(self): self.mime_type = None @@ -55,6 +90,10 @@ def specific_mime_type(self): return self.mime_type + @property + def encoder(self): + return self.ENCODERS[self.BACKEND]() + @classmethod def accepts(cls, mime_type): for accepted_mime_type in cls.SUPPORTED_MIME_TYPES: @@ -72,16 +111,18 @@ 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 '', - ) + return '<{} enabled="{}" priority="{}" mime_type="{}" ' \ + 'backend="{}">{}{}'.format( + self.__class__.__name__, + self.enabled, + self.priority, + self.specific_mime_type, + self.BACKEND, + ('\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'] @@ -95,6 +136,14 @@ class BitRateMixin(object): + + def __init__(self): + self.bit_rate = None + + @property + def encoder(self): + return self.ENCODERS[self.BACKEND](self.bit_rate) + def __eq__(self, other): return type(self) is type(other) and self.bit_rate == other.bit_rate @@ -102,29 +151,33 @@ 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' + ENCODERS = { + 'generic': pulseaudio_dlna.encoders.LameMp3Encoder, + 'ffmpeg': pulseaudio_dlna.encoders.FFMpegMp3Encoder, + 'avconv': pulseaudio_dlna.encoders.AVConvMp3Encoder, + } def __init__(self, mime_string=None): BaseCodec.__init__(self) + BitRateMixin.__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' + ENCODERS = { + 'generic': pulseaudio_dlna.encoders.SoxWavEncoder, + 'ffmpeg': pulseaudio_dlna.encoders.FFMpegWavEncoder, + 'avconv': pulseaudio_dlna.encoders.AVConvWavEncoder, + } def __init__(self, mime_string=None): BaseCodec.__init__(self) @@ -132,15 +185,16 @@ 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' + ENCODERS = { + 'generic': pulseaudio_dlna.encoders.SoxL16Encoder, + 'ffmpeg': pulseaudio_dlna.encoders.FFMpegL16Encoder, + 'avconv': pulseaudio_dlna.encoders.AVConvL16Encoder, + } def __init__(self, mime_string=None): BaseCodec.__init__(self) @@ -171,8 +225,7 @@ @property def encoder(self): - return pulseaudio_dlna.encoders.L16Encoder( - self.sample_rate, self.channels) + return self.ENCODERS[self.BACKEND](self.sample_rate, self.channels) def __eq__(self, other): return type(self) is type(other) and ( @@ -185,48 +238,51 @@ self.channels > other.channels) -@functools.total_ordering class AacCodec(BitRateMixin, BaseCodec): SUPPORTED_MIME_TYPES = ['audio/aac', 'audio/x-aac'] IDENTIFIER = 'aac' + ENCODERS = { + 'generic': pulseaudio_dlna.encoders.FaacAacEncoder, + 'ffmpeg': pulseaudio_dlna.encoders.FFMpegAacEncoder, + 'avconv': pulseaudio_dlna.encoders.AVConvAacEncoder, + } def __init__(self, mime_string=None): BaseCodec.__init__(self) + BitRateMixin.__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' + ENCODERS = { + 'generic': pulseaudio_dlna.encoders.OggencOggEncoder, + 'ffmpeg': pulseaudio_dlna.encoders.FFMpegOggEncoder, + 'avconv': pulseaudio_dlna.encoders.AVConvOggEncoder, + } def __init__(self, mime_string=None): BaseCodec.__init__(self) + BitRateMixin.__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' + ENCODERS = { + 'generic': pulseaudio_dlna.encoders.FlacFlacEncoder, + 'ffmpeg': pulseaudio_dlna.encoders.FFMpegFlacEncoder, + 'avconv': pulseaudio_dlna.encoders.AVConvFlacEncoder, + } def __init__(self, mime_string=None): BaseCodec.__init__(self) @@ -234,29 +290,24 @@ 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' + ENCODERS = { + 'generic': pulseaudio_dlna.encoders.OpusencOpusEncoder, + 'ffmpeg': pulseaudio_dlna.encoders.FFMpegOpusEncoder, + 'avconv': pulseaudio_dlna.encoders.AVConvOpusEncoder, + } def __init__(self, mime_string=None): BaseCodec.__init__(self) + BitRateMixin.__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:
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/covermodes.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/covermodes.py
Changed
@@ -29,6 +29,19 @@ MODES = {} +class UnknownCoverModeException(Exception): + def __init__(self, cover_mode): + Exception.__init__( + self, + 'You specified an unknown cover mode "{}"!'.format(cover_mode) + ) + + +def validate(cover_mode): + if cover_mode not in MODES: + raise UnknownCoverModeException(cover_mode) + + class BaseCoverMode(object): IDENTIFIER = None
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/daemon.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/daemon.py
Changed
@@ -155,8 +155,17 @@ 'LANG' ] compressed_env = {} + missing_env = [] for k in required_variables: - compressed_env[k] = proc_env[k] + if k in proc_env: + compressed_env[k] = proc_env[k] + else: + missing_env.append(k) + + if len(missing_env) > 0: + logger.warning( + 'The following environment variables were not set: "{}". ' + 'Starting as root may not work!'.format(','.join(missing_env))) try: self.application = (
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/encoders
Added
+(directory)
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/encoders/__init__.py
Added
@@ -0,0 +1,188 @@ +#!/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 distutils.spawn +import inspect +import sys +import logging + +logger = logging.getLogger('pulseaudio_dlna.encoder') + +ENCODERS = [] + + +class InvalidBitrateException(Exception): + def __init__(self, bit_rate): + Exception.__init__( + self, + 'You specified an invalid bit rate "{}"!'.format(bit_rate), + ) + + +class UnsupportedBitrateException(Exception): + def __init__(self, bit_rate, cls): + Exception.__init__( + self, + 'You specified an unsupported bit rate for the {encoder}! ' + 'Supported bit rates are "{bit_rates}"! '.format( + encoder=cls.__name__, + bit_rates=','.join( + str(e) for e in cls.SUPPORTED_BIT_RATES + ) + ) + ) + + +def set_bit_rate(bit_rate): + try: + bit_rate = int(bit_rate) + except ValueError: + raise InvalidBitrateException(bit_rate) + + for _type in 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: + raise UnsupportedBitrateException(bit_rate, _type) + + +class BaseEncoder(object): + + AVAILABLE = True + + def __init__(self): + self._binary = None + self._command = [] + self._bit_rate = None + self._writes_header = False + + @property + def binary(self): + return self._binary + + @property + def command(self): + return [self.binary] + self._command + + @property + def available(self): + return type(self).AVAILABLE + + @property + def writes_header(self): + return self._writes_header + + 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 supported_bit_rates(self): + raise UnsupportedBitrateException() + + def __str__(self): + return '<{} available="{}">'.format( + self.__class__.__name__, + unicode(self.available), + ) + + +class BitRateMixin(object): + + DEFAULT_BIT_RATE = 192 + + @property + def bit_rate(self): + return self._bit_rate + + @bit_rate.setter + def bit_rate(self, value): + if int(value) in self.SUPPORTED_BIT_RATES: + self._bit_rate = value + else: + raise UnsupportedBitrateException() + + @property + def supported_bit_rates(self): + return self.SUPPORTED_BIT_RATES + + def __str__(self): + return '<{} available="{}" bit-rate="{}">'.format( + self.__class__.__name__, + unicode(self.available), + unicode(self.bit_rate), + ) + + +class SamplerateChannelMixin(object): + + @property + def sample_rate(self): + return self._sample_rate + + @sample_rate.setter + def sample_rate(self, value): + self._sample_rate = int(value) + + @property + def channels(self): + return self._channels + + @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 NullEncoder(BaseEncoder): + + def __init__(self): + BaseEncoder.__init__(self) + self._binary = 'cat' + self._command = [] + + +from pulseaudio_dlna.encoders.generic import * +from pulseaudio_dlna.encoders.ffmpeg import * +from pulseaudio_dlna.encoders.avconv import * + + +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 + +load_encoders()
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/encoders/avconv.py
Added
@@ -0,0 +1,76 @@ +#!/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 logging + +from pulseaudio_dlna.encoders.ffmpeg import ( + FFMpegMp3Encoder, FFMpegWavEncoder, FFMpegL16Encoder, FFMpegAacEncoder, + FFMpegOggEncoder, FFMpegFlacEncoder, FFMpegOpusEncoder) + +logger = logging.getLogger('pulseaudio_dlna.encoder.avconv') + + +class AVConvMp3Encoder(FFMpegMp3Encoder): + + def __init__(self, bit_rate=None): + super(AVConvMp3Encoder, self).__init__(bit_rate=bit_rate) + self._binary = 'avconv' + + +class AVConvWavEncoder(FFMpegWavEncoder): + + def __init__(self, bit_rate=None): + super(AVConvWavEncoder, self).__init__() + self._binary = 'avconv' + + +class AVConvL16Encoder(FFMpegL16Encoder): + + def __init__(self, sample_rate=None, channels=None): + super(AVConvL16Encoder, self).__init__( + sample_rate=sample_rate, channels=channels) + self._binary = 'avconv' + + +class AVConvAacEncoder(FFMpegAacEncoder): + + def __init__(self, bit_rate=None): + super(AVConvAacEncoder, self).__init__(bit_rate=bit_rate) + self._binary = 'avconv' + + +class AVConvOggEncoder(FFMpegOggEncoder): + + def __init__(self, bit_rate=None): + super(AVConvOggEncoder, self).__init__(bit_rate=bit_rate) + self._binary = 'avconv' + + +class AVConvFlacEncoder(FFMpegFlacEncoder): + + def __init__(self, bit_rate=None): + super(AVConvFlacEncoder, self).__init__() + self._binary = 'avconv' + + +class AVConvOpusEncoder(FFMpegOpusEncoder): + + def __init__(self, bit_rate=None): + super(AVConvOpusEncoder, self).__init__(bit_rate=bit_rate) + self._binary = 'avconv'
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/encoders/ffmpeg.py
Added
@@ -0,0 +1,139 @@ +#!/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 logging + +from pulseaudio_dlna.encoders import ( + BitRateMixin, SamplerateChannelMixin, BaseEncoder) + +logger = logging.getLogger('pulseaudio_dlna.encoder.ffmpeg') + + +class FFMpegMixin(object): + + def _ffmpeg_command( + self, format, bit_rate=None, sample_rate=None, channels=None): + command = [ + '-loglevel', 'panic', + ] + command.extend([ + '-ac', '2', + '-ar', '44100', + '-f', 's16le', + '-i', '-', + ]) + command.extend([ + '-strict', '-2', + '-f', format, + ]) + if bit_rate: + command.extend(['-b:a', str(bit_rate) + 'k']) + if sample_rate: + command.extend(['-ar', str(sample_rate)]) + if channels: + command.extend(['-ac', str(channels)]) + command.append('pipe:') + return command + + +class FFMpegMp3Encoder(BitRateMixin, FFMpegMixin, BaseEncoder): + + SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, + 128, 160, 192, 224, 256, 320] + + def __init__(self, bit_rate=None): + BaseEncoder.__init__(self) + self.bit_rate = bit_rate or FFMpegMp3Encoder.DEFAULT_BIT_RATE + + self._writes_header = True + self._binary = 'ffmpeg' + self._command = self._ffmpeg_command('mp3', bit_rate=self.bit_rate) + + +class FFMpegWavEncoder(FFMpegMixin, BaseEncoder): + + def __init__(self): + BaseEncoder.__init__(self) + + self._writes_header = True + self._binary = 'ffmpeg' + self._command = self._ffmpeg_command('wav') + + +class FFMpegL16Encoder(SamplerateChannelMixin, FFMpegMixin, 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._writes_header = None + self._binary = 'ffmpeg' + self._command = self._ffmpeg_command( + 's16be', sample_rate=self.sample_rate, channels=self.channels) + + +class FFMpegAacEncoder(BitRateMixin, FFMpegMixin, BaseEncoder): + + SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, + 128, 160, 192, 224, 256, 320] + + def __init__(self, bit_rate=None): + BaseEncoder.__init__(self) + self.bit_rate = bit_rate or FFMpegAacEncoder.DEFAULT_BIT_RATE + + self._writes_header = False + self._binary = 'ffmpeg' + self._command = self._ffmpeg_command('adts', bit_rate=self.bit_rate) + + +class FFMpegOggEncoder(BitRateMixin, FFMpegMixin, BaseEncoder): + + SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, + 128, 160, 192, 224, 256, 320] + + def __init__(self, bit_rate=None): + BaseEncoder.__init__(self) + self.bit_rate = bit_rate or FFMpegOggEncoder.DEFAULT_BIT_RATE + + self._writes_header = True + self._binary = 'ffmpeg' + self._command = self._ffmpeg_command('ogg', bit_rate=self.bit_rate) + + +class FFMpegFlacEncoder(FFMpegMixin, BaseEncoder): + + def __init__(self): + BaseEncoder.__init__(self) + + self._writes_header = True + self._binary = 'ffmpeg' + self._command = self._ffmpeg_command('flac') + + +class FFMpegOpusEncoder(BitRateMixin, FFMpegMixin, BaseEncoder): + + SUPPORTED_BIT_RATES = [i for i in range(6, 257)] + + def __init__(self, bit_rate=None): + BaseEncoder.__init__(self) + self.bit_rate = bit_rate or FFMpegOpusEncoder.DEFAULT_BIT_RATE + + self._writes_header = True + self._binary = 'ffmpeg' + self._command = self._ffmpeg_command('opus', bit_rate=self.bit_rate)
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/encoders/generic.py
Added
@@ -0,0 +1,130 @@ +#!/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 logging + +from pulseaudio_dlna.encoders import ( + BitRateMixin, SamplerateChannelMixin, BaseEncoder) + +logger = logging.getLogger('pulseaudio_dlna.encoder.generic') + + +class LameMp3Encoder(BitRateMixin, BaseEncoder): + + SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, + 128, 160, 192, 224, 256, 320] + + def __init__(self, bit_rate=None): + BaseEncoder.__init__(self) + self.bit_rate = bit_rate or LameMp3Encoder.DEFAULT_BIT_RATE + + self._writes_header = False + self._binary = 'lame' + self._command = ['-b', str(self.bit_rate), '-r', '-'] + + +class SoxWavEncoder(BaseEncoder): + def __init__(self): + BaseEncoder.__init__(self) + + self._writes_header = True + self._binary = 'sox' + self._command = ['-t', 'raw', '-b', '16', '-e', 'signed', '-c', '2', + '-r', '44100', '-', + '-t', 'wav', '-b', '16', '-e', 'signed', '-c', '2', + '-r', '44100', + '-L', '-', + ] + + +class SoxL16Encoder(SamplerateChannelMixin, 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._writes_header = True + 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), + ] + + +class FaacAacEncoder(BitRateMixin, BaseEncoder): + + SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, + 128, 160, 192, 224, 256, 320] + + def __init__(self, bit_rate=None): + BaseEncoder.__init__(self) + self.bit_rate = bit_rate or FaacAacEncoder.DEFAULT_BIT_RATE + + self._writes_header = None + self._binary = 'faac' + self._command = ['-b', str(self.bit_rate), + '-X', '-P', '-o', '-', '-'] + + +class OggencOggEncoder(BitRateMixin, BaseEncoder): + + SUPPORTED_BIT_RATES = [32, 40, 48, 56, 64, 80, 96, 112, + 128, 160, 192, 224, 256, 320] + + def __init__(self, bit_rate=None): + BaseEncoder.__init__(self) + self.bit_rate = bit_rate or OggencOggEncoder.DEFAULT_BIT_RATE + + self._writes_header = True + self._binary = 'oggenc' + self._command = ['-b', str(self.bit_rate), + '-Q', '-r', '--ignorelength', '-'] + + +class FlacFlacEncoder(BaseEncoder): + + def __init__(self, bit_rate=None): + BaseEncoder.__init__(self) + + self._writes_header = True + self._binary = 'flac' + self._command = ['-', '-c', '--channels', '2', '--bps', '16', + '--sample-rate', '44100', + '--endian', 'little', '--sign', 'signed', '-s'] + + +class OpusencOpusEncoder(BitRateMixin, BaseEncoder): + + SUPPORTED_BIT_RATES = [i for i in range(6, 257)] + + def __init__(self, bit_rate=None): + BaseEncoder.__init__(self) + self.bit_rate = bit_rate or OpusencOpusEncoder.DEFAULT_BIT_RATE + + self._writes_header = True + self._binary = 'opusenc' + self._command = ['--bitrate', str(self.bit_rate), + '--padding', '0', '--max-delay', '0', + '--expect-loss', '1', '--framesize', '2.5', + '--raw-rate', '44100', + '--raw', '-', '-']
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/holder.py
Added
@@ -0,0 +1,125 @@ +#!/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 logging +import threading +import requests +import traceback + +logger = logging.getLogger('pulseaudio_dlna.holder') + + +class Holder(object): + def __init__( + self, plugins, + stream_ip=None, stream_port=None, message_queue=None, + device_filter=None, device_config=None): + self.plugins = plugins + self.stream_ip = stream_ip + self.stream_port = stream_port + self.device_filter = device_filter or [] + self.device_config = device_config or {} + self.message_queue = message_queue + self.devices = {} + self.lock = threading.Lock() + + def search(self, ttl=None): + threads = [] + for plugin in self.plugins: + thread = threading.Thread( + target=plugin.discover, args=[self, ttl]) + thread.daemon = True + threads.append(thread) + try: + for thread in threads: + thread.start() + for thread in threads: + thread.join() + except: + traceback.print_exc() + + logger.debug('Holder.search() quit') + + def lookup(self, locations): + xmls = {} + for url in locations: + try: + response = requests.get(url, timeout=5) + logger.debug('Response from device ({url})\n{response}'.format( + url=url, response=response.text)) + xmls[url] = response.content + except requests.exceptions.Timeout: + logger.warning( + 'Could no connect to {url}. ' + 'Connection timeout.'.format(url=url)) + except requests.exceptions.ConnectionError: + logger.warning( + 'Could no connect to {url}. ' + 'Connection refused.'.format(url=url)) + + for plugin in self.plugins: + for url, xml in xmls.items(): + device = plugin.lookup(url, xml) + self.add_device(device) + + def add_device(self, device): + if not device: + return + try: + self.lock.acquire() + if device.udn not in self.devices: + if device.validate(): + config = self.device_config.get(device.udn, None) + device.activate(config) + if self.stream_ip and self.stream_port: + device.set_server_location( + self.stream_ip, self.stream_port) + if device.name not in self.device_filter: + if config: + logger.info( + 'Using device configuration:\n{}'.format( + device.__str__(True))) + self.devices[device.udn] = device + self._send_message('add_device', device) + else: + logger.info('Skipped the device "{name}" ...'.format( + name=device.label)) + else: + if device.validate(): + self._send_message('update_device', device) + finally: + self.lock.release() + + def remove_device(self, device_id): + if not device_id or device_id not in self.devices: + return + try: + self.lock.acquire() + device = self.devices[device_id] + self._send_message('remove_device', device) + del self.devices[device_id] + finally: + self.lock.release() + + def _send_message(self, _type, device): + if self.message_queue: + self.message_queue.put({ + 'type': _type, + 'device': device + })
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/plugins/__init__.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/__init__.py
Changed
@@ -17,16 +17,38 @@ from __future__ import unicode_literals +import functools + class BasePlugin(object): def __init__(self): self.st_header = None - - def discover(self): - raise NotImplementedError() + self.holder = None def lookup(self, locations): raise NotImplementedError() - def create_device(self, header): + def discover(self, ttl): raise NotImplementedError() + + @staticmethod + def add_device_after(f, *args): + @functools.wraps(f) + def wrapper(*args, **kwargs): + device = f(*args, **kwargs) + self = args[0] + if self.holder: + self.holder.add_device(device) + return device + return wrapper + + @staticmethod + def remove_device_after(f, *args): + @functools.wraps(f) + def wrapper(*args, **kwargs): + device_id = f(*args, **kwargs) + self = args[0] + if self.holder: + self.holder.remove_device(device_id) + return device_id + return wrapper
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/plugins/chromecast/__init__.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/chromecast/__init__.py
Changed
@@ -17,25 +17,43 @@ from __future__ import unicode_literals +import logging + import pulseaudio_dlna.plugins -import pulseaudio_dlna.plugins.chromecast.renderer +import pulseaudio_dlna.plugins.chromecast.mdns +from pulseaudio_dlna.plugins.chromecast.renderer import ( + CoinedChromecastRenderer, ChromecastRendererFactory) + +logger = logging.getLogger('pulseaudio_dlna.plugins.chromecast') class ChromecastPlugin(pulseaudio_dlna.plugins.BasePlugin): + + GOOGLE_MDNS_DOMAIN = '_googlecast._tcp.local.' + def __init__(self, *args): pulseaudio_dlna.plugins.BasePlugin.__init__(self, *args) - self.st_header = 'urn:dial-multiscreen-org:service:dial:1' - - def lookup(self, locations): - renderers = [] - for url in locations: - renderer = pulseaudio_dlna.plugins.chromecast.renderer.ChromecastRendererFactory.from_url( - url, pulseaudio_dlna.plugins.chromecast.renderer.CoinedChromecastRenderer) - if renderer is not None: - renderers.append(renderer) - return renderers - - def create_device(self, header): - return pulseaudio_dlna.plugins.chromecast.renderer.ChromecastRendererFactory.from_header( - header, - pulseaudio_dlna.plugins.chromecast.renderer.CoinedChromecastRenderer) + + def lookup(self, url, xml): + return ChromecastRendererFactory.from_xml( + url, xml, CoinedChromecastRenderer) + + def discover(self, holder, ttl=None): + self.holder = holder + mdns = pulseaudio_dlna.plugins.chromecast.mdns.MDNSListener( + domain=self.GOOGLE_MDNS_DOMAIN, + cb_on_device_added=self._on_device_added, + cb_on_device_removed=self._on_device_removed + ) + mdns.run(ttl) + + @pulseaudio_dlna.plugins.BasePlugin.add_device_after + def _on_device_added(self, mdns_info): + if mdns_info: + return ChromecastRendererFactory.from_mdns_info( + mdns_info, CoinedChromecastRenderer) + return None + + @pulseaudio_dlna.plugins.BasePlugin.remove_device_after + def _on_device_removed(self, mdns_info): + return None
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/chromecast/mdns.py
Added
@@ -0,0 +1,76 @@ +#!/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 logging +import zeroconf +import gobject +import time + +logger = logging.getLogger('pulseaudio_dlna.plugins.chromecast.mdns') + + +class MDNSHandler(object): + + def __init__(self, server): + self.server = server + + def add_service(self, zeroconf, type, name): + info = zeroconf.get_service_info(type, name) + if self.server.cb_on_device_added: + self.server.cb_on_device_added(info) + + def remove_service(self, zeroconf, type, name): + info = zeroconf.get_service_info(type, name) + if self.server.cb_on_device_removed: + self.server.cb_on_device_removed(info) + + +class MDNSListener(object): + + def __init__( + self, domain, + cb_on_device_added=None, cb_on_device_removed=None): + self.domain = domain + self.cb_on_device_added = cb_on_device_added + self.cb_on_device_removed = cb_on_device_removed + + def run(self, ttl=None): + self.zeroconf = zeroconf.Zeroconf() + zeroconf.ServiceBrowser(self.zeroconf, self.domain, MDNSHandler(self)) + + if ttl: + gobject.timeout_add(ttl * 1000, self.shutdown) + + self.__running = True + self.__mainloop = gobject.MainLoop() + context = self.__mainloop.get_context() + while self.__running: + try: + if context.pending(): + context.iteration(True) + else: + time.sleep(0.1) + except KeyboardInterrupt: + break + self.zeroconf.close() + logger.debug('MDNSListener.run() quit') + + def shutdown(self): + logger.debug('MDNSListener.shutdown()') + self.__running = False
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/plugins/chromecast/pycastv2/__init__.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/chromecast/pycastv2/__init__.py
Changed
@@ -34,6 +34,10 @@ pass +class LaunchErrorException(Exception): + pass + + class ChannelController(object): def __init__(self, socket): self.request_id = 1 @@ -88,6 +92,8 @@ self.socket.send(commands.PongCommand()) elif response_type == 'CLOSE': raise ChannelClosedException() + elif response_type == 'LAUNCH_ERROR': + raise LaunchErrorException() def is_channel_connected(self, destination_id): return destination_id in self.channels @@ -117,9 +123,9 @@ APP_BACKDROP = 'E8C28D3C' WAIT_INTERVAL = 0.1 - def __init__(self, ip, timeout=10): + def __init__(self, ip, port, timeout=10): self.timeout = timeout - self.socket = cast_socket.CastSocket(ip) + self.socket = cast_socket.CastSocket(ip, port) self.channel_controller = ChannelController(self.socket) def is_app_running(self, app_id): @@ -156,7 +162,10 @@ self.socket.send(commands.CloseCommand(destination_id=False)) start_time = time.time() while not self.is_app_running(None): - self.socket.send_and_wait(commands.StatusCommand()) + try: + self.socket.send_and_wait(commands.StatusCommand()) + except cast_socket.ConnectionTerminatedException: + break current_time = time.time() if current_time - start_time > self.timeout: raise TimeoutException() @@ -216,8 +225,8 @@ PLAYER_STATE_PAUSED = 'PAUSED' PLAYER_STATE_IDLE = 'IDLE' - def __init__(self, ip, timeout=10): - ChromecastController.__init__(self, ip, timeout) + def __init__(self, ip, port, timeout=10): + ChromecastController.__init__(self, ip, port, timeout) self.media_session_id = None self.current_time = None self.media = None
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_socket.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/chromecast/pycastv2/cast_socket.py
Changed
@@ -35,11 +35,15 @@ pass +class ConnectionTerminatedException(Exception): + pass + + class BaseChromecastSocket(object): - def __init__(self, ip): + def __init__(self, ip, port): self.sock = socket.socket() self.sock = ssl.wrap_socket(self.sock) - self.sock.connect((ip, 8009)) + self.sock.connect((ip, port)) self.agent = 'chromecast_v2' def _generate_message(self, @@ -69,11 +73,16 @@ formatted_message = size + message.SerializeToString() self.sock.sendall(formatted_message) - def read(self): + def read(self, timeout=10): try: + start_time = time.time() data = str('') while len(data) < 4: + if time.time() - start_time > timeout: + raise NoResponseException() part = self.sock.recv(1) + if len(part) == 0: + raise ConnectionTerminatedException() data += part length = struct.unpack('>I', data)[0] data = str('') @@ -94,8 +103,8 @@ class CastSocket(BaseChromecastSocket): - def __init__(self, ip): - BaseChromecastSocket.__init__(self, ip) + def __init__(self, ip, port): + BaseChromecastSocket.__init__(self, ip, port) self.read_listeners = [] self.send_listeners = [] self.response_cache = {} @@ -194,9 +203,11 @@ def _is_socket_readable(self): try: - r, w, e = select.select([self.sock], [], [], 0) + r, w, e = select.select([self.sock], [], [self.sock], 0) for sock in r: return True + for sock in e: + raise NoResponseException() except socket.error: - pass + raise NoResponseException() return False
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/plugins/chromecast/renderer.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/chromecast/renderer.py
Changed
@@ -22,7 +22,7 @@ import urlparse import socket import traceback -import BeautifulSoup +import lxml import pycastv2 import pulseaudio_dlna.plugins.renderer @@ -31,18 +31,16 @@ logger = logging.getLogger('pulseaudio_dlna.plugins.chromecast.renderer') -CHROMECAST_MODEL_NAMES = ['Eureka Dongle', 'Chromecast Audio'] - - class ChromecastRenderer(pulseaudio_dlna.plugins.renderer.BaseRenderer): - def __init__(self, name, ip, udn, model_name, model_number, manufacturer): + def __init__( + self, name, ip, port, 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.port = port or 8009 self.state = self.IDLE self.codecs = [] @@ -51,30 +49,18 @@ self.set_rules_from_config(config) else: self.codecs = [ + pulseaudio_dlna.codecs.FlacCodec(), pulseaudio_dlna.codecs.Mp3Codec(), - pulseaudio_dlna.codecs.AacCodec(), - pulseaudio_dlna.codecs.OggCodec(), pulseaudio_dlna.codecs.WavCodec(), + pulseaudio_dlna.codecs.OggCodec(), + pulseaudio_dlna.codecs.AacCodec(), ] - def _get_media_player(self): - try: - return pycastv2.MediaPlayerController(self.ip, self.REQUEST_TIMEOUT) - except socket.error as e: - if e.errno == 111: - logger.info( - 'The chromecast refused the connection. Perhaps it ' - 'does not support the castv2 protocol.') - else: - traceback.print_exc() - return None - def play(self, url, artist=None, title=None, thumb=None): - cast = self._get_media_player() - if cast is None: - logger.error('No device was found!') - return 500 + self._before_play() try: + cast = pycastv2.MediaPlayerController( + self.ip, self.port, self.REQUEST_TIMEOUT) cast.load( url, mime_type=self.codec.mime_type, @@ -82,36 +68,60 @@ title=title, thumb=thumb) self.state = self.PLAYING - return 200 + return 200, None + except pycastv2.LaunchErrorException: + message = 'The media player could not be launched. ' \ + 'Maybe the chromecast is still closing a ' \ + 'running player instance. Try again in 30 seconds.' + return 503, message except pycastv2.ChannelClosedException: - logger.info('Connection was closed. I guess another ' - 'client is attached to it.') - return 423 + message = 'Connection was closed. I guess another ' \ + 'client is attached to it.' + return 423, message except pycastv2.TimeoutException: - logger.error('PLAY command - Could no connect to "{device}". ' - 'Connection timeout.'.format(device=self.label)) - return 408 + message = 'PLAY command - Could no connect to "{device}". ' \ + 'Connection timeout.'.format(device=self.label) + return 408, message + except socket.error as e: + if e.errno == 111: + message = 'The chromecast refused the connection. ' \ + 'Perhaps it does not support the castv2 ' \ + 'protocol.' + return 403, message + else: + traceback.print_exc() + return 500, None finally: + self._after_play() cast.cleanup() def stop(self): - cast = self._get_media_player() - if cast is None: - logger.error('No device was found!') - return 500 + self._before_stop() try: + cast = pycastv2.MediaPlayerController( + self.ip, self.port, self.REQUEST_TIMEOUT) self.state = self.IDLE cast.disconnect_application() - return 200 + return 200, None except pycastv2.ChannelClosedException: - logger.info('Connection was closed. I guess another ' - 'client is attached to it.') - return 423 + message = 'Connection was closed. I guess another ' \ + 'client is attached to it.' + return 423, message except pycastv2.TimeoutException: - logger.error('STOP command - Could no connect to "{device}". ' - 'Connection timeout.'.format(device=self.label)) - return 408 + message = 'STOP command - Could no connect to "{device}". ' \ + 'Connection timeout.'.format(device=self.label) + return 408, message + except socket.error as e: + if e.errno == 111: + message = 'The chromecast refused the connection. ' \ + 'Perhaps it does not support the castv2 ' \ + 'protocol.' + return 403, message + else: + traceback.print_exc() + return 500, None finally: + self._after_stop() cast.cleanup() def pause(self): @@ -119,57 +129,122 @@ class CoinedChromecastRenderer( - pulseaudio_dlna.plugins.renderer.CoinedBaseRendererMixin, ChromecastRenderer): + pulseaudio_dlna.plugins.renderer.CoinedBaseRendererMixin, + ChromecastRenderer): def play(self, url=None, codec=None, artist=None, title=None, thumb=None): try: stream_url = url or self.get_stream_url() return ChromecastRenderer.play( self, stream_url, artist=artist, title=title, thumb=thumb) - except pulseaudio_dlna.plugins.renderer.NoSuitableEncoderFoundException: - return 500 + except pulseaudio_dlna.plugins.renderer.NoEncoderFoundException: + return 500, 'Could not find a suitable encoder!' class ChromecastRendererFactory(object): + NOTIFICATION_TYPES = [ + 'urn:dial-multiscreen-org:device:dial:1', + ] + + CHROMECAST_MODELS = [ + 'Eureka Dongle', + 'Chromecast Audio', + 'Nexus Player', + 'Freebox Player Mini', + ] + @classmethod - def from_url(self, url, type_=ChromecastRenderer): + def from_url(cls, url, type_=ChromecastRenderer): try: - response = requests.get(url) + response = requests.get(url, timeout=5) logger.debug('Response from chromecast device ({url})\n' '{response}'.format(url=url, response=response.text)) + except requests.exceptions.Timeout: + logger.warning( + 'Could no connect to {url}. ' + 'Connection timeout.'.format(url=url)) + return None except requests.exceptions.ConnectionError: - logger.info( + logger.warning( 'Could no connect to {url}. ' 'Connection refused.'.format(url=url)) return None - soup = BeautifulSoup.BeautifulSoup( - response.content.decode('utf-8'), - convertEntities=BeautifulSoup.BeautifulSoup.HTML_ENTITIES) + return cls.from_xml(url, response.content, type_) + + @classmethod + def from_xml(cls, url, xml, type_=ChromecastRenderer): url_object = urlparse.urlparse(url) ip, port = url_object.netloc.split(':') try: - model_name = soup.root.device.modelname.text - if model_name.strip() not in CHROMECAST_MODEL_NAMES: - logger.info( - 'The Chromecast seems not to be an original Chromecast! ' - 'Model name: "{model_name}" Skipping device ...'.format( - model_name=model_name)) - return None - cast_device = type_( - soup.root.device.friendlyname.text, - ip, - soup.root.device.udn.text, - soup.root.device.modelname.text, - None, - soup.root.device.manufacturer.text) - return cast_device - except AttributeError: - logger.error( - 'No valid XML returned from {url}.'.format(url=url)) + xml_root = lxml.etree.fromstring(xml) + for device in xml_root.findall('.//{*}device'): + device_type = device.find('{*}deviceType') + device_friendlyname = device.find('{*}friendlyName') + device_udn = device.find('{*}UDN') + device_modelname = device.find('{*}modelName') + device_manufacturer = device.find('{*}manufacturer') + + if device_type.text not in cls.NOTIFICATION_TYPES: + continue + + if device_modelname.text.strip() not in cls.CHROMECAST_MODELS: + logger.info( + 'The Chromecast seems not to be an original one. ' + 'Model name: "{}" Skipping device ...'.format( + device_modelname.text)) + return None + + cast_device = type_( + unicode(device_friendlyname.text), + unicode(ip), + None, + unicode(device_udn.text), + unicode(device_modelname.text), + None, + unicode(device_manufacturer.text), + ) + return cast_device + except: + logger.error('No valid XML returned from {url}.'.format(url=url)) return None @classmethod - def from_header(self, header, type_=ChromecastRenderer): + def from_header(cls, header, type_=ChromecastRenderer): if header.get('location', None): - return self.from_url(header['location'], type_) + return cls.from_url(header['location'], type_) + + @classmethod + def from_mdns_info(cls, info, type_=ChromecastRenderer): + + def _bytes2string(bytes): + ip = [] + for b in bytes: + subnet = int(b.encode('hex'), 16) + ip.append(str(subnet)) + return '.'.join(ip) + + def _get_device_info(info): + try: + return { + 'udn': '{}:{}'.format('uuid', info.properties['id']), + 'type': info.properties['md'].decode('utf-8'), + 'name': info.properties['fn'].decode('utf-8'), + 'ip': _bytes2string(info.address), + 'port': int(info.port), + } + except (KeyError, AttributeError, TypeError): + return None + + device_info = _get_device_info(info) + if device_info: + return type_( + name=device_info['name'], + ip=device_info['ip'], + port=device_info['port'], + udn=device_info['udn'], + model_name=device_info['type'], + model_number=None, + manufacturer='Google Inc.' + ) + return None
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/plugins/renderer.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/renderer.py
Changed
@@ -31,7 +31,7 @@ logger = logging.getLogger('pulseaudio_dlna.plugins.renderer') -class NoSuitableEncoderFoundException(): +class NoEncoderFoundException(): pass @@ -62,6 +62,7 @@ self._flavour = None self._codecs = [] self._rules = pulseaudio_dlna.rules.Rules() + self._workarounds = [] @property def udn(self): @@ -146,14 +147,26 @@ @property def codec(self): for codec in self.codecs: - if codec.enabled and codec.encoder.available: + if codec.enabled and codec.encoder 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() + + missing_encoders = [] + for codec in self.codecs: + for identifier, encoder_type in codec.ENCODERS.items(): + encoder = encoder_type() + if encoder.binary not in missing_encoders: + missing_encoders.append(encoder.binary) + + logger.info( + 'There was no suitable codec found for "{name}". ' + 'The device can play "{codecs}". Install one of following ' + 'encoders: "{encoders}".'.format( + name=self.label, + codecs=','.join( + [codec.mime_type for codec in self.codecs]), + encoders=','.join(missing_encoders), + )) + raise NoEncoderFoundException() @property def flavour(self): @@ -179,6 +192,14 @@ def rules(self, value): self._rules = value + @property + def workarounds(self): + return self._workarounds + + @workarounds.setter + def workarounds(self, value): + self._workarounds = value + def activate(self): pass @@ -229,10 +250,6 @@ 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_rules_from_config(self, config): self.name = config['name'] @@ -255,6 +272,30 @@ self.__str__(True))) return True + def _before_register(self): + for workaround in self.workarounds: + workaround.run('before_register') + + def _after_register(self): + for workaround in self.workarounds: + workaround.run('after_register') + + def _before_play(self): + for workaround in self.workarounds: + workaround.run('before_play') + + def _after_play(self): + for workaround in self.workarounds: + workaround.run('after_play') + + def _before_stop(self): + for workaround in self.workarounds: + workaround.run('before_stop') + + def _after_stop(self): + for workaround in self.workarounds: + workaround.run('after_stop') + def __eq__(self, other): if isinstance(other, BaseRenderer): return self.short_name == other.short_name
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/plugins/upnp/__init__.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/upnp/__init__.py
Changed
@@ -17,25 +17,85 @@ from __future__ import unicode_literals +import logging +import threading +import traceback + import pulseaudio_dlna.plugins -import pulseaudio_dlna.plugins.upnp.renderer +import pulseaudio_dlna.plugins.upnp.ssdp +import pulseaudio_dlna.plugins.upnp.ssdp.listener +import pulseaudio_dlna.plugins.upnp.ssdp.discover +from pulseaudio_dlna.plugins.upnp.renderer import ( + CoinedUpnpMediaRenderer, UpnpMediaRendererFactory) + +logger = logging.getLogger('pulseaudio_dlna.plugins.upnp') class DLNAPlugin(pulseaudio_dlna.plugins.BasePlugin): + + NOTIFICATION_TYPES = [ + 'urn:schemas-upnp-org:device:MediaRenderer:1', + 'urn:schemas-upnp-org:device:MediaRenderer:2', + ] + def __init__(self, *args): pulseaudio_dlna.plugins.BasePlugin.__init__(self, *args) - self.st_header = 'urn:schemas-upnp-org:device:MediaRenderer:1' - - def lookup(self, locations): - renderers = [] - for url in locations: - renderer = pulseaudio_dlna.plugins.upnp.renderer.UpnpMediaRendererFactory.from_url( - url, pulseaudio_dlna.plugins.upnp.renderer.CoinedUpnpMediaRenderer) - if renderer is not None: - renderers.append(renderer) - return renderers - - def create_device(self, header): - return pulseaudio_dlna.plugins.upnp.renderer.UpnpMediaRendererFactory.from_header( - header, - pulseaudio_dlna.plugins.upnp.renderer.CoinedUpnpMediaRenderer) + + def lookup(self, url, xml): + return UpnpMediaRendererFactory.from_xml( + url, xml, CoinedUpnpMediaRenderer) + + def discover(self, holder, ttl=None): + self.holder = holder + + def launch_discover(): + discover = pulseaudio_dlna.plugins.upnp.ssdp.discover\ + .SSDPDiscover( + cb_on_device_response=self._on_device_response, + ) + discover.search(ssdp_ttl=ttl) + + def launch_listener(): + ssdp = pulseaudio_dlna.plugins.upnp.ssdp.listener\ + .ThreadedSSDPListener( + cb_on_device_alive=self._on_device_added, + cb_on_device_byebye=self._on_device_removed + ) + ssdp.run(ttl=ttl) + + threads = [] + for func in [launch_discover, launch_listener]: + thread = threading.Thread(target=func) + thread.daemon = True + threads.append(thread) + try: + for thread in threads: + thread.start() + for thread in threads: + thread.join() + except: + traceback.print_exc() + + logger.debug('DLNAPlugin.discover() quit') + + @pulseaudio_dlna.plugins.BasePlugin.add_device_after + def _on_device_response(self, header, address): + st_header = header.get('st', None) + if st_header and st_header in self.NOTIFICATION_TYPES: + return UpnpMediaRendererFactory.from_header( + header, CoinedUpnpMediaRenderer) + + @pulseaudio_dlna.plugins.BasePlugin.add_device_after + def _on_device_added(self, header): + nt_header = header.get('nt', None) + if nt_header and nt_header in self.NOTIFICATION_TYPES: + return UpnpMediaRendererFactory.from_header( + header, CoinedUpnpMediaRenderer) + + @pulseaudio_dlna.plugins.BasePlugin.remove_device_after + def _on_device_removed(self, header): + nt_header = header.get('nt', None) + if nt_header and nt_header in self.NOTIFICATION_TYPES: + device_id = pulseaudio_dlna.plugins.upnp.ssdp._get_device_id( + header) + return device_id
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/upnp/byto.py
Added
@@ -0,0 +1,42 @@ +#!/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/>. + +"""A module which runs things without importing unicode_literals + +Sometimes you want pythons builtin functions just to run on raw bytes. Since +the unicode_literals module changes that behavior for many string manipulations +this module is a workarounds for not using future.utils.bytes_to_native_str +method. + +""" + +import re + + +def repair_xml(bytes): + + def strip_namespaces(match): + return 'xmlns{prefix}="{content}"'.format( + prefix=match.group(1) if match.group(1) else '', + content=match.group(2).strip(), + ) + + bytes = re.sub( + r'xmlns(:.*?)?="(.*?)"', strip_namespaces, bytes, + flags=re.IGNORECASE) + + return bytes
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/plugins/upnp/renderer.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/upnp/renderer.py
Changed
@@ -21,11 +21,15 @@ import requests import urlparse import logging +import time import pkg_resources -import BeautifulSoup +import lxml + import pulseaudio_dlna.pulseaudio import pulseaudio_dlna.encoders +import pulseaudio_dlna.workarounds import pulseaudio_dlna.plugins.renderer +import pulseaudio_dlna.plugins.upnp.byto logger = logging.getLogger('pulseaudio_dlna.plugins.upnp.renderer') @@ -155,7 +159,13 @@ if config: self.set_rules_from_config(config) else: - self.get_protocol_info() + self.codecs = [] + mime_types = self._get_protocol_info() + if mime_types: + for mime_type in mime_types: + self.add_mime_type(mime_type) + self.check_for_codec_rules() + self.prioritize_codecs() def validate(self): if self.service_transport is None: @@ -184,6 +194,7 @@ 'stop': 'xml/stop.xml', 'pause': 'xml/pause.xml', 'get_protocol_info': 'xml/get_protocol_info.xml', + 'get_transport_info': 'xml/get_transport_info.xml', } for ident, path in self.xml_files.items(): file_name = pkg_resources.resource_filename( @@ -193,6 +204,8 @@ return content def _debug(self, action, url, headers, data, response): + response_code = response.status_code if response else 'none' + response_text = response.text if response else 'none' logger.debug( 'sending {action} to {url}:\n' ' - headers:\n{headers}\n' @@ -202,10 +215,27 @@ url=url, headers=headers, data=data, - status_code=response.status_code, - result=response.text)) - - def register(self, stream_url, codec=None, artist=None, title=None, thumb=None): + status_code=response_code, + result=response_text)) + + def _update_current_state(self): + start_time = time.time() + while time.time() - start_time <= self.REQUEST_TIMEOUT: + state = self._get_transport_info() + if state is None: + return False + elif state == 'PLAYING': + self.state = self.PLAYING + return True + elif state == 'STOPPED': + self.state = self.STOP + return True + time.sleep(1) + return False + + def register( + self, stream_url, codec=None, artist=None, title=None, thumb=None): + self._before_register() url = self.service_transport.control_url codec = codec or self.codec headers = { @@ -241,18 +271,54 @@ service_type=self.service_transport.service_type, ) try: + response = None response = requests.post( url, data=data.encode(self.ENCODING), headers=headers, timeout=self.REQUEST_TIMEOUT) + return response.status_code, None + except requests.exceptions.Timeout: + message = 'REGISTER command - Could no connect to {url}. ' \ + 'Connection timeout.'.format(url=url) + return 408, message + finally: self._debug('register', url, headers, data, response) - return response.status_code + self._after_register() + + def _get_transport_info(self): + url = self.service_transport.control_url + headers = { + 'Content-Type': + 'text/xml; charset="{encoding}"'.format( + encoding=self.ENCODING), + 'SOAPAction': '"{service_type}#GetTransportInfo"'.format( + service_type=self.service_transport.service_type), + } + data = self.xml['get_transport_info'].format( + encoding=self.ENCODING, + service_type=self.service_transport.service_type, + ) + try: + response = None + response = requests.post( + url, data=data.encode(self.ENCODING), + headers=headers, timeout=self.REQUEST_TIMEOUT) + if response.status_code == 200: + try: + xml_root = lxml.etree.fromstring(response.content) + return xml_root.find('.//{*}CurrentTransportState').text + except: + logger.error( + 'No valid XML returned from {url}.'.format(url=url)) + return None except requests.exceptions.Timeout: logger.error( - 'REGISTER command - Could no connect to {url}. ' + 'TRANSPORT_INFO command - Could no connect to {url}. ' 'Connection timeout.'.format(url=url)) - return 408 + return None + finally: + self._debug('get_transport_info', url, headers, data, response) - def get_protocol_info(self): + def _get_protocol_info(self): url = self.service_connection.control_url headers = { 'Content-Type': @@ -266,36 +332,36 @@ service_type=self.service_connection.service_type, ) try: + response = None 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 + mime_types = [] + xml_root = lxml.etree.fromstring(response.content) + sinks = xml_root.find('.//{*}Sink').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_codec_rules() - self.prioritize_codecs() - except IndexError: + mime_types.append(attributes[2]) + return mime_types + except: logger.error( - 'IndexError: No valid XML returned from {url}.'.format( - url=url)) + 'No valid XML returned from {url}.'.format(url=url)) + return None 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 + return None + finally: + self._debug('get_protocol_info', url, headers, data, response) def play(self): + self._before_play() url = self.service_transport.control_url headers = { 'Content-Type': @@ -309,20 +375,23 @@ service_type=self.service_transport.service_type, ) try: + response = None 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 + return response.status_code, None except requests.exceptions.Timeout: - logger.error( - 'PLAY command - Could no connect to {url}. ' - 'Connection timeout.'.format(url=url)) - return 408 + message = 'PLAY command - Could no connect to {url}. ' \ + 'Connection timeout.'.format(url=url) + return 408, message + finally: + self._debug('play', url, headers, data, response) + self._after_play() def stop(self): + self._before_stop() url = self.service_transport.control_url headers = { 'Content-Type': @@ -336,18 +405,20 @@ service_type=self.service_transport.service_type, ) try: + response = None 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 + return response.status_code, None except requests.exceptions.Timeout: - logger.error( - 'STOP command - Could no connect to {url}. ' - 'Connection timeout.'.format(url=url)) - return 408 + message = 'STOP command - Could no connect to {url}. ' \ + 'Connection timeout.'.format(url=url) + return 408, message + finally: + self._debug('stop', url, headers, data, response) + self._after_stop() def pause(self): url = self.service_transport.control_url @@ -363,98 +434,144 @@ service_type=self.service_transport.service_type, ) try: + response = None 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 + return response.status_code, None except requests.exceptions.Timeout: - logger.error( - 'PAUSE command - Could no connect to {url}. ' - 'Connection timeout.'.format(url=url)) - return 408 + message = 'PAUSE command - Could no connect to {url}. ' \ + 'Connection timeout.'.format(url=url) + return 408, message + finally: + self._debug('pause', url, headers, data, response) class CoinedUpnpMediaRenderer( - pulseaudio_dlna.plugins.renderer.CoinedBaseRendererMixin, UpnpMediaRenderer): + pulseaudio_dlna.plugins.renderer.CoinedBaseRendererMixin, + UpnpMediaRenderer): def play(self, url=None, codec=None, artist=None, title=None, thumb=None): try: stream_url = url or self.get_stream_url() - return_code = UpnpMediaRenderer.register( + return_code, message = UpnpMediaRenderer.register( self, stream_url, codec, artist=artist, title=title, thumb=thumb) if return_code == 200: - return UpnpMediaRenderer.play(self) + if self._update_current_state(): + if self.state == self.STOP: + logger.info( + 'Device state is stopped. Sending play command.') + return UpnpMediaRenderer.play(self) + elif self.state == self.PLAYING: + logger.info( + 'Device state is playing. No need ' + 'to send play command.') + return return_code, message + else: + logger.warning( + 'Updating device state unsuccessful! ' + 'Sending play command.') + return UpnpMediaRenderer.play(self) else: logger.error('"{}" registering failed!'.format(self.name)) - return return_code + return return_code, None except requests.exceptions.ConnectionError: - logger.error('The device refused the connection!') - return 404 - except pulseaudio_dlna.plugins.renderer.NoSuitableEncoderFoundException: - logger.error('Could not find a suitable encoder!') - return 500 + return 403, 'The device refused the connection!' + except pulseaudio_dlna.plugins.renderer.NoEncoderFoundException: + return 500, 'Could not find a suitable encoder!' class UpnpMediaRendererFactory(object): - ST_HEADER = 'urn:schemas-upnp-org:device:MediaRenderer:1' + NOTIFICATION_TYPES = [ + 'urn:schemas-upnp-org:device:MediaRenderer:1', + 'urn:schemas-upnp-org:device:MediaRenderer:2', + ] @classmethod - def from_url(self, url, type_=UpnpMediaRenderer): + def from_url(cls, url, type_=UpnpMediaRenderer): try: 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( + logger.warning( 'Could no connect to {url}. ' 'Connection timeout.'.format(url=url)) return None except requests.exceptions.ConnectionError: - logger.info( + logger.warning( 'Could no connect to {url}. ' 'Connection refused.'.format(url=url)) return None - soup = BeautifulSoup.BeautifulSoup( - response.content, - convertEntities=BeautifulSoup.BeautifulSoup.HTML_ENTITIES) - url_object = urlparse.urlparse(url) - ip, port = url_object.netloc.split(':') - services = [] - try: - for device in soup.root.findAll('device'): - if device.devicetype.text != self.ST_HEADER: + return cls.from_xml(url, response.content, type_) + + @classmethod + def from_xml(cls, url, xml, type_=UpnpMediaRenderer): + + def process_xml(url, xml_root, xml, type_): + url_object = urlparse.urlparse(url) + ip, port = url_object.netloc.split(':') + services = [] + for device in xml_root.findall('.//{*}device'): + device_type = device.find('{*}deviceType') + device_friendlyname = device.find('{*}friendlyName') + device_udn = device.find('{*}UDN') + device_modelname = device.find('{*}modelName') + device_modelnumber = device.find('{*}modelNumber') + device_manufacturer = device.find('{*}manufacturer') + + if device_type.text not in cls.NOTIFICATION_TYPES: continue - for service in device.findAll('service'): + + for service in device.findall('.//{*}service'): service = { - 'service_type': service.servicetype.text, - 'service_id': service.serviceid.text, - 'scpd_url': service.scpdurl.text, - 'control_url': service.controlurl.text, - 'eventsub_url': service.eventsuburl.text, + 'service_type': service.find('{*}serviceType').text, + 'service_id': service.find('{*}serviceId').text, + 'scpd_url': service.find('{*}SCPDURL').text, + 'control_url': service.find('{*}controlURL').text, + 'eventsub_url': service.find('{*}eventSubURL').text, } services.append(service) + upnp_device = type_( - device.friendlyname.text, - ip, + unicode(device_friendlyname.text), + unicode(ip), port, - 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) + unicode(device_udn.text), + unicode(device_modelname.text) if ( + device_modelname is not None) else None, + unicode(device_modelnumber.text) if ( + device_modelnumber is not None) else None, + unicode(device_manufacturer.text) if ( + device_manufacturer is not None) else None, + services, + ) + + if device_manufacturer is not None and \ + device_manufacturer.text.lower() == 'yamaha corporation': + upnp_device.workarounds.append( + pulseaudio_dlna.workarounds.YamahaWorkaround(xml)) + return upnp_device - except AttributeError: - logger.error( - 'No valid XML returned from {url}.'.format(url=url)) - logger.info(response.content) - return None + try: + xml_root = lxml.etree.fromstring(xml) + return process_xml(url, xml_root, xml, type_) + except: + logger.debug('Got broken xml, trying to fix it.') + xml = pulseaudio_dlna.plugins.upnp.byto.repair_xml(xml) + try: + xml_root = lxml.etree.fromstring(xml) + return process_xml(url, xml_root, xml, type_) + except: + logger.error('No valid XML returned from {url}.'.format( + url=url)) + return None @classmethod - def from_header(self, header, type_=UpnpMediaRenderer): + def from_header(cls, header, type_=UpnpMediaRenderer): if header.get('location', None): - return self.from_url(header['location'], type_) + return cls.from_url(header['location'], type_)
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/upnp/ssdp
Added
+(directory)
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/upnp/ssdp/__init__.py
Added
@@ -0,0 +1,37 @@ +#!/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 re + + +def _get_header_map(header): + header = re.findall(r"(?P<name>.*?):(?P<value>.*?)\n", header) + header = { + k.strip().lower(): v.strip() for k, v in dict(header).items() + } + return header + + +def _get_device_id(header): + if 'usn' in header: + match = re.search( + "(uuid:.*?)::(.*)", header['usn'], re.IGNORECASE) + if match: + return match.group(1) + return None
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/upnp/ssdp/discover.py
Added
@@ -0,0 +1,113 @@ +#!/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 socket +import logging +import chardet +import threading +import traceback + +import pulseaudio_dlna.utils.network +import pulseaudio_dlna.plugins.upnp.ssdp + +logger = logging.getLogger('pulseaudio_dlna.discover') + + +class SSDPDiscover(object): + + SSDP_ADDRESS = '239.255.255.250' + SSDP_PORT = 1900 + SSDP_MX = 3 + SSDP_TTL = 10 + SSDP_AMOUNT = 5 + + MSEARCH_PORT = 0 + MSEARCH_MSG = '\r\n'.join([ + 'M-SEARCH * HTTP/1.1', + 'HOST: {host}:{port}', + 'MAN: "ssdp:discover"', + 'MX: {mx}', + 'ST: ssdp:all', + ]) + '\r\n' * 2 + + BUFFER_SIZE = 1024 + USE_SINGLE_SOCKET = True + + def __init__(self, cb_on_device_response): + self.cb_on_device_response = cb_on_device_response + + def search(self, ssdp_ttl=None, ssdp_mx=None, ssdp_amount=None): + ssdp_mx = ssdp_mx or self.SSDP_MX + ssdp_ttl = ssdp_ttl or self.SSDP_TTL + ssdp_amount = ssdp_amount or self.SSDP_AMOUNT + + if self.USE_SINGLE_SOCKET: + logger.debug('Binding socket to "{}" ...'.format('')) + self._search('', ssdp_ttl, ssdp_mx, ssdp_amount) + else: + ips = pulseaudio_dlna.utils.network.ipv4_addresses() + threads = [] + for ip in ips: + logger.debug('Binding socket to "{}" ...'.format(ip)) + thread = threading.Thread( + target=self._search, + args=[ip, ssdp_ttl, ssdp_mx, ssdp_amount]) + threads.append(thread) + try: + for thread in threads: + thread.start() + for thread in threads: + thread.join() + except: + traceback.print_exc() + logger.debug('SSDPDiscover.search() quit') + + def _search(self, host, ssdp_ttl, ssdp_mx, ssdp_amount): + sock = socket.socket( + socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) + sock.settimeout(ssdp_mx) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.setsockopt( + socket.IPPROTO_IP, + socket.IP_MULTICAST_TTL, + ssdp_ttl) + sock.bind((host, self.MSEARCH_PORT)) + + for i in range(1, ssdp_amount + 1): + t = threading.Timer( + float(i) / 2, self._send_discover, args=[sock, ssdp_mx]) + t.start() + + while True: + try: + header, address = sock.recvfrom(self.BUFFER_SIZE) + if self.cb_on_device_response: + guess = chardet.detect(header) + header = header.decode(guess['encoding']) + header = pulseaudio_dlna.plugins.upnp.ssdp._get_header_map( + header) + self.cb_on_device_response(header, address) + except socket.timeout: + break + sock.close() + + def _send_discover(self, sock, ssdp_mx): + msg = self.MSEARCH_MSG.format( + host=self.SSDP_ADDRESS, port=self.SSDP_PORT, mx=ssdp_mx) + sock.sendto(msg, (self.SSDP_ADDRESS, self.SSDP_PORT))
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/upnp/ssdp/listener.py
Added
@@ -0,0 +1,149 @@ +#!/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 SocketServer +import logging +import socket +import struct +import setproctitle +import time +import gobject +import chardet + +import pulseaudio_dlna.plugins.upnp.ssdp + +logger = logging.getLogger('pulseaudio_dlna.plugins.upnp.ssdp') + + +class SSDPHandler(SocketServer.BaseRequestHandler): + + SSDP_ALIVE = 'ssdp:alive' + SSDP_BYEBYE = 'ssdp:byebye' + + def handle(self): + packet = self._decode(self.request[0]) + lines = packet.splitlines() + if len(lines) > 0: + if self._is_notify_method(lines[0]): + header = pulseaudio_dlna.plugins.upnp.ssdp._get_header_map( + packet) + nts_header = header.get('nts', None) + if nts_header and nts_header == self.SSDP_ALIVE: + if self.server.cb_on_device_alive: + self.server.cb_on_device_alive(header) + elif nts_header and nts_header == self.SSDP_BYEBYE: + if self.server.cb_on_device_byebye: + self.server.cb_on_device_byebye(header) + + def _decode(self, data): + guess = chardet.detect(data) + for encoding in [guess['encoding'], 'utf-8', 'ascii']: + try: + return data.decode(encoding) + except: + pass + logger.error('Could not decode SSDP packet.') + return '' + + def _is_notify_method(self, method_header): + method = self._get_method(method_header) + return method == 'NOTIFY' + + def _get_method(self, method_header): + return method_header.split(' ')[0] + + +class SSDPListener(SocketServer.UDPServer): + + SSDP_ADDRESS = '239.255.255.250' + SSDP_PORT = 1900 + SSDP_TTL = 10 + + DISABLE_SSDP_LISTENER = False + + def __init__(self, cb_on_device_alive=None, cb_on_device_byebye=None): + self.cb_on_device_alive = cb_on_device_alive + self.cb_on_device_byebye = cb_on_device_byebye + + def run(self, ttl=None): + if self.DISABLE_SSDP_LISTENER: + return + + self.allow_reuse_address = True + SocketServer.UDPServer.__init__( + self, ('', self.SSDP_PORT), SSDPHandler) + self.socket.setsockopt( + socket.IPPROTO_IP, + socket.IP_ADD_MEMBERSHIP, + self._multicast_struct(self.SSDP_ADDRESS)) + self.socket.setsockopt( + socket.IPPROTO_IP, + socket.IP_MULTICAST_TTL, + self.SSDP_TTL) + + if ttl: + gobject.timeout_add(ttl * 1000, self.shutdown) + + setproctitle.setproctitle('ssdp_listener') + self.serve_forever(self) + logger.debug('SSDPListener.run() quit') + + def _multicast_struct(self, address): + return struct.pack( + '4sl', socket.inet_aton(address), socket.INADDR_ANY) + + +class GobjectMainLoopMixin: + + def serve_forever(self, poll_interval=0.5): + self.__running = False + self.__mainloop = gobject.MainLoop() + + if hasattr(self, 'socket'): + gobject.io_add_watch( + self, gobject.IO_IN | gobject.IO_PRI, self._on_new_request) + + context = self.__mainloop.get_context() + while not self.__running: + try: + if context.pending(): + context.iteration(True) + else: + time.sleep(0.1) + except KeyboardInterrupt: + break + logger.debug('SSDPListener.serve_forever() quit') + + def _on_new_request(self, sock, *args): + self._handle_request_noblock() + return True + + def shutdown(self, *args): + logger.debug('SSDPListener.shutdown()') + try: + self.socket.shutdown(socket.SHUT_RDWR) + except socket.error: + pass + self.__running = True + self.server_close() + + +class ThreadedSSDPListener( + GobjectMainLoopMixin, SocketServer.ThreadingMixIn, SSDPListener): + pass
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/plugins/upnp/xml/get_transport_info.xml
Added
@@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="{encoding}" standalone="yes"?> +<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> + <s:Body> + <u:GetTransportInfo xmlns:u="{service_type}"> + <InstanceID>0</InstanceID> + </u:GetTransportInfo> + </s:Body> +</s:Envelope> \ No newline at end of file
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/pulseaudio.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/pulseaudio.py
Changed
@@ -18,7 +18,6 @@ from __future__ import unicode_literals import sys -import locale import dbus import dbus.mainloop.glib import os @@ -100,8 +99,10 @@ server_address = self._get_bus_address() return dbus.connection.Connection(server_address) except dbus.exceptions.DBusException: - logger.error('PulseAudio seems not to be running or PulseAudio' - ' dbus module could not be loaded.') + logger.critical( + 'PulseAudio seems not to be running or PulseAudio ' + 'dbus module could not be loaded. The application ' + 'cannot work properly!') sys.exit(1) def update(self): @@ -122,8 +123,8 @@ 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 ' + '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): @@ -194,7 +195,7 @@ for i, b in enumerate(byte_array): if not (i == len(byte_array) - 1 and int(b) == 0): name += struct.pack('<B', b) - return name.decode(locale.getpreferredencoding()) + return pulseaudio_dlna.utils.encoding.decode_default(name) class PulseClientFactory(PulseBaseFactory): @@ -215,8 +216,9 @@ binary=self._convert_bytes_to_unicode(binary_bytes), ) except dbus.exceptions.DBusException: - logger.error('PulseClientFactory - Could not get "{object_path}" from dbus.'.format( - object_path=client_path)) + logger.error( + 'PulseClientFactory - Could not get "{object_path}" ' + 'from dbus.'.format(object_path=client_path)) return None @@ -243,13 +245,14 @@ return self.object_path > other.object_path def __str__(self): - return '<PulseClient path="{}" index="{}" name="{}" icon="{}" binary={}>\n'.format( - self.object_path, - self.index, - self.name, - self.icon, - self.binary, - ) + return '<PulseClient path="{}" index="{}" name="{}" icon="{}" ' \ + 'binary="{}">\n'.format( + self.object_path, + self.index, + self.name, + self.icon, + self.binary + ) class PulseModuleFactory(PulseBaseFactory): @@ -264,8 +267,9 @@ name=unicode(obj.Get('org.PulseAudio.Core1.Module', 'Name')), ) except dbus.exceptions.DBusException: - logger.error('PulseModuleFactory - Could not get "{object_path}" from dbus.'.format( - object_path=module_path)) + logger.error( + 'PulseModuleFactory - Could not get "{object_path}" ' + 'from dbus.'.format(object_path=module_path)) return None @@ -317,8 +321,9 @@ module=PulseModuleFactory.new(bus, module_path), ) except dbus.exceptions.DBusException: - logger.error('PulseSinkFactory - Could not get "{object_path}" from dbus.'.format( - object_path=object_path)) + logger.error( + 'PulseSinkFactory - Could not get "{object_path}" ' + 'from dbus.'.format(object_path=object_path)) return None @@ -402,16 +407,20 @@ def new(self, bus, stream_path): try: obj = bus.get_object(object_path=stream_path) - client_path = unicode(obj.Get('org.PulseAudio.Core1.Stream', 'Client')) + client_path = unicode( + obj.Get('org.PulseAudio.Core1.Stream', 'Client')) return PulseStream( object_path=unicode(stream_path), - index=unicode(obj.Get('org.PulseAudio.Core1.Stream', 'Index')), - device=unicode(obj.Get('org.PulseAudio.Core1.Stream', 'Device')), + index=unicode(obj.Get( + 'org.PulseAudio.Core1.Stream', 'Index')), + device=unicode(obj.Get( + 'org.PulseAudio.Core1.Stream', 'Device')), client=PulseClientFactory.new(bus, client_path), ) except dbus.exceptions.DBusException: - logger.error('PulseStreamFactory - Could not get "{object_path}" from dbus.'.format( - object_path=stream_path)) + logger.debug( + 'PulseStreamFactory - Could not get "{object_path}" ' + 'from dbus.'.format(object_path=stream_path)) return None @@ -446,12 +455,13 @@ return self.object_path > other.object_path def __str__(self): - return '<PulseStream path="{}" device="{}" index="{}" client="{}">'.format( - self.object_path, - self.device, - self.index, - self.client.index if self.client else None, - ) + return '<PulseStream path="{}" device="{}" index="{}" ' \ + 'client="{}">'.format( + self.object_path, + self.device, + self.index, + self.client.index if self.client else None, + ) class PulseBridge(object): @@ -467,7 +477,7 @@ return self.device == other def __str__(self): - return "<Bridge>\n {}\n {}\n".format(self.sink, self.device) + return '<Bridge>\n {}\n {}\n'.format(self.sink, self.device) class PulseWatcher(PulseAudio): @@ -475,12 +485,12 @@ ASYNC_EXECUTION = True def __init__(self, bridges_shared, message_queue, disable_switchback=False, - disable_device_stop=False, cover_mode='application'): + disable_device_stop=False, disable_auto_reconnect=True, + cover_mode='application'): PulseAudio.__init__(self) self.bridges = [] self.bridges_shared = bridges_shared - self.devices = [] self.message_queue = message_queue self.blocked_devices = [] @@ -490,6 +500,7 @@ self.disable_switchback = disable_switchback self.disable_device_stop = disable_device_stop + self.disable_auto_reconnect = disable_auto_reconnect def terminate(self, signal_number=None, frame=None): if not self.is_terminating: @@ -552,13 +563,6 @@ del self.bridges_shared[:] self.bridges_shared.extend(bridges_copy) - def update_bridges(self): - for device in self.devices: - if device not in self.bridges: - sink = self.create_null_sink( - device.short_name, device.label) - self.bridges.append(PulseBridge(sink, device)) - def cleanup(self): for bridge in self.bridges: logger.info('Remove "{}" sink ...'.format(bridge.sink.name)) @@ -583,11 +587,10 @@ def switch_back(self, bridge, reason): title = 'Device "{label}"'.format(label=bridge.device.label) if self.fallback_sink: - message = ('{reason}. Your streams were switched ' + message = ('{reason} Your streams were switched ' 'back to <b>{name}</b>'.format( reason=reason, - name=pulseaudio_dlna.utils.encoding.encode_default( - self.fallback_sink.label))) + name=self.fallback_sink.label)) pulseaudio_dlna.notification.show(title, message) self._block_device_handling(bridge.sink.object_path) @@ -613,16 +616,21 @@ 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: + reason = 'The device disconnected' + if len(stopped_bridge.sink.streams) > 1: + if not self.disable_auto_reconnect: + self._handle_sink_update(stopped_bridge.sink.object_path) + elif not self.disable_switchback: 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): + elif len(stopped_bridge.sink.streams) == 1: + stream = stopped_bridge.sink.streams[0] + if not self._was_stream_moved(stream, stopped_bridge.sink): + if not self.disable_auto_reconnect: + self._handle_sink_update(stopped_bridge.sink.object_path) + elif not self.disable_switchback: self.switch_back(stopped_bridge, reason) - elif len(stopped_bridge.sink.streams) == 0: - pass + elif len(stopped_bridge.sink.streams) == 0: + pass def on_device_updated(self, sink_path): logger.info('on_device_updated "{path}"'.format( @@ -692,15 +700,20 @@ logger.info( 'Instructing the device "{}" to stop ...'.format( bridge.device.label)) - return_code = bridge.device.stop() + return_code, message = bridge.device.stop() if return_code == 200: - logger.info('The device "{}" was stopped.'.format( - bridge.device.label)) + logger.info( + 'The device "{}" was stopped.'.format( + bridge.device.label)) else: + if not message: + message = 'Unknown reason.' logger.error( - 'The device "{}" failed to stop! ({})'.format( + 'The device "{}" failed to stop! ({}) - {}'.format( bridge.device.label, - return_code)) + return_code, + message)) + self.switch_back(bridge, message) continue if bridge.sink.object_path == sink_path: if bridge.device.state == bridge.device.IDLE or \ @@ -709,24 +722,24 @@ 'Instructing the device "{}" to play ...'.format( bridge.device.label)) artist, title, thumb = self.cover_mode.get(bridge) - return_code = bridge.device.play( + return_code, message = bridge.device.play( artist=artist, title=title, thumb=thumb) if return_code == 200: - logger.info('The device "{}" is playing.'.format( - bridge.device.label)) + logger.info( + 'The device "{}" is playing.'.format( + bridge.device.label)) else: + if not message: + message = 'Unknown reason.' logger.error( - 'The device "{}" failed to play! ({})'.format( + 'The device "{}" failed to play! ({}) - {}'.format( bridge.device.label, - return_code)) - self.switch_back( - bridge, - 'The device failed to start playing. ({})'.format( - return_code)) + return_code, + message)) + self.switch_back(bridge, message) return False def add_device(self, device): - self.devices.append(device) sink = self.create_null_sink( device.short_name, device.label) self.bridges.append(PulseBridge(sink, device)) @@ -736,7 +749,6 @@ name=device.name, flavour=device.flavour)) def remove_device(self, device): - self.devices.remove(device) bridge_index_to_remove = None for index, bridge in enumerate(self.bridges): if bridge.device == device: @@ -750,3 +762,17 @@ self.share_bridges() logger.info('Removed the device "{name}".'.format( name=device.name)) + + def update_device(self, device): + for bridge in self.bridges: + if bridge.device == device: + if bridge.device.ip != device.ip or \ + bridge.device.port != device.port: + bridge.device.ip = device.ip + bridge.device.port = device.port + logger.info( + 'Updated device "{}" - New settings: {}:{}'.format( + device.label, device.ip, device.port)) + self.update() + self.share_bridges() + break
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/streamserver.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/streamserver.py
Changed
@@ -19,7 +19,6 @@ import re import subprocess -import threading import setproctitle import logging import time @@ -27,8 +26,6 @@ import select import sys import gobject -import functools -import atexit import base64 import urllib import json @@ -53,177 +50,25 @@ PROTOCOL_VERSION_V11 = 'HTTP/1.1' -@functools.total_ordering -class RemoteDevice(object): - def __init__(self, bridge, sock): - self.bridge = bridge - self.sock = sock - try: - self.ip, self.port = sock.getpeername() - except: - logger.info('Could not get socket IP and Port. Setting to ' - 'unknown.') - self.ip = 'unknown' - self.port = 'unknown' - - def __eq__(self, other): - if isinstance(other, RemoteDevice): - return self.ip == other.ip - raise NotImplementedError - - def __gt__(self, other): - if isinstance(other, RemoteDevice): - 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)) + def __init__(self, path, sock, recorder, encoder, bridge): self.path = path + self.sock = sock self.recorder = recorder self.encoder = encoder + self.bridge = bridge + + self.id = hex(id(self)) self.recorder_process = None self.encoder_process = None - self.manager = manager - - self.sockets = {} - self.timeouts = {} self.chunk_size = 1024 * 4 - self.lock = threading.Lock() - self.client_count = 0 self.reinitialize_count = 0 - atexit.register(self.shutdown) - gobject.timeout_add( 10000, self._on_regenerate_reinitialize_count) - class UpdateThread(threading.Thread): - def __init__(self, stream): - threading.Thread.__init__(self) - self.stream = stream - self.is_running = False - self.do_stop = False - self.lock = threading.Lock() - self.lock.acquire() - - def run(self): - while True: - if self.do_stop: - break - elif self.is_running is False: - self.lock.acquire() - else: - self.stream.communicate() - logger.info('Thread stopped for "{}".'.format( - self.stream.path)) - - def stop(self): - self.do_stop = True - 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() - - def register(self, bridge, sock, lock_override=False): - try: - if not lock_override: - self.lock.acquire() - device = RemoteDevice(bridge, sock) - logger.info( - 'Client {client} registered to stream {path}.'.format( - client=device.ip, - path=self.path)) - self.sockets[sock] = device - self.client_count += 1 - self.update_thread.resume() - finally: - if not lock_override: - self.lock.release() - - def unregister(self, sock, lock_override=False, method=0): - try: - if not lock_override: - self.lock.acquire() - try: - device = self.sockets[sock] - del self.sockets[sock] - sock.close() - except KeyError: - logger.info('A client id tries to unregister a stream which is ' - 'not registered, this should never happen...') - return - - logger.info( - 'Client {client} unregistered stream {path} ' - 'using method {method}.'.format( - client=device.ip, - method=method, - path=self.path)) - - if device.ip in self.timeouts: - gobject.source_remove(self.timeouts[device.ip]) - self.timeouts[device.ip] = gobject.timeout_add( - 2000, self._on_delayed_disconnect, device) - - self.client_count -= 1 - finally: - if not lock_override: - self.lock.release() - - def _on_regenerate_reinitialize_count(self): - if self.reinitialize_count > 0: - self.reinitialize_count -= 1 - return True - - def _on_delayed_disconnect(self, device): - self.timeouts.pop(device.ip) - - if len(self.sockets) == 0: - logger.info('Stream closed. ' - 'Cleaning up remaining processes ...') - self.update_thread.pause() - self.terminate_processes() - - self.manager._on_device_disconnect(device, self) - return False - - def communicate(self): - try: - self.lock.acquire() - + def run(self): + while True: if not self.do_processes_exist(): self.create_processes() logger.info( @@ -237,37 +82,22 @@ path=self.path)) data = self.encoder_process.stdout.read(self.chunk_size) - socks = self.sockets.keys() - try: - r, w, e = select.select(socks, socks, [], 0) - except socket.error: - for sock in socks: - try: - r, w, e = select.select([sock], [], [], 0) - except socket.error: - self.unregister(sock, lock_override=True, method=1) - return - - for sock in w: + r, w, e = select.select([self.sock], [self.sock], [], 0) + + if self.sock in w: + try: + self._send_data(self.sock, data) + except socket.error: + break + + if self.sock in r: try: - self._send_data(sock, data) + data = self.sock.recv(1024) + if len(data) == 0: + break 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() + break + self.terminate_processes() def _send_data(self, sock, data): bytes_total = len(data) @@ -275,6 +105,11 @@ while bytes_sent < bytes_total: bytes_sent += sock.send(data[bytes_sent:]) + def _on_regenerate_reinitialize_count(self): + if self.reinitialize_count > 0: + self.reinitialize_count -= 1 + return True + def do_processes_exist(self): return (self.encoder_process is not None and self.recorder_process is not None) @@ -287,6 +122,7 @@ def _kill_process(process): pid = process.pid + logger.debug('Terminating process {} ...'.format(pid)) try: os.kill(pid, signal.SIGTERM) _pid, return_code = os.waitpid(pid, 0) @@ -302,7 +138,7 @@ def create_processes(self): if self.reinitialize_count < 3: self.reinitialize_count += 1 - logger.debug('Starting processes "{recorder} | {encoder}"'.format( + logger.info('Starting processes "{recorder} | {encoder}"'.format( recorder=' '.join(self.recorder.command), encoder=' '.join(self.encoder.command))) self.recorder_process = subprocess.Popen( @@ -319,91 +155,67 @@ 'the record process. Aborting.'.format( self.reinitialize_count)) - def shutdown(self, *args): - 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): - return self.path == other.path - raise NotImplementedError - - def __gt__(self, other): - if isinstance(other, ProcessStream): - return self.path > other.path - raise NotImplementedError - def __str__(self): - return '<{} id="{}" path="{}" state="{}">\n{}'.format( + return '<{} id="{}">\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): - self.single_streams = [] - self.shared_streams = {} + self.streams = {} + self.timeouts = {} self.server = server - def _on_device_disconnect(self, remote_device, stream): - - def _send_bridge_disconnected(bridge): - logger.info('Device "{}" disconnected.'.format(bridge.device.name)) + def create_stream(self, path, request, bridge): + stream = ProcessStream( + path=path, + sock=request, + recorder=bridge.device.codec.get_recorder(bridge.sink.monitor), + encoder=bridge.device.codec.encoder, + bridge=bridge, + ) + self.register(stream) + stream.run() + self.unregister(stream) + + def register(self, stream): + logger.info('Registered stream "{}" ({}) ...'.format( + stream.path, stream.id)) + if not self.streams.get(stream.path, None): + self.streams[stream.path] = {} + self.streams[stream.path][stream.id] = stream + + def unregister(self, stream): + logger.info('Unregistered stream "{}" ({}) ...'.format( + stream.path, stream.id)) + del self.streams[stream.path][stream.id] + + if stream.path in self.timeouts: + gobject.source_remove(self.timeouts[stream.path]) + self.timeouts[stream.path] = gobject.timeout_add( + 2000, self._on_disconnect, stream) + + def _on_disconnect(self, stream): + self.timeouts.pop(stream.path) + if len(self.streams[stream.path]) == 0: + logger.info('No more stream from device "{}".'.format( + stream.bridge.device.name)) self.server.message_queue.put({ 'type': 'on_bridge_disconnected', - 'stopped_bridge': bridge, + 'stopped_bridge': stream.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 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) - self.single_streams.append(stream) - return stream - else: - # 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) - 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( + return '<{}>\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()]), + '\n'.join( + [' {}\n {}'.format( + path, + ' '.join([str(s) for id, s in streams.items()])) + for path, streams in self.streams.items()], + ), ) @@ -428,28 +240,15 @@ if isinstance(item, pulseaudio_dlna.images.BaseImage): self.wfile.write(item.data) elif isinstance(item, pulseaudio_dlna.pulseaudio.PulseBridge): - stream = self.server.stream_manager.get_stream(self.path, item) - stream.register(item, self.request) - self.keep_connection_alive() - - def keep_connection_alive(self): - self.close_connection = 0 - self.wfile.flush() - - while True: - try: - r, w, e = select.select([self.request], [], [], 0) - except socket.error: - logger.debug('Socket died, releasing request thread.') - break - time.sleep(1) + self.server.stream_manager.create_stream( + self.path, self.request, item) def handle_headers(self, item): response_code = 200 headers = {} if not item: - logger.info('Error 404: File not found "{}"'.format(self.path)) + logger.info('Requested file not found "{}"'.format(self.path)) self.send_error(404, 'File not found: %s' % self.path) return elif isinstance(item, pulseaudio_dlna.images.BaseImage): @@ -460,7 +259,8 @@ headers['Content-Type'] = bridge.device.codec.specific_mime_type if self.server.fake_http_content_length or \ - pulseaudio_dlna.rules.FAKE_HTTP_CONTENT_LENGTH in bridge.device.codec.rules: + 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: @@ -490,6 +290,7 @@ headers['contentFeatures.dlna.org'] = str(content_features) headers['Ext'] = '' headers['transferMode.dlna.org'] = 'Streaming' + headers['Content-Disposition'] = 'inline;' logger.debug('Sending header ({response_code}):\n{header}'.format( response_code=response_code, @@ -551,11 +352,7 @@ return {} def log_message(self, format, *args): - args = [unicode(arg) for arg in args] - logger.info('Got request from {host} - {args}' .format( - host=self.address_string(), - time=self.log_date_time_string(), - args=','.join(args))) + pass class StreamServer(SocketServer.TCPServer): @@ -572,8 +369,15 @@ def run(self): self.allow_reuse_address = True - SocketServer.TCPServer.__init__( - self, ('', self.port), StreamRequestHandler) + try: + SocketServer.TCPServer.__init__( + self, (self.ip or '', self.port), StreamRequestHandler) + except socket.error: + logger.critical( + 'The streaming server could not bind to your specified port ' + '({port}). Perhaps this is already in use? The application ' + 'cannot work properly!'.format(port=self.port)) + sys.exit(1) setproctitle.setproctitle('stream_server') self.serve_forever()
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/utils/encoding.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/utils/encoding.py
Changed
@@ -17,11 +17,56 @@ from __future__ import unicode_literals +import logging import sys import locale +import chardet +logger = logging.getLogger('pulseaudio_dlna.plugins.utils.encoding') -def encode_default(bytes): - return bytes.encode( - sys.stdout.encoding or locale.getpreferredencoding() or 'ascii', - errors='replace') + +class NotBytesException(Exception): + def __init__(self, var): + Exception.__init__( + self, + 'The specified variable is {}". ' + 'Must be bytes.'.format(type(var)) + ) + + +def decode_default(bytes): + if type(bytes) is not str: + raise NotBytesException(bytes) + guess = chardet.detect(bytes) + encodings = { + 'sys.stdout.encoding': sys.stdout.encoding, + 'locale.getpreferredencoding': locale.getpreferredencoding(), + 'chardet.detect': guess['encoding'], + 'utf-8': 'utf-8', + 'latin1': 'latin1', + } + for encoding in encodings.values(): + if encoding and encoding != 'ascii': + try: + return bytes.decode(encoding) + except UnicodeDecodeError: + continue + try: + return bytes.decode('ascii', errors='replace') + except UnicodeDecodeError: + logger.error( + 'Decoding failed using the following encodings: "{}"'.format( + ','.join( + ['{}:{}'.format(f, e) for f, e in encodings.items()] + ))) + return 'Unknown' + + +def _bytes2hex(bytes, seperator=':'): + if type(bytes) is not str: + raise NotBytesException(bytes) + return seperator.join('{:02x}'.format(ord(b)) for b in bytes) + + +def _hex2bytes(hex, seperator=':'): + return b''.join(chr(int(h, 16)) for h in hex.split(seperator))
View file
pulseaudio-dlna-0.4.7.tar.gz/pulseaudio_dlna/utils/git.py -> pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/utils/git.py
Changed
@@ -42,6 +42,6 @@ prefix, ref_path = [s.strip() for s in line.split('ref: ')] branch = os.path.basename(ref_path) ref_path = os.path.join(module_path, GIT_DIRECTORY, ref_path) - return branch, _get_first_line(ref_path).strip() + return branch, (_get_first_line(ref_path) or 'unknown').strip() else: return 'detached-head', line.strip()
View file
pulseaudio-dlna-0.5.0.1.tar.gz/pulseaudio_dlna/workarounds.py
Added
@@ -0,0 +1,350 @@ +#!/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 logging +from lxml import etree +import requests +import urlparse +import traceback + + +logger = logging.getLogger('pulseaudio_dlna.workarounds') + + +class BaseWorkaround(object): + """ + Define functions which are called at specific situations during the + application. + + Those may be: + - before_register + - after_register + - before_play + - after_play + - before_stop + - after_stop + + This may be extended in the future. + """ + + ENABLED = True + + def __init__(self): + pass + + def run(self, method_name, *args, **kwargs): + method = getattr(self, method_name, None) + if self.ENABLED and method and callable(method): + logger.info('Running workaround "{}".'.format(method_name)) + method(*args, **kwargs) + + +class YamahaWorkaround(BaseWorkaround): + # Misc constants + REQUEST_TIMEOUT = 5 + ENCODING = 'utf-8' + URL_FORMAT = 'http://{ip}:{port}{url}' + + # MediaRenderer constants + MR_YAMAHA_PREFIX = 'yamaha' + MR_YAMAHA_DEVICE = MR_YAMAHA_PREFIX + ':' + 'X_device' + MR_YAMAHA_URLBASE = MR_YAMAHA_PREFIX + ':' + 'X_URLBase' + MR_YAMAHA_SERVICELIST = MR_YAMAHA_PREFIX + ':' + 'X_serviceList' + MR_YAMAHA_SERVICE = MR_YAMAHA_PREFIX + ':' + 'X_service' + MR_YAMAHA_CONTROLURL = MR_YAMAHA_PREFIX + ':' + 'X_controlURL' + + MR_YAMAHA_URLBASE_PATH = '/'.join([MR_YAMAHA_DEVICE, MR_YAMAHA_URLBASE]) + MR_YAMAHA_CONTROLURL_PATH = '/'.join( + [MR_YAMAHA_DEVICE, MR_YAMAHA_SERVICELIST, MR_YAMAHA_SERVICE, + MR_YAMAHA_CONTROLURL]) + + # YamahaRemoteControl constants + YRC_TAG_ROOT = 'YAMAHA_AV' + YRC_KEY_RC = 'RC' + YRC_CMD_GETPARAM = 'GetParam' + YRC_BASEPATH_CONFIG = 'Config' + YRC_BASEPATH_BASICSTATUS = 'Basic_Status' + YRC_BASEPATH_FEATURES = 'Feature_Existence' + YRC_BASEPATH_INPUTNAMES = 'Name/Input' + YRC_BASEPATH_POWER = 'Power_Control/Power' + YRC_BASEPATH_SOURCE = 'Input/Input_Sel' + YRC_VALUE_POWER_ON = 'On' + YRC_VALUE_POWER_OFF = 'Standby' + + YRC_REQUEST_CONTENTTYPE = 'text/xml; charset="{encoding}"'.format( + encoding=ENCODING) + YRC_REQUEST_TEMPLATE = \ + '<?xml version="1.0" encoding="{encoding}"?>' \ + '<YAMAHA_AV cmd="{cmd}">{request}</YAMAHA_AV>' + + # Known server modes + YRC_SERVER_MODES = ['SERVER', 'PC'] + + def __init__(self, xml): + BaseWorkaround.__init__(self) + self.enabled = False + + self.control_url = None + self.ip = None + self.port = None + + self.zones = None + self.sources = None + + self.server_mode_zone = None + self.server_mode_source = None + + try: + # Initialize YamahaRemoteControl interface + if (not self._detect_remotecontrolinterface(xml)): + logger.warning( + 'Automatic source switching will not be enabled' + ' - Please switch to server mode manually to enable UPnP' + ' streaming' + ) + return + self.enabled = True + except: + traceback.print_exc() + + def _detect_remotecontrolinterface(self, xml): + # Check for YamahaRemoteControl support + if (not self._parse_xml(xml)): + logger.info('No Yamaha RemoteControl interface detected') + return False + logger.info('Yamaha RemoteControl found: ' + self.URL_FORMAT.format( + ip=self.ip, port=self.port, url=self.control_url)) + # Get supported features + self.zones, self.sources = self._query_supported_features() + if ((self.zones is None) or (self.sources is None)): + logger.error('Failed to query features') + return False + # Determine main zone + logger.info('Supported zones: ' + ', '.join(self.zones)) + self.server_mode_zone = self.zones[0] + logger.info('Using \'{zone}\' as main zone'.format( + zone=self.server_mode_zone + )) + # Determine UPnP server source + if (self.sources): + logger.info('Supported sources: ' + ', '.join(self.sources)) + for source in self.YRC_SERVER_MODES: + if (source not in self.sources): + continue + self.server_mode_source = source + break + else: + logger.warning('Querying supported features failed') + if (not self.server_mode_source): + logger.warning('Unable to determine UPnP server mode source') + return False + logger.info('Using \'{source}\' as UPnP server mode source'.format( + source=self.server_mode_source + )) + return True + + def _parse_xml(self, xml): + # Parse MediaRenderer description XML + xml_root = etree.fromstring(xml) + namespaces = xml_root.nsmap + namespaces.pop(None, None) + + # Determine AVRC URL + url_base = xml_root.find(self.MR_YAMAHA_URLBASE_PATH, namespaces) + control_url = xml_root.find(self.MR_YAMAHA_CONTROLURL_PATH, namespaces) + if ((url_base is None) or (control_url is None)): + return False + ip, port = urlparse.urlparse(url_base.text).netloc.split(':') + if ((not ip) or (not port)): + return False + + self.ip = ip + self.port = port + self.control_url = control_url.text + return True + + def _generate_request(self, cmd, root, path, value): + # Generate headers + headers = { + 'Content-Type': self.YRC_REQUEST_CONTENTTYPE, + } + # Generate XML request + tags = path.split('/') + if (root): + tags = [root] + tags + request = '' + for tag in tags: + request += '<{tag}>'.format(tag=tag) + request += value + for tag in reversed(tags): + request += '</{tag}>'.format(tag=tag) + body = self.YRC_REQUEST_TEMPLATE.format( + encoding=self.ENCODING, + cmd=cmd, + request=request, + ) + # Construct URL + url = self.URL_FORMAT.format( + ip=self.ip, + port=self.port, + url=self.control_url, + ) + return headers, body, url + + def _get(self, root, path, value, filter_path=None): + # Generate request + headers, data, url = self._generate_request('GET', root, path, value) + # POST request + try: + logger.debug('Yamaha RC request: '+data) + response = requests.post( + url, data.encode(self.ENCODING), + headers=headers, timeout=self.REQUEST_TIMEOUT) + logger.debug('Yamaha RC response: ' + response.text) + if response.status_code != 200: + logger.error( + 'Yamaha RC request failed - Status code: {code}'.format( + code=response.status_code)) + return None + except requests.exceptions.Timeout: + logger.error('Yamaha RC request failed - Connection timeout') + return None + # Parse response + xml_root = etree.fromstring(response.content) + if (xml_root.tag != self.YRC_TAG_ROOT): + logger.error("Malformed response: Root tag missing") + return None + # Parse response code + rc = xml_root.get(self.YRC_KEY_RC) + if (not rc): + logger.error("Malformed response: RC attribute missing") + return None + rc = int(rc) + if (rc > 0): + logger.error( + 'Yamaha RC request failed - Response code: {code}'.format( + code=rc)) + return rc + # Only return subtree + result_path = [] + if (root): + result_path.append(root) + result_path.append(path) + if (filter_path): + result_path.append(filter_path) + result_path = '/'.join(result_path) + return xml_root.find(result_path) + + def _put(self, root, path, value): + # Generate request + headers, data, url = self._generate_request('PUT', root, path, value) + # POST request + try: + logger.debug('Yamaha RC request: '+data) + response = requests.post( + url, data.encode(self.ENCODING), + headers=headers, timeout=self.REQUEST_TIMEOUT) + logger.debug('Yamaha RC response: ' + response.text) + if response.status_code != 200: + logger.error( + 'Yamaha RC request failed - Status code: {code}'.format( + code=response.status_code)) + return False + except requests.exceptions.Timeout: + logger.error('Yamaha RC request failed - Connection timeout') + return None + # Parse response + xml_root = etree.fromstring(response.content) + if (xml_root.tag != self.YRC_TAG_ROOT): + logger.error("Malformed response: Root tag missing") + return None + # Parse response code + rc = xml_root.get(self.YRC_KEY_RC) + if (not rc): + logger.error("Malformed response: RC attribute missing") + return None + rc = int(rc) + if (rc > 0): + logger.error( + 'Yamaha RC request failed - Response code: {code}'.format( + code=rc)) + return rc + return 0 + + def _query_supported_features(self): + xml_response = self._get('System', 'Config', self.YRC_CMD_GETPARAM) + if (xml_response is None): + return None, None + + xml_features = xml_response.find(self.YRC_BASEPATH_FEATURES) + if (xml_features is None): + logger.debug('Failed to find feature description') + return None, None + + # Features can be retrieved in different ways, most probably + # dependending on the recever's firmware / protocol version + # Here are the different responses known up to now: + # + # 1. Comma-separated list of all features in one single tag, containing + # all input sources + # 2. Each feature is enclosed by a tag along with context information + # depending on the XML path: + # - YRC_BASEPATH_FEATURES: availability and/or support + # (0 == not supported, 1 == supported) + # - YRC_BASEPATH_INPUTNAMES: input/source name + # Every feature is a input source, if it does not contain the + # substring 'Zone'. Otherwise, it is a zone supported by the + # receiver. + zones = [] + sources = [] + if (xml_features.text): + # Format 1: + sources = xml_features.text.split(',') + else: + # Format 2: + for child in xml_features.getchildren(): + if ((not child.text) or (int(child.text) == 0)): + continue + if ('Zone' in child.tag): + zones.append(child.tag) + else: + sources.append(child.tag) + xml_names = xml_response.find(self.YRC_BASEPATH_INPUTNAMES) + if (xml_names is not None): + for child in xml_names.getchildren(): + sources.append(child.tag) + + # If we got no zones up to now, we have to assume, that the receiver + # has no multi zone support. Thus there can be only one! + # Let's call it "System" and pray for the best! + if (len(zones) == 0): + zones.append('System') + + return zones, sources + + def _set_source(self, value, zone=None): + if (not zone): + zone = self.server_mode_zone + self._put(zone, self.YRC_BASEPATH_SOURCE, value) + + def before_register(self): + if (not self.enabled): + return + logger.info('Switching to UPnP server mode') + self._set_source(self.server_mode_source)
View file
pulseaudio-dlna-0.4.7.tar.gz/scripts/radio.py -> pulseaudio-dlna-0.5.0.1.tar.gz/scripts/radio.py
Changed
@@ -34,8 +34,7 @@ logger = logging.getLogger('radio') import pulseaudio_dlna -import pulseaudio_dlna.renderers -import pulseaudio_dlna.discover +import pulseaudio_dlna.holder import pulseaudio_dlna.plugins.upnp import pulseaudio_dlna.plugins.chromecast import pulseaudio_dlna.codecs @@ -59,7 +58,7 @@ def _stop(self, name, flavour=None): device = self._get_device(name, flavour) if device: - return_code = device.stop() + return_code, message = device.stop() if return_code == 200: logger.info( 'The device "{name}" was instructed to stop'.format( @@ -81,7 +80,7 @@ codec = self._get_codec(url) device = self._get_device(name, flavour) if device: - return_code = device.play(url, codec, artist, title, thumb) + return_code, message = device.play(url, codec, artist, title, thumb) if return_code == 200: logger.info( 'The device "{name}" was instructed to play'.format( @@ -116,14 +115,13 @@ return None def _discover_devices(self): - holder = pulseaudio_dlna.renderers.RendererHolder(self.PLUGINS) - discover = pulseaudio_dlna.discover.RendererDiscover(holder) - discover.search() + holder = pulseaudio_dlna.holder.Holder(self.PLUGINS) + holder.search(ttl=5) logger.info('Found the following devices:') - for udn, device in holder.renderers.iteritems(): + for udn, device in holder.devices.items(): logger.info(' - "{name}" ({flavour})'.format( name=device.name, flavour=device.flavour)) - return holder.renderers.values() + return holder.devices.values() # Local pulseaudio-dlna installations running in a virutalenv should run this # script as module: @@ -136,9 +134,7 @@ sys.exit(0) devices = [ - ('Wohnzimmer', 'Chromecast'), - ('Küche', 'Chromecast'), - ('Schlafzimmer', 'Chromecast'), + ('Alle', 'Chromecast'), ] for device in devices:
View file
pulseaudio-dlna-0.4.7.tar.gz/setup.py -> pulseaudio-dlna-0.5.0.1.tar.gz/setup.py
Changed
@@ -15,21 +15,9 @@ # You should have received a copy of the GNU General Public License # along with pulseaudio-dlna. If not, see <http://www.gnu.org/licenses/>. -import os -import re import setuptools -def get_version(): - path = os.path.abspath(os.path.dirname(__file__)) - path = os.path.join(path, "debian", "changelog") - ex = r"pulseaudio-dlna \((\d+\.\d+\.\d+(\.\d+)?)\) .*$" - with open(path) as f: - releases = f.readlines() - releases = [re.match(ex, i) for i in releases] - releases = [i.group(1) for i in releases if i] - return releases[0] - setuptools.setup( name="pulseaudio-dlna", author="Massimo Mund", @@ -46,11 +34,10 @@ "Topic :: Multimedia :: Sound/Audio", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", ], - version=get_version(), + version='0.5.0.1', py_modules=[], packages=setuptools.find_packages(), install_requires=[ - "BeautifulSoup >= 3.2.1", "docopt >= 0.6.1", "requests >= 2.2.1", "setproctitle >= 1.0.1", @@ -60,6 +47,8 @@ "futures >= 2.1.6", "chardet >= 2.0.1", "netifaces >= 0.8", + "lxml >= 3", + "zeroconf >= 0.17", ], entry_points={ "console_scripts": [ @@ -67,7 +56,7 @@ ] }, data_files=[ - ("share/man/man1", ["debian/pulseaudio-dlna.1"]), + ("share/man/man1", ["man/pulseaudio-dlna.1"]), ], package_data={ "pulseaudio_dlna.plugins.upnp": ["xml/*.xml"],
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
.