| OLD | NEW |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. | 1 // Copyright 2015 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 | 6 * @fileoverview |
| 7 * 'settings-camera' is the Polymer control used to take a picture from the | 7 * 'settings-camera' is the Polymer control used to take a picture from the |
| 8 * user webcam to use as a ChromeOS profile picture. | 8 * user webcam to use as a ChromeOS profile picture. |
| 9 */ | 9 */ |
| 10 (function() { | 10 (function() { |
| 11 | 11 |
| 12 /** | |
| 13 * Dimensions for camera capture. | |
| 14 * @const | |
| 15 */ | |
| 16 var CAPTURE_SIZE = { | |
| 17 height: 480, | |
| 18 width: 480 | |
| 19 }; | |
| 20 | |
| 21 Polymer({ | |
| 22 is: 'settings-camera', | |
| 23 | |
| 24 properties: { | |
| 25 /** | |
| 26 * True if the user has selected the camera as the user image source. | |
| 27 * @type {boolean} | |
| 28 */ | |
| 29 cameraActive: { | |
| 30 type: Boolean, | |
| 31 observer: 'cameraActiveChanged_', | |
| 32 value: false, | |
| 33 }, | |
| 34 | |
| 35 /** | |
| 36 * True when the camera is actually streaming video. May be false even when | |
| 37 * the camera is present and shown, but still initializing. | |
| 38 * @private {boolean} | |
| 39 */ | |
| 40 cameraOnline_: { | |
| 41 type: Boolean, | |
| 42 value: false, | |
| 43 }, | |
| 44 | |
| 45 /** | |
| 46 * True if the photo is currently marked flipped. | |
| 47 * @private {boolean} | |
| 48 */ | |
| 49 isFlipped_: { | |
| 50 type: Boolean, | |
| 51 value: false, | |
| 52 }, | |
| 53 }, | |
| 54 | |
| 55 /** @override */ | |
| 56 attached: function() { | |
| 57 this.$.cameraVideo.addEventListener('canplay', function() { | |
| 58 this.cameraOnline_ = true; | |
| 59 }.bind(this)); | |
| 60 }, | |
| 61 | |
| 62 /** | 12 /** |
| 63 * Performs photo capture from the live camera stream. 'phototaken' event | 13 * Dimensions for camera capture. |
| 64 * will be fired as soon as captured photo is available, with 'dataURL' | 14 * @const |
| 65 * property containing the photo encoded as a data URL. | |
| 66 * @private | |
| 67 */ | 15 */ |
| 68 takePhoto: function() { | 16 var CAPTURE_SIZE = {height: 480, width: 480}; |
| 69 if (!this.cameraOnline_) | 17 |
| 70 return; | 18 Polymer({ |
| 71 var canvas = | 19 is: 'settings-camera', |
| 72 /** @type {HTMLCanvasElement} */ (document.createElement('canvas')); | 20 |
| 73 canvas.width = CAPTURE_SIZE.width; | 21 properties: { |
| 74 canvas.height = CAPTURE_SIZE.height; | 22 /** |
| 75 this.captureFrame_( | 23 * True if the user has selected the camera as the user image source. |
| 76 this.$.cameraVideo, | 24 * @type {boolean} |
| 77 /** @type {!CanvasRenderingContext2D} */ (canvas.getContext('2d'))); | 25 */ |
| 78 | 26 cameraActive: { |
| 79 var photoDataUrl = this.isFlipped_ ? this.flipFrame_(canvas) : | 27 type: Boolean, |
| 80 canvas.toDataURL('image/png'); | 28 observer: 'cameraActiveChanged_', |
| 81 this.fire('phototaken', {photoDataUrl: photoDataUrl}); | 29 value: false, |
| 82 | 30 }, |
| 83 announceAccessibleMessage( | 31 |
| 84 loadTimeData.getString('photoCaptureAccessibleText')); | 32 /** |
| 85 }, | 33 * True when the camera is actually streaming video. May be false even |
| 86 | 34 * when |
| 87 /** | 35 * the camera is present and shown, but still initializing. |
| 88 * Observer for the cameraActive property. | 36 * @private {boolean} |
| 89 * @private | 37 */ |
| 90 */ | 38 cameraOnline_: { |
| 91 cameraActiveChanged_: function() { | 39 type: Boolean, |
| 92 if (this.cameraActive) | 40 value: false, |
| 93 this.startCamera_(); | 41 }, |
| 94 else | 42 |
| 43 /** |
| 44 * True if the photo is currently marked flipped. |
| 45 * @private {boolean} |
| 46 */ |
| 47 isFlipped_: { |
| 48 type: Boolean, |
| 49 value: false, |
| 50 }, |
| 51 }, |
| 52 |
| 53 /** @override */ |
| 54 attached: function() { |
| 55 this.$.cameraVideo.addEventListener('canplay', function() { |
| 56 this.cameraOnline_ = true; |
| 57 }.bind(this)); |
| 58 }, |
| 59 |
| 60 /** |
| 61 * Performs photo capture from the live camera stream. 'phototaken' event |
| 62 * will be fired as soon as captured photo is available, with 'dataURL' |
| 63 * property containing the photo encoded as a data URL. |
| 64 * @private |
| 65 */ |
| 66 takePhoto: function() { |
| 67 if (!this.cameraOnline_) |
| 68 return; |
| 69 var canvas = |
| 70 /** @type {HTMLCanvasElement} */ (document.createElement('canvas')); |
| 71 canvas.width = CAPTURE_SIZE.width; |
| 72 canvas.height = CAPTURE_SIZE.height; |
| 73 this.captureFrame_( |
| 74 this.$.cameraVideo, |
| 75 /** @type {!CanvasRenderingContext2D} */ (canvas.getContext('2d'))); |
| 76 |
| 77 var photoDataUrl = this.isFlipped_ ? this.flipFrame_(canvas) : |
| 78 canvas.toDataURL('image/png'); |
| 79 this.fire('phototaken', {photoDataUrl: photoDataUrl}); |
| 80 |
| 81 announceAccessibleMessage( |
| 82 loadTimeData.getString('photoCaptureAccessibleText')); |
| 83 }, |
| 84 |
| 85 /** |
| 86 * Observer for the cameraActive property. |
| 87 * @private |
| 88 */ |
| 89 cameraActiveChanged_: function() { |
| 90 if (this.cameraActive) |
| 91 this.startCamera_(); |
| 92 else |
| 93 this.stopCamera_(); |
| 94 }, |
| 95 |
| 96 /** |
| 97 * Tries to start the camera stream capture. |
| 98 * @private |
| 99 */ |
| 100 startCamera_: function() { |
| 95 this.stopCamera_(); | 101 this.stopCamera_(); |
| 96 }, | 102 this.cameraStartInProgress_ = true; |
| 97 | 103 |
| 98 /** | 104 var successCallback = function(stream) { |
| 99 * Tries to start the camera stream capture. | 105 if (this.cameraStartInProgress_) { |
| 100 * @private | 106 this.$.cameraVideo.src = URL.createObjectURL(stream); |
| 101 */ | 107 this.cameraStream_ = stream; |
| 102 startCamera_: function() { | 108 } else { |
| 103 this.stopCamera_(); | 109 this.stopVideoTracks_(stream); |
| 104 this.cameraStartInProgress_ = true; | 110 } |
| 105 | 111 this.cameraStartInProgress_ = false; |
| 106 var successCallback = function(stream) { | 112 }.bind(this); |
| 107 if (this.cameraStartInProgress_) { | 113 |
| 108 this.$.cameraVideo.src = URL.createObjectURL(stream); | 114 var errorCallback = function() { |
| 109 this.cameraStream_ = stream; | 115 this.cameraOnline_ = false; |
| 116 this.cameraStartInProgress_ = false; |
| 117 }.bind(this); |
| 118 |
| 119 navigator.webkitGetUserMedia( |
| 120 {video: true}, successCallback, errorCallback); |
| 121 }, |
| 122 |
| 123 /** |
| 124 * Stops camera capture, if it's currently cameraActive. |
| 125 * @private |
| 126 */ |
| 127 stopCamera_: function() { |
| 128 this.cameraOnline_ = false; |
| 129 this.$.cameraVideo.src = ''; |
| 130 if (this.cameraStream_) |
| 131 this.stopVideoTracks_(this.cameraStream_); |
| 132 // Cancel any pending getUserMedia() checks. |
| 133 this.cameraStartInProgress_ = false; |
| 134 }, |
| 135 |
| 136 /** |
| 137 * Stops all video tracks associated with a MediaStream object. |
| 138 * @param {!MediaStream} stream |
| 139 * @private |
| 140 */ |
| 141 stopVideoTracks_: function(stream) { |
| 142 var tracks = stream.getVideoTracks(); |
| 143 for (var t of tracks) |
| 144 t.stop(); |
| 145 }, |
| 146 |
| 147 /** |
| 148 * Flip the live camera stream. |
| 149 * @private |
| 150 */ |
| 151 onTapFlipPhoto_: function() { |
| 152 this.isFlipped_ = !this.isFlipped_; |
| 153 this.$.userImageStreamCrop.classList.toggle('flip-x', this.isFlipped_); |
| 154 |
| 155 var flipMessageId = this.isFlipped_ ? 'photoFlippedAccessibleText' : |
| 156 'photoFlippedBackAccessibleText'; |
| 157 announceAccessibleMessage(loadTimeData.getString(flipMessageId)); |
| 158 }, |
| 159 |
| 160 /** |
| 161 * Captures a single still frame from a <video> element, placing it at the |
| 162 * current drawing origin of a canvas context. |
| 163 * @param {!HTMLVideoElement} video Video element to capture from. |
| 164 * @param {!CanvasRenderingContext2D} ctx Canvas context to draw onto. |
| 165 * @private |
| 166 */ |
| 167 captureFrame_: function(video, ctx) { |
| 168 var width = video.videoWidth; |
| 169 var height = video.videoHeight; |
| 170 if (width < CAPTURE_SIZE.width || height < CAPTURE_SIZE.height) { |
| 171 console.error( |
| 172 'Video capture size too small: ' + width + 'x' + height + '!'); |
| 173 } |
| 174 var src = {}; |
| 175 if (width / CAPTURE_SIZE.width > height / CAPTURE_SIZE.height) { |
| 176 // Full height, crop left/right. |
| 177 src.height = height; |
| 178 src.width = height * CAPTURE_SIZE.width / CAPTURE_SIZE.height; |
| 110 } else { | 179 } else { |
| 111 this.stopVideoTracks_(stream); | 180 // Full width, crop top/bottom. |
| 181 src.width = width; |
| 182 src.height = width * CAPTURE_SIZE.height / CAPTURE_SIZE.width; |
| 112 } | 183 } |
| 113 this.cameraStartInProgress_ = false; | 184 src.x = (width - src.width) / 2; |
| 114 }.bind(this); | 185 src.y = (height - src.height) / 2; |
| 115 | 186 ctx.drawImage( |
| 116 var errorCallback = function() { | 187 video, src.x, src.y, src.width, src.height, 0, 0, CAPTURE_SIZE.width, |
| 117 this.cameraOnline_ = false; | 188 CAPTURE_SIZE.height); |
| 118 this.cameraStartInProgress_ = false; | 189 }, |
| 119 }.bind(this); | 190 |
| 120 | 191 /** |
| 121 navigator.webkitGetUserMedia({video: true}, successCallback, errorCallback); | |
| 122 }, | |
| 123 | |
| 124 /** | |
| 125 * Stops camera capture, if it's currently cameraActive. | |
| 126 * @private | |
| 127 */ | |
| 128 stopCamera_: function() { | |
| 129 this.cameraOnline_ = false; | |
| 130 this.$.cameraVideo.src = ''; | |
| 131 if (this.cameraStream_) | |
| 132 this.stopVideoTracks_(this.cameraStream_); | |
| 133 // Cancel any pending getUserMedia() checks. | |
| 134 this.cameraStartInProgress_ = false; | |
| 135 }, | |
| 136 | |
| 137 /** | |
| 138 * Stops all video tracks associated with a MediaStream object. | |
| 139 * @param {!MediaStream} stream | |
| 140 * @private | |
| 141 */ | |
| 142 stopVideoTracks_: function(stream) { | |
| 143 var tracks = stream.getVideoTracks(); | |
| 144 for (var t of tracks) | |
| 145 t.stop(); | |
| 146 }, | |
| 147 | |
| 148 /** | |
| 149 * Flip the live camera stream. | |
| 150 * @private | |
| 151 */ | |
| 152 onTapFlipPhoto_: function() { | |
| 153 this.isFlipped_ = !this.isFlipped_; | |
| 154 this.$.userImageStreamCrop.classList.toggle('flip-x', this.isFlipped_); | |
| 155 | |
| 156 var flipMessageId = this.isFlipped_ ? | |
| 157 'photoFlippedAccessibleText' : 'photoFlippedBackAccessibleText'; | |
| 158 announceAccessibleMessage(loadTimeData.getString(flipMessageId)); | |
| 159 }, | |
| 160 | |
| 161 /** | |
| 162 * Captures a single still frame from a <video> element, placing it at the | |
| 163 * current drawing origin of a canvas context. | |
| 164 * @param {!HTMLVideoElement} video Video element to capture from. | |
| 165 * @param {!CanvasRenderingContext2D} ctx Canvas context to draw onto. | |
| 166 * @private | |
| 167 */ | |
| 168 captureFrame_: function(video, ctx) { | |
| 169 var width = video.videoWidth; | |
| 170 var height = video.videoHeight; | |
| 171 if (width < CAPTURE_SIZE.width || height < CAPTURE_SIZE.height) { | |
| 172 console.error('Video capture size too small: ' + | |
| 173 width + 'x' + height + '!'); | |
| 174 } | |
| 175 var src = {}; | |
| 176 if (width / CAPTURE_SIZE.width > height / CAPTURE_SIZE.height) { | |
| 177 // Full height, crop left/right. | |
| 178 src.height = height; | |
| 179 src.width = height * CAPTURE_SIZE.width / CAPTURE_SIZE.height; | |
| 180 } else { | |
| 181 // Full width, crop top/bottom. | |
| 182 src.width = width; | |
| 183 src.height = width * CAPTURE_SIZE.height / CAPTURE_SIZE.width; | |
| 184 } | |
| 185 src.x = (width - src.width) / 2; | |
| 186 src.y = (height - src.height) / 2; | |
| 187 ctx.drawImage(video, src.x, src.y, src.width, src.height, | |
| 188 0, 0, CAPTURE_SIZE.width, CAPTURE_SIZE.height); | |
| 189 }, | |
| 190 | |
| 191 /** | |
| 192 * Flips frame horizontally. | 192 * Flips frame horizontally. |
| 193 * @param {!(HTMLImageElement|HTMLCanvasElement|HTMLVideoElement)} source | 193 * @param {!(HTMLImageElement|HTMLCanvasElement|HTMLVideoElement)} source |
| 194 * Frame to flip. | 194 * Frame to flip. |
| 195 * @return {string} Flipped frame as data URL. | 195 * @return {string} Flipped frame as data URL. |
| 196 */ | 196 */ |
| 197 flipFrame_: function(source) { | 197 flipFrame_: function(source) { |
| 198 var canvas = document.createElement('canvas'); | 198 var canvas = document.createElement('canvas'); |
| 199 canvas.width = CAPTURE_SIZE.width; | 199 canvas.width = CAPTURE_SIZE.width; |
| 200 canvas.height = CAPTURE_SIZE.height; | 200 canvas.height = CAPTURE_SIZE.height; |
| 201 var ctx = canvas.getContext('2d'); | 201 var ctx = canvas.getContext('2d'); |
| 202 ctx.translate(CAPTURE_SIZE.width, 0); | 202 ctx.translate(CAPTURE_SIZE.width, 0); |
| 203 ctx.scale(-1.0, 1.0); | 203 ctx.scale(-1.0, 1.0); |
| 204 ctx.drawImage(source, 0, 0); | 204 ctx.drawImage(source, 0, 0); |
| 205 return canvas.toDataURL('image/png'); | 205 return canvas.toDataURL('image/png'); |
| 206 }, | 206 }, |
| 207 }); | 207 }); |
| 208 | 208 |
| 209 })(); | 209 })(); |
| OLD | NEW |