blob: 9851a3bcba2c442f345c7f3de19a36864c1812b5 [file] [log] [blame]
// Copyright 2022 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.
/**
* @fileoverview The avatar-camera component displays a camera interface to
* allow the user to take a selfie.
*/
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.m.js';
import 'chrome://resources/polymer/v3_0/paper-spinner/paper-spinner-lite.js';
import '../cros_button_style.js';
import { assertInstanceof, assertNotReached } from 'chrome://resources/js/assert_ts.js';
import { afterNextRender } from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import { WithPersonalizationStore } from '../personalization_store.js';
import { getTemplate } from './avatar_camera_element.html.js';
import { saveCameraImage } from './user_controller.js';
import { getUserProvider } from './user_interface_provider.js';
import { GetUserMediaProxy, getWebcamUtils } from './webcam_utils_proxy.js';
function getNumFrames(mode) {
switch (mode) {
case "camera" /* CAMERA */:
return 1;
case "video" /* VIDEO */:
const webcamUtils = getWebcamUtils();
return webcamUtils.CAPTURE_DURATION_MS / webcamUtils.CAPTURE_INTERVAL_MS;
default:
assertNotReached(`Called with impossible AvatarCameraMode: ${mode}`);
}
}
function getCaptureSize(mode) {
const webcamUtils = getWebcamUtils();
switch (mode) {
case "camera" /* CAMERA */:
return webcamUtils.CAPTURE_SIZE;
case "video" /* VIDEO */:
return {
height: webcamUtils.CAPTURE_SIZE.height / 2,
width: webcamUtils.CAPTURE_SIZE.width / 2,
};
default:
assertNotReached(`Called with impossible AvatarCameraMode: ${mode}`);
}
}
export class AvatarCamera extends WithPersonalizationStore {
static get is() {
return 'avatar-camera';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
/**
* Set mode property to switch between camera and video.
*/
mode: {
type: String,
value: "camera" /* CAMERA */,
},
/** Keep track of the open handle to the webcam. */
cameraStream_: {
type: Object,
value: null,
},
/**
* Store a reference to the captured png data to know if the user has
* captured an image yet.
*/
pngBinary_: {
type: Object,
value: null,
},
/** Show the image as a blob to avoid URL length limits. */
previewBlobUrl_: {
type: String,
computed: 'computePreviewBlobUrl_(pngBinary_)',
observer: 'onPreviewBlobUrlChanged_',
},
captureInProgress_: {
type: Boolean,
value: false,
},
};
}
connectedCallback() {
super.connectedCallback();
this.startCamera_();
}
disconnectedCallback() {
super.disconnectedCallback();
this.stopCamera_();
}
computePreviewBlobUrl_() {
if (!this.pngBinary_) {
return null;
}
assertInstanceof(this.pngBinary_, Uint8Array, 'Preview binary should be a png uint8array');
const blob = new Blob([this.pngBinary_], { type: 'image/png' });
return URL.createObjectURL(blob);
}
onPreviewBlobUrlChanged_(_, old) {
if (old) {
// Revoke the last one to free memory.
URL.revokeObjectURL(old);
}
}
async startCamera_() {
this.stopCamera_();
try {
this.cameraStream_ = await GetUserMediaProxy.getInstance().getUserMedia();
if (!this.isConnected) {
// User closed the camera UI while waiting for the camera to start.
this.stopCamera_();
return;
}
const video = this.$.webcamVideo;
// Display the webcam feed to the user by binding it to |video|.
video.srcObject = this.cameraStream_;
await new Promise((resolve) => afterNextRender(this, resolve));
this.shadowRoot.getElementById('takePhoto').focus();
}
catch (e) {
console.error('Unable to start camera', e);
this.stopCamera_();
}
}
/**
* If the camera is active, stop all the active media. Safe to call even if
* the camera is off.
*/
stopCamera_() {
getWebcamUtils().stopMediaTracks(this.cameraStream_);
this.cameraStream_ = null;
this.pngBinary_ = null;
}
async takePhoto_() {
const webcamUtils = getWebcamUtils();
try {
this.captureInProgress_ = true;
// Let the animation start smoothly before beginning the capture.
await new Promise(resolve => requestAnimationFrame(resolve));
const frames = await webcamUtils.captureFrames(this.$.webcamVideo, getCaptureSize(this.mode), webcamUtils.CAPTURE_INTERVAL_MS, getNumFrames(this.mode));
this.pngBinary_ = webcamUtils.convertFramesToPngBinary(frames);
await new Promise(resolve => afterNextRender(this, resolve));
this.shadowRoot.getElementById('clearPhoto').focus();
}
catch (e) {
console.error('Failed to capture from webcam', e);
}
finally {
this.captureInProgress_ = false;
}
}
confirmPhoto_() {
assertInstanceof(this.pngBinary_, Uint8Array, 'Preview image binary must be set to confirm photo');
saveCameraImage(this.pngBinary_, getUserProvider());
this.pngBinary_ = null;
// Close the camera interface when an image is confirmed.
this.$.dialog.close();
}
clearPhoto_() {
this.pngBinary_ = null;
}
showLoading_() {
return !this.cameraStream_ && !this.previewBlobUrl_;
}
showSvgMask_() {
return this.showCameraFeed_() || !!this.previewBlobUrl_;
}
showCameraFeed_() {
return !!this.cameraStream_ && !this.previewBlobUrl_;
}
showTakePhotoButton_() {
return this.showCameraFeed_() && !this.captureInProgress_;
}
showLoadingSpinnerButton_() {
return this.mode === "video" /* VIDEO */ && this.showCameraFeed_() &&
this.captureInProgress_;
}
showFooter_() {
return this.showCameraFeed_() || !!this.previewBlobUrl_;
}
getTakePhotoIcon_(mode) {
return mode === "video" /* VIDEO */ ? 'personalization:loop' :
'personalization:camera_compact';
}
getTakePhotoText_(mode) {
return mode === "video" /* VIDEO */ ? this.i18n('takeWebcamVideo') :
this.i18n('takeWebcamPhoto');
}
getConfirmText_(mode) {
return mode === "video" /* VIDEO */ ? this.i18n('confirmWebcamVideo') :
this.i18n('confirmWebcamPhoto');
}
}
customElements.define(AvatarCamera.is, AvatarCamera);