Skip to content

Commit e97e949

Browse files
authored
Merge pull request #45 from millicast/develop
Release v0.0.1-beta.6
2 parents dc10975 + e45b586 commit e97e949

29 files changed

+940
-43
lines changed

packages/millicast-sdk-js/src/MillicastPublish.js

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import reemit from 're-emitter'
33
import MillicastLogger from './MillicastLogger'
44
import MillicastSignaling, { MillicastVideoCodec } from './MillicastSignaling'
55
import MillicastWebRTC, { webRTCEvents } from './MillicastWebRTC.js'
6-
import SdpParser from './utils/SdpParser'
76

87
const logger = MillicastLogger.get('MillicastPublish')
98

@@ -46,7 +45,9 @@ export default class MillicastPublish extends EventEmitter {
4645
* @param {Boolean} [options.disableVideo = false] - Disable the opportunity to send video stream.
4746
* @param {Boolean} [options.disableAudio = false] - Disable the opportunity to send audio stream.
4847
* @param {MillicastVideoCodec} options.codec - Codec for publish stream.
49-
* @param {Boolean} options.simulcast - Enable simulcast.
48+
* @param {Boolean} options.simulcast - Enable simulcast. **Only available in Google Chrome and with H.264 or VP8 video codecs.**
49+
* @param {String} options.scalabilityMode - Selected scalability mode. You can get the available capabilities using <a href="MillicastWebRTC#.getCapabilities">MillicastWebRTC.getCapabilities</a> method.
50+
* **Only available in Google Chrome.**
5051
* @returns {Promise<void>} Promise object which resolves when the broadcast started successfully.
5152
* @fires MillicastWebRTC#connectionStateChange
5253
* @example await millicastPublish.broadcast(options)
@@ -85,7 +86,8 @@ export default class MillicastPublish extends EventEmitter {
8586
disableVideo: false,
8687
disableAudio: false,
8788
codec: MillicastVideoCodec.H264,
88-
simulcast: false
89+
simulcast: false,
90+
scalabilityMode: null
8991
}
9092
) {
9193
logger.debug('Broadcast option values: ', options)
@@ -114,11 +116,9 @@ export default class MillicastPublish extends EventEmitter {
114116
offerToReceiveVideo: !options.disableVideo,
115117
offerToReceiveAudio: !options.disableAudio
116118
}
117-
const localSdp = await this.webRTCPeer.getRTCLocalSDP({ mediaStream: options.mediaStream, simulcast: options.simulcast, codec: options.codec })
119+
const localSdp = await this.webRTCPeer.getRTCLocalSDP({ mediaStream: options.mediaStream, simulcast: options.simulcast, codec: options.codec, scalabilityMode: options.scalabilityMode })
118120
let remoteSdp = await this.millicastSignaling.publish(localSdp, options.codec)
119-
if (remoteSdp?.indexOf('\na=extmap-allow-mixed') !== -1) {
120-
remoteSdp = SdpParser.removeSdpLine(remoteSdp, 'a=extmap-allow-mixed')
121-
}
121+
122122
if (!options.disableVideo && options.bandwidth > 0) {
123123
remoteSdp = this.webRTCPeer.updateBandwidthRestriction(remoteSdp, options.bandwidth)
124124
}
@@ -136,6 +136,7 @@ export default class MillicastPublish extends EventEmitter {
136136
logger.info('Stopping broadcast')
137137
this.webRTCPeer.closeRTCPeer()
138138
this.millicastSignaling?.close()
139+
this.millicastSignaling = null
139140
}
140141

141142
/**

packages/millicast-sdk-js/src/MillicastSignaling.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,11 @@ export default class MillicastSignaling extends EventEmitter {
237237
result.sdp = SdpParser.adaptCodecName(result.sdp, MillicastVideoCodec.AV1, 'AV1X')
238238
}
239239

240+
// remove a=extmap-allow-mixed for Chrome < M71
241+
if (result.sdp?.indexOf('\na=extmap-allow-mixed') !== -1) {
242+
result.sdp = SdpParser.removeSdpLine(result.sdp, 'a=extmap-allow-mixed')
243+
}
244+
240245
logger.info('Command sent, publisherId: ', result.publisherId)
241246
logger.debug('Command result: ', result)
242247
return result.sdp

packages/millicast-sdk-js/src/MillicastView.js

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,8 @@ export default class MillicastView extends EventEmitter {
8383
throw new Error('Subscriber data required')
8484
}
8585
if (this.isActive()) {
86-
logger.warn('Viewer currently subscriber')
87-
throw new Error('Viewer currently subscriber')
86+
logger.warn('Viewer currently subscribed')
87+
throw new Error('Viewer currently subscribed')
8888
}
8989

9090
this.millicastSignaling = new MillicastSignaling({
@@ -102,14 +102,10 @@ export default class MillicastView extends EventEmitter {
102102
const localSdp = await this.webRTCPeer.getRTCLocalSDP({ stereo: true })
103103

104104
const sdpSubscriber = await this.millicastSignaling.subscribe(localSdp)
105-
if (sdpSubscriber) {
106-
reemit(this.millicastSignaling, this, [signalingEvents.broadcastEvent])
107-
await this.webRTCPeer.setRTCRemoteSDP(sdpSubscriber)
108-
logger.info('Connected to streamName: ', this.streamName)
109-
} else {
110-
logger.error('Failed to connect to publisher: ', sdpSubscriber)
111-
throw new Error('Failed to connect to publisher: ', sdpSubscriber)
112-
}
105+
reemit(this.millicastSignaling, this, [signalingEvents.broadcastEvent])
106+
107+
await this.webRTCPeer.setRTCRemoteSDP(sdpSubscriber)
108+
logger.info('Connected to streamName: ', this.streamName)
113109
}
114110

115111
/**
@@ -121,6 +117,7 @@ export default class MillicastView extends EventEmitter {
121117
logger.info('Stopping connection')
122118
this.webRTCPeer.closeRTCPeer()
123119
this.millicastSignaling?.close()
120+
this.millicastSignaling = null
124121
}
125122

126123
/**

packages/millicast-sdk-js/src/MillicastWebRTC.js

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import axios from 'axios'
22
import EventEmitter from 'events'
33
import SdpParser from './utils/SdpParser'
4+
import UserAgent from './utils/UserAgent'
45
import MillicastLogger from './MillicastLogger'
56
import { MillicastVideoCodec, MillicastAudioCodec } from './MillicastSignaling'
67

@@ -133,15 +134,18 @@ export default class MillicastWebRTC extends EventEmitter {
133134
* @param {Boolean} options.stereo - True to modify SDP for support stereo. Otherwise False.
134135
* @param {MediaStream|Array<MediaStreamTrack>} options.mediaStream - MediaStream to offer in a stream. This object must have
135136
* 1 audio track and 1 video track, or at least one of them. Alternative you can provide both tracks in an array.
136-
* @param {'h264'|'vp8'|'vp9'|'av1'} options.codec - Selected codec for support simulcast.
137-
* @param {Boolean} options.simulcast - True to modify SDP for support simulcast.
137+
* @param {MillicastVideoCodec} options.codec - Selected codec for support simulcast.
138+
* @param {Boolean} options.simulcast - True to modify SDP for support simulcast. **Only available in Google Chrome and with H.264 or VP8 video codecs.**
139+
* @param {String} options.scalabilityMode - Selected scalability mode. You can get the available capabilities using <a href="MillicastWebRTC#.getCapabilities">MillicastWebRTC.getCapabilities</a> method.
140+
* **Only available in Google Chrome.**
138141
* @returns {Promise<String>} Promise object which represents the SDP information of the created offer.
139142
*/
140143
async getRTCLocalSDP (options = {
141144
stereo: false,
142145
mediaStream: null,
143146
codec: 'h264',
144-
simulcast: false
147+
simulcast: false,
148+
scalabilityMode: null
145149
}) {
146150
logger.info('Getting RTC Local SDP')
147151
logger.debug('Stereo value: ', options.stereo)
@@ -151,7 +155,20 @@ export default class MillicastWebRTC extends EventEmitter {
151155
if (mediaStream) {
152156
logger.info('Adding mediaStream tracks to RTCPeerConnection')
153157
for (const track of mediaStream.getTracks()) {
154-
this.peer.addTrack(track, mediaStream)
158+
if (track.kind === 'video' && options.scalabilityMode && new UserAgent(window.navigator.userAgent).isChrome()) {
159+
logger.debug(`Video track with scalability mode: ${options.scalabilityMode}, adding as transceiver.`)
160+
this.peer.addTransceiver(track, {
161+
streams: [mediaStream],
162+
sendEncodings: [
163+
{ scalabilityMode: options.scalabilityMode }
164+
]
165+
})
166+
} else {
167+
if (track.kind === 'video' && options.scalabilityMode) {
168+
logger.warn('SVC is only supported in Google Chrome')
169+
}
170+
this.peer.addTrack(track, mediaStream)
171+
}
155172
logger.info(`Track '${track.label}' added: `, `id: ${track.id}`, `kind: ${track.kind}`)
156173
}
157174
}
@@ -162,6 +179,7 @@ export default class MillicastWebRTC extends EventEmitter {
162179
logger.debug('Peer offer response: ', response.sdp)
163180

164181
this.sessionDescription = response
182+
this.sessionDescription.sdp = SdpParser.setMultiopus(this.sessionDescription.sdp)
165183
if (options.simulcast) {
166184
this.sessionDescription.sdp = SdpParser.setSimulcast(this.sessionDescription.sdp, options.codec)
167185
}
@@ -253,13 +271,18 @@ export default class MillicastWebRTC extends EventEmitter {
253271
const browserCapabilites = RTCRtpSender.getCapabilities(kind)
254272

255273
if (browserCapabilites) {
274+
const codecs = {}
256275
let regex = new RegExp(`^video/(${Object.values(MillicastVideoCodec).join('|')})x?$`, 'i')
257276

258277
if (kind === 'audio') {
259278
regex = new RegExp(`^audio/(${Object.values(MillicastAudioCodec).join('|')})$`, 'i')
279+
const browserData = new UserAgent(window.navigator.userAgent)
280+
281+
if (browserData.isChrome()) {
282+
codecs.multiopus = { mimeType: 'audio/multiopus', channels: 6 }
283+
}
260284
}
261285

262-
const codecs = {}
263286
for (const codec of browserCapabilites.codecs) {
264287
const matches = codec.mimeType.match(regex)
265288
if (matches) {

packages/millicast-sdk-js/src/utils/SdpParser.js

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import UAParser from 'ua-parser-js'
21
import SemanticSDP from 'semantic-sdp'
32
import MillicastLogger from '../MillicastLogger'
3+
import UserAgent from './UserAgent'
44

55
const logger = MillicastLogger.get('SdpParser')
66

@@ -12,15 +12,17 @@ const logger = MillicastLogger.get('SdpParser')
1212
export default class SdpParser {
1313
/**
1414
* Parse SDP for support simulcast.
15+
*
16+
* **Only available in Google Chrome.**
1517
* @param {String} sdp - Current SDP.
1618
* @param {String} codec - Codec.
1719
* @returns {String} SDP parsed with simulcast support.
1820
* @example SdpParser.setSimulcast(sdp, 'h264')
1921
*/
2022
static setSimulcast (sdp, codec) {
2123
logger.info('Setting simulcast. Codec: ', codec)
22-
const browserData = new UAParser(window.navigator.userAgent).getBrowser()
23-
if (!browserData.name.match(/Chrome/)) {
24+
const browserData = new UserAgent(window.navigator.userAgent)
25+
if (!browserData.isChrome()) {
2426
logger.warn('Simulcast is only available in Google Chrome browser')
2527
return sdp
2628
}
@@ -105,14 +107,14 @@ export default class SdpParser {
105107
logger.info('Remove bitrate restrictions')
106108
sdp = sdp.replace(/b=AS:.*\r\n/, '').replace(/b=TIAS:.*\r\n/, '')
107109
} else {
108-
const browserData = new UAParser(window.navigator.userAgent).getBrowser()
110+
const browserData = new UserAgent(window.navigator.userAgent)
109111
const offer = SemanticSDP.SDPInfo.parse(sdp)
110112
const videoOffer = offer.getMedia('video')
111113

112114
logger.info('Setting video bitrate')
113115
videoOffer.setBitrate(bitrate)
114116
sdp = offer.toString()
115-
if (sdp.indexOf('b=AS:') > -1 && browserData.name === 'Firefox') {
117+
if (sdp.indexOf('b=AS:') > -1 && browserData.isFirefox()) {
116118
logger.info('Updating SDP for firefox browser')
117119
sdp = sdp.replace('b=AS:', 'b=TIAS:')
118120
logger.debug('SDP updated for firefox: ', sdp)
@@ -155,4 +157,34 @@ export default class SdpParser {
155157

156158
return sdp.replace(regex, newCodecName)
157159
}
160+
161+
/**
162+
* Parse SDP for support multiopus.
163+
*
164+
* **Only available in Google Chrome.**
165+
* @param {String} sdp - Current SDP.
166+
* @returns {String} SDP parsed with multiopus support.
167+
* @example SdpParser.setMultiopus(sdp)
168+
*/
169+
static setMultiopus (sdp) {
170+
const browserData = new UserAgent(window.navigator.userAgent)
171+
if (browserData.isChrome()) {
172+
logger.info('Setting multiopus')
173+
// Find the audio m-line
174+
const res = /m=audio 9 UDP\/TLS\/RTP\/SAVPF (.*)\r\n/.exec(sdp)
175+
// Get audio line
176+
const audio = res[0]
177+
// Get free payload number for multiopus
178+
const pt = Math.max(...res[1].split(' ').map(Number)) + 1
179+
// Add multiopus
180+
const multiopus = audio.replace('\r\n', ' ') + pt + '\r\n' +
181+
'a=rtpmap:' + pt + ' multiopus/48000/6\r\n' +
182+
'a=fmtp:' + pt + ' channel_mapping=0,4,1,2,3,5;coupled_streams=2;minptime=10;num_streams=4;useinbandfec=1\r\n'
183+
// Change sdp
184+
sdp = sdp.replace(audio, multiopus)
185+
logger.info('Multiopus offer created')
186+
logger.debug('SDP parsed for multioups: ', sdp)
187+
}
188+
return sdp
189+
}
158190
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import UAParser from 'ua-parser-js'
2+
3+
export default class UserAgent extends UAParser {
4+
isChrome (excludedOS = ['iOS']) {
5+
const browserData = this.getBrowser()
6+
const osData = this.getOS()
7+
8+
let osAllowed = true
9+
if (excludedOS.length > 0) {
10+
const regex = new RegExp(excludedOS.join('|'), 'i')
11+
osAllowed = !regex.test(osData.name)
12+
}
13+
14+
return browserData.name.match(/Chrome/i) && osAllowed
15+
}
16+
17+
isFirefox () {
18+
const browserData = this.getBrowser()
19+
20+
return browserData.name.match(/Firefox/i)
21+
}
22+
}

packages/millicast-sdk-js/tests/features/GetCapabilities.feature

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,20 @@ Feature: As a user I want to get browser audio/video capabilities so I can choos
2020
When I get video capabilities
2121
Then returns VP9 with all scalability modes available
2222

23-
Scenario: Get audio capabilities
23+
Scenario: Get audio capabilities in Chrome
2424
Given my browser audio capabilities
2525
When I get audio capabilities
26-
Then returns same capabilities as browser
26+
Then returns opus and multiopus codecs
27+
28+
Scenario: Get audio capabilities in iOS Chrome
29+
Given my browser audio capabilities
30+
When I get audio capabilities
31+
Then returns opus codec
32+
33+
Scenario: Get audio capabilities in other Browser
34+
Given my browser audio capabilities
35+
When I get audio capabilities
36+
Then returns opus codec
2737

2838
Scenario: Get capabilities from inexistent kind
2939
When I get data capabilities
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
Feature: As a user I want to publish a stream without managing connections
2+
3+
Scenario: Instance publisher without streamName
4+
Given no stream name
5+
When I instance a MillicastPublish
6+
Then throws an error
7+
8+
Scenario: Broadcast stream
9+
Given an instance of MillicastPublish
10+
When I broadcast a stream with a connection path and media stream
11+
Then peer connection state is connected
12+
13+
Scenario: Broadcast stream default options
14+
Given an instance of MillicastPublish
15+
When I broadcast a stream without options
16+
Then throws an error
17+
18+
Scenario: Broadcast without connection path
19+
Given an instance of MillicastPublish
20+
When I broadcast a stream without a connection path
21+
Then throws an error
22+
23+
Scenario: Broadcast without mediaStream
24+
Given an instance of MillicastPublish
25+
When I broadcast a stream without a mediaStream
26+
Then throws an error
27+
28+
Scenario: Broadcast to active publisher
29+
Given an instance of MillicastPublish already connected
30+
When I broadcast again to the stream
31+
Then throws an error
32+
33+
Scenario: Broadcast stream with bandwidth restriction
34+
Given an instance of MillicastPublish
35+
When I broadcast a stream with bandwidth restriction
36+
Then peer connection state is connected
37+
38+
Scenario: Stop publish
39+
Given I am publishing a stream
40+
When I stop the publish
41+
Then peer connection and WebSocket are null
42+
43+
Scenario: Stop inactive publish
44+
Given I am not publishing a stream
45+
When I stop the publish
46+
Then peer connection and WebSocket are null
47+
48+
Scenario: Check status of active publish
49+
Given I am publishing a stream
50+
When I check if publish is active
51+
Then returns true
52+
53+
Scenario: Check status of inactive publish
54+
Given I am not publishing a stream
55+
When I check if publish is active
56+
Then returns false

0 commit comments

Comments
 (0)