blob: 59846c9dd066e16192f76d334651b2ff82114ce2 [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 mojo.
*/
cca.mojo = cca.mojo || {};
/**
* Type definition for cca.mojo.PhotoCapabilities.
* @extends {PhotoCapabilities}
* @record
*/
cca.mojo.PhotoCapabilities = function() {};
/** @type {Array<string>} */
cca.mojo.PhotoCapabilities.prototype.supportedEffects;
/**
* The mojo interface of CrOS Camera App API. It provides a singleton
* instance.
*/
cca.mojo.MojoInterface = class {
/** @public */
constructor() {
/**
* @type {cros.mojom.CameraAppDeviceProviderRemote} An interface remote that
* used to construct the mojo interface.
*/
this.deviceProvider = cros.mojom.CameraAppDeviceProvider.getRemote();
/**
* @type {Map<string, Promise<cros.mojom.CameraAppDeviceRemote>>} A map
* that stores the promise of remote that could be used to communicate
* with camera device through mojo interface.
*/
this.devices_ = new Map();
}
/**
* Gets the singleton instance.
* @return {cca.mojo.MojoInterface} The singleton instance of this object.
*/
static getInstance() {
if (!cca.mojo.MojoInterface.instance_) {
/**
* @type {cca.mojo.MojoInterface} The singleton instance of this object.
*/
cca.mojo.MojoInterface.instance_ = new cca.mojo.MojoInterface();
}
return cca.mojo.MojoInterface.instance_;
}
/**
* Gets the mojo interface remote which could be used to get the
* CameraAppDevice from Chrome.
* @return {cros.mojom.CameraAppDeviceProviderRemote} The mojo interface
* remote.
*/
getDeviceProvider() {
return this.deviceProvider;
}
/**
* Gets the mojo interface remote which could be used to communicate with
* Camera device in Chrome.
* @param {string} deviceId The id of the target camera device.
* @return {Promise<cros.mojom.CameraAppDeviceRemote>} The mojo interface
* remote of the camera device.
*/
getDevice(deviceId) {
if (this.devices_.has(deviceId)) {
return this.devices_.get(deviceId);
}
let device = this.deviceProvider.getCameraAppDevice(deviceId).then(
({status, device}) => {
if (status !== cros.mojom.GetCameraAppDeviceStatus.SUCCESS) {
console.error(
'Failed to get CameraAppDevice, error code: ', status);
// TODO(wtlee): Handle by different status.
}
return device;
});
this.devices_.set(deviceId, device);
return device;
}
/**
* Closes all mojo connections to devices.
*/
closeConnections() {
this.devices_.clear();
}
};
/**
* Creates the wrapper of JS image-capture and Mojo image-capture.
* @param {!MediaStreamTrack} videoTrack A video track whose still images will
* be taken.
* @constructor
*/
cca.mojo.ImageCapture = function(videoTrack) {
/**
* @type {string} The id of target media device.
*/
this.deviceId_ = cca.mojo.ImageCapture.resolveDeviceId(videoTrack) || '';
/**
* @type {ImageCapture}
* @private
*/
this.capture_ = new ImageCapture(videoTrack);
// End of properties, seal the object.
Object.seal(this);
};
/**
* Gets the data from Camera metadata by its tag.
* @param {cros.mojom.CameraMetadata} metadata Camera metadata from which to
* query the data.
* @param {cros.mojom.CameraMetadataTag} tag Camera metadata tag to query for.
* @return {Array<number>} An array containing elements whose types correspond
* to the format of input |tag|. If nothing is found, returns an empty
* array.
* @private
*/
cca.mojo.getMetadataData_ = function(metadata, tag) {
for (let i = 0; i < metadata.entryCount; i++) {
let entry = metadata.entries[i];
if (entry.tag !== tag) {
continue;
}
const {buffer} = Uint8Array.from(entry.data);
switch (entry.type) {
case cros.mojom.EntryType.TYPE_BYTE:
return Array.from(new Uint8Array(buffer));
case cros.mojom.EntryType.TYPE_INT32:
return Array.from(new Int32Array(buffer));
case cros.mojom.EntryType.TYPE_FLOAT:
return Array.from(new Float32Array(buffer));
case cros.mojom.EntryType.TYPE_DOUBLE:
return Array.from(new Float64Array(buffer));
case cros.mojom.EntryType.TYPE_INT64:
return Array.from(new BigInt64Array(buffer), (bigIntVal) => {
let numVal = Number(bigIntVal);
if (!Number.isSafeInteger(numVal)) {
console.warn('The int64 value is not a safe integer');
}
return numVal;
});
default:
// TODO(wtlee): Supports rational type.
throw new Error('Unsupported type: ' + entry.type);
}
}
return [];
};
/**
* Resolves video device id from its video track.
* @param {MediaStreamTrack} videoTrack Video track of device to be resolved.
* @return {?string} Resolved video device id. Returns null for unable to
* resolve.
*/
cca.mojo.ImageCapture.resolveDeviceId = function(videoTrack) {
const trackSettings = videoTrack.getSettings && videoTrack.getSettings();
return trackSettings && trackSettings.deviceId || null;
};
/**
* Gets the photo capabilities with the available options/effects.
* @return {Promise<cca.mojo.PhotoCapabilities>} Promise for the result.
*/
cca.mojo.ImageCapture.prototype.getPhotoCapabilities = async function() {
// TODO(wtlee): Change to portrait mode tag.
// This should be 0x80000000 but mojo interface will convert the tag to int32.
const portraitModeTag =
/** @type{cros.mojom.CameraMetadataTag} */ (-0x80000000);
let device =
await cca.mojo.MojoInterface.getInstance().getDevice(this.deviceId_);
if (device === null) {
throw new Error('Fail to construct device remote.');
}
let [/** @type {cca.mojo.PhotoCapabilities} */capabilities,
{/** @type {cros.mojom.CameraInfo} */cameraInfo},
] = await Promise.all([
this.capture_.getPhotoCapabilities(),
device.getCameraInfo(),
]);
if (cameraInfo === null) {
throw new Error('No photo capabilities is found for given device id.');
}
const staticMetadata = cameraInfo.staticCameraCharacteristics;
let supportedEffects = [cros.mojom.Effect.NO_EFFECT];
if (cca.mojo.getMetadataData_(staticMetadata, portraitModeTag).length > 0) {
supportedEffects.push(cros.mojom.Effect.PORTRAIT_MODE);
}
capabilities.supportedEffects = supportedEffects;
return capabilities;
};
/**
* Takes single or multiple photo(s) with the specified settings and effects.
* The amount of result photo(s) depends on the specified settings and effects,
* and the first promise in the returned array will always resolve with the
* unreprocessed photo.
* @param {!PhotoSettings} photoSettings Photo settings for ImageCapture's
* takePhoto().
* @param {?Array<cros.mojom.Effect>} photoEffects Photo effects to be applied.
* @return {Array<Promise<Blob>>} Array of promises for the result.
*/
cca.mojo.ImageCapture.prototype.takePhoto = function(
photoSettings, photoEffects) {
const takes = [];
if (photoEffects) {
photoEffects.forEach((effect) => {
const take = (async () => {
let device = await cca.mojo.MojoInterface.getInstance().getDevice(
this.deviceId_);
if (device === null) {
throw new Error('Failed to construct the device connection.');
}
let {status, blob} = await device.setReprocessOption(effect);
if (status !== 0) {
throw new Error('Mojo image capture error: ' + status);
}
const {data, mimeType} = blob;
return new Blob([new Uint8Array(data)], {type: mimeType});
})();
takes.push(take);
});
}
takes.splice(0, 0, this.capture_.takePhoto(photoSettings));
return takes;
};
/**
* Gets supported photo resolutions for specific camera.
* @param {string} deviceId The renderer-facing device Id of the target camera
* which could be retrieved from MediaDeviceInfo.deviceId.
* @return {Promise<Array<Object>>} Promise of supported resolutions. Each
* photo resolution is represented as [width, height].
*/
cca.mojo.getPhotoResolutions = async function(deviceId) {
const formatBlob = 33;
const typeOutputStream = 0;
const numElementPerEntry = 4;
let device = await cca.mojo.MojoInterface.getInstance().getDevice(deviceId);
if (device === null) {
throw new Error('Failed to construct the device connection.');
}
let {cameraInfo} = await device.getCameraInfo();
const staticMetadata = cameraInfo.staticCameraCharacteristics;
const streamConfigs = cca.mojo.getMetadataData_(
staticMetadata,
cros.mojom.CameraMetadataTag
.ANDROID_SCALER_AVAILABLE_STREAM_CONFIGURATIONS);
// The data of |streamConfigs| looks like:
// streamConfigs: [FORMAT_1, WIDTH_1, HEIGHT_1, TYPE_1,
// FORMAT_2, WIDTH_2, HEIGHT_2, TYPE_2, ...]
if (streamConfigs.length % numElementPerEntry != 0) {
throw new Error('Unexpected length of stream configurations');
}
let supportedResolutions = [];
for (let i = 0; i < streamConfigs.length; i += numElementPerEntry) {
const [format, width, height, type] =
streamConfigs.slice(i, i + numElementPerEntry);
if (format === formatBlob && type === typeOutputStream) {
supportedResolutions.push([width, height]);
}
}
return supportedResolutions;
};
/**
* Gets supported video configurations for specific camera.
* @param {string} deviceId The renderer-facing device Id of the target camera
* which could be retrieved from MediaDeviceInfo.deviceId.
* @return {Promise<Array<Object>>} Promise of supported video configurations.
* Each configuration is represented as [width, height, maxFps].
*/
cca.mojo.getVideoConfigs = async function(deviceId) {
// Currently we use YUV format for both recording and previewing on Chrome.
const formatYuv = 35;
const oneSecondInNs = 1000000000;
const numElementPerEntry = 4;
let device = await cca.mojo.MojoInterface.getInstance().getDevice(deviceId);
if (device === null) {
throw new Error('Failed to construct the device connection.');
}
let {cameraInfo} = await device.getCameraInfo();
const staticMetadata = cameraInfo.staticCameraCharacteristics;
const minFrameDurationConfigs = cca.mojo.getMetadataData_(
staticMetadata,
cros.mojom.CameraMetadataTag
.ANDROID_SCALER_AVAILABLE_MIN_FRAME_DURATIONS);
// The data of |minFrameDurationConfigs| looks like:
// minFrameDurationCOnfigs: [FORMAT_1, WIDTH_1, HEIGHT_1, DURATION_1,
// FORMAT_2, WIDTH_2, HEIGHT_2, DURATION_2,
// ...]
if (minFrameDurationConfigs.length % numElementPerEntry != 0) {
throw new Error('Unexpected length of frame durations configs');
}
let supportedConfigs = [];
for (let i = 0; i < minFrameDurationConfigs.length; i += numElementPerEntry) {
const [format, width, height, minDuration] =
minFrameDurationConfigs.slice(i, i + numElementPerEntry);
if (format === formatYuv) {
const maxFps = Math.round(oneSecondInNs / minDuration);
supportedConfigs.push([width, height, maxFps]);
}
}
return supportedConfigs;
};
/**
* Gets camera facing for given device.
* @param {string} deviceId The renderer-facing device Id of the target camera
* which could be retrieved from MediaDeviceInfo.deviceId.
* @return {Promise<cros.mojom.CameraFacing>} Promise of device facing.
*/
cca.mojo.getCameraFacing = async function(deviceId) {
let device = await cca.mojo.MojoInterface.getInstance().getDevice(deviceId);
if (device === null) {
throw new Error('Failed to construct the device connection.');
}
let {cameraInfo} = await device.getCameraInfo();
return cameraInfo.facing;
};
/**
* Gets supported fps ranges for specific camera.
* @param {string} deviceId The renderer-facing device Id of the target camera
* which could be retrieved from MediaDeviceInfo.deviceId.
* @return {Promise<Array<Array<number>>>} Promise of supported fps ranges.
* Each range is represented as [min, max].
*/
cca.mojo.getSupportedFpsRanges = async function(deviceId) {
const numElementPerEntry = 2;
let device = await cca.mojo.MojoInterface.getInstance().getDevice(deviceId);
if (device === null) {
throw new Error('Failed to construct the device connection.');
}
let {cameraInfo} = await device.getCameraInfo();
const staticMetadata = cameraInfo.staticCameraCharacteristics;
const availableFpsRanges = cca.mojo.getMetadataData_(
staticMetadata,
cros.mojom.CameraMetadataTag
.ANDROID_CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
// The data of |availableFpsRanges| looks like:
// availableFpsRanges: [RANGE_1_MIN, RANGE_1_MAX,
// RANGE_2_MIN, RANGE_2_MAX, ...]
if (availableFpsRanges.length % numElementPerEntry != 0) {
throw new Error('Unexpected length of available fps range configs');
}
let supportedFpsRanges = [];
for (let i = 0; i < availableFpsRanges.length; i += numElementPerEntry) {
const [rangeMin, rangeMax] =
availableFpsRanges.slice(i, i + numElementPerEntry);
supportedFpsRanges.push([rangeMin, rangeMax]);
}
return supportedFpsRanges;
};
/**
* Gets user media with custom negotiation through CrOS Camera App API,
* such as frame rate range negotiation.
* @param {string} deviceId The renderer-facing device Id of the target camera
* which could be retrieved from MediaDeviceInfo.deviceId.
* @param {!MediaStreamConstraints} constraints The constraints that would be
* passed to get user media. If frame rate range negotiation is needed, the
* caller should either set exact field or set both min and max fields for
* frame rate property.
* @return {Promise<MediaStream>} Promise of the MediaStream that returned from
* MediaDevices.getUserMedia().
*/
cca.mojo.getUserMedia = async function(deviceId, constraints) {
let streamWidth = 0;
let streamHeight = 0;
let minFrameRate = 0;
let maxFrameRate = 0;
try {
if (constraints && constraints.video && constraints.video.frameRate) {
const frameRate = constraints.video.frameRate;
if (frameRate.exact) {
minFrameRate = frameRate.exact;
maxFrameRate = frameRate.exact;
} else if (frameRate.min && frameRate.max) {
minFrameRate = frameRate.min;
maxFrameRate = frameRate.max;
}
streamWidth = constraints.video.width;
if (typeof streamWidth !== 'number') {
throw new Error('streamWidth expected to be a number');
}
streamHeight = constraints.video.height;
if (typeof streamHeight !== 'number') {
throw new Error('streamHeight expected to be a number');
}
}
let hasSpecifiedFrameRateRange = minFrameRate > 0 && maxFrameRate > 0;
// If the frame rate range is specified in |constraints|, we should try to
// set the frame rate range and should report error if fails since it is
// unexpected.
//
// Otherwise, if the frame rate is incomplete or totally missing in
// |constraints| , we assume the app wants to use default frame rate range.
// We set the frame rate range to an invalid range (e.g. 0 fps) so that it
// will fallback to use the default one.
let device = await cca.mojo.MojoInterface.getInstance().getDevice(deviceId);
if (device === null) {
throw new Error('Failed to construct the device connection.');
}
const {isSuccess} = await device.setFpsRange(
{'width': streamWidth, 'height': streamHeight},
{'start': minFrameRate, 'end': maxFrameRate});
if (!isSuccess && hasSpecifiedFrameRateRange) {
console.error('Failed to negotiate the frame rate range.');
}
} catch (e) {
// Ignore HALv1 Error.
}
return navigator.mediaDevices.getUserMedia(constraints);
};
/**
* Closes all mojo devices connections.
*/
cca.mojo.closeConnections = function() {
cca.mojo.MojoInterface.getInstance().closeConnections();
};