How the GLRecordingPipeline in FadCam Renders Watermarks in Real-Time
FadCam overlays dynamic watermarks onto every video frame using an OpenGL ES pipeline that regenerates the watermark bitmap each second and composites it via GPU texture blending before encoding.
The GLRecordingPipeline class in the FadCam Android application orchestrates a hardware-accelerated video pipeline that blends timestamp, location, and device information directly into the camera feed. This real-time rendering approach ensures that watermarks appear crisp and synchronized without post-processing delays.
Core Architecture Components
Three primary classes collaborate to inject watermarks during the camera-to-encoder path:
- GLRecordingPipeline – Manages the MediaCodec video encoder, owns the
GLWatermarkRendererinstance, and drives the render loop while synchronizing audio/video timestamps. - GLWatermarkRenderer – Executes low-level OpenGL ES commands to draw the camera texture and overlay the watermark bitmap using a custom shader program.
- WatermarkInfoProvider – An interface implemented by
WatermarkManagerthat supplies dynamic text content (timestamps, GPS coordinates, device names) to the pipeline.
According to the FadCam source code, these components reside in app/src/main/java/com/fadcam/opengl/ and interact through a strict callback-driven flow to maintain 30fps performance.
The Real-Time Watermark Rendering Flow
The pipeline implements a producer-consumer pattern where CPU-side text generation occurs asynchronously from GPU-side frame composition.
Step 1: Initialize the Pipeline and Watermark Updater
When recording begins, GLRecordingPipeline.startRecording() launches a dedicated HandlerThread for OpenGL operations and invokes ensureWatermarkUpdaterRunning(). This sets up a recurring Runnable that fires every 1000ms.
// Starting the pipeline with watermark support
GLRecordingPipeline pipeline = new GLRecordingPipeline(
context,
watermarkInfoProvider, // Provides dynamic text
videoWidth,
videoHeight,
videoFramerate,
outputFilePath,
maxFileSize,
1, // Segment index
segmentCallback,
previewSurface,
"portrait",
sensorOrientation,
VideoCodec.H264,
null, // Optional latitude
null // Optional longitude
);
pipeline.prepareSurfaces(); // Creates encoder surface & EGL context
pipeline.startRecording(); // Starts render thread and watermark updater
Step 2: Update Watermark Text via Handler
Every second, the updateWatermarkRunnable executes updateWatermark() (located around lines 460-480 in GLRecordingPipeline.java). This method queries the WatermarkInfoProvider for fresh text and forwards it to the renderer.
private void updateWatermark() {
try {
String text = watermarkInfoProvider != null
? watermarkInfoProvider.getWatermarkText()
: "";
glRenderer.setWatermarkText(text);
} catch (Exception e) {
FLog.w(TAG, "Watermark update failed", e);
}
}
Step 3: Render Loop and Frame Composition
The render loop triggers via OnFrameAvailableListener attached to the camera's SurfaceTexture. Each frame invokes renderRunnable, which calls GLWatermarkRenderer.renderFrame().
Inside renderToEncoderInternal() (lines ~330-420), the renderer performs these operations:
- Update the camera SurfaceTexture and compute presentation timestamps using
recordingPipeline.getSynchronizedVideoTimestamp(). - Bind the OES texture containing the camera frame.
- Draw the camera quad to the encoder's EGL surface.
- Invoke
drawWatermark()to composite the overlay texture on top.
Step 4: OpenGL Texture Generation and Blending
The drawWatermark() method generates an Android Bitmap from the current watermark text using Canvas and Paint, then uploads it to the GPU:
private void drawWatermark() {
if (watermarkBitmap == null || watermarkBitmap.isRecycled()) return;
// Generate or reuse GL texture ID
if (watermarkTextureId == 0) {
int[] tex = new int[1];
GLES20.glGenTextures(1, tex, 0);
watermarkTextureId = tex[0];
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, watermarkTextureId);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D,
GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
}
// Upload bitmap to GPU
GLUtils.texImage2D(GLES20.GL_TEXTURE_2D, 0, watermarkBitmap, 0);
// Activate shader and draw quad at top-left corner
GLES20.glUseProgram(watermarkProgram);
GLES20.glUniformMatrix4fv(watermarkTexCoordHandle, 1,
false, watermarkTexCoordBuffer.array(), 0);
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, watermarkTextureId);
GLES20.glUniform1i(watermarkSamplerHandle, 0);
mFullFrameBlit.drawFrame(watermarkRectBuffer, watermarkTexCoordBuffer);
}
Step 5: Commit to Encoder
Finally, EGL14.eglSwapBuffers() commits the fully composited frame (camera + watermark) to the MediaCodec input surface, where H.264 compression occurs before muxing into the MP4 container.
Timestamp Synchronization Mechanism
To prevent audio drift during watermark rendering, GLRecordingPipeline uses System.nanoTime() as the monotonic clock source for both video and audio tracks. The method getSynchronizedVideoTimestamp() converts nanoseconds to microseconds for MediaCodec presentation timestamps (PTS).
This approach guarantees that watermark frames remain temporally aligned with audio samples even after recording pauses, camera switches, or thermal throttling events.
Performance Characteristics
The real-time constraint is satisfied through three design decisions:
- Decoupled text rasterization occurs on the CPU once per second, not per frame.
- Texture reuse prevents GPU memory allocation during the render hot path.
- Shader-based positioning leverages hardware vertex/fragment processing rather than CPU pixel manipulation.
Because drawWatermark() executes within the same eglSwapBuffers boundary as the camera frame, the watermark appears atomically encoded into the video bitstream with zero frame latency.
Summary
- GLRecordingPipeline.java coordinates the encoder, renderer, and periodic watermark updates via a Handler posting every 1000ms.
- GLWatermarkRenderer.java composites camera textures with watermark bitmaps using OpenGL ES 2.0 shaders and texture mapping.
- WatermarkInfoProvider abstraction allows dynamic content injection without modifying the rendering logic.
- System.nanoTime() synchronization ensures audio/video alignment regardless of recording interruptions.
- The watermark updates propagate to the next available frame immediately upon text change, achieving true real-time overlay behavior.
Frequently Asked Questions
How often does the watermark text update during recording?
The watermark updates once per second via a Handler posting updateWatermarkRunnable in GLRecordingPipeline. However, because the render loop runs at the camera frame rate (typically 30fps), any text change appears on the very next encoded frame, creating the illusion of continuous real-time updates.
What OpenGL ES version does FadCam use for watermark rendering?
According to the source in GLWatermarkRenderer.java, FadCam targets OpenGL ES 2.0 using GLES20 classes. The implementation creates 2D textures with GLUtils.texImage2D() and uses custom fragment shaders for texture sampling, ensuring compatibility with nearly all Android devices back to API level 18.
Can the watermark position be changed from the top-left corner?
The current implementation in drawWatermark() uses pre-computed vertex buffers (watermarkRectBuffer) that position the quad at the top-left of the frame. Modifying the vertex coordinates in GLWatermarkRenderer.java would allow repositioning to any normalized screen coordinate, though this requires recompiling the application as no runtime configuration exists in the current codebase.
How does FadCam prevent watermark updates from causing frame drops?
Text rasterization to Bitmap occurs on the main updateWatermark() call, not on the GL thread. The setWatermarkText() method merely signals the renderer to use the new bitmap on the next drawWatermark() invocation. This double-buffering approach ensures that heavy Canvas operations never block the eglSwapBuffers() critical path.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →