Browse Source

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]
replace/b43bf02ddeddd088c0e6b94974ca1a46562eb3db
John-Mark Gurney 17 years ago
parent
commit
6ab094a420
5 changed files with 198 additions and 24 deletions
  1. +6
    -7
      ContentDirectory.py
  2. +11
    -3
      FSStorage.py
  3. +19
    -3
      README
  4. +145
    -0
      et.py
  5. +17
    -11
      upnp.py

+ 6
- 7
ContentDirectory.py View File

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



+ 11
- 3
FSStorage.py View File

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


+ 19
- 3
README View File

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


+ 145
- 0
et.py View File

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

+ 17
- 11
upnp.py View File

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

Loading…
Cancel
Save