diff --git a/config.go b/config.go index ce7d14cc..ed26986d 100644 --- a/config.go +++ b/config.go @@ -294,9 +294,19 @@ func LoadConfig() { // Until rolling logs land, do not persist verbose levels across reboots. loadedConfig.DefaultLogLevel = "WARN" - // Migrate old default EDID (Toshiba TSB, no CEA extension) to new JetKVM v1 EDID - const oldDefaultEDID = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b" - if loadedConfig.EdidString == "" || loadedConfig.EdidString == oldDefaultEDID { + // Migrate prior JetKVM defaults to the current native.DefaultEDID: + // - Toshiba TSB chip default (pre-JetKVM-v1 EDID, no CEA extension) + // - JetKVM v1 EDID without the 1280x720@120 DTD (the previous default that + // advertised only 1080p60 + 720p60 in the base block) + // - JetKVM v1 EDID with 720p120 in CTA extension only (NVIDIA didn't pick + // it up; superseded by base-block DTD1 = 720p120) + const tsbDefaultEDID = "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b" + const jkvV1NoHighRefresh = "00ffffffffffff0028b4010001eeffc0302301038047287856ee91a3544c99260f5054000000d1c081c0318001010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fd00174c0f5111000a202020202020000000fc004a65744b564d2076310a202020011d020322d1431004012309070783010000e200cfe40d100401e305000065030c001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cf" + const jkvV1CtaOnly120 = "00ffffffffffff0028b4010001eeffc0302301038047287856ee91a3544c99260f5054000000d1c081c0318001010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fd00174c0f5111000a202020202020000000fc004a65744b564d2076310a202020011d020322d1431004012309070783010000e200cfe40d100401e305000065030c001000773300a050d02b2030203500122c2100001a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001c" + if loadedConfig.EdidString == "" || + strings.EqualFold(loadedConfig.EdidString, tsbDefaultEDID) || + strings.EqualFold(loadedConfig.EdidString, jkvV1NoHighRefresh) || + strings.EqualFold(loadedConfig.EdidString, jkvV1CtaOnly120) { loadedConfig.EdidString = native.DefaultEDID } diff --git a/internal/native/cgo/video.c b/internal/native/cgo/video.c index 13f2c144..3ede30a9 100644 --- a/internal/native/cgo/video.c +++ b/internal/native/cgo/video.c @@ -165,21 +165,30 @@ static void configure_vui_video_signal(VENC_VUI_VIDEO_SIGNAL_S *video_signal, RK } } -static void populate_venc_attr(VENC_CHN_ATTR_S *stAttr, RK_U32 bitrate, RK_U32 max_bitrate, RK_U32 width, RK_U32 height) +static void populate_venc_attr(VENC_CHN_ATTR_S *stAttr, RK_U32 bitrate, RK_U32 max_bitrate, RK_U32 width, RK_U32 height, RK_U32 fps) { memset(stAttr, 0, sizeof(VENC_CHN_ATTR_S)); RK_U32 min_bitrate = bitrate / 2; if (min_bitrate < 2) min_bitrate = 2; + // GOP scales with framerate so IDR cadence stays ~0.5s regardless of source + // refresh — keeps WebRTC recovery latency bounded at 60 Hz and 120 Hz alike. + RK_U32 gop = fps > 0 ? fps / 2 : 30; + if (gop < 1) gop = 1; + if (codec_type == 1) { // H.265 (HEVC) stAttr->stRcAttr.enRcMode = VENC_RC_MODE_H265VBR; stAttr->stRcAttr.stH265Vbr.u32BitRate = bitrate; stAttr->stRcAttr.stH265Vbr.u32MaxBitRate = max_bitrate; stAttr->stRcAttr.stH265Vbr.u32MinBitRate = min_bitrate; - stAttr->stRcAttr.stH265Vbr.u32Gop = 30; + stAttr->stRcAttr.stH265Vbr.u32Gop = gop; stAttr->stRcAttr.stH265Vbr.u32StatTime = 2; + stAttr->stRcAttr.stH265Vbr.u32SrcFrameRateNum = fps; + stAttr->stRcAttr.stH265Vbr.u32SrcFrameRateDen = 1; + stAttr->stRcAttr.stH265Vbr.fr32DstFrameRateNum = fps; + stAttr->stRcAttr.stH265Vbr.fr32DstFrameRateDen = 1; stAttr->stVencAttr.enType = RK_VIDEO_ID_HEVC; stAttr->stVencAttr.u32Profile = H265E_PROFILE_MAIN; } else { @@ -188,8 +197,12 @@ static void populate_venc_attr(VENC_CHN_ATTR_S *stAttr, RK_U32 bitrate, RK_U32 m stAttr->stRcAttr.stH264Vbr.u32BitRate = bitrate; stAttr->stRcAttr.stH264Vbr.u32MaxBitRate = max_bitrate; stAttr->stRcAttr.stH264Vbr.u32MinBitRate = min_bitrate; - stAttr->stRcAttr.stH264Vbr.u32Gop = 30; + stAttr->stRcAttr.stH264Vbr.u32Gop = gop; stAttr->stRcAttr.stH264Vbr.u32StatTime = 2; + stAttr->stRcAttr.stH264Vbr.u32SrcFrameRateNum = fps; + stAttr->stRcAttr.stH264Vbr.u32SrcFrameRateDen = 1; + stAttr->stRcAttr.stH264Vbr.fr32DstFrameRateNum = fps; + stAttr->stRcAttr.stH264Vbr.fr32DstFrameRateDen = 1; stAttr->stVencAttr.enType = RK_VIDEO_ID_AVC; stAttr->stVencAttr.u32Profile = H264E_PROFILE_HIGH; } @@ -250,11 +263,11 @@ static void venc_configure_limited_range_vui(RK_U32 height) pthread_t *venc_read_thread = NULL; volatile bool venc_running = false; -static int32_t venc_start(int32_t bitrate, int32_t max_bitrate, int32_t width, int32_t height) +static int32_t venc_start(int32_t bitrate, int32_t max_bitrate, int32_t width, int32_t height, int32_t fps) { int32_t ret; VENC_CHN_ATTR_S stAttr; - populate_venc_attr(&stAttr, bitrate, max_bitrate, width, height); + populate_venc_attr(&stAttr, bitrate, max_bitrate, width, height, fps); ret = RK_MPI_VENC_CreateChn(VENC_CHANNEL, &stAttr); if (ret < 0) @@ -461,6 +474,9 @@ static void *venc_read_stream(void *arg) } uint32_t detected_width, detected_height; +// detected_fps is the rounded source vrefresh from the latest dv-timings query. +// 60 is a safe default until the first SOURCE_CHANGE event fires. +uint32_t detected_fps = 60; bool detected_signal = false, streaming_flag = false; bool streaming_stopped = true; @@ -539,6 +555,7 @@ void *run_video_stream(void *arg) uint32_t width = detected_width; uint32_t height = detected_height; + uint32_t fps = detected_fps; struct v4l2_format fmt; memset(&fmt, 0, sizeof(struct v4l2_format)); fmt.type = type; @@ -652,7 +669,7 @@ void *run_video_stream(void *arg) // Set VENC parameters int32_t bitrate = calculate_bitrate(quality_factor, width, height); - RK_S32 ret = venc_start(bitrate, bitrate * 3 / 2, width, height); + RK_S32 ret = venc_start(bitrate, bitrate * 3 / 2, width, height, fps); if (ret != RK_SUCCESS) { log_error("Set VENC parameters failed with %#x", ret); @@ -960,10 +977,19 @@ void *run_detect_format(void *arg) dv_timings.bt.hbackporch)); log_info("Frames per second: %.2f fps", frames_per_second); - bool should_restart = dv_timings.bt.width != detected_width || dv_timings.bt.height != detected_height || !detected_signal; + // Round to nearest integer for the encoder rate control. CVT-RB + // sources land a touch under (e.g. 119.91); rounding keeps the + // encoder sized for the nominal rate. + uint32_t fps_int = (uint32_t)(frames_per_second + 0.5); + if (fps_int < 1) fps_int = 1; + // Tolerate ±1 fps wobble between SOURCE_CHANGE events without + // tearing down the pipeline. + bool fps_changed = fps_int > detected_fps + 1 || fps_int + 1 < detected_fps; + bool should_restart = dv_timings.bt.width != detected_width || dv_timings.bt.height != detected_height || fps_changed || !detected_signal; detected_width = dv_timings.bt.width; detected_height = dv_timings.bt.height; + detected_fps = fps_int; detected_signal = true; video_report_format(true, NULL, detected_width, detected_height, frames_per_second); diff --git a/internal/native/video.go b/internal/native/video.go index fec90de1..af0b109e 100644 --- a/internal/native/video.go +++ b/internal/native/video.go @@ -11,7 +11,11 @@ const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mod // DefaultEDID is the default EDID for the video stream. // CEA-861 extension with HDMI vendor block, audio support, and JetKVM manufacturer ID. -const DefaultEDID = "00ffffffffffff0028b4010001eeffc0302301038047287856ee91a3544c99260f5054000000d1c081c0318001010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fd00174c0f5111000a202020202020000000fc004a65744b564d2076310a202020011d020322d1431004012309070783010000e200cfe40d100401e305000065030c001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cf" +// Base block DTDs: 1920x1080@60 (preferred, DTD0), 1280x720@120 (DTD1). 1280x720@60 +// is still advertised via the Standard Timings block (0x81C0 at offset 40). NVIDIA +// drivers ignore non-VIC DTDs in the CTA extension, so the high-refresh DTD has to +// live in the base block to be picked up by GeForce display settings. +const DefaultEDID = "00FFFFFFFFFFFF0028B4010001EEFFC0302301038047287856EE91A3544C99260F5054000000D1C081C0318001010101010101010101023A801871382D40582C4500C48E2100001E773300A050D02B2030203500122C2100001A000000FD00174C0F5111000A202020202020000000FC004A65744B564D2076310A20202001D5020322D1431004012309070783010000E200CFE40D100401E305000065030C001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000CF" var extraLockTimeout = 5 * time.Second diff --git a/ui/src/routes/devices.$id.settings.video.tsx b/ui/src/routes/devices.$id.settings.video.tsx index fe462cda..b86a40ab 100644 --- a/ui/src/routes/devices.$id.settings.video.tsx +++ b/ui/src/routes/devices.$id.settings.video.tsx @@ -13,8 +13,11 @@ import notifications from "@/notifications"; import { isLinuxDesktop } from "@/utils"; import { m } from "@localizations/messages.js"; +// JetKVM v1 default EDID. Base block DTDs: 1920x1080@60 (DTD0, preferred), +// 1280x720@120 (DTD1). 1280x720@60 still advertised via Standard Timings. +// Source switches refresh via OS display settings — no need to swap EDIDs. const defaultEdid = - "00ffffffffffff0028b4010001eeffc0302301038047287856ee91a3544c99260f5054000000d1c081c0318001010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fd00174c0f5111000a202020202020000000fc004a65744b564d2076310a202020011d020322d1431004012309070783010000e200cfe40d100401e305000065030c001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cf"; + "00FFFFFFFFFFFF0028B4010001EEFFC0302301038047287856EE91A3544C99260F5054000000D1C081C0318001010101010101010101023A801871382D40582C4500C48E2100001E773300A050D02B2030203500122C2100001A000000FD00174C0F5111000A202020202020000000FC004A65744B564D2076310A20202001D5020322D1431004012309070783010000E200CFE40D100401E305000065030C001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000CF"; const edids = [ { value: defaultEdid, @@ -110,13 +113,10 @@ export default function SettingsVideoRoute() { } const receivedEdid = resp.result as string; - const matchingEdid = edids.find(x => x.value.toLowerCase() === receivedEdid.toLowerCase()); if (matchingEdid) { - // EDID is stored in uppercase in the UI - setEdid(matchingEdid.value.toUpperCase()); - // Reset custom EDID value + setEdid(matchingEdid.value); setCustomEdidValue(null); } else { setEdid("custom"); @@ -160,6 +160,7 @@ export default function SettingsVideoRoute() { }; const handleEDIDChange = (newEdid: string) => { + const matched = edids.find(x => x.value.toLowerCase() === newEdid.toLowerCase()); setEdidLoading(true); void send("setEDID", { edid: newEdid }, (resp: JsonRpcResponse) => { setEdidLoading(false); @@ -169,14 +170,12 @@ export default function SettingsVideoRoute() { ); return; } - + setEdid(newEdid); notifications.success( m.video_edid_set_success({ - edid: edids.find(x => x.value === newEdid)?.label ?? "the custom EDID", + edid: matched?.label ?? "the custom EDID", }), ); - // Update the EDID value in the UI - setEdid(newEdid); }); }; @@ -300,6 +299,7 @@ export default function SettingsVideoRoute() { /> +