001package gwt.material.design.addins.client.camera; 002 003/* 004 * #%L 005 * GwtMaterial 006 * %% 007 * Copyright (C) 2015 - 2017 GwtMaterialDesign 008 * %% 009 * Licensed under the Apache License, Version 2.0 (the "License"); 010 * you may not use this file except in compliance with the License. 011 * You may obtain a copy of the License at 012 * 013 * http://www.apache.org/licenses/LICENSE-2.0 014 * 015 * Unless required by applicable law or agreed to in writing, software 016 * distributed under the License is distributed on an "AS IS" BASIS, 017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 018 * See the License for the specific language governing permissions and 019 * limitations under the License. 020 * #L% 021 */ 022 023import com.google.gwt.canvas.client.Canvas; 024import com.google.gwt.canvas.dom.client.Context2d; 025import com.google.gwt.core.client.GWT; 026import com.google.gwt.dom.client.*; 027import com.google.gwt.event.shared.HandlerRegistration; 028import gwt.material.design.addins.client.camera.base.HasCameraActions; 029import gwt.material.design.addins.client.camera.base.HasCameraCaptureHandlers; 030import gwt.material.design.addins.client.camera.constants.CameraFacingMode; 031import gwt.material.design.addins.client.camera.events.CameraCaptureEvent; 032import gwt.material.design.addins.client.camera.events.CameraCaptureEvent.CaptureStatus; 033import gwt.material.design.addins.client.camera.events.CameraCaptureHandler; 034import gwt.material.design.client.base.JsLoader; 035import gwt.material.design.client.base.MaterialWidget; 036import gwt.material.design.jscore.client.api.Navigator; 037import gwt.material.design.jscore.client.api.media.*; 038 039import static gwt.material.design.addins.client.camera.JsCamera.$; 040 041 042//@formatter:off 043 044/** 045 * <p> 046 * MaterialCameraCapture is a widget that captures the video stream from the camera, using the 047 * HTML5 {@code MediaDevices.getUserMedia()} (Streams API). This widget can capture images from the video, to allow 048 * the upload to the server of photos from the camera. 049 * </p> 050 * <p> 051 * <h3>XML Namespace Declaration</h3> 052 * <pre> 053 * {@code 054 * xmlns:ma='urn:import:gwt.material.design.addins.client' 055 * } 056 * </pre> 057 * <p> 058 * <h3>UiBinder Usage:</h3> 059 * <p><pre> 060 * {@code 061 * <ma:camera.MaterialCameraCapture ui:field="camera" /> 062 * } 063 * </pre></p> 064 * <p> 065 * <h3>Java Usage:</h3> 066 * <p><pre> 067 * if (MaterialCameraCapture.isSupported()){ 068 * MaterialCameraCapture camera = new MaterialCameraCapture(); 069 * camera.addCameraCaptureHandler(new CameraCaptureHandler() { 070 * {@literal @}Override 071 * public void onCameraCaptureChange(CameraCaptureEvent event) { 072 * if (event.getCaptureStatus() == CaptureStatus.ERRORED){ 073 * Window.alert("Error on starting the camera capture: " + event.getErrorMessage()); 074 * ((MaterialCameraCapture)event.getSource()).removeFromParent(); 075 * } 076 * } 077 * }); 078 * add(camera); //adds to the layout 079 * } 080 * else { 081 * Window.alert("Sorry, your browser doesn't support the camera capture."); 082 * } 083 * </pre></p> 084 * <p> 085 * <h3>Styling:</h3> 086 * <p> 087 * To limit the width of the camera capture widget on mobile devices, you can use {@code max-width: 100%} on the widget. 088 * The browser will take care of the aspect ratio of the video. 089 * </p> 090 * <p> 091 * <h3>Notice:</h3> 092 * <p> 093 * This widget only works on pages served by a secure protocol (HTTPS). For the browser compatibility, 094 * access <a href="http://caniuse.com/#feat=stream">http://caniuse.com/#feat=stream</a> 095 * </p> 096 * 097 * @author gilberto-torrezan 098 * @author kevzlou7979 099 */ 100// @formatter:on 101public class MaterialCameraCapture extends MaterialWidget implements JsLoader, HasCameraCaptureHandlers, HasCameraActions { 102 103 protected int width = 1280; 104 protected int height = 720; 105 protected boolean pauseOnUnload = false; 106 protected CameraFacingMode facingMode = CameraFacingMode.FRONT; 107 private MediaStream mediaStream; 108 private MaterialWidget video = new MaterialWidget(Document.get().createVideoElement()); 109 private MaterialWidget overlayPanel = new MaterialWidget(Document.get().createDivElement()); 110 111 public MaterialCameraCapture() { 112 super(Document.get().createDivElement(), "camera-wrapper"); 113 } 114 115 @Override 116 protected void onLoad() { 117 super.onLoad(); 118 119 setLayoutPosition(Style.Position.RELATIVE); 120 add(video); 121 122 overlayPanel.setLayoutPosition(Style.Position.FIXED); 123 overlayPanel.setTop(0); 124 overlayPanel.setLeft(0); 125 overlayPanel.setBottom(0); 126 overlayPanel.setRight(0); 127 add(overlayPanel); 128 129 load(); 130 } 131 132 @Override 133 public void load() { 134 play(); 135 } 136 137 @Override 138 protected void onUnload() { 139 super.onUnload(); 140 141 unload(); 142 } 143 144 @Override 145 public void unload() { 146 stop(); 147 } 148 149 @Override 150 public void reload() { 151 unload(); 152 load(); 153 } 154 155 @Override 156 public void play() { 157 if (!isSupported()) { 158 onCameraCaptureError("MaterialCameraCapture is not supported in this browser."); 159 return; 160 } 161 162 nativePlay(video.getElement()); 163 } 164 165 @Override 166 public void pause() { 167 VideoElement el = video.getElement().cast(); 168 el.pause(); 169 onCameraCapturePause(); 170 } 171 172 @Override 173 public void stop() { 174 if (pauseOnUnload) { 175 pause(); 176 } 177 178 if (mediaStream != null) { 179 for (MediaStreamTrack track : mediaStream.getTracks()) { 180 track.stop(); 181 } 182 } 183 } 184 185 @Override 186 public String captureToDataURL() { 187 return captureToDataURL("image/png"); 188 } 189 190 @Override 191 public String captureToDataURL(String mimeType) { 192 return nativeCaptureToDataURL(Canvas.createIfSupported().getCanvasElement(), video.getElement(), mimeType); 193 } 194 195 /** 196 * Sets if the camera capture should pause when the widget is unloaded. 197 * The default is <code>true</code>. 198 */ 199 public void setPauseOnUnload(boolean pauseOnUnload) { 200 this.pauseOnUnload = pauseOnUnload; 201 } 202 203 /** 204 * Returns if the camera capture should pause when the widget is unloaded. 205 * The default is <code>true</code>. 206 */ 207 public boolean isPauseOnUnload() { 208 return pauseOnUnload; 209 } 210 211 /** 212 * Native call to the streams API 213 */ 214 protected void nativePlay(Element video) { 215 MediaStream stream = null; 216 if (Navigator.getUserMedia != null) { 217 stream = Navigator.getUserMedia; 218 GWT.log("Uses Default user Media"); 219 } else if (Navigator.webkitGetUserMedia != null) { 220 stream = Navigator.webkitGetUserMedia; 221 GWT.log("Uses Webkit User Media"); 222 } else if (Navigator.mozGetUserMedia != null) { 223 stream = Navigator.mozGetUserMedia; 224 GWT.log("Uses Moz User Media"); 225 } else if (Navigator.msGetUserMedia != null) { 226 stream = Navigator.msGetUserMedia; 227 GWT.log("Uses Microsoft user Media"); 228 } else { 229 GWT.log("No supported media found in your browser"); 230 } 231 232 if (stream != null) { 233 Navigator.getMedia = stream; 234 Constraints constraints = new Constraints(); 235 constraints.audio = false; 236 237 MediaTrackConstraints mediaTrackConstraints = new MediaTrackConstraints(); 238 mediaTrackConstraints.width = width; 239 mediaTrackConstraints.height = height; 240 mediaTrackConstraints.facingMode = facingMode.getName(); 241 constraints.video = mediaTrackConstraints; 242 243 Navigator.mediaDevices.getUserMedia(constraints).then(streamObj -> { 244 mediaStream = (MediaStream) streamObj; 245 if (URL.createObjectURL(mediaStream) != null) { 246 $(video).attr("src", URL.createObjectURL(mediaStream)); 247 } else if (WebkitURL.createObjectURL(mediaStream) != null) { 248 $(video).attr("src", WebkitURL.createObjectURL(mediaStream)); 249 } 250 if (video instanceof VideoElement) { 251 ((VideoElement) video).play(); 252 } 253 onCameraCaptureLoad(); 254 return null; 255 }).catchException(error -> { 256 GWT.log("MaterialCameraCapture: An error occured! " + error); 257 onCameraCaptureError(error.toString()); 258 return null; 259 }); 260 } 261 } 262 263 /** 264 * Native call to capture the frame of the video stream. 265 */ 266 protected String nativeCaptureToDataURL(CanvasElement canvas, Element element, String mimeType) { 267 VideoElement videoElement = (VideoElement) element; 268 int width = videoElement.getVideoWidth(); 269 int height = videoElement.getVideoHeight(); 270 if (Double.isNaN(width) || Double.isNaN(height)) { 271 width = videoElement.getClientWidth(); 272 height = videoElement.getClientHeight(); 273 } 274 canvas.setWidth(width); 275 canvas.setHeight(height); 276 Context2d context = canvas.getContext2d(); 277 context.drawImage(videoElement, 0, 0, width, height); 278 return canvas.toDataUrl(mimeType); 279 } 280 281 /** 282 * Tests if the browser supports the Streams API. This should be called before creating any 283 * MaterialCameraCapture widgets to avoid errors on the browser. 284 * 285 * @return <code>true</code> if the browser supports this widget, <code>false</code> otherwise 286 */ 287 public static boolean isSupported() { 288 return Navigator.webkitGetUserMedia != null 289 || Navigator.getUserMedia != null 290 || Navigator.mozGetUserMedia != null 291 || Navigator.msGetUserMedia != null; 292 } 293 294 public void addOverlay(MaterialWidget overlay) { 295 overlayPanel.add(overlay); 296 } 297 298 public void removeOverlay(MaterialWidget overlay) { 299 overlayPanel.remove(overlay); 300 } 301 302 public void clearOverlays() { 303 overlayPanel.clear(); 304 } 305 306 public MaterialWidget getVideo() { 307 return video; 308 } 309 310 public void setVideo(MaterialWidget video) { 311 this.video = video; 312 } 313 314 /** 315 * Set the resolution of the camera 316 */ 317 public void setResolution(int width, int height) { 318 this.width = width; 319 this.height = height; 320 reload(); 321 } 322 323 /** 324 * Set the facing mode of the camera (Best usecase for Mobile Devices) 325 */ 326 public void setFacingMode(CameraFacingMode facingMode) { 327 this.facingMode = facingMode; 328 reload(); 329 } 330 331 /** 332 * Called by the component when the stream has started. 333 */ 334 protected void onCameraCaptureLoad() { 335 CameraCaptureEvent.fire(this, CaptureStatus.STARTED); 336 } 337 338 /** 339 * Called by the component when the stream has paused. 340 */ 341 protected void onCameraCapturePause() { 342 CameraCaptureEvent.fire(this, CaptureStatus.PAUSED); 343 } 344 345 /** 346 * Called by the component when the stream when an error occurs. 347 */ 348 protected void onCameraCaptureError(String error) { 349 CameraCaptureEvent.fire(this, error); 350 } 351 352 @Override 353 public HandlerRegistration addCameraCaptureHandler(final CameraCaptureHandler handler) { 354 return addHandler(event -> { 355 if (isEnabled()) { 356 handler.onCameraCaptureChange(event); 357 } 358 }, CameraCaptureEvent.getType()); 359 } 360}