diff --git a/vcamera/pom.xml b/vcamera/pom.xml index b08235d..dec9477 100755 --- a/vcamera/pom.xml +++ b/vcamera/pom.xml @@ -4,7 +4,7 @@ org.vaadin.addons.parttio vcamera - 4.0.1-SNAPSHOT + 4.1.0-SNAPSHOT Vcamera Integration of app-media for Vaadin 14+ @@ -89,6 +89,12 @@ 3.8.1 test + + com.fasterxml.jackson.core + jackson-core + 2.18.2 + + diff --git a/vcamera/src/main/java/org/vaadin/vcamera/VCamera.java b/vcamera/src/main/java/org/vaadin/vcamera/VCamera.java index f0d8ab9..55cb03f 100755 --- a/vcamera/src/main/java/org/vaadin/vcamera/VCamera.java +++ b/vcamera/src/main/java/org/vaadin/vcamera/VCamera.java @@ -1,23 +1,36 @@ package org.vaadin.vcamera; import java.io.OutputStream; +import java.util.List; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; import com.vaadin.flow.component.Component; import com.vaadin.flow.component.ComponentEventListener; +import com.vaadin.flow.component.HasSize; import com.vaadin.flow.component.Tag; +import com.vaadin.flow.component.page.PendingJavaScriptResult; import com.vaadin.flow.server.StreamReceiver; import com.vaadin.flow.server.StreamVariable; import com.vaadin.flow.shared.Registration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.vaadin.vcamera.mediadevices.Device; +import org.vaadin.vcamera.mediadevices.DevicesListedEvent; /** * A special video element that streams content from browser camera. *

During streaming, users can record still or video clips of the stream, that browser send to the server. The data can be accessed using the DataReceiver interface, see {@link #setReceiver(DataReceiver)}.

*/ @Tag("video") -public class VCamera extends Component { +public class VCamera extends Component implements HasSize { private boolean cameraOn; private boolean recording; + private boolean flashOn; + + private static final Logger LOG = LoggerFactory.getLogger(VCamera.class); public VCamera() { getElement().setProperty("volume", 0); @@ -49,7 +62,7 @@ public void startRecording() { }).then(response => console.log(response)); } this.recorder.start(); - """); + """); } public void stopRecording() { @@ -110,9 +123,99 @@ public void openCamera(String optionsJson) { }); } } - """.formatted(optionsJson)); + """.formatted(optionsJson)); + } + + public boolean isFlashOn() { + return flashOn; + } + + public void toggleFlashlight() { + flashOn = !flashOn; + getElement().executeJs(""" + if(this.stream != null) { + const track = this.stream.getVideoTracks()[0]; + + //Create image capture object and get camera capabilities + const imageCapture = new ImageCapture(track) + const photoCapabilities = imageCapture.getPhotoCapabilities().then(() => { + track.applyConstraints({ + advanced: [{torch: %s}] + }); + }); + } + """.formatted(flashOn)); } + /** + * Opens a specific videoinput device. + * @param device device to open + * @throws IllegalArgumentException if device is not of kind "videoinput" + */ + public void openCamera(Device device) { + String sourceId = device.getDeviceId(); + if(device.getKind() != Device.DeviceKind.videoinput) { + throw new IllegalArgumentException("Device "+device+" is not a video device"); + } + openCamera(""" + { video: { + optional: [{sourceId: "%s"}] + }} + """.formatted(sourceId)); + } + + /** + * Reads available devices on the users browser + * @param listener is called with a list of devices after Javascript execution completes + */ + public void listDevices(ComponentEventListener listener) { + PendingJavaScriptResult pendingJavaScriptResult = getElement().executeJs(""" + if (!navigator.mediaDevices || !navigator.mediaDevices.enumerateDevices) { + console.log("enumerateDevices() not supported."); + return ""; + } + + // List cameras and microphones + return new Promise(function(myResolve, myReject){ + navigator.mediaDevices.enumerateDevices() + .then(function(devices) { + devicesString = JSON.stringify(devices); + myResolve(devicesString); // when successful + myReject(""); // when error + } ) + }); + """); + + pendingJavaScriptResult.then(String.class, (s) -> { + ObjectMapper objectMapper = new ObjectMapper(); + try { + List devices = objectMapper.readValue(s, new TypeReference<>() { + }); + DevicesListedEvent event = new DevicesListedEvent(this, false, devices); + listener.onComponentEvent(event); + } catch (JsonProcessingException e) { + LOG.error("Could not read devices"); + listener.onComponentEvent(new DevicesListedEvent(this, false, List.of())); + } + }); + } + + /** + * Tries to open the "normal", back-facing camera on a mobile phone. While it is possible to set a constraint facingMode = environment, + * it is up to the device, which camera to use. E.g. there could be a fishEye camera which is then used, which is rarely wanted. + * A reddit post mentions, that the most reliable way + * was to just use the last enumerated device as returned by {@link #listDevices(ComponentEventListener)} + */ + public void openNormalEnvironmentCamera() { + listDevices(e -> { + List videoDevices = e.getDevices().stream() + .filter(device -> device.getKind() == Device.DeviceKind.videoinput) + .toList(); + openCamera(videoDevices.get(videoDevices.size() - 1)); + }); + } + + public boolean isCameraOpen() { return cameraOn; } diff --git a/vcamera/src/main/java/org/vaadin/vcamera/mediadevices/Device.java b/vcamera/src/main/java/org/vaadin/vcamera/mediadevices/Device.java new file mode 100644 index 0000000..6ff31a1 --- /dev/null +++ b/vcamera/src/main/java/org/vaadin/vcamera/mediadevices/Device.java @@ -0,0 +1,56 @@ +package org.vaadin.vcamera.mediadevices; + +/** + * A device as returned by Javascripts MediaDevices: enumerateDevices + */ +public class Device { + + public enum DeviceKind { + videoinput, audioinput, audiooutput + } + + private String deviceId, label, groupId; + private DeviceKind kind; + + public DeviceKind getKind() { + return kind; + } + + public void setKind(DeviceKind kind) { + this.kind = kind; + } + + public String getDeviceId() { + return deviceId; + } + + public void setDeviceId(String deviceId) { + this.deviceId = deviceId; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + public String getGroupId() { + return groupId; + } + + public void setGroupId(String groupId) { + this.groupId = groupId; + } + + @Override + public String toString() { + return "Device{" + + "deviceId='" + deviceId + '\'' + + ", label='" + label + '\'' + + ", groupId='" + groupId + '\'' + + ", kind=" + kind + + '}'; + } +} diff --git a/vcamera/src/main/java/org/vaadin/vcamera/mediadevices/DevicesListedEvent.java b/vcamera/src/main/java/org/vaadin/vcamera/mediadevices/DevicesListedEvent.java new file mode 100644 index 0000000..47ba79a --- /dev/null +++ b/vcamera/src/main/java/org/vaadin/vcamera/mediadevices/DevicesListedEvent.java @@ -0,0 +1,20 @@ +package org.vaadin.vcamera.mediadevices; + +import com.vaadin.flow.component.ComponentEvent; +import org.vaadin.vcamera.VCamera; + +import java.util.List; + +public class DevicesListedEvent extends ComponentEvent { + + private final List devices; + + public DevicesListedEvent(VCamera source, boolean fromClient, List devices) { + super(source, fromClient); + this.devices = devices; + } + + public List getDevices() { + return devices; + } +}