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

Side by Side 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: Revert ImageDecoder changes. 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 unified diff | Download patch | Annotate | Revision Log
OLDNEW
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** 5 /**
6 * @fileoverview Oobe user image screen implementation. 6 * @fileoverview Oobe user image screen implementation.
7 */ 7 */
8 8
9 cr.define('oobe', function() { 9 cr.define('oobe', function() {
10
11 var UserImagesGrid = options.UserImagesGrid; 10 var UserImagesGrid = options.UserImagesGrid;
12 var ButtonImages = UserImagesGrid.ButtonImages; 11 var ButtonImages = UserImagesGrid.ButtonImages;
13 12
14 /** 13 /**
15 * Array of button URLs used on this page. 14 * Array of button URLs used on this page.
16 * @type {Array.<string>} 15 * @type {Array.<string>}
16 * @const
17 */ 17 */
18 const ButtonImageUrls = [ 18 var ButtonImageUrls = [
19 ButtonImages.TAKE_PHOTO 19 ButtonImages.TAKE_PHOTO
20 ]; 20 ];
21 21
22 /** 22 /**
23 * Creates a new oobe screen div. 23 * Creates a new OOBE screen div.
24 * @constructor 24 * @constructor
25 * @extends {HTMLDivElement} 25 * @extends {HTMLDivElement}
26 */ 26 */
27 var UserImageScreen = cr.ui.define('div'); 27 var UserImageScreen = cr.ui.define('div');
28 28
29 /** 29 /**
30 * Dimensions for camera capture.
31 * @const
32 */
33 var CAPTURE_SIZE = {
34 height: 480,
35 width: 480
36 };
37
38 /**
39 * Interval between consecutive camera presence checks in msec while camera is
40 * not present.
41 * @const
42 */
43 var CAMERA_CHECK_INTERVAL_MS = 3000;
44
45 /**
46 * Interval between consecutive camera liveness checks in msec.
47 * @const
48 */
49 var CAMERA_LIVENESS_CHECK_MS = 1000;
50
51 /**
30 * Registers with Oobe. 52 * Registers with Oobe.
31 */ 53 */
32 UserImageScreen.register = function() { 54 UserImageScreen.register = function() {
33 var screen = $('user-image'); 55 var screen = $('user-image');
56 var isWebRTC = document.documentElement.getAttribute('camera') == 'webrtc';
57 UserImageScreen.prototype = isWebRTC ? UserImageScreenWebRTCProto :
58 UserImageScreenOldProto;
34 UserImageScreen.decorate(screen); 59 UserImageScreen.decorate(screen);
35 Oobe.getInstance().registerScreen(screen); 60 Oobe.getInstance().registerScreen(screen);
36 }; 61 };
37 62
38 UserImageScreen.prototype = { 63 var UserImageScreenOldProto = {
39 __proto__: HTMLDivElement.prototype, 64 __proto__: HTMLDivElement.prototype,
40 65
41 /** 66 /**
42 * Currently selected user image index (take photo button is with zero 67 * Currently selected user image index (take photo button is with zero
43 * index). 68 * index).
44 * @type {number} 69 * @type {number}
45 */ 70 */
46 selectedUserImage_: -1, 71 selectedUserImage_: -1,
47 72
48 /** @inheritDoc */ 73 /** @inheritDoc */
(...skipping 46 matching lines...) Expand 10 before | Expand all | Expand 10 after
95 120
96 /** 121 /**
97 * Buttons in oobe wizard's button strip. 122 * Buttons in oobe wizard's button strip.
98 * @type {array} Array of Buttons. 123 * @type {array} Array of Buttons.
99 */ 124 */
100 get buttons() { 125 get buttons() {
101 var okButton = this.ownerDocument.createElement('button'); 126 var okButton = this.ownerDocument.createElement('button');
102 okButton.id = 'ok-button'; 127 okButton.id = 'ok-button';
103 okButton.textContent = localStrings.getString('okButtonText'); 128 okButton.textContent = localStrings.getString('okButtonText');
104 okButton.addEventListener('click', this.acceptImage_.bind(this)); 129 okButton.addEventListener('click', this.acceptImage_.bind(this));
105 return [ okButton ]; 130 return [okButton];
106 }, 131 },
107 132
108 /** 133 /**
109 * The caption to use for the Profile image preview. 134 * The caption to use for the Profile image preview.
110 * @type {string} 135 * @type {string}
111 */ 136 */
112 get profileImageCaption() { 137 get profileImageCaption() {
113 return this.profileImageCaption_; 138 return this.profileImageCaption_;
114 }, 139 },
115 set profileImageCaption(value) { 140 set profileImageCaption(value) {
(...skipping 199 matching lines...) Expand 10 before | Expand all | Expand 10 after
315 * Updates the image preview caption. 340 * Updates the image preview caption.
316 * @private 341 * @private
317 */ 342 */
318 updateCaption_: function() { 343 updateCaption_: function() {
319 $('user-image-preview-caption').textContent = 344 $('user-image-preview-caption').textContent =
320 this.profileImageSelected ? this.profileImageCaption : ''; 345 this.profileImageSelected ? this.profileImageCaption : '';
321 }, 346 },
322 347
323 /** 348 /**
324 * Updates localized content of the screen that is not updated via template. 349 * Updates localized content of the screen that is not updated via template.
325 * @public
326 */ 350 */
327 updateLocalizedContent: function() { 351 updateLocalizedContent: function() {
328 this.updateProfileImageCaption_(); 352 this.updateProfileImageCaption_();
353 },
354
355 /**
356 * Updates profile image caption.
357 * @private
358 */
359 updateProfileImageCaption_: function() {
360 this.profileImageCaption = localStrings.getString(
361 this.profileImageLoading_ ? 'profilePhotoLoading' : 'profilePhoto');
362 }
363 };
364
365 var UserImageScreenWebRTCProto = {
366 __proto__: HTMLDivElement.prototype,
367
368 /**
369 * Currently selected user image index (take photo button is with zero
370 * index).
371 * @type {number}
372 */
373 selectedUserImage_: -1,
374
375 /** @inheritDoc */
376 decorate: function(element) {
377 var imageGrid = $('user-image-grid');
378 UserImagesGrid.decorate(imageGrid);
379
380 imageGrid.addEventListener('change',
381 this.handleSelection_.bind(this));
382 imageGrid.addEventListener('activate',
383 this.handleImageActivated_.bind(this));
384 imageGrid.addEventListener('dblclick',
385 this.handleImageDblClick_.bind(this));
386
387 // Profile image data (if present).
388 this.profileImage_ = imageGrid.addItem(
389 ButtonImages.PROFILE_PICTURE,
390 undefined, undefined, undefined,
391 function(el) { // Custom decorator for Profile image element.
392 var spinner = el.ownerDocument.createElement('div');
393 spinner.className = 'spinner';
394 var spinnerBg = el.ownerDocument.createElement('div');
395 spinnerBg.className = 'spinner-bg';
396 spinnerBg.appendChild(spinner);
397 el.appendChild(spinnerBg);
398 el.id = 'profile-image';
399 });
400 this.profileImage_.type = 'profile';
401 this.selectionType = 'default';
402
403 var video = $('user-image-stream');
404 video.addEventListener('canplay', this.handleVideoStarted_.bind(this));
405 video.addEventListener('timeupdate', this.handleVideoUpdate_.bind(this));
406 $('take-photo').addEventListener('click',
407 this.handleTakePhoto_.bind(this));
408 $('discard-photo').addEventListener('click',
409 this.handleDiscardPhoto_.bind(this));
410 this.cameraImage = null;
411 // Perform an early check if camera is present, without starting capture.
412 this.checkCameraPresence_(false, false);
413 },
414
415 /**
416 * Header text of the screen.
417 * @type {string}
418 */
419 get header() {
420 return localStrings.getString('userImageScreenTitle');
421 },
422
423 /**
424 * Buttons in oobe wizard's button strip.
425 * @type {array} Array of Buttons.
426 */
427 get buttons() {
428 var okButton = this.ownerDocument.createElement('button');
429 okButton.id = 'ok-button';
430 okButton.textContent = localStrings.getString('okButtonText');
431 okButton.addEventListener('click', this.acceptImage_.bind(this));
432 return [okButton];
433 },
434
435 /**
436 * The caption to use for the Profile image preview.
437 * @type {string}
438 */
439 get profileImageCaption() {
440 return this.profileImageCaption_;
441 },
442 set profileImageCaption(value) {
443 this.profileImageCaption_ = value;
444 this.updateCaption_();
445 },
446
447 /**
448 * True if the Profile image is being loaded.
449 * @type {boolean}
450 */
451 get profileImageLoading() {
452 return this.profileImageLoading_;
453 },
454 set profileImageLoading(value) {
455 this.profileImageLoading_ = value;
456 $('user-image-screen-main').classList[
457 value ? 'add' : 'remove']('profile-image-loading');
458 this.updateProfileImageCaption_();
459 },
460
461 /**
462 * True when camera is in live mode (i.e. no still photo selected).
463 * @type {boolean}
464 */
465 get cameraLive() {
466 return this.cameraLive_;
467 },
468 set cameraLive(value) {
469 this.cameraLive_ = value;
470 $('user-image-preview').classList[value ? 'add' : 'remove']('live');
471 },
472
473 /**
474 * Type of the selected image (one of 'default', 'profile', 'camera').
475 * @type {string}
476 */
477 get selectionType() {
478 return this.selectionType_;
479 },
480 set selectionType(value) {
481 this.selectionType_ = value;
482 var previewClassList = $('user-image-preview').classList;
483 previewClassList[value == 'default' ? 'add' : 'remove']('default-image');
484 previewClassList[value == 'profile' ? 'add' : 'remove']('profile-image');
485 previewClassList[value == 'camera' ? 'add' : 'remove']('camera');
486 this.updateCaption_();
487 },
488
489 /**
490 * Handles image activation (by pressing Enter).
491 * @private
492 */
493 handleImageActivated_: function() {
494 switch ($('user-image-grid').selectedItemUrl) {
495 case ButtonImages.TAKE_PHOTO:
496 this.handleTakePhoto_();
497 break;
498 default:
499 this.acceptImage_();
500 break;
501 }
502 },
503
504 /**
505 * Handles photo capture from the live camera stream.
506 * @private
507 */
508 handleTakePhoto_: function() {
509 var self = this;
510 var photoURL = this.captureFrame_($('user-image-stream'), CAPTURE_SIZE);
511 chrome.send('photoTaken', [photoURL]);
512 // Wait until image is loaded before displaying it.
513 var previewImg = new Image();
514 previewImg.addEventListener('load', function(e) {
515 self.cameraImage = this.src;
516 });
517 previewImg.src = photoURL;
518 },
519
520 /**
521 * Discard current photo and return to the live camera stream.
522 * @private
523 */
524 handleDiscardPhoto_: function() {
525 this.cameraImage = null;
526 },
527
528 /**
529 * Capture a single still frame from a <video> element.
530 * @param {HTMLVideoElement} video Video element to capture from.
531 * @param {{width: number, height: number}} destSize Capture size.
532 * @return {string} Captured frame as a data URL.
533 * @private
534 */
535 captureFrame_: function(video, destSize) {
536 var canvas = document.createElement('canvas');
537 canvas.width = destSize.width;
538 canvas.height = destSize.height;
539 var ctx = canvas.getContext('2d');
540 var width = video.videoWidth;
541 var height = video.videoHeight;
542 if (width < destSize.width || height < destSize.height) {
543 console.error('Video capture size too small: ' +
544 width + 'x' + height + '!');
545 }
546 var src = {};
547 if (width / destSize.width > height / destSize.height) {
548 // Full height, crop left/right.
549 src.height = height;
550 src.width = height * destSize.width / destSize.height;
551 } else {
552 // Full width, crop top/bottom.
553 src.width = width;
554 src.height = width * destSize.height / destSize.width;
555 }
556 src.x = (width - src.width) / 2;
557 src.y = (height - src.height) / 2;
558 ctx.drawImage(video, src.x, src.y, src.width, src.height,
559 0, 0, destSize.width, destSize.height);
560 return canvas.toDataURL('image/png');
561 },
562
563 /**
564 * Handles selection change.
565 * @private
566 */
567 handleSelection_: function() {
568 var selectedItem = $('user-image-grid').selectedItem;
569 if (selectedItem === null)
570 return;
571
572 // Update preview image URL.
573 var url = selectedItem.url;
574 $('user-image-preview-img').src = url;
575
576 // Update current selection type.
577 this.selectionType = selectedItem.type;
578
579 // Show grey silhouette with the same border as stock images.
580 if (/^chrome:\/\/theme\//.test(url))
581 $('user-image-preview').classList.add('default-image');
582
583 if (ButtonImageUrls.indexOf(url) == -1) {
584 // Non-button image is selected.
585 $('ok-button').disabled = false;
586 chrome.send('selectImage', [url]);
587 } else {
588 $('ok-button').disabled = true;
589 }
590 },
591
592 /**
593 * Handles double click on the image grid.
594 * @param {Event} e Double click Event.
595 */
596 handleImageDblClick_: function(e) {
597 // If an image is double-clicked and not the grid itself, handle this
598 // as 'OK' button button press.
599 if (e.target.id != 'user-image-grid')
600 this.acceptImage_();
601 },
602
603 /**
604 * Event handler that is invoked just before the screen is shown.
605 * @param {object} data Screen init payload.
606 */
607 onBeforeShow: function(data) {
608 Oobe.getInstance().headerHidden = true;
609 $('user-image-grid').updateAndFocus();
610 chrome.send('onUserImageScreenShown');
611 // Now check again for camera presence and start capture.
612 this.checkCameraPresence_(true, true);
613 },
614
615 /**
616 * Event handler that is invoked just before the screen is hidden.
617 */
618 onBeforeHide: function() {
619 $('user-image-stream').src = '';
620 },
621
622 /**
623 * Accepts currently selected image, if possible.
624 * @private
625 */
626 acceptImage_: function() {
627 var okButton = $('ok-button');
628 if (!okButton.disabled) {
629 // This ensures that #ok-button won't be re-enabled again.
630 $('user-image-grid').disabled = true;
631 okButton.disabled = true;
632 chrome.send('onUserImageAccepted');
633 }
634 },
635
636 /**
637 * @param {boolean} present Whether a camera is present or not.
638 */
639 get cameraPresent() {
640 return this.cameraPresent_;
641 },
642 set cameraPresent(value) {
643 this.cameraPresent_ = value;
644 if (this.cameraLive)
645 this.cameraImage = null;
646 },
647
648 /**
649 * Start camera presence check.
650 * @param {boolean} autoplay Whether to start capture immediately.
651 * @param {boolean} preselect Whether to select camera automatically.
652 * @private
653 */
654 checkCameraPresence_: function(autoplay, preselect) {
655 $('user-image-preview').classList.remove('online');
656 navigator.webkitGetUserMedia(
657 {video: true},
658 this.handleCameraAvailable_.bind(this, autoplay, preselect),
659 // When ready to capture camera, poll regularly for camera presence.
660 this.handleCameraAbsent_.bind(this, /* recheck= */ autoplay));
661 },
662
663 /**
664 * Handles successful camera check.
665 * @param {boolean} autoplay Whether to start capture immediately.
666 * @param {boolean} preselect Whether to select camera automatically.
667 * @param {MediaStream} stream Stream object as returned by getUserMedia.
668 * @private
669 */
670 handleCameraAvailable_: function(autoplay, preselect, stream) {
671 if (autoplay)
672 $('user-image-stream').src = window.webkitURL.createObjectURL(stream);
673 this.cameraPresent = true;
674 if (preselect)
675 $('user-image-grid').selectedItem = this.cameraImage;
676 },
677
678 /**
679 * Handles camera check failure.
680 * @param {boolean} recheck Whether to check for camera again.
681 * @param {NavigatorUserMediaError=} err Error object.
682 * @private
683 */
684 handleCameraAbsent_: function(recheck, err) {
685 this.cameraPresent = false;
686 $('user-image-preview').classList.remove('online');
687 // |preselect| is |false| in this case to not override user's selection.
688 if (recheck) {
689 setTimeout(this.checkCameraPresence_.bind(this, true, false),
690 CAMERA_CHECK_INTERVAL_MS);
691 }
692 if (this.cameraLiveCheckTimer_) {
693 clearInterval(this.cameraLiveCheckTimer_);
694 this.cameraLiveCheckTimer_ = null;
695 }
696 },
697
698 /**
699 * Handles successful camera capture start.
700 * @private
701 */
702 handleVideoStarted_: function() {
703 $('user-image-preview').classList.add('online');
704 this.cameraLiveCheckTimer_ = setInterval(this.checkCameraLive_.bind(this),
705 CAMERA_LIVENESS_CHECK_MS);
706 },
707
708 /**
709 * Handles camera stream update. Called regularly (at rate no greater then
710 * 4/sec) while camera stream is live.
711 * @private
712 */
713 handleVideoUpdate_: function() {
714 this.lastFrameTime_ = new Date().getTime();
715 },
716
717 /**
718 * Checks if camera is still live by comparing the timestamp of the last
719 * 'timeupdate' event with the current time.
720 * @private
721 */
722 checkCameraLive_: function() {
723 if (new Date().getTime() - this.lastFrameTime_ > CAMERA_LIVENESS_CHECK_MS)
724 this.handleCameraAbsent_(true, null);
725 },
726
727 /**
728 * Current image captured from camera as data URL. Setting to null will
729 * return to the live camera stream.
730 * @type {string=}
731 */
732 get cameraImage() {
733 return this.cameraImage_;
734 },
735 set cameraImage(imageUrl) {
736 this.cameraLive = !imageUrl;
737 var imageGrid = $('user-image-grid');
738 if (this.cameraPresent && !imageUrl) {
739 imageUrl = ButtonImages.TAKE_PHOTO;
740 }
741 if (imageUrl) {
742 this.cameraImage_ = this.cameraImage_ ?
743 imageGrid.updateItem(this.cameraImage_, imageUrl) :
744 imageGrid.addItem(imageUrl, undefined, undefined, 0);
745 this.cameraImage_.type = 'camera';
746 } else {
747 imageGrid.removeItem(this.cameraImage_);
748 this.cameraImage_ = null;
749 }
750 imageGrid.focus();
751 },
752
753 /**
754 * Updates user profile image.
755 * @param {?string} imageUrl Image encoded as data URL. If null, user has
756 * the default profile image, which we don't want to show.
757 * @private
758 */
759 setProfileImage_: function(imageUrl) {
760 this.profileImageLoading = false;
761 if (imageUrl !== null) {
762 this.profileImage_ =
763 $('user-image-grid').updateItem(this.profileImage_, imageUrl);
764 }
765 },
766
767 /**
768 * Appends received images to the list.
769 * @param {Array.<string>} images An array of URLs to user images.
770 * @private
771 */
772 setUserImages_: function(images) {
773 var imageGrid = $('user-image-grid');
774 for (var i = 0, url; url = images[i]; i++)
775 imageGrid.addItem(url).type = 'default';
776 },
777
778 /**
779 * Selects user image with the given URL.
780 * @param {string} url URL of the image to select.
781 * @private
782 */
783 setSelectedImage_: function(url) {
784 var imageGrid = $('user-image-grid');
785 imageGrid.selectedItemUrl = url;
786 imageGrid.focus();
787 },
788
789 /**
790 * Updates the image preview caption.
791 * @private
792 */
793 updateCaption_: function() {
794 $('user-image-preview-caption').textContent =
795 (this.selectionType == 'profile') ? this.profileImageCaption : '';
796 },
797
798 /**
799 * Updates localized content of the screen that is not updated via template.
800 */
801 updateLocalizedContent: function() {
802 this.updateProfileImageCaption_();
329 }, 803 },
330 804
331 /** 805 /**
332 * Updates profile image caption. 806 * Updates profile image caption.
333 * @private 807 * @private
334 */ 808 */
335 updateProfileImageCaption_: function() { 809 updateProfileImageCaption_: function() {
336 this.profileImageCaption = localStrings.getString( 810 this.profileImageCaption = localStrings.getString(
337 this.profileImageLoading_ ? 'profilePhotoLoading' : 'profilePhoto'); 811 this.profileImageLoading_ ? 'profilePhotoLoading' : 'profilePhoto');
338 } 812 }
339 }; 813 };
340 814
341 // Forward public APIs to private implementations. 815 // Forward public APIs to private implementations.
342 [ 816 [
343 'setCameraPresent', 817 'setCameraPresent',
344 'setProfileImage', 818 'setProfileImage',
345 'setSelectedImage', 819 'setSelectedImage',
346 'setUserImages', 820 'setUserImages',
347 'setUserPhoto', 821 'setUserPhoto',
348 ].forEach(function(name) { 822 ].forEach(function(name) {
349 UserImageScreen[name] = function(value) { 823 UserImageScreen[name] = function(value) {
350 $('user-image')[name + '_'](value); 824 $('user-image')[name + '_'](value);
351 }; 825 };
352 }); 826 });
353 827
354 return { 828 return {
355 UserImageScreen: UserImageScreen 829 UserImageScreen: UserImageScreen
356 }; 830 };
357 }); 831 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698