Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(7419)

Unified Diff: chrome/browser/resources/chromeos/login/oobe_screen_user_image.js

Issue 10532048: [cros] Initial WebRTC-enabled implementation of user image picker on OOBE. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Cleanup Created 8 years, 6 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: chrome/browser/resources/chromeos/login/oobe_screen_user_image.js
diff --git a/chrome/browser/resources/chromeos/login/oobe_screen_user_image.js b/chrome/browser/resources/chromeos/login/oobe_screen_user_image.js
index 59eaa02509f69d7cdb7571420c42bc519afec37d..593854f1a62bf9c42b1bf97e2acbb52a8d79da82 100644
--- a/chrome/browser/resources/chromeos/login/oobe_screen_user_image.js
+++ b/chrome/browser/resources/chromeos/login/oobe_screen_user_image.js
@@ -7,35 +7,60 @@
*/
cr.define('oobe', function() {
-
var UserImagesGrid = options.UserImagesGrid;
var ButtonImages = UserImagesGrid.ButtonImages;
/**
* Array of button URLs used on this page.
* @type {Array.<string>}
+ * @const
*/
- const ButtonImageUrls = [
+ var ButtonImageUrls = [
ButtonImages.TAKE_PHOTO
];
/**
- * Creates a new oobe screen div.
+ * Creates a new OOBE screen div.
* @constructor
* @extends {HTMLDivElement}
*/
var UserImageScreen = cr.ui.define('div');
/**
+ * Dimensions for camera capture.
+ * @const
+ */
+ var CAPTURE_SIZE = {
+ height: 480,
+ width: 480
+ };
+
+ /**
+ * Interval between consecutive camera presence checks in msec while camera is
+ * not present.
+ * @const
+ */
+ var CAMERA_CHECK_INTERVAL_MS = 1000;
Nikita (slow) 2012/06/13 14:07:35 3000ms
Ivan Korotkov 2012/06/13 14:26:06 Done.
+
+ /**
+ * Interval between consecutive camera liveness checks in msec.
+ * @const
+ */
+ var CAMERA_LIVENESS_CHECK_MS = 1000;
+
+ /**
* Registers with Oobe.
*/
UserImageScreen.register = function() {
var screen = $('user-image');
+ var isWebRTC = document.documentElement.getAttribute('camera') == 'webrtc';
+ UserImageScreen.prototype = isWebRTC ? UserImageScreenWebRTCProto :
+ UserImageScreenOldProto;
UserImageScreen.decorate(screen);
Oobe.getInstance().registerScreen(screen);
};
- UserImageScreen.prototype = {
+ var UserImageScreenOldProto = {
__proto__: HTMLDivElement.prototype,
/**
@@ -102,7 +127,7 @@ cr.define('oobe', function() {
okButton.id = 'ok-button';
okButton.textContent = localStrings.getString('okButtonText');
okButton.addEventListener('click', this.acceptImage_.bind(this));
- return [ okButton ];
+ return [okButton];
},
/**
@@ -322,7 +347,455 @@ cr.define('oobe', function() {
/**
* Updates localized content of the screen that is not updated via template.
- * @public
+ */
+ updateLocalizedContent: function() {
+ this.updateProfileImageCaption_();
+ },
+
+ /**
+ * Updates profile image caption.
+ * @private
+ */
+ updateProfileImageCaption_: function() {
+ this.profileImageCaption = localStrings.getString(
+ this.profileImageLoading_ ? 'profilePhotoLoading' : 'profilePhoto');
+ }
+ };
+
+ var UserImageScreenWebRTCProto = {
+ __proto__: HTMLDivElement.prototype,
+
+ /**
+ * Currently selected user image index (take photo button is with zero
+ * index).
+ * @type {number}
+ */
+ selectedUserImage_: -1,
+
+ /** @inheritDoc */
+ decorate: function(element) {
+ var imageGrid = $('user-image-grid');
+ UserImagesGrid.decorate(imageGrid);
+
+ imageGrid.addEventListener('change',
+ this.handleSelection_.bind(this));
+ imageGrid.addEventListener('activate',
+ this.handleImageActivated_.bind(this));
+ imageGrid.addEventListener('dblclick',
+ this.handleImageDblClick_.bind(this));
+
+ // Profile image data (if present).
+ this.profileImage_ = imageGrid.addItem(
+ ButtonImages.PROFILE_PICTURE,
+ undefined, undefined, undefined,
+ function(el) { // Custom decorator for Profile image element.
+ var spinner = el.ownerDocument.createElement('div');
+ spinner.className = 'spinner';
+ var spinnerBg = el.ownerDocument.createElement('div');
+ spinnerBg.className = 'spinner-bg';
+ spinnerBg.appendChild(spinner);
+ el.appendChild(spinnerBg);
+ el.id = 'profile-image';
+ });
+ this.profileImage_.type = 'profile';
+ this.selectionType = 'default';
+
+ var video = $('user-image-stream');
+ video.addEventListener('canplay', this.handleVideoStarted_.bind(this));
+ video.addEventListener('timeupdate', this.handleVideoUpdate_.bind(this));
+ $('take-photo').addEventListener('click',
+ this.handleTakePhoto_.bind(this));
+ $('discard-photo').addEventListener('click',
+ this.handleDiscardPhoto_.bind(this));
+ this.cameraImage = null;
+ // Perform an early check if camera is present, without starting capture.
+ this.checkCameraPresence_(false, false);
+ },
+
+ /**
+ * Header text of the screen.
+ * @type {string}
+ */
+ get header() {
+ return localStrings.getString('userImageScreenTitle');
+ },
+
+ /**
+ * Buttons in oobe wizard's button strip.
+ * @type {array} Array of Buttons.
+ */
+ get buttons() {
+ var okButton = this.ownerDocument.createElement('button');
+ okButton.id = 'ok-button';
+ okButton.textContent = localStrings.getString('okButtonText');
+ okButton.addEventListener('click', this.acceptImage_.bind(this));
+ return [okButton];
+ },
+
+ /**
+ * The caption to use for the Profile image preview.
+ * @type {string}
+ */
+ get profileImageCaption() {
+ return this.profileImageCaption_;
+ },
+ set profileImageCaption(value) {
+ this.profileImageCaption_ = value;
+ this.updateCaption_();
+ },
+
+ /**
+ * True if the Profile image is being loaded.
+ * @type {boolean}
+ */
+ get profileImageLoading() {
+ return this.profileImageLoading_;
+ },
+ set profileImageLoading(value) {
+ this.profileImageLoading_ = value;
+ $('user-image-screen-main').classList[
+ value ? 'add' : 'remove']('profile-image-loading');
+ this.updateProfileImageCaption_();
+ },
+
+ /**
+ * True when camera is in live mode (i.e. no still photo selected).
+ * @type {boolean}
+ */
+ get cameraLive() {
+ return this.cameraLive_;
+ },
+ set cameraLive(value) {
+ this.cameraLive_ = value;
+ $('user-image-preview').classList[value ? 'add' : 'remove']('live');
+ },
+
+ /**
+ * Type of the selected image (one of 'default', 'profile', 'camera').
+ * @type {string}
+ */
+ get selectionType() {
+ return this.selectionType_;
+ },
+ set selectionType(value) {
+ this.selectionType_ = value;
+ var previewClassList = $('user-image-preview').classList;
+ previewClassList[value == 'default' ? 'add' : 'remove']('default-image');
+ previewClassList[value == 'profile' ? 'add' : 'remove']('profile-image');
+ previewClassList[value == 'camera' ? 'add' : 'remove']('camera');
+ this.updateCaption_();
+ },
+
+ /**
+ * Handles image activation (by pressing Enter).
+ * @private
+ */
+ handleImageActivated_: function() {
+ switch ($('user-image-grid').selectedItemUrl) {
+ case ButtonImages.TAKE_PHOTO:
+ this.handleTakePhoto_();
+ break;
+ default:
+ this.acceptImage_();
+ break;
+ }
+ },
+
+ /**
+ * Handles photo capture from the live camera stream.
+ * @private
+ */
+ handleTakePhoto_: function() {
+ var self = this;
+ var photoURL = this.captureFrame_($('user-image-stream'), CAPTURE_SIZE);
+ chrome.send('photoTaken', [photoURL]);
+ // Wait until image is loaded before displaying it.
+ var previewImg = new Image();
+ previewImg.addEventListener('load', function(e) {
+ self.cameraImage = this.src;
+ });
+ previewImg.src = photoURL;
+ },
+
+ /**
+ * Discard current photo and return to the live camera stream.
+ * @private
+ */
+ handleDiscardPhoto_: function() {
+ this.cameraImage = null;
+ },
+
+ /**
+ * Capture a single still frame from a <video> element.
+ * @param {HTMLVideoElement} video Video element to capture from.
+ * @param {{width: number, height: number}} destSize Capture size.
+ * @return {string} Captured frame as a data URL.
+ * @private
+ */
+ captureFrame_: function(video, destSize) {
+ var canvas = document.createElement('canvas');
+ canvas.width = destSize.width;
+ canvas.height = destSize.height;
+ var ctx = canvas.getContext('2d');
+ var width = video.videoWidth;
+ var height = video.videoHeight;
+ if (width < destSize.width || height < destSize.height) {
+ console.error('Video capture size too small: ' +
+ width + 'x' + height + '!');
+ }
+ var src = {};
+ if (width / destSize.width > height / destSize.height) {
+ // Full height, crop left/right.
+ src.height = height;
+ src.width = height * destSize.width / destSize.height;
+ } else {
+ // Full width, crop top/bottom.
+ src.width = width;
+ src.height = width * destSize.height / destSize.width;
+ }
+ src.x = (width - src.width) / 2;
+ src.y = (height - src.height) / 2;
+ ctx.drawImage(video, src.x, src.y, src.width, src.height,
+ 0, 0, destSize.width, destSize.height);
+ return canvas.toDataURL('image/png');
+ },
+
+ /**
+ * Handles selection change.
+ * @private
+ */
+ handleSelection_: function() {
+ var selectedItem = $('user-image-grid').selectedItem;
+ if (selectedItem === null)
+ return;
+
+ // Update preview image URL.
+ var url = selectedItem.url;
+ $('user-image-preview-img').src = url;
+
+ // Update current selection type.
+ this.selectionType = selectedItem.type;
+
+ // Show grey silhouette with the same border as stock images.
+ if (/^chrome:\/\/theme\//.test(url))
+ $('user-image-preview').classList.add('default-image');
+
+ if (ButtonImageUrls.indexOf(url) == -1) {
+ // Non-button image is selected.
+ $('ok-button').disabled = false;
+ chrome.send('selectImage', [url]);
+ } else {
+ $('ok-button').disabled = true;
+ }
+ },
+
+ /**
+ * Handles double click on the image grid.
+ * @param {Event} e Double click Event.
+ */
+ handleImageDblClick_: function(e) {
+ // If an image is double-clicked and not the grid itself, handle this
+ // as 'OK' button button press.
+ if (e.target.id != 'user-image-grid')
+ this.acceptImage_();
+ },
+
+ /**
+ * Event handler that is invoked just before the screen is shown.
+ * @param {object} data Screen init payload.
+ */
+ onBeforeShow: function(data) {
+ Oobe.getInstance().headerHidden = true;
+ $('user-image-grid').updateAndFocus();
+ chrome.send('onUserImageScreenShown');
+ // Now check again for camera presence and start capture.
+ this.checkCameraPresence_(true, true);
+ },
+
+ /**
+ * Event handler that is invoked just before the screen is hidden.
+ */
+ onBeforeHide: function() {
+ $('user-image-stream').src = '';
+ },
+
+ /**
+ * Accepts currently selected image, if possible.
+ * @private
+ */
+ acceptImage_: function() {
+ var okButton = $('ok-button');
+ if (!okButton.disabled) {
+ // This ensures that #ok-button won't be re-enabled again.
+ $('user-image-grid').disabled = true;
+ okButton.disabled = true;
+ chrome.send('onUserImageAccepted');
+ }
+ },
+
+ /**
+ * @param {boolean} present Whether a camera is present or not.
+ */
+ get cameraPresent() {
+ return this.cameraPresent_;
+ },
+ set cameraPresent(value) {
+ this.cameraPresent_ = value;
+ if (this.cameraLive)
+ this.cameraImage = null;
+ },
+
+ /**
+ * Start camera presence check.
+ * @param {boolean} autoplay Whether to start capture immediately.
+ * @param {boolean} preselect Whether to select camera automatically.
+ * @private
+ */
+ checkCameraPresence_: function(autoplay, preselect) {
+ $('user-image-preview').classList.remove('online');
+ navigator.webkitGetUserMedia(
+ {video: true},
+ this.handleCameraAvailable_.bind(this, autoplay, preselect),
+ // When ready to capture camera, poll regularly for camera presence.
+ this.handleCameraAbsent_.bind(this, /* recheck= */ autoplay));
+ },
+
+ /**
+ * Handles successful camera check.
+ * @param {boolean} autoplay Whether to start capture immediately.
+ * @param {boolean} preselect Whether to select camera automatically.
+ * @param {MediaStream} stream Stream object as returned by getUserMedia.
+ * @private
+ */
+ handleCameraAvailable_: function(autoplay, preselect, stream) {
+ if (autoplay)
+ $('user-image-stream').src = window.webkitURL.createObjectURL(stream);
+ this.cameraPresent = true;
+ if (preselect)
+ $('user-image-grid').selectedItem = this.cameraImage;
+ },
+
+ /**
+ * Handles camera check failure.
+ * @param {boolean} recheck Whether to check for camera again.
+ * @param {NavigatorUserMediaError=} err Error object.
+ * @private
+ */
+ handleCameraAbsent_: function(recheck, err) {
+ this.cameraPresent = false;
Nikita (slow) 2012/06/13 14:07:35 $('user-image-preview').classList.remove('online')
Ivan Korotkov 2012/06/13 14:26:06 Done.
+ // |preselect| is |false| in this case to not override user's selection.
+ if (recheck) {
+ setTimeout(this.checkCameraPresence_.bind(this, true, false),
+ CAMERA_CHECK_INTERVAL_MS);
+ }
+ if (this.cameraLiveCheckTimer_) {
+ clearInterval(this.cameraLiveCheckTimer_);
+ this.cameraLiveCheckTimer_ = null;
+ }
+ },
+
+ /**
+ * Handles successful camera capture start.
+ * @private
+ */
+ handleVideoStarted_: function() {
+ $('user-image-preview').classList.add('online');
+ this.cameraLiveCheckTimer_ = setInterval(this.checkCameraLive_.bind(this),
+ CAMERA_LIVENESS_CHECK_MS);
+ },
+
+ /**
+ * Handles camera stream update. Called regularly (at rate no greater then
+ * 4/sec) while camera stream is live.
+ * @private
+ */
+ handleVideoUpdate_: function() {
+ this.lastFrameTime_ = new Date().getTime();
+ },
+
+ /**
+ * Checks if camera is still live by comparing the timestamp of the last
+ * 'timeupdate' event with the current time.
+ * @private
+ */
+ checkCameraLive_: function() {
+ if (new Date().getTime() - this.lastFrameTime_ > CAMERA_LIVENESS_CHECK_MS)
+ this.handleCameraAbsent_(true, null);
+ },
+
+ /**
+ * Current image captured from camera as data URL. Setting to null will
+ * return to the live camera stream.
+ * @type {string=}
+ */
+ get cameraImage() {
+ return this.cameraImage_;
+ },
+ set cameraImage(imageUrl) {
+ this.cameraLive = !imageUrl;
+ var imageGrid = $('user-image-grid');
+ if (this.cameraPresent && !imageUrl) {
+ imageUrl = ButtonImages.TAKE_PHOTO;
+ }
+ if (imageUrl) {
+ this.cameraImage_ = this.cameraImage_ ?
+ imageGrid.updateItem(this.cameraImage_, imageUrl) :
+ imageGrid.addItem(imageUrl, undefined, undefined, 0);
+ this.cameraImage_.type = 'camera';
+ } else {
+ imageGrid.removeItem(this.cameraImage_);
+ this.cameraImage_ = null;
+ }
+ imageGrid.focus();
+ },
+
+ /**
+ * Updates user profile image.
+ * @param {?string} imageUrl Image encoded as data URL. If null, user has
+ * the default profile image, which we don't want to show.
+ * @private
+ */
+ setProfileImage_: function(imageUrl) {
+ this.profileImageLoading = false;
+ if (imageUrl !== null) {
+ this.profileImage_ =
+ $('user-image-grid').updateItem(this.profileImage_, imageUrl);
+ }
+ },
+
+ /**
+ * Appends received images to the list.
+ * @param {Array.<string>} images An array of URLs to user images.
+ * @private
+ */
+ setUserImages_: function(images) {
+ var imageGrid = $('user-image-grid');
+ for (var i = 0, url; url = images[i]; i++)
+ imageGrid.addItem(url).type = 'default';
+ },
+
+ /**
+ * Selects user image with the given URL.
+ * @param {string} url URL of the image to select.
+ * @private
+ */
+ setSelectedImage_: function(url) {
+ var imageGrid = $('user-image-grid');
+ imageGrid.selectedItemUrl = url;
+ imageGrid.focus();
+ },
+
+ /**
+ * Updates the image preview caption.
+ * @private
+ */
+ updateCaption_: function() {
+ $('user-image-preview-caption').textContent =
+ (this.selectionType == 'profile') ? this.profileImageCaption : '';
+ },
+
+ /**
+ * Updates localized content of the screen that is not updated via template.
*/
updateLocalizedContent: function() {
this.updateProfileImageCaption_();

Powered by Google App Engine
This is Rietveld 408576698