A Python UPnP Media Server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

299 lines
7.9 KiB

  1. #!/usr/bin/env python
  2. # Copyright 2006-2009 John-Mark Gurney <jmg@funkthat.com>
  3. __version__ = '$Change$'
  4. # $Id$
  5. ffmpeg_path = '/a/home/jmg/src/ffmpeg/ffmpeg'
  6. ffmpeg_path = '/usr/local/bin/ffmpeg'
  7. ffmpeg_path = '/a/home/jmg/src/ffmpeg.tmp/ffmpeg'
  8. ffmpeg_path = '/usr/local/bin/ffmpeg-devel'
  9. import FileDIDL
  10. import errno
  11. import itertools
  12. import os
  13. import stat
  14. from DIDLLite import StorageFolder, Item, Resource, ResourceList
  15. from twisted.web import resource, server, static
  16. from twisted.python import log
  17. from twisted.internet import abstract, interfaces, process, protocol, reactor
  18. from zope.interface import implementer
  19. __all__ = [ 'registerklassfun', 'registerfiletoignore',
  20. 'FSObject', 'FSItem', 'FSDirectory',
  21. ]
  22. mimedict = static.loadMimeTypes()
  23. _klassfuns = []
  24. def registerklassfun(fun, debug=False):
  25. _klassfuns.append((fun, debug))
  26. _filestoignore = {
  27. '.DS_Store': None
  28. }
  29. def registerfiletoignore(f):
  30. _filestoignore[f] = None
  31. # Return this class when you want the file to be skipped. If you return this,
  32. # no other modules will be applied, and it won't be added. Useful for things
  33. # like .DS_Store which are known to useless on a media server.
  34. class IgnoreFile:
  35. pass
  36. def statcmp(a, b, cmpattrs = [ 'st_ino', 'st_dev', 'st_size', 'st_mtime', ]):
  37. if a is None or b is None:
  38. return False
  39. for i in cmpattrs:
  40. if getattr(a, i) != getattr(b, i):
  41. return False
  42. return True
  43. class FSObject(object):
  44. def __init__(self, path):
  45. self.FSpath = path
  46. self.pstat = None
  47. def checkUpdate(self, **kwargs):
  48. # need to handle no such file or directory
  49. # push it up? but still need to handle disappearing
  50. try:
  51. nstat = os.stat(self.FSpath)
  52. #print 'cU:', `self`, `self.pstat`, `nstat`, statcmp(self.pstat, nstat)
  53. if statcmp(self.pstat, nstat):
  54. return
  55. self.pstat = nstat
  56. self.doUpdate(**kwargs)
  57. except OSError as x:
  58. log.msg('os.stat, OSError: %s' % x)
  59. if x.errno in (errno.ENOENT, errno.ENOTDIR, errno.EPERM, ):
  60. # We can't access it anymore, delete it
  61. self.cd.delItem(self.id)
  62. return
  63. else:
  64. raise
  65. def __repr__(self):
  66. return '<%s.%s: path: %s, id: %s, parent: %s, title: %s>' % \
  67. (self.__class__.__module__, self.__class__.__name__,
  68. repr(self.FSpath), self.id, self.parentID, repr(self.title))
  69. #@implementer(interfaces.IConsumer)
  70. #class NullConsumer(file, abstract.FileDescriptor):
  71. #
  72. # def __init__(self):
  73. # file.__init__(self, '/dev/null', 'w')
  74. # abstract.FileDescriptor.__init__(self)
  75. #
  76. # def write(self, data):
  77. # pass
  78. class DynamTransfer(protocol.ProcessProtocol):
  79. def __init__(self, path, mods, request):
  80. self.path = path
  81. self.mods = mods
  82. self.request = request
  83. def outReceived(self, data):
  84. self.request.write(data)
  85. def outConnectionLost(self):
  86. if self.request:
  87. self.request.unregisterProducer()
  88. self.request.finish()
  89. self.request = None
  90. def errReceived(self, data):
  91. pass
  92. #log.msg(data)
  93. def stopProducing(self):
  94. if self.request:
  95. self.request.unregisterProducer()
  96. self.request.finish()
  97. if self.proc:
  98. self.proc.loseConnection()
  99. self.proc.signalProcess('INT')
  100. self.request = None
  101. self.proc = None
  102. pauseProducing = lambda x: x.proc.pauseProducing()
  103. resumeProducing = lambda x: x.proc.resumeProducing()
  104. def render(self):
  105. mods = self.mods
  106. path = self.path
  107. request = self.request
  108. mimetype = { 'xvid': 'video/x-msvideo',
  109. 'mpeg2': 'video/mpeg',
  110. 'mp4': 'video/mp4',
  111. }
  112. vcodec = mods[0]
  113. if mods[0] not in mimetype:
  114. vcodec = 'mp4'
  115. request.setHeader('content-type', mimetype[vcodec])
  116. if request.method == 'HEAD':
  117. return ''
  118. audiomp3 = [ '-acodec', 'mp3', '-ab', '192k', '-ac', '2', ]
  119. audiomp2 = [ '-acodec', 'mp2', '-ab', '256k', '-ac', '2', ]
  120. audioac3 = [ '-acodec', 'ac3', '-ab', '640k', ]
  121. audioaac = [ '-acodec', 'aac', '-ab', '640k', ]
  122. optdict = {
  123. 'xvid': [ '-vcodec', 'xvid',
  124. #'-mv4', '-gmc', '-g', '240',
  125. '-f', 'avi', ] + audiomp3,
  126. 'mpeg2': [ '-vcodec', 'mpeg2video', #'-g', '60',
  127. '-f', 'mpegts', ] + audioac3,
  128. 'mp4': [ '-vcodec', 'libx264', #'-g', '60',
  129. '-f', 'mpegts', ] + audioaac,
  130. }
  131. args = [ 'ffmpeg', '-i', path,
  132. '-sameq',
  133. '-threads', '4',
  134. #'-vb', '8000k',
  135. #'-sc_threshold', '500000', '-b_strategy', '1', '-max_b_frames', '6',
  136. ] + optdict[vcodec] + [ '-', ]
  137. log.msg(*[repr(i) for i in args])
  138. self.proc = process.Process(reactor, ffmpeg_path, args,
  139. None, None, self)
  140. self.proc.closeStdin()
  141. request.registerProducer(self, 1)
  142. return server.NOT_DONE_YET
  143. class DynamicTrans(resource.Resource):
  144. isLeaf = True
  145. def __init__(self, path, notrans):
  146. self.path = path
  147. self.notrans = notrans
  148. def render(self, request):
  149. #if request.getHeader('getcontentfeatures.dlna.org'):
  150. # request.setHeader('contentFeatures.dlna.org', 'DLNA.ORG_OP=01;DLNA.ORG_CI=0')
  151. # # we only want the headers
  152. # self.notrans.render(request)
  153. # request.unregisterProducer()
  154. # return ''
  155. if request.postpath:
  156. # Translation request
  157. return DynamTransfer(self.path, request.postpath, request).render()
  158. else:
  159. request.setHeader('transferMode.dlna.org', 'Streaming')
  160. request.setHeader('contentFeatures.dlna.org', 'DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=017000 00000000000000000000000000')
  161. return self.notrans.render(request)
  162. class FSItem(FSObject, Item):
  163. def __init__(self, *args, **kwargs):
  164. FSObject.__init__(self, kwargs.pop('path'))
  165. mimetype = kwargs.pop('mimetype')
  166. kwargs['content'] = DynamicTrans(self.FSpath,
  167. static.File(self.FSpath, mimetype))
  168. Item.__init__(self, *args, **kwargs)
  169. self.url = '%s/%s' % (self.cd.urlbase, self.id)
  170. self.mimetype = mimetype
  171. self.checkUpdate()
  172. def doUpdate(self):
  173. #print 'FSItem doUpdate:', `self`
  174. self.res = ResourceList()
  175. r = Resource(self.url, 'http-get:*:%s:*' % self.mimetype)
  176. r.size = os.path.getsize(self.FSpath)
  177. self.res.append(r)
  178. if self.mimetype.split('/', 1)[0] == 'video':
  179. self.res.append(Resource(self.url + '/mpeg2',
  180. 'http-get:*:%s:*' % 'video/mpeg'))
  181. self.res.append(Resource(self.url + '/xvid',
  182. 'http-get:*:%s:*' % 'video/x-msvideo'))
  183. Item.doUpdate(self)
  184. def ignoreFiles(path, fobj):
  185. bn = os.path.basename(path)
  186. if bn in _filestoignore:
  187. return IgnoreFile, None
  188. elif bn[:2] == '._' and open(path).read(4) == '\x00\x05\x16\x07':
  189. # AppleDouble encoded Macintosh Resource Fork
  190. return IgnoreFile, None
  191. return None, None
  192. def defFS(path, fobj):
  193. if os.path.isdir(path):
  194. # new dir
  195. return FSDirectory, { 'path': path }
  196. elif os.path.isfile(path):
  197. # new file - fall through to below
  198. pass
  199. else:
  200. log.msg('skipping (not dir or reg): %s' % path)
  201. return None, None
  202. klass, mt = FileDIDL.buildClassMT(FSItem, path)
  203. return klass, { 'path': path, 'mimetype': mt }
  204. def dofileadd(path, name):
  205. klass = None
  206. fsname = os.path.join(path, name)
  207. try:
  208. fobj = open(fsname)
  209. except:
  210. fobj = None
  211. for i, debug in itertools.chain(( (ignoreFiles, False), ), _klassfuns, ( (defFS, False), )):
  212. try:
  213. try:
  214. # incase the call expects a clean file
  215. fobj.seek(0)
  216. except:
  217. pass
  218. #log.msg('testing:', `i`, `fsname`, `fobj`)
  219. klass, kwargs = i(fsname, fobj)
  220. if klass is not None:
  221. break
  222. except:
  223. if debug:
  224. import traceback
  225. traceback.print_exc(file=log.logfile)
  226. if klass is None or klass is IgnoreFile:
  227. return None, None, None, None
  228. #print 'matched:', os.path.join(path, name), `i`, `klass`
  229. return klass, name, (), kwargs
  230. class FSDirectory(FSObject, StorageFolder):
  231. def __init__(self, *args, **kwargs):
  232. path = kwargs['path']
  233. del kwargs['path']
  234. StorageFolder.__init__(self, *args, **kwargs)
  235. FSObject.__init__(self, path)
  236. def genCurrent(self):
  237. return ((x.id, os.path.basename(x.FSpath)) for x in self )
  238. def genChildren(self):
  239. return os.listdir(self.FSpath)
  240. def createObject(self, i):
  241. return dofileadd(self.FSpath, i)
  242. def __repr__(self):
  243. return ('<%s.%s: path: %s, id: %s, parent: %s, title: %s, ' + \
  244. 'cnt: %d>') % (self.__class__.__module__,
  245. self.__class__.__name__, self.FSpath, self.id,
  246. self.parentID, self.title, len(self))