Commit f8f8df63 authored by Kuo Jen Wei's avatar Kuo Jen Wei Committed by Commit Bot

[CCA] Add camera intent view handling and returning intent result.

Bug: 980812
Test: Send intent from ARC++ app, see if result is correctly return to
app.

Change-Id: I2810a31879abf80657aa75becde3e2a99ddcf1ff
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1804940
Auto-Submit: Kuo Jen Wei <inker@chromium.org>
Reviewed-by: default avatarShik Chen <shik@chromium.org>
Commit-Queue: Shik Chen <shik@chromium.org>
Cr-Commit-Position: refs/heads/master@{#701960}
parent af84347b
...@@ -79,6 +79,9 @@ copy("chrome_camera_app_images") { ...@@ -79,6 +79,9 @@ copy("chrome_camera_app_images") {
"src/images/camera_button_timer_on_10s.svg", "src/images/camera_button_timer_on_10s.svg",
"src/images/camera_button_timer_on_3s.svg", "src/images/camera_button_timer_on_3s.svg",
"src/images/camera_focus_aim.svg", "src/images/camera_focus_aim.svg",
"src/images/camera_intent_play_video.svg",
"src/images/camera_intent_result_cancel.svg",
"src/images/camera_intent_result_confirm.svg",
"src/images/camera_mode_photo.svg", "src/images/camera_mode_photo.svg",
"src/images/camera_mode_portrait.svg", "src/images/camera_mode_portrait.svg",
"src/images/camera_mode_square.svg", "src/images/camera_mode_square.svg",
...@@ -145,6 +148,7 @@ copy("chrome_camera_app_js_device") { ...@@ -145,6 +148,7 @@ copy("chrome_camera_app_js_device") {
"src/js/device/camera3_device_info.js", "src/js/device/camera3_device_info.js",
"src/js/device/constraints_preferrer.js", "src/js/device/constraints_preferrer.js",
"src/js/device/device_info_updater.js", "src/js/device/device_info_updater.js",
"src/js/device/error.js",
] ]
outputs = [ outputs = [
...@@ -154,11 +158,13 @@ copy("chrome_camera_app_js_device") { ...@@ -154,11 +158,13 @@ copy("chrome_camera_app_js_device") {
copy("chrome_camera_app_js_models") { copy("chrome_camera_app_js_models") {
sources = [ sources = [
"src/js/models/file_video_saver.js",
"src/js/models/filenamer.js", "src/js/models/filenamer.js",
"src/js/models/filesystem.js", "src/js/models/filesystem.js",
"src/js/models/gallery.js", "src/js/models/gallery.js",
"src/js/models/intent_video_saver.js",
"src/js/models/result_saver.js", "src/js/models/result_saver.js",
"src/js/models/video_saver.js", "src/js/models/video_saver_interface.js",
] ]
outputs = [ outputs = [
...@@ -182,6 +188,7 @@ copy("chrome_camera_app_js_views") { ...@@ -182,6 +188,7 @@ copy("chrome_camera_app_js_views") {
sources = [ sources = [
"src/js/views/browser.js", "src/js/views/browser.js",
"src/js/views/camera.js", "src/js/views/camera.js",
"src/js/views/camera_intent.js",
"src/js/views/dialog.js", "src/js/views/dialog.js",
"src/js/views/gallery_base.js", "src/js/views/gallery_base.js",
"src/js/views/settings.js", "src/js/views/settings.js",
...@@ -201,6 +208,7 @@ copy("chrome_camera_app_js_views_camera") { ...@@ -201,6 +208,7 @@ copy("chrome_camera_app_js_views_camera") {
"src/js/views/camera/options.js", "src/js/views/camera/options.js",
"src/js/views/camera/preview.js", "src/js/views/camera/preview.js",
"src/js/views/camera/recordtime.js", "src/js/views/camera/recordtime.js",
"src/js/views/camera/review_result.js",
"src/js/views/camera/timertick.js", "src/js/views/camera/timertick.js",
] ]
......
...@@ -93,6 +93,9 @@ RESOURCES = \ ...@@ -93,6 +93,9 @@ RESOURCES = \
src/images/camera_button_timer_on_10s.svg \ src/images/camera_button_timer_on_10s.svg \
src/images/camera_button_timer_on_3s.svg \ src/images/camera_button_timer_on_3s.svg \
src/images/camera_focus_aim.svg \ src/images/camera_focus_aim.svg \
src/images/camera_intent_play_video.svg \
src/images/camera_intent_result_cancel.svg \
src/images/camera_intent_result_confirm.svg \
src/images/camera_mode_photo.svg \ src/images/camera_mode_photo.svg \
src/images/camera_mode_portrait.svg \ src/images/camera_mode_portrait.svg \
src/images/camera_mode_square.svg \ src/images/camera_mode_square.svg \
...@@ -119,16 +122,19 @@ RESOURCES = \ ...@@ -119,16 +122,19 @@ RESOURCES = \
src/js/device/camera3_device_info.js \ src/js/device/camera3_device_info.js \
src/js/device/constraints_preferrer.js \ src/js/device/constraints_preferrer.js \
src/js/device/device_info_updater.js \ src/js/device/device_info_updater.js \
src/js/device/error.js \
src/js/gallerybutton.js \ src/js/gallerybutton.js \
src/js/google-analytics-bundle.js \ src/js/google-analytics-bundle.js \
src/js/intent.js \ src/js/intent.js \
src/js/main.js \ src/js/main.js \
src/js/metrics.js \ src/js/metrics.js \
src/js/models/file_video_saver.js \
src/js/models/filenamer.js \ src/js/models/filenamer.js \
src/js/models/filesystem.js \ src/js/models/filesystem.js \
src/js/models/gallery.js \ src/js/models/gallery.js \
src/js/models/intent_video_saver.js \
src/js/models/result_saver.js \ src/js/models/result_saver.js \
src/js/models/video_saver.js \ src/js/models/video_saver_interface.js \
src/js/mojo/chrome_helper.js \ src/js/mojo/chrome_helper.js \
src/js/mojo/device_operator.js \ src/js/mojo/device_operator.js \
src/js/mojo/image_capture.js \ src/js/mojo/image_capture.js \
...@@ -142,11 +148,13 @@ RESOURCES = \ ...@@ -142,11 +148,13 @@ RESOURCES = \
src/js/util.js \ src/js/util.js \
src/js/views/browser.js \ src/js/views/browser.js \
src/js/views/camera.js \ src/js/views/camera.js \
src/js/views/camera_intent.js \
src/js/views/camera/layout.js \ src/js/views/camera/layout.js \
src/js/views/camera/modes.js \ src/js/views/camera/modes.js \
src/js/views/camera/options.js \ src/js/views/camera/options.js \
src/js/views/camera/preview.js \ src/js/views/camera/preview.js \
src/js/views/camera/recordtime.js \ src/js/views/camera/recordtime.js \
src/js/views/camera/review_result.js \
src/js/views/camera/timertick.js \ src/js/views/camera/timertick.js \
src/js/views/dialog.js \ src/js/views/dialog.js \
src/js/views/gallery_base.js \ src/js/views/gallery_base.js \
......
...@@ -17,11 +17,13 @@ ...@@ -17,11 +17,13 @@
<structure name="IDR_CAMERA_BUNDLE_JS" file="src/js/google-analytics-bundle.js" type="chrome_html" /> <structure name="IDR_CAMERA_BUNDLE_JS" file="src/js/google-analytics-bundle.js" type="chrome_html" />
<structure name="IDR_CAMERA_CAMERA3_DEVICE_INFO_JS" file="src/js/device/camera3_device_info.js" type="chrome_html" /> <structure name="IDR_CAMERA_CAMERA3_DEVICE_INFO_JS" file="src/js/device/camera3_device_info.js" type="chrome_html" />
<structure name="IDR_CAMERA_CAMERA_JS" file="src/js/views/camera.js" type="chrome_html" /> <structure name="IDR_CAMERA_CAMERA_JS" file="src/js/views/camera.js" type="chrome_html" />
<structure name="IDR_CAMERA_CAMERA_INTENT_JS" file="src/js/views/camera_intent.js" type="chrome_html" />
<structure name="IDR_CAMERA_CONSTRAINTS_PREFERRER_JS" file="src/js/device/constraints_preferrer.js" type="chrome_html" /> <structure name="IDR_CAMERA_CONSTRAINTS_PREFERRER_JS" file="src/js/device/constraints_preferrer.js" type="chrome_html" />
<structure name="IDR_CAMERA_CHROME_HELPER_JS" file="src/js/mojo/chrome_helper.js" type="chrome_html" /> <structure name="IDR_CAMERA_CHROME_HELPER_JS" file="src/js/mojo/chrome_helper.js" type="chrome_html" />
<structure name="IDR_CAMERA_DEVICE_OPERATOR_JS" file="src/js/mojo/device_operator.js" type="chrome_html" /> <structure name="IDR_CAMERA_DEVICE_OPERATOR_JS" file="src/js/mojo/device_operator.js" type="chrome_html" />
<structure name="IDR_CAMERA_DEVICE_INFO_UPDATER_JS" file="src/js/device/device_info_updater.js" type="chrome_html" /> <structure name="IDR_CAMERA_DEVICE_INFO_UPDATER_JS" file="src/js/device/device_info_updater.js" type="chrome_html" />
<structure name="IDR_CAMERA_DIALOG_JS" file="src/js/views/dialog.js" type="chrome_html" /> <structure name="IDR_CAMERA_DIALOG_JS" file="src/js/views/dialog.js" type="chrome_html" />
<structure name="IDR_CAMERA_ERROR_JS" file="src/js/device/error.js" type="chrome_html" />
<structure name="IDR_CAMERA_FILENAMER_JS" file="src/js/models/filenamer.js" type="chrome_html" /> <structure name="IDR_CAMERA_FILENAMER_JS" file="src/js/models/filenamer.js" type="chrome_html" />
<structure name="IDR_CAMERA_FILESYSTEM_JS" file="src/js/models/filesystem.js" type="chrome_html" /> <structure name="IDR_CAMERA_FILESYSTEM_JS" file="src/js/models/filesystem.js" type="chrome_html" />
<structure name="IDR_CAMERA_GALLERY_BASE_JS" file="src/js/views/gallery_base.js" type="chrome_html" /> <structure name="IDR_CAMERA_GALLERY_BASE_JS" file="src/js/views/gallery_base.js" type="chrome_html" />
...@@ -42,6 +44,7 @@ ...@@ -42,6 +44,7 @@
<structure name="IDR_CAMERA_RECORDTIME_JS" file="src/js/views/camera/recordtime.js" type="chrome_html" /> <structure name="IDR_CAMERA_RECORDTIME_JS" file="src/js/views/camera/recordtime.js" type="chrome_html" />
<structure name="IDR_CAMERA_RESOLUTION_EVENT_BROKER_JS" file="src/js/resolution_event_broker.js" type="chrome_html" /> <structure name="IDR_CAMERA_RESOLUTION_EVENT_BROKER_JS" file="src/js/resolution_event_broker.js" type="chrome_html" />
<structure name="IDR_CAMERA_RESULT_SAVER_JS" file="src/js/models/result_saver.js" type="chrome_html" /> <structure name="IDR_CAMERA_RESULT_SAVER_JS" file="src/js/models/result_saver.js" type="chrome_html" />
<structure name="IDR_CAMERA_REVIEW_RESULT_JS" file="src/js/views/camera/review_result.js" type="chrome_html" />
<structure name="IDR_CAMERA_SCROLLBAR_JS" file="src/js/scrollbar.js" type="chrome_html" /> <structure name="IDR_CAMERA_SCROLLBAR_JS" file="src/js/scrollbar.js" type="chrome_html" />
<structure name="IDR_CAMERA_SETTINGS_JS" file="src/js/views/settings.js" type="chrome_html" /> <structure name="IDR_CAMERA_SETTINGS_JS" file="src/js/views/settings.js" type="chrome_html" />
<structure name="IDR_CAMERA_SOUND_JS" file="src/js/sound.js" type="chrome_html" /> <structure name="IDR_CAMERA_SOUND_JS" file="src/js/sound.js" type="chrome_html" />
...@@ -50,7 +53,9 @@ ...@@ -50,7 +53,9 @@
<structure name="IDR_CAMERA_TOAST_JS" file="src/js/toast.js" type="chrome_html" /> <structure name="IDR_CAMERA_TOAST_JS" file="src/js/toast.js" type="chrome_html" />
<structure name="IDR_CAMERA_TOOLTIP_JS" file="src/js/tooltip.js" type="chrome_html" /> <structure name="IDR_CAMERA_TOOLTIP_JS" file="src/js/tooltip.js" type="chrome_html" />
<structure name="IDR_CAMERA_UTIL_JS" file="src/js/util.js" type="chrome_html" /> <structure name="IDR_CAMERA_UTIL_JS" file="src/js/util.js" type="chrome_html" />
<structure name="IDR_CAMERA_VIDEO_SAVER_JS" file="src/js/models/video_saver.js" type="chrome_html" /> <structure name="IDR_CAMERA_VIDEO_SAVER_INTERFACE_JS" file="src/js/models/video_saver_interface.js" type="chrome_html" />
<structure name="IDR_CAMERA_INTENT_VIDEO_SAVER_JS" file="src/js/models/file_video_saver.js" type="chrome_html" />
<structure name="IDR_CAMERA_FILE_VIDEO_SAVER_JS" file="src/js/models/intent_video_saver.js" type="chrome_html" />
<structure name="IDR_CAMERA_VIEW_JS" file="src/js/views/view.js" type="chrome_html" /> <structure name="IDR_CAMERA_VIEW_JS" file="src/js/views/view.js" type="chrome_html" />
<structure name="IDR_CAMERA_WARNING_JS" file="src/js/views/warning.js" type="chrome_html" /> <structure name="IDR_CAMERA_WARNING_JS" file="src/js/views/warning.js" type="chrome_html" />
<structure name="IDR_CAMERA_WEBUI_BROWSER_PROXY" file="src/js/browser_proxy/webui_browser_proxy.js" type="chrome_html" /> <structure name="IDR_CAMERA_WEBUI_BROWSER_PROXY" file="src/js/browser_proxy/webui_browser_proxy.js" type="chrome_html" />
......
...@@ -214,6 +214,14 @@ body.tab-navigation .circle input:focus::after { ...@@ -214,6 +214,14 @@ body.tab-navigation .circle input:focus::after {
bottom: calc((var(--modes-bottom) + var(--modes-height)) + 16px); bottom: calc((var(--modes-bottom) + var(--modes-height)) + 16px);
} }
body.review-result #shutters-group {
display: none;
}
body.should-handle-intent-result #shutters-group {
bottom: var(--modes-bottom);
}
body.tablet-landscape .actions-group { body.tablet-landscape .actions-group {
flex-direction: column-reverse; flex-direction: column-reverse;
} }
...@@ -239,6 +247,52 @@ body.taking #modes-group { ...@@ -239,6 +247,52 @@ body.taking #modes-group {
padding: var(--modes-gradient-padding) 8px; padding: var(--modes-gradient-padding) 8px;
} }
body.should-handle-intent-result #modes-group {
display: none;
}
.preview-content {
height: 0; /* Calculate at runtime. */
width: 0; /* Calculate at runtime. */
}
#play-result-video {
background-image: url(../images/camera_intent_play_video.svg);
height: 80px;
width: 80px;
}
#confirm-result-groups {
bottom: calc(var(--bottom-line) - 36px);
display: flex;
flex-direction: column;
}
#confirm-result-groups>button {
flex: 0 0 76px;
height: 72px;
width: 72px;
}
body.review-result #preview-video,
body:not(.review-result) #review-photo-result,
body:not(.review-result) #review-video-result,
body:not(.review-photo-result) #review-photo-result,
body.review-photo-result #review-video-result,
body.review-photo-result #play-result-video,
body.playing-result-video #play-result-video,
body:not(.review-result) #confirm-result-groups {
display: none;
}
#confirm-result {
background-image: url(../images/camera_intent_result_confirm.svg);
}
#cancel-result {
background-image: url(../images/camera_intent_result_cancel.svg);
}
.mode-item { .mode-item {
flex: 0 0 var(--mode-item-height); flex: 0 0 var(--mode-item-height);
position: relative; position: relative;
...@@ -415,6 +469,10 @@ body.mode-switching:not(.streaming) #camera-mode { ...@@ -415,6 +469,10 @@ body.mode-switching:not(.streaming) #camera-mode {
width: var(--big-icon); width: var(--big-icon);
} }
body.should-handle-intent-result #gallery-enter {
display: none;
}
.centered-overlay { .centered-overlay {
left: 50%; left: 50%;
position: absolute; position: absolute;
...@@ -495,6 +553,10 @@ body._60fps #toggle-fps { ...@@ -495,6 +553,10 @@ body._60fps #toggle-fps {
background-image: url(../images/camera_button_settings.svg); background-image: url(../images/camera_button_settings.svg);
} }
body.should-handle-intent-result #open-settings {
display: none;
}
#camera, #camera,
#settings, #settings,
#gridsettings, #gridsettings,
...@@ -606,7 +668,7 @@ body:not(.w-letterbox).preview-vertical-dock #camera { ...@@ -606,7 +668,7 @@ body:not(.w-letterbox).preview-vertical-dock #camera {
} }
#preview-wrapper, #preview-wrapper,
#preview-video { .preview-content {
flex-shrink: 0; flex-shrink: 0;
pointer-events: none; pointer-events: none;
position: relative; position: relative;
...@@ -625,17 +687,17 @@ body.square-preview #preview-wrapper { ...@@ -625,17 +687,17 @@ body.square-preview #preview-wrapper {
width: 0; /* Calculate at runtime. */ width: 0; /* Calculate at runtime. */
} }
body.square-preview #preview-video { body.square-preview .preview-content {
left: 0; /* Calculate at runtime. */ left: 0; /* Calculate at runtime. */
position: absolute; position: absolute;
top: 0; /* Calculate at runtime. */ top: 0; /* Calculate at runtime. */
} }
body.streaming #preview-video { body.streaming .preview-content {
pointer-events: auto; pointer-events: auto;
} }
body.mirror #preview-video, body.mirror .preview-content ,
body.mirror #preview-focus { body.mirror #preview-focus {
transform: scaleX(-1); transform: scaleX(-1);
} }
...@@ -1270,6 +1332,6 @@ body.tab-navigation .dialog-buttons button:focus::after { ...@@ -1270,6 +1332,6 @@ body.tab-navigation .dialog-buttons button:focus::after {
z-index: 1; z-index: 1;
} }
body:not(.mode-switching):not(.streaming) #spinner { body:not(.mode-switching):not(.streaming):not(.review-result) #spinner {
visibility: visible; visibility: visible;
} }
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 57.1 (83088) - https://sketch.com -->
<title>btn_play_video_1x</title>
<desc>Created with Sketch.</desc>
<g id="btn_play_video_1x" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="camera-button" transform="translate(6.127660, 6.127660)" opacity="0.9">
<circle id="Oval-3" fill="#FFFFFF" cx="30.0154866" cy="30.0154866" r="30.0154866"></circle>
<circle id="Oval-3" fill-opacity="0.1" fill="#5F6368" cx="30.0154866" cy="30.0154866" r="30.0154866"></circle>
</g>
<g id="dense/av/play" transform="translate(29.412766, 24.817021)" fill="#5F6368">
<polygon id="↳Color-fill" points="0 22.7345051 19.4867186 11.3672525 0 0"></polygon>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 57.1 (83088) - https://sketch.com -->
<title>btn_stop_timer_1x</title>
<desc>Created with Sketch.</desc>
<defs>
<polygon id="path-1" points="44 29.6114286 42.3885714 28 36 34.3885714 29.6114286 28 28 29.6114286 34.3885714 36 28 42.3885714 29.6114286 44 36 37.6114286 42.3885714 44 44 42.3885714 37.6114286 36"></polygon>
</defs>
<g id="btn_stop_timer_1x" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<circle id="Oval-3" fill="#BDC1C6" opacity="0.9" cx="36" cy="36" r="30"></circle>
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="ic_close_24px" fill="#FFFFFF" fill-rule="nonzero" xlink:href="#path-1"></use>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 57.1 (83088) - https://sketch.com -->
<title>btn_confirm_1x</title>
<desc>Created with Sketch.</desc>
<defs>
<polygon id="path-1" points="14.3166667 25.9666667 7.36666667 19.0166667 5 21.3666667 14.3166667 30.6833333 34.3166667 10.6833333 31.9666667 8.33333333"></polygon>
</defs>
<g id="btn_confirm_1x" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="camera-button" transform="translate(6.127660, 6.127660)" fill="#1A73E8" opacity="0.9">
<circle id="Oval-3" cx="30.0154866" cy="30.0154866" r="30.0154866"></circle>
<circle id="Oval-3" cx="30.0154866" cy="30.0154866" r="30.0154866"></circle>
</g>
<g id="ic_check_24px" transform="translate(16.000000, 16.000000)">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use fill="#FFFFFF" fill-rule="nonzero" xlink:href="#path-1"></use>
</g>
</g>
</svg>
\ No newline at end of file
...@@ -9,6 +9,7 @@ js_type_check("closure_compile") { ...@@ -9,6 +9,7 @@ js_type_check("closure_compile") {
":camera3_device_info", ":camera3_device_info",
":constraints_preferrer", ":constraints_preferrer",
":device_info_updater", ":device_info_updater",
":error",
] ]
} }
...@@ -31,6 +32,10 @@ js_library("device_info_updater") { ...@@ -31,6 +32,10 @@ js_library("device_info_updater") {
deps = [ deps = [
":camera3_device_info", ":camera3_device_info",
":constraints_preferrer", ":constraints_preferrer",
":error",
"..:state", "..:state",
] ]
} }
js_library("error") {
}
...@@ -231,7 +231,7 @@ cca.device.DeviceInfoUpdater = class { ...@@ -231,7 +231,7 @@ cca.device.DeviceInfoUpdater = class {
async getDeviceResolutions(deviceId) { async getDeviceResolutions(deviceId) {
const devices = await this.getCamera3DevicesInfo(); const devices = await this.getCamera3DevicesInfo();
if (!devices) { if (!devices) {
throw new Error('HALv1-api'); throw new cca.device.LegacyVCDError();
} }
const info = devices.find((info) => info.deviceId === deviceId); const info = devices.find((info) => info.deviceId === deviceId);
return [info.photoResols, info.videoResols]; return [info.photoResols, info.videoResols];
......
// 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 || {};
/**
* Throws from calls to methods requiring mojo supporting VCD on HALv1 device
* equipped with legacy VCD implementation.
*/
cca.device.LegacyVCDError = class extends Error {
/**
* @param {string=} message
* @public
*/
constructor(
message =
'Call to unsupported mojo operation on legacy VCD implementation.') {
super(message);
this.name = 'LegacyVCDError';
}
};
...@@ -14,6 +14,10 @@ var cca = cca || {}; ...@@ -14,6 +14,10 @@ var cca = cca || {};
* @constructor * @constructor
*/ */
cca.App = function() { cca.App = function() {
const shouldHandleIntentResult =
window.intent !== null && window.intent.shouldHandleResult;
cca.state.set('should-handle-intent-result', shouldHandleIntentResult);
/** /**
* @type {cca.models.Gallery} * @type {cca.models.Gallery}
* @private * @private
...@@ -63,9 +67,13 @@ cca.App = function() { ...@@ -63,9 +67,13 @@ cca.App = function() {
* @type {cca.views.Camera} * @type {cca.views.Camera}
* @private * @private
*/ */
this.cameraView_ = new cca.views.Camera( this.cameraView_ = shouldHandleIntentResult ?
this.gallery_, this.infoUpdater_, this.photoPreferrer_, new cca.views.CameraIntent(
this.videoPreferrer_); window.intent, this.infoUpdater_, this.photoPreferrer_,
this.videoPreferrer_) :
new cca.views.Camera(
this.gallery_, this.infoUpdater_, this.photoPreferrer_,
this.videoPreferrer_);
// End of properties. Seal the object. // End of properties. Seal the object.
Object.seal(this); Object.seal(this);
...@@ -110,8 +118,10 @@ cca.App.prototype.setupToggles_ = function() { ...@@ -110,8 +118,10 @@ cca.App.prototype.setupToggles_ = function() {
cca.proxy.browserProxy.localStorageGet( cca.proxy.browserProxy.localStorageGet(
{expert: false}, ({expert}) => cca.state.set('expert', expert)); {expert: false}, ({expert}) => cca.state.set('expert', expert));
document.querySelectorAll('input').forEach((element) => { document.querySelectorAll('input').forEach((element) => {
element.addEventListener('keypress', (event) => element.addEventListener(
cca.util.getShortcutIdentifier(event) == 'Enter' && element.click()); 'keypress',
(event) => cca.util.getShortcutIdentifier(event) == 'Enter' &&
element.click());
var css = element.getAttribute('data-state'); var css = element.getAttribute('data-state');
var key = element.getAttribute('data-key'); var key = element.getAttribute('data-key');
...@@ -129,14 +139,15 @@ cca.App.prototype.setupToggles_ = function() { ...@@ -129,14 +139,15 @@ cca.App.prototype.setupToggles_ = function() {
if (element.type == 'radio' && element.checked) { if (element.type == 'radio' && element.checked) {
// Handle unchecked grouped sibling radios. // Handle unchecked grouped sibling radios.
var grouped = `input[type=radio][name=${element.name}]:not(:checked)`; var grouped = `input[type=radio][name=${element.name}]:not(:checked)`;
document.querySelectorAll(grouped).forEach((radio) => document.querySelectorAll(grouped).forEach(
radio.dispatchEvent(new Event('change')) && radio.save()); (radio) =>
radio.dispatchEvent(new Event('change')) && radio.save());
} }
} }
}); });
element.toggleChecked = (checked) => { element.toggleChecked = (checked) => {
element.checked = checked; element.checked = checked;
element.dispatchEvent(new Event('change')); // Trigger toggling css. element.dispatchEvent(new Event('change')); // Trigger toggling css.
}; };
element.save = () => { element.save = () => {
return key && cca.proxy.browserProxy.localStorageSet(payload()); return key && cca.proxy.browserProxy.localStorageSet(payload());
...@@ -154,34 +165,38 @@ cca.App.prototype.setupToggles_ = function() { ...@@ -154,34 +165,38 @@ cca.App.prototype.setupToggles_ = function() {
*/ */
cca.App.prototype.start = function() { cca.App.prototype.start = function() {
var ackMigrate = false; var ackMigrate = false;
cca.models.FileSystem.initialize(() => { cca.models.FileSystem
// Prompt to migrate pictures if needed. .initialize(() => {
var message = chrome.i18n.getMessage('migrate_pictures_msg'); // Prompt to migrate pictures if needed.
return cca.nav.open('message-dialog', {message, cancellable: false}) var message = chrome.i18n.getMessage('migrate_pictures_msg');
.then((acked) => { return cca.nav.open('message-dialog', {message, cancellable: false})
if (!acked) { .then((acked) => {
throw new Error('no-migrate'); if (!acked) {
} throw new Error('no-migrate');
ackMigrate = true; }
}); ackMigrate = true;
}).then((external) => { });
cca.state.set('ext-fs', external); })
this.gallery_.addObserver(this.galleryButton_); .then((external) => {
if (!cca.App.useGalleryApp()) { cca.state.set('ext-fs', external);
this.gallery_.addObserver(this.browserView_); this.gallery_.addObserver(this.galleryButton_);
} if (!cca.App.useGalleryApp()) {
this.gallery_.load(); this.gallery_.addObserver(this.browserView_);
cca.nav.open('camera'); }
}).catch((error) => { this.gallery_.load();
console.error(error); cca.nav.open('camera');
if (error && error.message == 'no-migrate') { })
chrome.app.window.current().close(); .catch((error) => {
return; console.error(error);
} if (error && error.message == 'no-migrate') {
cca.nav.open('warning', 'filesystem-failure'); chrome.app.window.current().close();
}).finally(() => { return;
cca.metrics.log(cca.metrics.Type.LAUNCH, ackMigrate); }
}); cca.nav.open('warning', 'filesystem-failure');
})
.finally(() => {
cca.metrics.log(cca.metrics.Type.LAUNCH, ackMigrate);
});
}; };
/** /**
...@@ -190,7 +205,7 @@ cca.App.prototype.start = function() { ...@@ -190,7 +205,7 @@ cca.App.prototype.start = function() {
* @private * @private
*/ */
cca.App.prototype.onKeyPressed_ = function(event) { cca.App.prototype.onKeyPressed_ = function(event) {
cca.tooltip.hide(); // Hide shown tooltip on any keypress. cca.tooltip.hide(); // Hide shown tooltip on any keypress.
cca.nav.onKeyPressed(event); cca.nav.onKeyPressed(event);
}; };
......
...@@ -33,4 +33,12 @@ js_library("result_saver") { ...@@ -33,4 +33,12 @@ js_library("result_saver") {
} }
js_library("video_saver") { js_library("video_saver") {
sources = [
"file_video_saver.js",
"intent_video_saver.js",
"video_saver_interface.js",
]
deps = [
"..:intent",
]
} }
...@@ -16,8 +16,9 @@ cca.models = cca.models || {}; ...@@ -16,8 +16,9 @@ cca.models = cca.models || {};
/** /**
* Used to save captured video. * Used to save captured video.
* @implements {cca.models.VideoSaver}
*/ */
cca.models.VideoSaver = class { cca.models.FileVideoSaver = class {
/** /**
* @param {!FileEntry} file * @param {!FileEntry} file
* @param {!FileWriter} writer * @param {!FileWriter} writer
...@@ -42,9 +43,7 @@ cca.models.VideoSaver = class { ...@@ -42,9 +43,7 @@ cca.models.VideoSaver = class {
} }
/** /**
* Writes video data to result video. * @override
* @param {!Blob} blob Video data to be written.
* @return {!Promise}
*/ */
async write(blob) { async write(blob) {
this.curWrite_ = (async () => { this.curWrite_ = (async () => {
...@@ -58,8 +57,7 @@ cca.models.VideoSaver = class { ...@@ -58,8 +57,7 @@ cca.models.VideoSaver = class {
} }
/** /**
* Finishes the write of video data parts and returns result video file. * @override
* @return {!Promise<!FileEntry>} Result video file.
*/ */
async endWrite() { async endWrite() {
await this.curWrite_; await this.curWrite_;
...@@ -67,14 +65,14 @@ cca.models.VideoSaver = class { ...@@ -67,14 +65,14 @@ cca.models.VideoSaver = class {
} }
/** /**
* Create VideoSaver. * Creates FileVideoSaver.
* @param {!FileEntry} file The file which VideoSaver saves the result video * @param {!FileEntry} file The file which FileVideoSaver saves the result
* into. * video into.
* @return {!Promise<!cca.models.VideoSaver>} * @return {!Promise<!cca.models.FileVideoSaver>}
*/ */
static async create(file) { static async create(file) {
const writer = await new Promise( const writer = await new Promise(
(resolve, reject) => file.createWriter(resolve, reject)); (resolve, reject) => file.createWriter(resolve, reject));
return new cca.models.VideoSaver(file, writer); return new cca.models.FileVideoSaver(file, writer);
} }
}; };
...@@ -36,6 +36,12 @@ cca.models.FileSystem.THUMBNAIL_PREFIX = 'thumb-'; ...@@ -36,6 +36,12 @@ cca.models.FileSystem.THUMBNAIL_PREFIX = 'thumb-';
*/ */
cca.models.FileSystem.internalDir = null; cca.models.FileSystem.internalDir = null;
/**
* Temporary directory in the internal file system.
* @type {DirectoryEntry}
*/
cca.models.FileSystem.internalTempDir = null;
/** /**
* Directory in the external file system. * Directory in the external file system.
* @type {DirectoryEntry} * @type {DirectoryEntry}
...@@ -49,7 +55,21 @@ cca.models.FileSystem.externalDir = null; ...@@ -49,7 +55,21 @@ cca.models.FileSystem.externalDir = null;
*/ */
cca.models.FileSystem.initInternalDir_ = function() { cca.models.FileSystem.initInternalDir_ = function() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
webkitRequestFileSystem(window.PERSISTENT, 768 * 1024 * 1024 /* 768MB */, webkitRequestFileSystem(
window.PERSISTENT, 768 * 1024 * 1024 /* 768MB */,
(fs) => resolve(fs.root), reject);
});
};
/**
* Initializes the temporary directory in the internal file system.
* @return {!Promise<DirectoryEntry>} Promise for the directory result.
* @private
*/
cca.models.FileSystem.initInternalTempDir_ = function() {
return new Promise((resolve, reject) => {
webkitRequestFileSystem(
window.TEMPORARY, 768 * 1024 * 1024 /* 768MB */,
(fs) => resolve(fs.root), reject); (fs) => resolve(fs.root), reject);
}); });
}; };
...@@ -118,48 +138,59 @@ cca.models.FileSystem.initialize = function(promptMigrate) { ...@@ -118,48 +138,59 @@ cca.models.FileSystem.initialize = function(promptMigrate) {
var doneMigrate = () => chrome.chromeosInfoPrivate && var doneMigrate = () => chrome.chromeosInfoPrivate &&
chrome.chromeosInfoPrivate.set('cameraMediaConsolidated', true); chrome.chromeosInfoPrivate.set('cameraMediaConsolidated', true);
return Promise.all([ return Promise
cca.models.FileSystem.initInternalDir_(), .all([
cca.models.FileSystem.initExternalDir_(), cca.models.FileSystem.initInternalDir_(),
checkAcked, cca.models.FileSystem.initInternalTempDir_(),
checkMigrated, cca.models.FileSystem.initExternalDir_(),
]).then(([internalDir, externalDir, acked, migrated]) => { checkAcked,
cca.models.FileSystem.internalDir = internalDir; checkMigrated,
cca.models.FileSystem.externalDir = externalDir; ])
if (migrated && !externalDir) { .then(([internalDir, internalTempDir, externalDir, acked, migrated]) => {
throw new Error('External file system should be available.'); cca.models.FileSystem.internalDir = internalDir;
} cca.models.FileSystem.internalTempDir = internalTempDir;
// Check if acknowledge-prompt and migrate-pictures are needed. cca.models.FileSystem.externalDir = externalDir;
if (migrated || !cca.models.FileSystem.externalDir) { if (migrated && !externalDir) {
return [false, false]; throw new Error('External file system should be available.');
} }
// Check if any internal picture other than thumbnail needs migration. // Check if acknowledge-prompt and migrate-pictures are needed.
// Pictures taken by old Camera App may not have IMG_ or VID_ prefix. if (migrated || !cca.models.FileSystem.externalDir) {
var dir = cca.models.FileSystem.internalDir; return [false, false];
return cca.models.FileSystem.readDir_(dir).then((entries) => { }
return entries.some( // Check if any internal picture other than thumbnail needs migration.
(entry) => !cca.models.FileSystem.hasThumbnailPrefix_(entry)); // Pictures taken by old Camera App may not have IMG_ or VID_ prefix.
}).then((migrateNeeded) => { var dir = cca.models.FileSystem.internalDir;
if (migrateNeeded) { return cca.models.FileSystem.readDir_(dir)
return [!acked, true]; .then((entries) => {
} return entries.some(
// If the external file system is supported and there is already no (entry) => !cca.models.FileSystem.hasThumbnailPrefix_(entry));
// picture in the internal file system, it implies done migration and })
// then doesn't need acknowledge-prompt. .then((migrateNeeded) => {
ackMigrate(); if (migrateNeeded) {
doneMigrate(); return [!acked, true];
return [false, false]; }
}); // If the external file system is supported and there is already
}).then(([promptNeeded, migrateNeeded]) => { // Prompt to migrate if needed. // no picture in the internal file system, it implies done
return !promptNeeded ? migrateNeeded : promptMigrate().then(() => { // migration and then doesn't need acknowledge-prompt.
ackMigrate(); ackMigrate();
return migrateNeeded; doneMigrate();
}); return [false, false];
}).then((migrateNeeded) => { // Migrate pictures if needed. });
const external = cca.models.FileSystem.externalDir != null; })
return !migrateNeeded ? external : cca.models.FileSystem.migratePictures() .then(
.then(doneMigrate).then(() => external); ([promptNeeded, migrateNeeded]) => { // Prompt to migrate if needed.
}); return !promptNeeded ? migrateNeeded : promptMigrate().then(() => {
ackMigrate();
return migrateNeeded;
});
})
.then((migrateNeeded) => { // Migrate pictures if needed.
const external = cca.models.FileSystem.externalDir != null;
return !migrateNeeded ? external :
cca.models.FileSystem.migratePictures()
.then(doneMigrate)
.then(() => external);
});
}; };
/** /**
...@@ -302,6 +333,26 @@ cca.models.FileSystem.createTempVideoFile = async function() { ...@@ -302,6 +333,26 @@ cca.models.FileSystem.createTempVideoFile = async function() {
return file; return file;
}; };
/**
* @const {string}
*/
cca.models.FileSystem.PRIVATE_TEMPFILE_NAME = 'video-intent.mkv';
/**
* @return {!Promise<!FileEntry>} Newly created temporary file.
* @throws {Error} If failed to create video temp file.
*/
cca.models.FileSystem.createPrivateTempVideoFile = async function() {
// TODO(inker): Handles running out of space case.
const dir = cca.models.FileSystem.internalTempDir;
const file = await cca.models.FileSystem.getFile(
dir, cca.models.FileSystem.PRIVATE_TEMPFILE_NAME, true);
if (file === null) {
throw new Error('Failed to create private video temp file.');
}
return file;
};
/** /**
* Saves temporary video file to predefined default location. * Saves temporary video file to predefined default location.
* @param {FileEntry} tempfile Temporary video file to be saved. * @param {FileEntry} tempfile Temporary video file to be saved.
......
...@@ -88,11 +88,15 @@ cca.models.Gallery.Picture.parseTimestamp_ = function(pictureEntry) { ...@@ -88,11 +88,15 @@ cca.models.Gallery.Picture.parseTimestamp_ = function(pictureEntry) {
var name = cca.models.FileSystem.regulatePictureName(pictureEntry); var name = cca.models.FileSystem.regulatePictureName(pictureEntry);
// Match numeric parts from filenames, e.g. IMG_'yyyyMMdd_HHmmss (n)'.jpg. // Match numeric parts from filenames, e.g. IMG_'yyyyMMdd_HHmmss (n)'.jpg.
// Assume no more than one picture taken within one millisecond. // Assume no more than one picture taken within one millisecond.
// Use String.raw instead of /...regex.../ here to avoid breaking syntax
// highlight on gerrit.
var match = name.match( var match = name.match(
/_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})(?: \((\d+)\))?/); String.raw`_(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})(?: \((\d+)\))?/`);
return match ? new Date(num(match[1]), num(match[2]) - 1, num(match[3]), return match ?
num(match[4]), num(match[5]), num(match[6]), new Date(
match[7] ? num(match[7]) : 0) : new Date(0); num(match[1]), num(match[2]) - 1, num(match[3]), num(match[4]),
num(match[5]), num(match[6]), match[7] ? num(match[7]) : 0) :
new Date(0);
}; };
cca.models.Gallery.Picture.prototype = { cca.models.Gallery.Picture.prototype = {
...@@ -313,7 +317,7 @@ cca.models.Gallery.prototype.savePhoto = function(blob, name) { ...@@ -313,7 +317,7 @@ cca.models.Gallery.prototype.savePhoto = function(blob, name) {
*/ */
cca.models.Gallery.prototype.startSaveVideo = async function() { cca.models.Gallery.prototype.startSaveVideo = async function() {
const tempFile = await cca.models.FileSystem.createTempVideoFile(); const tempFile = await cca.models.FileSystem.createTempVideoFile();
return cca.models.VideoSaver.create(tempFile); return cca.models.FileVideoSaver.create(tempFile);
}; };
/** /**
......
// 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 models.
*/
cca.models = cca.models || {};
/**
* Used to save captured video into a preview file and forward to intent result.
* @implements {cca.models.VideoSaver}
*/
cca.models.IntentVideoSaver = class {
/**
* @param {!cca.intent.Intent} intent
* @param {!cca.models.FileVideoSaver} fileSaver
* @private
*/
constructor(intent, fileSaver) {
/**
* @const {!cca.intent.Intent} intent
* @private
*/
this.intent_ = intent;
/**
* @const {!cca.models.FileVideoSaver}
* @private
*/
this.fileSaver_ = fileSaver;
}
/**
* @override
*/
async write(blob) {
await this.fileSaver_.write(blob);
const arrayBuffer = await blob.arrayBuffer();
this.intent_.appendData(new Uint8Array(arrayBuffer));
}
/**
* @override
*/
async endWrite() {
return this.fileSaver_.endWrite();
}
/**
* Creates IntentVideoSaver.
* @param {!cca.intent.Intent} intent
* @return {!Promise<!cca.models.IntentVideoSaver>}
*/
static async create(intent) {
const tmpFile = await cca.models.FileSystem.createPrivateTempVideoFile();
const fileSaver = await cca.models.FileVideoSaver.create(tmpFile);
return new cca.models.IntentVideoSaver(intent, fileSaver);
}
};
// 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 models.
*/
cca.models = cca.models || {};
/**
* Used to save captured video.
* @interface
*/
cca.models.VideoSaver = class {
/**
* Writes video data to result video.
* @param {!Blob} blob Video data to be written.
* @return {!Promise}
*/
async write(blob) {}
/**
* Finishes the write of video data parts and returns result video file.
* @return {!Promise<!FileEntry>} Result video file.
*/
async endWrite() {}
};
...@@ -7,6 +7,7 @@ import("//third_party/closure_compiler/compile_js.gni") ...@@ -7,6 +7,7 @@ import("//third_party/closure_compiler/compile_js.gni")
group("closure_compile") { group("closure_compile") {
deps = [ deps = [
":compile_resources", ":compile_resources",
"camera:compile_resources",
] ]
} }
......
...@@ -129,13 +129,10 @@ cca.views.Camera = function( ...@@ -129,13 +129,10 @@ cca.views.Camera = function(
/** /**
* Promise for the current take of photo or recording. * Promise for the current take of photo or recording.
* @type {?Promise} * @type {?Promise}
* @private * @protected
*/ */
this.take_ = null; this.take_ = null;
// End of properties, seal the object.
Object.seal(this);
document.querySelectorAll('#start-takephoto, #start-recordvideo') document.querySelectorAll('#start-takephoto, #start-recordvideo')
.forEach((btn) => btn.addEventListener('click', () => this.beginTake_())); .forEach((btn) => btn.addEventListener('click', () => this.beginTake_()));
...@@ -189,11 +186,13 @@ cca.views.Camera.prototype.focus = function() { ...@@ -189,11 +186,13 @@ cca.views.Camera.prototype.focus = function() {
/** /**
* Begins to take photo or recording with the current options, e.g. timer. * Begins to take photo or recording with the current options, e.g. timer.
* @private * @return {?Promise} Promise resolved when take action completes. Returns null
* if CCA can't start take action.
* @protected
*/ */
cca.views.Camera.prototype.beginTake_ = function() { cca.views.Camera.prototype.beginTake_ = function() {
if (!cca.state.get('streaming') || cca.state.get('taking')) { if (!cca.state.get('streaming') || cca.state.get('taking')) {
return; return null;
} }
cca.state.set('taking', true); cca.state.set('taking', true);
...@@ -214,6 +213,7 @@ cca.views.Camera.prototype.beginTake_ = function() { ...@@ -214,6 +213,7 @@ cca.views.Camera.prototype.beginTake_ = function() {
this.focus(); // Refocus the visible shutter button for ChromeVox. this.focus(); // Refocus the visible shutter button for ChromeVox.
} }
})(); })();
return this.take_;
}; };
/** /**
...@@ -273,74 +273,75 @@ cca.views.Camera.prototype.restart = async function() { ...@@ -273,74 +273,75 @@ cca.views.Camera.prototype.restart = async function() {
}; };
/** /**
* Try start stream reconfiguration with specified device id. * Try start stream reconfiguration with specified mode and device id.
* @async
* @param {?string} deviceId * @param {?string} deviceId
* @return {boolean} If found suitable stream and reconfigure successfully. * @param {string} mode
* @return {!Promise<boolean>} If found suitable stream and reconfigure
* successfully.
*/ */
cca.views.Camera.prototype.startWithDevice_ = async function(deviceId) { cca.views.Camera.prototype.startWithMode_ = async function(deviceId, mode) {
let supportedModes = null; const deviceOperator = await cca.mojo.DeviceOperator.getInstance();
for (const mode of this.modes_.getModeCandidates()) { let resolCandidates = null;
try { if (deviceOperator !== null) {
if (!deviceId) { if (deviceId !== null) {
// Null for requesting default camera on HALv1.
throw new Error('HALv1-api');
}
const previewRs = const previewRs =
(await this.infoUpdater_.getDeviceResolutions(deviceId))[1]; (await this.infoUpdater_.getDeviceResolutions(deviceId))[1];
var resolCandidates = resolCandidates =
this.modes_.getResolutionCandidates(mode, deviceId, previewRs); this.modes_.getResolutionCandidates(mode, deviceId, previewRs);
} catch (e) { } else {
// Assume the exception here is thrown from error of HALv1 not support console.error('Null device id present on HALv3 device. Fallback to v1.');
// resolution query, fallback to use v1 constraints-candidates.
if (e.message == 'HALv1-api') {
resolCandidates =
await this.modes_.getResolutionCandidatesV1(mode, deviceId);
} else {
throw e;
}
} }
for (const [captureResolution, previewCandidates] of resolCandidates) { }
if (supportedModes && !supportedModes.includes(mode)) { if (resolCandidates === null) {
break; resolCandidates =
await this.modes_.getResolutionCandidatesV1(mode, deviceId);
}
for (const [captureResolution, previewCandidates] of resolCandidates) {
for (const constraints of previewCandidates) {
if (this.suspended) {
throw new cca.views.CameraSuspendedError();
} }
for (const constraints of previewCandidates) { try {
if (this.suspended) { if (deviceOperator !== null) {
throw new cca.views.CameraSuspendedError(); await deviceOperator.setFpsRange(deviceId, constraints);
} await deviceOperator.setCaptureIntent(
try { deviceId, this.modes_.getCaptureIntent(mode));
const deviceOperator = await cca.mojo.DeviceOperator.getInstance();
if (deviceOperator) {
await deviceOperator.setFpsRange(deviceId, constraints);
await deviceOperator.setCaptureIntent(
deviceId, this.modes_.getCaptureIntent(mode));
}
const stream = await navigator.mediaDevices.getUserMedia(constraints);
if (!supportedModes) {
supportedModes = await this.modes_.getSupportedModes(stream);
if (!supportedModes.includes(mode)) {
stream.getTracks()[0].stop();
break;
}
}
await this.preview_.start(stream);
this.facingMode_ =
await this.options_.updateValues(constraints, stream);
await this.modes_.updateModeSelectionUI(supportedModes);
await this.modes_.updateMode(
mode, stream, deviceId, captureResolution);
cca.nav.close('warning', 'no-camera');
return true;
} catch (e) {
this.preview_.stop();
console.error(e);
} }
const stream = await navigator.mediaDevices.getUserMedia(constraints);
await this.preview_.start(stream);
this.facingMode_ =
await this.options_.updateValues(constraints, stream);
await this.modes_.updateModeSelectionUI(deviceId);
await this.modes_.updateMode(mode, stream, deviceId, captureResolution);
cca.nav.close('warning', 'no-camera');
return true;
} catch (e) {
this.preview_.stop();
console.error(e);
} }
} }
} }
return false; return false;
}; };
/**
* Try start stream reconfiguration with specified device id.
* @param {?string} deviceId
* @return {!Promise<boolean>} If found suitable stream and reconfigure
* successfully.
*/
cca.views.Camera.prototype.startWithDevice_ = async function(deviceId) {
const supportedModes = await this.modes_.getSupportedModes(deviceId);
const modes =
this.modes_.getModeCandidates().filter((m) => supportedModes.includes(m));
for (const mode of modes) {
if (await this.startWithMode_(deviceId, mode)) {
return true;
}
}
return false;
};
/** /**
* Starts camera configuration process. * Starts camera configuration process.
* @return {!Promise<boolean>} Resolved to boolean for whether the configuration * @return {!Promise<boolean>} Resolved to boolean for whether the configuration
......
# 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.
import("//third_party/closure_compiler/compile_js.gni")
group("closure_compile") {
deps = [
":compile_resources",
]
}
js_type_check("compile_resources") {
deps = [
":review_result",
]
}
js_library("review_result") {
deps = [
"../..:state",
"../..:util",
]
}
...@@ -38,7 +38,14 @@ cca.views.camera.Layout = function() { ...@@ -38,7 +38,14 @@ cca.views.camera.Layout = function() {
* @private * @private
*/ */
this.squareVideo_ = this.squareVideo_ =
cca.views.camera.Layout.cssStyle_('body.square-preview #preview-video'); cca.views.camera.Layout.cssStyle_('body.square-preview .preview-content');
/**
* CSS style of what is currently put as camera preview.
* @type {CSSStyleDeclaration}
* @private
*/
this.previewContent_ = cca.views.camera.Layout.cssStyle_('.preview-content');
// End of properties, seal the object. // End of properties, seal the object.
Object.seal(this); Object.seal(this);
...@@ -73,7 +80,9 @@ cca.views.camera.Layout.prototype.updatePreviewSize_ = function() { ...@@ -73,7 +80,9 @@ cca.views.camera.Layout.prototype.updatePreviewSize_ = function() {
// it may fill up the window or be letterboxed when fullscreen/maximized. // it may fill up the window or be letterboxed when fullscreen/maximized.
// Don't use app-window.innerBounds' width/height properties during resizing // Don't use app-window.innerBounds' width/height properties during resizing
// as they are not updated immediately. // as they are not updated immediately.
var video = document.querySelector('#preview-video'); const video = document.querySelector('#preview-video');
let contentWidth = 0;
let contentHeight = 0;
if (video.videoHeight) { if (video.videoHeight) {
var scale = cca.state.get('square-mode') ? var scale = cca.state.get('square-mode') ?
Math.min(window.innerHeight, window.innerWidth) / Math.min(window.innerHeight, window.innerWidth) /
...@@ -81,15 +90,19 @@ cca.views.camera.Layout.prototype.updatePreviewSize_ = function() { ...@@ -81,15 +90,19 @@ cca.views.camera.Layout.prototype.updatePreviewSize_ = function() {
Math.min( Math.min(
window.innerHeight / video.videoHeight, window.innerHeight / video.videoHeight,
window.innerWidth / video.videoWidth); window.innerWidth / video.videoWidth);
video.width = scale * video.videoWidth; contentWidth = scale * video.videoWidth;
video.height = scale * video.videoHeight; contentHeight = scale * video.videoHeight;
this.previewContent_.setProperty('width', `${contentWidth}px`);
this.previewContent_.setProperty('height', `${contentHeight}px`);
} }
var [viewportW, viewportH] = [video.width, video.height]; var [viewportW, viewportH] = [contentWidth, contentHeight];
cca.state.set('square-preview', cca.state.get('square-mode')); cca.state.set('square-preview', cca.state.get('square-mode'));
if (cca.state.get('square-mode')) { if (cca.state.get('square-mode')) {
viewportW = viewportH = Math.min(video.width, video.height); viewportW = viewportH = Math.min(contentWidth, contentHeight);
this.squareVideo_.setProperty('left', `${(viewportW - video.width) / 2}px`); this.squareVideo_.setProperty(
this.squareVideo_.setProperty('top', `${(viewportH - video.height) / 2}px`); 'left', `${(viewportW - contentWidth) / 2}px`);
this.squareVideo_.setProperty(
'top', `${(viewportH - contentHeight) / 2}px`);
this.squareViewport_.setProperty('width', `${viewportW}px`); this.squareViewport_.setProperty('width', `${viewportW}px`);
this.squareViewport_.setProperty('height', `${viewportH}px`); this.squareViewport_.setProperty('height', `${viewportH}px`);
} }
......
...@@ -162,12 +162,14 @@ cca.views.camera.Modes = function( ...@@ -162,12 +162,14 @@ cca.views.camera.Modes = function(
'portrait-mode': { 'portrait-mode': {
captureFactory: () => new cca.views.camera.Portrait( captureFactory: () => new cca.views.camera.Portrait(
this.stream_, doSavePhoto, this.captureResolution_), this.stream_, doSavePhoto, this.captureResolution_),
isSupported: async (stream) => { isSupported: async (deviceId) => {
if (deviceId === null) {
return false;
}
const deviceOperator = await cca.mojo.DeviceOperator.getInstance(); const deviceOperator = await cca.mojo.DeviceOperator.getInstance();
if (!deviceOperator) { if (deviceOperator === null) {
return false; return false;
} }
const deviceId = stream.getVideoTracks()[0].getSettings().deviceId;
return await deviceOperator.isPortraitModeSupported(deviceId); return await deviceOperator.isPortraitModeSupported(deviceId);
}, },
resolutionConfig: photoResolPreferrer, resolutionConfig: photoResolPreferrer,
...@@ -273,7 +275,7 @@ cca.views.camera.Modes.prototype.switchMode_ = function(mode) { ...@@ -273,7 +275,7 @@ cca.views.camera.Modes.prototype.switchMode_ = function(mode) {
/** /**
* Gets all mode candidates. Desired trying sequence of candidate modes is * Gets all mode candidates. Desired trying sequence of candidate modes is
* reflected in the order of the returned array. * reflected in the order of the returned array.
* @return {Array<string>} Mode candidates to be tried out. * @return {!Array<string>} Mode candidates to be tried out.
*/ */
cca.views.camera.Modes.prototype.getModeCandidates = function() { cca.views.camera.Modes.prototype.getModeCandidates = function() {
const tried = {}; const tried = {};
...@@ -326,14 +328,15 @@ cca.views.camera.Modes.prototype.getCaptureIntent = function(mode) { ...@@ -326,14 +328,15 @@ cca.views.camera.Modes.prototype.getCaptureIntent = function(mode) {
}; };
/** /**
* Gets supported modes for video device of the given stream. * Gets supported modes for video device of given device id.
* @param {MediaStream} stream Stream of the video device. * @param {?string} deviceId Device id of the video device.
* @return {Array<string>} Names of all supported mode for the video device. * @return {!Promise<!Array<cca.views.camera.Mode>>} All supported mode for the
* video device.
*/ */
cca.views.camera.Modes.prototype.getSupportedModes = async function(stream) { cca.views.camera.Modes.prototype.getSupportedModes = async function(deviceId) {
let supportedModes = []; let supportedModes = [];
for (const [mode, obj] of Object.entries(this.allModes_)) { for (const [mode, obj] of Object.entries(this.allModes_)) {
if (await obj.isSupported(stream)) { if (await obj.isSupported(deviceId)) {
supportedModes.push(mode); supportedModes.push(mode);
} }
} }
...@@ -341,12 +344,13 @@ cca.views.camera.Modes.prototype.getSupportedModes = async function(stream) { ...@@ -341,12 +344,13 @@ cca.views.camera.Modes.prototype.getSupportedModes = async function(stream) {
}; };
/** /**
* Updates mode selection UI according to given supported modes. * Updates mode selection UI according to given device id.
* @param {Array<string>} supportedModes Supported mode names to be updated * @param {?string} deviceId
* with. * @return {!Promise}
*/ */
cca.views.camera.Modes.prototype.updateModeSelectionUI = function( cca.views.camera.Modes.prototype.updateModeSelectionUI =
supportedModes) { async function(deviceId) {
const supportedModes = await this.getSupportedModes(deviceId);
document.querySelectorAll('.mode-item').forEach((element) => { document.querySelectorAll('.mode-item').forEach((element) => {
const radio = element.querySelector('input[type=radio]'); const radio = element.querySelector('input[type=radio]');
element.classList.toggle( element.classList.toggle(
......
...@@ -123,6 +123,7 @@ cca.views.camera.Preview.prototype.toString = function() { ...@@ -123,6 +123,7 @@ cca.views.camera.Preview.prototype.toString = function() {
cca.views.camera.Preview.prototype.setSource_ = function(stream) { cca.views.camera.Preview.prototype.setSource_ = function(stream) {
var video = document.createElement('video'); var video = document.createElement('video');
video.id = 'preview-video'; video.id = 'preview-video';
video.classList = this.video_.classList;
video.muted = true; // Mute to avoid echo from the captured audio. video.muted = true; // Mute to avoid echo from the captured audio.
return new Promise((resolve) => { return new Promise((resolve) => {
var handler = () => { var handler = () => {
...@@ -186,27 +187,6 @@ cca.views.camera.Preview.prototype.stop = function() { ...@@ -186,27 +187,6 @@ cca.views.camera.Preview.prototype.stop = function() {
cca.state.set('streaming', false); cca.state.set('streaming', false);
}; };
/**
* Creates an image blob of the current frame.
* @return {!Promise<Blob>} Promise for the result.
*/
cca.views.camera.Preview.prototype.toImage = function() {
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
canvas.width = this.video_.videoWidth;
canvas.height = this.video_.videoHeight;
ctx.drawImage(this.video_, 0, 0);
return new Promise((resolve, reject) => {
canvas.toBlob((blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('Photo blob error.'));
}
}, 'image/jpeg');
});
};
/** /**
* Checks preview whether to show preview metadata or not. * Checks preview whether to show preview metadata or not.
* @private * @private
......
// Copyright (c) 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 views.
*/
cca.views = cca.views || {};
/**
* Namespace for Camera view.
*/
cca.views.camera = cca.views.camera || {};
/**
* Creates a controller for reviewing intent result in Camera view.
*/
cca.views.camera.ReviewResult = class {
/**
* @public
*/
constructor() {
/**
* @const {!HTMLImageElement}
* @private
*/
this.reviewPhotoResult_ = /** @type {!HTMLImageElement} */ (
document.querySelector('#review-photo-result'));
/**
* @const {!HTMLVideoElement}
* @private
*/
this.reviewVideoResult_ = /** @type {!HTMLVideoElement} */ (
document.querySelector('#review-video-result'));
/**
* Function resolving open result call called with whether user confirms
* after reviewing intent result.
* @type {?function(boolean)}
* @private
*/
this.resolveOpen_ = null;
this.reviewVideoResult_.onended = () => {
this.reviewVideoResult_.currentTime = 0;
cca.state.set('playing-result-video', false);
};
const addClickListener = (selector, handler) =>
document.querySelector(selector).addEventListener('click', handler);
addClickListener('#confirm-result', () => this.close_(true));
addClickListener('#cancel-result', () => this.close_(false));
addClickListener('#play-result-video', () => this.playResultVideo_());
}
/**
* Starts playing result video.
* @private
*/
playResultVideo_() {
if (cca.state.get('playing-result-video')) {
return;
}
cca.state.set('playing-result-video', true);
this.reviewVideoResult_.play();
}
/**
* Closes review result UI and resolves its open promise with whether user
* confirms after reviewing the result.
* @param {boolean} confirmed
* @private
*/
close_(confirmed) {
if (this.resolveOpen_ === null) {
console.error('Close review result with no unresolved open.');
return;
}
const resolve = this.resolveOpen_;
this.resolveOpen_ = null;
cca.state.set('review-result', false);
cca.state.set('playing-result-video', false);
this.reviewPhotoResult_.src = '';
this.reviewVideoResult_.src = '';
resolve(confirmed);
}
/**
* Opens photo result blob and shows photo on review result UI.
* @param {!Blob} blob Photo result blob.
* @return {!Promise<boolean>} Promise resolved with whether user confirms
* with the photo result.
*/
async openPhoto(blob) {
const img = await cca.util.blobToImage(blob);
this.reviewPhotoResult_.src = img.src;
cca.state.set('review-photo-result', true);
cca.state.set('review-result', true);
return new Promise((resolve) => {
this.resolveOpen_ = resolve;
});
}
/**
* Opens video result file and shows video on review result UI.
* @param {!FileEntry} fileEntry Video result file.
* @return {!Promise<boolean>} Promise resolved with whether user confirms
* with the video result.
*/
async openVideo(fileEntry) {
this.reviewVideoResult_.src = fileEntry.toURL();
cca.state.set('review-photo-result', false);
cca.state.set('review-result', true);
return new Promise((resolve) => {
this.resolveOpen_ = resolve;
});
}
};
// Copyright (c) 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 views.
*/
cca.views = cca.views || {};
/**
* Creates the camera-intent-view controller.
*/
cca.views.CameraIntent = class extends cca.views.Camera {
/**
* @param {!cca.intent.Intent} intent
* @param {!cca.device.DeviceInfoUpdater} infoUpdater
* @param {!cca.device.PhotoResolPreferrer} photoPreferrer
* @param {!cca.device.VideoConstraintsPreferrer} videoPreferrer
*/
constructor(intent, infoUpdater, photoPreferrer, videoPreferrer) {
const resultSaver = {
savePhoto: async (blob) => {
this.photoResult_ = blob;
const buf = await blob.arrayBuffer();
await this.intent_.appendData(new Uint8Array(buf));
},
startSaveVideo: async () => {
return await cca.models.IntentVideoSaver.create(intent);
},
finishSaveVideo: async (video, savedName) => {
this.videoResult_ = await video.endWrite();
},
};
super(resultSaver, infoUpdater, photoPreferrer, videoPreferrer);
/**
* @type {!cca.intent.Intent}
* @private
*/
this.intent_ = intent;
/**
* @type {?Blob}
* @private
*/
this.photoResult_ = null;
/**
* @type {?FileEntry}
* @private
*/
this.videoResult_ = null;
/**
* @type {!cca.views.camera.ReviewResult}
* @private
*/
this.reviewResult_ = new cca.views.camera.ReviewResult();
}
/**
* @override
*/
beginTake_() {
if (this.photoResult_ !== null) {
URL.revokeObjectURL(this.photoResult_);
}
this.photoResult_ = null;
this.videoResult_ = null;
const take = super.beginTake_();
if (take === null) {
return null;
}
return (async () => {
await take;
if (this.photoResult_ === null && this.videoResult_ === null) {
console.warn('End take without intent result.');
return;
}
cca.state.set('suspend', true);
await this.restart();
const confirmed = await (
this.photoResult_ !== null ?
this.reviewResult_.openPhoto(this.photoResult_) :
this.reviewResult_.openVideo(this.videoResult_));
if (confirmed) {
await this.intent_.finish();
window.close();
return;
}
cca.state.set('suspend', false);
await this.intent_.clearData();
await this.restart();
})();
}
/**
* @override
*/
async startWithDevice_(deviceId) {
return this.startWithMode_(deviceId, this.defaultMode);
}
};
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
<script src="../js/sound.js"></script> <script src="../js/sound.js"></script>
<script src="../js/scrollbar.js"></script> <script src="../js/scrollbar.js"></script>
<script src="../js/gallerybutton.js"></script> <script src="../js/gallerybutton.js"></script>
<script src="../js/device/error.js"></script>
<script src="../js/device/camera3_device_info.js"></script> <script src="../js/device/camera3_device_info.js"></script>
<script src="../js/device/constraints_preferrer.js"></script> <script src="../js/device/constraints_preferrer.js"></script>
<script src="../js/device/device_info_updater.js"></script> <script src="../js/device/device_info_updater.js"></script>
...@@ -28,7 +29,9 @@ ...@@ -28,7 +29,9 @@
<script src="../js/models/gallery.js"></script> <script src="../js/models/gallery.js"></script>
<script src="../js/models/filesystem.js"></script> <script src="../js/models/filesystem.js"></script>
<script src="../js/models/result_saver.js"></script> <script src="../js/models/result_saver.js"></script>
<script src="../js/models/video_saver.js"></script> <script src="../js/models/video_saver_interface.js"></script>
<script src="../js/models/file_video_saver.js"></script>
<script src="../js/models/intent_video_saver.js"></script>
<script src="../js/mojo/mojo_bindings_lite.js"></script> <script src="../js/mojo/mojo_bindings_lite.js"></script>
<script src="../js/mojo/camera_metadata_tags.mojom-lite.js"></script> <script src="../js/mojo/camera_metadata_tags.mojom-lite.js"></script>
<script src="../js/mojo/camera_metadata.mojom-lite.js"></script> <script src="../js/mojo/camera_metadata.mojom-lite.js"></script>
...@@ -45,10 +48,12 @@ ...@@ -45,10 +48,12 @@
<script src="../js/views/view.js"></script> <script src="../js/views/view.js"></script>
<script src="../js/views/gallery_base.js"></script> <script src="../js/views/gallery_base.js"></script>
<script src="../js/views/camera.js"></script> <script src="../js/views/camera.js"></script>
<script src="../js/views/camera_intent.js"></script>
<script src="../js/views/camera/layout.js"></script> <script src="../js/views/camera/layout.js"></script>
<script src="../js/views/camera/options.js"></script> <script src="../js/views/camera/options.js"></script>
<script src="../js/views/camera/preview.js"></script> <script src="../js/views/camera/preview.js"></script>
<script src="../js/views/camera/recordtime.js"></script> <script src="../js/views/camera/recordtime.js"></script>
<script src="../js/views/camera/review_result.js"></script>
<script src="../js/views/camera/timertick.js"></script> <script src="../js/views/camera/timertick.js"></script>
<script src="../js/views/camera/modes.js"></script> <script src="../js/views/camera/modes.js"></script>
<script src="../js/views/dialog.js"></script> <script src="../js/views/dialog.js"></script>
...@@ -61,7 +66,12 @@ ...@@ -61,7 +66,12 @@
<body class="sound mirror mic _3x3"> <body class="sound mirror mic _3x3">
<div id="camera"> <div id="camera">
<div id="preview-wrapper" aria-hidden="true"> <div id="preview-wrapper" aria-hidden="true">
<video id="preview-video"></video> <img id="review-photo-result" class="preview-content">
<video id="review-video-result" class="preview-content"></video>
<div class="buttons circle centered-overlay">
<button id="play-result-video"></button>
</div>
<video id="preview-video" class="preview-content"></video>
<div id="preview-metadata"> <div id="preview-metadata">
<div id="preview-stat" class="metadata-row mode-on"> <div id="preview-stat" class="metadata-row mode-on">
<span class="metadata-category">Stat</span> <span class="metadata-category">Stat</span>
...@@ -140,6 +150,10 @@ ...@@ -140,6 +150,10 @@
<button id="gallery-enter" tabindex="0" <button id="gallery-enter" tabindex="0"
i18n-label="gallery_button" hidden></button> i18n-label="gallery_button" hidden></button>
</div> </div>
<div id="confirm-result-groups" class="buttons right-stripe circle">
<button id="confirm-result"></button>
<button id="cancel-result"></button>
</div>
<div class="bottom-stripe left-stripe buttons circle"> <div class="bottom-stripe left-stripe buttons circle">
<button id="switch-device" tabindex="0" <button id="switch-device" tabindex="0"
i18n-label="switch_camera_button"></button> i18n-label="switch_camera_button"></button>
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment