Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
a817a45
added maxOpusPlaybackRate param
jpsantosbh Mar 8, 2025
1d8c330
Merge branch 'main' into joao/opus_config
jpsantosbh Mar 11, 2025
ff9f615
testing
jpsantosbh Mar 11, 2025
51ab08a
maxaveragebitrate
jpsantosbh Mar 11, 2025
0670610
add PlaybackRate enum
jpsantosbh Mar 12, 2025
950e8df
replace sdp with sdpTransform
jpsantosbh Mar 20, 2025
c65f62d
renaming parameters
jpsantosbh Mar 20, 2025
0941f1d
hardening
jpsantosbh Mar 20, 2025
94623fc
playground changes
jpsantosbh Mar 21, 2025
20bcb3e
Merge branch 'main' into joao/opus_config
jpsantosbh Mar 21, 2025
c7a4220
Merge branch 'main' into joao/opus_config
jpsantosbh Mar 21, 2025
07c55d2
Fix build
jpsantosbh Mar 25, 2025
db83282
cleanup
jpsantosbh Apr 8, 2025
93d1fb0
manual prettier
jpsantosbh Apr 15, 2025
78a1f78
Update internal/playground-js/src/fabric/index.js
jpsantosbh Apr 15, 2025
81a1b4c
Update packages/js/src/fabric/WSClient.ts
jpsantosbh Apr 15, 2025
8e924e0
flexible audio codec params
jpsantosbh May 6, 2025
8d0f26e
Merge remote-tracking branch 'origin/main' into joao/opus_config
jpsantosbh May 6, 2025
035d424
changeset
jpsantosbh May 6, 2025
dc0c11f
Atualizar o index.html
jpsantosbh May 7, 2025
51c5eea
Atualizar o index.html
jpsantosbh May 7, 2025
6e37d34
Atualizar o index.html
jpsantosbh May 7, 2025
00b65d9
Update internal/playground-js/src/fabric/index.html
jpsantosbh May 7, 2025
aad67a7
UI fixes
jpsantosbh May 7, 2025
41817e3
dependencies fix
jpsantosbh May 7, 2025
b8704ad
build fix
jpsantosbh May 7, 2025
255f0cf
formating
jpsantosbh May 7, 2025
37e545d
Update packages/webrtc/src/RTCPeer.ts
jpsantosbh May 7, 2025
a514e78
Update packages/js/src/fabric/interfaces/wsClient.ts
jpsantosbh May 7, 2025
5f99076
fixes
jpsantosbh May 7, 2025
5d5b474
fix
jpsantosbh May 7, 2025
43d5abe
fix test
jpsantosbh May 7, 2025
eb67f33
test fix
jpsantosbh May 7, 2025
b54aa5a
fix sdpMediaOrderHack
jpsantosbh May 7, 2025
4d49452
don't change the remote description
jpsantosbh May 8, 2025
9b38f4a
Merge branch 'main' into joao/opus_config
jpsantosbh May 26, 2025
7e5704c
cleanup
jpsantosbh May 26, 2025
9e7335b
Update audio codec selection and input handling in the demo
jpsantosbh May 26, 2025
a1ec706
Merge branch 'main' into joao/opus_config
jpsantosbh May 29, 2025
88534ea
testing
jpsantosbh Jun 4, 2025
eb4b374
testing
jpsantosbh Jun 4, 2025
aede240
Merge branch 'joao/opus_config' of github.com:signalwire/signalwire-j…
jpsantosbh Jun 4, 2025
97afc46
testing
jpsantosbh Jun 4, 2025
d9f1610
revert
jpsantosbh Jun 4, 2025
bd5996f
Merge main into joao/opus_config and resolve conflicts
jpsantosbh Jun 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions internal/playground-js/src/fabric/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ <h5>Connect</h5>
Include Video
</label>
</div>
<div class="form-group">
<label for="opusConfig">OPUS playbackRate</label>
<input
type="text"
class="form-control"
id="opusConfig"
placeholder="8000"
onchange="saveInLocalStorage(event)"
/>
</div>
</div>
<div class="d-grid gap-2">
<button
Expand Down
3 changes: 3 additions & 0 deletions internal/playground-js/src/fabric/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,7 @@ window.dial = async ({ reattach = false } = {}) => {
rootElement: document.getElementById('rootElement'),
video: document.getElementById('video').checked,
audio: document.getElementById('audio').checked,
maxOpusPlaybackRate: parseInt(document.getElementById('opusConfig').value)
})

