Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC Renegotiation In Parallel #1140

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
451667e
testing
jpsantosbh Jul 17, 2024
0bbb68a
Merge branch 'main' into joao/renegotiation
jpsantosbh Jul 22, 2024
56ac7b5
cleanup
jpsantosbh Jul 22, 2024
b73d367
Merge branch 'main' into joao/renegotiation
jpsantosbh Sep 9, 2024
d312dc7
test hacking renegotiation passing
jpsantosbh Sep 11, 2024
f1cfd61
added renegotiateMedia
jpsantosbh Oct 2, 2024
9109a44
Merge branch 'main' into joao/renegotiation
jpsantosbh Oct 8, 2024
96b0c42
fix getMediaConstraints
jpsantosbh Oct 14, 2024
8607fa6
Merge branch 'joao/fix_media_constraints' into joao/renegotiation
jpsantosbh Oct 14, 2024
02b2d5c
start with offering true
jpsantosbh Oct 14, 2024
1249777
check MCU
jpsantosbh Oct 14, 2024
e350878
Merge branch 'main' into joao/renegotiation
jpsantosbh Oct 14, 2024
b7fcd79
changeset
jpsantosbh Oct 14, 2024
5708919
fixes
jpsantosbh Oct 15, 2024
46b833c
Update internal/e2e-js/tests/callfabric/renegotiation.spec.ts
jpsantosbh Oct 21, 2024
cbdacc0
Update packages/webrtc/src/RTCPeer.ts
jpsantosbh Oct 21, 2024
3a0be5f
Update packages/webrtc/src/RTCPeer.ts
jpsantosbh Oct 21, 2024
dec5f50
Update internal/e2e-js/tests/callfabric/renegotiation.spec.ts
jpsantosbh Oct 21, 2024
e5485e7
added to the public contract
jpsantosbh Oct 21, 2024
6231930
refactor
jpsantosbh Oct 21, 2024
2ca56ad
add to playground
jpsantosbh Oct 21, 2024
fc3803a
Merge branch 'main' into joao/renegotiation
jpsantosbh Oct 21, 2024
e821a1c
merge fix
jpsantosbh Oct 21, 2024
fb875a6
Update packages/webrtc/src/RTCPeer.ts
jpsantosbh Oct 22, 2024
a5ab32e
Update internal/playground-js/src/fabric/index.js
jpsantosbh Oct 22, 2024
7dc89f6
poc
jpsantosbh Nov 6, 2024
1bbb57b
fix
jpsantosbh Nov 7, 2024
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
5 changes: 5 additions & 0 deletions .changeset/sweet-garlics-tease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@signalwire/webrtc': minor
---

Added public APIs for media renegotiation
185 changes: 185 additions & 0 deletions internal/e2e-js/tests/callfabric/renegotiation.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { uuid } from '@signalwire/core'
import { test, expect } from '../../fixtures'
import {
SERVER_URL,
createCFClient,
dialAddress,
expectMCUVisible,
expectMCUVisibleForAudience,
getStats,
} from '../../utils'
import { CallFabricRoomSession } from '@signalwire/js'

