# How the GLRecordingPipeline in FadCam Renders Watermarks in Real-Time

> Discover how the GLRecordingPipeline in FadCam renders watermarks in real-time by exploring its GPU texture blending and dynamic bitmap regeneration techniques for seamless video overlay.

- Repository: [Faded/FadCam](https://github.com/anonfaded/FadCam)
- Tags: internals
- Published: 2026-05-13

---

**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 `GLWatermarkRenderer` instance, 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 `WatermarkManager` that 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.

```java
// 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`](https://github.com/anonfaded/FadCam/blob/main/GLRecordingPipeline.java)). This method queries the `WatermarkInfoProvider` for fresh text and forwards it to the renderer.

```java
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:

1. **Update the camera SurfaceTexture** and compute presentation timestamps using `recordingPipeline.getSynchronizedVideoTimestamp()`.
2. **Bind the OES texture** containing the camera frame.
3. **Draw the camera quad** to the encoder's EGL surface.
4. **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:

```java
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`](https://github.com/anonfaded/FadCam/blob/main/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`](https://github.com/anonfaded/FadCam/blob/main/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.