From 5b1c698fff4749fd7e03f868d36057f3c869164f Mon Sep 17 00:00:00 2001 From: Glenn Pringle Date: Mon, 20 Jul 2026 13:19:06 +1000 Subject: [PATCH] feat(playback): 60/90fps DVR playback at native rate via the live-video display path Replaces this branch's earlier UI-path retime wholesale: bench testing on both goggle generations proved the FPGA's UI input path only ever locks 1080p50, so retimed playback now enters the genuine live-video display mode and the decoded video rides the vdpo overlay plane. - ts_probe: the platform demuxer reports 0 fps for goggle TS recordings; derive the rate from video PES PTS spacing. - goggles2 (hardware-verified): 90fps -> 720p90 with the VO layer sized 1280x720, 60fps -> video-path 1080p60. - goggle v1 (hardware-verified): same modes; the UI re-renders at 1280x720 during 720p90 (like the race OSD) so the control bar is not comb-torn by the 1280-wide scanout; one receiver init per boot leaves a black raster behind the FPGA overlay chroma-key, then the receiver stays off; prewarm runs while the user browses the playback list. - boxpro: UI already runs 720p60; no-op stubs. - playback UX: control bar auto-hides after 4s and wakes on dial input; unplayable files show a message instead of a silent black screen; thumbnails read straight off the card (the /tmp staging overflowed tmpfs and degraded previews to placeholder icons). - diagnostics: in-firmware display bench (right-click during playback) used for hardware bring-up, and an SD-card transition/stall log. - TS-only on purpose: MP4 playback keeps the stock display path. --- src/driver/hardware-boxpro.c | 20 ++++ src/driver/hardware-goggle.c | 218 ++++++++++++++++++++++++++++++++++ src/driver/hardware-goggle2.c | 44 +++++++ src/driver/hardware.h | 16 +++ src/player/awdmx.c | 3 +- src/player/awdmx.h | 1 + src/player/media.c | 87 +++++++++++++- src/player/media.h | 1 + src/player/ts_probe.c | 131 ++++++++++++++++++++ src/player/ts_probe.h | 20 ++++ src/ui/page_playback.c | 16 ++- src/ui/ui_player.c | 112 +++++++++++++++++ src/util/hwlog.c | 26 ++++ src/util/hwlog.h | 6 + 14 files changed, 692 insertions(+), 9 deletions(-) create mode 100644 src/player/ts_probe.c create mode 100644 src/player/ts_probe.h create mode 100644 src/util/hwlog.c create mode 100644 src/util/hwlog.h diff --git a/src/driver/hardware-boxpro.c b/src/driver/hardware-boxpro.c index 3409a9f3b..31cf9d0c3 100644 --- a/src/driver/hardware-boxpro.c +++ b/src/driver/hardware-boxpro.c @@ -606,6 +606,26 @@ void Display_UI() { pthread_mutex_unlock(&hardware_mutex); } +// The BoxPRO UI already runs at 720p60, which suits 60 and 90 fps DVR files, +// so there are no playback display modes to switch to. +void Display_Playback_SetMode(int hz) { + (void)hz; +} + +void Display_Playback_Prewarm(void) { +} + +// No retime bench either, for the same reason. +int Display_UI_BenchNext(const char **desc) { + (void)desc; + return -1; +} + +int Display_UI_BenchRestore(const char **desc) { + (void)desc; + return -1; +} + void HDZero_open(int bw) { if (bw != g_hw_stat.hdz_bw) // reopen with different bw HDZero_Close(); diff --git a/src/driver/hardware-goggle.c b/src/driver/hardware-goggle.c index 585092fcd..9068d4cb5 100644 --- a/src/driver/hardware-goggle.c +++ b/src/driver/hardware-goggle.c @@ -3,9 +3,11 @@ #if HDZGOGGLE #include +#include #include #include #include +#include #include #include @@ -29,6 +31,7 @@ #include "tp2825.h" #include "uart.h" #include "ui/ui_porting.h" +#include "util/hwlog.h" #include "util/system.h" ///////////////////////////////////////////////////////////////////////// @@ -38,6 +41,10 @@ int fhd_req = 0; // local pthread_mutex_t hardware_mutex; +// Hardware transitions log to the shared SD-card diagnostic log +// (util/hwlog): every receiver open/close and display-mode entry. +// Un-commanded mode entries during playback are what this catches. + uint32_t vclk_phase_default[VIDEO_SOURCE_NUM] = { // 0x8d, 0x8e, 0x14, hdmi_out 0x00000006, // VIDEO_SOURCE_VERSION @@ -569,6 +576,7 @@ void hw_screen_on(int bON) { } void Display_UI_init() { + hwlog("Display_UI_init (src=%d bb=%d)", g_hw_stat.source_mode, g_hw_stat.hdzero_open); g_hw_stat.source_mode = SOURCE_MODE_UI; I2C_Write(ADDR_FPGA, 0x8C, 0x00); @@ -596,8 +604,209 @@ void Display_UI() { pthread_mutex_unlock(&hardware_mutex); } +// ---- DVR display bring-up bench (generation 2) ----------------------------- +// Generation 1 established (on goggles2 hardware) that the FPGA's UI input +// path (reg 0x20=0) only produces a picture at 1080p50 - even a bare vdpo +// switch to 1080p60 with no other change goes black. So these recipes take +// the opposite route: enter the *genuine* live-video display mode (FPGA +// video input, reg 0x20=1), which provably runs at 720p90/1080p60/720p60 +// every day, and let the playback video ride in on the vdpo overlay plane. +// The VRX contributes only its mute raster (or nothing, BB-off variants); +// overlay pixels that are pure black chroma-key through to it, which is +// why OSD widgets use 0x010101 backgrounds. Stepped from the playback +// screen with the right button; recipe 0 always restores a working screen. +void Display_720P90_t(int mode); +void Display_1080P30_t(int mode); +void Display_720P60_50_t(int mode, uint8_t is_43); + +static int bench_idx = 0; +static bool bench_opened_bb = false; + +static const char *const bench_desc[] = { + "stock 1080p50 UI", + "video path 720p90", + "video path 720p90 +BB", + "video path 1080p60", + "video path 1080p60 +BB", + "video path 720p60 +BB", + "control 0x11: bg MUST be GREEN", + "key probe 0x01: TEXT on BLACK = winner", + "key probe 0x13: TEXT on BLACK = winner", + "key probe 0x15: TEXT on BLACK = winner", + "key probe 0x19: TEXT on BLACK = winner", + "key probe 0x31: TEXT on BLACK = winner", +}; +#define BENCH_RECIPES (int)(sizeof(bench_desc) / sizeof(bench_desc[0])) + +static void bench_apply(int idx) { + bool const wants_bb = (idx == 2) || (idx == 4) || (idx == 5); + + pthread_mutex_lock(&hardware_mutex); + screen.display(0); + + Display_UI_init(); // deterministic 1080p50 baseline for every recipe + + // Close unconditionally, not just when the bench opened it: playback + // keeps the baseband warm, and a warm baseband gives the key probes a + // black raster behind everything - voiding the green/black readout. + if (!wants_bb && g_hw_stat.hdzero_open) { + HDZero_Close(); + bench_opened_bb = false; + } + if (wants_bb && !g_hw_stat.hdzero_open) { + HDZero_open(g_setting.source.hdzero_bw); + bench_opened_bb = true; + } + + switch (idx) { + case 1: + case 2: + Display_720P90_t(VR_540P90); + break; + case 3: + case 4: + Display_1080P30_t(VR_1080P30); + break; + case 5: + Display_720P60_50_t(VR_720P60, 0); + break; + case 6: + case 7: + case 8: + case 9: + case 10: + case 11: { + // Chroma-key hunt, round 3: 720p90 with the baseband FORCED off, + // so keyed-through pixels show the FPGA's green idle raster. + // Round 1 (bit0 cleared) blanked the whole overlay; round 2 was + // void - playback's warm baseband gave every probe a black raster. + // Recipe 6 is the control at the stock value 0x11: it must show + // GREEN behind the text or the experiment premise is wrong. The + // probes keep bit0 (overlay enable) and toggle one bit each; the + // winner shows the text on a BLACK background. + static const uint8_t osd_probe[] = {0x11, 0x01, 0x13, 0x15, 0x19, 0x31}; + Display_720P90_t(VR_540P90); + I2C_Write(ADDR_FPGA, 0x84, osd_probe[idx - 6]); + break; + } + default: + break; // recipe 0: the Display_UI_init baseline is the recipe + } + + screen.display(1); + pthread_mutex_unlock(&hardware_mutex); + + hwlog("bench recipe %d: %s", idx, bench_desc[idx]); + LOGI("bench recipe %d: %s", idx, bench_desc[idx]); + beep_dur(idx ? BEEP_SHORT : BEEP_LONG); // long beep = back on stock timing +} + +int Display_UI_BenchNext(const char **desc) { + bench_idx = (bench_idx + 1) % BENCH_RECIPES; + bench_apply(bench_idx); + *desc = bench_desc[bench_idx]; + return bench_idx; +} + +int Display_UI_BenchRestore(const char **desc) { + *desc = bench_desc[0]; + if (bench_idx != 0) { + bench_idx = 0; + bench_apply(0); + } + return 0; +} + +// The receiver contributes nothing to playback - it only blackens the +// raster the FPGA chroma-key punches through to, and the bench control +// test proved the raster STAYS black after the receiver has run once +// since boot and been shut down again. So: initialize once, close +// immediately, and play back with the receiver off. Runs in the +// background while the user is still on the playback list; SetMode +// falls back to doing it synchronously BEFORE blanking the panel. +static bool playback_raster_black = false; + +static void playback_blacken_raster(void) { // hardware_mutex held + if (playback_raster_black) + return; + if (g_hw_stat.hdzero_open) { + hwlog("raster already black - receiver ran this boot"); + playback_raster_black = true; + return; + } + HDZero_open(g_setting.source.hdzero_bw); + HDZero_Close(); + playback_raster_black = true; + hwlog("raster blackened - receiver back off"); +} + +static void *playback_prewarm_thread(void *arg) { + (void)arg; + pthread_mutex_lock(&hardware_mutex); + hwlog("prewarm start (src=%d bb=%d)", g_hw_stat.source_mode, g_hw_stat.hdzero_open); + if (g_hw_stat.source_mode == SOURCE_MODE_UI) + playback_blacken_raster(); + hwlog("prewarm done"); + pthread_mutex_unlock(&hardware_mutex); + return NULL; +} + +void Display_Playback_Prewarm(void) { + hwlog("Prewarm called (raster_black=%d bb=%d)", playback_raster_black, g_hw_stat.hdzero_open); + if (playback_raster_black) + return; + pthread_t tid; + if (pthread_create(&tid, NULL, playback_prewarm_thread, NULL) == 0) + pthread_detach(tid); +} + +void Display_Playback_SetMode(int hz) { + struct timespec t0, t1; + clock_gettime(CLOCK_MONOTONIC, &t0); + pthread_mutex_lock(&hardware_mutex); + hwlog("Playback_SetMode %dHz start (src=%d bb=%d)", hz, g_hw_stat.source_mode, g_hw_stat.hdzero_open); + + if (hz) { + // The G1 FPGA idles its video input on a green raster, and the + // overlay chroma-key punches near-black pixels through to it - + // dark video areas glow green (goggles2 idles black, so this + // never showed there). One receiver init since boot leaves the + // raster black for good; only runs here if the prewarm missed, + // and before blanking so any wait shows the menu, not black. + playback_blacken_raster(); + } + + screen.display(0); + + if (hz) { + // Switch straight from the menu timing - the same transition the + // live race path makes daily - no UI-baseline reset first. + if (hz == 90) + Display_720P90_t(VR_540P90); + else + Display_1080P30_t(VR_1080P30); + // The race functions mark source_mode HDZERO, which makes the + // hardware monitor thread treat playback as live race mode - with + // the baseband running it may see camera-mode telemetry and + // reprogram the display mid-video. Mark the source back as UI so + // the monitor leaves playback alone (M0 and the mute raster are + // already up and unaffected). + g_hw_stat.source_mode = SOURCE_MODE_UI; + } else { + Display_UI_init(); + } + + screen.display(1); + clock_gettime(CLOCK_MONOTONIC, &t1); + hwlog("Playback_SetMode %dHz done in %ldms", hz, + (t1.tv_sec - t0.tv_sec) * 1000 + (t1.tv_nsec - t0.tv_nsec) / 1000000L); + pthread_mutex_unlock(&hardware_mutex); + LOGI("Display_Playback_SetMode: %dHz", hz ? hz : 50); +} + void Display_720P60_50_t(int mode, uint8_t is_43) // fps: 0=50, 1=60 { + hwlog("Display_720P60_50_t mode=%d 43=%d (src=%d bb=%d)", mode, is_43, g_hw_stat.source_mode, g_hw_stat.hdzero_open); screen.display(0); I2C_Write(ADDR_FPGA, 0x8C, 0x00); @@ -625,6 +834,7 @@ void Display_720P60_50_t(int mode, uint8_t is_43) // fps: 0=50, 1=60 } void Display_720P90_t(int mode) { + hwlog("Display_720P90_t mode=%d (src=%d bb=%d)", mode, g_hw_stat.source_mode, g_hw_stat.hdzero_open); screen.display(0); I2C_Write(ADDR_FPGA, 0x8C, 0x00); @@ -647,6 +857,7 @@ void Display_720P90_t(int mode) { } void Display_1080P30_t(int mode) { + hwlog("Display_1080P30_t mode=%d (src=%d bb=%d)", mode, g_hw_stat.source_mode, g_hw_stat.hdzero_open); screen.display(0); I2C_Write(ADDR_FPGA, 0x8C, 0x00); @@ -671,6 +882,7 @@ void Display_1080P30_t(int mode) { } void Display_1080P24_t(int mode) { + hwlog("Display_1080P24_t mode=%d (src=%d bb=%d)", mode, g_hw_stat.source_mode, g_hw_stat.hdzero_open); screen.display(0); I2C_Write(ADDR_FPGA, 0x8C, 0x00); @@ -723,16 +935,22 @@ void HDZero_open(int bw) { HDZero_Close(); if (g_hw_stat.hdzero_open == 0) { + struct timespec t0, t1; + clock_gettime(CLOCK_MONOTONIC, &t0); g_hw_stat.hdz_bw = bw; DM5680_SetBR(g_hw_stat.hdz_bw); DM6302_init(0, g_hw_stat.hdz_bw); DM5680_SetBB(1); g_hw_stat.hdzero_open = 1; + clock_gettime(CLOCK_MONOTONIC, &t1); + hwlog("HDZero_open bw=%d took %ldms", bw, + (t1.tv_sec - t0.tv_sec) * 1000 + (t1.tv_nsec - t0.tv_nsec) / 1000000L); LOGI("HDZero: open"); } } void HDZero_Close() { + hwlog("HDZero_Close"); DM5680_SetBB(0); DM5680_ResetRF(0); g_hw_stat.hdzero_open = 0; diff --git a/src/driver/hardware-goggle2.c b/src/driver/hardware-goggle2.c index 9a80ed116..dacd9cc83 100755 --- a/src/driver/hardware-goggle2.c +++ b/src/driver/hardware-goggle2.c @@ -569,6 +569,50 @@ void Display_UI() { pthread_mutex_unlock(&hardware_mutex); } +// The DVR playback display bench (see hardware-goggle.c) served its purpose +// here: goggles2 findings are hardware-verified and shipped as +// Display_Playback_SetMode below. Key facts, so nobody re-treads the dead +// end: the FPGA's UI input path (reg 0x20=0) only ever locks 1080p50 - even +// a bare vdpo switch to 1080p60 goes black regardless of FPGA/MFPGA/vtmg +// configuration. The live-video path (reg 0x20=1) is the way: the decoded +// video rides the vdpo overlay plane over the VRX mute raster (pure black +// 0x000000 chroma-keys through, which is why OSD widgets use 0x010101). +int Display_UI_BenchNext(const char **desc) { + (void)desc; + return -1; +} + +int Display_UI_BenchRestore(const char **desc) { + (void)desc; + return -1; +} + +void Display_720P90_t(int mode); +void Display_1080P30_t(int mode); + +// goggles2 needs no baseband for playback - nothing to warm up. +void Display_Playback_Prewarm(void) { +} + +// Playback display modes, hardware-verified (9.5.11 bench, 9.5.12 field): +// 1080p60 pixel-perfect; 720p90 needs the VO layer sized 1280x720 and shows +// the top-left 1280x720 of the UI layout. Baseband stays off - the VRX mute +// raster behind the vdpo overlay is all the video input the FPGA needs. +void Display_Playback_SetMode(int hz) { + pthread_mutex_lock(&hardware_mutex); + screen.display(0); + + Display_UI_init(); + if (hz == 90) + Display_720P90_t(VR_540P90); + else if (hz == 60) + Display_1080P30_t(VR_1080P30); + + screen.display(1); + pthread_mutex_unlock(&hardware_mutex); + LOGI("Display_Playback_SetMode: %dHz", hz ? hz : 50); +} + void Display_720P60_50_t(int mode, uint8_t is_43) // fps: 0=50, 1=60 { screen.display(0); diff --git a/src/driver/hardware.h b/src/driver/hardware.h index b232a713c..db72b77db 100644 --- a/src/driver/hardware.h +++ b/src/driver/hardware.h @@ -90,6 +90,22 @@ void Source_AV(bool is_av_in); void Display_UI_init(); void Display_UI(); +// DVR playback display mode, bench-verified on goggles2 hardware: the live +// video path (FPGA video input) displays the vdpo overlay plane - which +// carries the decoded video - at 1080p60 and 720p90; the UI path only ever +// locks 1080p50. 90 = video-path 720p90 (caller must size the VO layer +// 1280x720), 60 = video-path 1080p60, 0 = restore the stock menu UI. +void Display_Playback_SetMode(int hz); +void Display_Playback_Prewarm(void); // background bring-up of anything SetMode would block on; call when the playback page opens + +// DVR display bring-up bench: step through candidate display-mode recipes +// on live hardware (right-button during playback). Returns the applied +// recipe index and points *desc at a static description, or -1 on targets +// without a bench. Restore puts the stock UI timing back; it is a no-op +// when the bench is already on recipe 0. +int Display_UI_BenchNext(const char **desc); +int Display_UI_BenchRestore(const char **desc); + void Display_720P90(int mode); void Display_720P60_50(int mode, uint8_t is_43); void Display_1080P30(int mode); diff --git a/src/player/awdmx.c b/src/player/awdmx.c index 5d47f1e5c..4a37ddeff 100644 --- a/src/player/awdmx.c +++ b/src/player/awdmx.c @@ -181,8 +181,9 @@ AwdmxContext_t *awdmx_open(char *sFile, CB_onDmxEof cbOnEof, void *context) { dmxCtx->width = DemuxMediaInfo.mVideoStreamInfo[nIndex].mWidth; dmxCtx->height = DemuxMediaInfo.mVideoStreamInfo[nIndex].mHeight; dmxCtx->codecType = DemuxMediaInfo.mVideoStreamInfo[nIndex].mCodecType; + dmxCtx->fpsX1000 = DemuxMediaInfo.mVideoStreamInfo[nIndex].mFrameRate; dmxCtx->msDuration = DemuxMediaInfo.mDuration; - LOGD("stream info %dx%d", DemuxMediaInfo.mVideoStreamInfo[nIndex].mWidth, DemuxMediaInfo.mVideoStreamInfo[nIndex].mHeight); + LOGD("stream info %dx%d @%dmfps", DemuxMediaInfo.mVideoStreamInfo[nIndex].mWidth, DemuxMediaInfo.mVideoStreamInfo[nIndex].mHeight, dmxCtx->fpsX1000); if (DemuxMediaInfo.mAudioNum > 0) { nIndex = DemuxMediaInfo.mAudioIndex; diff --git a/src/player/awdmx.h b/src/player/awdmx.h index d9ab4c4c2..038d469ae 100644 --- a/src/player/awdmx.h +++ b/src/player/awdmx.h @@ -30,6 +30,7 @@ typedef struct int videoNum; uint16_t width; uint16_t height; + int fpsX1000; // video frame rate * 1000, 0 if unknown PAYLOAD_TYPE_E codecType; int audioNum; diff --git a/src/player/media.c b/src/player/media.c index 59abb2ee4..391b50b3c 100644 --- a/src/player/media.c +++ b/src/player/media.c @@ -1,5 +1,9 @@ #include "media.h" +#include "ts_probe.h" + +#include "util/hwlog.h" + #include #include #include @@ -8,6 +12,7 @@ #include #include #include +#include #include #include @@ -17,6 +22,7 @@ #include "adec2ao.h" #include "awdmx.h" +#include "driver/hardware.h" #include "gogglemsg.h" #include "vdec2vo.h" #include "version.h" @@ -66,6 +72,7 @@ typedef struct pthread_mutex_t mutex; int playingTime; // ms + int retimedHz; // nonzero while the UI output is retimed for this file } PlayContext_t; static int play_start(PlayContext_t *playCtx) { @@ -148,6 +155,12 @@ void *thread_media(void *params) { media_t *media = (media_t *)params; PlayContext_t *playCtx = (PlayContext_t *)media->context; notify_cb_t notify = media->notify; + // Stall detector: the hardware log proved the display/receiver stay + // silent during playback, so any freeze lives in the demux/decoder + // pipeline. Catch the video clock standing still and log when and + // for how long, plus what un-stuck it. + int stall_last_ms = -1; + int stall_ticks = 0; for (;;) { if (!media) break; @@ -164,6 +177,22 @@ void *thread_media(void *params) { } else { playCtx->playingTime = -2; } + + bool const rolling = (playCtx->state & PLAY_statSTARTED) && + !(playCtx->state & (PLAY_statPAUSED | PLAY_statCOMPLETED | PLAY_statSEEKING)) && + playCtx->playingTime > 0; + if (rolling && playCtx->playingTime == stall_last_ms) { + stall_ticks++; + if (stall_ticks == 5) { // ~500ms frozen: a real stall, not jitter + hwlog("playback stall at t=%dms", playCtx->playingTime); + } + } else { + if (stall_ticks >= 5) { + hwlog("playback resumed after ~%dms at t=%dms", stall_ticks * 100, playCtx->playingTime); + } + stall_ticks = 0; + } + stall_last_ms = rolling ? playCtx->playingTime : -1; pthread_mutex_unlock(&playCtx->mutex); // if(media->is_media_thread_exit) @@ -238,8 +267,50 @@ media_t *media_instantiate(char *filename, notify_cb_t notify) { goto failed; } else { Vdec2VoParams_t vvParams; + int voWidth = VO_WIDTH; + int voHeight = VO_HEIGHT; memset(&vvParams, 0, sizeof(vvParams)); +#if PLAY_HDZERO && (defined(HDZGOGGLE) || defined(HDZGOGGLE2)) + size_t const fnlen = strlen(filename); + bool const is_ts = fnlen >= 3 && strcasecmp(filename + fnlen - 3, ".ts") == 0; + int fps = (playCtx->dmx->fpsX1000 + 500) / 1000; + if (fps == 0 && is_ts) { + // The platform demuxer reports 0 fps for TS streams without + // embedded timing info - which is every goggle race + // recording; derive the frame rate from the PTS spacing. + int const probed = ts_probe_fps_x1000(filename); + if (probed > 0) { + fps = (probed + 500) / 1000; + LOGI("ts fps probe: %d mfps", probed); + } + } + LOGI("playback: %dx%d demux %d mfps -> fps %d, %s", playCtx->dmx->width, + playCtx->dmx->height, playCtx->dmx->fpsX1000, fps, is_ts ? "ts" : "not ts"); + // Play 60/90fps TS recordings through the live-video display path + // instead of the 1080p50 menu timing, which drops their frames + // unevenly. Bench-verified on goggles2 and goggle (v1) hardware: + // the FPGA's UI path only ever locks 1080p50, but the video path + // (FPGA video input, decoded video riding the vdpo overlay plane + // over the VRX mute raster) displays 1080p60 pixel-perfectly and + // locks 720p90. The 720p90 mode scans out a 1280x720 raster, so + // the VO layer must be sized to match - and 90fps recordings are + // the 540p/720p race modes by definition, so a misdetected 1080p + // file must not land there. TS-only: MP4s have a black-screen + // history of their own and stay on the stock path. + if (is_ts && fps >= 80 && playCtx->dmx->height <= 720) { + playCtx->retimedHz = 90; + voWidth = 1280; + voHeight = 720; + } else if (is_ts && fps >= 55) { + playCtx->retimedHz = 60; + } + if (playCtx->retimedHz) { + LOGI("retiming display to %dHz for %dfps file", playCtx->retimedHz, fps); + Display_Playback_SetMode(playCtx->retimedHz); + } +#endif + vvParams.initRotation = 0; vvParams.pixelFormat = MM_PIXEL_FORMAT_YVU_PLANAR_420; @@ -247,8 +318,8 @@ media_t *media_instantiate(char *filename, notify_cb_t notify) { vvParams.vdec.width = playCtx->dmx->width; vvParams.vdec.height = playCtx->dmx->height; - vvParams.vo.width = VO_WIDTH; - vvParams.vo.height = VO_HEIGHT; + vvParams.vo.width = voWidth; + vvParams.vo.height = voHeight; vvParams.vo.intfType = VO_intfTYPE; vvParams.vo.intfSync = VO_intfSYNC; vvParams.vo.uiChn = VO_uiCHN; @@ -288,6 +359,9 @@ media_t *media_instantiate(char *filename, notify_cb_t notify) { adec2ao_deinitSys(playCtx->aa); vdec2vo_deinitSys(playCtx->vv); awdmx_close(playCtx->dmx); + if (playCtx->retimedHz) { + Display_Playback_SetMode(0); + } pthread_mutex_destroy(&playCtx->mutex); free(playCtx); LOGD("exit done"); @@ -295,6 +369,12 @@ media_t *media_instantiate(char *filename, notify_cb_t notify) { return NULL; } +int media_retimed_hz(media_t *media) { + if (!media) + return 0; + return ((PlayContext_t *)media->context)->retimedHz; +} + void media_exit(media_t *media) { assert(media); media->is_media_thread_exit = true; @@ -305,6 +385,9 @@ void media_exit(media_t *media) { adec2ao_deinitSys(playCtx->aa); vdec2vo_deinitSys(playCtx->vv); awdmx_close(playCtx->dmx); + if (playCtx->retimedHz) { + Display_Playback_SetMode(0); + } pthread_mutex_destroy(&playCtx->mutex); free(media->context); free(media); diff --git a/src/player/media.h b/src/player/media.h index e713f3b1f..5d4be4561 100644 --- a/src/player/media.h +++ b/src/player/media.h @@ -39,6 +39,7 @@ typedef struct { media_t *media_instantiate(char *filename, notify_cb_t notify); void media_exit(media_t *media); void media_control(media_t *media, player_cmd_t *cmd); +int media_retimed_hz(media_t *media); // 0 = stock display timing, 60/90 = retimed for this file #ifdef __cplusplus } diff --git a/src/player/ts_probe.c b/src/player/ts_probe.c new file mode 100644 index 000000000..4bcff22ce --- /dev/null +++ b/src/player/ts_probe.c @@ -0,0 +1,131 @@ +#include "ts_probe.h" + +#include +#include +#include +#include +#include + +#define TS_PKT_SIZE 188 +#define TS_SYNC_BYTE 0x47 +#define PROBE_MAX_BYTES (8 * 1024 * 1024) // plenty for >64 video PES at any bitrate +#define PROBE_MAX_PTS 64 +#define PROBE_MAX_PIDS 8 + +typedef struct { + uint16_t pid; + int count; + uint64_t pts[PROBE_MAX_PTS]; +} pid_pts_t; + +static int cmp_u64(const void *a, const void *b) { + uint64_t x = *(const uint64_t *)a, y = *(const uint64_t *)b; + return (x > y) - (x < y); +} + +int ts_probe_fps_x1000(const char *path) { + FILE *f = fopen(path, "rb"); + if (!f) { + return 0; + } + + pid_pts_t pids[PROBE_MAX_PIDS]; + int npids = 0; + memset(pids, 0, sizeof(pids)); + + uint8_t pkt[TS_PKT_SIZE]; + long consumed = 0; + bool full = false; + + while (!full && consumed < PROBE_MAX_BYTES && fread(pkt, 1, TS_PKT_SIZE, f) == TS_PKT_SIZE) { + consumed += TS_PKT_SIZE; + + if (pkt[0] != TS_SYNC_BYTE) { + // resync: scan forward one byte at a time + fseek(f, 1 - TS_PKT_SIZE, SEEK_CUR); + continue; + } + + bool const pusi = pkt[1] & 0x40; + if (!pusi) { + continue; + } + uint16_t const pid = ((pkt[1] & 0x1F) << 8) | pkt[2]; + uint8_t const afc = (pkt[3] >> 4) & 0x3; + int off = 4; + if (afc & 0x2) { // adaptation field present + off += 1 + pkt[4]; + } + if (!(afc & 0x1) || off + 14 > TS_PKT_SIZE) { + continue; + } + + // PES start: 00 00 01 , video stream ids are 0xE0-0xEF + const uint8_t *pes = pkt + off; + if (pes[0] != 0x00 || pes[1] != 0x00 || pes[2] != 0x01 || (pes[3] & 0xF0) != 0xE0) { + continue; + } + if ((pes[7] & 0x80) == 0) { // no PTS + continue; + } + uint64_t const pts = ((uint64_t)((pes[9] >> 1) & 0x07) << 30) | + ((uint64_t)pes[10] << 22) | + ((uint64_t)(pes[11] >> 1) << 15) | + ((uint64_t)pes[12] << 7) | + ((uint64_t)pes[13] >> 1); + + pid_pts_t *slot = NULL; + for (int i = 0; i < npids; ++i) { + if (pids[i].pid == pid) { + slot = &pids[i]; + break; + } + } + if (!slot && npids < PROBE_MAX_PIDS) { + slot = &pids[npids++]; + slot->pid = pid; + } + if (slot && slot->count < PROBE_MAX_PTS) { + slot->pts[slot->count++] = pts; + if (slot->count == PROBE_MAX_PTS) { + full = true; + } + } + } + fclose(f); + + // The video stream is the PID with the most PES starts + pid_pts_t *best = NULL; + for (int i = 0; i < npids; ++i) { + if (!best || pids[i].count > best->count) { + best = &pids[i]; + } + } + if (!best || best->count < 8) { + return 0; + } + + // PES packets arrive in decode order; with B-frames the PTS jumps + // around, so sort into presentation order first, then take the median + // of successive deltas (90kHz ticks) - robust against discontinuities. + qsort(best->pts, best->count, sizeof(best->pts[0]), cmp_u64); + uint64_t deltas[PROBE_MAX_PTS]; + int ndeltas = 0; + for (int i = 1; i < best->count; ++i) { + uint64_t const d = best->pts[i] - best->pts[i - 1]; + if (d > 0 && d < 90000) { // ignore duplicates and >1s jumps + deltas[ndeltas++] = d; + } + } + if (ndeltas < 4) { + return 0; + } + qsort(deltas, ndeltas, sizeof(deltas[0]), cmp_u64); + uint64_t const med = deltas[ndeltas / 2]; + + int const fps_x1000 = (int)((90000ULL * 1000 + med / 2) / med); + if (fps_x1000 < 10000 || fps_x1000 > 200000) { // sane range 10..200 fps + return 0; + } + return fps_x1000; +} diff --git a/src/player/ts_probe.h b/src/player/ts_probe.h new file mode 100644 index 000000000..9ff6414c5 --- /dev/null +++ b/src/player/ts_probe.h @@ -0,0 +1,20 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * Derive the video frame rate of an MPEG-TS file from the PTS spacing of + * its video PES packets. The platform demuxer reports 0 fps for TS files + * whose stream carries no timing info, which silently disables the DVR + * playback display retiming - this probe is the fallback. + * + * Returns fps * 1000 (e.g. 90000 for 90fps), or 0 if it cannot be + * determined. + */ +int ts_probe_fps_x1000(const char *path); + +#ifdef __cplusplus +} +#endif diff --git a/src/ui/page_playback.c b/src/ui/page_playback.c index f580ca7b2..19514c253 100644 --- a/src/ui/page_playback.c +++ b/src/ui/page_playback.c @@ -16,6 +16,7 @@ #include "common.hh" #include "core/app_state.h" #include "core/osd.h" +#include "driver/hardware.h" #include "lang/language.h" #include "record/record_definitions.h" #include "ui/page_common.h" @@ -153,9 +154,12 @@ static void show_pb_item(uint8_t pos, char *label, bool star) { lv_obj_set_pos(pb_ui[pos]._label, labelPosX, labelPosY); lv_obj_set_pos(pb_ui[pos]._arrow, labelPosX - lv_obj_get_width(pb_ui[pos]._arrow) - 5, labelPosY); - snprintf(fname, sizeof(fname), "%s/%s." REC_packJPG, TMP_DIR, label); + // read the preview straight off the card: staging every thumbnail in + // /tmp overflows its small tmpfs once the card holds a few dozen + // recordings, and each one past the overflow fell back to the reel icon + snprintf(fname, sizeof(fname), "%s%s." REC_packJPG, MEDIA_FILES_DIR, label); if (fs_file_exists(fname)) - snprintf(fname, sizeof(fname), "A:%s/%s." REC_packJPG, TMP_DIR, label); + snprintf(fname, sizeof(fname), "A:%s%s." REC_packJPG, MEDIA_FILES_DIR, label); else osd_resource_path(fname, "%s", OSD_RESOURCE_720, DEF_VIDEOICON); lv_img_set_src(pb_ui[pos]._img, fname); @@ -283,10 +287,6 @@ static int walk_sdcard() { } free(namelist); - // copy all thumbnail files to /tmp - snprintf(fname, sizeof(fname), "cp %s*." REC_packJPG " %s", MEDIA_FILES_DIR, TMP_DIR); - system_exec(fname); - return media_db.count; } @@ -427,6 +427,10 @@ static void page_playback_exit() { } static void page_playback_enter() { + // Warm the playback display path in the background while the user + // browses - by selection time the first video needs no bring-up pause. + Display_Playback_Prewarm(); + const int ret = walk_sdcard(); update_page(); diff --git a/src/ui/ui_player.c b/src/ui/ui_player.c index 232880e5b..b447657a5 100644 --- a/src/ui/ui_player.c +++ b/src/ui/ui_player.c @@ -8,10 +8,16 @@ #include "../conf/ui.h" #include "common.hh" +#include "driver/hardware.h" #include "player/media.h" #include "record/record_definitions.h" +#include "ui/ui_porting.h" #include "ui/ui_style.h" +#if defined(HDZGOGGLE) +static bool lvgl_at_720p = false; +#endif + /////////////////////////////////////////////////////////////////////////////// // locals // UI @@ -32,6 +38,40 @@ static size_t stars_timestamps_s[MAX_STARS] = { 0, }; +// Display bring-up bench readout (see Display_UI_BenchNext). The label is +// only readable when the recipe under test actually displays - which is +// exactly the signal the bench is after. +static lv_obj_t *bench_label = NULL; + +static void bench_show(int idx, const char *desc) { + char text[96]; + + if (idx < 0 || !bench_label) + return; + + snprintf(text, sizeof(text), + "Display test %d: %s\nright click = next, hold = restore", idx, desc); + lv_label_set_text(bench_label, text); + lv_obj_clear_flag(bench_label, LV_OBJ_FLAG_HIDDEN); +} + +// Auto-hide for the control bar: gone after a few quiet seconds, back on +// any dial activity. +#define BAR_HIDE_MS 4000 +static lv_timer_t *bar_hide_timer = NULL; + +static void bar_hide_cb(lv_timer_t *timer) { + (void)timer; + if (controller.enable) + lv_obj_add_flag(controller.bar, LV_OBJ_FLAG_HIDDEN); +} + +static void bar_show(void) { + lv_obj_clear_flag(controller.bar, LV_OBJ_FLAG_HIDDEN); + if (bar_hide_timer) + lv_timer_reset(bar_hide_timer); +} + /////////////////////////////////////////////////////////////////////////////// static void time2str(uint32_t t1, uint32_t t2, char *s) { int m1, s1, m2, s2; @@ -172,10 +212,21 @@ static void init_mplayer() { controller.enable = true; controller.is_playing = true; controller.value = controller.range = 0; + + bench_label = lv_label_create(controller.bg); + lv_label_set_text(bench_label, ""); + lv_obj_set_style_text_font(bench_label, &lv_font_montserrat_26, 0); + lv_obj_set_style_text_color(bench_label, lv_color_make(255, 255, 255), 0); + lv_obj_set_style_bg_color(bench_label, lv_color_make(0, 0, 0), 0); + lv_obj_set_style_bg_opa(bench_label, LV_OPA_70, 0); + lv_obj_set_style_pad_all(bench_label, 8, 0); + lv_obj_set_pos(bench_label, 20, 20); + lv_obj_add_flag(bench_label, LV_OBJ_FLAG_HIDDEN); } static void free_mplayer() { controller.enable = false; + bench_label = NULL; // child of controller.bg, freed with it lv_obj_del(controller._btn); lv_obj_del(controller._label); lv_obj_del(controller._slider); @@ -206,6 +257,7 @@ uint8_t mplayer_on_key(uint8_t key) { return 0; LOGI("mplayer_on_key: %d", key); + bar_show(); switch (key) { case DIAL_KEY_PRESS: @@ -236,6 +288,18 @@ uint8_t mplayer_on_key(uint8_t key) { media_seek(controller.value); break; + + case RIGHT_KEY_CLICK: { + const char *desc = NULL; + bench_show(Display_UI_BenchNext(&desc), desc); + break; + } + + case RIGHT_KEY_PRESS: { + const char *desc = NULL; + bench_show(Display_UI_BenchRestore(&desc), desc); + break; + } } update_mplayer(); @@ -286,10 +350,23 @@ void load_stars(char *fname) { } } +static lv_obj_t *play_error_label = NULL; + void media_init(char *fname) { media = media_instantiate(fname, notify_cb); if (!media) { perror("media_instantiate failed."); + // an honest message beats a silent black screen + play_error_label = lv_label_create(lv_scr_act()); + lv_label_set_text(play_error_label, + "Cannot play this file on the goggles.\n" + "The recording itself is fine - copy it to a PC to watch it.\n" + "Record in TS format for on-goggle playback.\n\n" + "Long-press the Enter button to exit"); + lv_obj_set_style_text_font(play_error_label, &lv_font_montserrat_26, 0); + lv_obj_set_style_text_align(play_error_label, LV_TEXT_ALIGN_CENTER, 0); + lv_obj_set_style_text_color(play_error_label, lv_color_make(255, 64, 64), 0); + lv_obj_center(play_error_label); return; } } @@ -346,16 +423,51 @@ void mplayer_file(char *fname) { load_stars(fname); init_mplayer(); media_init(fname); + int const retimed = media_retimed_hz(media); + if (retimed == 90) { +#if defined(HDZGOGGLE) + // The G1 DE wraps a 1920-stride framebuffer at this mode's + // 1280-wide scanout - the control bar rendered comb-torn and + // doubled. Re-render the UI at 1280x720 exactly like the race + // OSD does in this display mode (app_state does the same when + // entering live 720p90). goggles2 crops the 1920 layout cleanly + // and stays as field-verified. + lvgl_switch_to_720p(); + lvgl_at_720p = true; +#endif + // the 720p90 display mode scans out the top-left 1280x720 of the + // layout; move the control bar up into the visible window + lv_obj_set_pos(controller.bar, (1280 - UI_MPLAYER_CB_WIDTH) >> 1, 720 - 160); + } media_start(); update_mplayer(); + bar_hide_timer = lv_timer_create(bar_hide_cb, BAR_HIDE_MS, NULL); } void mplayer_exit() { + if (bar_hide_timer) { + lv_timer_del(bar_hide_timer); + bar_hide_timer = NULL; + } + // never leave a bench display mode running outside the player + const char *desc = NULL; + Display_UI_BenchRestore(&desc); + + if (play_error_label) { + lv_obj_del(play_error_label); + play_error_label = NULL; + } pthread_mutex_unlock(&lvgl_mutex); if (media) { media_exit(media); media = NULL; } pthread_mutex_lock(&lvgl_mutex); +#if defined(HDZGOGGLE) + if (lvgl_at_720p) { + lvgl_switch_to_1080p(); + lvgl_at_720p = false; + } +#endif free_mplayer(); } diff --git a/src/util/hwlog.c b/src/util/hwlog.c new file mode 100644 index 000000000..8b257c2aa --- /dev/null +++ b/src/util/hwlog.c @@ -0,0 +1,26 @@ +#include "hwlog.h" + +#include +#include +#include +#include + +void hwlog(const char *fmt, ...) { + FILE *f = fopen("/mnt/extsd/hwlog.txt", "a"); + if (!f) + return; + + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + fprintf(f, "[%6ld.%03ld] ", (long)ts.tv_sec, ts.tv_nsec / 1000000L); + + va_list ap; + va_start(ap, fmt); + vfprintf(f, fmt, ap); + va_end(ap); + + fputc('\n', f); + fflush(f); + fsync(fileno(f)); + fclose(f); +} diff --git a/src/util/hwlog.h b/src/util/hwlog.h new file mode 100644 index 000000000..4771ac890 --- /dev/null +++ b/src/util/hwlog.h @@ -0,0 +1,6 @@ +#pragma once + +// Append a line to the diagnostic log on the SD card (/mnt/extsd/hwlog.txt), +// stamped with seconds since boot and synced immediately. Silently does +// nothing when the card is absent. +void hwlog(const char *fmt, ...);