|
| 1 | +# Camera Video Track |
| 2 | + |
| 3 | +This guide focuses on setting up a peer connection with camera video capture capabilities, which allows you to capture and stream video from connected cameras in your WebRTC connection. |
| 4 | + |
| 5 | +## Camera Video Source Selection |
| 6 | + |
| 7 | +To enable camera video capture, you need to: |
| 8 | + |
| 9 | +1. List available video devices (cameras) |
| 10 | +2. Create and configure a VideoDeviceSource |
| 11 | +3. Create a video track with the camera source |
| 12 | +4. Add the track to your peer connection |
| 13 | + |
| 14 | +### Getting Available Cameras and Capabilities |
| 15 | + |
| 16 | +Before configuring your camera source, you may want to check the supported resolutions and frame rates: |
| 17 | + |
| 18 | +```java |
| 19 | +import dev.onvoid.webrtc.media.MediaDevices; |
| 20 | +import dev.onvoid.webrtc.media.video.VideoDevice; |
| 21 | +import dev.onvoid.webrtc.media.video.VideoCaptureCapability; |
| 22 | +import java.util.List; |
| 23 | + |
| 24 | +// Get all cameras |
| 25 | +List<VideoDevice> cameras = MediaDevices.getVideoCaptureDevices(); |
| 26 | +if (cameras.isEmpty()) { |
| 27 | + System.out.println("No cameras found"); |
| 28 | + return; |
| 29 | +} |
| 30 | + |
| 31 | +// Get the first camera |
| 32 | +VideoDevice camera = cameras.get(0); |
| 33 | +System.out.println("Camera: " + camera.getName()); |
| 34 | + |
| 35 | +// Get camera capabilities |
| 36 | +List<VideoCaptureCapability> capabilities = MediaDevices.getVideoCaptureCapabilities(camera); |
| 37 | + |
| 38 | +// Print camera capabilities |
| 39 | +for (VideoCaptureCapability capability : capabilities) { |
| 40 | + System.out.println(" Resolution: " + capability.width + "x" + capability.height); |
| 41 | + System.out.println(" Frame Rate: " + capability.frameRate + " fps"); |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +### Creating and Configuring a VideoDeviceSource |
| 46 | + |
| 47 | +The `VideoDeviceSource` class allows you to capture video from a camera: |
| 48 | + |
| 49 | +```java |
| 50 | +import dev.onvoid.webrtc.media.video.VideoDeviceSource; |
| 51 | +import dev.onvoid.webrtc.media.video.VideoDevice; |
| 52 | +import dev.onvoid.webrtc.media.video.VideoCaptureCapability; |
| 53 | + |
| 54 | +// Create a video device source |
| 55 | +VideoDeviceSource videoSource = new VideoDeviceSource(); |
| 56 | + |
| 57 | +// Select a specific camera to capture from |
| 58 | +VideoDevice camera = cameras.get(0); // Use the first available camera |
| 59 | +videoSource.setVideoCaptureDevice(camera); |
| 60 | + |
| 61 | +// Configure the capture capability (optional) |
| 62 | +// Choose a capability from the list obtained earlier |
| 63 | +VideoCaptureCapability capability = capabilities.get(0); // Use the first available capability |
| 64 | +videoSource.setVideoCaptureCapability(capability); |
| 65 | + |
| 66 | +// Start capturing |
| 67 | +videoSource.start(); |
| 68 | +``` |
| 69 | + |
| 70 | +### Creating a Video Track with the Camera Source |
| 71 | + |
| 72 | +Once you have configured your camera video source, you can create a video track: |
| 73 | + |
| 74 | +```java |
| 75 | +import dev.onvoid.webrtc.PeerConnectionFactory; |
| 76 | +import dev.onvoid.webrtc.media.video.VideoTrack; |
| 77 | + |
| 78 | +// Create a PeerConnectionFactory |
| 79 | +PeerConnectionFactory factory = new PeerConnectionFactory(); |
| 80 | + |
| 81 | +// Create a video track with the camera source |
| 82 | +VideoTrack videoTrack = factory.createVideoTrack("video0", videoSource); |
| 83 | +``` |
| 84 | + |
| 85 | +### Adding the Track to a Peer Connection |
| 86 | + |
| 87 | +Add the video track to your peer connection: |
| 88 | + |
| 89 | +```java |
| 90 | +import java.util.ArrayList; |
| 91 | +import java.util.List; |
| 92 | +import dev.onvoid.webrtc.RTCPeerConnection; |
| 93 | + |
| 94 | +// Assuming you already have a configured RTCPeerConnection |
| 95 | +RTCPeerConnection peerConnection = factory.createPeerConnection(config, observer); |
| 96 | + |
| 97 | +// Add the track to the peer connection |
| 98 | +List<String> streamIds = new ArrayList<>(); |
| 99 | +streamIds.add("stream1"); |
| 100 | +peerConnection.addTrack(videoTrack, streamIds); |
| 101 | +``` |
| 102 | + |
| 103 | +## Additional Features |
| 104 | + |
| 105 | +The `VideoDeviceSource` provides additional methods for controlling the camera capture: |
| 106 | + |
| 107 | +### Resource Management |
| 108 | + |
| 109 | +Always properly dispose of resources when done: |
| 110 | + |
| 111 | +```java |
| 112 | +// Dispose of resources when done |
| 113 | +videoSource.stop(); |
| 114 | +videoSource.dispose(); |
| 115 | +``` |
| 116 | + |
| 117 | +### Handling Device Changes |
| 118 | + |
| 119 | +If cameras might change during your application's lifecycle (e.g., cameras connecting or disconnecting), you should implement a device change listener: |
| 120 | + |
| 121 | +```java |
| 122 | +import dev.onvoid.webrtc.media.Device; |
| 123 | +import dev.onvoid.webrtc.media.DeviceChangeListener; |
| 124 | +import dev.onvoid.webrtc.media.MediaDevices; |
| 125 | +import dev.onvoid.webrtc.media.video.VideoDevice; |
| 126 | + |
| 127 | +// Create a device change listener |
| 128 | +DeviceChangeListener listener = new DeviceChangeListener() { |
| 129 | + @Override |
| 130 | + public void deviceConnected(Device device) { |
| 131 | + if (device instanceof VideoDevice) { |
| 132 | + System.out.println("Camera connected: " + device.getName()); |
| 133 | + // You might want to update your UI or offer the new camera as an option |
| 134 | + } |
| 135 | + } |
| 136 | + |
| 137 | + @Override |
| 138 | + public void deviceDisconnected(Device device) { |
| 139 | + if (device instanceof VideoDevice) { |
| 140 | + System.out.println("Camera disconnected: " + device.getName()); |
| 141 | + // Handle the case where the current camera was disconnected |
| 142 | + } |
| 143 | + } |
| 144 | +}; |
| 145 | + |
| 146 | +// Register the listener |
| 147 | +MediaDevices.addDeviceChangeListener(listener); |
| 148 | + |
| 149 | +// ... later, when you're done listening for events |
| 150 | +// Unregister the listener |
| 151 | +MediaDevices.removeDeviceChangeListener(listener); |
| 152 | +``` |
| 153 | + |
| 154 | +## Receiving Video Frames |
| 155 | + |
| 156 | +Once you have set up your camera video source and peer connection, you'll likely want to receive and process the video frames. This section explains how to receive both local and remote video frames. |
| 157 | + |
| 158 | +### Creating a VideoTrackSink |
| 159 | + |
| 160 | +To receive video frames, you need to implement the `VideoTrackSink` interface and add it to a video track: |
| 161 | + |
| 162 | +```java |
| 163 | +import dev.onvoid.webrtc.media.video.VideoFrame; |
| 164 | +import dev.onvoid.webrtc.media.video.VideoTrackSink; |
| 165 | + |
| 166 | +// Create a class that implements VideoTrackSink |
| 167 | +public class MyVideoSink implements VideoTrackSink { |
| 168 | + @Override |
| 169 | + public void onVideoFrame(VideoFrame frame) { |
| 170 | + // Process the video frame |
| 171 | + System.out.printf("Received frame: %dx%d%n", |
| 172 | + frame.buffer.getWidth(), frame.buffer.getHeight()); |
| 173 | + |
| 174 | + // IMPORTANT: Always release the frame when done to prevent memory leaks |
| 175 | + frame.release(); |
| 176 | + } |
| 177 | +} |
| 178 | +``` |
| 179 | + |
| 180 | +### Receiving Local Video Frames |
| 181 | + |
| 182 | +To receive frames from your local camera: |
| 183 | + |
| 184 | +```java |
| 185 | +// Create your video sink |
| 186 | +MyVideoSink localVideoSink = new MyVideoSink(); |
| 187 | + |
| 188 | +// Add the sink to your local video track |
| 189 | +videoTrack.addSink(localVideoSink); |
| 190 | + |
| 191 | +// Later, when you're done, remove the sink |
| 192 | +videoTrack.removeSink(localVideoSink); |
| 193 | +``` |
| 194 | + |
| 195 | +### Receiving Remote Video Frames |
| 196 | + |
| 197 | +To receive frames from a remote peer, you need to add a sink to the remote video track when it's received. This is typically done in the `onTrack` method of your `PeerConnectionObserver`: |
| 198 | + |
| 199 | +```java |
| 200 | +import dev.onvoid.webrtc.PeerConnectionObserver; |
| 201 | +import dev.onvoid.webrtc.RTCRtpTransceiver; |
| 202 | +import dev.onvoid.webrtc.media.MediaStreamTrack; |
| 203 | +import dev.onvoid.webrtc.media.video.VideoTrack; |
| 204 | + |
| 205 | +public class MyPeerConnectionObserver implements PeerConnectionObserver { |
| 206 | + private final MyVideoSink remoteVideoSink = new MyVideoSink(); |
| 207 | + |
| 208 | + // Other methods omitted for brevity... |
| 209 | + |
| 210 | + @Override |
| 211 | + public void onTrack(RTCRtpTransceiver transceiver) { |
| 212 | + MediaStreamTrack track = transceiver.getReceiver().getTrack(); |
| 213 | + String kind = track.getKind(); |
| 214 | + |
| 215 | + if (kind.equals(MediaStreamTrack.VIDEO_TRACK_KIND)) { |
| 216 | + VideoTrack videoTrack = (VideoTrack) track; |
| 217 | + videoTrack.addSink(remoteVideoSink); |
| 218 | + System.out.println("Added sink to remote video track"); |
| 219 | + } |
| 220 | + } |
| 221 | + |
| 222 | + // Make sure to clean up when done |
| 223 | + public void dispose() { |
| 224 | + // Remove sinks from any tracks they were added to |
| 225 | + } |
| 226 | +} |
| 227 | +``` |
| 228 | + |
| 229 | +### Processing Video Frames |
| 230 | + |
| 231 | +When processing video frames, consider these important points: |
| 232 | + |
| 233 | +1. **Frame Properties**: Each `VideoFrame` contains: |
| 234 | + - A `VideoFrameBuffer` with the actual pixel data |
| 235 | + - Width and height information |
| 236 | + - Rotation value |
| 237 | + - Timestamp |
| 238 | + |
| 239 | +2. **Memory Management**: Always call `frame.release()` when you're done with a frame to prevent memory leaks. |
| 240 | + |
| 241 | +3. **Performance Considerations**: Frame processing happens on the WebRTC thread, so keep your processing efficient. For heavy processing, consider: |
| 242 | + - Copying the frame data and processing it on a separate thread |
| 243 | + - Using a frame queue with a dedicated processing thread |
| 244 | + - Skipping frames if processing can't keep up with the frame rate |
| 245 | + |
| 246 | +#### Converting VideoFrame to BufferedImage |
| 247 | + |
| 248 | +To display or process video frames in Java applications, you often need to convert the `VideoFrame` to a `BufferedImage`. Here's how to do it: |
| 249 | + |
| 250 | +```java |
| 251 | +import dev.onvoid.webrtc.media.FourCC; |
| 252 | +import dev.onvoid.webrtc.media.video.VideoBufferConverter; |
| 253 | +import dev.onvoid.webrtc.media.video.VideoFrame; |
| 254 | +import dev.onvoid.webrtc.media.video.VideoFrameBuffer; |
| 255 | +import java.awt.image.BufferedImage; |
| 256 | +import java.awt.image.DataBufferByte; |
| 257 | + |
| 258 | +public void onVideoFrame(VideoFrame frame) { |
| 259 | + try { |
| 260 | + // Get frame dimensions |
| 261 | + VideoFrameBuffer frameBuffer = frame.buffer; |
| 262 | + int frameWidth = frameBuffer.getWidth(); |
| 263 | + int frameHeight = frameBuffer.getHeight(); |
| 264 | + |
| 265 | + // Create a BufferedImage with ABGR format (compatible with RGBA conversion) |
| 266 | + BufferedImage image = new BufferedImage(frameWidth, frameHeight, BufferedImage.TYPE_4BYTE_ABGR); |
| 267 | + |
| 268 | + // Get the underlying byte array from the BufferedImage |
| 269 | + byte[] imageBuffer = ((DataBufferByte) image.getRaster().getDataBuffer()).getData(); |
| 270 | + |
| 271 | + // Convert the frame buffer from I420 format to RGBA format |
| 272 | + VideoBufferConverter.convertFromI420(frameBuffer, imageBuffer, FourCC.RGBA); |
| 273 | + |
| 274 | + // Now you can use the BufferedImage for display or further processing |
| 275 | + // For example, you could display it in a Swing component: |
| 276 | + // myJLabel.setIcon(new ImageIcon(image)); |
| 277 | + |
| 278 | + } catch (Exception e) { |
| 279 | + e.printStackTrace(); |
| 280 | + } finally { |
| 281 | + // Always release the frame when done |
| 282 | + frame.release(); |
| 283 | + } |
| 284 | +} |
| 285 | +``` |
| 286 | + |
| 287 | +This conversion process works as follows: |
| 288 | + |
| 289 | +1. Create a `BufferedImage` with the same dimensions as the video frame, using `BufferedImage.TYPE_4BYTE_ABGR` format which is compatible with the RGBA format we'll convert to. |
| 290 | + |
| 291 | +2. Get the underlying byte array from the BufferedImage using `((DataBufferByte) image.getRaster().getDataBuffer()).getData()`. |
| 292 | + |
| 293 | +3. Use `VideoBufferConverter.convertFromI420()` to convert the frame buffer from I420 format (which is the internal format used by WebRTC) to RGBA format, storing the result directly in the BufferedImage's byte array. |
| 294 | + |
| 295 | +4. The resulting BufferedImage can be used for display in Swing/JavaFX components or for further image processing. |
| 296 | + |
| 297 | +#### Scaling Video Frames |
| 298 | + |
| 299 | +Sometimes you may need to resize video frames to a different resolution, either to reduce processing requirements or to fit a specific display area. The `VideoFrameBuffer` interface provides a `cropAndScale` method that can be used for both cropping and scaling operations: |
| 300 | + |
| 301 | +```java |
| 302 | +import dev.onvoid.webrtc.media.video.VideoFrame; |
| 303 | +import dev.onvoid.webrtc.media.video.VideoFrameBuffer; |
| 304 | +import dev.onvoid.webrtc.media.video.VideoTrackSink; |
| 305 | + |
| 306 | +public class ScalingVideoSink implements VideoTrackSink { |
| 307 | + private final int targetWidth; |
| 308 | + private final int targetHeight; |
| 309 | + |
| 310 | + public ScalingVideoSink(int targetWidth, int targetHeight) { |
| 311 | + this.targetWidth = targetWidth; |
| 312 | + this.targetHeight = targetHeight; |
| 313 | + } |
| 314 | + |
| 315 | + @Override |
| 316 | + public void onVideoFrame(VideoFrame frame) { |
| 317 | + try { |
| 318 | + // Get the original frame buffer |
| 319 | + VideoFrameBuffer originalBuffer = frame.buffer; |
| 320 | + int originalWidth = originalBuffer.getWidth(); |
| 321 | + int originalHeight = originalBuffer.getHeight(); |
| 322 | + |
| 323 | + // Scale the frame to the target resolution |
| 324 | + // For scaling only (no cropping), use the original dimensions for the crop region |
| 325 | + VideoFrameBuffer scaledBuffer = originalBuffer.cropAndScale( |
| 326 | + 0, 0, // No cropping from top-left (cropX, cropY) |
| 327 | + originalWidth, originalHeight, // Use full frame width and height |
| 328 | + targetWidth, targetHeight // Scale to target dimensions |
| 329 | + ); |
| 330 | + |
| 331 | + // Create a new frame with the scaled buffer |
| 332 | + VideoFrame scaledFrame = new VideoFrame(scaledBuffer, frame.rotation, frame.timestampNs); |
| 333 | + |
| 334 | + // Process the scaled frame |
| 335 | + System.out.printf("Scaled frame from %dx%d to %dx%d%n", |
| 336 | + originalWidth, originalHeight, scaledBuffer.getWidth(), scaledBuffer.getHeight()); |
| 337 | + |
| 338 | + // Don't forget to release both frames when done |
| 339 | + scaledFrame.release(); |
| 340 | + } finally { |
| 341 | + // Always release the original frame |
| 342 | + frame.release(); |
| 343 | + } |
| 344 | + } |
| 345 | +} |
| 346 | +``` |
| 347 | + |
| 348 | +This scaling process works as follows: |
| 349 | + |
| 350 | +1. Get the original frame buffer and its dimensions. |
| 351 | + |
| 352 | +2. Call `cropAndScale` on the original buffer with parameters: |
| 353 | + - `cropX = 0`, `cropY = 0`: Start from the top-left corner (no cropping) |
| 354 | + - `cropWidth = originalWidth`, `cropHeight = originalHeight`: Use the full frame (no cropping) |
| 355 | + - `scaleWidth = targetWidth`, `scaleHeight = targetHeight`: Scale to the desired dimensions |
| 356 | + |
| 357 | +3. Create a new `VideoFrame` with the scaled buffer, preserving the original rotation and timestamp. |
| 358 | + |
| 359 | +4. Process the scaled frame as needed. |
| 360 | + |
| 361 | +5. Release both the scaled frame and the original frame to prevent memory leaks. |
| 362 | + |
| 363 | +Note that scaling is a computationally expensive operation, especially for high-resolution frames. Consider the performance implications when choosing target dimensions and how frequently you scale frames. |
| 364 | + |
| 365 | +## Best Practices |
| 366 | + |
| 367 | +When implementing camera capture in your application, consider these best practices: |
| 368 | + |
| 369 | +1. **Check for available cameras**: Always check if cameras are available before trying to use them. |
| 370 | +2. **Handle no cameras gracefully**: Provide a fallback mechanism if no cameras are available. |
| 371 | +3. **Let users select cameras**: If multiple cameras are available, let users choose which one to use. |
| 372 | +4. **Select appropriate resolution**: Choose a resolution that balances quality and performance. |
| 373 | +5. **Properly manage resources**: Always stop and dispose of video sources when they're no longer needed. |
| 374 | +6. **Handle device changes**: Implement device change listeners to handle cameras being connected or disconnected. |
| 375 | +7. **Consider privacy concerns**: In some applications, you may need to request permission to use the camera. |
| 376 | + |
| 377 | +--- |
| 378 | + |
| 379 | +## Conclusion |
| 380 | + |
| 381 | +This guide has demonstrated how to set up a WebRTC peer connection with camera video capture capabilities and how to receive and process both local and remote video frames. |
| 382 | + |
| 383 | +Camera capture is essential for video conferencing applications, video chat, remote interviews, and any other applications where face-to-face communication is important. |
0 commit comments