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