Skip to content

Commit 5f579af

Browse files
authored
expose facingMode functions (livekit#753)
* expose facingMode functions * add changeset
1 parent 3efaf9a commit 5f579af

File tree

6 files changed

+140
-132
lines changed

6 files changed

+140
-132
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"livekit-client": patch
3+
---
4+
5+
expose facingMode functions

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export * from './room/events';
3737
export * from './room/track/Track';
3838
export * from './room/track/create';
3939
export * from './room/track/options';
40+
export { facingModeFromDeviceLabel, facingModeFromLocalTrack } from './room/track/facingMode';
4041
export * from './room/track/types';
4142
export type { DataPublishOptions, SimulationScenario } from './room/types';
4243
export * from './version';

src/room/track/facingMode.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { facingModeFromDeviceLabel } from './facingMode';
2+
3+
describe('Test facingMode detection', () => {
4+
test('OBS virtual camera should be detected.', () => {
5+
const result = facingModeFromDeviceLabel('OBS Virtual Camera');
6+
expect(result?.facingMode).toEqual('environment');
7+
expect(result?.confidence).toEqual('medium');
8+
});
9+
10+
test.each([
11+
['Peter’s iPhone Camera', { facingMode: 'environment', confidence: 'medium' }],
12+
['iPhone de Théo Camera', { facingMode: 'environment', confidence: 'medium' }],
13+
])(
14+
'Device labels that contain "iphone" should return facingMode "environment".',
15+
(label, expected) => {
16+
const result = facingModeFromDeviceLabel(label);
17+
expect(result?.facingMode).toEqual(expected.facingMode);
18+
expect(result?.confidence).toEqual(expected.confidence);
19+
},
20+
);
21+
22+
test.each([
23+
['Peter’s iPad Camera', { facingMode: 'environment', confidence: 'medium' }],
24+
['iPad de Théo Camera', { facingMode: 'environment', confidence: 'medium' }],
25+
])('Device label that contain "ipad" should detect.', (label, expected) => {
26+
const result = facingModeFromDeviceLabel(label);
27+
expect(result?.facingMode).toEqual(expected.facingMode);
28+
expect(result?.confidence).toEqual(expected.confidence);
29+
});
30+
});

src/room/track/facingMode.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import log from 'loglevel';
2+
import LocalTrack from './LocalTrack';
3+
import type { VideoCaptureOptions } from './options';
4+
5+
type FacingMode = NonNullable<VideoCaptureOptions['facingMode']>;
6+
type FacingModeFromLocalTrackOptions = {
7+
/**
8+
* If no facing mode can be determined, this value will be used.
9+
* @defaultValue 'user'
10+
*/
11+
defaultFacingMode?: FacingMode;
12+
};
13+
type FacingModeFromLocalTrackReturnValue = {
14+
/**
15+
* The (probable) facingMode of the track.
16+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/facingMode | MDN docs on facingMode}
17+
*/
18+
facingMode: FacingMode;
19+
/**
20+
* The confidence that the returned facingMode is correct.
21+
*/
22+
confidence: 'high' | 'medium' | 'low';
23+
};
24+
25+
/**
26+
* Try to analyze the local track to determine the facing mode of a track.
27+
*
28+
* @remarks
29+
* There is no property supported by all browsers to detect whether a video track originated from a user- or environment-facing camera device.
30+
* 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.
31+
* If both methods fail, the default facing mode will be used.
32+
*
33+
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/facingMode | MDN docs on facingMode}
34+
* @experimental
35+
*/
36+
export function facingModeFromLocalTrack(
37+
localTrack: LocalTrack | MediaStreamTrack,
38+
options: FacingModeFromLocalTrackOptions = {},
39+
): FacingModeFromLocalTrackReturnValue {
40+
const track = localTrack instanceof LocalTrack ? localTrack.mediaStreamTrack : localTrack;
41+
const trackSettings = track.getSettings();
42+
let result: FacingModeFromLocalTrackReturnValue = {
43+
facingMode: options.defaultFacingMode ?? 'user',
44+
confidence: 'low',
45+
};
46+
47+
// 1. Try to get facingMode from track settings.
48+
if ('facingMode' in trackSettings) {
49+
const rawFacingMode = trackSettings.facingMode;
50+
log.debug('rawFacingMode', { rawFacingMode });
51+
if (rawFacingMode && typeof rawFacingMode === 'string' && isFacingModeValue(rawFacingMode)) {
52+
result = { facingMode: rawFacingMode, confidence: 'high' };
53+
}
54+
}
55+
56+
// 2. If we don't have a high confidence we try to get the facing mode from the device label.
57+
if (['low', 'medium'].includes(result.confidence)) {
58+
log.debug(`Try to get facing mode from device label: (${track.label})`);
59+
const labelAnalysisResult = facingModeFromDeviceLabel(track.label);
60+
if (labelAnalysisResult !== undefined) {
61+
result = labelAnalysisResult;
62+
}
63+
}
64+
65+
return result;
66+
}
67+
68+
const knownDeviceLabels = new Map<string, FacingModeFromLocalTrackReturnValue>([
69+
['obs virtual camera', { facingMode: 'environment', confidence: 'medium' }],
70+
]);
71+
const knownDeviceLabelSections = new Map<string, FacingModeFromLocalTrackReturnValue>([
72+
['iphone', { facingMode: 'environment', confidence: 'medium' }],
73+
['ipad', { facingMode: 'environment', confidence: 'medium' }],
74+
]);
75+
/**
76+
* Attempt to analyze the device label to determine the facing mode.
77+
*
78+
* @experimental
79+
*/
80+
export function facingModeFromDeviceLabel(
81+
deviceLabel: string,
82+
): FacingModeFromLocalTrackReturnValue | undefined {
83+
const label = deviceLabel.trim().toLowerCase();
84+
// Empty string is a valid device label but we can't infer anything from it.
85+
if (label === '') {
86+
return undefined;
87+
}
88+
89+
// Can we match against widely known device labels.
90+
if (knownDeviceLabels.has(label)) {
91+
return knownDeviceLabels.get(label);
92+
}
93+
94+
// Can we match against sections of the device label.
95+
return Array.from(knownDeviceLabelSections.entries()).find(([section]) =>
96+
label.includes(section),
97+
)?.[1];
98+
}
99+
100+
function isFacingModeValue(item: string): item is FacingMode {
101+
const allowedValues: FacingMode[] = ['user', 'environment', 'left', 'right'];
102+
return item === undefined || allowedValues.includes(item as FacingMode);
103+
}

src/room/track/utils.test.ts

Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AudioCaptureOptions, VideoCaptureOptions, VideoPresets } from './options';
2-
import { constraintsForOptions, facingModeFromDeviceLabel, mergeDefaultOptions } from './utils';
2+
import { constraintsForOptions, mergeDefaultOptions } from './utils';
33

