Simulcast
Simulcast is a technique used in WebRTC to optimize video streaming by sending multiple versions of the same video stream at different resolutions. These streams are then sent to VPaaS service simultaneously. The receiver can select the most appropriate stream based on network conditions and device capabilities, ensuring the best possible video quality and minimizing buffering or latency issues.
A powerful device with a fast internet connection can display high-resolution video, while a less capable device or one on a slower connection might only be able to handle lower resolution video.
Simulcast is particularly useful in scenarios where network conditions may vary or where we have devices with different capabilities.
You can download the starter code from Step.06-Exercise-Simulcast.
RTPStreamId interface
The first step is to open the file ./client/src/types/RTPStreamId.ts
and
define the different video stream qualities. Our application will define two
qualities: High
and Low
:
export enum RTPStreamId {
Low = 'l',
High = 'h',
}
StreamInfo interface
Once we have defined all the possible RTPStreamId
, we will open the file
./client/src/types/StreamInfo.ts
and add two new parameters to our
StreamInfo
interface:
- layers: Array with all possible
layers
or qualities to which we can subscribe to. - rid: The second parameter is called
rid
and represents the current quality to which we are subscribed to.
export interface StreamInfo {
...
layers: Layer[]
rid?: RTPStreamId
}
Define LocalStorage key
In the previous lesson we have define a file with all possible localStorage
keys in which we save the selected media devices. We will do the same for the
rid
. Open the file ./client/src/types/LocalStorageKey.ts
and add the new
parameter:
export enum LocalStorageKey {
...
IncomingVideoQualityKey = `${prefix}:incomingVideoQuality`
}
Settings component
Now we need a way to change the preferred rid
. We will add a new selector in
the Settings
component that we have created for changing the media device.
Open the file ./client/src/Meeting/Settings/Settings.tsx
to make these
modifications.
The first step is to add Select
to the list of imports from
@pexip/components
:
import {Bar, Button, Modal, Select} from '@pexip/components';
Next now we import the RTPStreamId
that we have previously defined:
import {RTPStreamId} from '../../types/RTPStreamId';
Now we will jump to the Settings
function and define a state for
incomingVideoQuality
. Here we will store the current preferred quality for the
incoming streams:
const [incomingVideoQuality, setIncomingVideoQuality] = useState<RTPStreamId>(
RTPStreamId.High,
);
Now we will save the current value in the localStorage
before calling
props.onSave()
:
const handleSave = (): void => {
...
localStorage.setItem(
LocalStorageKey.IncomingVideoQualityKey,
incomingVideoQuality
)
localStream?.getTracks().forEach((track) => {
track.stop()
})
props.onSave()
}
Next we need to add a couple of lines to retrieve the last value of the
incomingVideoQuality
from the localStorage
each time the Settings
panel is
displayed:
useEffect(() => {
if (props.isOpen) {
...
const quality: RTPStreamId = localStorage.getItem(
LocalStorageKey.IncomingVideoQualityKey
) as RTPStreamId
if (quality != null) {
setIncomingVideoQuality(quality)
}
}
}, [props.isOpen])
Next, we render the Select
component that will allow us to change the quality:
<Select
className="IncomingVideoQuality"
label="Incoming Video Quality"
value={incomingVideoQuality}
onValueChange={(id) => {
setIncomingVideoQuality(id as RTPStreamId)
}}
options={[
{ id: RTPStreamId.High, label: 'High' },
{ id: RTPStreamId.Low, label: 'Low' }
]}
/>
Meeting component
Finally, we need to edit the file ./client/src/Meeting/Meeting.tsx
and modify
how the meetings are created and managed. Our new functionality will:
- Save the
layers
offered by other participants. - Include the
rid
when the user subscribes to a new stream. - Define the different
rid
that the user offers when they connect to the meeting. - Unsubscribe and subscribe again when the user changes the preferred
rid
.
We start by importing MediaEncodingParameters
from @pexip/peer-connection
:
import type {
MediaInit,
TransceiverConfig,
MediaEncodingParameters,
} from '@pexip/peer-connection';
We also import RTPStreamId
and LocalStorageKey
:
import {RTPStreamId} from '../types/RTPStreamId';
import {LocalStorageKey} from '../types/LocalStorageKey';
We modify the handler for the roster
event and save the layers
in
streamsInfo
:
const handleRosterUpdate = (roster: Record<string, RosterEntry>): void => {
...
if (!found) {
let layers = []
if ((stream as any)?.layers != null) {
layers = (stream as any)?.layers
}
streamsInfo.push({
streamId,
type: stream.type,
participantId,
layers
})
}
}
}
...
}
We modify the subscribeStream
function that now will receive an optional
parameter preferredRid
. If the preferredRid
exists in the available
layers
, we add the rid
in the call to requestStream
and save this value in
streamInfo.rid
to indicate to which quality we are subscribed:
const subscribeStream = async (
streamInfo: StreamInfo,
preferredRid?: RTPStreamId,
): Promise<void> => {
const requestedRid = streamInfo.layers?.some(
layer => layer.rid === preferredRid,
)
? preferredRid
: undefined;
const response = await vpaas.requestStream({
producer_id: streamInfo.participantId,
stream_id: streamInfo.streamId,
rid: (requestedRid as string) ?? null,
receive_mid: null,
});
streamInfo.mid = response.receive_mid;
streamInfo.rid = requestedRid;
setStreamsInfo([...streamsInfo]);
};
Now we modify connectMeeting
to send the video with two different qualities.
For that we add the sendEncodings
property and build it in the
getEncodings(mediaStream)
that we create in the following step:
const connectMeeting = async (apiAddress: string, participant: Participant, mediaStream: MediaStream): Promise<void> => {
...
const mediaInits: MediaInit[] = [{
content: 'main',
direction: 'sendonly',
kindOrTrack: mediaStream.getAudioTracks()[0],
streams: [mediaStream]
}, {
content: 'main',
direction: 'sendonly',
kindOrTrack: mediaStream.getVideoTracks()[0],
streams: [mediaStream],
sendEncodings: getEncodings(mediaStream)
},
...createRecvTransceivers('audio', RECV_AUDIO_COUNT),
...createRecvTransceivers('video', RECV_VIDEO_COUNT)
]
...
}
We will define the function getEncodings
that returns the different
resolutions that user will deliver to VPaaS. We will define a low resolution
that will be a quarter of the original resolution and a high resolution that
will be the original resolution captured by the camera:
const getEncodings = (
mediaStream: MediaStream,
): MediaEncodingParameters[] | undefined => {
const [videoTrack] = mediaStream.getVideoTracks();
const {width, height} = videoTrack?.getSettings() ?? {};
if (width != null && height != null) {
return [
{
rid: RTPStreamId.Low,
scaleResolutionDownBy: 4.0,
maxWidth: Math.trunc(width / 4),
maxHeight: Math.trunc(height / 4),
},
{
rid: RTPStreamId.High,
maxWidth: width,
maxHeight: height,
},
];
} else {
return undefined;
}
};
Now we modify the callback function for when the user changes a parameter in the
settings. We will retrieve the value of incomingVideoQuality
from the
localStorage and, if the rid
was changed, we unsubscribe from the previous
stream and subscribe again with the new value of rid
:
const handleSaveSettings = async (): Promise<void> => {
...
streamsInfo.forEach((streamInfo) => {
if (streamInfo.type === 'video') {
const preferredRid =
(localStorage.getItem(
LocalStorageKey.IncomingVideoQualityKey
) as RTPStreamId) ?? RTPStreamId.High
if (streamInfo.mid != null && streamInfo.rid !== preferredRid) {
unsubscribeStream(streamInfo)
.then(() => {
subscribeStream(streamInfo, preferredRid).catch((e) => {
console.error(e)
})
})
.catch((e) => {
console.error(e)
})
}
}
})
}
}
Finally, we change the subscribeStream()
call in the useEffect()
hook to
include the preferredRid
:
useEffect(() => {
...
const preferredRid =
(localStorage.getItem(
LocalStorageKey.IncomingVideoQualityKey
) as RTPStreamId) ?? RTPStreamId.High
subscribeStream(streamInfo, preferredRid).catch((e) => {
console.error(e)
})
}
})
}
}, [streamsInfo, remoteTransceiversConfig])
Run the app
If you want to run the application, you will need to launch the server and the client at the same time. We start by launching the server:
$ cd server
$ npm start
And in another terminal we launch the client:
$ cd client
$ npm start
The browser will be opened automatically with the URL https://localhost:4000.
If you click on the settings button during a meeting, you will see a new selector to modify the incoming video quality:
You can compare your code with the solution in Step.06-Solution-Simulcast. You can also check the differences with the previous lesson in the git diff.