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) |