From 5f579af13e7a28d2aa1ac13fd987b6d9a7320cd4 Mon Sep 17 00:00:00 2001 From: Jonas Schell Date: Fri, 23 Jun 2023 15:57:40 +0200 Subject: [PATCH] expose facingMode functions (#753) * expose facingMode functions * add changeset --- .changeset/selfish-planets-arrive.md | 5 ++ src/index.ts | 1 + src/room/track/facingMode.test.ts | 30 ++++++++ src/room/track/facingMode.ts | 103 +++++++++++++++++++++++++++ src/room/track/utils.test.ts | 31 +------- src/room/track/utils.ts | 102 -------------------------- 6 files changed, 140 insertions(+), 132 deletions(-) create mode 100644 .changeset/selfish-planets-arrive.md create mode 100644 src/room/track/facingMode.test.ts create mode 100644 src/room/track/facingMode.ts diff --git a/.changeset/selfish-planets-arrive.md b/.changeset/selfish-planets-arrive.md new file mode 100644 index 0000000000..4564aa3035 --- /dev/null +++ b/.changeset/selfish-planets-arrive.md @@ -0,0 +1,5 @@ +--- +"livekit-client": patch +--- + +expose facingMode functions diff --git a/src/index.ts b/src/index.ts index 21ab2ce600..696ab3c56b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -37,6 +37,7 @@ export * from './room/events'; export * from './room/track/Track'; export * from './room/track/create'; export * from './room/track/options'; +export { facingModeFromDeviceLabel, facingModeFromLocalTrack } from './room/track/facingMode'; export * from './room/track/types'; export type { DataPublishOptions, SimulationScenario } from './room/types'; export * from './version'; diff --git a/src/room/track/facingMode.test.ts b/src/room/track/facingMode.test.ts new file mode 100644 index 0000000000..cd53e25341 --- /dev/null +++ b/src/room/track/facingMode.test.ts @@ -0,0 +1,30 @@ +import { facingModeFromDeviceLabel } from './facingMode'; + +describe('Test facingMode detection', () => { + test('OBS virtual camera should be detected.', () => { + const result = facingModeFromDeviceLabel('OBS Virtual Camera'); + expect(result?.facingMode).toEqual('environment'); + expect(result?.confidence).toEqual('medium'); + }); + + test.each([ + ['Peter’s iPhone Camera', { facingMode: 'environment', confidence: 'medium' }], + ['iPhone de Théo Camera', { facingMode: 'environment', confidence: 'medium' }], + ])( + 'Device labels that contain "iphone" should return facingMode "environment".', + (label, expected) => { + const result = facingModeFromDeviceLabel(label); + expect(result?.facingMode).toEqual(expected.facingMode); + expect(result?.confidence).toEqual(expected.confidence); + }, + ); + + test.each([ + ['Peter’s iPad Camera', { facingMode: 'environment', confidence: 'medium' }], + ['iPad de Théo Camera', { facingMode: 'environment', confidence: 'medium' }], + ])('Device label that contain "ipad" should detect.', (label, expected) => { + const result = facingModeFromDeviceLabel(label); + expect(result?.facingMode).toEqual(expected.facingMode); + expect(result?.confidence).toEqual(expected.confidence); + }); +}); diff --git a/src/room/track/facingMode.ts b/src/room/track/facingMode.ts new file mode 100644 index 0000000000..f4bd282221 --- /dev/null +++ b/src/room/track/facingMode.ts @@ -0,0 +1,103 @@ +import log from 'loglevel'; +import LocalTrack from './LocalTrack'; +import type { VideoCaptureOptions } from './options'; + +type FacingMode = NonNullable; +type FacingModeFromLocalTrackOptions = { + /** + * If no facing mode can be determined, this value will be used. + * @defaultValue 'user' + */ + defaultFacingMode?: FacingMode; +}; +type FacingModeFromLocalTrackReturnValue = { + /** + * The (probable) facingMode of the track. + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/facingMode | MDN docs on facingMode} + */ + facingMode: FacingMode; + /** + * The confidence that the returned facingMode is correct. + */ + confidence: 'high' | 'medium' | 'low'; +}; + +/** + * Try to analyze the local track to determine the facing mode of a track. + * + * @remarks + * There is no property supported by all browsers to detect whether a video track originated from a user- or environment-facing camera device. + * For this reason, we use the `facingMode` property when available, but will fall back on a string-based analysis of the device label to determine the facing mode. + * If both methods fail, the default facing mode will be used. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/facingMode | MDN docs on facingMode} + * @experimental + */ +export function facingModeFromLocalTrack( + localTrack: LocalTrack | MediaStreamTrack, + options: FacingModeFromLocalTrackOptions = {}, +): FacingModeFromLocalTrackReturnValue { + const track = localTrack instanceof LocalTrack ? localTrack.mediaStreamTrack : localTrack; + const trackSettings = track.getSettings(); + let result: FacingModeFromLocalTrackReturnValue = { + facingMode: options.defaultFacingMode ?? 'user', + confidence: 'low', + }; + + // 1. Try to get facingMode from track settings. + if ('facingMode' in trackSettings) { + const rawFacingMode = trackSettings.facingMode; + log.debug('rawFacingMode', { rawFacingMode }); + if (rawFacingMode && typeof rawFacingMode === 'string' && isFacingModeValue(rawFacingMode)) { + result = { facingMode: rawFacingMode, confidence: 'high' }; + } + } + + // 2. If we don't have a high confidence we try to get the facing mode from the device label. + if (['low', 'medium'].includes(result.confidence)) { + log.debug(`Try to get facing mode from device label: (${track.label})`); + const labelAnalysisResult = facingModeFromDeviceLabel(track.label); + if (labelAnalysisResult !== undefined) { + result = labelAnalysisResult; + } + } + + return result; +} + +const knownDeviceLabels = new Map([ + ['obs virtual camera', { facingMode: 'environment', confidence: 'medium' }], +]); +const knownDeviceLabelSections = new Map([ + ['iphone', { facingMode: 'environment', confidence: 'medium' }], + ['ipad', { facingMode: 'environment', confidence: 'medium' }], +]); +/** + * Attempt to analyze the device label to determine the facing mode. + * + * @experimental + */ +export function facingModeFromDeviceLabel( + deviceLabel: string, +): FacingModeFromLocalTrackReturnValue | undefined { + const label = deviceLabel.trim().toLowerCase(); + // Empty string is a valid device label but we can't infer anything from it. + if (label === '') { + return undefined; + } + + // Can we match against widely known device labels. + if (knownDeviceLabels.has(label)) { + return knownDeviceLabels.get(label); + } + + // Can we match against sections of the device label. + return Array.from(knownDeviceLabelSections.entries()).find(([section]) => + label.includes(section), + )?.[1]; +} + +function isFacingModeValue(item: string): item is FacingMode { + const allowedValues: FacingMode[] = ['user', 'environment', 'left', 'right']; + return item === undefined || allowedValues.includes(item as FacingMode); +} diff --git a/src/room/track/utils.test.ts b/src/room/track/utils.test.ts index 54bf1e4ca2..0251306b17 100644 --- a/src/room/track/utils.test.ts +++ b/src/room/track/utils.test.ts @@ -1,5 +1,5 @@ import { AudioCaptureOptions, VideoCaptureOptions, VideoPresets } from './options'; -import { constraintsForOptions, facingModeFromDeviceLabel, mergeDefaultOptions } from './utils'; +import { constraintsForOptions, mergeDefaultOptions } from './utils'; describe('mergeDefaultOptions', () => { const audioDefaults: AudioCaptureOptions = { @@ -108,32 +108,3 @@ describe('constraintsForOptions', () => { expect(videoOpts.aspectRatio).toEqual(VideoPresets.h720.resolution.aspectRatio); }); }); - -describe('Test facingMode detection', () => { - test('OBS virtual camera should be detected.', () => { - const result = facingModeFromDeviceLabel('OBS Virtual Camera'); - expect(result?.facingMode).toEqual('environment'); - expect(result?.confidence).toEqual('medium'); - }); - - test.each([ - ['Peter’s iPhone Camera', { facingMode: 'environment', confidence: 'medium' }], - ['iPhone de Théo Camera', { facingMode: 'environment', confidence: 'medium' }], - ])( - 'Device labels that contain "iphone" should return facingMode "environment".', - (label, expected) => { - const result = facingModeFromDeviceLabel(label); - expect(result?.facingMode).toEqual(expected.facingMode); - expect(result?.confidence).toEqual(expected.confidence); - }, - ); - - test.each([ - ['Peter’s iPad Camera', { facingMode: 'environment', confidence: 'medium' }], - ['iPad de Théo Camera', { facingMode: 'environment', confidence: 'medium' }], - ])('Device label that contain "ipad" should detect.', (label, expected) => { - const result = facingModeFromDeviceLabel(label); - expect(result?.facingMode).toEqual(expected.facingMode); - expect(result?.confidence).toEqual(expected.confidence); - }); -}); diff --git a/src/room/track/utils.ts b/src/room/track/utils.ts index e7a4613c37..0e03db2927 100644 --- a/src/room/track/utils.ts +++ b/src/room/track/utils.ts @@ -1,6 +1,4 @@ import { sleep } from '../utils'; -import log from './../../logger'; -import LocalTrack from './LocalTrack'; import type { AudioCaptureOptions, CreateLocalTracksOptions, VideoCaptureOptions } from './options'; import type { AudioTrack } from './types'; @@ -114,103 +112,3 @@ export function getNewAudioContext(): AudioContext | void { return new AudioContext({ latencyHint: 'interactive' }); } } - -type FacingMode = NonNullable; -type FacingModeFromLocalTrackOptions = { - /** - * If no facing mode can be determined, this value will be used. - * @defaultValue 'user' - */ - defaultFacingMode?: FacingMode; -}; -type FacingModeFromLocalTrackReturnValue = { - /** - * The (probable) facingMode of the track. - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/facingMode | MDN docs on facingMode} - */ - facingMode: FacingMode; - /** - * The confidence that the returned facingMode is correct. - */ - confidence: 'high' | 'medium' | 'low'; -}; - -/** - * Try to analyze the local track to determine the facing mode of a track. - * - * @remarks - * There is no property supported by all browsers to detect whether a video track originated from a user- or environment-facing camera device. - * For this reason, we use the `facingMode` property when available, but will fall back on a string-based analysis of the device label to determine the facing mode. - * If both methods fail, the default facing mode will be used. - * - * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/facingMode | MDN docs on facingMode} - * @experimental - */ -export function facingModeFromLocalTrack( - localTrack: LocalTrack | MediaStreamTrack, - options: FacingModeFromLocalTrackOptions = {}, -): FacingModeFromLocalTrackReturnValue { - const track = localTrack instanceof LocalTrack ? localTrack.mediaStreamTrack : localTrack; - const trackSettings = track.getSettings(); - let result: FacingModeFromLocalTrackReturnValue = { - facingMode: options.defaultFacingMode ?? 'user', - confidence: 'low', - }; - - // 1. Try to get facingMode from track settings. - if ('facingMode' in trackSettings) { - const rawFacingMode = trackSettings.facingMode; - log.debug('rawFacingMode', { rawFacingMode }); - if (rawFacingMode && typeof rawFacingMode === 'string' && isFacingModeValue(rawFacingMode)) { - result = { facingMode: rawFacingMode, confidence: 'high' }; - } - } - - // 2. If we don't have a high confidence we try to get the facing mode from the device label. - if (['low', 'medium'].includes(result.confidence)) { - log.debug(`Try to get facing mode from device label: (${track.label})`); - const labelAnalysisResult = facingModeFromDeviceLabel(track.label); - if (labelAnalysisResult !== undefined) { - result = labelAnalysisResult; - } - } - - return result; -} - -const knownDeviceLabels = new Map([ - ['obs virtual camera', { facingMode: 'environment', confidence: 'medium' }], -]); -const knownDeviceLabelSections = new Map([ - ['iphone', { facingMode: 'environment', confidence: 'medium' }], - ['ipad', { facingMode: 'environment', confidence: 'medium' }], -]); -/** - * Attempt to analyze the device label to determine the facing mode. - * - * @experimental - */ -export function facingModeFromDeviceLabel( - deviceLabel: string, -): FacingModeFromLocalTrackReturnValue | undefined { - const label = deviceLabel.trim().toLowerCase(); - // Empty string is a valid device label but we can't infer anything from it. - if (label === '') { - return undefined; - } - - // Can we match against widely known device labels. - if (knownDeviceLabels.has(label)) { - return knownDeviceLabels.get(label); - } - - // Can we match against sections of the device label. - return Array.from(knownDeviceLabelSections.entries()).find(([section]) => - label.includes(section), - )?.[1]; -} - -function isFacingModeValue(item: string): item is FacingMode { - const allowedValues: FacingMode[] = ['user', 'environment', 'left', 'right']; - return item === undefined || allowedValues.includes(item as FacingMode); -}