UNPKG

@wowzamediasystems/sdk-rts

Version:

SDK for building a realtime broadcaster using the Wowza platform.

229 lines (209 loc) 10.1 kB
import jwtDecode from 'jwt-decode' import reemit from 're-emitter' import { atob } from 'Base64' import Logger from './Logger' import BaseWebRTC from './utils/BaseWebRTC' import Signaling, { signalingEvents } from './Signaling' import { VideoCodec } from './utils/Codecs' import PeerConnection, { webRTCEvents } from './PeerConnection' import FetchError from './utils/FetchError' const logger = Logger.get('Publish') const connectOptions = { mediaStream: null, bandwidth: 0, disableVideo: false, disableAudio: false, codec: VideoCodec.H264, simulcast: false, scalabilityMode: null, peerConfig: {} } /** * @class Publish * @extends BaseWebRTC * @classdesc Manages connection with a secure WebSocket path to signal the Wowza server * and establishes a WebRTC connection to broadcast a MediaStream. * * Before you can broadcast, you will need: * * - [MediaStream](https://developer.mozilla.org/en-US/docs/Web/API/Media_Streams_API) which has at most one audio track and at most one video track. This will be used for stream the contained tracks. * * - A connection path that you can get from {@link Director} module or from your own implementation. * @constructor * @param {String} streamName - Wowza existing stream name. * @param {tokenGeneratorCallback} tokenGenerator - Callback function executed when a new token is needed. * @param {Boolean} [autoReconnect=true] - Enable auto reconnect to stream. */ export default class Publish extends BaseWebRTC { constructor (streamName, tokenGenerator, autoReconnect = true) { super(streamName, tokenGenerator, logger, autoReconnect) } /** * Starts broadcast to an existing stream name. * * In the example, `getYourMediaStream` and `getYourPublisherConnection` is your own implementation. * @param {Object} options - General broadcast options. * @param {String} options.sourceId - Source unique id. Only avialable if stream is multisource. * @param {Boolean} [options.stereo = false] - True to modify SDP for support stereo. Otherwise False. * @param {Boolean} [options.dtx = false] - True to modify SDP for supporting dtx in opus. Otherwise False. * @param {Boolean} [options.absCaptureTime = false] - True to modify SDP for supporting absolute capture time header extension. Otherwise False. * @param {Boolean} [options.dependencyDescriptor = false] - True to modify SDP for supporting aom dependency descriptor header extension. Otherwise False. * @param {MediaStream|Array<MediaStreamTrack>} options.mediaStream - MediaStream to offer in a stream. This object must have * 1 audio track and 1 video track, or at least one of them. Alternative you can provide both tracks in an array. * @param {Number} [options.bandwidth = 0] - Broadcast bandwidth. 0 for unlimited. * @param {Boolean} [options.disableVideo = false] - Disable the opportunity to send video stream. * @param {Boolean} [options.disableAudio = false] - Disable the opportunity to send audio stream. * @param {VideoCodec} [options.codec = 'h264'] - Codec for publish stream. * @param {Boolean} [options.simulcast = false] - Enable simulcast. **Only available in Google Chrome and with H.264 or VP8 video codecs.** * @param {String} [options.scalabilityMode = null] - Selected scalability mode. You can get the available capabilities using <a href="PeerConnection#.getCapabilities">PeerConnection.getCapabilities</a> method. * **Only available in Google Chrome.** * @param {RTCConfiguration} [options.peerConfig = null] - Options to configure the new RTCPeerConnection. * @param {Boolean} [options.record = false ] - Enable stream recording. If record is not provided, use default Token configuration. **Only available in Tokens with recording enabled.** * @param {Array<String>} [options.events = null] - Specify which events will be delivered by the server (any of "active" | "inactive" | "viewercount").* * @param {Number} [options.priority = null] - When multiple ingest streams are provided by the customer, add the ability to specify a priority between all ingest streams. Decimal integer between the range [-2^31, +2^31 - 1]. For more information, visit [our documentation](https://docs.dolby.io/streaming-apis/docs/backup-publishing). * @returns {Promise<void>} Promise object which resolves when the broadcast started successfully. * @fires PeerConnection#connectionStateChange * @fires Signaling#broadcastEvent * @example await publish.connect(options) * @example * import Publish from '@wowzamediasystems/sdk-rts' * * //Define callback for generate new token * const tokenGenerator = () => getYourPublisherConnection(token, streamName) * * //Create a new instance * const streamName = "My Wowza Stream Name" * const wowzaPublish = new Publish(streamName, tokenGenerator) * * //Get MediaStream * const mediaStream = getYourMediaStream() * * //Options * const broadcastOptions = { * mediaStream: mediaStream * } * * //Start broadcast * try { * await wowzaPublish.connect(broadcastOptions) * } catch (e) { * console.log('Connection failed, handle error', e) * } */ async connect (options = connectOptions) { this.options = { ...connectOptions, ...options, setSDPToPeer: false } await this.initConnection({ migrate: false }) } async reconnect (data) { this.options.mediaStream = this.webRTCPeer?.getTracks() ?? this.options.mediaStream super.reconnect(data) } async replaceConnection () { logger.info('Migrating current connection') this.options.mediaStream = this.webRTCPeer?.getTracks() ?? this.options.mediaStream await this.initConnection({ migrate: true }) } /** * Initialize recording in an active stream and change the current record option. */ async record () { if (this.recordingAvailable) { this.options.record = true await this.signaling?.cmd('record') logger.info('Broadcaster start recording') } else { logger.error('Record not available') } } /** * Finalize recording in an active stream and change the current record option. */ async unrecord () { if (this.recordingAvailable) { this.options.record = false await this.signaling?.cmd('unrecord') logger.info('Broadcaster stop recording') } else { logger.error('Unrecord not available') } } async initConnection (data) { logger.debug('Broadcast option values: ', this.options) this.stopReconnection = false let promises if (!this.options.mediaStream) { logger.error('Error while broadcasting. MediaStream required') throw new Error('MediaStream required') } if (!data.migrate && this.isActive()) { logger.warn('Broadcast currently working') throw new Error('Broadcast currently working') } let publisherData try { publisherData = await this.tokenGenerator() // Set the iceServers from the publish data into the peerConfig this.options.peerConfig.iceServers = publisherData?.iceServers } catch (error) { logger.error('Error generating token.') if (error instanceof FetchError) { if (error.status === 401 || !this.autoReconnect) { // should not reconnect this.stopReconnection = true } else { // should reconnect with exponential back off if autoReconnect is true this.reconnect() } } throw error } if (!publisherData) { logger.error('Error while broadcasting. Publisher data required') throw new Error('Publisher data required') } this.recordingAvailable = jwtDecode(publisherData.jwt)[atob('bWlsbGljYXN0')].record if (this.options.record && !this.recordingAvailable) { logger.error('Error while broadcasting. Record option detected but recording is not available') throw new Error('Record option detected but recording is not available') } const signalingInstance = new Signaling({ streamName: this.streamName, url: `${publisherData.urls[0]}?token=${publisherData.jwt}` }) const webRTCPeerInstance = data.migrate ? new PeerConnection() : this.webRTCPeer await webRTCPeerInstance.createRTCPeer(this.options.peerConfig) // Stop emiting events from the previous instances this.stopReemitingWebRTCPeerInstanceEvents?.() this.stopReemitingSignalingInstanceEvents?.() // And start emitting from the new ones this.stopReemitingWebRTCPeerInstanceEvents = reemit(webRTCPeerInstance, this, [webRTCEvents.connectionStateChange]) this.stopReemitingSignalingInstanceEvents = reemit(signalingInstance, this, [signalingEvents.broadcastEvent]) const getLocalSDPPromise = webRTCPeerInstance.getRTCLocalSDP(this.options) const signalingConnectPromise = signalingInstance.connect() promises = await Promise.all([getLocalSDPPromise, signalingConnectPromise]) const localSdp = promises[0] let oldSignaling = this.signaling this.signaling = signalingInstance const publishPromise = this.signaling.publish(localSdp, this.options) const setLocalDescriptionPromise = webRTCPeerInstance.peer.setLocalDescription(webRTCPeerInstance.sessionDescription) promises = await Promise.all([publishPromise, setLocalDescriptionPromise]) let remoteSdp = promises[0] if (!this.options.disableVideo && this.options.bandwidth > 0) { remoteSdp = webRTCPeerInstance.updateBandwidthRestriction(remoteSdp, this.options.bandwidth) } await webRTCPeerInstance.setRTCRemoteSDP(remoteSdp) logger.info('Broadcasting to streamName: ', this.streamName) let oldWebRTCPeer = this.webRTCPeer this.webRTCPeer = webRTCPeerInstance this.setReconnect() if (data.migrate) { this.webRTCPeer.on(webRTCEvents.connectionStateChange, (state) => { if (['connected', 'disconnected', 'failed', 'closed'].includes(state)) { oldSignaling?.close?.() oldWebRTCPeer?.closeRTCPeer?.() oldSignaling = oldWebRTCPeer = null } }) } } };