From 6ab094a4209645f0a45ea7f8bd165f297c8fd209 Mon Sep 17 00:00:00 2001 From: John-Mark Gurney Date: Fri, 23 Nov 2007 23:56:23 -0800 Subject: [PATCH] update to use soap_lite from coherence, this makes the PS3 work... This includes other fixes like using methodResponse instead of other crap.. Thing brings it more inline w/ the UPnP spec... We now include the namespace bit, which is apparently what the PS3 needs... note that the PS3 works in the readme w/ restrictions... This removes the dependance on SOAPpy, and alsp fpconst... [git-p4: depot-paths = "//depot/": change = 1105] --- ContentDirectory.py | 13 ++-- FSStorage.py | 14 ++++- README | 22 ++++++- et.py | 145 ++++++++++++++++++++++++++++++++++++++++++++ upnp.py | 28 +++++---- 5 files changed, 198 insertions(+), 24 deletions(-) create mode 100644 et.py diff --git a/ContentDirectory.py b/ContentDirectory.py index 9ae5734..d68757f 100644 --- a/ContentDirectory.py +++ b/ContentDirectory.py @@ -1,7 +1,7 @@ # Licensed under the MIT license # http://opensource.org/licenses/mit-license.php # Copyright 2005, Tim Potter -# Copyright 2006 John-Mark Gurney +# Copyright 2006-2007 John-Mark Gurney __version__ = '$Change$' # $Id$ @@ -83,6 +83,7 @@ def doRecallgen(defer, fun, *args, **kwargs): class ContentDirectoryControl(UPnPPublisher, dict): """This class implements the CDS actions over SOAP.""" + namespace = 'urn:schemas-upnp-org:service:ContentDirectory:1' updateID = property(lambda x: x['0'].updateID) urlbase = property(lambda x: x._urlbase) @@ -160,20 +161,20 @@ class ContentDirectoryControl(UPnPPublisher, dict): """Required: Return the searching capabilities supported by the device.""" log.msg('GetSearchCapabilities()') - return { 'SearchCapabilitiesResponse': { 'SearchCaps': '' }} + return { 'SearchCaps': '' } def soap_GetSortCapabilities(self, *args, **kwargs): """Required: Return the CSV list of meta-data tags that can be used in sortCriteria.""" log.msg('GetSortCapabilities()') - return { 'SortCapabilitiesResponse': { 'SortCaps': '' }} + return { 'SortCaps': '' } def soap_GetSystemUpdateID(self, *args, **kwargs): """Required: Return the current value of state variable SystemUpdateID.""" log.msg('GetSystemUpdateID()') - return { 'SystemUpdateIdResponse': { 'Id': self.updateID }} + return { 'Id': self.updateID } BrowseFlags = ('BrowseMetaData', 'BrowseDirectChildren') @@ -205,7 +206,6 @@ class ContentDirectoryControl(UPnPPublisher, dict): RequestedCount = int(RequestedCount) didl = DIDLElement() - result = {} # return error code if we don't exist anymore if ObjectID not in self: @@ -231,14 +231,13 @@ class ContentDirectoryControl(UPnPPublisher, dict): r = { 'Result': didl.toString(), 'TotalMatches': total, 'NumberReturned': didl.numItems(), } - result = { 'BrowseResponse': r } if hasattr(self[ObjectID], 'updateID'): r['UpdateID'] = self[ObjectID].updateID else: r['UpdateID'] = self.updateID - return result + return r # Optional actions diff --git a/FSStorage.py b/FSStorage.py index 55049fb..ca4cb6c 100644 --- a/FSStorage.py +++ b/FSStorage.py @@ -4,7 +4,7 @@ __version__ = '$Change$' # $Id$ -ffmpeg_path = '/Users/jgurney/src/ffmpeg/ffmpeg' +ffmpeg_path = '/usr/local/bin/ffmpeg' import FileDIDL import errno @@ -143,6 +143,7 @@ class DynamTransfer(protocol.ProcessProtocol): vcodec = 'xvid' mimetype = { 'xvid': 'video/avi', 'mpeg2': 'video/mpeg', } + mimetype = { 'xvid': 'video/x-msvideo', 'mpeg2': 'video/mpeg', } request.setHeader('content-type', mimetype[vcodec]) if request.method == 'HEAD': return '' @@ -158,7 +159,7 @@ class DynamTransfer(protocol.ProcessProtocol): args = [ 'ffmpeg', '-i', path, '-b', '8000', #'-sc_threshold', '500000', '-b_strategy', '1', '-max_b_frames', '6', ] + optdict[vcodec] + audio + [ '-', ] - #log.msg(*args) + #log.msg(*[`i` for i in args]) self.proc = process.Process(reactor, ffmpeg_path, args, None, None, self) self.proc.closeStdin() @@ -174,6 +175,13 @@ class DynamicTrans(resource.Resource): self.notrans = notrans def render(self, request): + #if request.getHeader('getcontentfeatures.dlna.org'): + # request.setHeader('contentFeatures.dlna.org', 'DLNA.ORG_OP=01;DLNA.ORG_CI=0') + # # we only want the headers + # self.notrans.render(request) + # request.unregisterProducer() + # return '' + if request.postpath: # Translation request return DynamTransfer(self.path, request.postpath, request).render() @@ -197,7 +205,7 @@ class FSItem(FSObject, Item): self.res.size = os.path.getsize(self.FSpath) self.res = [ self.res ] self.res.append(Resource(self.url + '/mpeg2', 'http-get:*:%s:*' % 'video/mpeg')) - self.res.append(Resource(self.url + '/xvid', 'http-get:*:%s:*' % 'video/avi')) + self.res.append(Resource(self.url + '/xvid', 'http-get:*:%s:*' % 'video/x-msvideo')) Item.doUpdate(self) def ignoreFiles(path, fobj): diff --git a/README b/README index 1555b9f..c1a977d 100644 --- a/README +++ b/README @@ -12,6 +12,7 @@ it. Tested devices and/or programs: Intel's Media Control Point and Media Renderer D-Link DSM-520 + Sony PlayStation 3 The Intel tools are good for testing and are available at: http://www.intel.com/cd/ids/developer/asmo-na/eng/downloads/upnp/index.htm @@ -24,8 +25,12 @@ The following packages are required to run the media server: * Twisted (only core and web necessary, tested w/ 2.1.0 and Web 0.5.0) * ElementTree - * SOAPpy available from Python Web Services - * fpconst (required by SOAPpy) + +NOTE: SOAPpy is no longer required as I have included soap_lite from the +Coherence project: https://coherence.beebits.net/ . + +Thanks to Coherence for soap_lite that solved the issues w/ PS3 not seeing +the media server. For more information, check out the software page at: http://resnet.uoregon.edu/~gurney_j/jmpc/pymeds.html @@ -49,10 +54,21 @@ Ideas for future improvements: childCount isn't a required attribute. Autodetect IP address. Support sorting by other attributes. - Finish support for playing DVD stream. + Finish support for playing DVD's. v0.x: Support multiple SSDP servers on the same box. + Fix SSDP to set the max-age to 7 days. We now retransmit replies + and reannounce ourselves randomly before our original announcement + expires. This fixes the Server Disconnects I was seeing on the + DSM-520! + Change how the mpegtsmod handles multi-stream TS's. Instead of + calling tssel.py, we fixup the PAT to only contain the channel + we want. This does mean we send more data than we need, but + means that we can make the stream seekable. + Now works w/ PS3. The PS3 still has issues as it has limited + codec support (D-Link is better) and does not support AC3 audio + in MPEG-TS streams yet (not even downsampling to stereo PCM). v0.3: Include some patches for twisted in the distro, in the directory diff --git a/et.py b/et.py new file mode 100644 index 0000000..4bdf77e --- /dev/null +++ b/et.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +# Licensed under the MIT license +# http://opensource.org/licenses/mit-license.php + +# Copyright 2006,2007 Frank Scholz +# +# a little helper to get the proper ElementTree package + +import re + +try: + import cElementTree as ET + import elementtree + #print "we are on CET" +except ImportError: + try: + from elementtree import ElementTree as ET + import elementtree + #print "simply using ET" + except ImportError: + """ this seems to be necessary with the python2.5 on the Maemo platform """ + try: + from xml.etree import cElementTree as ET + from xml import etree as elementtree + except ImportError: + try: + from xml.etree import ElementTree as ET + from xml import etree as elementtree + except ImportError: + #print "no ElementTree module found, critical error" + raise ImportError, "no ElementTree module found, critical error" + +utf8_escape = re.compile(eval(r'u"[&<>\"]+"')) +escape = re.compile(eval(r'u"[&<>\"\u0080-\uffff]+"')) + +def encode_entity(text, pattern=escape): + # map reserved and non-ascii characters to numerical entities + def escape_entities(m, map=elementtree.ElementTree._escape_map): + out = [] + append = out.append + for char in m.group(): + t = map.get(char) + if t is None: + t = "&#%d;" % ord(char) + append(t) + return ''.join(out) + try: + return elementtree.ElementTree._encode(pattern.sub(escape_entities, text), 'ascii') + except TypeError: + elementtree.ElementTree._raise_serialization_error(text) + +def new_encode_entity(text, pattern=utf8_escape): + # map reserved and non-ascii characters to numerical entities + def escape_entities(m, map=elementtree.ElementTree._escape_map): + out = [] + append = out.append + for char in m.group(): + t = map.get(char) + if t is None: + t = "&#%d;" % ord(char) + append(t) + if type(text) == unicode: + return ''.join(out) + else: + return u''.encode('utf-8').join(out) + try: + if type(text) == unicode: + return elementtree.ElementTree._encode(escape.sub(escape_entities, text), 'ascii') + else: + return elementtree.ElementTree._encode(utf8_escape.sub(escape_entities, text.decode('utf-8')), 'utf-8') + except TypeError: + + elementtree.ElementTree._raise_serialization_error(text) + +elementtree.ElementTree._encode_entity = new_encode_entity + +# it seems there are some ElementTree libs out there +# which have the alias XMLParser and some that haven't. +# +# So we just use the XMLTreeBuilder method for now +# if XMLParser isn't available. + +if not hasattr(ET, 'XMLParser'): + def XMLParser(encoding='utf-8'): + return ET.XMLTreeBuilder() + + ET.XMLParser = XMLParser + +def namespace_map_update(namespaces): + #try: + # from xml.etree import ElementTree + #except ImportError: + # from elementtree import ElementTree + + elementtree.ElementTree._namespace_map.update(namespaces) + +class ElementInterface(elementtree.ElementTree._ElementInterface): + """ helper class """ + +def indent(elem, level=0): + """ generate pretty looking XML, based upon: + http://effbot.org/zone/element-lib.htm#prettyprint + """ + i = "\n" + level*" " + if len(elem): + if not elem.text or not elem.text.strip(): + elem.text = i + " " + for elem in elem: + indent(elem, level+1) + if not elem.tail or not elem.tail.strip(): + elem.tail = i + if not elem.tail or not elem.tail.strip(): + elem.tail = i + else: + if level and (not elem.tail or not elem.tail.strip()): + elem.tail = i + +def parse_xml(data, encoding="utf-8"): + p = ET.XMLParser(encoding=encoding) + + # my version of twisted.web returns page_infos as a dictionary in + # the second item of the data list + if isinstance(data, (list, tuple)): + data, _ = data + + try: + data = data.encode(encoding) + except UnicodeDecodeError: + pass + except Exception, error: + print "parse_xml encode Exception", error + import traceback + traceback.print_exc() + + # Guess from who we're getting this? + data = data.replace('\x00','') + try: + p.feed(data) + except Exception, error: + print "parse_xml feed Exception", error + print error, repr(data) + return None + else: + return ET.ElementTree(p.close()) diff --git a/upnp.py b/upnp.py index a0a406b..9581232 100644 --- a/upnp.py +++ b/upnp.py @@ -2,7 +2,7 @@ # http://opensource.org/licenses/mit-license.php # Copyright 2005, Tim Potter -# Copyright 2006 John-Mark Gurney +# Copyright 2006-2007 John-Mark Gurney __version__ = '$Change$' # $Id$ @@ -10,7 +10,9 @@ __version__ = '$Change$' from twisted.web import soap from twisted.python import log -import SOAPpy +from types import * + +import soap_lite class errorCode(Exception): def __init__(self, status): @@ -20,20 +22,24 @@ class UPnPPublisher(soap.SOAPPublisher): """UPnP requires OUT parameters to be returned in a slightly different way than the SOAPPublisher class does.""" + namespace = None + def _gotResult(self, result, request, methodName): - response = SOAPpy.buildSOAP(kw=result, encoding=self.encoding) + ns = self.namespace + if ns: + meth = "{%s}%s" % (ns, methodName) + else: + meth = methodName + response = soap_lite.build_soap_call(meth, result, + is_response=True, encoding=None) self._sendResponse(request, response) def _gotError(self, failure, request, methodName): e = failure.value status = 500 - if isinstance(e, SOAPpy.faultType): - fault = e + if isinstance(e, errorCode): + status = e.status else: - if isinstance(e, errorCode): - status = e.status - else: - failure.printTraceback(file = log.logfile) - fault = SOAPpy.faultType("%s:Server" % SOAPpy.NS.ENV_T, "Method %s failed." % methodName) - response = SOAPpy.buildSOAP(fault, encoding=self.encoding) + failure.printTraceback(file = log.logfile) + response = soap_lite.build_soap_error(status) self._sendResponse(request, response, status=status)