001package gwt.material.design.addins.client.camera;
002
003/*
004 * #%L
005 * GwtMaterial
006 * %%
007 * Copyright (C) 2016 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.dom.client.CanvasElement;
025import com.google.gwt.dom.client.Document;
026import com.google.gwt.dom.client.Element;
027import com.google.gwt.dom.client.VideoElement;
028import com.google.gwt.event.shared.HandlerRegistration;
029import gwt.material.design.addins.client.camera.base.HasCameraCaptureHandlers;
030import gwt.material.design.addins.client.camera.events.CameraCaptureEvent;
031import gwt.material.design.addins.client.camera.events.CameraCaptureEvent.CaptureStatus;
032import gwt.material.design.addins.client.camera.events.CameraCaptureHandler;
033import gwt.material.design.client.base.MaterialWidget;
034
035//@formatter:off
036/**
037 * <p>
038 * MaterialCameraCapture is a widget that captures the video stream from the camera, using the
039 * HTML5 {@code MediaDevices.getUserMedia()} (Streams API). This widget can capture images from the video, to allow
040 * the upload to the server of photos from the camera.
041 * </p>
042 *
043 * <h3>XML Namespace Declaration</h3>
044 * <pre>
045 * {@code
046 * xmlns:ma='urn:import:gwt.material.design.addins.client'
047 * }
048 * </pre>
049 *
050 * <h3>UiBinder Usage:</h3>
051 * <p><pre>
052 * {@code
053 * <ma:camera.MaterialCameraCapture ui:field="camera" />
054 * }
055 * </pre></p>
056 *
057 * <h3>Java Usage:</h3>
058 * <p><pre>
059 *  if (MaterialCameraCapture.isSupported()){
060 *      MaterialCameraCapture camera = new MaterialCameraCapture();
061 *      camera.addCameraCaptureHandler(new CameraCaptureHandler() {
062 *          {@literal @}Override
063 *          public void onCameraCaptureChange(CameraCaptureEvent event) {
064 *              if (event.getCaptureStatus() == CaptureStatus.ERRORED){
065 *                  Window.alert("Error on starting the camera capture: " + event.getErrorMessage());
066 *                  ((MaterialCameraCapture)event.getSource()).removeFromParent();
067 *              }
068 *          }
069 *      });
070 *      add(camera); //adds to the layout
071 *  }
072 *  else {
073 *      Window.alert("Sorry, your browser doesn't support the camera capture.");
074 *  }
075 * </pre></p>
076 *
077 * <h3>Styling:</h3>
078 * <p>
079 * To limit the width of the camera capture widget on mobile devices, you can use {@code max-width: 100%} on the widget.
080 * The browser will take care of the aspect ratio of the video.
081 * </p>
082 *
083 * <h3>Notice:</h3>
084 * <p>
085 * This widget only works on pages served by a secure protocol (HTTPS). For the browser compatibility,
086 * access <a href="http://caniuse.com/#feat=stream">http://caniuse.com/#feat=stream</a>
087 * </p>
088 *
089 * @author gilberto-torrezan
090 */
091// @formatter:on
092public class MaterialCameraCapture extends MaterialWidget implements HasCameraCaptureHandlers {
093
094    protected boolean pauseOnUnload = true;
095
096    public MaterialCameraCapture() {
097        super(Document.get().createVideoElement());
098    }
099
100    /**
101     * Captures the current frame of the video to an image data URL. It's the same as calling
102     * {@link #captureToDataURL(String)} using "image/png".
103     *
104     * @return The Data URL of the captured image, which can be used as "src" on an "img" element
105     * or sent to the server
106     */
107    public String captureToDataURL() {
108        return captureToDataURL("image/png");
109    }
110
111    /**
112     * Captures the current frame of the video to an image data URL.
113     *
114     * @param mimeType The type of the output image, such as "image/png" or "image/jpeg".
115     *
116     * @return The Data URL of the captured image, which can be used as "src" on an "img" element
117     * or sent to the server
118     */
119    public String captureToDataURL(String mimeType) {
120        return nativeCaptureToDataURL(Canvas.createIfSupported().getCanvasElement(), getElement(), mimeType);
121    }
122
123    @Override
124    protected void onLoad() {
125        super.onLoad();
126        VideoElement el = getElement().cast();
127        if (el.getSrc() == null || el.isPaused()) {
128            play();
129        }
130    }
131
132    @Override
133    protected void onUnload() {
134        if (pauseOnUnload) {
135            pause();
136        }
137    }
138
139    /**
140     * <p>
141     * Starts the video stream from the camera. This is called when the component is loaded.
142     * Use {@link CameraCaptureHandler}s to be notified when the stream actually starts or if
143     * an error occurs.
144     * </p>
145     * <p>
146     * At this point the user is requested by the browser to allow the application to use the camera.
147     * If the user doesn't allow it, an error is notified to the {@link CameraCaptureHandler}s.
148     * </p>
149     */
150    public void play() {
151        if (!isSupported()) {
152            onCameraCaptureError("MaterialCameraCapture is not supported in this browser.");
153            return;
154        }
155        VideoElement el = getElement().cast();
156        if (el.getSrc() == null) {
157            nativePlay(getElement());
158        } else {
159            el.play();
160        }
161    }
162
163    /**
164     * Restarts the video stream from the camera. 
165     * The user is requested again by the browser to allow the application to use the camera.
166     */
167    public void restart() {
168        if (!isSupported()) {
169            onCameraCaptureError("MaterialCameraCapture is not supported in this browser.");
170            return;
171        }
172        nativePlay(getElement());
173    }
174
175    /**
176     * Pauses the video stream from the camera.
177     */
178    public void pause() {
179        VideoElement el = getElement().cast();
180        el.pause();
181        onCameraCapturePause();
182    }
183
184    /**
185     * Sets if the camera capture should pause when the widget is unloaded.
186     * The default is <code>true</code>. 
187     */
188    public void setPauseOnUnload(boolean pauseOnUnload) {
189        this.pauseOnUnload = pauseOnUnload;
190    }
191
192    /**
193     * Returns if the camera capture should pause when the widget is unloaded.
194     * The default is <code>true</code>.
195     */
196    public boolean isPauseOnUnload() {
197        return pauseOnUnload;
198    }
199
200    /**
201     * Native call to the streams API
202     */
203    protected native void nativePlay(Element video)/*-{
204        var navigator = $wnd.navigator;
205        var widget = this;
206
207        var onSuccess = function(stream) {
208            var vendorURL = $wnd.URL || $wnd.webkitURL;
209            video.src = vendorURL.createObjectURL(stream);
210            video.play();
211            widget.@gwt.material.design.addins.client.camera.MaterialCameraCapture::onCameraCaptureLoad()();
212        };
213
214        var onFailure = function(err) {
215            console.log("MaterialCameraCapture: An error occured! " + err);
216            widget.@gwt.material.design.addins.client.camera.MaterialCameraCapture::onCameraCaptureError(Ljava/lang/String;)(err.message);
217        };
218
219        //using premisses
220        if (navigator.mediaDevices.getUserMedia){
221            var p = navigator.mediaDevices.getUserMedia({video: true, audio: false});
222            p.then(onSuccess);
223            //workaround for the catch keyword. See https://groups.google.com/forum/#!topic/google-web-toolkit/t1KGh-7KL-k
224            p["catch"](onFailure);
225        }
226        else {
227            navigator.getMedia = (navigator.getUserMedia ||
228            navigator.webkitGetUserMedia ||
229            navigator.mozGetUserMedia ||
230            navigator.msGetUserMedia);
231            navigator.getMedia({video: true, audio: false}, onSuccess, onFailure);
232        }
233    }-*/;
234
235    /**
236     * Native call to capture the frame of the video stream.
237     */
238    protected native String nativeCaptureToDataURL(CanvasElement canvas, Element video, String mimeType)/*-{
239        var width = video.videoWidth;
240        var height = video.videoHeight;
241
242        if (isNaN(width) || isNaN(height)) {
243            width = video.clientWidth;
244            height = video.clientHeight;
245        }
246
247        canvas.width = width;
248        canvas.height = height;
249
250        var context = canvas.getContext('2d');
251        context.drawImage(video, 0, 0, width, height);
252
253        var data = canvas.toDataURL(mimeType);
254        return data;
255    }-*/;
256
257    /**
258     * Tests if the browser supports the Streams API. This should be called before creating any
259     * MaterialCameraCapture widgets to avoid errors on the browser.
260     *
261     * @return <code>true</code> if the browser supports this widget, <code>false</code> otherwise 
262     */
263    public static native boolean isSupported()/*-{
264        return !!(navigator.mediaDevices.getUserMedia ||
265        navigator.getUserMedia ||
266        navigator.webkitGetUserMedia ||
267        navigator.mozGetUserMedia ||
268        navigator.msGetUserMedia);
269    }-*/;
270
271    /**
272     * Called by the component when the stream has started.
273     */
274    protected void onCameraCaptureLoad() {
275        CameraCaptureEvent.fire(this, CaptureStatus.STARTED);
276    }
277
278    /**
279     * Called  by the component when the stream has paused.
280     */
281    protected void onCameraCapturePause() {
282        CameraCaptureEvent.fire(this, CaptureStatus.PAUSED);
283    }
284
285    /**
286     *  Called by the component when the stream when an error occurs.
287     */
288    protected void onCameraCaptureError(String error) {
289        CameraCaptureEvent.fire(this, error);
290    }
291
292    @Override
293    public HandlerRegistration addCameraCaptureHandler(final CameraCaptureHandler handler) {
294        return addHandler(new CameraCaptureHandler() {
295            @Override
296            public void onCameraCaptureChange(CameraCaptureEvent event) {
297                if(isEnabled()){
298                    handler.onCameraCaptureChange(event);
299                }
300            }
301        }, CameraCaptureEvent.getType());
302    }
303
304}