window.__call = call
Expand Down Expand Up @@ -888,6 +889,8 @@ window.ready(async function () {
document.getElementById('audio').checked = true
document.getElementById('video').checked =
localStorage.getItem('fabric.ws.video') === 'true'
document.getElementById('opusConfig').value =
localStorage.getItem('fabric.ws.opusConfig') || ''

const urlParams = new URLSearchParams(window.location.search)
const room = urlParams.get('room')
Expand Down
1 change: 1 addition & 0 deletions packages/js/src/fabric/WSClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export class WSClient extends BaseClient<{}> implements WSClientContract {
attach: params.attach ?? false,
disableUdpIceServers: params.disableUdpIceServers || false,
userVariables: params.userVariables || this.wsClientOptions.userVariables,
maxOpusPlaybackRate: params.maxOpusPlaybackRate
})

// WebRTC connection left the room.
Expand Down
2 changes: 2 additions & 0 deletions packages/js/src/fabric/interfaces/wsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ export interface CallParams {
negotiateVideo?: boolean
/** User & UserAgent metadata */
userVariables?: WSClientOptions['userVariables']
/** OPUS audio codec max playback rate in Hz */
maxOpusPlaybackRate?: number
}

export interface DialParams extends CallParams {
Expand Down
13 changes: 7 additions & 6 deletions packages/webrtc/src/RTCPeer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import {
getUserMedia,
getMediaConstraints,
filterIceServers,
getSenderAudioMaxBitrate,
} from './utils/helpers'
import {
sdpStereoHack,
sdpBitrateHack,
sdpMediaOrderHack,
sdpHasValidCandidates,
opusConfigsHack,
} from './utils/sdpHelpers'
import { BaseConnection } from './BaseConnection'
import {
Expand Down Expand Up @@ -571,6 +572,7 @@ export default class RTCPeer<EventTypes extends EventEmitter.ValidEventTypes> {
const audioTransceiverParams: RTCRtpTransceiverInit = {
direction: this.options.negotiateAudio ? 'sendrecv' : 'sendonly',
streams: [this._localStream],
sendEncodings: [{maxBitrate: getSenderAudioMaxBitrate(this.options)}]
}
this.logger.debug(
'Applying audioTransceiverParams',
Expand Down Expand Up @@ -801,13 +803,12 @@ export default class RTCPeer<EventTypes extends EventEmitter.ValidEventTypes> {

private _setLocalDescription(localDescription: RTCSessionDescriptionInit) {
const {
useStereo,
googleMaxBitrate,
googleMinBitrate,
googleStartBitrate,
} = this.options
if (localDescription.sdp && useStereo) {
localDescription.sdp = sdpStereoHack(localDescription.sdp)
if (localDescription.sdp) {
localDescription.sdp = opusConfigsHack(localDescription.sdp, this.options)
}
if (
localDescription.sdp &&
Expand All @@ -832,8 +833,8 @@ export default class RTCPeer<EventTypes extends EventEmitter.ValidEventTypes> {
}

private _setRemoteDescription(remoteDescription: RTCSessionDescriptionInit) {
if (remoteDescription.sdp && this.options.useStereo) {
remoteDescription.sdp = sdpStereoHack(remoteDescription.sdp)
if (remoteDescription.sdp) {
remoteDescription.sdp = opusConfigsHack(remoteDescription.sdp, this.options)
}
if (remoteDescription.sdp && this.instance.localDescription) {
remoteDescription.sdp = sdpMediaOrderHack(
Expand Down
79 changes: 65 additions & 14 deletions packages/webrtc/src/utils/helpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getMediaConstraints } from '../utils/helpers'
import { getMediaConstraints, getSenderAudioMaxBitrate } from '../utils/helpers'

jest.mock('../utils/deviceHelpers', () => ({
assureDeviceId: jest.fn().mockImplementation(async(p:any)=> Promise.resolve(p))
Expand All @@ -17,25 +17,49 @@ describe('Helpers functions', () => {

it('should return audio === true & video === false', async () => {
const mediaConstraints = await getMediaConstraints({})
expect(mediaConstraints.audio).toStrictEqual(true)
expect(mediaConstraints.audio).toEqual({"channelCount": 1})
expect(mediaConstraints.video).toStrictEqual(false)
})

it('should return audio === true & video === false', async () => {
const mediaConstraints = await getMediaConstraints({audio: true, video: true})
expect(mediaConstraints.audio).toStrictEqual(true)
expect(mediaConstraints.video).toStrictEqual(true)
expect(mediaConstraints.audio).toEqual({"channelCount": 1})
expect(mediaConstraints.video).toEqual({})
})

it('should return audio === {}', async () => {
const mediaConstraints = await getMediaConstraints({audio: {}})
expect(mediaConstraints.audio).toEqual({})
expect(mediaConstraints.audio).toEqual({"channelCount": 1})
expect(mediaConstraints.video).toStrictEqual(false)
})

it('should return audio === {}', async () => {
const mediaConstraints = await getMediaConstraints({audio: {}, video: {}})
expect(mediaConstraints.audio).toEqual({})
expect(mediaConstraints.audio).toEqual({"channelCount": 1})
expect(mediaConstraints.video).toEqual({})
})

it('should return compatible audio constrains, mono - audio only', async () => {
const mediaConstraints = await getMediaConstraints({audio: true, video: false, maxOpusPlaybackRate: 8000, useStereo: false})
expect(mediaConstraints.audio).toEqual({"channelCount": 1, "sampleRate": 8000})
expect(mediaConstraints.video).toStrictEqual(false)
})

it('should return compatible audio constrains, stereo - audio only', async () => {
const mediaConstraints = await getMediaConstraints({audio: true, video: false, maxOpusPlaybackRate: 8000, useStereo: true})
expect(mediaConstraints.audio).toEqual({"channelCount": 2, "sampleRate": 8000})
expect(mediaConstraints.video).toStrictEqual(false)
})

it('should return compatible audio constrains, mono - with video', async () => {
const mediaConstraints = await getMediaConstraints({audio: true, video: true, maxOpusPlaybackRate: 8000, useStereo: false})
expect(mediaConstraints.audio).toEqual({"channelCount": 1, "sampleRate": 8000})
expect(mediaConstraints.video).toEqual({})
})

it('should return compatible audio constrains, stereo - with video', async () => {
const mediaConstraints = await getMediaConstraints({audio: true, video: true, maxOpusPlaybackRate: 8000, useStereo: true})
expect(mediaConstraints.audio).toEqual({"channelCount": 2, "sampleRate": 8000})
expect(mediaConstraints.video).toEqual({})
})

Expand All @@ -47,17 +71,17 @@ describe('Helpers functions', () => {

it('should return audio === true', async () => {
const mediaConstraints = await getMediaConstraints({remoteSdp: SDP})
expect(mediaConstraints.audio).toStrictEqual(true)
expect(mediaConstraints.audio).toEqual({"channelCount": 1})
})

it('should return audio === {}', async () => {
const mediaConstraints = await getMediaConstraints({audio: {}, remoteSdp: SDP})
expect(mediaConstraints.audio).toEqual({})
expect(mediaConstraints.audio).toEqual({"channelCount": 1})
})

it('should return audio === {deviceId: { exact: "abcd" }}', async () => {
const mediaConstraints = await getMediaConstraints({micId: 'abcd', remoteSdp: SDP})
expect(mediaConstraints.audio).toEqual({deviceId: { exact: "abcd" }})
expect(mediaConstraints.audio).toEqual({deviceId: { exact: "abcd" }, channelCount: 1})
})
})

Expand Down Expand Up @@ -107,27 +131,54 @@ describe('Helpers functions', () => {

it('should return audio === true & video === false', async () => {
const mediaConstraints = await getMediaConstraints({remoteSdp: SDP})
expect(mediaConstraints.audio).toStrictEqual(true)
expect(mediaConstraints.audio).toEqual({"channelCount": 1})
expect(mediaConstraints.video).toStrictEqual(false)
})

it('should return audio === true & video === true', async () => {
const mediaConstraints = await getMediaConstraints({video: true, remoteSdp: SDP})
expect(mediaConstraints.audio).toStrictEqual(true)
expect(mediaConstraints.video).toStrictEqual(true)
expect(mediaConstraints.audio).toEqual({"channelCount": 1})
expect(mediaConstraints.video).toEqual({})
})

it('should return audio === {} & video === {}', async () => {
const mediaConstraints = await getMediaConstraints({audio: {}, video: {}, remoteSdp: SDP})
expect(mediaConstraints.audio).toEqual({})
expect(mediaConstraints.audio).toEqual({"channelCount": 1})
expect(mediaConstraints.video).toEqual({})
})

it('should return audio === {deviceId: { exact: "abcd" }} & video === {deviceId: { exact: "abcd" }}' , async () => {
const mediaConstraints = await getMediaConstraints({micId: 'abcd', camId: 'abcd', remoteSdp: SDP})
expect(mediaConstraints.audio).toEqual({deviceId: { exact: "abcd" }})
expect(mediaConstraints.audio).toEqual({deviceId: { exact: "abcd" }, channelCount: 1})
expect(mediaConstraints.video).toEqual({deviceId: { exact: "abcd" }})
})
})
})

describe('getSenderAudioMaxBitrate', () => {
it('should return 20000', () => {
expect(getSenderAudioMaxBitrate({useStereo: false, maxOpusPlaybackRate: 8000}))
})
it('should return 40000', () => {
expect(getSenderAudioMaxBitrate({useStereo: true, maxOpusPlaybackRate: 8000}))
})
it('should return 320000', () => {
expect(getSenderAudioMaxBitrate({useStereo: false, maxOpusPlaybackRate: 16000}))
})
it('should return 640000', () => {
expect(getSenderAudioMaxBitrate({useStereo: true, maxOpusPlaybackRate: 16000}))
})
it('should return 320000', () => {
expect(getSenderAudioMaxBitrate({useStereo: false, maxOpusPlaybackRate: 32000}))
})
it('should return 640000', () => {
expect(getSenderAudioMaxBitrate({useStereo: true, maxOpusPlaybackRate: 32000}))
})
it('should return 640000', () => {
expect(getSenderAudioMaxBitrate({useStereo: false, maxOpusPlaybackRate: 48000}))
})
it('should return 1280000', () => {
expect(getSenderAudioMaxBitrate({useStereo: true, maxOpusPlaybackRate: 48000}))
})
})
})
48 changes: 39 additions & 9 deletions packages/webrtc/src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { assureDeviceId } from './deviceHelpers'
import { ConnectionOptions } from './interfaces'
import { sdpHasAudio, sdpHasVideo } from './sdpHelpers'

const DEFAULT_MAX_BITRATE = 64000

// FIXME: Remove and use getUserMedia directly
export const getUserMedia = (constraints: MediaStreamConstraints) => {
getLogger().info('RTCService.getUserMedia', constraints)
Expand Down Expand Up @@ -43,38 +45,66 @@ export const getMediaConstraints = async (
options: ConnectionOptions
): Promise<MediaStreamConstraints> => {
let audio = _getAudioConstraints(options)
const { micLabel = '', micId } = options
let video = _getVideoConstraints(options)

const { micLabel = '', micId, camLabel = '', camId, maxOpusPlaybackRate, useStereo } = options

const channelCount = useStereo ? 2 : 1
if (typeof audio === 'boolean' && audio) {
audio = { channelCount }
} else if (typeof audio === 'object') {
audio.channelCount = channelCount
}

if (typeof video === 'boolean' && video) {
video = {}
}

if (micId && audio) {
const newMicId = await assureDeviceId(micId, micLabel, 'microphone').catch(
(_error) => null
)
if (newMicId) {
if (typeof audio === 'boolean') {
audio = {}
}
audio.deviceId = { exact: newMicId }
}
}

let video = _getVideoConstraints(options)
const { camLabel = '', camId } = options
if(maxOpusPlaybackRate && audio) {
audio.sampleRate = maxOpusPlaybackRate
}

if (camId && video) {
const newCamId = await assureDeviceId(camId, camLabel, 'camera').catch(
(_error) => null
)
if (newCamId) {
if (typeof video === 'boolean') {
video = {}
}
video.deviceId = { exact: newCamId }
}
}

return { audio, video }
}

export const getSenderAudioMaxBitrate = (options: ConnectionOptions) => {
if(!options.maxOpusPlaybackRate && !options.useStereo) {
return
}

let maxBitrate = DEFAULT_MAX_BITRATE

if(options.maxOpusPlaybackRate && (options.maxOpusPlaybackRate <= 8000)) {
maxBitrate = options.useStereo ? 20000 : 40000
} else if(options.maxOpusPlaybackRate && (options.maxOpusPlaybackRate <= 16000)) {
maxBitrate = options.useStereo ? 32000 : 64000
} else if(options.maxOpusPlaybackRate && (options.maxOpusPlaybackRate <= 24000)) {
maxBitrate = options.useStereo ? 40000 : 80000
} else if (options.useStereo) {
maxBitrate = 128000
}

return maxBitrate
}

interface FilterIceServersOptions {
disableUdpIceServers?: boolean
}
Expand Down
3 changes: 3 additions & 0 deletions packages/webrtc/src/utils/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export interface ConnectionOptions {
maxIceGatheringTimeout?: number
/** @internal */
maxConnectionStateTimeout?: number
/** OPUS audio codec max playback rate in Hz */
maxOpusPlaybackRate?: number
/** @internal */
watchMediaPackets?: boolean
/** @internal */
Expand All @@ -99,6 +101,7 @@ export interface ConnectionOptions {
nodeId?: string

layout?: string

positions?: VideoPositions
}

Expand Down
Loading
Loading