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.

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