Skip to content

Commit

Permalink
Refactored webrtc and signaling events management (#437)
Browse files Browse the repository at this point in the history
* refactored webrtc and signaling events management, ensure track event is forwarded after the active event

* removed console log

* removed isDrm property in subscribe request and changed to production environment

* addressed review comments and fixed unit test

* addressed review comment

* updated drm sdk
  • Loading branch information
vincentsong authored Sep 17, 2024
1 parent 97f6be6 commit bb8de6f
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 95 deletions.
12 changes: 10 additions & 2 deletions packages/millicast-sdk/src/Director.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,20 @@ let apiEndpoint = defaultApiEndpoint
* You will need your own Publishing token and Stream name, please refer to [Managing Your Tokens](https://docs.dolby.io/streaming-apis/docs/managing-your-tokens).
*/

/**
* @typedef {Object} DRMObject
* @property {String} fairPlayCertUrl - URL of the FairPlay certificate server.
* @property {String} fairPlayUrl - URL of the FairPlay license server.
* @property {String} widevineUrl - URL of the Widevine license server.
*/

/**
* @typedef {Object} MillicastDirectorResponse
* @global
* @property {Array<String>} urls - WebSocket available URLs.
* @property {String} jwt - Access token for signaling initialization.
* @property {Array<RTCIceServer>} iceServers - Object which represents a list of Ice servers.
* @property {DRMObject} [drmObject] - DRM proxy server information.
*/

/**
Expand Down Expand Up @@ -176,12 +184,12 @@ const Director = {
* await millicastView.connect(options)
*/

getSubscriber: async (options, streamAccountId = null, subscriberToken = null, isDRMEnabled = false) => {
getSubscriber: async (options, streamAccountId = null, subscriberToken = null) => {
const optionsParsed = getSubscriberOptions(options, streamAccountId, subscriberToken)
Diagnostics.initAccountId(optionsParsed.streamAccountId)
logger.info(`Getting subscriber connection data for stream name: ${optionsParsed.streamName} and account id: ${optionsParsed.streamAccountId}`)

const payload = { streamAccountId: optionsParsed.streamAccountId, streamName: optionsParsed.streamName, isDrm: isDRMEnabled }
const payload = { streamAccountId: optionsParsed.streamAccountId, streamName: optionsParsed.streamName }
let headers = { 'Content-Type': 'application/json' }
if (optionsParsed.subscriberToken) {
headers = { ...headers, Authorization: `Bearer ${optionsParsed.subscriberToken}` }
Expand Down
141 changes: 75 additions & 66 deletions packages/millicast-sdk/src/View.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const connectOptions = {
* @constructor
* @param {String} streamName - Deprecated: Millicast existing stream name.
* @param {tokenGeneratorCallback} tokenGenerator - Callback function executed when a new token is needed.
* @param {HTMLMediaElement} [mediaElement=null] - Target HTML media element to mount stream.
* @param {HTMLMediaElement} [mediaElement=null] - Deprecated: Target HTML media element to mount stream.
* @param {Boolean} [autoReconnect=true] - Enable auto reconnect to stream.
*/
export default class View extends BaseWebRTC {
Expand All @@ -53,10 +53,11 @@ export default class View extends BaseWebRTC {
this.tracksMidValues = {}
// mapping media ID of RTCRtcTransceiver to DRM Options
this.drmOptionsMap = null
// cache of events to coordinate re-emitting
this.eventQueue = []
this.isMainStreamActive = false
if (mediaElement) {
this.on(webRTCEvents.track, e => {
mediaElement.srcObject = e.streams[0]
})
logger.warn('The mediaElement property has been deprecated. In a future release, this will be removed. Please do not rely on this value. Instead, do this in either the `track` or the `active` broadcast event.')
}
}

Expand Down Expand Up @@ -98,26 +99,6 @@ export default class View extends BaseWebRTC {
* @example
* import View from '@millicast/sdk'
*
* // Create media element
* const videoElement = document.createElement("video")
*
* //Define callback for generate new token
* const tokenGenerator = () => getYourSubscriberInformation(accountId, streamName)
*
* //Create a new instance
* // Stream name is not necessary in the constructor anymore, could be null | undefined
* const streamName = "Millicast Stream Name where i want to connect"
* const millicastView = new View(streamName, tokenGenerator, videoElement)
*
* //Start connection to broadcast
* try {
* await millicastView.connect()
* } catch (e) {
* console.log('Connection failed, handle error', e)
* }
* @example
* import View from '@millicast/sdk'
*
* //Define callback for generate new token
* const tokenGenerator = () => getYourSubscriberInformation(accountId, streamName)
*
Expand All @@ -143,6 +124,7 @@ export default class View extends BaseWebRTC {
*/
async connect (options = connectOptions) {
this.options = { ...connectOptions, ...options, peerConfig: { ...connectOptions.peerConfig, ...options.peerConfig }, setSDPToPeer: false }
this.eventQueue.length = 0
await this.initConnection({ migrate: false })
}

Expand Down Expand Up @@ -222,6 +204,7 @@ export default class View extends BaseWebRTC {
this.worker = null
this.payloadTypeCodec = {}
this.tracksMidValues = {}
this.eventQueue.length = 0
}

async initConnection (data) {
Expand Down Expand Up @@ -277,7 +260,7 @@ export default class View extends BaseWebRTC {
this.stopReemitingWebRTCPeerInstanceEvents?.()
this.stopReemitingSignalingInstanceEvents?.()
// And start emitting from the new ones
this.stopReemitingWebRTCPeerInstanceEvents = reemit(webRTCPeerInstance, this, Object.values(webRTCEvents))
this.stopReemitingWebRTCPeerInstanceEvents = reemit(webRTCPeerInstance, this, Object.values(webRTCEvents).filter(e => e !== webRTCEvents.track))
this.stopReemitingSignalingInstanceEvents = reemit(signalingInstance, this, [signalingEvents.broadcastEvent])

if (this.options.metadata) {
Expand Down Expand Up @@ -315,46 +298,28 @@ export default class View extends BaseWebRTC {
}
}

webRTCPeerInstance.on('track', (trackEvent) => {
this.tracksMidValues[trackEvent.transceiver?.mid] = trackEvent.track
if (this.isDRMOn) {
const mediaId = trackEvent.transceiver.mid
const drmOptions = this.getDRMConfiguration(mediaId)
try {
rtcDrmOnTrack(trackEvent, drmOptions)
} catch (error) {
logger.error('Failed to apply DRM on media Id:', mediaId, 'error is: ', error)
this.emit('error', new Error('Failed to apply DRM on media Id: ' + mediaId + ' error is: ' + error))
}
if (!this.worker) {
this.worker = new TransformWorker()
}
this.worker.addEventListener('message', (message) => {
if (message.data.event === 'complete') {
// feed the frame to DRM processing worker
rtcDrmFeedFrame(message.data.frame, null, drmOptions)
}
})
webRTCPeerInstance.on(webRTCEvents.track, (trackEvent) => {
if (!this.isMainStreamActive) {
this.eventQueue.push(trackEvent)
return
}
if (this.options.metadata) {
if (supportsRTCRtpScriptTransform) {
// eslint-disable-next-line no-undef
trackEvent.receiver.transform = new RTCRtpScriptTransform(this.worker, {
name: 'receiverTransform',
payloadTypeCodec: { ...this.payloadTypeCodec },
codec: this.options.metadata && 'h264',
mid: trackEvent.transceiver?.mid
})
} else if (supportsInsertableStreams) {
const { readable, writable } = trackEvent.receiver.createEncodedStreams()
this.worker.postMessage({
action: 'insertable-streams-receiver',
payloadTypeCodec: { ...this.payloadTypeCodec },
codec: this.options.metadata && 'h264',
mid: trackEvent.transceiver?.mid,
readable,
writable
}, [readable, writable])
this.onTrackEvent(trackEvent)
})

signalingInstance.on(signalingEvents.broadcastEvent, (event) => {
if (event.data.sourceId === null) {
switch (event.name) {
case 'active':
this.isMainStreamActive = true
while (this.eventQueue.length > 0) {
this.onTrackEvent(this.eventQueue.shift())
}
break
case 'inactive':
this.isMainStreamActive = false
break
default:
break
}
}
})
Expand Down Expand Up @@ -400,6 +365,51 @@ export default class View extends BaseWebRTC {
}
}

onTrackEvent (trackEvent) {
this.tracksMidValues[trackEvent.transceiver?.mid] = trackEvent.track
if (this.isDRMOn) {
const mediaId = trackEvent.transceiver.mid
const drmOptions = this.getDRMConfiguration(mediaId)
try {
rtcDrmOnTrack(trackEvent, drmOptions)
} catch (error) {
logger.error('Failed to apply DRM on media Id:', mediaId, 'error is: ', error)
this.emit('error', new Error('Failed to apply DRM on media Id: ' + mediaId + ' error is: ' + error))
}
if (!this.worker) {
this.worker = new TransformWorker()
}
this.worker.addEventListener('message', (message) => {
if (message.data.event === 'complete') {
// feed the frame to DRM processing worker
rtcDrmFeedFrame(message.data.frame, null, drmOptions)
}
})
}
if (this.options.metadata) {
if (supportsRTCRtpScriptTransform) {
// eslint-disable-next-line no-undef
trackEvent.receiver.transform = new RTCRtpScriptTransform(this.worker, {
name: 'receiverTransform',
payloadTypeCodec: { ...this.payloadTypeCodec },
codec: this.options.metadata && 'h264',
mid: trackEvent.transceiver?.mid
})
} else if (supportsInsertableStreams) {
const { readable, writable } = trackEvent.receiver.createEncodedStreams()
this.worker.postMessage({
action: 'insertable-streams-receiver',
payloadTypeCodec: { ...this.payloadTypeCodec },
codec: this.options.metadata && 'h264',
mid: trackEvent.transceiver?.mid,
readable,
writable
}, [readable, writable])
}
}
this.emit(webRTCEvents.track, trackEvent)
}

getDRMConfiguration (mediaId) {
return this.drmOptionsMap ? this.drmOptionsMap.get(mediaId) : null
}
Expand Down Expand Up @@ -451,8 +461,7 @@ export default class View extends BaseWebRTC {
}
const drmOptions = {
merchant: 'dolby',
// TODO: change to Product when backend is ready
environment: rtcDrmEnvironments.Staging,
environment: rtcDrmEnvironments.Production,
customTransform: this.options.metadata,
videoElement: options.videoElement,
audioElement: options.audioElement,
Expand Down
2 changes: 1 addition & 1 deletion packages/millicast-sdk/src/drm/rtc-drm-transform.js

Large diffs are not rendered by default.

5 changes: 0 additions & 5 deletions packages/millicast-sdk/tests/features/View.feature
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,6 @@ Feature: As a user I want to subscribe to a stream without managing connections
When I subscribe to a stream with a connection path
Then peer connection state is connected

Scenario: Subscribe to stream with media element
Given an instance of View with media element
When I subscribe to a stream with a connection path
Then peer connection state is connected

Scenario: Connect subscriber without connection path
Given I want to subscribe
When I instance a View with a token generator without connection path
Expand Down
21 changes: 0 additions & 21 deletions packages/millicast-sdk/tests/unit/View.steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,27 +67,6 @@ defineFeature(feature, test => {
})
})

test('Subscribe to stream with media element', ({ given, when, then }) => {
let viewer
const videoElement = { srcObject: null }

given('an instance of View with media element', async () => {
viewer = new View('streamName', mockTokenGenerator, videoElement)
})

when('I subscribe to a stream with a connection path', async () => {
await viewer.connect()
viewer.webRTCPeer.peer.emitMockEvent('ontrack', { streams: ['new stream incoming'] })
})

then('peer connection state is connected', async () => {
// PeerConnection's track event is asynchronous now
await new Promise((resolve) => setTimeout(resolve, 100))
expect(viewer.webRTCPeer.getRTCPeerStatus()).toEqual('connected')
expect(videoElement.srcObject).not.toBeNull()
})
})

test('Connect subscriber without connection path', ({ given, when, then }) => {
let viewer
let expectError
Expand Down

0 comments on commit bb8de6f

Please sign in to comment.