test.describe('CallFabric Renegotiation', () => {

test('Joining a room with audio channel only and enable sendrecv video', async ({
createCustomPage,
resource,
}) => {
const page = await createCustomPage({ name: '[page]' })
await page.goto(SERVER_URL)

const roomName = `e2e-video-room_${uuid()}`
await resource.createVideoRoomResource(roomName)

await createCFClient(page)

// Dial an address with audio only channel
const roomSession = await dialAddress(page, {
address: `/public/${roomName}?channel=audio`,
})

expect(roomSession.room_session).toBeDefined()
expect(
roomSession.room_session.members.some(
(member: any) => member.member_id === roomSession.member_id
)
).toBeTruthy()

await page.waitForTimeout(1000)

let stats = await getStats(page)
0
expect(stats.outboundRTP).not.toHaveProperty('video')
expect(stats.inboundRTP).not.toHaveProperty('video')

expect(stats.inboundRTP.audio.packetsReceived).toBeGreaterThan(0)

await page.evaluate(async () => {
// @ts-expect-error
const cfRoomSession = (window._roomObj as CallFabricRoomSession)

await cfRoomSession.enableVideo();
});

await page.waitForTimeout(2000)

stats = await getStats(page)

expect(stats.outboundRTP).toHaveProperty('video')
expect(stats.inboundRTP).toHaveProperty('video')

await page.waitForTimeout(2000)
expectMCUVisible(page)
})

test('Joining a room with audio channel only and enable sendOnly video', async ({
createCustomPage,
resource,
}) => {
const page = await createCustomPage({ name: '[page]' })
await page.goto(SERVER_URL)

const roomName = `e2e-video-room_${uuid()}`
await resource.createVideoRoomResource(roomName)

await createCFClient(page)

// Dial an address with audio only channel
const roomSession = await dialAddress(page, {
address: `/public/${roomName}?channel=audio`,
})

expect(roomSession.room_session).toBeDefined()
expect(
roomSession.room_session.members.some(
(member: any) => member.member_id === roomSession.member_id
)
).toBeTruthy()

await page.waitForTimeout(1000)

let stats = await getStats(page)

expect(stats.outboundRTP).not.toHaveProperty('video')
expect(stats.inboundRTP).not.toHaveProperty('video')

expect(stats.inboundRTP.audio.packetsReceived).toBeGreaterThan(0)

await page.evaluate(async () => {
// @ts-expect-error
const cfRoomSession = (window._roomObj as CallFabricRoomSession)

await cfRoomSession.enableVideo({sendOnly: true});
});

await page.waitForTimeout(1000)

stats = await getStats(page)

expect(stats.outboundRTP).toHaveProperty('video')
expect(stats.inboundRTP).not.toHaveProperty('video')
})

test('Joining a room with audio channel only and enable recvOnly video', async ({
createCustomPage,
resource,
}) => {
const page = await createCustomPage({ name: '[page]' })
await page.goto(SERVER_URL)

const roomName = `e2e-video-room_${uuid()}`
await resource.createVideoRoomResource(roomName)

await createCFClient(page)

// Dial an address with audio only channel
const roomSession = await page.evaluate(
async ({ roomName }) => {
return new Promise<any>(async (resolve, _reject) => {
// @ts-expect-error
const client = window._client

const call = await client.dial({
to: `/public/${roomName}?channel=audio`,
rootElement: document.getElementById('rootElement'),
})

call.on('room.joined', resolve)
call.on('room.updated', () => {})



// @ts-expect-error
window._roomObj = call

await call.start()
})
},
{ roomName }
)

expect(roomSession.room_session).toBeDefined()
expect(
roomSession.room_session.members.some(
(member: any) => member.member_id === roomSession.member_id
)
).toBeTruthy()

await page.waitForTimeout(1000)

let stats = await getStats(page)

expect(stats.outboundRTP).not.toHaveProperty('video')
expect(stats.inboundRTP).not.toHaveProperty('video')

expect(stats.inboundRTP.audio.packetsReceived).toBeGreaterThan(0)

await page.evaluate(async () => {
// @ts-expect-error
const cfRoomSession = (window._roomObj as CallFabricRoomSession)

await cfRoomSession.enableVideo({video: false, sendOnly: false});
});

await page.waitForTimeout(1000)

stats = await getStats(page)

expect(stats.outboundRTP).not.toHaveProperty('video')
expect(stats.inboundRTP).toHaveProperty('video')

await page.waitForTimeout(1000)
expectMCUVisibleForAudience(page)
})
})
8 changes: 8 additions & 0 deletions internal/playground-js/src/fabric/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,14 @@ <h5>Connect</h5>
<div id="roomControls" class="card mt-4 d-none">
<div class="card-body">
<h5>Controls</h5>
<Button
id="enableVideoBtn"
class="btn btn-warning px-3 mt-2"
onClick="enableVideo()"
>
Enable Video
</Button>

<div class="btn-group w-100" role="group">
<button
id="muteSelfBtn"
Expand Down
15 changes: 15 additions & 0 deletions internal/playground-js/src/fabric/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const inCallElements = [
pauseRecordingBtn,
resumeRecordingBtn,
controlPlayback,
enableVideoBtn,
]

