Skip to content

Fix/surface pts shares audio origin - #2181

Merged
pedroSG94 merged 3 commits into
masterfrom
fix/surface-pts-shares-audio-origin
Aug 12, 2026
Merged

Fix/surface pts shares audio origin#2181
pedroSG94 merged 3 commits into
masterfrom
fix/surface-pts-shares-audio-origin

Conversation

@pedroSG94

Copy link
Copy Markdown
Owner

No description provided.

x270880x and others added 3 commits August 11, 2026 09:48
In SURFACE mode VideoEncoder.checkBuffer rebases every frame on firstTimestamp —
the PTS of the first frame this encoder produced. AudioEncoder.calculatePts
rebases on presentTimeUs, the origin StreamBase hands to every encoder in
startSources(). The two timelines therefore start at different moments: audio at
the instant the stream started, video at the instant its first frame came out of
the encoder. Whatever sits between them — opening the camera, warming up GL —
becomes a constant offset, with the picture running ahead of the sound.

Measured on a Samsung SM-A065F (Android 15, camera source, built-in mic, RTMP):
video ran 470 ms ahead of audio, 18 clap pairs, spread 427-517 ms. Lips move,
speech follows. Both TimestampMode branches perform the same rebase, so switching
CLOCK/BUFFER makes no difference — verified by measuring both.

Rebasing on presentTimeUs is safe here: GlStreamInterface stamps frames through
GlTimestamp, which anchors the first frame to TimeUtils and clamps later frames to
within one frame of that clock, and StreamBase reads the same TimeUtils to start
the encoders. Both sides are on CLOCK_BOOTTIME microseconds, so the subtraction is
meaningful and matches what the audio path already does. Non-surface paths are
untouched, and the old behaviour still applies when the encoder was started
without a shared origin.

With the same test the 490 ms cluster disappears; what remains is inside the
resolution of a clap measurement.
@x270880x

Copy link
Copy Markdown
Contributor

Tested on device. Your version works, and it works without the workaround we had been carrying.

Setup: Samsung SM-A065F (Android 15), camera source, built-in mic, RTMP to a local ffmpeg -listen 1 sink over an adb reverse tunnel. Instead of clapping I used a generated reference — a 60 fps clip that flashes one white frame and emits a 1200 Hz click on the same frame, verified sample-accurate before use. The phone films the laptop screen playing it, so light and sound leave the same source at the same instant; the only systematic bias is the laptop's audio output latency, which pushes the measured audio slightly late.

Camera, your patch applied, our own fix disabled: 47 pairs, median −87 ms, spread −143…−53. Unpatched it was +470 ms with the picture ahead of the sound (18 clap pairs, 427–517 ms). So the half-second is gone.

The residual ~87 ms in the other direction is not the origin bug — we measured the same figure with our own fix in place, and it matches the delay between the sensor exposing a frame and the GL interface stamping it. Different problem, much smaller, outside the scope of this PR.

Screen capture: no regression. 45 pairs, median +93 ms against +57 ms unpatched — both within one to three frames at 30 fps.

I built your commit on top of the 2.8.0 tag rather than master, because our app does not compile against master: SpriteGestureController now takes a Sprite and getFilterRender() is gone, which breaks every consumer driving overlay gestures through the filter render (five call sites for us). Unrelated to this PR, but probably worth a line in the release notes.

Thanks for the fix, and for catching the case my patch would have broken — FromFileBase and DisplayBase without OpenGL would have been rebased against an origin from a different clock.

@x270880x

Copy link
Copy Markdown
Contributor

Unrelated to this PR, but found while measuring the same builds, in case you want to look at both together: #2182.

Portrait broadcasts announce the wrong shape. VideoEncoder keeps width/height as prepareVideoEncoder received them and applies rotation only when creating the codec, so (1920, 1080, rotation = 90) produces 1080x1920 frames while getVideoResolution() still returns 1920x1080 — and that pair goes into onMetaData from GenericStream.kt:111, RtmpStream.kt:73 and MultiStream.kt:188. YouTube believes the metadata, opens a 16:9 window and pillarboxes the vertical picture. MultiCamera1/2 already swap at the call site, so only the newer classes are affected.

@pedroSG94
pedroSG94 merged commit e5bcb1b into master Aug 12, 2026
1 check passed
@pedroSG94
pedroSG94 deleted the fix/surface-pts-shares-audio-origin branch August 12, 2026 10:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants