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

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: 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 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 = 1000;
Nikita (slow) 2012/06/13 14:07:35 3000ms
Ivan Korotkov 2012/06/13 14:26:06 Done.
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;
Nikita (slow) 2012/06/13 14:07:35 $('user-image-preview').classList.remove('online')
Ivan Korotkov 2012/06/13 14:26:06 Done.
686 // |preselect| is |false| in this case to not override user's selection.
687 if (recheck) {
688 setTimeout(this.checkCameraPresence_.bind(this, true, false),
689 CAMERA_CHECK_INTERVAL_MS);
690 }
691 if (this.cameraLiveCheckTimer_) {
692 clearInterval(this.cameraLiveCheckTimer_);
693 this.cameraLiveCheckTimer_ = null;
694 }
695 },
696
697 /**
698 * Handles successful camera capture start.
699 * @private
700 */
701 handleVideoStarted_: function() {
702 $('user-image-preview').classList.add('online');
703 this.cameraLiveCheckTimer_ = setInterval(this.checkCameraLive_.bind(this),
704 CAMERA_LIVENESS_CHECK_MS);
705 },
706
707 /**
708 * Handles camera stream update. Called regularly (at rate no greater then
709 * 4/sec) while camera stream is live.
710 * @private
711 */
712 handleVideoUpdate_: function() {
713 this.lastFrameTime_ = new Date().getTime();
714 },
715
716 /**
717 * Checks if camera is still live by comparing the timestamp of the last
718 * 'timeupdate' event with the current time.
719 * @private
720 */
721 checkCameraLive_: function() {
722 if (new Date().getTime() - this.lastFrameTime_ > CAMERA_LIVENESS_CHECK_MS)
723 this.handleCameraAbsent_(true, null);
724 },
725
726 /**
727 * Current image captured from camera as data URL. Setting to null will
728 * return to the live camera stream.
729 * @type {string=}
730 */
731 get cameraImage() {
732 return this.cameraImage_;
733 },
734 set cameraImage(imageUrl) {
735 this.cameraLive = !imageUrl;
736 var imageGrid = $('user-image-grid');
737 if (this.cameraPresent && !imageUrl) {
738 imageUrl = ButtonImages.TAKE_PHOTO;
739 }
740 if (imageUrl) {
741 this.cameraImage_ = this.cameraImage_ ?
742 imageGrid.updateItem(this.cameraImage_, imageUrl) :
743 imageGrid.addItem(imageUrl, undefined, undefined, 0);
744 this.cameraImage_.type = 'camera';
745 } else {
746 imageGrid.removeItem(this.cameraImage_);
747 this.cameraImage_ = null;
748 }
749 imageGrid.focus();
750 },
751
752 /**
753 * Updates user profile image.
754 * @param {?string} imageUrl Image encoded as data URL. If null, user has
755 * the default profile image, which we don't want to show.
756 * @private
757 */
758 setProfileImage_: function(imageUrl) {
759 this.profileImageLoading = false;
760 if (imageUrl !== null) {
761 this.profileImage_ =
762 $('user-image-grid').updateItem(this.profileImage_, imageUrl);
763 }
764 },
765
766 /**
767 * Appends received images to the list.
768 * @param {Array.<string>} images An array of URLs to user images.
769 * @private
770 */
771 setUserImages_: function(images) {
772 var imageGrid = $('user-image-grid');
773 for (var i = 0, url; url = images[i]; i++)
774 imageGrid.addItem(url).type = 'default';
775 },
776
777 /**
778 * Selects user image with the given URL.
779 * @param {string} url URL of the image to select.
780 * @private
781 */
782 setSelectedImage_: function(url) {
783 var imageGrid = $('user-image-grid');
784 imageGrid.selectedItemUrl = url;
785 imageGrid.focus();
786 },
787
788 /**
789 * Updates the image preview caption.
790 * @private
791 */
792 updateCaption_: function() {
793 $('user-image-preview-caption').textContent =
794 (this.selectionType == 'profile') ? this.profileImageCaption : '';
795 },
796
797 /**
798 * Updates localized content of the screen that is not updated via template.
799 */
800 updateLocalizedContent: function() {
801 this.updateProfileImageCaption_();
329 }, 802 },
330 803
331 /** 804 /**
332 * Updates profile image caption. 805 * Updates profile image caption.
333 * @private 806 * @private
334 */ 807 */
335 updateProfileImageCaption_: function() { 808 updateProfileImageCaption_: function() {
336 this.profileImageCaption = localStrings.getString( 809 this.profileImageCaption = localStrings.getString(
337 this.profileImageLoading_ ? 'profilePhotoLoading' : 'profilePhoto'); 810 this.profileImageLoading_ ? 'profilePhotoLoading' : 'profilePhoto');
338 } 811 }
339 }; 812 };
340 813
341 // Forward public APIs to private implementations. 814 // Forward public APIs to private implementations.
342 [ 815 [
343 'setCameraPresent', 816 'setCameraPresent',
344 'setProfileImage', 817 'setProfileImage',
345 'setSelectedImage', 818 'setSelectedImage',
346 'setUserImages', 819 'setUserImages',
347 'setUserPhoto', 820 'setUserPhoto',
348 ].forEach(function(name) { 821 ].forEach(function(name) {
349 UserImageScreen[name] = function(value) { 822 UserImageScreen[name] = function(value) {
350 $('user-image')[name + '_'](value); 823 $('user-image')[name + '_'](value);
351 }; 824 };
352 }); 825 });
353 826
354 return { 827 return {
355 UserImageScreen: UserImageScreen 828 UserImageScreen: UserImageScreen
356 }; 829 };
357 }); 830 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698