[go: up one dir, main page]

blob: 3ccafdcdc45af7656055e9ba8e7e016527ca3712 [file] [log] [blame]
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import {assert, assertExists} from '../assert.js';
import {reportError} from '../error.js';
import {I18nString} from '../i18n_string.js';
import * as loadTimeData from '../models/load_time_data.js';
import {DeviceOperator} from '../mojo/device_operator.js';
import {speak} from '../spoken_msg.js';
import {ErrorLevel, ErrorType, Facing, VideoConfig} from '../type.js';
import {sleep} from '../util.js';
import {WaitableEvent} from '../waitable_event.js';
import {Camera3DeviceInfo} from './camera3_device_info.js';
import {
StreamConstraints,
toMediaStreamConstraints,
} from './stream_constraints.js';
/**
* The singleton instance of StreamManager. Initialized by the first
* invocation of getInstance().
*/
let instance: StreamManager|null = null;
/**
* Device information includes MediaDeviceInfo and Camera3DeviceInfo.
*/
export interface DeviceInfo {
v1Info: MediaDeviceInfo;
v3Info: Camera3DeviceInfo|null;
}
/**
* Real and virtual device mapping.
*/
interface VirtualMap {
realId: string;
virtualId: string;
}
/**
* Monitors device change and provides different listener callbacks for
* device changes. It also provides streams for different modes.
*/
export class StreamManager {
/**
* MediaDeviceInfo of all available video devices.
*/
private devicesInfo: Promise<MediaDeviceInfo[]>|null = null;
/**
* Camera3DeviceInfo of all available video devices. Is null on HALv1 device
* without mojo api support.
*/
private camera3DevicesInfo: Promise<DeviceInfo[]|null>|null = null;
/**
* Listeners for real device change event.
*/
private readonly realListeners: Array<(devices: DeviceInfo[]) => void> = [];
/**
* Latest result of Camera3DeviceInfo of all real video devices.
*/
private realDevices: DeviceInfo[] = [];
/**
* Real device id and corresponding virtual devices id mapping and it is
* only available on HALv3.
*/
private virtualMap: VirtualMap|null = null;
/**
* Signal it to indicate that the virtual device is ready.
*/
private waitVirtual: WaitableEvent<string>|null = null;
/**
* Signal to indicate that the virtual device is successfully removed.
*/
private waitVirtualRemoved: WaitableEvent|null = null;
/**
* Filter out lagging 720p on grunt. See https://crbug.com/1122852.
*/
private readonly videoConfigFilter: (config: VideoConfig) => boolean;
private constructor() {
this.videoConfigFilter = (() => {
const board = loadTimeData.getBoard();
return board === 'grunt' ? ({height}: VideoConfig) => height < 720 :
() => true;
})();
navigator.mediaDevices.addEventListener(
'devicechange', () => this.deviceUpdate());
}
/**
* Creates a new instance of StreamManager if it is not set. Returns the
* exist instance.
*
* @return The singleton instance.
*/
static getInstance(): StreamManager {
if (instance === null) {
instance = new StreamManager();
}
return instance;
}
/**
* Registers listener to be called when state of available real devices
* changes.
*/
addRealDeviceChangeListener(listener: (devices: DeviceInfo[]) => void): void {
this.realListeners.push(listener);
}
/**
* Creates extra stream according to the constraints.
*/
async openCaptureStream(constraints: StreamConstraints):
Promise<MediaStream> {
const realDeviceId = constraints.deviceId;
if (DeviceOperator.isSupported()) {
try {
await this.setVirtualDeviceEnabled(realDeviceId, true);
assert(this.virtualMap !== null);
constraints.deviceId = this.virtualMap.virtualId;
} catch (e) {
reportError(ErrorType.MULTIPLE_STREAMS_FAILURE, ErrorLevel.ERROR, e);
}
}
const stream = await navigator.mediaDevices.getUserMedia(
toMediaStreamConstraints(constraints));
return stream;
}
/**
* Closes the given capture stream.
*/
async closeCaptureStream(captureStream: MediaStream): Promise<void> {
assertExists(captureStream.getVideoTracks()[0]).stop();
const deviceOperator = DeviceOperator.getInstance();
if (deviceOperator !== null) {
// We need to cache |virtualId| first since it will be wiped out after
// disabling multi-stream.
assert(this.virtualMap !== null);
const virtualId = this.virtualMap.virtualId;
try {
await this.setVirtualDeviceEnabled(this.virtualMap.realId, false);
} catch (e) {
reportError(ErrorType.MULTIPLE_STREAMS_FAILURE, ErrorLevel.ERROR, e);
}
await deviceOperator.dropConnection(virtualId);
}
}
/**
* Handling function for device changing.
*/
async deviceUpdate(): Promise<void> {
const devices = await this.doDeviceInfoUpdate();
if (devices === null) {
return;
}
await this.doDeviceNotify(devices);
}
/**
* Gets devices information via mojo IPC.
*/
private async doDeviceInfoUpdate(): Promise<DeviceInfo[]|null> {
this.devicesInfo = this.enumerateDevices();
this.camera3DevicesInfo = this.queryMojoDevicesInfo();
try {
return await this.camera3DevicesInfo;
} catch (e) {
reportError(ErrorType.DEVICE_INFO_UPDATE_FAILURE, ErrorLevel.ERROR, e);
}
return null;
}
/**
* Notifies device changes to listeners and create a mapping for real and
* virtual device.
*/
private async doDeviceNotify(devices: DeviceInfo[]) {
function isVirtual(d: DeviceInfo) {
return d.v3Info !== null &&
(d.v3Info.facing === Facing.VIRTUAL_USER ||
d.v3Info.facing === Facing.VIRTUAL_ENV ||
d.v3Info.facing === Facing.VIRTUAL_EXT);
}
const realDevices = devices.filter((d) => !isVirtual(d));
const virtualDevices = devices.filter(isVirtual);
// We currently only support one virtual device.
assert(virtualDevices.length <= 1);
if (virtualDevices.length === 1 && this.waitVirtual !== null) {
this.waitVirtual.signal(virtualDevices[0].v1Info.deviceId);
this.waitVirtual = null;
}
if (virtualDevices.length === 0 && this.waitVirtualRemoved !== null) {
this.waitVirtualRemoved.signal();
this.waitVirtualRemoved = null;
}
let isRealDeviceChange = false;
for (const added of this.getDifference(realDevices, this.realDevices)) {
speak(I18nString.STATUS_MSG_CAMERA_PLUGGED, added.v1Info.label);
isRealDeviceChange = true;
}
for (const removed of this.getDifference(this.realDevices, realDevices)) {
speak(I18nString.STATUS_MSG_CAMERA_UNPLUGGED, removed.v1Info.label);
isRealDeviceChange = true;
}
if (isRealDeviceChange) {
this.realListeners.map((l) => l(realDevices));
}
this.realDevices = realDevices;
}
/**
* Computes |devices| - |devices2|.
*/
private getDifference(devices: DeviceInfo[], devices2: DeviceInfo[]):
DeviceInfo[] {
const ids = new Set(devices2.map((d) => d.v1Info.deviceId));
return devices.filter((d) => !ids.has(d.v1Info.deviceId));
}
/**
* Enumerates all available devices and gets their MediaDeviceInfo. Retry at
* one-second intervals if devices length is zero.
*/
private async enumerateDevices(): Promise<MediaDeviceInfo[]> {
const deviceType = loadTimeData.getDeviceType();
const shouldHaveBuiltinCamera =
deviceType === 'chromebook' || deviceType === 'chromebase';
let attempts = 5;
while (attempts--) {
const devices = (await navigator.mediaDevices.enumerateDevices())
.filter((device) => device.kind === 'videoinput');
if (!shouldHaveBuiltinCamera || devices.length > 0) {
return devices;
}
await sleep(1000);
}
throw new Error('Device list empty.');
}
/**
* Queries Camera3DeviceInfo of available devices through private mojo API.
*
* @return Camera3DeviceInfo of available devices. Maybe null on HALv1
* devices without supporting private mojo api.
* @throws Thrown when camera unplugging happens between enumerating devices
* and querying mojo APIs with current device info results.
*/
private async queryMojoDevicesInfo(): Promise<DeviceInfo[]|null> {
const deviceInfos = await this.devicesInfo;
assert(deviceInfos !== null);
const isV3Supported = DeviceOperator.isSupported();
return Promise.all(deviceInfos.map(
async (d) => ({
v1Info: d,
v3Info: isV3Supported ?
(await Camera3DeviceInfo.create(d, this.videoConfigFilter)) :
null,
})));
}
/**
* Enables/Disables virtual device on target camera device. The extra
* stream will be reported as virtual video device from
* navigator.mediaDevices.enumerateDevices().
*
* @param deviceId The id of target camera device.
* @param enabled True for enabling virtual device.
*/
async setVirtualDeviceEnabled(deviceId: string, enabled: boolean):
Promise<void> {
const deviceOperator = DeviceOperator.getInstance();
assert(deviceOperator !== null);
if (enabled) {
const waitEvent = new WaitableEvent<string>();
this.waitVirtual = waitEvent;
await deviceOperator.setVirtualDeviceEnabled(deviceId, enabled);
await this.deviceUpdate();
const virtualId = await waitEvent.timedWait(3000);
this.virtualMap = {realId: deviceId, virtualId};
} else {
const waitEvent = new WaitableEvent();
this.waitVirtualRemoved = waitEvent;
await deviceOperator.setVirtualDeviceEnabled(deviceId, enabled);
await this.deviceUpdate();
await waitEvent.timedWait(3000);
this.virtualMap = null;
}
}
}