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.

277 lines
7.0 KiB

  1. #!/usr/bin/env python
  2. # Copyright 2009 John-Mark Gurney <jmg@funkthat.com>
  3. '''Audio Source'''
  4. __version__ = '$Change$'
  5. # $Id$
  6. import ossaudiodev
  7. import os.path
  8. from DIDLLite import Container, MusicGenre, AudioItem, Resource, ResourceList
  9. from FSStorage import registerklassfun
  10. from twisted.internet.abstract import FileDescriptor
  11. from twisted.internet import fdesc
  12. from twisted.python import log, threadable, failure
  13. from twisted.web import error, http, resource, server
  14. from zope.interface import implements
  15. mttobytes = {
  16. 'audio/l8': 1,
  17. 'audio/l16': 2,
  18. }
  19. def bytespersecmt(mt):
  20. tmp = [ x.strip() for x in mt.split(';') ]
  21. try:
  22. r = mttobytes[tmp[0].lower()]
  23. except KeyError:
  24. raise ValueError('invalid audio type: %s' % repr(tmp[0]))
  25. v = set(('rate', 'channels'))
  26. for i in tmp[1:]:
  27. arg, value = [ x.strip() for x in i.split('=', 1) ]
  28. if arg in v:
  29. v.remove(arg)
  30. r *= int(value)
  31. else:
  32. raise ValueError('invalid audio parameter %s in %s' %
  33. (repr(arg), repr(mt)))
  34. return r
  35. class AudioPlayer(FileDescriptor):
  36. def __init__(self, consumer, dev, mode, params):
  37. self._dev = ossaudiodev.open(dev, mode)
  38. # Set some sub-functions
  39. self.fileno = self._dev.fileno
  40. self.setparameters = self._dev.setparameters
  41. res = self.setparameters(*params)
  42. self._dev.nonblock()
  43. FileDescriptor.__init__(self)
  44. self.connected = True
  45. self.attached = consumer
  46. self.writefun = self.attached.write
  47. consumer.registerProducer(self, True)
  48. self.dobuffer = False
  49. self.buffer = None
  50. self.startReading()
  51. # Drop our useless write connection
  52. self._writeDisconnected = True
  53. def writeSomeData(self, data):
  54. print('wsd:', len(data))
  55. return fdesc.writeToFD(self.fileno(), data)
  56. def doRead(self):
  57. return fdesc.readFromFD(self.fileno(), self.writefun)
  58. def connectionLost(self, reason):
  59. FileDescriptor.connectionLost(self, reason)
  60. print('AP, connectionLost')
  61. self.fileno = lambda: -1
  62. self.setparameters = None
  63. if self._dev is not None:
  64. self._dev.close()
  65. self._dev = None
  66. self.attached = None
  67. def stopProducing(self):
  68. print('AP, sp')
  69. self.writefun = lambda x: None
  70. FileDescriptor.stopProducing(self)
  71. def pauseProducing(self):
  72. if not self.dobuffer:
  73. self.buffer = []
  74. self.dobuffer = True
  75. self.writefun = self.buffer.append
  76. #FileDescriptor.pauseProducing(self)
  77. def resumeProducing(self):
  78. if self.dobuffer:
  79. self.attached.write(''.join(self.buffer))
  80. self.dobuffer = False
  81. self.buffer = None
  82. self.writefun = self.attached.write
  83. #FileDescriptor.resumeProducing(self)
  84. def __repr__(self):
  85. return '<AudioPlayer: fileno: %d, connected: %s, self.disconnecting: %s, _writeDisconnected: %s>' % (self.fileno(), self.connected, self.disconnecting, self._writeDisconnected)
  86. class AudioResource(resource.Resource):
  87. isLeaf = True
  88. mtformat = {
  89. ossaudiodev.AFMT_S16_BE: 'audio/L16',
  90. ossaudiodev.AFMT_U8: 'audio/L8',
  91. }
  92. producerFactory = AudioPlayer
  93. def __init__(self, dev, default):
  94. resource.Resource.__init__(self)
  95. self.dev = dev
  96. self.default = default
  97. @staticmethod
  98. def getfmt(fmt):
  99. return getattr(ossaudiodev, 'AFMT_%s' % fmt)
  100. def getmimetype(self, *args):
  101. if len(args) == 0:
  102. args = self.default
  103. elif len(args) != 3:
  104. raise TypeError('getmimetype() takes exactly 0 or 3 aruments (%d given)' % len(args))
  105. fmt, nchan, rate = args
  106. origfmt = fmt
  107. try:
  108. fmt = getattr(ossaudiodev, 'AFMT_%s' % fmt)
  109. nchan = int(nchan)
  110. rate = int(rate)
  111. except AttributeError:
  112. raise ValueError('Invalid audio format: %s' % repr(origfmt))
  113. try:
  114. mt = self.mtformat[fmt]
  115. except KeyError:
  116. raise KeyError('No mime-type for audio format: %s.' %
  117. repr(origfmt))
  118. return '%s;rate=%d;channels=%d' % (mt, rate, nchan)
  119. def render(self, request):
  120. default = self.default
  121. if request.postpath:
  122. default = request.postpath
  123. fmt, nchan, rate = default
  124. nchan = int(nchan)
  125. rate = int(rate)
  126. try:
  127. request.setHeader('content-type',
  128. self.getmimetype(fmt, nchan, rate))
  129. except (ValueError, AttributeError, KeyError) as x:
  130. return error.ErrorPage(http.UNSUPPORTED_MEDIA_TYPE,
  131. 'Unsupported Media Type', str(x)).render(request)
  132. #except AttributeError:
  133. # return error.NoResource('Unknown audio format.').render(request)
  134. #except ValueError:
  135. # return error.NoResource('Unknown channels (%s) or rate (%s).' % (`nchan`, `rate`)).render(request)
  136. #except KeyError:
  137. # return error.ErrorPage(http.UNSUPPORTED_MEDIA_TYPE,
  138. # 'Unsupported Media Type',
  139. # 'No mime-type for audio format: %s.' %
  140. # `fmt`).render(request)
  141. if request.method == 'HEAD':
  142. return ''
  143. self.producerFactory(request, self.dev, 'r',
  144. (self.getfmt(fmt), nchan, rate, True))
  145. # and make sure the connection doesn't get closed
  146. return server.NOT_DONE_YET
  147. synchronized = [ 'render' ]
  148. threadable.synchronize(AudioResource)
  149. class ossaudiodev_fmts:
  150. pass
  151. for i in (k for k in dir(ossaudiodev) if k[:5] == 'AFMT_' and \
  152. isinstance(getattr(ossaudiodev, k), int)):
  153. setattr(ossaudiodev_fmts, i, getattr(ossaudiodev, i))
  154. class AudioSource(AudioItem):
  155. def __init__(self, *args, **kwargs):
  156. file = kwargs.pop('file')
  157. fargs = eval(open(file).read().strip(), { '__builtins__': {}, })
  158. # 'ossaudiodev': ossaudiodev_fmts })
  159. self.dev = fargs.pop('dev')
  160. default = fargs.pop('default')
  161. kwargs['content'] = AudioResource(self.dev, default)
  162. AudioItem.__init__(self, *args, **kwargs)
  163. if False:
  164. self.bitrate = bitrate
  165. self.url = '%s/%s' % (self.cd.urlbase, self.id)
  166. self.res = ResourceList()
  167. self.res.append(Resource(self.url, 'http-get:*:%s:*' %
  168. kwargs['content'].getmimetype()))
  169. # XXX - add other non-default formats
  170. def getfmtstrings(f):
  171. r = []
  172. for i in ( x for x in dir(ossaudiodev) if x[:5] == 'AFMT_' ):
  173. val = getattr(ossaudiodev, i)
  174. if val & f:
  175. f &= ~val
  176. r.append(i)
  177. while f:
  178. print(f, f & -f)
  179. r.append(f & -f)
  180. f ^= f & -f
  181. return r
  182. def detectaudiosource(origpath, fobj):
  183. path = os.path.basename(origpath)
  184. ext = os.path.splitext(path)[1]
  185. if ext == '.asrc':
  186. return AudioSource, { 'file': origpath }
  187. return None, None
  188. registerklassfun(detectaudiosource)
  189. from zope.interface import implements
  190. from twisted.internet.interfaces import IConsumer
  191. class FileConsumer:
  192. implements(IConsumer)
  193. def __init__(self, fp):
  194. self.fp = open(fp, 'w')
  195. self.producer = None
  196. def registerProducer(self, producer, streaming):
  197. if self.producer is not None:
  198. raise RuntimeError('already have a producer')
  199. self.streaming = streaming
  200. self.producer = producer
  201. producer.resumeProducing()
  202. def unregisterProducer(self):
  203. if self.producer is None:
  204. raise RuntimeError('none registered')
  205. self.producer = None
  206. def write(self, data):
  207. self.fp.write(data)
  208. if __name__ == '__main__':
  209. if False:
  210. i = ossaudiodev.open('/dev/dsp2', 'r')
  211. print(getfmtstrings(i.getfmts()))
  212. i.setparameters(ossaudiodev.AFMT_S16_BE, 2, 44100, True)
  213. print(repr(i.read(16)))
  214. else:
  215. aplr = AudioPlayer('/dev/dsp2', 'r',
  216. (ossaudiodev.AFMT_S16_BE, 2, 44100, True))
  217. file = FileConsumer('test.output')
  218. file.registerProducer(aplr, True)
  219. from twisted.internet import reactor
  220. reactor.run()