Skip to content

Commit 4126ea0

Browse files
committed
chore: add custom video source and generator to the web example
1 parent 05d0c5b commit 4126ea0

File tree

4 files changed

+283
-2
lines changed

4 files changed

+283
-2
lines changed

webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/web/WebClientExample.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public WebClientExample(URI signalingUri, String subprotocol) {
7676
peerConnectionManager = new PeerConnectionManager();
7777

7878
mediaManager = new MediaManager();
79-
mediaManager.createTracks(peerConnectionManager, true);
79+
mediaManager.createTracks(peerConnectionManager, true, true);
8080

8181
setupCallbacks();
8282

webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/web/media/AudioGenerator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ public void start() {
100100
byte[] audioData = new byte[DEFAULT_FRAME_COUNT * DEFAULT_CHANNELS * bytesPerSample];
101101

102102
// Generate a pleasant sine wave at 440 Hz (A4 note).
103-
double amplitude = 0.2; // 30% of maximum to avoid being too loud
103+
double amplitude = 0.2; // 30% of the maximum to avoid being too loud
104104
double frequency = 440.0; // A4 note (440 Hz)
105105
double radiansPerSample = 2.0 * Math.PI * frequency / DEFAULT_SAMPLE_RATE;
106106

webrtc-examples/src/main/java/dev/onvoid/webrtc/examples/web/media/MediaManager.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import dev.onvoid.webrtc.media.audio.AudioTrack;
2323
import dev.onvoid.webrtc.media.audio.AudioTrackSink;
2424
import dev.onvoid.webrtc.media.audio.CustomAudioSource;
25+
import dev.onvoid.webrtc.media.video.CustomVideoSource;
2526
import dev.onvoid.webrtc.media.video.VideoDeviceSource;
2627
import dev.onvoid.webrtc.media.video.VideoTrack;
2728
import dev.onvoid.webrtc.media.video.VideoTrackSink;
@@ -52,6 +53,12 @@ public class MediaManager {
5253
/** The audio generator responsible for creating and pushing audio frames to the custom audio source. */
5354
private AudioGenerator audioGenerator;
5455

56+
/** The custom video source for generating video frames. */
57+
private CustomVideoSource customVideoSource;
58+
59+
/** The video generator responsible for creating and pushing video frames to the custom video source. */
60+
private VideoGenerator videoGenerator;
61+
5562

5663
/**
5764
* Creates a new MediaManager.
@@ -99,6 +106,35 @@ public void createTracks(PeerConnectionManager peerConnectionManager, boolean us
99106
peerConnectionManager.addTrack(getVideoTrack(), streamIds);
100107
}
101108

109+
/**
110+
* Creates and initializes tracks with options for both custom audio and video sources.
111+
*
112+
* @param peerConnectionManager The peer connection manager to use for creating tracks.
113+
* @param useCustomAudio Whether to use a custom audio source that can push frames.
114+
* @param useCustomVideo Whether to use a custom video source that can push frames.
115+
*/
116+
public void createTracks(PeerConnectionManager peerConnectionManager, boolean useCustomAudio, boolean useCustomVideo) {
117+
if (useCustomAudio) {
118+
createCustomAudioTrack(peerConnectionManager);
119+
}
120+
else {
121+
createAudioTrack(peerConnectionManager);
122+
}
123+
124+
if (useCustomVideo) {
125+
createCustomVideoTrack(peerConnectionManager);
126+
}
127+
else {
128+
createVideoTrack(peerConnectionManager);
129+
}
130+
131+
// Add tracks to the peer connection.
132+
List<String> streamIds = List.of("stream0");
133+
134+
peerConnectionManager.addTrack(getAudioTrack(), streamIds);
135+
peerConnectionManager.addTrack(getVideoTrack(), streamIds);
136+
}
137+
102138
/**
103139
* Creates an audio track with default options.
104140
*/
@@ -143,6 +179,24 @@ private void createVideoTrack(PeerConnectionManager peerConnectionManager) {
143179
LOG.info("Video track created");
144180
}
145181

182+
/**
183+
* Creates a video track with a custom video source that can push video frames.
184+
* Also starts the video generator thread that pushes video frames at regular intervals.
185+
*
186+
* @param peerConnectionManager The peer connection manager to use for creating the track.
187+
*/
188+
public void createCustomVideoTrack(PeerConnectionManager peerConnectionManager) {
189+
customVideoSource = new CustomVideoSource();
190+
191+
videoTrack = peerConnectionManager.createVideoTrack(customVideoSource, "video0");
192+
193+
// Start the video generator.
194+
videoGenerator = new VideoGenerator(customVideoSource);
195+
videoGenerator.start();
196+
197+
LOG.info("Custom video track created with video generator");
198+
}
199+
146200
/**
147201
* Gets the audio track.
148202
*
@@ -197,6 +251,12 @@ public void dispose() {
197251
audioGenerator = null;
198252
}
199253

254+
// Stop the video generator if it's running.
255+
if (videoGenerator != null) {
256+
videoGenerator.stop();
257+
videoGenerator = null;
258+
}
259+
200260
if (audioTrack != null) {
201261
audioTrack.dispose();
202262
audioTrack = null;
@@ -208,6 +268,7 @@ public void dispose() {
208268
}
209269

210270
customAudioSource = null;
271+
customVideoSource = null;
211272

212273
LOG.info("Media resources disposed");
213274
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
* Copyright 2025 Alex Andres
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package dev.onvoid.webrtc.examples.web.media;
18+
19+
import java.nio.ByteBuffer;
20+
import java.util.concurrent.Executors;
21+
import java.util.concurrent.ScheduledExecutorService;
22+
import java.util.concurrent.ScheduledFuture;
23+
import java.util.concurrent.TimeUnit;
24+
import java.util.concurrent.atomic.AtomicBoolean;
25+
import java.util.concurrent.atomic.AtomicInteger;
26+
27+
import dev.onvoid.webrtc.media.video.CustomVideoSource;
28+
import dev.onvoid.webrtc.media.video.NativeI420Buffer;
29+
import dev.onvoid.webrtc.media.video.VideoFrame;
30+
31+
import org.slf4j.Logger;
32+
import org.slf4j.LoggerFactory;
33+
34+
/**
35+
* A generator that produces synthetic video frames.
36+
* <p>
37+
* This class creates a color pattern that changes over time and delivers video frames
38+
* at regular intervals (30 fps by default). The generated video is in I420 format
39+
* with a default resolution of 640x480. The video generator runs on a dedicated scheduled
40+
* thread that can be started and stopped on demand.
41+
* </p>
42+
*
43+
* @author Alex Andres
44+
*/
45+
public class VideoGenerator {
46+
47+
private static final Logger LOG = LoggerFactory.getLogger(VideoGenerator.class);
48+
49+
/** The custom video source that receives generated video frames. */
50+
private final CustomVideoSource customVideoSource;
51+
52+
/** Flag indicating whether the video generator is running. */
53+
private final AtomicBoolean generatorRunning = new AtomicBoolean(false);
54+
55+
/** Counter for frame animation. */
56+
private final AtomicInteger frameCounter = new AtomicInteger(0);
57+
58+
/** Executor service for scheduling video frame generation. */
59+
private ScheduledExecutorService executorService;
60+
61+
/** Future for the scheduled video frame generation task. */
62+
private ScheduledFuture<?> generatorFuture;
63+
64+
/** Default video parameters */
65+
private static final int DEFAULT_WIDTH = 640;
66+
private static final int DEFAULT_HEIGHT = 480;
67+
private static final int DEFAULT_FPS = 30;
68+
private static final int FRAME_INTERVAL_MS = 1000 / DEFAULT_FPS;
69+
70+
71+
/**
72+
* Creates a new VideoGenerator that will push frames to the specified video source.
73+
*
74+
* @param videoSource The custom video source to receive the generated frames.
75+
*/
76+
public VideoGenerator(CustomVideoSource videoSource) {
77+
customVideoSource = videoSource;
78+
}
79+
80+
/**
81+
* Starts the video frame generator thread that pushes video frames
82+
* to the custom video source at the specified frame rate.
83+
*/
84+
public void start() {
85+
if (generatorRunning.get()) {
86+
LOG.info("Video generator is already running");
87+
return;
88+
}
89+
90+
executorService = Executors.newSingleThreadScheduledExecutor();
91+
generatorRunning.set(true);
92+
93+
generatorFuture = executorService.scheduleAtFixedRate(() -> {
94+
if (!generatorRunning.get()) {
95+
return;
96+
}
97+
98+
try {
99+
// Create a new video frame with a color pattern.
100+
VideoFrame frame = generateVideoFrame();
101+
102+
// Push the frame to the custom video source.
103+
customVideoSource.pushFrame(frame);
104+
105+
// Release the frame after pushing it.
106+
frame.release();
107+
108+
// Increment frame counter for animation.
109+
frameCounter.incrementAndGet();
110+
}
111+
catch (Exception e) {
112+
LOG.error("Error in video generator thread", e);
113+
}
114+
}, 0, FRAME_INTERVAL_MS, TimeUnit.MILLISECONDS);
115+
116+
LOG.info("Video generator started at {} fps", DEFAULT_FPS);
117+
}
118+
119+
/**
120+
* Stops the video frame generator thread.
121+
*/
122+
public void stop() {
123+
if (!generatorRunning.get()) {
124+
return;
125+
}
126+
127+
generatorRunning.set(false);
128+
129+
if (generatorFuture != null) {
130+
generatorFuture.cancel(false);
131+
generatorFuture = null;
132+
}
133+
134+
if (executorService != null) {
135+
executorService.shutdown();
136+
try {
137+
if (!executorService.awaitTermination(100, TimeUnit.MILLISECONDS)) {
138+
executorService.shutdownNow();
139+
}
140+
}
141+
catch (InterruptedException e) {
142+
executorService.shutdownNow();
143+
Thread.currentThread().interrupt();
144+
}
145+
executorService = null;
146+
}
147+
148+
LOG.info("Video generator stopped");
149+
}
150+
151+
/**
152+
* Generates a video frame with a color pattern that changes over time.
153+
*
154+
* @return A new VideoFrame with the generated pattern.
155+
*/
156+
private VideoFrame generateVideoFrame() {
157+
// Allocate a new I420 buffer for the frame
158+
NativeI420Buffer buffer = NativeI420Buffer.allocate(DEFAULT_WIDTH, DEFAULT_HEIGHT);
159+
160+
// Get the Y, U, V planes
161+
ByteBuffer dataY = buffer.getDataY();
162+
ByteBuffer dataU = buffer.getDataU();
163+
ByteBuffer dataV = buffer.getDataV();
164+
165+
int strideY = buffer.getStrideY();
166+
int strideU = buffer.getStrideU();
167+
int strideV = buffer.getStrideV();
168+
169+
// Generate a color pattern
170+
fillFrameWithPattern(dataY, dataU, dataV, strideY, strideU, strideV, frameCounter.get());
171+
172+
// Create a new video frame with the buffer and current timestamp
173+
return new VideoFrame(buffer, System.nanoTime());
174+
}
175+
176+
/**
177+
* Fills the frame buffers with a color pattern.
178+
* This creates a simple test pattern that changes over time.
179+
*
180+
* @param dataY The Y plane buffer.
181+
* @param dataU The U plane buffer.
182+
* @param dataV The V plane buffer.
183+
* @param strideY The Y plane stride.
184+
* @param strideU The U plane stride.
185+
* @param strideV The V plane stride.
186+
* @param frameCount The current frame count for animation.
187+
*/
188+
private void fillFrameWithPattern(ByteBuffer dataY, ByteBuffer dataU, ByteBuffer dataV,
189+
int strideY, int strideU, int strideV, int frameCount) {
190+
// Reset buffer positions
191+
dataY.position(0);
192+
dataU.position(0);
193+
dataV.position(0);
194+
195+
// Animation parameters
196+
int animOffset = frameCount % 255;
197+
198+
// Fill Y plane (luma)
199+
for (int y = 0; y < DEFAULT_HEIGHT; y++) {
200+
for (int x = 0; x < DEFAULT_WIDTH; x++) {
201+
// Create a gradient pattern
202+
byte value = (byte) ((x + y + animOffset) % 255);
203+
dataY.put(y * strideY + x, value);
204+
}
205+
}
206+
207+
// Fill U and V planes (chroma)
208+
// In I420 format, U and V planes are quarter size of Y plane
209+
for (int y = 0; y < DEFAULT_HEIGHT / 2; y++) {
210+
for (int x = 0; x < DEFAULT_WIDTH / 2; x++) {
211+
// Create color patterns that change over time
212+
byte uValue = (byte) ((x * 2 + animOffset) % 255);
213+
byte vValue = (byte) ((y * 2 + animOffset) % 255);
214+
215+
dataU.put(y * strideU + x, uValue);
216+
dataV.put(y * strideV + x, vValue);
217+
}
218+
}
219+
}
220+
}

0 commit comments

Comments
 (0)