blob: 6a181d56ae7bf06a882f46b33fd58e64bdb9dfd1 [file] [log] [blame]
// Copyright 2018 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 {browserProxy} from '../../browser_proxy/browser_proxy.js';
// eslint-disable-next-line no-unused-vars
import {Camera3DeviceInfo} from '../../device/camera3_device_info.js';
// eslint-disable-next-line no-unused-vars
import {DeviceInfoUpdater} from '../../device/device_info_updater.js';
import * as dom from '../../dom.js';
import {sendBarcodeEnabledEvent} from '../../metrics.js';
import * as nav from '../../nav.js';
import * as state from '../../state.js';
import {Facing, Mode, PerfEvent, ViewName} from '../../type.js';
import * as util from '../../util.js';
/**
* Creates a controller for the options of Camera view.
*/
export class Options {
/**
* @param {!DeviceInfoUpdater} infoUpdater
* @param {function()} doSwitchDevice Callback to trigger device switching.
*/
constructor(infoUpdater, doSwitchDevice) {
/**
* @type {!DeviceInfoUpdater}
* @private
* @const
*/
this.infoUpdater_ = infoUpdater;
/**
* @type {function()}
* @private
* @const
*/
this.doSwitchDevice_ = doSwitchDevice;
/**
* @type {!HTMLInputElement}
* @private
* @const
*/
this.toggleMic_ = dom.get('#toggle-mic', HTMLInputElement);
/**
* @type {!HTMLInputElement}
* @private
* @const
*/
this.toggleMirror_ = dom.get('#toggle-mirror', HTMLInputElement);
/**
* @type {!HTMLInputElement}
* @private
* @const
*/
this.toggleBarcode_ = dom.get('#toggle-barcode', HTMLInputElement);
/**
* Device id of the camera device currently used or selected.
* @type {?string}
* @private
*/
this.videoDeviceId_ = null;
/**
* Whether list of video devices is being refreshed now.
* @type {boolean}
* @private
*/
this.refreshingVideoDeviceIds_ = false;
/**
* Whether the current device is HALv1 and lacks facing configuration.
* get facing information.
* @type {?boolean}
* private
*/
this.isV1NoFacingConfig_ = null;
/**
* Mirroring set per device.
* @type {!Object}
* @private
*/
this.mirroringToggles_ = {};
/**
* Current audio track in use.
* @type {?MediaStreamTrack}
* @private
*/
this.audioTrack_ = null;
[['#switch-device', () => this.switchDevice_()],
['#toggle-grid', () => this.animatePreviewGrid_()],
['#open-settings', () => nav.open(ViewName.SETTINGS)],
]
.forEach(
([selector, fn]) =>
document.querySelector(selector).addEventListener('click', fn));
this.toggleMic_.addEventListener('click', () => this.updateAudioByMic_());
this.toggleMirror_.addEventListener('click', () => this.saveMirroring_());
this.toggleBarcode_.addEventListener('click', () => this.updateBarcode_());
state.addObserver(Mode.PHOTO, (inPhotoMode) => {
if (!inPhotoMode) {
this.toggleBarcode_.checked = false;
this.updateBarcode_();
}
});
util.bindElementAriaLabelWithState({
element: dom.get('#toggle-timer', Element),
state: state.State.TIMER_3SEC,
onLabel: 'toggle_timer_3s_button',
offLabel: 'toggle_timer_10s_button',
});
// Restore saved mirroring states per video device.
browserProxy.localStorageGet({mirroringToggles: {}})
.then((values) => this.mirroringToggles_ = values['mirroringToggles']);
// Remove the deprecated values.
browserProxy.localStorageRemove(
['effectIndex', 'toggleMulti', 'toggleMirror']);
this.infoUpdater_.addDeviceChangeListener(async (updater) => {
state.set(
state.State.MULTI_CAMERA,
(await updater.getDevicesInfo()).length >= 2);
});
}
/**
* Device id of the camera device currently used or selected.
* @return {?string}
*/
get currentDeviceId() {
return this.videoDeviceId_;
}
/**
* Switches to the next available camera device.
* @private
*/
async switchDevice_() {
if (!state.get(state.State.STREAMING) || state.get(state.State.TAKING)) {
return;
}
state.set(PerfEvent.CAMERA_SWITCHING, true);
const devices = await this.infoUpdater_.getDevicesInfo();
util.animateOnce(dom.get('#switch-device', HTMLElement));
let index =
devices.findIndex((entry) => entry.deviceId === this.videoDeviceId_);
if (index === -1) {
index = 0;
}
if (devices.length > 0) {
index = (index + 1) % devices.length;
this.videoDeviceId_ = devices[index].deviceId;
}
const isSuccess = await this.doSwitchDevice_();
state.set(PerfEvent.CAMERA_SWITCHING, false, {hasError: !isSuccess});
}
/**
* Animates the preview grid.
* @private
*/
animatePreviewGrid_() {
Array.from(document.querySelector('#preview-grid').children)
.forEach((grid) => util.animateOnce(grid));
}
/**
* Maps MediaTrackSettings.facingMode to CCA facing type.
* @param {string|undefined} facing The target facingMode to map.
* @return {!Facing} The mapped CCA facing.
* @private
*/
mapFacing_(facing) {
switch (facing) {
case undefined:
return Facing.EXTERNAL;
case 'user':
return Facing.USER;
case 'environment':
return Facing.ENVIRONMENT;
default:
throw new Error('Unknown facing: ' + facing);
}
}
/**
* Updates the options' values for the current constraints and stream.
* @param {!MediaStream} stream Current Stream in use.
* @return {!Promise<!Facing>} Facing-mode in use.
*/
async updateValues(stream) {
const track = stream.getVideoTracks()[0];
const trackSettings = track.getSettings && track.getSettings();
const facingMode = trackSettings && trackSettings.facingMode;
if (this.isV1NoFacingConfig_ === null) {
// Because the facing mode of external camera will be set to undefined on
// all devices, to distinguish HALv1 device without facing configuration,
// assume the first opened camera is built-in camera. Device without
// facing configuration won't set facing of built-in cameras. Also if
// HALv1 device with facing configuration opened external camera first
// after CCA launched the logic here may misjudge it as this category.
this.isV1NoFacingConfig_ = facingMode === undefined;
}
const facing =
this.isV1NoFacingConfig_ ? Facing.NOT_SET : this.mapFacing_(facingMode);
this.videoDeviceId_ = trackSettings && trackSettings.deviceId || null;
this.updateMirroring_(facing);
this.audioTrack_ = stream.getAudioTracks()[0];
this.updateAudioByMic_();
return facing;
}
/**
* Updates mirroring for a new stream.
* @param {!Facing} facing Facing of the stream.
* @private
*/
updateMirroring_(facing) {
// Update mirroring by detected facing-mode. Enable mirroring by default if
// facing-mode isn't available.
let enabled = facing !== Facing.ENVIRONMENT;
// Override mirroring only if mirroring was toggled manually.
if (this.videoDeviceId_ in this.mirroringToggles_) {
enabled = this.mirroringToggles_[this.videoDeviceId_];
}
util.toggleChecked(this.toggleMirror_, enabled);
}
/**
* Saves the toggled mirror state for the current video device.
* @private
*/
saveMirroring_() {
this.mirroringToggles_[this.videoDeviceId_] = this.toggleMirror_.checked;
browserProxy.localStorageSet({mirroringToggles: this.mirroringToggles_});
}
/**
* Enables/disables the current audio track according to the microphone
* option.
* @private
*/
updateAudioByMic_() {
if (this.audioTrack_) {
this.audioTrack_.enabled = this.toggleMic_.checked;
}
}
/**
* Enables/disables barcode scanning according to the barcode option.
* @private
*/
updateBarcode_() {
state.set(state.State.SCAN_BARCODE, this.toggleBarcode_.checked);
if (this.toggleBarcode_.checked) {
sendBarcodeEnabledEvent();
}
}
/**
* Gets the video device ids sorted by preference.
* @return {!Promise<!Array<?string>>} May contain null for user facing camera
* on HALv1 devices.
*/
async videoDeviceIds() {
/** @type {!Array<(!Camera3DeviceInfo|!MediaDeviceInfo)>} */
let devices;
/**
* Object mapping from device id to facing. Set to null on HALv1 device.
* @type {?Object<string, !Facing>}
*/
let facings = null;
const camera3Info = await this.infoUpdater_.getCamera3DevicesInfo();
if (camera3Info) {
devices = camera3Info;
facings = {};
for (const {deviceId, facing} of camera3Info) {
facings[deviceId] = facing;
}
this.isV1NoFacingConfig_ = false;
} else {
devices = await this.infoUpdater_.getDevicesInfo();
}
const defaultFacing = util.getDefaultFacing();
// Put the selected video device id first.
const sorted = devices.map((device) => device.deviceId).sort((a, b) => {
if (a === b) {
return 0;
}
if (this.videoDeviceId_ ? a === this.videoDeviceId_ :
(facings && facings[a] === defaultFacing)) {
return -1;
}
return 1;
});
// Prepended 'null' deviceId means the system default camera on HALv1
// device. Add it only when the app is launched (no video-device-id set).
if (!facings && this.videoDeviceId_ === null) {
sorted.unshift(null);
}
return sorted;
}
}