mirror of
https://github.com/spoopyghosty0/frameport.git
synced 2026-10-26 15:02:32 +01:00
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.
This commit is contained in:
1 parent
0ae754fa5d
commit
da7655ee25
6 files changed
+47
-15
No files matched your search
@@ -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
|
||||
Binary file not shown.
@@ -160,6 +160,20 @@
|
||||
- The panel's current refresh rate can be read without privileges through DRM: `/dev/dri/card0` is mode 0666, and
|
||||
GETCRTC reports e.g. `2*2160x2160_96` (clock 1402720 kHz / 4448 × 3285 = 96 Hz) even while the headset sleeps.
|
||||
The panel offers 72/80/90/96/108/120/144 Hz.
|
||||
- `/dev/video99` (v4l2loopback) has `max_buffers=2`: a reader asking for more gets 2.
|
||||
- Mid-stream keyframe requests (FORCE_KEY_FRAME) take effect on the next frame, and the GOP restarts from there.
|
||||
- Measured 2026-10-07 with the headset asleep (still picture):
|
||||
- `fp_venc` alone at 32/36 fps: 1% of a core at 1080p, 2.5% at 720p.
|
||||
- Live view end to end: `fp_venc` 2.6% + ffmpeg 8.4% (AAC encoding + muxing).
|
||||
- Conversion cost per new picture (self-test, NEON): 1.5 ms at 1080p→1080p. Box-scaling to 720p takes 5.6 ms
|
||||
(~11 ms at idle clocks), so with live content 1080p costs the CPU *less* than the scaled qualities.
|
||||
- ffmpeg with raw H.264 on a pipe:
|
||||
- `-framerate` is ignored.
|
||||
- `-fflags nobuffer` loses the first seconds of tiny frames.
|
||||
- Numbering frames from 0 (`setts`) next to pulse's wall-clock audio holds all output ~7 s.
|
||||
- What works: `-probesize 32 -analyzeduration 0 -use_wallclock_as_timestamps 1`, plus `frag_keyframe` so that a
|
||||
requested keyframe starts a fragment.
|
||||
- The default sink is SUSPENDED while nothing plays; its monitor still delivers (silent) audio.
|
||||
|
||||
## Text input
|
||||
|
||||
|
||||
+18
-8
@@ -350,6 +350,8 @@ struct enc {
|
||||
uint8_t *out_mem[ENC_BUFS], *cap_mem[ENC_BUFS];
|
||||
uint32_t out_len[ENC_BUFS], cap_len[ENC_BUFS];
|
||||
int out_free[ENC_BUFS];
|
||||
long out_pic[ENC_BUFS]; /* which picture each OUTPUT buffer holds (0 = black, -1 = none): a repeat that finds
|
||||
* its picture already in the buffer skips the conversion (standby, still scenes) */
|
||||
};
|
||||
|
||||
static int src_open(struct src *s, const char *path)
|
||||
@@ -375,8 +377,10 @@ static int src_open(struct src *s, const char *path)
|
||||
memset(&rb, 0, sizeof rb);
|
||||
rb.count = SRC_BUFS, rb.type = V4L2_BUF_TYPE_VIDEO_CAPTURE, rb.memory = V4L2_MEMORY_MMAP;
|
||||
r = sys_ioctl(s->fd, 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user