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]replace/b43bf02ddeddd088c0e6b94974ca1a46562eb3db
| @@ -1,7 +1,7 @@ | |||
| # Licensed under the MIT license | |||
| # http://opensource.org/licenses/mit-license.php | |||
| # Copyright 2005, Tim Potter <tpot@samba.org> | |||
| # Copyright 2006 John-Mark Gurney <gurney_j@resnet.uoregon.edu> | |||
| # Copyright 2006-2007 John-Mark Gurney <gurney_j@resnet.uoregon.edu> | |||
| __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 | |||
| @@ -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): | |||
| @@ -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 | |||
| @@ -0,0 +1,145 @@ | |||
| # -*- coding: utf-8 -*- | |||
| # Licensed under the MIT license | |||
| # http://opensource.org/licenses/mit-license.php | |||
| # Copyright 2006,2007 Frank Scholz <coherence@beebits.net> | |||
| # | |||
| # 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()) | |||
| @@ -2,7 +2,7 @@ | |||
| # http://opensource.org/licenses/mit-license.php | |||
| # Copyright 2005, Tim Potter <tpot@samba.org> | |||
| # Copyright 2006 John-Mark Gurney <gurney_j@resnet.uroegon.edu> | |||
| # Copyright 2006-2007 John-Mark Gurney <gurney_j@resnet.uroegon.edu> | |||
| __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) | |||