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); + }) + ); } }