| // 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_); |
| } |
| } |