blob: df6cd459a0a8f6a3c16065f0f5dab48404adcf14 [file] [log] [blame]
// Copyright (c) 2020 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 {assertInstanceof, assertString} from '../../../chrome_util.js';
import {reportError} from '../../../error.js';
import {I18nString} from '../../../i18n_string.js';
import {Filenamer} from '../../../models/file_namer.js';
import * as filesystem from '../../../models/file_system.js';
import {DeviceOperator, parseMetadata} from '../../../mojo/device_operator.js';
import {CrosImageCapture} from '../../../mojo/image_capture.js';
import * as state from '../../../state.js';
import * as toast from '../../../toast.js';
import {
ErrorLevel,
ErrorType,
Facing, // eslint-disable-line no-unused-vars
PerfEvent,
Resolution,
} from '../../../type.js';
import * as util from '../../../util.js';
import {ModeBase, ModeFactory} from './mode_base.js';
/**
* Contains photo taking result.
* @typedef {{
* resolution: !Resolution,
* blob: !Blob,
* isVideoSnapshot: (boolean|undefined),
* }}
*/
export let PhotoResult;
/**
* Provides external dependency functions used by photo mode and handles the
* captured result photo.
* @interface
*/
export class PhotoHandler {
/**
* Handles the result photo.
* @param {!PhotoResult} photo Captured photo result.
* @param {string} name Name of the photo result to be saved as.
* @return {!Promise}
* @abstract
*/
handleResultPhoto(photo, name) {}
/**
* Plays UI effect when taking photo.
*/
playShutterEffect() {}
/**
* Gets frame image blob from current preview.
* @return {!Promise<!Blob>}
* @abstract
*/
getPreviewFrame() {}
}
/**
* Photo mode capture controller.
*/
export class Photo extends ModeBase {
/**
* @param {!MediaStream} stream
* @param {!Facing} facing
* @param {?Resolution} captureResolution
* @param {!PhotoHandler} handler
*/
constructor(stream, facing, captureResolution, handler) {
super(stream, facing);
/**
* Capture resolution. May be null on device not support of setting
* resolution.
* @type {?Resolution}
* @protected
*/
this.captureResolution_ = captureResolution;
/**
* @const {!PhotoHandler}
* @protected
*/
this.handler_ = handler;
/**
* CrosImageCapture object to capture still photos.
* @type {?CrosImageCapture}
* @protected
*/
this.crosImageCapture_ = null;
/**
* The observer id for saving metadata.
* @type {?number}
* @protected
*/
this.metadataObserverId_ = null;
/**
* Metadata names ready to be saved.
* @type {!Array<string>}
* @protected
*/
this.metadataNames_ = [];
}
/**
* @override
*/
async start_() {
if (this.crosImageCapture_ === null) {
this.crosImageCapture_ =
new CrosImageCapture(this.stream_.getVideoTracks()[0]);
}
const imageName = (new Filenamer()).newImageName();
if (this.metadataObserverId_ !== null) {
this.metadataNames_.push(Filenamer.getMetadataName(imageName));
}
state.set(PerfEvent.PHOTO_CAPTURE_SHUTTER, true);
try {
state.set(PerfEvent.PHOTO_CAPTURE_SHUTTER, false, {facing: this.facing_});
this.handler_.playShutterEffect();
state.set(PerfEvent.PHOTO_CAPTURE_POST_PROCESSING, true);
const blob = await this.takePhoto_();
const image = await util.blobToImage(blob);
const resolution = new Resolution(image.width, image.height);
await this.handler_.handleResultPhoto({resolution, blob}, imageName);
state.set(
PerfEvent.PHOTO_CAPTURE_POST_PROCESSING, false,
{resolution, facing: this.facing_});
} catch (e) {
state.set(PerfEvent.PHOTO_CAPTURE_SHUTTER, false, {hasError: true});
state.set(
PerfEvent.PHOTO_CAPTURE_POST_PROCESSING, false, {hasError: true});
toast.show(I18nString.ERROR_MSG_TAKE_PHOTO_FAILED);
throw e;
}
}
/**
* @return {!Promise<!Blob>}
*/
async takePhoto_() {
if (state.get(state.State.ENABLE_PTZ)) {
// Workaround for b/184089334 on PTZ camera to use preview frame as
// photo result.
return this.handler_.getPreviewFrame();
}
let photoSettings;
if (this.captureResolution_) {
photoSettings = /** @type {!PhotoSettings} */ ({
imageWidth: this.captureResolution_.width,
imageHeight: this.captureResolution_.height,
});
} else {
const caps = await this.crosImageCapture_.getPhotoCapabilities();
photoSettings = /** @type {!PhotoSettings} */ ({
imageWidth: caps.imageWidth.max,
imageHeight: caps.imageHeight.max,
});
}
const results = await this.crosImageCapture_.takePhoto(photoSettings);
return results[0];
}
/**
* Adds an observer to save metadata.
* @return {!Promise} Promise for the operation.
*/
async addMetadataObserver() {
if (!this.stream_) {
return;
}
const deviceOperator = await DeviceOperator.getInstance();
if (!deviceOperator) {
return;
}
const cameraMetadataTagInverseLookup = {};
Object.entries(cros.mojom.CameraMetadataTag).forEach(([key, value]) => {
if (key === 'MIN_VALUE' || key === 'MAX_VALUE') {
return;
}
cameraMetadataTagInverseLookup[value] = key;
});
const callback = (metadata) => {
const parsedMetadata = {};
for (const entry of metadata.entries) {
const key = cameraMetadataTagInverseLookup[entry.tag];
if (key === undefined) {
// TODO(kaihsien): Add support for vendor tags.
continue;
}
const val = parseMetadata(entry);
parsedMetadata[key] = val;
}
filesystem.saveBlob(
new Blob(
[JSON.stringify(parsedMetadata, null, 2)],
{type: 'application/json'}),
this.metadataNames_.shift());
};
const deviceId = this.stream_.getVideoTracks()[0].getSettings().deviceId;
this.metadataObserverId_ = await deviceOperator.addMetadataObserver(
deviceId, callback, cros.mojom.StreamType.JPEG_OUTPUT);
}
/**
* Removes the observer that saves metadata.
* @return {!Promise} Promise for the operation.
*/
async removeMetadataObserver() {
if (!this.stream_ || this.metadataObserverId_ === null) {
return;
}
const deviceOperator = await DeviceOperator.getInstance();
if (!deviceOperator) {
return;
}
const deviceId = this.stream_.getVideoTracks()[0].getSettings().deviceId;
const isSuccess = await deviceOperator.removeMetadataObserver(
deviceId, this.metadataObserverId_);
if (!isSuccess) {
reportError(
ErrorType.REMOVE_METADATA_OBSERVER_FAILURE, ErrorLevel.ERROR,
new Error(`Failed to remove metadata observer with id: ${
this.metadataObserverId_}`));
}
this.metadataObserverId_ = null;
}
}
/**
* Factory for creating photo mode capture object.
*/
export class PhotoFactory extends ModeFactory {
/**
* @param {!PhotoHandler} handler
*/
constructor(handler) {
super();
/**
* @const {!PhotoHandler}
* @protected
*/
this.handler_ = handler;
}
/**
* @override
*/
async prepareDevice(constraints, resolution) {
this.captureResolution_ = resolution;
const deviceOperator = await DeviceOperator.getInstance();
if (deviceOperator !== null) {
const deviceId = assertString(constraints.video.deviceId.exact);
await deviceOperator.setCaptureIntent(
deviceId, cros.mojom.CaptureIntent.STILL_CAPTURE);
await deviceOperator.setStillCaptureResolution(
deviceId, assertInstanceof(this.captureResolution_, Resolution));
}
}
/**
* @override
*/
produce_() {
return new Photo(
this.previewStream_, this.facing_, this.captureResolution_,
this.handler_);
}
}