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 | # Licensed under the MIT license | ||||
| # http://opensource.org/licenses/mit-license.php | # http://opensource.org/licenses/mit-license.php | ||||
| # Copyright 2005, Tim Potter <tpot@samba.org> | # 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$' | __version__ = '$Change$' | ||||
| # $Id$ | # $Id$ | ||||
| @@ -83,6 +83,7 @@ def doRecallgen(defer, fun, *args, **kwargs): | |||||
| class ContentDirectoryControl(UPnPPublisher, dict): | class ContentDirectoryControl(UPnPPublisher, dict): | ||||
| """This class implements the CDS actions over SOAP.""" | """This class implements the CDS actions over SOAP.""" | ||||
| namespace = 'urn:schemas-upnp-org:service:ContentDirectory:1' | |||||
| updateID = property(lambda x: x['0'].updateID) | updateID = property(lambda x: x['0'].updateID) | ||||
| urlbase = property(lambda x: x._urlbase) | urlbase = property(lambda x: x._urlbase) | ||||
| @@ -160,20 +161,20 @@ class ContentDirectoryControl(UPnPPublisher, dict): | |||||
| """Required: Return the searching capabilities supported by the device.""" | """Required: Return the searching capabilities supported by the device.""" | ||||
| log.msg('GetSearchCapabilities()') | log.msg('GetSearchCapabilities()') | ||||
| return { 'SearchCapabilitiesResponse': { 'SearchCaps': '' }} | |||||
| return { 'SearchCaps': '' } | |||||
| def soap_GetSortCapabilities(self, *args, **kwargs): | def soap_GetSortCapabilities(self, *args, **kwargs): | ||||
| """Required: Return the CSV list of meta-data tags that can be used in | """Required: Return the CSV list of meta-data tags that can be used in | ||||
| sortCriteria.""" | sortCriteria.""" | ||||
| log.msg('GetSortCapabilities()') | log.msg('GetSortCapabilities()') | ||||
| return { 'SortCapabilitiesResponse': { 'SortCaps': '' }} | |||||
| return { 'SortCaps': '' } | |||||
| def soap_GetSystemUpdateID(self, *args, **kwargs): | def soap_GetSystemUpdateID(self, *args, **kwargs): | ||||
| """Required: Return the current value of state variable SystemUpdateID.""" | """Required: Return the current value of state variable SystemUpdateID.""" | ||||
| log.msg('GetSystemUpdateID()') | log.msg('GetSystemUpdateID()') | ||||
| return { 'SystemUpdateIdResponse': { 'Id': self.updateID }} | |||||
| return { 'Id': self.updateID } | |||||
| BrowseFlags = ('BrowseMetaData', 'BrowseDirectChildren') | BrowseFlags = ('BrowseMetaData', 'BrowseDirectChildren') | ||||
| @@ -205,7 +206,6 @@ class ContentDirectoryControl(UPnPPublisher, dict): | |||||
| RequestedCount = int(RequestedCount) | RequestedCount = int(RequestedCount) | ||||
| didl = DIDLElement() | didl = DIDLElement() | ||||
| result = {} | |||||
| # return error code if we don't exist anymore | # return error code if we don't exist anymore | ||||
| if ObjectID not in self: | if ObjectID not in self: | ||||
| @@ -231,14 +231,13 @@ class ContentDirectoryControl(UPnPPublisher, dict): | |||||
| r = { 'Result': didl.toString(), 'TotalMatches': total, | r = { 'Result': didl.toString(), 'TotalMatches': total, | ||||
| 'NumberReturned': didl.numItems(), } | 'NumberReturned': didl.numItems(), } | ||||
| result = { 'BrowseResponse': r } | |||||
| if hasattr(self[ObjectID], 'updateID'): | if hasattr(self[ObjectID], 'updateID'): | ||||
| r['UpdateID'] = self[ObjectID].updateID | r['UpdateID'] = self[ObjectID].updateID | ||||
| else: | else: | ||||
| r['UpdateID'] = self.updateID | r['UpdateID'] = self.updateID | ||||
| return result | |||||
| return r | |||||
| # Optional actions | # Optional actions | ||||
| @@ -4,7 +4,7 @@ | |||||
| __version__ = '$Change$' | __version__ = '$Change$' | ||||
| # $Id$ | # $Id$ | ||||
| ffmpeg_path = '/Users/jgurney/src/ffmpeg/ffmpeg' | |||||
| ffmpeg_path = '/usr/local/bin/ffmpeg' | |||||
| import FileDIDL | import FileDIDL | ||||
| import errno | import errno | ||||
| @@ -143,6 +143,7 @@ class DynamTransfer(protocol.ProcessProtocol): | |||||
| vcodec = 'xvid' | vcodec = 'xvid' | ||||
| mimetype = { 'xvid': 'video/avi', 'mpeg2': 'video/mpeg', } | mimetype = { 'xvid': 'video/avi', 'mpeg2': 'video/mpeg', } | ||||
| mimetype = { 'xvid': 'video/x-msvideo', 'mpeg2': 'video/mpeg', } | |||||
| request.setHeader('content-type', mimetype[vcodec]) | request.setHeader('content-type', mimetype[vcodec]) | ||||
| if request.method == 'HEAD': | if request.method == 'HEAD': | ||||
| return '' | return '' | ||||
| @@ -158,7 +159,7 @@ class DynamTransfer(protocol.ProcessProtocol): | |||||
| args = [ 'ffmpeg', '-i', path, '-b', '8000', | args = [ 'ffmpeg', '-i', path, '-b', '8000', | ||||
| #'-sc_threshold', '500000', '-b_strategy', '1', '-max_b_frames', '6', | #'-sc_threshold', '500000', '-b_strategy', '1', '-max_b_frames', '6', | ||||
| ] + optdict[vcodec] + audio + [ '-', ] | ] + optdict[vcodec] + audio + [ '-', ] | ||||
| #log.msg(*args) | |||||
| #log.msg(*[`i` for i in args]) | |||||
| self.proc = process.Process(reactor, ffmpeg_path, args, | self.proc = process.Process(reactor, ffmpeg_path, args, | ||||
| None, None, self) | None, None, self) | ||||
| self.proc.closeStdin() | self.proc.closeStdin() | ||||
| @@ -174,6 +175,13 @@ class DynamicTrans(resource.Resource): | |||||
| self.notrans = notrans | self.notrans = notrans | ||||
| def render(self, request): | 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: | if request.postpath: | ||||
| # Translation request | # Translation request | ||||
| return DynamTransfer(self.path, request.postpath, request).render() | 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.size = os.path.getsize(self.FSpath) | ||||
| self.res = [ self.res ] | self.res = [ self.res ] | ||||
| self.res.append(Resource(self.url + '/mpeg2', 'http-get:*:%s:*' % 'video/mpeg')) | 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) | Item.doUpdate(self) | ||||
| def ignoreFiles(path, fobj): | def ignoreFiles(path, fobj): | ||||
| @@ -12,6 +12,7 @@ it. | |||||
| Tested devices and/or programs: | Tested devices and/or programs: | ||||
| Intel's Media Control Point and Media Renderer | Intel's Media Control Point and Media Renderer | ||||
| D-Link DSM-520 | D-Link DSM-520 | ||||
| Sony PlayStation 3 | |||||
| The Intel tools are good for testing and are available at: | 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 | 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 | * Twisted (only core and web necessary, tested w/ 2.1.0 and | ||||
| Web 0.5.0) | Web 0.5.0) | ||||
| * ElementTree | * 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: | For more information, check out the software page at: | ||||
| http://resnet.uoregon.edu/~gurney_j/jmpc/pymeds.html | http://resnet.uoregon.edu/~gurney_j/jmpc/pymeds.html | ||||
| @@ -49,10 +54,21 @@ Ideas for future improvements: | |||||
| childCount isn't a required attribute. | childCount isn't a required attribute. | ||||
| Autodetect IP address. | Autodetect IP address. | ||||
| Support sorting by other attributes. | Support sorting by other attributes. | ||||
| Finish support for playing DVD stream. | |||||
| Finish support for playing DVD's. | |||||
| v0.x: | v0.x: | ||||
| Support multiple SSDP servers on the same box. | 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: | v0.3: | ||||
| Include some patches for twisted in the distro, in the directory | 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 | # http://opensource.org/licenses/mit-license.php | ||||
| # Copyright 2005, Tim Potter <tpot@samba.org> | # 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$' | __version__ = '$Change$' | ||||
| # $Id$ | # $Id$ | ||||
| @@ -10,7 +10,9 @@ __version__ = '$Change$' | |||||
| from twisted.web import soap | from twisted.web import soap | ||||
| from twisted.python import log | from twisted.python import log | ||||
| import SOAPpy | |||||
| from types import * | |||||
| import soap_lite | |||||
| class errorCode(Exception): | class errorCode(Exception): | ||||
| def __init__(self, status): | def __init__(self, status): | ||||
| @@ -20,20 +22,24 @@ class UPnPPublisher(soap.SOAPPublisher): | |||||
| """UPnP requires OUT parameters to be returned in a slightly | """UPnP requires OUT parameters to be returned in a slightly | ||||
| different way than the SOAPPublisher class does.""" | different way than the SOAPPublisher class does.""" | ||||
| namespace = None | |||||
| def _gotResult(self, result, request, methodName): | 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) | self._sendResponse(request, response) | ||||
| def _gotError(self, failure, request, methodName): | def _gotError(self, failure, request, methodName): | ||||
| e = failure.value | e = failure.value | ||||
| status = 500 | status = 500 | ||||
| if isinstance(e, SOAPpy.faultType): | |||||
| fault = e | |||||
| if isinstance(e, errorCode): | |||||
| status = e.status | |||||
| else: | 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) | self._sendResponse(request, response, status=status) | ||||