Real-Time Watermark Rendering Using GLRecordingPipeline in FadCam
FadCam's GLRecordingPipeline enables real-time watermark rendering by delegating OpenGL texture updates to a dedicated background thread that overlays dynamic text onto camera frames before they reach the video encoder.
The FadCam Android application provides a production-ready implementation of real-time watermark rendering using OpenGL ES. By combining the GLRecordingPipeline class with the GLWatermarkRenderer, developers can overlay live data—such as GPS coordinates, timestamps, or speed—onto every video frame without interrupting the encoding process or dropping frames.
Architecture of the GLRecordingPipeline
The rendering system splits responsibilities between two primary classes to maintain clean separation between lifecycle management and graphics operations.
Core Components: Pipeline and Renderer
GLRecordingPipeline (defined in app/src/main/java/com/fadcam/opengl/GLRecordingPipeline.java) orchestrates the entire recording lifecycle. It manages the EGL context, creates the encoder surface, synchronizes timestamps between video and audio threads, and coordinates segment rollover. The constructor (lines 665-694) accepts a WatermarkInfoProvider interface that supplies the initial watermark text and a GLWatermarkRenderer instance that performs the actual drawing.
GLWatermarkRenderer (defined in app/src/main/java/com/fadcam/opengl/GLWatermarkRenderer.java) handles the low-level OpenGL work. It creates the OES texture that receives camera frames, executes the draw calls to render frames to the encoder surface, and manages the watermark texture updates. When the watermark text changes, the renderer regenerates the bitmap on the GL thread via updateWatermarkTextOnGlThread() and uploads it as a texture through applyWatermarkAndOverlayPayload() (lines 588-614).
Thread Safety and EGL Context
Camera frames arrive via SurfaceTexture.onFrameAvailable() and are queued on a dedicated HandlerThread running a renderRunnable. All GL operations—including watermark texture updates—execute on this thread to ensure the EGL context remains current. The pipeline provides getSynchronizedVideoTimestamp() to align video and audio streams using a monotonic clock, preventing drift between the overlay graphics and encoded audio.
Initializing the Recording Pipeline
To begin real-time watermark rendering, instantiate the pipeline with your desired configuration and a WatermarkInfoProvider implementation.
// Context from Activity or Service
Context ctx = this;
// Implement the provider to supply dynamic watermark content
WatermarkInfoProvider waterMarkProvider = new WatermarkInfoProvider() {
@Override
public String getWatermarkText() {
return "Lat:" + latitude + " Lon:" + longitude + " Speed:" + speed + "km/h";
}
};
// Configure video parameters
int videoWidth = 1280;
int videoHeight = 720;
int frameRate = 30;
String outputPath = getExternalFilesDir(null) + "/recording.mp4";
// Create the pipeline (constructor at lines 665-694)
GLRecordingPipeline pipeline = new GLRecordingPipeline(
ctx,
waterMarkProvider,
videoWidth,
videoHeight,
frameRate,
outputPath,
5L * 1024 * 1024, // 5MB max file size
1, // Segment number
null, // Segment callback
null, // Preview surface (null for encoder-only)
"portrait", // Orientation
0, // Sensor orientation
VideoCodec.H264, // Video codec
null, // Latitude
null // Longitude
);
Implementing Real-Time Watermark Updates
Once initialized, the pipeline requires surface preparation before accepting frames.
Preparing Surfaces and Starting Recording
Call prepareSurfaces() to create the EGL context, encoder input surface, and camera input surfaces. This method instantiates the internal GLWatermarkRenderer and configures the OpenGL viewport.
// Initialize EGL and create renderer (lines 556-711)
pipeline.prepareSurfaces();
// Start audio capture, muxer, and render loop
pipeline.startRecording();
The startRecording() method also invokes ensureWatermarkUpdaterRunning() (lines 606-608) to activate a low-frequency handler that polls the WatermarkInfoProvider for text changes.
Updating Watermark Text Dynamically
For immediate updates—such as when a new GPS fix arrives—you can push text directly to the renderer:
String updatedText = "Lat:" + lat + " Lon:" + lon + " Speed:" + speed + "km/h";
pipeline.glRenderer.setWatermarkText(updatedText); // Executes on GL thread
Alternatively, update the WatermarkInfoProvider implementation and let the pipeline's internal handler manage the refresh automatically. Both approaches trigger updateWatermarkTextOnGlThread(), ensuring the texture regeneration occurs safely within the EGL context without blocking the camera preview or encoder.
Advanced Configuration Options
The pipeline supports additional rendering modes for preview display and multi-camera setups.
Live Preview Integration
To render the watermarked output to a screen preview while encoding, pass a Surface from a TextureView or SurfaceView to the constructor:
Surface previewSurface = surfaceView.getHolder().getSurface();
GLRecordingPipeline pipeline = new GLRecordingPipeline(
ctx, waterMarkProvider, 1280, 720, 30,
outputPath, 5L * 1024 * 1024, 1, null,
previewSurface, "portrait", 0,
VideoCodec.H264, null, null);
pipeline.prepareSuraces(); // Creates both encoder and preview EGL surfaces
pipeline.startRecording(); // Renders to both targets simultaneously
The preview rendering path uses GLWatermarkRenderer.renderToPreview() (lines 723-835) with aspect-ratio-preserving viewport calculations (lines 786-845) to maintain proper display scaling without affecting the full-resolution encoder output.
Dual-Camera Picture-in-Picture
Enable PiP recording by passing a DualCameraConfig to the appropriate constructor overload (starting at line 661). The pipeline exposes getSecondaryCameraInputSurface() and swapCameras() to manage dual-stream input, with the renderer compositing the secondary feed over the primary camera frame before applying the watermark texture.
Resource Cleanup
Properly release resources to ensure the output MP4 file is finalized and playable:
pipeline.stopRecording(); // Drains encoders, stops audio, signals EOF to muxer
pipeline.release(); // Releases EGL context, surfaces, and renderer resources
The stopRecording() method handles the final frame drain and muxer shutdown, ensuring all watermarked frames are written to disk.
Summary
GLRecordingPipelinemanages the recording lifecycle and EGL context inapp/src/main/java/com/fadcam/opengl/GLRecordingPipeline.java, whileGLWatermarkRendererexecutes OpenGL draw calls inapp/src/main/java/com/fadcam/opengl/GLWatermarkRenderer.java.- Watermark updates occur on the GL thread via
setWatermarkText()or theWatermarkInfoProviderinterface, guaranteeing thread safety without encoder stalls. - The system supports simultaneous preview and encoding using separate EGL surfaces with independent viewport calculations.
- Dual-camera PiP mode is available through extended constructor APIs that accept
DualCameraConfig. - All EGL operations include retry logic to survive transient graphics driver failures (lines 663-720).
Frequently Asked Questions
How does GLRecordingPipeline ensure thread-safe watermark updates?
All watermark texture updates execute on the dedicated GL thread through updateWatermarkTextOnGlThread(). This method creates a bitmap from the text string and uploads it to the GPU using applyWatermarkAndOverlayPayload() (lines 588-614), ensuring the EGL context is current and preventing race conditions with the frame rendering loop.
Can I update the watermark text while recording without stopping the encoder?
Yes. The setWatermarkText() method can be called at any time during recording. The change applies to the next frame rendered to the encoder surface without interrupting the MediaCodec session or causing frame drops, as the texture swap occurs atomically within the existing GL rendering cycle.
What video codecs does GLRecordingPipeline support?
The pipeline supports H.264 and H.265 (HEVC) through the VideoCodec enum defined in app/src/main/java/com/fadcam/VideoCodec.java. Pass the desired codec to the GLRecordingPipeline constructor to configure the underlying MediaCodec encoder format.
How do I handle orientation changes during recording?
The constructor accepts orientation and sensorOrientation parameters that configure the renderer's transformation matrix. For dynamic orientation changes while recording, create a new pipeline instance with updated parameters and restart the recording session, as the encoder surface format is fixed at initialization time.
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 →