Projects
Multimedia
pulseaudio-dlna
Sign Up
Log In
Username
Password
We truncated the diff of some files because they were too big. If you want to see the full diff for every file,
click here
.
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
Expand all
Collapse all
Changes of Revision 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', '-', '-']
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:
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(',')
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
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'),
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
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(
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():
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,
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
.