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

Add fallback for addTrack if addTransceiver is not supported by a device #403

Merged
merged 7 commits into from
Aug 19, 2022
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
11 changes: 10 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,13 @@ import RemoteTrack from './room/track/RemoteTrack';
import RemoteTrackPublication from './room/track/RemoteTrackPublication';
import RemoteVideoTrack, { ElementInfo } from './room/track/RemoteVideoTrack';
import { TrackPublication } from './room/track/TrackPublication';
import { getEmptyAudioStreamTrack, getEmptyVideoStreamTrack } from './room/utils';
import {
getEmptyAudioStreamTrack,
getEmptyVideoStreamTrack,
isDeviceSupported,
supportsAdaptiveStream,
supportsDynacast,
} from './room/utils';

export * from './options';
export * from './room/errors';
Expand All @@ -30,6 +36,9 @@ export {
setLogExtension,
getEmptyAudioStreamTrack,
getEmptyVideoStreamTrack,
isDeviceSupported,
supportsAdaptiveStream,
supportsDynacast,
LogLevel,
Room,
ConnectionState,
Expand Down
2 changes: 1 addition & 1 deletion src/proto/google/protobuf/timestamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ export type DeepPartial<T> = T extends Builtin
type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & Record<Exclude<keyof I, KeysOfUnion<P>>, never>;
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function longToNumber(long: Long): number {
if (long.gt(Number.MAX_SAFE_INTEGER)) {
Expand Down
34 changes: 19 additions & 15 deletions src/proto/livekit_models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2921,25 +2921,29 @@ var globalThis: any = (() => {
throw 'Unable to locate global object';
})();

const atob: (b64: string) => string =
globalThis.atob || ((b64) => globalThis.Buffer.from(b64, 'base64').toString('binary'));
function bytesFromBase64(b64: string): Uint8Array {
const bin = atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) {
arr[i] = bin.charCodeAt(i);
if (globalThis.Buffer) {
return Uint8Array.from(globalThis.Buffer.from(b64, 'base64'));
} else {
const bin = globalThis.atob(b64);
const arr = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) {
arr[i] = bin.charCodeAt(i);
}
return arr;
}
return arr;
}

const btoa: (bin: string) => string =
globalThis.btoa || ((bin) => globalThis.Buffer.from(bin, 'binary').toString('base64'));
function base64FromBytes(arr: Uint8Array): string {
const bin: string[] = [];
arr.forEach((byte) => {
bin.push(String.fromCharCode(byte));
});
return btoa(bin.join(''));
if (globalThis.Buffer) {
return globalThis.Buffer.from(arr).toString('base64');
} else {
const bin: string[] = [];
arr.forEach((byte) => {
bin.push(String.fromCharCode(byte));
});
return globalThis.btoa(bin.join(''));
}
}

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;
Expand All @@ -2959,7 +2963,7 @@ export type DeepPartial<T> = T extends Builtin
type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & Record<Exclude<keyof I, KeysOfUnion<P>>, never>;
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function toTimestamp(date: Date): Timestamp {
const seconds = date.getTime() / 1_000;
Expand Down
2 changes: 1 addition & 1 deletion src/proto/livekit_rtc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3480,7 +3480,7 @@ export type DeepPartial<T> = T extends Builtin
type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & Record<Exclude<keyof I, KeysOfUnion<P>>, never>;
: P & { [K in keyof P]: Exact<P[K], I[K]> } & { [K in Exclude<keyof I, KeysOfUnion<P>>]: never };

function longToNumber(long: Long): number {
if (long.gt(Number.MAX_SAFE_INTEGER)) {
Expand Down
142 changes: 141 additions & 1 deletion src/room/RTCEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ import { ConnectionError, TrackInvalidError, UnexpectedConnectionState } from '.
import { EngineEvent } from './events';
import PCTransport from './PCTransport';
import { ReconnectContext, ReconnectPolicy } from './ReconnectPolicy';
import { isWeb, sleep } from './utils';
import LocalTrack from './track/LocalTrack';
import LocalVideoTrack, { SimulcastTrackInfo } from './track/LocalVideoTrack';
import { TrackPublishOptions, VideoCodec } from './track/options';
import { Track } from './track/Track';
import { isWeb, sleep, supportsAddTrack, supportsTransceiver } from './utils';

const lossyDataChannel = '_lossy';
const reliableDataChannel = '_reliable';
Expand Down Expand Up @@ -475,6 +479,142 @@ export default class RTCEngine extends (EventEmitter as new () => TypedEventEmit
}
};

private setPreferredCodec(
transceiver: RTCRtpTransceiver,
kind: Track.Kind,
videoCodec: VideoCodec,
) {
if (!('getCapabilities' in RTCRtpSender)) {
return;
}
const cap = RTCRtpSender.getCapabilities(kind);
if (!cap) return;
log.debug('get capabilities', cap);
const matched: RTCRtpCodecCapability[] = [];
const partialMatched: RTCRtpCodecCapability[] = [];
const unmatched: RTCRtpCodecCapability[] = [];
cap.codecs.forEach((c) => {
const codec = c.mimeType.toLowerCase();
if (codec === 'audio/opus') {
matched.push(c);
return;
}
const matchesVideoCodec = codec === `video/${videoCodec}`;
if (!matchesVideoCodec) {
unmatched.push(c);
return;
}
// for h264 codecs that have sdpFmtpLine available, use only if the
// profile-level-id is 42e01f for cross-browser compatibility
if (videoCodec === 'h264') {
if (c.sdpFmtpLine && c.sdpFmtpLine.includes('profile-level-id=42e01f')) {
matched.push(c);
} else {
partialMatched.push(c);
}
return;
}

matched.push(c);
});

if ('setCodecPreferences' in transceiver) {
transceiver.setCodecPreferences(matched.concat(partialMatched, unmatched));
}
}

async createSender(
track: LocalTrack,
opts: TrackPublishOptions,
encodings?: RTCRtpEncodingParameters[],
) {
if (supportsTransceiver()) {
return this.createTransceiverRTCRtpSender(track, opts, encodings);
}
if (supportsAddTrack()) {
console.log('using add-track fallback');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switch to our logger?

return this.createRTCRtpSender(track.mediaStreamTrack);
}
throw new UnexpectedConnectionState('Required webRTC APIs not supported on this device');
}

async createSimulcastSender(
track: LocalVideoTrack,
simulcastTrack: SimulcastTrackInfo,
opts: TrackPublishOptions,
encodings?: RTCRtpEncodingParameters[],
) {
// store RTCRtpSender
// @ts-ignore
if (supportsTransceiver()) {
return this.createSimulcastTransceiverSender(track, simulcastTrack, opts, encodings);
}
if (supportsAddTrack()) {
console.log('using add-track');
return this.createRTCRtpSender(track.mediaStreamTrack);
}

throw new UnexpectedConnectionState('Cannot stream on this device');
}

private async createTransceiverRTCRtpSender(
track: LocalTrack,
opts: TrackPublishOptions,
encodings?: RTCRtpEncodingParameters[],
) {
if (!this.publisher) {
throw new UnexpectedConnectionState('publisher is closed');
}

const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
if (encodings) {
transceiverInit.sendEncodings = encodings;
}
// addTransceiver for react-native is async. web is synchronous, but await won't effect it.
const transceiver = await this.publisher.pc.addTransceiver(
track.mediaStreamTrack,
transceiverInit,
);
if (track.kind === Track.Kind.Video && opts.videoCodec) {
this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
track.codec = opts.videoCodec;
}
return transceiver.sender;
}

private async createSimulcastTransceiverSender(
track: LocalVideoTrack,
simulcastTrack: SimulcastTrackInfo,
opts: TrackPublishOptions,
encodings?: RTCRtpEncodingParameters[],
) {
if (!this.publisher) {
throw new UnexpectedConnectionState('publisher is closed');
}
const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
if (encodings) {
transceiverInit.sendEncodings = encodings;
}
// addTransceiver for react-native is async. web is synchronous, but await won't effect it.
const transceiver = await this.publisher.pc.addTransceiver(
simulcastTrack.mediaStreamTrack,
transceiverInit,
);
if (!opts.videoCodec) {
return;
}
this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
track.setSimulcastTrackSender(opts.videoCodec, transceiver.sender);
return transceiver.sender;
}

private async createRTCRtpSender(track: MediaStreamTrack) {
if (!this.publisher) {
throw new UnexpectedConnectionState('publisher is closed');
}
return this.publisher.pc.addTrack(track);
}

// websocket reconnect behavior. if websocket is interrupted, and the PeerConnection
// continues to work, we can reconnect to websocket to continue the session
// after a number of retries, we'll close and give up permanently
Expand Down
67 changes: 2 additions & 65 deletions src/room/participant/LocalParticipant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import {
ScreenSharePresets,
TrackPublishOptions,
VideoCaptureOptions,
VideoCodec,
} from '../track/options';
import { Track } from '../track/Track';
import { constraintsForOptions, mergeDefaultOptions } from '../track/utils';
Expand Down Expand Up @@ -556,15 +555,6 @@ export default class LocalParticipant extends Participant {
if (encodings) {
transceiverInit.sendEncodings = encodings;
}
// addTransceiver for react-native is async. web is synchronous, but await won't effect it.
const transceiver = await this.engine.publisher.pc.addTransceiver(
track.mediaStreamTrack,
transceiverInit,
);
if (track.kind === Track.Kind.Video && opts.videoCodec) {
this.setPreferredCodec(transceiver, track.kind, opts.videoCodec);
track.codec = opts.videoCodec;
}

if (track.codec === 'av1' && encodings && encodings[0]?.maxBitrate) {
this.engine.publisher.setTrackCodecBitrate(
Expand All @@ -577,7 +567,7 @@ export default class LocalParticipant extends Participant {
this.engine.negotiate();

// store RTPSender
track.sender = transceiver.sender;
track.sender = await this.engine.createSender(track, opts, encodings);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will need to happen before this.engine.negotiate() and setTrackCodecBitrate. tho today this works because of the debounce logic that we have in place.

if (track instanceof LocalVideoTrack) {
track.startMonitor(this.engine.client);
} else if (track instanceof LocalAudioTrack) {
Expand Down Expand Up @@ -652,20 +642,11 @@ export default class LocalParticipant extends Participant {

const ti = await this.engine.addTrack(req);

if (!this.engine.publisher) {
throw new UnexpectedConnectionState('publisher is closed');
}
const transceiverInit: RTCRtpTransceiverInit = { direction: 'sendonly' };
if (encodings) {
transceiverInit.sendEncodings = encodings;
}
// addTransceiver for react-native is async. web is synchronous, but await won't effect it.
const transceiver = await this.engine.publisher.pc.addTransceiver(
simulcastTrack.mediaStreamTrack,
transceiverInit,
);
this.setPreferredCodec(transceiver, track.kind, videoCodec);
track.setSimulcastTrackSender(videoCodec, transceiver.sender);
await this.engine.createSimulcastSender(track, simulcastTrack, opts, encodings);

this.engine.negotiate();
log.debug(`published ${videoCodec} for track ${track.sid}`, { encodings, trackInfo: ti });
Expand Down Expand Up @@ -1000,50 +981,6 @@ export default class LocalParticipant extends Participant {
return publication;
}

private setPreferredCodec(
transceiver: RTCRtpTransceiver,
kind: Track.Kind,
videoCodec: VideoCodec,
) {
if (!('getCapabilities' in RTCRtpSender)) {
return;
}
const cap = RTCRtpSender.getCapabilities(kind);
if (!cap) return;
log.debug('get capabilities', cap);
const matched: RTCRtpCodecCapability[] = [];
const partialMatched: RTCRtpCodecCapability[] = [];
const unmatched: RTCRtpCodecCapability[] = [];
cap.codecs.forEach((c) => {
const codec = c.mimeType.toLowerCase();
if (codec === 'audio/opus') {
matched.push(c);
return;
}
const matchesVideoCodec = codec === `video/${videoCodec}`;
if (!matchesVideoCodec) {
unmatched.push(c);
return;
}
// for h264 codecs that have sdpFmtpLine available, use only if the
// profile-level-id is 42e01f for cross-browser compatibility
if (videoCodec === 'h264') {
if (c.sdpFmtpLine && c.sdpFmtpLine.includes('profile-level-id=42e01f')) {
matched.push(c);
} else {
partialMatched.push(c);
}
return;
}

matched.push(c);
});

if ('setCodecPreferences' in transceiver) {
transceiver.setCodecPreferences(matched.concat(partialMatched, unmatched));
}
}

/** @internal */
publishedTracksInfo(): TrackPublishedResponse[] {
const infos: TrackPublishedResponse[] = [];
Expand Down
22 changes: 22 additions & 0 deletions src/room/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,28 @@ export async function sleep(duration: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, duration));
}

/** @internal */
export function supportsTransceiver() {
return 'addTransceiver' in RTCPeerConnection;
}

/** @internal */
export function supportsAddTrack() {
return 'addTrack' in RTCPeerConnection;
}

export function supportsAdaptiveStream() {
return typeof ResizeObserver !== undefined && typeof IntersectionObserver !== undefined;
}

export function supportsDynacast() {
return supportsTransceiver();
}

export function isDeviceSupported() {
return supportsTransceiver() || supportsAddTrack();
}

export function isFireFox(): boolean {
if (!isWeb()) return false;
return navigator.userAgent.indexOf('Firefox') !== -1;
Expand Down