Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion vcamera/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>org.vaadin.addons.parttio</groupId>
<artifactId>vcamera</artifactId>
<version>4.0.1-SNAPSHOT</version>
<version>4.1.0-SNAPSHOT</version>
<name>Vcamera</name>
<description>Integration of app-media for Vaadin 14+</description>

Expand Down Expand Up @@ -89,6 +89,12 @@
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.18.2</version>
</dependency>


</dependencies>

Expand Down
109 changes: 106 additions & 3 deletions vcamera/src/main/java/org/vaadin/vcamera/VCamera.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>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)}.</p>
*/
@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);
Expand Down Expand Up @@ -49,7 +62,7 @@ public void startRecording() {
}).then(response => console.log(response));
}
this.recorder.start();
""");
""");
}

public void stopRecording() {
Expand Down Expand Up @@ -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<DevicesListedEvent> 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<Device> 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 href="https://www.reddit.com/r/javascript/comments/8eg8w5/comment/dxvqycs/">A reddit post</a> 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<Device> videoDevices = e.getDevices().stream()
.filter(device -> device.getKind() == Device.DeviceKind.videoinput)
.toList();
openCamera(videoDevices.get(videoDevices.size() - 1));
});
}


public boolean isCameraOpen() {
return cameraOn;
}
Expand Down
56 changes: 56 additions & 0 deletions vcamera/src/main/java/org/vaadin/vcamera/mediadevices/Device.java
Original file line number Diff line number Diff line change
@@ -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 +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -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<VCamera> {

private final List<Device> devices;

public DevicesListedEvent(VCamera source, boolean fromClient, List<Device> devices) {
super(source, fromClient);
this.devices = devices;
}

public List<Device> getDevices() {
return devices;
}
}