livekit-client
Version:
JavaScript/TypeScript client SDK for LiveKit
383 lines (335 loc) • 11.8 kB
text/typescript
import {
ParticipantTracks,
SubscriptionError,
TrackInfo,
UpdateSubscription,
UpdateTrackSettings,
} from '@livekit/protocol';
import { TrackEvent } from '../events';
import type { LoggerOptions } from '../types';
import { isRemoteVideoTrack } from '../utils';
import type RemoteTrack from './RemoteTrack';
import { Track, VideoQuality } from './Track';
import { TrackPublication } from './TrackPublication';
import { areDimensionsSmaller, layerDimensionsFor } from './utils';
export default class RemoteTrackPublication extends TrackPublication {
track?: RemoteTrack = undefined;
/** @internal */
protected allowed = true;
// keeps track of client's desire to subscribe to a track, also true if autoSubscribe is active
protected subscribed?: boolean;
protected requestedDisabled: boolean | undefined = undefined;
protected visible: boolean = true;
protected videoDimensionsAdaptiveStream?: Track.Dimensions;
protected requestedVideoDimensions?: Track.Dimensions;
protected requestedMaxQuality?: VideoQuality;
protected fps?: number;
protected subscriptionError?: SubscriptionError;
constructor(
kind: Track.Kind,
ti: TrackInfo,
autoSubscribe: boolean | undefined,
loggerOptions?: LoggerOptions,
) {
super(kind, ti.sid, ti.name, loggerOptions);
this.subscribed = autoSubscribe;
this.updateInfo(ti);
}
/**
* Subscribe or unsubscribe to this remote track
* @param subscribed true to subscribe to a track, false to unsubscribe
*/
setSubscribed(subscribed: boolean) {
const prevStatus = this.subscriptionStatus;
const prevPermission = this.permissionStatus;
this.subscribed = subscribed;
// reset allowed status when desired subscription state changes
// server will notify client via signal message if it's not allowed
if (subscribed) {
this.allowed = true;
}
const sub = new UpdateSubscription({
trackSids: [this.trackSid],
subscribe: this.subscribed,
participantTracks: [
new ParticipantTracks({
// sending an empty participant id since TrackPublication doesn't keep it
// this is filled in by the participant that receives this message
participantSid: '',
trackSids: [this.trackSid],
}),
],
});
this.emit(TrackEvent.UpdateSubscription, sub);
this.emitSubscriptionUpdateIfChanged(prevStatus);
this.emitPermissionUpdateIfChanged(prevPermission);
}
get subscriptionStatus(): TrackPublication.SubscriptionStatus {
if (this.subscribed === false) {
return TrackPublication.SubscriptionStatus.Unsubscribed;
}
if (!super.isSubscribed) {
return TrackPublication.SubscriptionStatus.Desired;
}
return TrackPublication.SubscriptionStatus.Subscribed;
}
get permissionStatus(): TrackPublication.PermissionStatus {
return this.allowed
? TrackPublication.PermissionStatus.Allowed
: TrackPublication.PermissionStatus.NotAllowed;
}
/**
* Returns true if track is subscribed, and ready for playback
*/
get isSubscribed(): boolean {
if (this.subscribed === false) {
return false;
}
return super.isSubscribed;
}
// returns client's desire to subscribe to a track, also true if autoSubscribe is enabled
get isDesired(): boolean {
return this.subscribed !== false;
}
get isEnabled(): boolean {
return this.requestedDisabled !== undefined
? !this.requestedDisabled
: this.isAdaptiveStream
? this.visible
: true;
}
get isLocal() {
return false;
}
/**
* disable server from sending down data for this track. this is useful when
* the participant is off screen, you may disable streaming down their video
* to reduce bandwidth requirements
* @param enabled
*/
setEnabled(enabled: boolean) {
if (!this.isManualOperationAllowed() || this.requestedDisabled === !enabled) {
return;
}
this.requestedDisabled = !enabled;
this.emitTrackUpdate();
}
/**
* for tracks that support simulcasting, adjust subscribed quality
*
* This indicates the highest quality the client can accept. if network
* bandwidth does not allow, server will automatically reduce quality to
* optimize for uninterrupted video
*/
setVideoQuality(quality: VideoQuality) {
if (!this.isManualOperationAllowed() || this.requestedMaxQuality === quality) {
return;
}
this.requestedMaxQuality = quality;
this.requestedVideoDimensions = undefined;
this.emitTrackUpdate();
}
/**
* Explicitly set the video dimensions for this track.
*
* This will take precedence over adaptive stream dimensions.
*
* @param dimensions The video dimensions to set.
*/
setVideoDimensions(dimensions: Track.Dimensions) {
if (!this.isManualOperationAllowed()) {
return;
}
if (
this.requestedVideoDimensions?.width === dimensions.width &&
this.requestedVideoDimensions?.height === dimensions.height
) {
return;
}
if (isRemoteVideoTrack(this.track)) {
this.requestedVideoDimensions = dimensions;
}
this.requestedMaxQuality = undefined;
this.emitTrackUpdate();
}
setVideoFPS(fps: number) {
if (!this.isManualOperationAllowed()) {
return;
}
if (!isRemoteVideoTrack(this.track)) {
return;
}
if (this.fps === fps) {
return;
}
this.fps = fps;
this.emitTrackUpdate();
}
get videoQuality(): VideoQuality | undefined {
return this.requestedMaxQuality ?? VideoQuality.HIGH;
}
/** @internal */
setTrack(track?: RemoteTrack) {
const prevStatus = this.subscriptionStatus;
const prevPermission = this.permissionStatus;
const prevTrack = this.track;
if (prevTrack === track) {
return;
}
if (prevTrack) {
// unregister listener
prevTrack.off(TrackEvent.VideoDimensionsChanged, this.handleVideoDimensionsChange);
prevTrack.off(TrackEvent.VisibilityChanged, this.handleVisibilityChange);
prevTrack.off(TrackEvent.Ended, this.handleEnded);
prevTrack.detach();
prevTrack.stopMonitor();
this.emit(TrackEvent.Unsubscribed, prevTrack);
}
super.setTrack(track);
if (track) {
track.sid = this.trackSid;
track.on(TrackEvent.VideoDimensionsChanged, this.handleVideoDimensionsChange);
track.on(TrackEvent.VisibilityChanged, this.handleVisibilityChange);
track.on(TrackEvent.Ended, this.handleEnded);
this.emit(TrackEvent.Subscribed, track);
}
this.emitPermissionUpdateIfChanged(prevPermission);
this.emitSubscriptionUpdateIfChanged(prevStatus);
}
/** @internal */
setAllowed(allowed: boolean) {
const prevStatus = this.subscriptionStatus;
const prevPermission = this.permissionStatus;
this.allowed = allowed;
this.emitPermissionUpdateIfChanged(prevPermission);
this.emitSubscriptionUpdateIfChanged(prevStatus);
}
/** @internal */
setSubscriptionError(error: SubscriptionError) {
this.emit(TrackEvent.SubscriptionFailed, error);
}
/** @internal */
updateInfo(info: TrackInfo) {
super.updateInfo(info);
const prevMetadataMuted = this.metadataMuted;
this.metadataMuted = info.muted;
if (this.track) {
this.track.setMuted(info.muted);
} else if (prevMetadataMuted !== info.muted) {
this.emit(info.muted ? TrackEvent.Muted : TrackEvent.Unmuted);
}
}
private emitSubscriptionUpdateIfChanged(previousStatus: TrackPublication.SubscriptionStatus) {
const currentStatus = this.subscriptionStatus;
if (previousStatus === currentStatus) {
return;
}
this.emit(TrackEvent.SubscriptionStatusChanged, currentStatus, previousStatus);
}
private emitPermissionUpdateIfChanged(
previousPermissionStatus: TrackPublication.PermissionStatus,
) {
const currentPermissionStatus = this.permissionStatus;
if (currentPermissionStatus !== previousPermissionStatus) {
this.emit(
TrackEvent.SubscriptionPermissionChanged,
this.permissionStatus,
previousPermissionStatus,
);
}
}
private isManualOperationAllowed(): boolean {
if (!this.isDesired) {
this.log.warn('cannot update track settings when not subscribed', this.logContext);
return false;
}
return true;
}
protected handleEnded = (track: RemoteTrack) => {
this.setTrack(undefined);
this.emit(TrackEvent.Ended, track);
};
protected get isAdaptiveStream(): boolean {
return isRemoteVideoTrack(this.track) && this.track.isAdaptiveStream;
}
protected handleVisibilityChange = (visible: boolean) => {
this.log.debug(
`adaptivestream video visibility ${this.trackSid}, visible=${visible}`,
this.logContext,
);
this.visible = visible;
this.emitTrackUpdate();
};
protected handleVideoDimensionsChange = (dimensions: Track.Dimensions) => {
this.log.debug(
`adaptivestream video dimensions ${dimensions.width}x${dimensions.height}`,
this.logContext,
);
this.videoDimensionsAdaptiveStream = dimensions;
this.emitTrackUpdate();
};
/* @internal */
emitTrackUpdate() {
const settings: UpdateTrackSettings = new UpdateTrackSettings({
trackSids: [this.trackSid],
disabled: !this.isEnabled,
fps: this.fps,
});
if (this.kind === Track.Kind.Video) {
let minDimensions = this.requestedVideoDimensions;
if (this.videoDimensionsAdaptiveStream !== undefined) {
if (minDimensions) {
// check whether the adaptive stream dimensions are smaller than the requested dimensions and use smaller one
const smallerAdaptive = areDimensionsSmaller(
this.videoDimensionsAdaptiveStream,
minDimensions,
);
if (smallerAdaptive) {
this.log.debug('using adaptive stream dimensions instead of requested', {
...this.logContext,
...this.videoDimensionsAdaptiveStream,
});
minDimensions = this.videoDimensionsAdaptiveStream;
}
} else if (this.requestedMaxQuality !== undefined && this.trackInfo) {
// check whether adaptive stream dimensions are smaller than the max quality layer and use smaller one
const maxQualityLayer = layerDimensionsFor(this.trackInfo, this.requestedMaxQuality);
if (
maxQualityLayer &&
areDimensionsSmaller(this.videoDimensionsAdaptiveStream, maxQualityLayer)
) {
this.log.debug('using adaptive stream dimensions instead of max quality layer', {
...this.logContext,
...this.videoDimensionsAdaptiveStream,
});
minDimensions = this.videoDimensionsAdaptiveStream;
}
} else {
this.log.debug('using adaptive stream dimensions', {
...this.logContext,
...this.videoDimensionsAdaptiveStream,
});
minDimensions = this.videoDimensionsAdaptiveStream;
}
}
if (minDimensions) {
settings.width = Math.ceil(minDimensions.width);
settings.height = Math.ceil(minDimensions.height);
} else if (this.requestedMaxQuality !== undefined) {
this.log.debug('using requested max quality', {
...this.logContext,
quality: this.requestedMaxQuality,
});
settings.quality = this.requestedMaxQuality;
} else {
this.log.debug('using default quality', {
...this.logContext,
quality: VideoQuality.HIGH,
});
// defaults to high quality
settings.quality = VideoQuality.HIGH;
}
}
this.emit(TrackEvent.UpdateSettings, settings);
}
}