const playbackElements = [
Expand Down Expand Up @@ -233,6 +234,8 @@ const initializeMicAnalyzer = async (stream) => {
function restoreUI() {
btnConnect.classList.remove('d-none')
btnDisconnect.classList.add('d-none')
enableVideoBtn.classList.add('d-none')
enableVideoBtn.innerHTML = 'Enable Video'
connectStatus.innerHTML = 'Not Connected'

inCallElements.forEach((button) => {
Expand Down Expand Up @@ -401,6 +404,8 @@ window.connect = async ({ reattach = false } = {}) => {
btnConnect.classList.add('d-none')
btnDisconnect.classList.remove('d-none')
connectStatus.innerHTML = 'Connected'
enableVideoBtn.classList.remove('d-none')
enableVideoBtn.innerHTML = roomObj.options.video ? 'Disable Video' : 'Enable Video'
inCallElements.forEach((button) => {
button.classList.remove('d-none')
button.disabled = false
Expand Down Expand Up @@ -776,6 +781,16 @@ window.seekForwardPlayback = () => {
})
}

window.enableVideo = async () => {
if(!roomObj.options.video) {
await roomObj.enableVideo()
enableVideoBtn.innerHTML = 'Disable Video'
} else {
await roomObj.disableVideo()
enableVideoBtn.innerHTML = 'Enable Video'
}
}

/**
* On document ready auto-fill the input values from the localStorage.
*/
Expand Down
22 changes: 22 additions & 0 deletions packages/js/src/utils/interfaces/callFabric.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { UpdateMediaOptions } from '@signalwire/webrtc'
import { VideoLayoutChangedEventParams, VideoPosition } from '@signalwire/core'

export interface CallFabricRoomSessionConnectionContract {
Expand Down Expand Up @@ -37,4 +38,25 @@ export interface CallFabricRoomSessionConnectionContract {
* ```
*/
hangup(id?: string): Promise<void>

/**
* Renegotiate RTC media channels based on the new media constraints
*
* @param renegotiateMediaParams
*/
renegotiateMedia(renegotiateMediaParams: UpdateMediaOptions): Promise<void>

/**
* Convenience method to enable video in a call
*
* @param enableVideoParam
*/
enableVideo(enableVideoParam?: Pick<UpdateMediaOptions, 'video'> & {sendOnly?: boolean}): Promise<void>

/**
* Convenience method to disable video in a call
*
* @param enableVideoParam
*/
disableVideo(disableVideoParam?: {recvOnly?: boolean}): Promise<void>
}
42 changes: 33 additions & 9 deletions packages/webrtc/src/BaseConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from '@signalwire/core'
import type { ReduxComponent } from '@signalwire/core'
import RTCPeer from './RTCPeer'
import { ConnectionOptions } from './utils/interfaces'
import { ConnectionOptions, UpdateMediaOptions } from './utils/interfaces'
import { stopTrack, getUserMedia, streamIsValid } from './utils'
import { sdpRemoveLocalCandidates } from './utils/sdpHelpers'
import * as workers from './workers'
Expand Down Expand Up @@ -391,9 +391,23 @@ export class BaseConnection<EventTypes extends EventEmitter.ValidEventTypes>
const rtcPeer = this._buildPeer('offer')
this.logger.debug('Trigger start for the new RTCPeer!')
await rtcPeer.start()
return rtcPeer
} catch (error) {
this.logger.error('Error building new RTCPeer to promote/demote', error)
throw error
}
}

async _renegotiateInParallel() {
const oldPeer = this.peer
try {
await this._triggerNewRTCPeer()
oldPeer?.detachAndStop()
} catch(e) {
this.peer = oldPeer
throw e
}

}

updateCamera(constraints: MediaTrackConstraints) {
Expand Down Expand Up @@ -682,7 +696,7 @@ export class BaseConnection<EventTypes extends EventEmitter.ValidEventTypes>
}

/** @internal */
onLocalSDPReady(rtcPeer: RTCPeer<EventTypes>) {
async onLocalSDPReady(rtcPeer: RTCPeer<EventTypes>) {
if (!rtcPeer.instance.localDescription) {
this.logger.error('Missing localDescription', rtcPeer)
throw new Error('Invalid RTCPeerConnection localDescription')
Expand All @@ -694,7 +708,9 @@ export class BaseConnection<EventTypes extends EventEmitter.ValidEventTypes>
case 'offer':
this._watchSessionAuth()
// If we have a remoteDescription already, send reinvite
if (!this.resuming && rtcPeer.instance.remoteDescription) {
if (!this.resuming && (rtcPeer.instance.remoteDescription || this.peer !== rtcPeer)) {
rtcPeer.uuid = this.peer?.uuid!
this.peer = rtcPeer
return this.executeUpdateMedia(mungedSDP, rtcPeer.uuid)
} else {
return this.executeInvite(mungedSDP, rtcPeer.uuid)
Expand Down Expand Up @@ -987,12 +1003,7 @@ export class BaseConnection<EventTypes extends EventEmitter.ValidEventTypes>
}

/** @internal */
updateMediaOptions(options: {
audio?: boolean
video?: boolean
negotiateAudio?: boolean
negotiateVideo?: boolean
}) {
updateMediaOptions(options: UpdateMediaOptions) {
this.logger.debug('updateMediaOptions', { ...options })
this.options = {
...this.options,
Expand Down Expand Up @@ -1086,4 +1097,17 @@ export class BaseConnection<EventTypes extends EventEmitter.ValidEventTypes>
})
this.rtcPeerMap.clear()
}

async renegotiateMedia(renegotiateMediaParams: UpdateMediaOptions): Promise<void> {
this.updateMediaOptions(renegotiateMediaParams)
await this._renegotiateInParallel();
}

async enableVideo(enableVideoParam?: Pick<UpdateMediaOptions, 'video'> & {sendOnly?: boolean}): Promise<void> {
await this.renegotiateMedia({video: enableVideoParam?.video ?? true, negotiateVideo: !enableVideoParam?.sendOnly})
}

async disableVideo(disableVideoParam?: {recvOnly?: boolean}): Promise<void> {
await this.renegotiateMedia({video: false, negotiateVideo: disableVideoParam?.recvOnly})
}
}
Loading
Loading