Handle FPS mismatch between video source and video encoder output#331
Open
dimadesu wants to merge 7 commits into
Open
Handle FPS mismatch between video source and video encoder output#331dimadesu wants to merge 7 commits into
dimadesu wants to merge 7 commits into
Conversation
---
Prompt:
/grill-me i want lifestreamer app to try basinng fps for setting off what video encoder supports, not what built-in camera supports. this app can have RTMP/SRT player (Exoplayer) as source - that can have different FPS than built-in camera as well as USB/UVC camera source - same. i need to have a way to switch between different video sources during one stream that will work when input fps is lower or higher than output video encoder FPS.
For inspiration you can check inspiration/moblin project. I think it has some timers for each frame and it either duplicates a previous frame or render black frame depending on the source if source FPS is lower than target. Not sure what happens when source FPS is higher then target.
---
Here is the complete list of files that have been changed across the project:
* **[SettingsFragment.kt](file:///Users/dmytro.antonov/git/LifeStreamer/app/src/main/java/com/dimadesu/lifestreamer/ui/settings/SettingsFragment.kt)**: Decoupled target FPS preferences from camera capabilities, showing values strictly based on encoder support.
* **[DefaultSurfaceProcessor.kt](file:///Users/dmytro.antonov/git/LifeStreamer/StreamPack/core/src/main/java/io/github/thibaultbee/streampack/core/elements/processing/video/DefaultSurfaceProcessor.kt)**: Implemented the new timer-driven render loop running at the target FPS, duplicating or dropping frames as necessary, and handling the 500ms inactivity timeout (by sending black frames).
* **[ISurfaceProcessor.kt](file:///Users/dmytro.antonov/git/LifeStreamer/StreamPack/core/src/main/java/io/github/thibaultbee/streampack/core/elements/processing/video/ISurfaceProcessor.kt)**: Exposed `setTargetFps` in `ISurfaceProcessorInternal`.
* **[OpenGlRenderer.kt](file:///Users/dmytro.antonov/git/LifeStreamer/StreamPack/core/src/main/java/io/github/thibaultbee/streampack/core/elements/processing/video/OpenGlRenderer.kt)**: Added the `renderBlack` method to clear EGL window buffers to black.
* **[VideoInput.kt](file:///Users/dmytro.antonov/git/LifeStreamer/StreamPack/core/src/main/java/io/github/thibaultbee/streampack/core/pipelines/inputs/VideoInput.kt)**: Updated processor instantiation and configuration flows to feed the target FPS parameter into the surface processor.
---
The goal is to decouple the video input source's framerate (e.g. built-in camera, USB/UVC camera, or RTMP/SRT ExoPlayer source) from the video encoder's target framerate. Currently, rendering and encoding are event-driven, triggered directly by the input source's `onFrameAvailable` callback. Under the new decoupled architecture, the renderer will run a stable, target FPS-driven render loop.
- When the input FPS is **lower** than the target FPS, the render loop will duplicate the previous frame.
- When the input FPS is **higher** than the target FPS, the render loop will naturally drop extra frames.
- When the source is **stopped, disconnected, or switching** (no frame received within a 500ms timeout), the render loop will render a solid black frame to keep the encoder fed.
- Settings will be updated so that the video FPS preference list is based on the video encoder's capabilities, not filtered by the camera. The warning about camera FPS support will be removed.
---
We have aligned on the following design decisions through the `/grill-me` session:
1. **Frame Decoupling Behavior:** Duplicate the last frame when the active source's frame rate is lower than target. Render a black frame when the source is stopped, disconnected, or switching.
2. **Timeout Threshold:** A 500ms timeout of no frames from the active input surface will declare it "disconnected/stopped" and transition the render loop from frame-duplication to black-frame rendering.
3. **Camera FPS Warning:** Remove the warning dialog in the Settings screen because the camera's FPS no longer restricts or blocks the target encoder FPS.
---
- Add `fun setTargetFps(fps: Int)` to `ISurfaceProcessorInternal` to allow updating the target loop FPS.
- Add a `renderBlack(timestampNs: Long, surface: Surface)` method that clears the color buffer using `GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)` and `GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)` to draw a solid black frame on the specified output surface.
- Implement `setTargetFps(fps: Int)` on `DefaultSurfaceProcessor`.
- Add a periodic render loop (`Runnable` running via `glHandler.postDelayed`) that ticks at the target FPS. It should use a self-correcting timer to ensure precise and stable intervals.
- Track state: `lastActiveSurfaceTexture: SurfaceTexture?`, `lastFrameTimeMs: Long`, `hasFrame: Boolean`, and cached texture/output matrices.
- Modify `onFrameAvailable(surfaceTexture)` to only update the texture (`surfaceTexture.updateTexImage()`), store the matrix/timestamp, and set flags. Remove the direct rendering call.
- In the render loop tick:
- If any output surface is streaming or previewing (i.e. `surfaceOutput.isStreaming()` is true):
- Check if we have an active texture and the elapsed time since the last frame is $\leq$ 500ms.
- If yes, draw the texture frame on the active surfaces with `renderer.render` using the current uptime timestamp (`System.nanoTime()`).
- If no (timeout or no source), clear the active surfaces to solid black using `renderer.renderBlack` with the current uptime timestamp.
- Cancel the render loop inside `release()`.
- In the constructor, call `processor.setTargetFps(30)` (or a sensible default).
- In `applySourceConfig`, call `processor.setTargetFps(videoConfig.fps)`.
- In `buildSurfaceProcessor`, call `newSurfaceProcessor.setTargetFps(videoSourceConfig.fps)`.
---
- Update `loadVideoSettings(encoder: String)` where `videoFpsListPreference` is populated:
- Instead of getting supported framerates via `streamerInfo.video.getSupportedFramerates(..., cameraId)`, query `streamerInfo.video.getSupportedFramerate(encoder)` which returns the `Range<Int>` supported by the video encoder.
- Filter `FpsEntries` (24, 25, 30, 60) based on whether they fall within this encoder range.
- Remove the camera FPS warning from the `setOnPreferenceChangeListener` block.
---
- Run Gradle check tasks to ensure the core library and app modules build successfully:
```bash
./gradlew :StreamPack:core:assembleDebug
./gradlew :app:assembleDebug
```
- Deploy the app to a device or emulator and run a stream.
- In Settings, verify that 60 FPS is selectable even if the built-in front camera only supports 30 FPS.
- Start streaming and switch video sources (e.g. from built-in Camera to UVC camera or RTMP player source). Verify that:
- The stream continues to run at a stable target FPS.
- During the source switch gap, a solid black frame is rendered, preventing connection drops or stream freezes.
- Disconnecting a USB camera results in a solid black screen rather than a frozen last frame.
30f02d5 to
da1e569
Compare
…amera switch - Align EGL rendering presentation time with hardware capture clocks by converting SurfaceTexture.timestamp via VideoTimebaseConverter. - Enforce strict monotonicity on rendered frame timestamps to avoid backward/duplicate timestamps during camera switch or transition. - Reset the running lastRenderedTimestampNs tracker when all streaming outputs are inactive/empty to ensure fresh start on new sessions. - Clear previousPresentationTimeUs state in FrameFactory on MediaCodecEncoder resets to prevent stale timestamp tracking when restarting the encoder.
…amera switch - Align EGL rendering presentation time with hardware capture clocks by converting SurfaceTexture.timestamp via VideoTimebaseConverter. - Enforce strict monotonicity on rendered frame timestamps to avoid backward/duplicate timestamps during camera switch or transition. - Reset the running lastRenderedTimestampNs tracker when all streaming outputs are inactive/empty to ensure fresh start on new sessions. - Clear previousPresentationTimeUs state in FrameFactory on MediaCodecEncoder resets to prevent stale timestamp tracking when restarting the encoder.
94cd1fe to
65896c3
Compare
…estamps Update StreamPack submodule to include two camera switch glitch fixes: 1. IDR keyframe request after camera switch (high impact): After a camera switch completes during a live SRT stream, the pipeline now immediately requests an IDR keyframe from the video encoder. This gives OBS a clean reference frame right away instead of waiting for the next natural keyframe interval, eliminating video corruption artifacts during the transition gap. 2. Black-frame timestamp spacing fix (medium impact): During camera transitions, black frames are now spaced at proper frame intervals (1/targetFps) instead of 1-microsecond increments, producing natural timestamps that decoders and muxers handle correctly.
65896c3 to
a10bbda
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Prompt
/grill-me i want lifestreamer app to try basing fps for setting off what video encoder supports, not what built-in camera supports. this app can have RTMP/SRT player (Exoplayer) as source - that can have different FPS than built-in camera as well as USB/UVC camera source - same. i need to have a way to switch between different video sources during one stream that will work when input fps is lower or higher than output video encoder FPS.
For inspiration you can check inspiration/moblin project. I think it has some timers for each frame and it either duplicates a previous frame or render black frame depending on the source if source FPS is lower than target. Not sure what happens when source FPS is higher then target.
Here is the complete list of files that have been changed across the project:
Main Application
StreamPackSubmodulesetTargetFpsinISurfaceProcessorInternal.renderBlackmethod to clear EGL window buffers to black.Implementation Plan - Decouple Video Source FPS from Video Encoder FPS
The goal is to decouple the video input source's framerate (e.g. built-in camera, USB/UVC camera, or RTMP/SRT ExoPlayer source) from the video encoder's target framerate. Currently, rendering and encoding are event-driven, triggered directly by the input source's
onFrameAvailablecallback. Under the new decoupled architecture, the renderer will run a stable, target FPS-driven render loop.User Review Required
We have aligned on the following design decisions through the
/grill-mesession:Proposed Changes
StreamPack Core Library
[MODIFY] ISurfaceProcessor.kt
fun setTargetFps(fps: Int)toISurfaceProcessorInternalto allow updating the target loop FPS.[MODIFY] OpenGlRenderer.kt
renderBlack(timestampNs: Long, surface: Surface)method that clears the color buffer usingGLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f)andGLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)to draw a solid black frame on the specified output surface.[MODIFY] DefaultSurfaceProcessor.kt
setTargetFps(fps: Int)onDefaultSurfaceProcessor.Runnablerunning viaglHandler.postDelayed) that ticks at the target FPS. It should use a self-correcting timer to ensure precise and stable intervals.lastActiveSurfaceTexture: SurfaceTexture?,lastFrameTimeMs: Long,hasFrame: Boolean, and cached texture/output matrices.onFrameAvailable(surfaceTexture)to only update the texture (surfaceTexture.updateTexImage()), store the matrix/timestamp, and set flags. Remove the direct rendering call.surfaceOutput.isStreaming()is true):renderer.renderusing the current uptime timestamp (System.nanoTime()).renderer.renderBlackwith the current uptime timestamp.release().[MODIFY] VideoInput.kt
processor.setTargetFps(30)(or a sensible default).applySourceConfig, callprocessor.setTargetFps(videoConfig.fps).buildSurfaceProcessor, callnewSurfaceProcessor.setTargetFps(videoSourceConfig.fps).LifeStreamer App
[MODIFY] SettingsFragment.kt
loadVideoSettings(encoder: String)wherevideoFpsListPreferenceis populated:streamerInfo.video.getSupportedFramerates(..., cameraId), querystreamerInfo.video.getSupportedFramerate(encoder)which returns theRange<Int>supported by the video encoder.FpsEntries(24, 25, 30, 60) based on whether they fall within this encoder range.setOnPreferenceChangeListenerblock.Verification Plan
Automated Tests
Manual Verification