From da7655ee252246e7eb325c65a18e2fed24d90977 Mon Sep 17 00:00:00 2001 From: spoopyghosty0 <336754034+spoopyghosty0@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:06:02 -0400 Subject: [PATCH] Live view hardware encoder: fixes from the first device run - fp_venc: accept the source's buffer count (the Frame's v4l2loopback has max_buffers=2; asking for 3 failed the probe); OUTPUT buffers remember which picture they hold, so repeats (standby, still scenes) skip the conversion (720p: 35% -> 2.5% of a core with the headset asleep). - Pipeline: raw H.264 stamped on arrival (-use_wallclock_as_timestamps, same clock as pulse) with a 32-byte probe: -framerate was ignored, -fflags nobuffer lost the first seconds, frames numbered from 0 held all output ~7 s next to the sound. frag_keyframe so a requested keyframe starts a fragment (a joining viewer starts there). Verified on the Frame (headset asleep): selftest ok (104 cases), probe at 360p-1080p, end to end through livestream.start: 720p36 + AAC in 0.1 s fragments, audio and video spans match, a second viewer starts on its requested keyframe at once, nothing left running after stop; FP_VENC_DISABLE=1 falls back to x264. --- artifacts/SHA256SUMS | 2 +- artifacts/linux-arm64-bin/fp_venc | Bin 32216 -> 31960 bytes docs/FRAME_RUNTIME.md | 14 ++++++++++++++ native/venc/fp_venc.c | 26 ++++++++++++++++++-------- src/frameport/install/livestream.py | 12 ++++++++---- tests/test_livestream.py | 8 ++++++-- 6 files changed, 47 insertions(+), 15 deletions(-) diff --git a/artifacts/SHA256SUMS b/artifacts/SHA256SUMS index c277ce0b..5ef08450 100644 --- a/artifacts/SHA256SUMS +++ b/artifacts/SHA256SUMS @@ -12,6 +12,6 @@ c10412dd76a65a1d587b7ed226367d1a6f2bec238c733c97ee67eb8870c868e8 ./armeabi-v7a/ 1871eae093432d277da4bc751bf5f3269dfcf11f9168260b0c3caadb0dedb19d ./dex/oculusos-stubs.dex 1aa733117cf57ccff7cf23f0425dbfd73b0332a3ef1c905ceeca0ccd0449c4e0 ./linux-arm64/XR_APILAYER_FRAMEPORT_timefix.json 28c2430a02bbd8902c5bfb9562c6fd0e318654f9e05c095bfeb94b0450ab1b07 ./linux-arm64/libxr_frameport_timefix.so -33a4060010a9d2c5d1515fa2d5294beb664edeaef7d37f4d325ab6e6ce6a5f0e ./linux-arm64-bin/fp_venc +a4a6d9f41c290a421842294a39562e1e6f0ca271bb47b72f615c808560338f6a ./linux-arm64-bin/fp_venc c2c70f4af1a5c3e6089dff1130161dd417f8044f99ec639eeecf1083930aab16 ./win-x64/fp_oculushmd.exe 60ebc5fea05b8945082e2ca8b9f2eb74616012a26eb50133af273dff0fcddc42 ./win-x64/fp_vrsettings.exe diff --git a/artifacts/linux-arm64-bin/fp_venc b/artifacts/linux-arm64-bin/fp_venc index 7402062cd1d4cd9d685f97d79a360c92de446a01..696ccfd02d656b9983c415722be7a2af16de9f64 100644 GIT binary patch delta 8217 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 t57*{#E7U=siJs-rV(7Y&%nEVbxfd, VIDIOC_REQBUFS, &rb); - if (r < 0 || rb.count < SRC_BUFS) return fail(3, "source VIDIOC_REQBUFS", r < 0 ? r : -ENOMEM); - for (int i = 0; i < SRC_BUFS; i++) { + /* The driver may grant fewer (the Frame's v4l2loopback has max_buffers=2); two are enough: one held, one filling. */ + if (r < 0 || rb.count < 2) return fail(3, "source VIDIOC_REQBUFS", r < 0 ? r : -ENOMEM); + if (rb.count > SRC_BUFS) rb.count = SRC_BUFS; + for (int i = 0; i < (int)rb.count; i++) { struct v4l2_buffer b; memset(&b, 0, sizeof b); b.index = (uint32_t)i, b.type = V4L2_BUF_TYPE_VIDEO_CAPTURE, b.memory = V4L2_MEMORY_MMAP; @@ -710,6 +714,8 @@ int main(int argc, char **argv, char **envp) const int64_t t0 = now_ns(), period = 1000000000 / fps; int64_t next_stats = t0 + 10000000000LL, quiet_src = 0, quiet_enc = 0; long slot = 0; /* next slot to submit; due at t0 + slot * 1e9 / fps */ + long pic = 0; /* current picture: 0 = black (no source frame yet), +1 per new source frame */ + for (int i = 0; i < ENC_BUFS; i++) e.out_pic[i] = -1; int want_key = 0, stdin_open = 1, sent_any = 0, rc = 0; for (;;) { @@ -726,12 +732,16 @@ int main(int argc, char **argv, char **envp) while (i < ENC_BUFS && !e.out_free[i]) i++; if (i == ENC_BUFS) break; uint8_t *m = e.out_mem[i]; - if (s.held >= 0) { - fp_convert(s.mem[s.held], s.w, s.h, s.stride, m, e.stride, m + (size_t)e.stride * e.hpad, e.stride, - ow, oh, scratch); - } else { /* no source frame yet: black */ - memset(m, 16, (size_t)e.stride * oh); - memset(m + (size_t)e.stride * e.hpad, 128, (size_t)e.stride * oh / 2); + if (s.held >= 0 && s.fresh) pic++; + if (e.out_pic[i] != pic) { + if (s.held >= 0) { + fp_convert(s.mem[s.held], s.w, s.h, s.stride, m, e.stride, m + (size_t)e.stride * e.hpad, + e.stride, ow, oh, scratch); + } else { /* no source frame yet: black */ + memset(m, 16, (size_t)e.stride * oh); + memset(m + (size_t)e.stride * e.hpad, 128, (size_t)e.stride * oh / 2); + } + e.out_pic[i] = pic; } if (!s.fresh && sent_any) st.repeats++; s.fresh = 0; diff --git a/src/frameport/install/livestream.py b/src/frameport/install/livestream.py index 10c51444..d64970e2 100644 --- a/src/frameport/install/livestream.py +++ b/src/frameport/install/livestream.py @@ -428,11 +428,15 @@ info= hwfps=$(printf %s "$info" | sed -n 's/.*"fps":\\([0-9][0-9]*\\).*/\\1/p') if [ -n "$hwfps" ]; then echo "{INFO_PREFIX}encoder=hardware fps=$hwfps" >&2 - # fp_venc reads the SSH channel (stdin): "k" = keyframe, EOF = stop; ffmpeg then ends with it (-shortest) + # fp_venc reads the SSH channel (stdin): "k" = keyframe, EOF = stop; ffmpeg then ends with it (-shortest). + # Raw H.264 has no timestamps: frames are stamped on arrival (wall clock, like pulse's audio; fp_venc sends them on + # its even fps grid, so arrival = capture + a constant). Measured on the Frame (2026-10-07): -framerate is ignored; + # numbering frames from 0 (setts) held all output ~7 s when sound was on (interleaving against pulse's wall clock); + # -fflags nobuffer lost the first seconds of a still picture's tiny frames; a big probesize waits seconds for one. + # frag_keyframe: a requested keyframe starts its own fragment, where a joining viewer can begin. "$venc" {venc_args} --fps "$hwfps" | nice -n 10 ffmpeg -nostdin -hide_banner -loglevel error \\ - -probesize 262144 -analyzeduration 500000 -fflags +genpts+nobuffer -f h264 -framerate "$hwfps" -i pipe:0 \\ - "${{audio[@]}}" -c:v copy -shortest \\ - -f mp4 -movflags empty_moov+default_base_moof -frag_duration 100000 - + -probesize 32 -analyzeduration 0 -use_wallclock_as_timestamps 1 -f h264 -i pipe:0 "${{audio[@]}}" \\ + -c:v copy -shortest -f mp4 -movflags empty_moov+default_base_moof+frag_keyframe -frag_duration 100000 - exit $? fi echo "{INFO_PREFIX}encoder=software fps={fps}" >&2 diff --git a/tests/test_livestream.py b/tests/test_livestream.py index 2a72aefa..6a0de6b0 100644 --- a/tests/test_livestream.py +++ b/tests/test_livestream.py @@ -243,7 +243,9 @@ def test_relay_with_audio(): def test_source_command_captures_default_output(): cmd = L.source_command() assert "pactl get-default-sink" in cmd and "$sink.monitor" in cmd and "-c:a aac" in cmd - assert "-ts mono2abs" in cmd and "-use_wallclock_as_timestamps" not in cmd # both on pulse's wall clock + audio = cmd[cmd.index("audio=(-thread_queue_size"):cmd.index("fi\n", cmd.index("audio=(-thread_queue_size"))] + assert "-ts mono2abs" in cmd and "-use_wallclock_as_timestamps" not in audio # both on pulse's wall clock + # (the hardware path stamps its raw H.264 input with the wall clock; the pulse input never gets that flag) assert "aresample=async=1" in cmd @@ -291,7 +293,9 @@ def test_source_command_hardware_path(tmp_path): assert p.wait(timeout=5) == 0 assert b"live: encoder=hardware fps=32" in p.stderr.read() args = (tmp_path / "ffmpeg.args").read_text() - assert "-f h264 -framerate 32 -i pipe:0" in args and "-c:v copy" in args and "libx264" not in args + assert "-f h264 -i pipe:0" in args and "-c:v copy" in args and "libx264" not in args + assert "-use_wallclock_as_timestamps 1" in args and "nobuffer" not in args # same clock as pulse's audio + assert "+frag_keyframe" in args # a requested keyframe starts a fragment (where a new viewer begins) venc_calls = (tmp_path / "venc.args").read_text().splitlines() assert venc_calls[0].startswith("--probe ") and "--height 720" in venc_calls[0] and "--bitrate 3000000" in \ venc_calls[0]