blob: a3d1e88b364d7de182ca7a0883167c86085beb2c [file] [log] [blame]
// Copyright 2019 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.
'use strict';
/**
* Namespace for the Camera app.
*/
var cca = cca || {};
/**
* Namespace for device.
*/
cca.device = cca.device || {};
/**
* Controller for managing preference of capture settings and generating a list
* of stream constraints-candidates sorted by user preference.
* @abstract
*/
cca.device.ConstraintsPreferrer = class {
/**
* @param {!cca.ResolutionEventBroker} resolBroker
* @param {function()} doReconfigureStream Trigger stream reconfiguration to
* reflect changes in user preferred settings.
* @protected
*/
constructor(resolBroker, doReconfigureStream) {
/**
* @type {!cca.ResolutionEventBroker}
* @protected
*/
this.resolBroker_ = resolBroker;
/**
* @type {function()}
* @protected
*/
this.doReconfigureStream_ = doReconfigureStream;
/**
* Object saving resolution preference that each of its key as device id and
* value to be preferred width, height of resolution of that video device.
* @type {!Object<string, {width: number, height: number}>}
* @protected
*/
this.prefResolution_ = {};
/**
* Device id of currently working video device.
* @type {?string}
* @protected
*/
this.deviceId_ = null;
/**
* Object of device id as its key and all of available capture resolutions
* supported by that video device as its value.
* @type {!Object<string, !ResolList>}
* @protected
*/
this.deviceResolutions_ = {};
}
/**
* Gets preferred capture resolution for a specific device.
* @param {string} deviceId Device id of the device.
* @return {!Array<number>} Preferred resolution formatted as [width, height].
* @throws {Error}
*/
getPrefResolution(deviceId) {
const {width, height} = this.prefResolution_[deviceId] || {};
if (width === undefined || height === undefined) {
throw new Error(`Query non-existent device id ${deviceId}`);
}
return [width, height];
}
/**
* Updates with new video device information.
* @param {!Array<!cca.device.Camera3DeviceInfo>} devices
* @abstract
*/
updateDevicesInfo(devices) {}
/**
* Updates values according to currently working video device and capture
* settings.
* @param {string} deviceId Device id of video device to be updated.
* @param {!MediaStream} stream Currently active preview stream.
* @param {number} width Width of resolution to be updated to.
* @param {number} height Height of resolution to be updated to.
* @abstract
*/
updateValues(deviceId, stream, width, height) {}
/**
* Gets all available candidates for capturing under this controller and its
* corresponding preview constraints for the specified video device. Returned
* resolutions and constraints candidates are both sorted in desired trying
* order.
* @abstract
* @param {string} deviceId Device id of video device.
* @param {!ResolList} previewResolutions Available preview resolutions for
* the video device.
* @return {!Array<!Array<number>|!Array<!Object>>} Result capture
* resolution width, height and constraints-candidates for its preview.
* It's formatted as [[[w1, h1], [constraints_for_w1_h1...]],
* [[w2, h2], [constraints_for_w2_h2...]], ...].
*/
getSortedCandidates(deviceId, previewResolutions) {}
};
/**
* All supported constant fps options of video recording.
* @type {!Array<number>}
* @const
*/
cca.device.SUPPORTED_CONSTANT_FPS = [30, 60];
/**
* Controller for handling video resolution preference.
*/
cca.device.VideoConstraintsPreferrer =
class extends cca.device.ConstraintsPreferrer {
/**
* @param {!cca.ResolutionEventBroker} resolBroker
* @param {function()} doReconfigureStream
* @public
*/
constructor(resolBroker, doReconfigureStream) {
super(resolBroker, doReconfigureStream);
/**
* Object saving information of supported constant fps. Each of its key as
* device id and value as an object mapping from resolution to all constant
* fps options supported by that resolution.
* @type {!Object<string, Object<Array<number>, Array<number>>>}
* @private
*/
this.constFpsInfo_ = {};
/**
* Object saving fps preference that each of its key as device id and value
* as an object mapping from resolution to preferred constant fps for that
* resolution.
* @type {!Object<string, Object<Array<number>, number>>}
* @private
*/
this.prefFpses_ = {};
/**
* @type {!HTMLButtonElement}
* @private
*/
this.toggleFps_ = /** @type {!HTMLButtonElement} */ (
document.querySelector('#toggle-fps'));
/**
* Currently in used recording resolution.
* [width, height]
* @type {!Array<number>}
* @protected
*/
this.resolution_ = [-1, -1];
// Restore saved preferred recording fps per video device per resolution.
cca.proxy.browserProxy.localStorageGet(
{deviceVideoFps: {}},
(values) => this.prefFpses_ = values.deviceVideoFps);
// Restore saved preferred video resolution per video device.
cca.proxy.browserProxy.localStorageGet(
{deviceVideoResolution: {}},
(values) => this.prefResolution_ = values.deviceVideoResolution);
this.resolBroker_.registerChangeVideoPrefResolHandler(
(deviceId, width, height) => {
this.prefResolution_[deviceId] = {width, height};
cca.proxy.browserProxy.localStorageSet(
{deviceVideoResolution: this.prefResolution_});
if (cca.state.get('video-mode') && deviceId == this.deviceId_) {
this.doReconfigureStream_();
} else {
this.resolBroker_.notifyVideoPrefResolChange(
deviceId, width, height);
}
});
this.toggleFps_.addEventListener('click', (event) => {
if (!cca.state.get('streaming') || cca.state.get('taking')) {
event.preventDefault();
}
});
this.toggleFps_.addEventListener('change', (event) => {
this.setPreferredConstFps_(
/** @type {string} */ (this.deviceId_), ...this.resolution_,
this.toggleFps_.checked ? 60 : 30);
this.doReconfigureStream_();
});
}
/**
* Sets the preferred fps used in video recording for particular video device
* with particular resolution.
* @param {string} deviceId Device id of video device to be set with.
* @param {number} width Resolution width to be set with.
* @param {number} height Resolution height to be set with.
* @param {number} prefFps Preferred fps to be set with.
* @private
*/
setPreferredConstFps_(deviceId, width, height, prefFps) {
if (!cca.device.SUPPORTED_CONSTANT_FPS.includes(prefFps)) {
return;
}
this.toggleFps_.checked = prefFps === 60;
cca.device.SUPPORTED_CONSTANT_FPS.forEach(
(fps) => cca.state.set(`_${fps}fps`, fps == prefFps));
this.prefFpses_[deviceId] = this.prefFpses_[deviceId] || {};
this.prefFpses_[deviceId][[width, height]] = prefFps;
cca.proxy.browserProxy.localStorageSet({deviceVideoFps: this.prefFpses_});
}
/**
* @override
*/
updateDevicesInfo(devices) {
this.deviceResolutions_ = {};
this.constFpsInfo_ = {};
devices.forEach(({deviceId, videoResols, videoMaxFps, fpsRanges}) => {
this.deviceResolutions_[deviceId] = videoResols;
let {width = -1, height = -1} = this.prefResolution_[deviceId] || {};
if (!videoResols.find(([w, h]) => w === width && h === height)) {
[width, height] = videoResols.reduce(
(maxR, R) => (maxR[0] * maxR[1] < R[0] * R[1] ? R : maxR), [0, 0]);
}
this.prefResolution_[deviceId] = {width, height};
const constFpses = cca.device.SUPPORTED_CONSTANT_FPS.filter(
(fps) => !!fpsRanges.find(
([minFps, maxFps]) => minFps === fps && maxFps === fps));
const fpsInfo = {};
for (const r of Object.keys(videoMaxFps).map((r) => r.toString())) {
fpsInfo[r] = constFpses.filter((fps) => fps <= videoMaxFps[r]);
}
this.constFpsInfo_[deviceId] = fpsInfo;
});
cca.proxy.browserProxy.localStorageSet(
{deviceVideoResolution: this.prefResolution_});
}
/**
* @override
*/
updateValues(deviceId, stream, width, height) {
this.deviceId_ = deviceId;
this.resolution_ = [width, height];
this.prefResolution_[deviceId] = {width, height};
cca.proxy.browserProxy.localStorageSet(
{deviceVideoResolution: this.prefResolution_});
this.resolBroker_.notifyVideoPrefResolChange(deviceId, width, height);
const fps = stream.getVideoTracks()[0].getSettings().frameRate;
const availableFpses = this.constFpsInfo_[deviceId][[width, height]];
if (availableFpses.includes(fps)) {
this.setPreferredConstFps_(deviceId, width, height, fps);
}
cca.state.set('multi-fps', availableFpses.length > 1);
}
/**
* @override
*/
getSortedCandidates(deviceId, previewResolutions) {
// Due to the limitation of MediaStream API, preview stream is used directly
// to do video recording.
const prefR = this.prefResolution_[deviceId] || {width: 0, height: -1};
const sortPrefResol = ([w, h], [w2, h2]) => {
if (w == w2 && h == h2) {
return 0;
}
// Exactly the preferred resolution.
if (w == prefR.width && h == prefR.height) {
return -1;
}
if (w2 == prefR.width && h2 == prefR.height) {
return 1;
}
// Aspect ratio same as preferred resolution.
if (w * h2 != w2 * h) {
if (w * prefR.height == prefR.width * h) {
return -1;
}
if (w2 * prefR.height == prefR.width * h2) {
return 1;
}
}
return w2 * h2 - w * h;
};
// Maps specified video resolution width, height to tuple of width, height
// and all supported constant fps under that resolution or null fps for not
// support multiple constant fps options. The resolution-fps tuple are
// sorted by user preference of constant fps.
const getFpses = (r) => {
let constFpses = [null];
if (this.constFpsInfo_[deviceId][r].includes(30)) {
if (this.constFpsInfo_[deviceId][r].includes(60)) {
const prefFps =
this.prefFpses_[deviceId] && this.prefFpses_[deviceId][r] || 30;
constFpses = prefFps == 30 ? [30, 60] : [60, 30];
} else {
constFpses = [30];
}
}
return constFpses.map((fps) => [...r, fps]);
};
const toConstraints = (width, height, fps) => ({
audio: true,
video: {
deviceId: {exact: deviceId},
frameRate: fps ? {exact: fps} : {min: 24},
width,
height,
},
});
return [...this.deviceResolutions_[deviceId]]
.sort(sortPrefResol)
.flatMap(getFpses)
.map(([width, height, fps]) => ([
[width, height],
[toConstraints(width, height, fps)],
]));
}
};
/**
* Controller for handling photo resolution preference.
*/
cca.device.PhotoResolPreferrer = class extends cca.device.ConstraintsPreferrer {
/**
* @param {!cca.ResolutionEventBroker} resolBroker
* @param {function()} doReconfigureStream
* @public
*/
constructor(resolBroker, doReconfigureStream) {
super(resolBroker, doReconfigureStream);
// Restore saved preferred photo resolution per video device.
cca.proxy.browserProxy.localStorageGet(
{devicePhotoResolution: {}},
(values) => this.prefResolution_ = values.devicePhotoResolution);
this.resolBroker_.registerChangePhotoPrefResolHandler(
(deviceId, width, height) => {
this.prefResolution_[deviceId] = {width, height};
cca.proxy.browserProxy.localStorageSet(
{devicePhotoResolution: this.prefResolution_});
if (!cca.state.get('video-mode') && deviceId == this.deviceId_) {
this.doReconfigureStream_();
} else {
this.resolBroker_.notifyPhotoPrefResolChange(
deviceId, width, height);
}
});
}
/**
* @override
*/
updateDevicesInfo(devices) {
this.deviceResolutions_ = {};
devices.forEach(({deviceId, photoResols}) => {
this.deviceResolutions_[deviceId] = photoResols;
let {width = -1, height = -1} = this.prefResolution_[deviceId] || {};
if (!photoResols.find(([w, h]) => w == width && h == height)) {
[width, height] = photoResols.reduce(
(maxR, R) => (maxR[0] * maxR[1] < R[0] * R[1] ? R : maxR), [0, 0]);
}
this.prefResolution_[deviceId] = {width, height};
return [deviceId, width, height, photoResols];
});
cca.proxy.browserProxy.localStorageSet(
{devicePhotoResolution: this.prefResolution_});
}
/**
* @override
*/
updateValues(deviceId, stream, width, height) {
this.deviceId_ = deviceId;
this.prefResolution_[deviceId] = {width, height};
cca.proxy.browserProxy.localStorageSet(
{devicePhotoResolution: this.prefResolution_});
this.resolBroker_.notifyPhotoPrefResolChange(deviceId, width, height);
}
/**
* Finds and pairs photo resolutions and preview resolutions with the same
* aspect ratio.
* @param {!ResolList} captureResolutions Available photo capturing
* resolutions.
* @param {!ResolList} previewResolutions Available preview resolutions.
* @return {!Array<!Array<!ResolList>>} Each item of returned array is a pair
* of capture and preview resolutions of same aspect ratio.
* @private
*/
pairCapturePreviewResolutions_(captureResolutions, previewResolutions) {
const toAspectRatio = (w, h) => (w / h).toFixed(4);
const previewRatios = previewResolutions.reduce((rs, [w, h]) => {
const key = toAspectRatio(w, h);
rs[key] = rs[key] || [];
rs[key].push([w, h]);
return rs;
}, {});
const captureRatios = captureResolutions.reduce((rs, [w, h]) => {
const key = toAspectRatio(w, h);
if (key in previewRatios) {
rs[key] = rs[key] || [];
rs[key].push([w, h]);
}
return rs;
}, {});
return Object.entries(captureRatios)
.map(([aspectRatio,
captureRs]) => [captureRs, previewRatios[aspectRatio]]);
}
/**
* @override
*/
getSortedCandidates(deviceId, previewResolutions) {
const photoResolutions = this.deviceResolutions_[deviceId];
const prefR = this.prefResolution_[deviceId] || {width: 0, height: -1};
return this
.pairCapturePreviewResolutions_(photoResolutions, previewResolutions)
.map(([captureRs, previewRs]) => {
if (captureRs.some(
([w, h]) => w == prefR.width && h == prefR.height)) {
var captureR = [prefR.width, prefR.height];
} else {
var captureR = captureRs.reduce(
(captureR, r) => (r[0] > captureR[0] ? r : captureR), [0, -1]);
}
const candidates = [...previewRs]
.sort(([w, h], [w2, h2]) => w2 - w)
.map(([width, height]) => ({
audio: false,
video: {
deviceId: {exact: deviceId},
frameRate: {min: 24},
width,
height,
},
}));
// Format of map result:
// [
// [[CaptureW 1, CaptureH 1], [CaptureW 2, CaptureH 2], ...],
// [PreviewConstraint 1, PreviewConstraint 2, ...]
// ]
return [captureR, candidates];
})
.sort(([[w, h]], [[w2, h2]]) => {
if (w == w2 && h == h2) {
return 0;
}
if (w == prefR.width && h == prefR.height) {
return -1;
}
if (w2 == prefR.width && h2 == prefR.height) {
return 1;
}
return w2 * h2 - w * h;
});
}
};