UNPKG

@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

266 lines (239 loc) • 8.42 kB
import { HMSVideoTrack } from './HMSVideoTrack'; import { VideoElementManager } from './VideoElementManager'; import { VideoTrackLayerUpdate } from '../../connection/channel-messages'; import { HMSPreferredSimulcastLayer, HMSSimulcastLayer, HMSSimulcastLayerDefinition, } from '../../interfaces/simulcast-layers'; import { MAINTAIN_TRACK_HISTORY } from '../../utils/constants'; import HMSLogger from '../../utils/logger'; import { isEmptyTrack } from '../../utils/track'; import { HMSRemoteStream } from '../streams'; export class HMSRemoteVideoTrack extends HMSVideoTrack { private _degraded = false; private _degradedAt: Date | null = null; private _layerDefinitions: HMSSimulcastLayerDefinition[] = []; private history = new TrackHistory(); private preferredLayer: HMSPreferredSimulcastLayer = HMSSimulcastLayer.HIGH; private bizTrackId!: string; private disableNoneLayerRequest = false; constructor(stream: HMSRemoteStream, track: MediaStreamTrack, source?: string, disableNoneLayerRequest?: boolean) { super(stream, track, source); this.disableNoneLayerRequest = !!disableNoneLayerRequest; this.setVideoHandler(new VideoElementManager(this)); } setTrackId(trackId: string) { this.bizTrackId = trackId; } get trackId(): string { return this.bizTrackId || super.trackId; } public get degraded() { return this._degraded; } public get degradedAt() { return this._degradedAt; } async setEnabled(value: boolean): Promise<void> { if (value === this.enabled) { return; } super.setEnabled(value); this.videoHandler.updateSinks(true); } async setPreferredLayer(layer: HMSPreferredSimulcastLayer) { //@ts-ignore if (layer === HMSSimulcastLayer.NONE) { HMSLogger.w(`layer ${HMSSimulcastLayer.NONE} will be ignored`); return; } this.preferredLayer = layer; if (!this.shouldSendVideoLayer(layer, 'preferLayer')) { return; } if (!this.hasSinks()) { HMSLogger.d( `[Remote Track] ${this.logIdentifier} streamId=${this.stream.id} trackId=${this.trackId} saving ${layer}, source=${this.source} Track does not have any sink`, ); return; } await this.requestLayer(layer, 'preferLayer'); this.pushInHistory(`uiPreferLayer-${layer}`); } /** * @deprecated * @returns {HMSSimulcastLayer} */ getSimulcastLayer() { return (this.stream as HMSRemoteStream).getSimulcastLayer(); } getLayer() { return (this.stream as HMSRemoteStream).getVideoLayer(); } getPreferredLayer() { return this.preferredLayer; } getPreferredLayerDefinition() { return this._layerDefinitions.find(layer => layer.layer === this.preferredLayer); } replaceTrack(track: HMSRemoteVideoTrack) { this.nativeTrack = track.nativeTrack; if (track.transceiver) { this.transceiver = track.transceiver; // replace dummy streamId with actual streamId retaining all other properties this.stream.updateId(track.stream.id); } this.videoHandler.updateSinks(); } async addSink(videoElement: HTMLVideoElement, shouldSendVideoLayer = true) { // if the native track is empty track, just request the preferred layer else attach it if (isEmptyTrack(this.nativeTrack)) { await this.requestLayer(this.preferredLayer, 'addSink'); } else { super.addSink(videoElement); if (shouldSendVideoLayer) { await this.updateLayer('addSink'); } } this.pushInHistory(`uiSetLayer-high`); } async removeSink(videoElement: HTMLVideoElement, shouldSendVideoLayer = true) { super.removeSink(videoElement); if (shouldSendVideoLayer) { await this.updateLayer('removeSink'); } this._degraded = false; this.pushInHistory('uiSetLayer-none'); } /** * Method to get available simulcast definitions for the track * @returns {HMSSimulcastLayerDefinition[]} */ getSimulcastDefinitions() { // send a clone to store as it will freeze the object from further updates return [...this._layerDefinitions]; } /** @internal */ setSimulcastDefinitons(definitions: HMSSimulcastLayerDefinition[]) { this._layerDefinitions = definitions; } /** * @internal * SFU will change track's layer(degrade or restore) and tell the sdk to update * it locally. * @returns {boolean} isDegraded - returns true if degraded * */ setLayerFromServer(layerUpdate: VideoTrackLayerUpdate) { this._degraded = this.getDegradationValue(layerUpdate); this._degradedAt = this._degraded ? new Date() : this._degradedAt; const currentLayer = layerUpdate.current_layer; HMSLogger.d( `[Remote Track] ${this.logIdentifier} streamId=${this.stream.id} trackId=${this.trackId} layer update from sfu currLayer=${layerUpdate.current_layer} preferredLayer=${layerUpdate.expected_layer} sub_degraded=${layerUpdate.subscriber_degraded} pub_degraded=${layerUpdate.publisher_degraded} isDegraded=${this._degraded}`, ); // No need to send preferLayer update, as server has done it already (this.stream as HMSRemoteStream).setVideoLayerLocally(currentLayer, this.logIdentifier, 'setLayerFromServer'); this.pushInHistory(`sfuLayerUpdate-${currentLayer}`); return this._degraded; } private getDegradationValue(layerUpdate: VideoTrackLayerUpdate) { return ( this.enabled && (layerUpdate.publisher_degraded || layerUpdate.subscriber_degraded) && layerUpdate.current_layer === HMSSimulcastLayer.NONE ); } private async updateLayer(source: string) { let newLayer: HMSSimulcastLayer = this.preferredLayer; if (this.enabled && this.hasSinks()) { newLayer = this.preferredLayer; // send none only when the flag is not set } else if (!this.disableNoneLayerRequest) { newLayer = HMSSimulcastLayer.NONE; } if (!this.shouldSendVideoLayer(newLayer, source)) { return; } await this.requestLayer(newLayer, source); } private pushInHistory(action: string) { if (MAINTAIN_TRACK_HISTORY) { this.history.push({ name: action, layer: this.getLayer(), degraded: this.degraded }); } } private async requestLayer(layer: HMSSimulcastLayer, source: string) { try { const response = await (this.stream as HMSRemoteStream).setVideoLayer( layer, this.trackId, this.logIdentifier, source, ); HMSLogger.d( `[Remote Track] ${this.logIdentifier} streamId=${this.stream.id} trackId=${this.trackId} Requested layer ${layer}, source=${source}`, ); return response; } catch (error) { HMSLogger.d( `[Remote Track] ${this.logIdentifier} streamId=${this.stream.id} trackId=${this.trackId} Failed to set layer ${layer}, source=${source} error=${(error as Error).message}`, ); throw error; } } /** * given the new layer, figure out if the update should be sent to server or not. * It won't be sent if the track is already on the targetLayer. If the track is * degraded though and the target layer is none, update will be sent. * If there are tracks degraded on a page and user paginates away to other page, * it's necessary to send the layer none message to SFU so it knows that the app * is no longer interested in the track and doesn't recover degraded tracks on non * visible pages. * * TODO: if track is degraded, send the update if target layer is lower than current layer * @private */ private shouldSendVideoLayer(targetLayer: HMSSimulcastLayer, source: string) { const currLayer = this.getLayer(); if (this.degraded && targetLayer === HMSSimulcastLayer.NONE) { return true; } if (currLayer === targetLayer) { HMSLogger.d( `[Remote Track] ${this.logIdentifier}`, `Not sending update, already on layer ${targetLayer}, source=${source}`, ); return false; } return true; } } /** * to store history of everything that happened to a remote track which decides * it's current layer and degraded status. */ class TrackHistory { history: Record<string, any>[] = []; push(action: Record<string, any>) { action.time = new Date().toISOString().split('T')[1]; this.history.push(action); } }