PageRenderTime 55ms CodeModel.GetById 29ms RepoModel.GetById 1ms app.codeStats 0ms

/src/streammachine/slave/index.coffee

https://gitlab.com/nuitdeboutlyon-group/nuitdeboutlyon-radio
CoffeeScript | 284 lines | 174 code | 70 blank | 40 comment | 25 complexity | dcc36621a46eb7955c577ea9e7f5420c MD5 | raw file
  1. _ = require "underscore"
  2. Stream = require "./stream"
  3. Server = require "./server"
  4. Alerts = require "../alerts"
  5. IO = require "./slave_io"
  6. SocketSource = require "./socket_source"
  7. URL = require "url"
  8. HTTP = require "http"
  9. tz = require 'timezone'
  10. debug = require("debug")("sm:slave:slave")
  11. module.exports = class Slave extends require("events").EventEmitter
  12. Outputs:
  13. pumper: require "../outputs/pumper"
  14. shoutcast: require "../outputs/shoutcast"
  15. raw: require "../outputs/raw_audio"
  16. live_streaming: require "../outputs/live_streaming"
  17. constructor: (@options,@_worker) ->
  18. @_configured = false
  19. debug "Init for Slave"
  20. @master = null
  21. @streams = {}
  22. @stream_groups = {}
  23. @root_route = null
  24. @connected = false
  25. @_retrying = null
  26. @_shuttingDown = false
  27. # -- Global Stats -- #
  28. # we'll track these at the stream level and then bubble them up
  29. @_totalConnections = 0
  30. @_totalKBytesSent = 0
  31. # -- Set up logging -- #
  32. @log = @options.logger
  33. # -- create an alerts object -- #
  34. @alerts = new Alerts logger:@log.child(module:"alerts")
  35. # -- Make sure we have the proper slave config options -- #
  36. if @options.slave?.master
  37. debug "Connecting IO to master"
  38. @io = new IO @, @log.child(module:"slave_io"), @options.slave
  39. @io.on "connected", =>
  40. debug "IO is connected"
  41. @alerts.update "slave_disconnected", @io.id, false
  42. @log.proxyToMaster(@io)
  43. @io.on "disconnected", =>
  44. debug "IO is disconnected"
  45. @alerts.update "slave_disconnected", @io.id, true
  46. @log.proxyToMaster()
  47. @once "streams", =>
  48. debug "Streams event received"
  49. @_configured = true
  50. # -- set up our stream server -- #
  51. @server = new Server core:@, logger:@log.child(subcomponent:"server"), config:@options
  52. #----------
  53. once_configured: (cb) ->
  54. if @_configured
  55. cb()
  56. else
  57. @once "streams", => cb()
  58. once_rewinds_loaded: (cb) ->
  59. @once_configured =>
  60. @log.debug "Looking for sources to load in #{ Object.keys(@streams).length } streams."
  61. aFunc = _.after Object.keys(@streams).length, =>
  62. @log.debug "All sources are loaded."
  63. cb()
  64. # watch for each configured stream to have its rewind buffer loaded.
  65. obj._once_source_loaded aFunc for k,obj of @streams
  66. #----------
  67. _shutdown: (cb) ->
  68. if !@_worker
  69. cb "Don't have _worker to trigger shutdown on."
  70. return false
  71. if @_shuttingDown
  72. # already shutting down...
  73. cb "Shutdown already in progress."
  74. return false
  75. @_shuttingDown = true
  76. # A shutdown involves a) stopping listening for new connections and
  77. # b) transferring our listeners to a different slave
  78. # tell our server to stop listening
  79. @server.close()
  80. # tell our worker process to transfer out our listeners
  81. @_worker.shutdown cb
  82. #----------
  83. configureStreams: (options) ->
  84. debug "In configureStreams"
  85. @log.debug "In slave configureStreams with ", options:options
  86. # are any of our current streams missing from the new options? if so,
  87. # disconnect them
  88. for k,obj of @streams
  89. if !options?[k]
  90. debug "configureStreams: Disconnecting stream #{k}"
  91. @log.info "configureStreams: Calling disconnect on #{k}"
  92. obj.disconnect()
  93. delete @streams[k]
  94. # run through the streams we've been passed, initializing sources and
  95. # creating rewind buffers
  96. debug "configureStreams: New options start"
  97. for key,opts of options
  98. debug "configureStreams: Configuring #{key}"
  99. if @streams[key]
  100. # existing stream... pass it updated configuration
  101. @log.debug "Passing updated config to stream: #{key}", opts:opts
  102. @streams[key].configure opts
  103. else
  104. @log.debug "Starting up stream: #{key}", opts:opts
  105. # HLS support?
  106. opts.hls = true if @options.hls
  107. # FIXME: Eventually it would make sense to allow a per-stream
  108. # value here
  109. opts.tz = tz(require "timezone/zones")(@options.timezone||"UTC")
  110. stream = @streams[key] = new Stream @, key, @log.child(stream:key), opts
  111. if @io
  112. source = @socketSource stream
  113. stream.useSource source
  114. # part of a stream group?
  115. if g = @streams[key].opts.group
  116. # do we have a matching group?
  117. sg = ( @stream_groups[ g ] ||= new Stream.StreamGroup g, @log.child stream_group:g )
  118. sg.addStream @streams[key]
  119. #@streams[key].hls_segmenter?.syncToGroup sg
  120. # should this stream accept requests to /?
  121. if opts.root_route
  122. @root_route = key
  123. # emit a streams event for any components under us that might
  124. # need to know
  125. debug "Done with configureStreams"
  126. @emit "streams", @streams
  127. #----------
  128. # Get a status snapshot by looping through each stream to return buffer
  129. # stats. Lets master know that we're still listening and current
  130. _streamStatus: (cb) ->
  131. status = {}
  132. totalKBytes = 0
  133. totalConnections = 0
  134. for key,s of @streams
  135. status[ key ] = s.status()
  136. totalKBytes += status[key].kbytes_sent
  137. totalConnections += status[key].connections
  138. cb null, _.extend status, _stats:{ kbytes_sent:totalKBytes, connections:totalConnections }
  139. #----------
  140. socketSource: (stream) ->
  141. new SocketSource @, stream
  142. #----------
  143. ejectListeners: (lFunc,cb) ->
  144. # transfer listeners, one at a time
  145. @log.info "Preparing to eject listeners from slave."
  146. @_enqueued = []
  147. # -- prep our listeners -- #
  148. for k,s of @streams
  149. @log.info "Preparing #{ Object.keys(s._lmeta).length } listeners for #{ s.key }"
  150. @_enqueued.push [s,obj] for id,obj of s._lmeta
  151. # -- short-circuit if there are no listeners -- #
  152. return cb?() if @_enqueued.length == 0
  153. # -- now send them one-by-one -- #
  154. sFunc = =>
  155. sl = @_enqueued.shift()
  156. if !sl
  157. @log.info "All listeners have been ejected."
  158. return cb null
  159. [stream,l] = sl
  160. # wrap the listener send in an error domain to try as
  161. # hard as we can to get it all there
  162. d = require("domain").create()
  163. d.on "error", (err) =>
  164. console.error "Handoff error: #{err}"
  165. @log.error "Eject listener for #{l.id} hit error: #{err}"
  166. d.exit()
  167. sFunc()
  168. d.run =>
  169. l.obj.prepForHandoff (skipHandoff=false) =>
  170. # some listeners don't need handoffs
  171. if skipHandoff
  172. return sFunc()
  173. socket = l.obj.socket
  174. lopts =
  175. key: [stream.key,l.id].join("::"),
  176. stream: stream.key
  177. id: l.id
  178. startTime: l.startTime
  179. client: l.obj.client
  180. # there's a chance that the connection could end
  181. # after we recorded the id but before we get here.
  182. # don't send in that case...
  183. if socket && !socket.destroyed
  184. lFunc lopts, socket, (err) =>
  185. if err
  186. @log.error "Failed to send listener #{lopts.id}: #{err}"
  187. # move on to the next one...
  188. sFunc()
  189. else
  190. @log.info "Listener #{lopts.id} perished in the queue. Moving on."
  191. sFunc()
  192. sFunc()
  193. #----------
  194. landListener: (obj,socket,cb) ->
  195. # check and make sure they haven't disconnected mid-flight
  196. if socket && !socket.destroyed
  197. # create an output and attach it to the proper stream
  198. output = new @Outputs[ obj.client.output ] @streams[ obj.stream ],
  199. socket: socket
  200. client: obj.client
  201. startTime: new Date(obj.startTime)
  202. cb null
  203. else
  204. cb "Listener disconnected in-flight"
  205. #----------