From 2cd134d0c99ef4341e93b36828baaa3c0a0bf5d5 Mon Sep 17 00:00:00 2001 From: Nikola Novakovic Date: Wed, 12 May 2021 11:24:18 -1000 Subject: [PATCH] Export Layer and allow updating params per layer (#175) * Export Layer type * Allow tweaking encoding params per layer by using optional layer param. This can be used to disable certain layer (for example keep high layer disabled and enable it only for main speaker) * Adding support for active layer channel message Support legacy channel message --- src/client.ts | 42 ++++++++++++++++++++++++++++++++++++--- src/index.ts | 4 ++-- src/stream.ts | 55 +++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 83 insertions(+), 18 deletions(-) diff --git a/src/client.ts b/src/client.ts index 6f3b60d3..ef099202 100644 --- a/src/client.ts +++ b/src/client.ts @@ -18,6 +18,12 @@ export interface Trickle { target: Role; } +export interface ActiveLayer { + streamId: string, + activeLayer: string, + availableLayers: string[] +} + enum Role { pub = 0, sub = 1, @@ -73,6 +79,7 @@ export default class Client { offer?: RTCSessionDescriptionInit, answer?: RTCSessionDescriptionInit, ) => void; + onactivelayer?: (al: ActiveLayer) => void; constructor( signal: Signal, @@ -111,9 +118,14 @@ export default class Client { this.transports![Role.sub].pc.ondatachannel = (ev: RTCDataChannelEvent) => { if (ev.channel.label === API_CHANNEL) { this.transports![Role.sub].api = ev.channel; + this.transports![Role.pub].api = ev.channel; ev.channel.onmessage = (e) => { - if (this.onspeaker) { - this.onspeaker(JSON.parse(e.data)); + try { + const msg = JSON.parse(e.data); + this.processChannelMessage(msg); + } catch (err) { + /* tslint:disable-next-line:no-console */ + console.error(err); } }; resolve(); @@ -162,7 +174,7 @@ export default class Client { if (!this.transports) { throw Error(ERR_NO_SESSION); } - stream.publish(this.transports[Role.pub].pc); + stream.publish(this.transports[Role.pub]); } createDataChannel(label: string) { @@ -228,4 +240,28 @@ export default class Client { if (this.onerrnegotiate) this.onerrnegotiate(Role.pub, err, offer, answer); } } + + private processChannelMessage(msg: any) { + if (msg.method !== undefined && msg.params !== undefined) { + switch (msg.method) { + case "audioLevels": + if (this.onspeaker) { + this.onspeaker(msg.params); + } + break; + case "activeLayer": + if (this.onactivelayer) { + this.onactivelayer(msg.params); + } + break; + default: + // do nothing + } + } else { + // legacy channel message - payload contains audio levels + if (this.onspeaker) { + this.onspeaker(msg) + } + } + } } diff --git a/src/index.ts b/src/index.ts index aab81f79..7f0c332a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ import Client from './client'; -import { LocalStream, RemoteStream, Constraints } from './stream'; +import { LocalStream, RemoteStream, Constraints, Layer } from './stream'; import { Signal, Trickle } from './signal'; -export { Client, LocalStream, RemoteStream, Constraints, Signal, Trickle }; \ No newline at end of file +export { Client, LocalStream, RemoteStream, Constraints, Signal, Trickle, Layer }; diff --git a/src/stream.ts b/src/stream.ts index 30808bc6..17e8a77b 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -96,7 +96,7 @@ export const VideoConstraints: VideoConstraints = { }, }; -type Layer = 'low' | 'medium' | 'high'; +export type Layer = 'low' | 'medium' | 'high'; export interface Encoding { layer: Layer; @@ -158,6 +158,7 @@ export class LocalStream extends MediaStream { constraints: Constraints; pc?: RTCPeerConnection; + api?: RTCDataChannel; constructor(stream: MediaStream, constraints: Constraints) { super(stream); @@ -229,7 +230,6 @@ export class LocalStream extends MediaStream { maxFramerate: VideoConstraints[resolutions[idx - 2]].encodings.maxFramerate, }); } - const transceiver = this.pc.addTransceiver(track, { streams: [this], direction: 'sendonly', @@ -327,8 +327,9 @@ export class LocalStream extends MediaStream { return stream.getVideoTracks()[0]; } - publish(pc: RTCPeerConnection) { - this.pc = pc; + publish(transport: Transport) { + this.pc = transport.pc; + this.api = transport.api; this.getTracks().forEach(this.publishTrack.bind(this)); } @@ -383,22 +384,50 @@ export class LocalStream extends MediaStream { this.updateTrack(track, prev); } - updateMediaEncodingParams(encodingParams: RTCRtpEncodingParameters) { + async enableLayers(layers: Layer[]) { + const call = { + streamId: this.id, + layers + }; + const callStr = JSON.stringify(call); + + if (this.api) { + if (this.api.readyState !== 'open') { + // queue call if we aren't open yet + this.api.onopen = () => this.api?.send(JSON.stringify(call)); + } else { + this.api.send(JSON.stringify(call)); + } + } + const layerValues = ['high', 'medium', 'low'] as const; + await Promise.all(layerValues.map(async (layer) => { + await this.updateMediaEncodingParams({active: layers.includes(layer)}, layer) + })); + } + + async updateMediaEncodingParams(encodingParams: RTCRtpEncodingParameters, layer?: Layer) { if (!this.pc) return; - this.getTracks().forEach((track) => { - const senders = this.pc?.getSenders()?.filter((sender) => track.id === sender.track?.id); - senders?.forEach((sender) => { + const tracks = this.getTracks(); + await Promise.all(this.pc?.getSenders() + .filter(sender => sender.track && tracks.includes(sender.track)) + .map(async (sender) => { const params = sender.getParameters(); if (!params.encodings) { params.encodings = [{}]; } - params.encodings[0] = { - ...params.encodings[0], + let idx = 0; + if (this.constraints.simulcast && layer) { + const rid = layer === 'high' ? 'f' : layer === 'medium' ? 'h' : 'q'; + idx = params.encodings.findIndex(encoding => encoding.rid === rid); + if (params.encodings.length < idx + 1) return; + } + params.encodings[idx] = { + ...params.encodings[idx], ...encodingParams, }; - sender.setParameters(params); - }); - }); + await sender.setParameters(params); + }) + ); } }