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