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.

364 lines
8.7 KiB

  1. #!/usr/bin/env python
  2. # Copyright 2006-2008 John-Mark Gurney <jmg@funkthat.com>
  3. '''MPEG-TS Handling'''
  4. __version__ = '$Change$'
  5. # $Id$
  6. tsselpypath = 'mpegts/tssel.py'
  7. default_audio_lang = 'eng'
  8. import array
  9. import itertools
  10. import os
  11. import sets
  12. import struct
  13. import sys
  14. mpegtspath = 'mpegts'
  15. if mpegtspath not in sys.path:
  16. sys.path.append(mpegtspath)
  17. import mpegts
  18. import tssel
  19. from DIDLLite import StorageFolder, VideoItem, Resource
  20. from FSStorage import FSObject, registerklassfun
  21. from twisted.python import log, threadable
  22. from twisted.spread import pb
  23. from twisted.internet import abstract, process, protocol, reactor
  24. from twisted.web import error, http, resource, server
  25. class _LimitedFile(file):
  26. def __init__(self, *args, **kwargs):
  27. self.__size = kwargs['size']
  28. del kwargs['size']
  29. file.__init__(self, *args, **kwargs)
  30. def remain(self):
  31. pos = self.tell()
  32. if pos > self.__size:
  33. return 0
  34. return self.__size - pos
  35. def read(self, size=-1):
  36. if size < 0:
  37. return file.read(self, self.remain())
  38. return file.read(self, min(size, self.remain()))
  39. def _gennameindexes(chan):
  40. d = {}
  41. for i in chan:
  42. t = '%s %s.%s' % (i['name'], i['major'], i['minor'])
  43. d[t] = i
  44. return d
  45. class MPEGTSTransfer(pb.Viewable):
  46. def __init__(self, iterable, request):
  47. self.iter = iter(iterable)
  48. self.request = request
  49. request.registerProducer(self, 0)
  50. def resumeProducing(self):
  51. if not self.request:
  52. return
  53. # get data and write to request.
  54. try:
  55. data = self.iter.next()
  56. if data:
  57. # this .write will spin the reactor, calling
  58. # .doWrite and then .resumeProducing again, so
  59. # be prepared for a re-entrant call
  60. self.request.write(data)
  61. except StopIteration:
  62. if self.request:
  63. self.request.unregisterProducer()
  64. self.request.finish()
  65. self.request = None
  66. def pauseProducing(self):
  67. pass
  68. def stopProducing(self):
  69. # close zipfile
  70. self.request = None
  71. # Remotely relay producer interface.
  72. def view_resumeProducing(self, issuer):
  73. self.resumeProducing()
  74. def view_pauseProducing(self, issuer):
  75. self.pauseProducing()
  76. def view_stopProducing(self, issuer):
  77. self.stopProducing()
  78. synchronized = ['resumeProducing', 'stopProducing']
  79. threadable.synchronize(MPEGTSTransfer)
  80. class DynamTSTransfer(pb.Viewable):
  81. def __init__(self, path, pmt, *pids):
  82. self.path = path
  83. #log.msg("DynamTSTransfer: pmt: %s, pids: %s" % (pmt, pids))
  84. self.pmt = pmt
  85. self.pids = pids
  86. self.didpat = False
  87. def resumeProducing(self):
  88. if not self.request:
  89. return
  90. repcnt = 0
  91. data = self.fp.read(min(abstract.FileDescriptor.bufferSize,
  92. self.size - self.written) // 188 * 188)
  93. dataarray = array.array('B', data)
  94. for i in xrange(0, len(data), 188):
  95. if data[i] != 'G':
  96. print 'bad sync'
  97. continue
  98. frst = dataarray[i + 1]
  99. pid = (frst & 0x1f) << 8 | dataarray[i + 2]
  100. if not frst & 0x40:
  101. continue
  102. elif not self.didpat and pid == 0:
  103. startpmt = i + 4
  104. if ((dataarray[i + 3] >> 4) & 0x3) == 0x3:
  105. # Adaptation
  106. startpmt += dataarray[startpmt] + 1
  107. startpmt += dataarray[startpmt] + 1
  108. assert data[startpmt] =='\x00', (startpmt,
  109. data[i:startpmt + 4])
  110. arraysize = ((dataarray[startpmt + 1] &
  111. 0xf) << 8) | dataarray[startpmt + 2]
  112. startpmt += 3
  113. arraysize -= 4 # CRC
  114. # Remaining fields before array
  115. startpmt += 5
  116. arraysize -= 5
  117. for startpmt in xrange(startpmt,
  118. min(i + 188 - 3, startpmt + arraysize), 4):
  119. prognum, ppid = struct.unpack('>2H',
  120. data[startpmt:startpmt + 4])
  121. ppid = ppid & 0x1fff
  122. if ppid == self.pmt:
  123. break
  124. else:
  125. raise KeyError, 'unable to find pmt(%d) in pkt: %s' % (pmt, `data[i:i + 188]`)
  126. self.pats = itertools.cycle(tssel.genpats(
  127. self.pmt, prognum))
  128. self.didpat = True
  129. if pid == 0 and self.didpat:
  130. assert data[i + 4] =='\x00' and \
  131. data[i + 5] == '\x00', 'error: %s' % `data[i:i + 10]`
  132. repcnt += 1
  133. pn = self.pats.next()
  134. data = data[:i] + pn + data[i +
  135. 188:]
  136. if repcnt > 1:
  137. print 'repcnt:', repcnt, 'len(data):', len(data)
  138. if data:
  139. self.written += len(data)
  140. self.request.write(data)
  141. if self.request and self.fp.tell() == self.size:
  142. self.request.unregisterProducer()
  143. self.request.finish()
  144. self.request = None
  145. def pauseProducing(self):
  146. pass
  147. def stopProducing(self):
  148. self.fp.close()
  149. self.request = None
  150. def render(self, request):
  151. path = self.path
  152. pmt = self.pmt
  153. pids = self.pids
  154. self.request = request
  155. fsize = size = os.path.getsize(path)
  156. request.setHeader('accept-ranges','bytes')
  157. request.setHeader('content-type', 'video/mpeg')
  158. try:
  159. self.fp = open(path)
  160. except IOError, e:
  161. import errno
  162. if e[0] == errno.EACCESS:
  163. return error.ForbiddenResource().render(request)
  164. else:
  165. raise
  166. if request.setLastModified(os.path.getmtime(path)) is http.CACHED:
  167. return ''
  168. trans = True
  169. # Commented out because it's totally broken. --jknight 11/29/04
  170. # XXX - fixed? jmg 2/17/06
  171. range = request.getHeader('range')
  172. tsize = size
  173. if range is not None:
  174. # This is a request for partial data...
  175. bytesrange = range.split('=')
  176. assert bytesrange[0] == 'bytes', \
  177. "Syntactically invalid http range header!"
  178. start, end = bytesrange[1].split('-', 1)
  179. if start:
  180. start = int(start)
  181. self.fp.seek(start)
  182. if end and int(end) < size:
  183. end = int(end)
  184. else:
  185. end = size - 1
  186. else:
  187. lastbytes = int(end)
  188. if size < lastbytes:
  189. lastbytes = size
  190. start = size - lastbytes
  191. self.fp.seek(start)
  192. fsize = lastbytes
  193. end = size - 1
  194. start = start // 188 * 188
  195. self.fp.seek(start)
  196. size = (end + 1) // 188 * 188
  197. fsize = end - int(start) + 1
  198. # start is the byte offset to begin, and end is the
  199. # byte offset to end.. fsize is size to send, tsize
  200. # is the real size of the file, and size is the byte
  201. # position to stop sending.
  202. if fsize <= 0:
  203. request.setResponseCode(http.REQUESTED_RANGE_NOT_SATISFIABLE
  204. )
  205. fsize = tsize
  206. trans = False
  207. else:
  208. request.setResponseCode(http.PARTIAL_CONTENT)
  209. request.setHeader('content-range',"bytes %s-%s/%s " % (
  210. str(start), str(end), str(tsize)))
  211. request.setHeader('content-length', str(fsize))
  212. if request.method == 'HEAD' or trans is False:
  213. request.method = 'HEAD'
  214. return ''
  215. self.size = tsize
  216. self.written = 0
  217. request.registerProducer(self, 0)
  218. return server.NOT_DONE_YET
  219. class MPEGTSResource(resource.Resource):
  220. isLeaf = True
  221. def __init__(self, *args):
  222. resource.Resource.__init__(self)
  223. self.args = args
  224. def render(self, request):
  225. request.setHeader('content-type', 'video/mpeg')
  226. # return data
  227. return DynamTSTransfer(*self.args).render(request)
  228. class MPEGTS(FSObject, VideoItem):
  229. def __init__(self, *args, **kwargs):
  230. self.path = path = kwargs['path']
  231. del kwargs['path']
  232. self.tvct = tvct = kwargs['tvct']
  233. del kwargs['tvct']
  234. #log.msg('tvct w/ keys:', tvct, tvct.keys())
  235. kwargs['content'] = MPEGTSResource(path, tvct['PMTpid'],
  236. *sum(mpegts.getaudiovideopids(tvct['PMT']), []))
  237. VideoItem.__init__(self, *args, **kwargs)
  238. FSObject.__init__(self, path)
  239. self.url = '%s/%s' % (self.cd.urlbase, self.id)
  240. self.res = Resource(self.url, 'http-get:*:video/mpeg:*')
  241. def doUpdate(self):
  242. pass
  243. class MultiMPEGTS(FSObject, StorageFolder):
  244. def __init__(self, *args, **kwargs):
  245. path = kwargs['path']
  246. del kwargs['path']
  247. StorageFolder.__init__(self, *args, **kwargs)
  248. FSObject.__init__(self, path)
  249. def genChildren(self):
  250. f = mpegts.TSPStream(_LimitedFile(self.FSpath,
  251. size=2*1024*1024))
  252. self.tvct = mpegts.GetTVCT(f)
  253. #log.msg('MultiMPEGTS genChildren: tvct: %s' % self.tvct)
  254. return _gennameindexes(self.tvct['channels'])
  255. def createObject(self, i, arg):
  256. if arg['prog_num'] == 0:
  257. log.msg('bogus:', arg)
  258. return None, None, (), {}
  259. #log.msg('real tvct:', arg, toindex.keys(), self.tvct)
  260. return MPEGTS, i, (), { 'path': self.FSpath, 'tvct': arg }
  261. def sort(self):
  262. return super(MultiMPEGTS, self).sort(lambda x, y:
  263. cmp((x.tvct['major'], x.tvct['minor']),
  264. (y.tvct['major'], y.tvct['minor'])))
  265. def findtsstream(fp, pktsz=188):
  266. d = fp.read(200*pktsz)
  267. i = 5
  268. pos = 0
  269. while i and pos < len(d) and pos != -1:
  270. if d[pos] == 'G':
  271. i -= 1
  272. pos += pktsz
  273. else:
  274. i = 5
  275. pos = d.find('G', pos + 1)
  276. if i or pos == -1:
  277. return False
  278. return True
  279. def detectmpegts(path, fobj):
  280. if not findtsstream(fobj):
  281. return None, None
  282. f = mpegts.TSPStream(_LimitedFile(path, size= 2*1024*1024))
  283. tvct = mpegts.GetTVCT(f)
  284. if len(tvct['channels']) == 1:
  285. #return None, None
  286. # We might reenable this once we have pid filtering working
  287. # fast enough.
  288. return MPEGTS, { 'path': path, 'tvct': tvct['channels'][0] }
  289. elif len(tvct['channels']) > 1:
  290. #log.msg('MultiMPEGTS: path: %s' % path)
  291. return MultiMPEGTS, { 'path': path }
  292. return None, None
  293. registerklassfun(detectmpegts)