44
describe('mergeDefaultOptions', () => {
55
const audioDefaults: AudioCaptureOptions = {
@@ -108,32 +108,3 @@ describe('constraintsForOptions', () => {
108108
expect(videoOpts.aspectRatio).toEqual(VideoPresets.h720.resolution.aspectRatio);
109109
});
110110
});
111-
112-
describe('Test facingMode detection', () => {
113-
test('OBS virtual camera should be detected.', () => {
114-
const result = facingModeFromDeviceLabel('OBS Virtual Camera');
115-
expect(result?.facingMode).toEqual('environment');
116-
expect(result?.confidence).toEqual('medium');
117-
});
118-
119-
test.each([
120-
['Peter’s iPhone Camera', { facingMode: 'environment', confidence: 'medium' }],
121-
['iPhone de Théo Camera', { facingMode: 'environment', confidence: 'medium' }],
122-
])(
123-
'Device labels that contain "iphone" should return facingMode "environment".',
124-
(label, expected) => {
125-
const result = facingModeFromDeviceLabel(label);
126-
expect(result?.facingMode).toEqual(expected.facingMode);
127-
expect(result?.confidence).toEqual(expected.confidence);
128-
},
129-
);
130-
131-
test.each([
132-
['Peter’s iPad Camera', { facingMode: 'environment', confidence: 'medium' }],
133-
['iPad de Théo Camera', { facingMode: 'environment', confidence: 'medium' }],
134-
])('Device label that contain "ipad" should detect.', (label, expected) => {
135-
const result = facingModeFromDeviceLabel(label);
136-
expect(result?.facingMode).toEqual(expected.facingMode);
137-
expect(result?.confidence).toEqual(expected.confidence);
138-
});
139-
});

src/room/track/utils.ts

Lines changed: 0 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import { sleep } from '../utils';
2-
import log from './../../logger';
3-
import LocalTrack from './LocalTrack';
42
import type { AudioCaptureOptions, CreateLocalTracksOptions, VideoCaptureOptions } from './options';
53
import type { AudioTrack } from './types';
64

@@ -114,103 +112,3 @@ export function getNewAudioContext(): AudioContext | void {
114112
return new AudioContext({ latencyHint: 'interactive' });
115113
}
116114
}
117-
118-
type FacingMode = NonNullable<VideoCaptureOptions['facingMode']>;
119-
type FacingModeFromLocalTrackOptions = {
120-
/**
121-
* If no facing mode can be determined, this value will be used.
122-
* @defaultValue 'user'
123-
*/
124-
defaultFacingMode?: FacingMode;
125-
};
126-
type FacingModeFromLocalTrackReturnValue = {
127-
/**
128-
* The (probable) facingMode of the track.
129-
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/facingMode | MDN docs on facingMode}
130-
*/
131-
facingMode: FacingMode;
132-
/**
133-
* The confidence that the returned facingMode is correct.
134-
*/
135-
confidence: 'high' | 'medium' | 'low';
136-
};
137-
138-
/**
139-
* Try to analyze the local track to determine the facing mode of a track.
140-
*
141-
* @remarks
142-
* There is no property supported by all browsers to detect whether a video track originated from a user- or environment-facing camera device.
143-
* 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.
144-
* If both methods fail, the default facing mode will be used.
145-
*
146-
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints/facingMode | MDN docs on facingMode}
147-
* @experimental
148-
*/
149-
export function facingModeFromLocalTrack(
150-
localTrack: LocalTrack | MediaStreamTrack,
151-
options: FacingModeFromLocalTrackOptions = {},
152-
): FacingModeFromLocalTrackReturnValue {
153-
const track = localTrack instanceof LocalTrack ? localTrack.mediaStreamTrack : localTrack;
154-
const trackSettings = track.getSettings();
155-
let result: FacingModeFromLocalTrackReturnValue = {
156-
facingMode: options.defaultFacingMode ?? 'user',
157-
confidence: 'low',
158-
};
159-
160-
// 1. Try to get facingMode from track settings.
161-
if ('facingMode' in trackSettings) {
162-
const rawFacingMode = trackSettings.facingMode;
163-
log.debug('rawFacingMode', { rawFacingMode });
164-
if (rawFacingMode && typeof rawFacingMode === 'string' && isFacingModeValue(rawFacingMode)) {
165-
result = { facingMode: rawFacingMode, confidence: 'high' };
166-
}
167-
}
168-
169-
// 2. If we don't have a high confidence we try to get the facing mode from the device label.
170-
if (['low', 'medium'].includes(result.confidence)) {
171-
log.debug(`Try to get facing mode from device label: (${track.label})`);
172-
const labelAnalysisResult = facingModeFromDeviceLabel(track.label);
173-
if (labelAnalysisResult !== undefined) {
174-
result = labelAnalysisResult;
175-
}
176-
}
177-
178-
return result;
179-
}
180-
181-
const knownDeviceLabels = new Map<string, FacingModeFromLocalTrackReturnValue>([
182-
['obs virtual camera', { facingMode: 'environment', confidence: 'medium' }],
183-
]);
184-
const knownDeviceLabelSections = new Map<string, FacingModeFromLocalTrackReturnValue>([
185-
['iphone', { facingMode: 'environment', confidence: 'medium' }],
186-
['ipad', { facingMode: 'environment', confidence: 'medium' }],
187-
]);
188-
/**
189-
* Attempt to analyze the device label to determine the facing mode.
190-
*
191-
* @experimental
192-
*/
193-
export function facingModeFromDeviceLabel(
194-
deviceLabel: string,
195-
): FacingModeFromLocalTrackReturnValue | undefined {
196-
const label = deviceLabel.trim().toLowerCase();
197-
// Empty string is a valid device label but we can't infer anything from it.
198-
if (label === '') {
199-
return undefined;
200-
}
201-
202-
// Can we match against widely known device labels.
203-
if (knownDeviceLabels.has(label)) {
204-
return knownDeviceLabels.get(label);
205-
}
206-
207-
// Can we match against sections of the device label.
208-
return Array.from(knownDeviceLabelSections.entries()).find(([section]) =>
209-
label.includes(section),
210-
)?.[1];
211-
}
212-
213-
function isFacingModeValue(item: string): item is FacingMode {
214-
const allowedValues: FacingMode[] = ['user', 'environment', 'left', 'right'];
215-
return item === undefined || allowedValues.includes(item as FacingMode);
216-
}

0 commit comments

Comments
 (0)