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}