def testRenderHTTPTokenAuthorized(self): streamer = FakeStreamer(mediumClass=FakeTokenMedium) httpauth = HTTPAuthentication(streamer) resource = MultiFdSinkStreamingResource(streamer, httpauth) # override issuer httpauth.setBouncerName('fakebouncer') httpauth.setDomain('FakeDomain') streamer.caps = True self.failUnless(resource.isReady()) d = defer.Deferred() def rightToken(_): request = FakeRequest(ip='127.0.0.1', args={'token': 'LETMEIN'}) return self.deferAssertAuthorized(httpauth, request) def rightTokenTwice(_): request = FakeRequest(ip='127.0.0.1', args={'token': ['LETMEIN', 'LETMEIN']}) return self.deferAssertAuthorized(httpauth, request) d.addCallback(rightToken) d.addCallback(rightTokenTwice) d.callback(None) return d
def _setRequestHeaders(self, request): MultiFdSinkStreamingResource._setRequestHeaders(self, request) if request.serveIcy: additionalHeaders = self.streamer.get_icy_headers() for header in additionalHeaders: request.setHeader(header, additionalHeaders[header])
def testRenderHTTPAllowDefault(self): streamer = FakeStreamer(mediumClass=FakeAuthFailingMedium) httpauth = HTTPAuthentication(streamer) resource = MultiFdSinkStreamingResource(streamer, httpauth) httpauth.setBouncerName('fakebouncer') streamer.caps = True self.failUnless(resource.isReady()) d = defer.Deferred() def wrongToken(_): request = FakeRequest(user='******') return self.deferAssertUnauthorized(httpauth, request) def errorDisallowDefault(_, error): streamer.medium.failure = error request = FakeRequest(user='******') return self.deferAssertInternalServerError(httpauth, request) def errorAllowDefault(_, error): streamer.medium.failure = error httpauth.setAllowDefault(True) request = FakeRequest(user='******') return self.deferAssertAuthorized(httpauth, request) d.addCallback(wrongToken) d.addCallback(errorDisallowDefault, errors.NotConnectedError()) d.addCallback(errorDisallowDefault, errors.UnknownComponentError()) d.addCallback(errorAllowDefault, errors.NotConnectedError()) d.addCallback(errorAllowDefault, errors.UnknownComponentError()) d.callback(None) return d
def testRenderNotReady(self): streamer = FakeStreamer() httpauth = HTTPAuthentication(streamer) resource = MultiFdSinkStreamingResource(streamer, httpauth) self.failIf(resource.isReady()) status = resource.render(FakeRequest(ip='')) self.assertEquals(status, server.NOT_DONE_YET)
def testRenderNew(self): streamer = FakeStreamer() httpauth = HTTPAuthentication(streamer) resource = MultiFdSinkStreamingResource(streamer, httpauth) streamer.caps = True streamer.mime = 'application/x-ogg' request = FakeRequest(ip='127.0.0.1') data = resource.render(request) self.failUnless(server.NOT_DONE_YET)
def testRenderHTTPAuthUnauthorized(self): streamer = FakeStreamer() httpauth = HTTPAuthentication(streamer) resource = MultiFdSinkStreamingResource(streamer, httpauth) httpauth.setBouncerName('fakebouncer') httpauth.setDomain('FakeDomain') streamer.caps = True self.failUnless(resource.isReady()) request = FakeRequest(ip='127.0.0.1', user='******') return self.deferAssertUnauthorized(httpauth, request)
def testRenderTopStreamer(self): # a streamer that is at /a root = HTTPRoot() site = server.Site(resource=root) streamer = FakeStreamer() httpauth = HTTPAuthentication(streamer) resource = MultiFdSinkStreamingResource(streamer, httpauth) root.putChild('a', resource) # a request for / should give 404 log.debug('unittest', 'requesting /, should 404') request = FakeRequest(ip='') r = site.getResourceFor(request) output = r.render(request) self.assertEquals(request.response, http.NOT_FOUND) # a request for /a should work log.debug('unittest', 'requesting /a, should work') request = FakeRequest(ip='', postpath=['a']) r = site.getResourceFor(request) self.assertEquals(r, resource) output = r.render(request) self.assertEquals(output, server.NOT_DONE_YET) # a request for /a/b should give 404 log.debug('unittest', 'requesting /a/b, should 404') request = FakeRequest(ip='', postpath=['a', 'b']) r = site.getResourceFor(request) output = r.render(request) self.assertEquals(request.response, http.NOT_FOUND)
def testRenderReachedMaxClients(self): streamer = FakeStreamer() httpauth = HTTPAuthentication(streamer) resource = MultiFdSinkStreamingResource(streamer, httpauth) self.failIf(resource.isReady()) streamer.caps = True self.failUnless(resource.isReady()) #assert resource.maxAllowedClients() == 974 resource._requests = ' ' * (resource.maxclients + 1) self.failUnless(resource.reachedServerLimits()) request = FakeRequest(ip='127.0.0.1') data = resource.render(request) error_code = http.SERVICE_UNAVAILABLE self.assertEquals(request.headers.get('content-type', ''), 'text/html') self.assertEquals(request.headers.get('server', ''), HTTP_VERSION) self.assertEquals(request.response, error_code) expected = ERROR_TEMPLATE % { 'code': error_code, 'error': http.RESPONSES[error_code] } self.assertEquals(data, expected)
def testRenderReachedMaxClients(self): streamer = FakeStreamer() httpauth = HTTPAuthentication(streamer) resource = MultiFdSinkStreamingResource(streamer, httpauth) self.failIf(resource.isReady()) streamer.caps = True self.failUnless(resource.isReady()) #assert resource.maxAllowedClients() == 974 resource._requests = ' ' * (resource.maxclients + 1) self.failUnless(resource.reachedServerLimits()) request = FakeRequest(ip='127.0.0.1') data = resource.render(request) error_code = http.SERVICE_UNAVAILABLE self.assertEquals(request.headers.get('content-type', ''), 'text/html') self.assertEquals(request.headers.get('server', ''), HTTP_VERSION) self.assertEquals(request.response, error_code) expected = ERROR_TEMPLATE % { 'code': error_code, 'error': http.RESPONSES[error_code]} self.assertEquals(data, expected)
def _render(self, request): headerValue = request.getHeader('Icy-MetaData') request.serveIcy = (headerValue == '1') return MultiFdSinkStreamingResource._render(self, request)
def configure_auth_and_resource(self): self.httpauth = http.HTTPAuthentication(self) self.resource = MultiFdSinkStreamingResource(self, self.httpauth)
class MultifdSinkStreamer(streamer.Streamer, Stats): pipe_template = 'multifdsink name=sink ' + \ 'sync=false ' + \ 'recover-policy=3' defaultSyncMethod = 0 def setup_burst_mode(self, sink): if self.burst_on_connect: if self.burst_time and \ gstreamer.element_factory_has_property('multifdsink', 'units-max'): self.debug("Configuring burst mode for %f second burst", self.burst_time) # Set a burst for configurable minimum time, plus extra to # start from a keyframe if needed. sink.set_property('sync-method', 4) # burst-keyframe sink.set_property('burst-unit', 2) # time sink.set_property('burst-value', long(self.burst_time * gst.SECOND)) # We also want to ensure that we have sufficient data available # to satisfy this burst; and an appropriate maximum, all # specified in units of time. sink.set_property('time-min', long((self.burst_time + 5) * gst.SECOND)) sink.set_property('unit-type', 2) # time sink.set_property('units-soft-max', long((self.burst_time + 8) * gst.SECOND)) sink.set_property('units-max', long((self.burst_time + 10) * gst.SECOND)) elif self.burst_size: self.debug("Configuring burst mode for %d kB burst", self.burst_size) # If we have a burst-size set, use modern # needs-recent-multifdsink behaviour to have complex bursting. # In this mode, we burst a configurable minimum, plus extra # so we start from a keyframe (or less if we don't have a # keyframe available) sink.set_property('sync-method', 'burst-keyframe') sink.set_property('burst-unit', 'bytes') sink.set_property('burst-value', self.burst_size * 1024) # To use burst-on-connect, we need to ensure that multifdsink # has a minimum amount of data available - assume 512 kB beyond # the burst amount so that we should have a keyframe available sink.set_property('bytes-min', (self.burst_size + 512) * 1024) # And then we need a maximum still further above that - the # exact value doesn't matter too much, but we want it # reasonably small to limit memory usage. multifdsink doesn't # give us much control here, we can only specify the max # values in buffers. We assume each buffer is close enough # to 4kB - true for asf and ogg, at least sink.set_property('buffers-soft-max', (self.burst_size + 1024) / 4) sink.set_property('buffers-max', (self.burst_size + 2048) / 4) else: # Old behaviour; simple burst-from-latest-keyframe self.debug("simple burst-on-connect, setting sync-method 2") sink.set_property('sync-method', 2) sink.set_property('buffers-soft-max', 250) sink.set_property('buffers-max', 500) else: self.debug("no burst-on-connect, setting sync-method 0") sink.set_property('sync-method', self.defaultSyncMethod) sink.set_property('buffers-soft-max', 250) sink.set_property('buffers-max', 500) def parseExtraProperties(self, properties): # check how to set client sync mode self.burst_on_connect = properties.get('burst-on-connect', False) self.burst_size = properties.get('burst-size', 0) self.burst_time = properties.get('burst-time', 0.0) def _configure_sink(self, sink): self.setup_burst_mode(sink) if gstreamer.element_factory_has_property('multifdsink', 'resend-streamheader'): sink.set_property('resend-streamheader', False) else: self.debug("resend-streamheader property not available, " "resending streamheader when it changes in the caps") sink.set_property('timeout', self.timeout) sink.connect('deep-notify::caps', self._notify_caps_cb) # these are made threadsafe using idle_add in the handler sink.connect('client-added', self._client_added_handler) # We now require a sufficiently recent multifdsink anyway that we can # use the new client-fd-removed signal sink.connect('client-fd-removed', self._client_fd_removed_cb) sink.connect('client-removed', self._client_removed_cb) sink.caps = None def check_properties(self, props, addMessage): streamer.Streamer.check_properties(self, props, addMessage) # tcp is where multifdsink is version = gstreamer.get_plugin_version('tcp') if version < (0, 10, 9, 1): m = messages.Error( T_( N_("Version %s of the '%s' GStreamer plug-in is too old.\n" ), ".".join(map(str, version)), 'multifdsink')) m.add( T_(N_("Please upgrade '%s' to version %s."), 'gst-plugins-base', '0.10.10')) addMessage(m) def configure_auth_and_resource(self): self.httpauth = http.HTTPAuthentication(self) self.resource = MultiFdSinkStreamingResource(self, self.httpauth) def configure_pipeline(self, pipeline, properties): sink = self.get_element('sink') Stats.__init__(self, sink) streamer.Streamer.configure_pipeline(self, pipeline, properties) self.parseExtraProperties(properties) self._configure_sink(sink) def _get_root(self): root = HTTPRoot() # TwistedWeb wants the child path to not include the leading / mount = self.mountPoint[1:] root.putChild(mount, self.resource) return root def __repr__(self): return '<MultifdSinkStreamer (%s)>' % self.name def getMaxClients(self): return self.resource.maxclients def get_mime(self): if self.sinks[0].caps: return self.sinks[0].caps[0].get_name() def get_content_type(self): mime = self.get_mime() if mime == 'multipart/x-mixed-replace': mime += ";boundary=ThisRandomString" return mime def add_client(self, fd, request): sink = self.get_element('sink') sink.emit('add', fd) def remove_client(self, fd): sink = self.get_element('sink') sink.emit('remove', fd) def remove_all_clients(self): """Remove all the clients. Returns a deferred fired once all clients have been removed. """ if self.resource: # can be None if we never went happy self.debug("Asking for all clients to be removed") return self.resource.removeAllClients() def _client_added_handler(self, sink, fd): self.log('[fd %5d] client_added_handler', fd) Stats.clientAdded(self) self.update_ui_state() def _client_removed_handler(self, sink, fd, reason, stats): self.log('[fd %5d] client_removed_handler, reason %s', fd, reason) if reason.value_name == 'GST_CLIENT_STATUS_ERROR': self.warning('[fd %5d] Client removed because of write error' % fd) self.resource.clientRemoved(sink, fd, reason, stats) Stats.clientRemoved(self) self.update_ui_state() ### START OF THREAD-AWARE CODE (called from non-reactor threads) def _notify_caps_cb(self, element, pad, param): # We store caps in sink objects as # each sink might (and will) serve different content-type caps = pad.get_negotiated_caps() if caps == None: return caps_str = gstreamer.caps_repr(caps) self.debug('Got caps: %s' % caps_str) if not element.caps == None: self.warning('Already had caps: %s, replacing' % caps_str) self.debug('Storing caps: %s' % caps_str) element.caps = caps reactor.callFromThread(self.update_ui_state) # We now use both client-removed and client-fd-removed. We call get-stats # from the first callback ('client-removed'), but don't actually start # removing the client until we get 'client-fd-removed'. This ensures that # there's no window in which multifdsink still knows about the fd, # but we've actually closed it, so we no longer get spurious duplicates. # this can be called from both application and streaming thread ! def _client_removed_cb(self, sink, fd, reason): stats = sink.emit('get-stats', fd) self._pending_removals[fd] = (stats, reason) # this can be called from both application and streaming thread ! def _client_fd_removed_cb(self, sink, fd): (stats, reason) = self._pending_removals.pop(fd) reactor.callFromThread(self._client_removed_handler, sink, fd, reason, stats)
class MultifdSinkStreamer(streamer.Streamer, Stats): pipe_template = "multifdsink name=sink " + "sync=false " + "recover-policy=3" defaultSyncMethod = 0 def setup_burst_mode(self, sink): if self.burst_on_connect: if self.burst_time and gstreamer.element_factory_has_property("multifdsink", "units-max"): self.debug("Configuring burst mode for %f second burst", self.burst_time) # Set a burst for configurable minimum time, plus extra to # start from a keyframe if needed. sink.set_property("sync-method", 4) # burst-keyframe sink.set_property("burst-unit", 2) # time sink.set_property("burst-value", long(self.burst_time * Gst.SECOND)) # We also want to ensure that we have sufficient data available # to satisfy this burst; and an appropriate maximum, all # specified in units of time. sink.set_property("time-min", long((self.burst_time + 5) * Gst.SECOND)) sink.set_property("unit-type", 2) # time sink.set_property("units-soft-max", long((self.burst_time + 8) * Gst.SECOND)) sink.set_property("units-max", long((self.burst_time + 10) * Gst.SECOND)) elif self.burst_size: self.debug("Configuring burst mode for %d kB burst", self.burst_size) # If we have a burst-size set, use modern # needs-recent-multifdsink behaviour to have complex bursting. # In this mode, we burst a configurable minimum, plus extra # so we start from a keyframe (or less if we don't have a # keyframe available) sink.set_property("sync-method", "burst-keyframe") sink.set_property("burst-unit", "bytes") sink.set_property("burst-value", self.burst_size * 1024) # To use burst-on-connect, we need to ensure that multifdsink # has a minimum amount of data available - assume 512 kB beyond # the burst amount so that we should have a keyframe available sink.set_property("bytes-min", (self.burst_size + 512) * 1024) # And then we need a maximum still further above that - the # exact value doesn't matter too much, but we want it # reasonably small to limit memory usage. multifdsink doesn't # give us much control here, we can only specify the max # values in buffers. We assume each buffer is close enough # to 4kB - true for asf and ogg, at least sink.set_property("buffers-soft-max", (self.burst_size + 1024) / 4) sink.set_property("buffers-max", (self.burst_size + 2048) / 4) else: # Old behaviour; simple burst-from-latest-keyframe self.debug("simple burst-on-connect, setting sync-method 2") sink.set_property("sync-method", 2) sink.set_property("buffers-soft-max", 250) sink.set_property("buffers-max", 500) else: self.debug("no burst-on-connect, setting sync-method 0") sink.set_property("sync-method", self.defaultSyncMethod) sink.set_property("buffers-soft-max", 250) sink.set_property("buffers-max", 500) def parseExtraProperties(self, properties): # check how to set client sync mode self.burst_on_connect = properties.get("burst-on-connect", False) self.burst_size = properties.get("burst-size", 0) self.burst_time = properties.get("burst-time", 0.0) def _configure_sink(self, sink): self.setup_burst_mode(sink) if gstreamer.element_factory_has_property("multifdsink", "resend-streamheader"): sink.set_property("resend-streamheader", False) else: self.debug( "resend-streamheader property not available, " "resending streamheader when it changes in the caps" ) sink.set_property("timeout", self.timeout) sink.connect("deep-notify::caps", self._notify_caps_cb) # these are made threadsafe using idle_add in the handler sink.connect("client-added", self._client_added_handler) # We now require a sufficiently recent multifdsink anyway that we can # use the new client-fd-removed signal sink.connect("client-fd-removed", self._client_fd_removed_cb) sink.connect("client-removed", self._client_removed_cb) sink.caps = None def check_properties(self, props, addMessage): streamer.Streamer.check_properties(self, props, addMessage) # tcp is where multifdsink is version = gstreamer.get_plugin_version("tcp") if version < (0, 10, 9, 1): m = messages.Error( T_( N_("Version %s of the '%s' GStreamer plug-in is too old.\n"), ".".join(map(str, version)), "multifdsink", ) ) m.add(T_(N_("Please upgrade '%s' to version %s."), "gst-plugins-base", "0.10.10")) addMessage(m) def configure_auth_and_resource(self): self.httpauth = http.HTTPAuthentication(self) self.resource = MultiFdSinkStreamingResource(self, self.httpauth) def configure_pipeline(self, pipeline, properties): sink = self.get_element("sink") Stats.__init__(self, sink) streamer.Streamer.configure_pipeline(self, pipeline, properties) self.parseExtraProperties(properties) self._configure_sink(sink) def _get_root(self): root = HTTPRoot() # TwistedWeb wants the child path to not include the leading / mount = self.mountPoint[1:] root.putChild(mount, self.resource) return root def __repr__(self): return "<MultifdSinkStreamer (%s)>" % self.name def getMaxClients(self): return self.resource.maxclients def get_mime(self): if self.sinks[0].caps: return self.sinks[0].caps[0].get_name() def get_content_type(self): mime = self.get_mime() if mime == "multipart/x-mixed-replace": mime += ";boundary=ThisRandomString" return mime def add_client(self, fd, request): sink = self.get_element("sink") sink.emit("add", fd) def remove_client(self, fd): sink = self.get_element("sink") sink.emit("remove", fd) def remove_all_clients(self): """Remove all the clients. Returns a deferred fired once all clients have been removed. """ if self.resource: # can be None if we never went happy self.debug("Asking for all clients to be removed") return self.resource.removeAllClients() def _client_added_handler(self, sink, fd): self.log("[fd %5d] client_added_handler", fd) Stats.clientAdded(self) self.update_ui_state() def _client_removed_handler(self, sink, fd, reason, stats): self.log("[fd %5d] client_removed_handler, reason %s", fd, reason) if reason.value_name == "GST_CLIENT_STATUS_ERROR": self.warning("[fd %5d] Client removed because of write error" % fd) self.resource.clientRemoved(sink, fd, reason, stats) Stats.clientRemoved(self) self.update_ui_state() ### START OF THREAD-AWARE CODE (called from non-reactor threads) def _notify_caps_cb(self, element, pad, param): # We store caps in sink objects as # each sink might (and will) serve different content-type caps = pad.get_negotiated_caps() if caps == None: return caps_str = gstreamer.caps_repr(caps) self.debug("Got caps: %s" % caps_str) if not element.caps == None: self.warning("Already had caps: %s, replacing" % caps_str) self.debug("Storing caps: %s" % caps_str) element.caps = caps reactor.callFromThread(self.update_ui_state) # We now use both client-removed and client-fd-removed. We call get-stats # from the first callback ('client-removed'), but don't actually start # removing the client until we get 'client-fd-removed'. This ensures that # there's no window in which multifdsink still knows about the fd, # but we've actually closed it, so we no longer get spurious duplicates. # this can be called from both application and streaming thread ! def _client_removed_cb(self, sink, fd, reason): stats = sink.emit("get-stats", fd) self._pending_removals[fd] = (stats, reason) # this can be called from both application and streaming thread ! def _client_fd_removed_cb(self, sink, fd): (stats, reason) = self._pending_removals.pop(fd) reactor.callFromThread(self._client_removed_handler, sink, fd, reason, stats)