Signaling.js

  1. import EventEmitter from 'events'
  2. import TransactionManager from 'transaction-manager'
  3. import Logger from './Logger'
  4. import SdpParser from './utils/SdpParser'
  5. import { VideoCodec } from './utils/Codecs'
  6. import PeerConnection from './PeerConnection'
  7. const logger = Logger.get('Signaling')
  8. export const signalingEvents = {
  9. connectionSuccess: 'wsConnectionSuccess',
  10. connectionError: 'wsConnectionError',
  11. connectionClose: 'wsConnectionClose',
  12. broadcastEvent: 'broadcastEvent'
  13. }
  14. /**
  15. * @typedef {Object} LayerInfo
  16. * @property {String} encodingId - rid value of the simulcast encoding of the track (default: automatic selection)
  17. * @property {Number} spatialLayerId - The spatial layer id to send to the outgoing stream (default: max layer available)
  18. * @property {Number} temporalLayerId - The temporaral layer id to send to the outgoing stream (default: max layer available)
  19. * @property {Number} maxSpatialLayerId - Max spatial layer id (default: unlimited)
  20. * @property {Number} maxTemporalLayerId - Max temporal layer id (default: unlimited)
  21. */
  22. /**
  23. * @typedef {Object} SignalingSubscribeOptions
  24. * @property {String} vad - Enable VAD multiplexing for secondary sources.
  25. * @property {String} pinnedSourceId - Id of the main source that will be received by the default MediaStream.
  26. * @property {Array<String>} excludedSourceIds - Do not receive media from the these source ids.
  27. * @property {Array<String>} events - Override which events will be delivered by the server ("active" | "inactive" | "vad" | "layers").
  28. * @property {LayerInfo} layer - Select the simulcast encoding layer and svc layers for the main video track, leave empty for automatic layer selection based on bandwidth estimation.
  29. */
  30. /**
  31. * @typedef {Object} SignalingPublishOptions
  32. * @property {VideoCodec} [codec="h264"] - Codec for publish stream.
  33. * @property {Boolean} [record] - Enable stream recording. If record is not provided, use default Token configuration. **Only available in Tokens with recording enabled.**
  34. * @property {String} [sourceId] - Source unique id. **Only available in Tokens with multisource enabled.***
  35. * @property {Array<String>} events - Override which events will be delivered by the server ("active" | "inactive").
  36. */
  37. /**
  38. * @class Signaling
  39. * @extends EventEmitter
  40. * @classdesc Starts WebSocket connection and manages the messages between peers.
  41. * @example const wowzaSignaling = new Signaling(options)
  42. * @constructor
  43. * @param {Object} options - General signaling options.
  44. * @param {String} options.streamName - Wowza stream name to get subscribed.
  45. * @param {String} options.url - WebSocket URL to signal Wowza server and establish a WebRTC connection.
  46. */
  47. export default class Signaling extends EventEmitter {
  48. constructor (options = {
  49. streamName: null,
  50. url: 'ws://localhost:8080/'
  51. }
  52. ) {
  53. super()
  54. this.streamName = options.streamName
  55. this.wsUrl = options.url
  56. this.webSocket = null
  57. this.transactionManager = null
  58. this.serverId = null
  59. this.clusterId = null
  60. }
  61. /**
  62. * Starts a WebSocket connection with signaling server.
  63. * @example const response = await wowzaSignaling.connect()
  64. * @returns {Promise<WebSocket>} Promise object which represents the [WebSocket object]{@link https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API} of the establshed connection.
  65. * @fires Signaling#wsConnectionSuccess
  66. * @fires Signaling#wsConnectionError
  67. * @fires Signaling#wsConnectionClose
  68. * @fires Signaling#broadcastEvent
  69. */
  70. async connect () {
  71. logger.info('Connecting to Signaling Server')
  72. if (this.transactionManager && this.webSocket?.readyState === WebSocket.OPEN) {
  73. logger.info('Connected to server: ', this.webSocket.url)
  74. logger.debug('WebSocket value: ', {
  75. url: this.webSocket.url,
  76. protocol: this.webSocket.protocol,
  77. readyState: this.webSocket.readyState,
  78. binaryType: this.webSocket.binaryType,
  79. extensions: this.webSocket.extensions
  80. })
  81. /**
  82. * WebSocket connection was successfully established with signaling server.
  83. *
  84. * @event Signaling#wsConnectionSuccess
  85. * @type {Object}
  86. * @property {WebSocket} ws - WebSocket object which represents active connection.
  87. * @property {TransactionManager} tm - [TransactionManager](https://github.com/medooze/transaction-manager) object that simplify WebSocket commands.
  88. */
  89. this.emit(signalingEvents.connectionSuccess, { ws: this.webSocket, tm: this.transactionManager })
  90. return this.webSocket
  91. }
  92. return new Promise((resolve, reject) => {
  93. this.webSocket = new WebSocket(this.wsUrl)
  94. this.transactionManager = new TransactionManager(this.webSocket)
  95. this.webSocket.onopen = () => {
  96. logger.info('WebSocket opened')
  97. this.transactionManager.on('event', (evt) => {
  98. /**
  99. * Passthrough of available Wowza broadcast events.
  100. *
  101. * Active - Fires when the live stream is, or has started broadcasting.
  102. *
  103. * Inactive - Fires when the stream has stopped broadcasting, but is still available.
  104. *
  105. * Stopped - Fires when the stream has stopped for a given reason.
  106. *
  107. * Vad - Fires when using multiplexed tracks for audio.
  108. *
  109. * Layers - Fires when there is an update of the state of the layers in a stream (when broadcasting with simulcast).
  110. *
  111. * Migrate - Fires when the server is having problems, is shutting down or when viewers need to move for load balancing purposes.
  112. *
  113. * Viewercount - Fires when the viewer count changes.
  114. *
  115. * More information here: {@link https://docs.dolby.io/streaming-apis/docs/web#broadcast-events}
  116. *
  117. * @event Signaling#broadcastEvent
  118. * @type {Object}
  119. * @property {String} type - In this case the type of this message is "event".
  120. * @property {("active" | "inactive" | "stopped" | "vad" | "layers" | "migrate" | "viewercount")} name - Event name.
  121. * @property {Object} data - Custom event data.
  122. */
  123. this.emit(signalingEvents.broadcastEvent, evt)
  124. })
  125. logger.info('Connected to server: ', this.webSocket.url)
  126. logger.debug('WebSocket value: ', {
  127. url: this.webSocket.url,
  128. protocol: this.webSocket.protocol,
  129. readyState: this.webSocket.readyState,
  130. binaryType: this.webSocket.binaryType,
  131. extensions: this.webSocket.extensions
  132. })
  133. this.emit(signalingEvents.connectionSuccess, { ws: this.webSocket, tm: this.transactionManager })
  134. resolve(this.webSocket)
  135. }
  136. this.webSocket.onerror = () => {
  137. logger.error('WebSocket not connected: ', this.webSocket.url)
  138. /**
  139. * WebSocket connection failed with signaling server.
  140. * Returns url of WebSocket
  141. *
  142. * @event Signaling#wsConnectionError
  143. * @type {String}
  144. */
  145. this.emit(signalingEvents.connectionError, this.webSocket.url)
  146. reject(this.webSocket.url)
  147. }
  148. this.webSocket.onclose = () => {
  149. this.webSocket = null
  150. this.transactionManager = null
  151. logger.info('Connection closed with Signaling Server.')
  152. /**
  153. * WebSocket connection with signaling server was successfully closed.
  154. *
  155. * @event Signaling#wsConnectionClose
  156. */
  157. this.emit(signalingEvents.connectionClose)
  158. }
  159. })
  160. }
  161. /**
  162. * Close WebSocket connection with Wowza server.
  163. * @example wowzaSignaling.close()
  164. */
  165. close () {
  166. logger.info('Closing connection with Signaling Server.')
  167. this.webSocket?.close()
  168. }
  169. /**
  170. * Establish WebRTC connection with Wowza Server as Subscriber role.
  171. * @param {String} sdp - The SDP information created by your offer.
  172. * @param {SignalingSubscribeOptions} options - Signaling Subscribe Options.
  173. * @example const response = await wowzaSignaling.subscribe(sdp)
  174. * @return {Promise<String>} Promise object which represents the SDP command response.
  175. */
  176. async subscribe (sdp, options, pinnedSourceId = null, excludedSourceIds = null) {
  177. logger.info('Starting subscription to streamName: ', this.streamName)
  178. logger.debug('Subcription local description: ', sdp)
  179. const optionsParsed = getSubscribeOptions(options, pinnedSourceId, excludedSourceIds)
  180. // Signaling server only recognizes 'AV1' and not 'AV1X'
  181. sdp = SdpParser.adaptCodecName(sdp, 'AV1X', VideoCodec.AV1)
  182. const data = { sdp, streamId: this.streamName, pinnedSourceId: optionsParsed.pinnedSourceId, excludedSourceIds: optionsParsed.excludedSourceIds }
  183. if (optionsParsed.vad) { data.vad = true }
  184. if (Array.isArray(optionsParsed.events)) { data.events = optionsParsed.events }
  185. if (optionsParsed.forcePlayoutDelay) { data.forcePlayoutDelay = optionsParsed.forcePlayoutDelay }
  186. try {
  187. await this.connect()
  188. logger.info('Sending view command')
  189. const result = await this.transactionManager.cmd('view', data)
  190. // Check if browser supports AV1X
  191. const AV1X = RTCRtpReceiver.getCapabilities?.('video')?.codecs?.find?.(codec => codec.mimeType === 'video/AV1X')
  192. // Signaling server returns 'AV1'. If browser supports AV1X, we change it to AV1X
  193. result.sdp = AV1X ? SdpParser.adaptCodecName(result.sdp, VideoCodec.AV1, 'AV1X') : result.sdp
  194. logger.info('Command sent, subscriberId: ', result.subscriberId)
  195. logger.debug('Command result: ', result)
  196. this.serverId = result.subscriberId
  197. this.clusterId = result.clusterId
  198. return result.sdp
  199. } catch (e) {
  200. logger.error('Error sending view command, error: ', e)
  201. throw e
  202. }
  203. }
  204. /**
  205. * Establish WebRTC connection with Wowza Server as Publisher role.
  206. * @param {String} sdp - The SDP information created by your offer.
  207. * @param {SignalingPublishOptions} options - Signaling Publish Options.
  208. * @example const response = await wowzaSignaling.publish(sdp, {codec: 'h264'})
  209. * @return {Promise<String>} Promise object which represents the SDP command response.
  210. */
  211. async publish (sdp, options, record = null, sourceId = null) {
  212. const optionsParsed = getPublishOptions(options, record, sourceId)
  213. logger.info(`Starting publishing to streamName: ${this.streamName}, codec: ${optionsParsed.codec}`)
  214. logger.debug('Publishing local description: ', sdp)
  215. const supportedVideoCodecs = PeerConnection.getCapabilities?.('video')?.codecs?.map(cdc => cdc.codec) ?? []
  216. const videoCodecs = Object.values(VideoCodec)
  217. if (videoCodecs.indexOf(optionsParsed.codec) === -1) {
  218. logger.error(`Invalid codec ${optionsParsed.codec}. Possible values are: `, videoCodecs)
  219. throw new Error(`Invalid codec ${optionsParsed.codec}. Possible values are: ${videoCodecs}`)
  220. }
  221. if (supportedVideoCodecs.length > 0 && supportedVideoCodecs.indexOf(optionsParsed.codec) === -1) {
  222. logger.error(`Unsupported codec ${optionsParsed.codec}. Possible values are: `, supportedVideoCodecs)
  223. throw new Error(`Unsupported codec ${optionsParsed.codec}. Possible values are: ${supportedVideoCodecs}`)
  224. }
  225. // Signaling server only recognizes 'AV1' and not 'AV1X'
  226. if (optionsParsed.codec === VideoCodec.AV1) {
  227. sdp = SdpParser.adaptCodecName(sdp, 'AV1X', VideoCodec.AV1)
  228. }
  229. const data = {
  230. name: this.streamName,
  231. sdp,
  232. codec: optionsParsed.codec,
  233. sourceId: optionsParsed.sourceId
  234. }
  235. if (optionsParsed.priority) {
  236. if (
  237. Number.isInteger(optionsParsed.priority) &&
  238. optionsParsed.priority >= -2147483648 &&
  239. optionsParsed.priority <= 2147483647
  240. ) {
  241. data.priority = optionsParsed.priority
  242. } else {
  243. throw new Error('Invalid value for priority option. It should be a decimal integer between the range [-2^31, +2^31 - 1]')
  244. }
  245. }
  246. if (optionsParsed.record !== null) {
  247. data.record = optionsParsed.record
  248. }
  249. if (Array.isArray(optionsParsed.events)) {
  250. data.events = optionsParsed.events
  251. }
  252. try {
  253. await this.connect()
  254. logger.info('Sending publish command')
  255. const result = await this.transactionManager.cmd('publish', data)
  256. if (optionsParsed.codec === VideoCodec.AV1) {
  257. // If browser supports AV1X, we change from AV1 to AV1X
  258. const AV1X = RTCRtpSender.getCapabilities?.('video')?.codecs?.find?.(codec => codec.mimeType === 'video/AV1X')
  259. result.sdp = AV1X ? SdpParser.adaptCodecName(result.sdp, VideoCodec.AV1, 'AV1X') : result.sdp
  260. }
  261. logger.info('Command sent, publisherId: ', result.publisherId)
  262. logger.debug('Command result: ', result)
  263. this.serverId = result.publisherId
  264. this.clusterId = result.clusterId
  265. return result.sdp
  266. } catch (e) {
  267. logger.error('Error sending publish command, error: ', e)
  268. throw e
  269. }
  270. }
  271. /**
  272. * Send command to the server.
  273. * @param {String} cmd - Command name.
  274. * @param {Object} [data] - Command parameters.
  275. * @return {Promise<Object>} Promise object which represents the command response.
  276. */
  277. async cmd (cmd, data) {
  278. logger.info(`Sending cmd: ${cmd}`)
  279. return this.transactionManager.cmd(cmd, data)
  280. }
  281. }
  282. const getSubscribeOptions = (options, legacyPinnedSourceId, legacyExcludedSourceIds) => {
  283. let parsedOptions = (typeof options === 'object') ? options : {}
  284. if (Object.keys(parsedOptions).length === 0) {
  285. parsedOptions = {
  286. vad: options,
  287. pinnedSourceId: legacyPinnedSourceId,
  288. excludedSourceIds: legacyExcludedSourceIds
  289. }
  290. }
  291. return parsedOptions
  292. }
  293. const getPublishOptions = (options, legacyRecord, legacySourceId) => {
  294. let parsedOptions = (typeof options === 'object') ? options : {}
  295. if (Object.keys(parsedOptions).length === 0) {
  296. const defaultCodec = VideoCodec.H264
  297. parsedOptions = {
  298. codec: options ?? defaultCodec,
  299. record: legacyRecord,
  300. sourceId: legacySourceId
  301. }
  302. }
  303. return parsedOptions
  304. }
  305. JAVASCRIPT
    Copied!