@100mslive/hms-video-store
Version:
@100mslive Core SDK which abstracts the complexities of webRTC while providing a reactive store for data management with a unidirectional data flow
215 lines (187 loc) • 7.98 kB
text/typescript
import IConnectionObserver, { RTCIceCandidatePair } from './IConnectionObserver';
import { HMSConnectionRole } from './model';
import { ErrorFactory } from '../error/ErrorFactory';
import { HMSAction } from '../error/HMSAction';
import { HMSAudioTrackSettings, HMSVideoTrackSettings } from '../media/settings';
import { HMSLocalTrack, HMSLocalVideoTrack } from '../media/tracks';
import { TrackState } from '../notification-manager';
import JsonRpcSignal from '../signal/jsonrpc';
import HMSLogger from '../utils/logger';
import { enableOpusDtx, fixMsid } from '../utils/session-description';
const TAG = '[HMSConnection]';
export default abstract class HMSConnection {
readonly role: HMSConnectionRole;
protected readonly signal: JsonRpcSignal;
protected abstract readonly observer: IConnectionObserver;
abstract readonly nativeConnection: RTCPeerConnection;
/**
* We keep a list of pending IceCandidates received
* from the signalling server. When the peer-connection
* is initialized we call [addIceCandidate] for each.
*
* WARN:
* - [HMSPublishConnection] keeps the complete list of candidates (for
* ice-connection failed/disconnect) forever.
* - [HMSSubscribeConnection] clears this list as soon as we call [addIceCandidate]
*/
readonly candidates = new Array<RTCIceCandidateInit>();
// @ts-ignore
sfuNodeId?: string;
selectedCandidatePair?: RTCIceCandidatePair;
protected constructor(role: HMSConnectionRole, signal: JsonRpcSignal) {
this.role = role;
this.signal = signal;
}
public get iceConnectionState(): RTCIceConnectionState {
return this.nativeConnection.iceConnectionState;
}
public get connectionState(): RTCPeerConnectionState {
return this.nativeConnection.connectionState;
}
private get action() {
return this.role === HMSConnectionRole.Publish ? HMSAction.PUBLISH : HMSAction.SUBSCRIBE;
}
setSfuNodeId(nodeId?: string) {
this.sfuNodeId = nodeId;
}
addTransceiver(track: MediaStreamTrack, init: RTCRtpTransceiverInit): RTCRtpTransceiver {
return this.nativeConnection.addTransceiver(track, init);
}
async createOffer(tracks?: Map<string, TrackState>, options?: RTCOfferOptions): Promise<RTCSessionDescriptionInit> {
try {
const offer = await this.nativeConnection.createOffer(options);
HMSLogger.d(TAG, `[role=${this.role}] createOffer offer=${JSON.stringify(offer, null, 1)}`);
return enableOpusDtx(fixMsid(offer, tracks));
} catch (error) {
throw ErrorFactory.WebrtcErrors.CreateOfferFailed(this.action, (error as Error).message);
}
}
async createAnswer(options: RTCOfferOptions | undefined = undefined): Promise<RTCSessionDescriptionInit> {
try {
const answer = await this.nativeConnection.createAnswer(options);
HMSLogger.d(TAG, `[role=${this.role}] createAnswer answer=${JSON.stringify(answer, null, 1)}`);
return answer;
} catch (error) {
throw ErrorFactory.WebrtcErrors.CreateAnswerFailed(this.action, (error as Error).message);
}
}
async setLocalDescription(description: RTCSessionDescriptionInit): Promise<void> {
try {
HMSLogger.d(TAG, `[role=${this.role}] setLocalDescription description=${JSON.stringify(description, null, 1)}`);
await this.nativeConnection.setLocalDescription(description);
} catch (error) {
throw ErrorFactory.WebrtcErrors.SetLocalDescriptionFailed(this.action, (error as Error).message);
}
}
async setRemoteDescription(description: RTCSessionDescriptionInit): Promise<void> {
try {
HMSLogger.d(TAG, `[role=${this.role}] setRemoteDescription description=${JSON.stringify(description, null, 1)}`);
await this.nativeConnection.setRemoteDescription(description);
} catch (error) {
throw ErrorFactory.WebrtcErrors.SetRemoteDescriptionFailed(this.action, (error as Error).message);
}
}
async addIceCandidate(candidate: RTCIceCandidateInit): Promise<void> {
if (this.nativeConnection.signalingState === 'closed') {
HMSLogger.d(TAG, `[role=${this.role}] addIceCandidate signalling state closed`);
return;
}
HMSLogger.d(TAG, `[role=${this.role}] addIceCandidate candidate=${JSON.stringify(candidate, null, 1)}`);
await this.nativeConnection.addIceCandidate(candidate);
}
public get remoteDescription(): RTCSessionDescription | null {
return this.nativeConnection.remoteDescription;
}
getSenders(): Array<RTCRtpSender> {
return this.nativeConnection.getSenders();
}
handleSelectedIceCandidatePairs() {
/**
* for the very first peer in the room we don't have any subscribe ice candidates
* because the peer hasn't subscribed to anything.
*
* For all peers joining after this peer, we have published and subscribed at the time of join itself
* so we're able to log both publish and subscribe ice candidates.
* Added try catch for the whole section as the getSenders and getReceivers is throwing errors in load test
*/
try {
const transmitters = this.role === HMSConnectionRole.Publish ? this.getSenders() : this.getReceivers();
transmitters.forEach(transmitter => {
const kindOfTrack = transmitter.track?.kind;
if (transmitter.transport) {
const iceTransport = transmitter.transport.iceTransport;
const handleSelectedCandidate = () => {
// @ts-expect-error
if (typeof iceTransport.getSelectedCandidatePair === 'function') {
// @ts-expect-error
this.selectedCandidatePair = iceTransport.getSelectedCandidatePair();
if (this.selectedCandidatePair) {
this.observer.onSelectedCandidatePairChange(this.selectedCandidatePair);
HMSLogger.d(
TAG,
`${HMSConnectionRole[this.role]} connection`,
`selected ${kindOfTrack || 'unknown'} candidate pair`,
JSON.stringify(this.selectedCandidatePair, null, 2),
);
}
}
};
// @ts-expect-error
if (typeof iceTransport.onselectedcandidatepairchange === 'function') {
// @ts-expect-error
iceTransport.onselectedcandidatepairchange = handleSelectedCandidate;
}
handleSelectedCandidate();
}
});
} catch (error) {
HMSLogger.w(
TAG,
`Error in logging selected ice candidate pair for ${HMSConnectionRole[this.role]} connection`,
error,
);
}
}
removeTrack(sender: RTCRtpSender) {
if (this.nativeConnection.signalingState !== 'closed') {
this.nativeConnection.removeTrack(sender);
}
}
// eslint-disable-next-line
async setMaxBitrateAndFramerate(
track: HMSLocalTrack,
updatedSettings?: HMSAudioTrackSettings | HMSVideoTrackSettings,
) {
const maxBitrate = updatedSettings?.maxBitrate || track.settings.maxBitrate;
const maxFramerate = track instanceof HMSLocalVideoTrack && track.settings.maxFramerate;
const sender = this.getSenders().find(s => s?.track?.id === track.getTrackIDBeingSent());
if (sender) {
const params = sender.getParameters();
// modify only for non-simulcast encodings
if (params.encodings.length === 1) {
if (maxBitrate) {
params.encodings[0].maxBitrate = maxBitrate * 1000;
}
if (maxFramerate) {
// @ts-ignore
params.encodings[0].maxFramerate = maxFramerate;
}
}
await sender.setParameters(params);
} else {
HMSLogger.w(
TAG,
`no sender found to setMaxBitrate for track - ${track.trackId}, sentTrackId - ${track.getTrackIDBeingSent()}`,
);
}
}
async getStats() {
return await this.nativeConnection.getStats();
}
close() {
this.nativeConnection.close();
}
private getReceivers() {
return this.nativeConnection.getReceivers();
}
}