mirror of
https://github.com/jetkvm/kvm.git
synced 2026-05-21 05:20:35 +00:00
51e7a95f19
* feat(video): experimental 120 Hz low-latency mode Add an opt-in setting that swaps the JetKVM default EDID for one advertising 848x480@120 (preferred) and 1280x720@120. At 120 fps the per-frame display→encode delay drops from ~16.7 ms to ~8.3 ms, halving source-side video latency vs the standard 1080p60 path. The TC358743 capture chip on JetKVM v1 has a hard ~120 Hz vrefresh ceiling (Toshiba spec is 1080p@60; everything above 60 Hz is undocumented territory). 144/240 Hz were tested and do not lock — the chip's internal blocks above the TMDS PHY were never validated past 60 Hz. 120 Hz works reliably across both 480p and 720p; that's what this EDID advertises. Wiring: - internal/native/video.go: new LowLatency120HzEDID constant (CVT-RB, EDID 1.4, single base block, no CEA extension) - config.go: VideoLowLatencyMode bool, with reconciliation on load — toggling only swaps EdidString when it currently holds one of the well-known JetKVM defaults; user-supplied custom EDIDs are preserved - jsonrpc.go: getVideoLowLatencyMode / setVideoLowLatencyMode RPCs - UI: experimental Checkbox in Settings → Video and an extra entry in the EDID preset dropdown Source-side note: enabling the toggle does not switch the source PC's display mode. The user must manually pick 1280x720@120 or 848x480@120 in their OS display settings; the EDID alone just tells the source what's allowed. scripts/edid_gen.py is the generator used to produce the EDID hex. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(video): address PR review on 120 Hz toggle Single source of truth for the toggle is now `EdidString`. The separate `VideoLowLatencyMode` config bool is gone, along with the load-time reconciliation logic that could silently revert the EDID dropdown's choice on reboot. Addresses: - cursor[bot] HIGH "Config reconciliation reverts explicit EDID dropdown choices on reboot" — drop `VideoLowLatencyMode` field; `rpcGetVideoLowLatencyMode` now derives state from `EdidString`, `rpcSetVideoLowLatencyMode` only writes `EdidString`. UI toggle is derived from `edid` state. Dropdown ↔ toggle can no longer drift. - Copilot ui/src/routes/.../video.tsx:30 — same root cause; same fix. - Copilot ui/src/routes/.../video.tsx:199 — drop the spurious `.toUpperCase()` on the matched-EDID value so SelectMenuBasic strict equality matches the option's actual `value`. - Copilot jsonrpc.go:276 + config.go:311 — case-insensitive EDID comparisons via `strings.EqualFold`. - Copilot scripts/edid_gen.py:13 — drop unused `import struct`. - Copilot scripts/edid_gen.py:63 — `pclk_khz` was holding Hz; rename to `pclk_hz` and use a `raw_pclk_hz` intermediate for the pre-quantized value. Generator output is byte-identical. - cursor[bot] LOW en.json:1041 "wrong advice when disabling" — split the single `video_low_latency_set_success` (which always told users to switch to 120 Hz) into `video_low_latency_enabled` and `video_low_latency_disabled`; the disabled message tells the user to switch their source back to its usual resolution. Also: small UX cleanup — extracted `applyEDID(...)` helper so the toggle and the dropdown don't double-fire success notifications. Verified locally: `go vet` clean, `tsc --noEmit` clean, `oxlint` 0 errors, `python3 -c "import py_compile; py_compile.compile(...)"` OK, and the regenerated EDID hex matches `LowLatency120HzEDID` byte for byte. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(video): replace 120 Hz toggle with four single-mode EDIDs in the dropdown Drops the experimental "Low Latency 120 Hz Mode" toggle and the LowLatency120HzEDID bundle (which packed both 848x480@120 and 1280x720@120 DTDs into a single EDID) in favor of four standalone single-mode EDIDs added directly to the existing EDID dropdown: - JetKVM 1280x720 @ 120 Hz (low latency) - JetKVM 1280x720 @ 60 Hz - JetKVM 848x480 @ 120 Hz (low latency) - JetKVM 848x480 @ 60 Hz Each EDID advertises exactly one DTD, the monitor range descriptor, and the model name — no CEA extension. Generated by scripts/edid_gen.py. Why the redesign The toggle was special-casing a single EDID bundle and trying to keep two pieces of state (the toggle and the EDID dropdown) in agreement. The reviewer flagged that the load-time reconciliation could revert explicit EDID-dropdown choices on reboot, and even the simpler derive-toggle-state-from-EdidString version was carrying: - a separate i18n string set for toggle-on / toggle-off, - a Checkbox plus an extra dropdown row labeling the same EDID, - special-cased applyEDID plumbing distinct from setEDID, - a config-load reconciliation path. Treating the 120 Hz modes as ordinary EDID choices removes all of that. Picking a 120 Hz EDID writes through setEDID like every other entry; the dropdown is the only source of truth. Bundling 480p120 and 720p120 into one EDID also forced the source PC to choose between two preferred modes. With separate EDIDs the source sees exactly one preferred timing per choice. Changes internal/native/video.go: add EDID720p120, EDID720p60, EDID480p120, EDID480p60. Drop LowLatency120HzEDID. jsonrpc.go: drop rpcGetVideoLowLatencyMode / rpcSetVideoLowLatencyMode and their handler registrations. Drop the now-unused native and strings imports. ui/src/routes/devices.\$id.settings.video.tsx: drop the Checkbox, the derived lowLatencyMode flag, the handleLowLatencyChange handler, the applyEDID helper, and the warning paragraph. Add four new entries to the EDID dropdown. ui/localization/messages/en.json: drop the five video_low_latency_* keys and the single-bundle video_edid_jetkvm_120hz key. Add four new per-mode keys; update video_edid_jetkvm_default to spell out the resolution so the dropdown is internally consistent. Verified locally: go vet clean on all packages, tsc --noEmit clean, oxlint 0 errors, EDID hex round-trips byte-for-byte through scripts/edid_gen.py. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Drop unused EDID constants; fix CVT-RB v1 vertical timing in edid_gen.py internal/native/video.go: the four EDID720p120 / EDID720p60 / EDID480p120 / EDID480p60 constants were never read — the dropdown carries the hex inline. Remove them; nothing else in the Go side referenced them. scripts/edid_gen.py: CVT-RB v1 specifies a fixed 3-line vertical front porch and an aspect-dependent vsync; the back porch absorbs the remainder of v_blank. The previous implementation had it backwards (back porch fixed at RB_V_BACK_PORCH, front porch = remainder), which produced a multi-hundred-line front porch and a 6-line back porch — technically a valid frame, but not CVT-RB v1. If the recomputed back porch falls below the spec minimum, bump v_blank so it does, and let v_total follow. ui/src/routes/devices.\$id.settings.video.tsx: regenerate the three affected EDID hex strings (720p120, 720p60, 480p120). 480p60 is unchanged because its v_blank already left front porch = 3 in the old code path. All four still lock end-to-end on TC358743 hardware. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Set sRGB color space + 640x480@60 established-timing in generated EDIDs scripts/edid_gen.py: - Feature-support byte (offset 24): 0x0A -> 0x0E. Adds the sRGB color-space bit while keeping the existing flags (digital, RGB 4:4:4, YCbCr 4:2:2, preferred timing in DTD0). EDID 1.4 already requires DTD0 to be the preferred timing; tagging sRGB lets the source treat the chromaticity block as authoritative instead of guessing. - Established-timings byte (offset 35): 0x00 -> 0x20. Bit 5 advertises 640x480@60 as a VGA-fallback mode some sources fall back to during early-boot / BIOS. The source still prefers DTD0 for the active desktop, so this is harmless for the 120 Hz / 60 Hz advertised modes. ui/src/routes/devices.\$id.settings.video.tsx: regenerate all four dropdown EDIDs with the new flag bytes and recomputed checksums. Strings are now uppercase to match the rest of the dropdown. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(video): plumb source vrefresh into MPP encoder rate control The MPP H.264/H.265 rate-control structure has Src/Dst FrameRateNum/Den fields the firmware never set, so MPP defaulted to fps_fix [30/1]. That made the encoder size its bitrate budget for 30 fps and behave unpredictably when 120 fps arrived from the capture chip. Now run_detect_format rounds the v4l2 dv-timings vrefresh to an integer (stored in detected_fps), and run_video_stream passes that value through venc_start -> populate_venc_attr where it's written into both Src and Dst FrameRate fields. GOP is sized to fps/2, which keeps the IDR cadence at ~0.5s for any source refresh — same WebRTC recovery latency at 60 Hz and 120 Hz. run_detect_format now also restarts the streaming pipeline when the rounded fps changes by more than ±1 fps, so an EDID swap that keeps resolution but changes refresh (e.g. 720p60 -> 720p120) actually reconfigures the encoder. The ±1 tolerance absorbs CVT-RB rounding (119.91 fps and 119.87 fps both round to 120). Verified on device: before: fps fix [30/1] -> fix [30/1] gop i [30] after: fps fix [120/1] -> fix [120/1] gop i [60] WebRTC inbound-rtp framesPerSecond now sustains ~120 with 0 dropped packets at 720p120, 19 ms jitter buffer, glass-to-glass delay halved on a 120 Hz panel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Collapse 4 single-mode EDIDs into one combined 720p entry Drop the four single-mode 480p/720p × 60/120 EDIDs and replace them with one multi-mode JetKVM 720p EDID that advertises both 1280x720@120 (DTD0, preferred) and 1280x720@60 (DTD1). Drop 480p entirely — 848x480 is non-standard and the source PC's display panel UI usually doesn't expose it. The source picks 60 Hz vs 120 Hz via OS-side display settings (`xrandr --rate 60/120` on Linux, Display Settings on Windows) without needing to swap EDIDs. The encoder-fps plumbing fromfa36843already reconfigures MPP on each v4l2 source-change event, so rate swaps work end-to-end against this single EDID. Validated at edidcraft.com: 0 errors / 0 warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fold low-latency 720p modes into the JetKVM default EDID The JetKVM v1 default EDID had two empty CTA-extension DTD slots after its existing data blocks (audio, YCbCr 4:2:2, vendor-specific). Use the first slot to advertise 1280x720@120 alongside the existing 1080p60 (DTD0, preferred) and 1280x720@60 (DTD1) base-block timings. Source picks rate via OS display settings (`xrandr --rate 120`, Windows Display Settings, etc.) — no separate "low latency" EDID needed in the dropdown. The encoder-fps plumbing fromfa36843already reconfigures MPP on every v4l2 source-change event, so OS-side rate swaps work end-to-end against this single combined EDID. Migration: config.LoadConfig now also rewrites EdidString to the new default when the user is currently on the previous JetKVM v1 EDID (without the 720p120 DTD), so existing devices auto-pick up the high-refresh option on next boot. Validated at edidcraft.com: 0 errors / 0 warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Drop scripts/edid_gen.py — exploration tool, not a build dependency The Python EDID generator was only used during this PR's development phase to probe the TC358743 chip's max vrefresh and produce candidate EDIDs. Now that the only EDID change is one DTD added to the JetKVM default (a fully-validated 18-byte hex string in internal/native/video.go and ui/src/routes/devices.\$id.settings.video.tsx), the generator is no longer referenced by any code path. Drop it from the PR to keep the diff focused on the runtime change. Available in git history if anyone wants to extend the EDID set later. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Revert "fix(video): plumb source vrefresh into MPP encoder rate control" This reverts commitfa36843. Empirically the MPP encoder forwards every input frame whether or not Src/DstFrameRateNum/Den are set in the rate control struct — those fields appear to only size the bitrate budget, not gate frame submission. With this code in, dmesg shows fps fix [120/1] -> fix [120/1] gop i [60]; with it reverted, dmesg shows fps fix [30/1] -> fix [30/1] gop i [30]; in both cases WebRTC inbound-rtp framesPerSecond sustains ~120 at the receiver. Drop the plumbing to keep the PR's surface area minimal — the EDID-side change alone is what unlocks 120 Hz end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Move 720p120 DTD from CTA extension to base block NVIDIA's display driver enumerates base-block DTDs reliably but ignores DTDs in the CTA extension that don't carry a CTA-861 VIC. 1280x720@120 isn't a CTA VIC, so the previous layout (720p120 in CTA-extension first DTD slot) silently dropped the 120 Hz mode on GeForce hosts — `xrandr` listed 1080p60 / 720p60 / 640p variants only. Swap base-block DTD1 from 720p60 to 720p120. 720p60 stays advertised through the Standard Timings block (0x81C0 at offset 40), which every driver respects. Also drop the now-redundant 720p120 DTD from the CTA extension and add the prior CTA-only EDID to the migration list so existing devices auto-upgrade on next boot. Validated at edidcraft.com: 0 errors / 0 warnings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Reapply "fix(video): plumb source vrefresh into MPP encoder rate control" This reverts commit3cb61dfc8f. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Adam Shiervani <adam.shiervani@gmail.com>
1040 lines
31 KiB
C
1040 lines
31 KiB
C
#define _POSIX_C_SOURCE 200809L
|
|
#include <unistd.h>
|
|
#include <time.h>
|
|
#include <rk_type.h>
|
|
#include <rk_mpi_venc.h>
|
|
#include <rk_mpi_sys.h>
|
|
#include <string.h>
|
|
#include <rk_debug.h>
|
|
#include <malloc.h>
|
|
#include <stdbool.h>
|
|
#include <rk_mpi_mb.h>
|
|
#include <fcntl.h>
|
|
#include <linux/videodev2.h>
|
|
#include <sys/ioctl.h>
|
|
#include <errno.h>
|
|
#include <unistd.h>
|
|
#include <stdatomic.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <rk_mpi_mmz.h>
|
|
#include <pthread.h>
|
|
#include <assert.h>
|
|
#include <sys/un.h>
|
|
#include <sys/socket.h>
|
|
#include "video.h"
|
|
#include "ctrl.h"
|
|
#include "log.h"
|
|
|
|
#define VIDEO_DEV "/dev/video0"
|
|
#define SUB_DEV "/dev/v4l-subdev2"
|
|
#define SLEEP_MODE_FILE "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
|
|
|
|
#define RK_ALIGN(x, a) (((x) + (a)-1) & ~((a)-1))
|
|
#define RK_ALIGN_2(x) RK_ALIGN(x, 2)
|
|
#define RK_ALIGN_16(x) RK_ALIGN(x, 16)
|
|
#define RK_ALIGN_32(x) RK_ALIGN(x, 32)
|
|
|
|
int sub_dev_fd = -1;
|
|
#define VENC_CHANNEL 0
|
|
MB_POOL memPool = MB_INVALID_POOLID;
|
|
|
|
bool sleep_mode_available = false;
|
|
bool should_exit = false;
|
|
float quality_factor = 1.0f;
|
|
int codec_type = 0;
|
|
|
|
static void *venc_read_stream(void *arg);
|
|
|
|
RK_U64 get_us()
|
|
{
|
|
struct timespec time = {0, 0};
|
|
clock_gettime(CLOCK_MONOTONIC, &time);
|
|
return (RK_U64)time.tv_sec * 1000000 + (RK_U64)time.tv_nsec / 1000; /* microseconds */
|
|
}
|
|
|
|
static void ensure_sleep_mode_disabled()
|
|
{
|
|
if (!sleep_mode_available)
|
|
{
|
|
return;
|
|
}
|
|
|
|
int fd = open(SLEEP_MODE_FILE, O_RDWR);
|
|
if (fd < 0)
|
|
{
|
|
log_error("Failed to open sleep mode file: %s", strerror(errno));
|
|
return;
|
|
}
|
|
lseek(fd, 0, SEEK_SET);
|
|
char buffer[1];
|
|
read(fd, buffer, 1);
|
|
if (buffer[0] == '0') {
|
|
close(fd);
|
|
return;
|
|
}
|
|
log_warn("HDMI sleep mode is not disabled, disabling it");
|
|
lseek(fd, 0, SEEK_SET);
|
|
write(fd, "0", 1);
|
|
close(fd);
|
|
|
|
usleep(1000); // give some time to the system to disable the sleep mode
|
|
return;
|
|
}
|
|
|
|
static void detect_sleep_mode()
|
|
{
|
|
if (access(SLEEP_MODE_FILE, F_OK) != 0) {
|
|
sleep_mode_available = false;
|
|
return;
|
|
}
|
|
sleep_mode_available = true;
|
|
ensure_sleep_mode_disabled();
|
|
}
|
|
|
|
double calculate_bitrate(float bitrate_factor, int width, int height)
|
|
{
|
|
const int32_t base_bitrate_high = 4000;
|
|
const int32_t base_bitrate_low = 512;
|
|
|
|
double pixels = (double)width * height;
|
|
double ref_pixels = 1920.0 * 1080.0;
|
|
|
|
double scale_factor = pixels / ref_pixels;
|
|
|
|
int32_t base_bitrate = base_bitrate_low + (int32_t)((base_bitrate_high - base_bitrate_low) * bitrate_factor);
|
|
|
|
int32_t bitrate = (int32_t)(base_bitrate * scale_factor);
|
|
|
|
const int32_t min_bitrate = 200;
|
|
if (bitrate < min_bitrate)
|
|
{
|
|
bitrate = min_bitrate;
|
|
}
|
|
|
|
return bitrate;
|
|
}
|
|
|
|
static bool is_hd_video(RK_U32 height)
|
|
{
|
|
return height > 576;
|
|
}
|
|
|
|
static COLOR_GAMUT_E color_gamut_for_height(RK_U32 height)
|
|
{
|
|
return is_hd_video(height) ? COLOR_GAMUT_BT709 : COLOR_GAMUT_BT601;
|
|
}
|
|
|
|
static void set_v4l2_yuv_colorimetry(struct v4l2_pix_format_mplane *pix, RK_U32 height)
|
|
{
|
|
if (is_hd_video(height))
|
|
{
|
|
pix->colorspace = V4L2_COLORSPACE_REC709;
|
|
pix->ycbcr_enc = V4L2_YCBCR_ENC_709;
|
|
}
|
|
else
|
|
{
|
|
pix->colorspace = V4L2_COLORSPACE_SMPTE170M;
|
|
pix->ycbcr_enc = V4L2_YCBCR_ENC_601;
|
|
}
|
|
|
|
pix->quantization = V4L2_QUANTIZATION_LIM_RANGE;
|
|
pix->xfer_func = V4L2_XFER_FUNC_709;
|
|
}
|
|
|
|
static void configure_vui_video_signal(VENC_VUI_VIDEO_SIGNAL_S *video_signal, RK_U32 height)
|
|
{
|
|
memset(video_signal, 0, sizeof(VENC_VUI_VIDEO_SIGNAL_S));
|
|
video_signal->video_signal_type_present_flag = 1;
|
|
video_signal->video_format = 5; // unspecified
|
|
video_signal->video_full_range_flag = 0; // YUV video levels: black=16, white=235
|
|
video_signal->colour_description_present_flag = 1;
|
|
|
|
if (is_hd_video(height))
|
|
{
|
|
video_signal->colour_primaries = 1; // BT.709
|
|
video_signal->transfer_characteristics = 1; // BT.709
|
|
video_signal->matrix_coefficients = 1; // BT.709
|
|
}
|
|
else
|
|
{
|
|
video_signal->colour_primaries = 6; // SMPTE 170M
|
|
video_signal->transfer_characteristics = 6; // SMPTE 170M
|
|
video_signal->matrix_coefficients = 6; // SMPTE 170M
|
|
}
|
|
}
|
|
|
|
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 = 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 {
|
|
// H.264 (AVC)
|
|
stAttr->stRcAttr.enRcMode = VENC_RC_MODE_H264VBR;
|
|
stAttr->stRcAttr.stH264Vbr.u32BitRate = bitrate;
|
|
stAttr->stRcAttr.stH264Vbr.u32MaxBitRate = max_bitrate;
|
|
stAttr->stRcAttr.stH264Vbr.u32MinBitRate = min_bitrate;
|
|
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;
|
|
}
|
|
|
|
stAttr->stVencAttr.enPixelFormat = RK_FMT_YUV422_YUYV;
|
|
stAttr->stVencAttr.u32PicWidth = width;
|
|
stAttr->stVencAttr.u32PicHeight = height;
|
|
stAttr->stVencAttr.u32VirWidth = RK_ALIGN_16(width);
|
|
stAttr->stVencAttr.u32VirHeight = RK_ALIGN_16(height);
|
|
stAttr->stVencAttr.u32StreamBufCnt = 3;
|
|
stAttr->stVencAttr.u32BufSize = width * height * 3 / 2;
|
|
stAttr->stVencAttr.enMirror = MIRROR_NONE;
|
|
}
|
|
|
|
static void venc_configure_limited_range_vui(RK_U32 height)
|
|
{
|
|
int32_t ret;
|
|
|
|
if (codec_type == 1)
|
|
{
|
|
VENC_H265_VUI_S stH265Vui;
|
|
memset(&stH265Vui, 0, sizeof(VENC_H265_VUI_S));
|
|
ret = RK_MPI_VENC_GetH265Vui(VENC_CHANNEL, &stH265Vui);
|
|
if (ret != RK_SUCCESS)
|
|
{
|
|
log_warn("RK_MPI_VENC_GetH265Vui failed: %#x", ret);
|
|
return;
|
|
}
|
|
configure_vui_video_signal(&stH265Vui.stVuiVideoSignal, height);
|
|
ret = RK_MPI_VENC_SetH265Vui(VENC_CHANNEL, &stH265Vui);
|
|
if (ret != RK_SUCCESS)
|
|
{
|
|
log_warn("RK_MPI_VENC_SetH265Vui failed: %#x", ret);
|
|
return;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
VENC_H264_VUI_S stH264Vui;
|
|
memset(&stH264Vui, 0, sizeof(VENC_H264_VUI_S));
|
|
ret = RK_MPI_VENC_GetH264Vui(VENC_CHANNEL, &stH264Vui);
|
|
if (ret != RK_SUCCESS)
|
|
{
|
|
log_warn("RK_MPI_VENC_GetH264Vui failed: %#x", ret);
|
|
return;
|
|
}
|
|
configure_vui_video_signal(&stH264Vui.stVuiVideoSignal, height);
|
|
ret = RK_MPI_VENC_SetH264Vui(VENC_CHANNEL, &stH264Vui);
|
|
if (ret != RK_SUCCESS)
|
|
{
|
|
log_warn("RK_MPI_VENC_SetH264Vui failed: %#x", ret);
|
|
return;
|
|
}
|
|
}
|
|
|
|
log_info("configured VENC VUI as limited-range %s", is_hd_video(height) ? "BT.709" : "BT.601");
|
|
}
|
|
|
|
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, int32_t fps)
|
|
{
|
|
int32_t ret;
|
|
VENC_CHN_ATTR_S stAttr;
|
|
populate_venc_attr(&stAttr, bitrate, max_bitrate, width, height, fps);
|
|
|
|
ret = RK_MPI_VENC_CreateChn(VENC_CHANNEL, &stAttr);
|
|
if (ret < 0)
|
|
{
|
|
RK_LOGE("error RK_MPI_VENC_CreateChn, %d", ret);
|
|
return ret;
|
|
}
|
|
|
|
venc_configure_limited_range_vui(height);
|
|
|
|
VENC_RECV_PIC_PARAM_S stRecvParam;
|
|
memset(&stRecvParam, 0, sizeof(VENC_RECV_PIC_PARAM_S));
|
|
stRecvParam.s32RecvPicNum = -1;
|
|
ret = RK_MPI_VENC_StartRecvFrame(VENC_CHANNEL, &stRecvParam);
|
|
if (ret < 0)
|
|
{
|
|
RK_LOGE("error RK_MPI_VENC_StartRecvFrame, %d", ret);
|
|
return ret;
|
|
}
|
|
|
|
venc_running = true;
|
|
venc_read_thread = malloc(sizeof(pthread_t));
|
|
if (pthread_create(venc_read_thread, NULL, venc_read_stream, NULL) != 0)
|
|
{
|
|
RK_LOGE("Failed to create venc_read_thread");
|
|
return RK_FAILURE;
|
|
}
|
|
|
|
return RK_SUCCESS;
|
|
}
|
|
|
|
static int32_t venc_stop()
|
|
{
|
|
venc_running = false;
|
|
|
|
int32_t ret;
|
|
ret = RK_MPI_VENC_StopRecvFrame(VENC_CHANNEL);
|
|
if (ret != RK_SUCCESS)
|
|
{
|
|
RK_LOGE("Failed to stop receiving frames for VENC_CHANNEL, error code: %d", ret);
|
|
return ret;
|
|
}
|
|
|
|
if (venc_read_thread != NULL)
|
|
{
|
|
pthread_join(*venc_read_thread, NULL);
|
|
free(venc_read_thread);
|
|
venc_read_thread = NULL;
|
|
}
|
|
|
|
ret = RK_MPI_VENC_DestroyChn(VENC_CHANNEL);
|
|
if (ret != RK_SUCCESS)
|
|
{
|
|
RK_LOGE("Failed to destroy VENC_CHANNEL, error code: %d", ret);
|
|
return ret;
|
|
}
|
|
|
|
return RK_SUCCESS;
|
|
}
|
|
|
|
struct buffer
|
|
{
|
|
struct v4l2_plane plane_buffer;
|
|
MB_BLK mb_blk;
|
|
};
|
|
|
|
const int input_buffer_count = 3;
|
|
|
|
static int32_t buf_init()
|
|
{
|
|
MB_POOL_CONFIG_S stMbPoolCfg;
|
|
memset(&stMbPoolCfg, 0, sizeof(MB_POOL_CONFIG_S));
|
|
stMbPoolCfg.u64MBSize = 1920 * 1080 * 3; // max resolution
|
|
stMbPoolCfg.u32MBCnt = input_buffer_count;
|
|
stMbPoolCfg.enAllocType = MB_ALLOC_TYPE_DMA;
|
|
stMbPoolCfg.bPreAlloc = RK_TRUE;
|
|
memPool = RK_MPI_MB_CreatePool(&stMbPoolCfg);
|
|
if (memPool == MB_INVALID_POOLID)
|
|
{
|
|
return -1;
|
|
}
|
|
log_info("created memory pool");
|
|
|
|
return RK_SUCCESS;
|
|
}
|
|
|
|
pthread_t *format_thread = NULL;
|
|
|
|
int video_init(float factor)
|
|
{
|
|
detect_sleep_mode();
|
|
|
|
if (factor <= 0) {
|
|
factor = 1.0f;
|
|
}
|
|
quality_factor = factor;
|
|
|
|
if (RK_MPI_SYS_Init() != RK_SUCCESS)
|
|
{
|
|
log_error("RK_MPI_SYS_Init failed");
|
|
return RK_FAILURE;
|
|
}
|
|
|
|
if (sub_dev_fd < 0)
|
|
{
|
|
sub_dev_fd = open(SUB_DEV, O_RDWR);
|
|
if (sub_dev_fd < 0)
|
|
{
|
|
log_error("failed to open control sub device %s: %s", SUB_DEV, strerror(errno));
|
|
return errno;
|
|
}
|
|
log_info("opened control sub device %s", SUB_DEV);
|
|
}
|
|
|
|
int32_t ret = buf_init();
|
|
if (ret != RK_SUCCESS)
|
|
{
|
|
log_error("buf_init failed with error: %d", ret);
|
|
return ret;
|
|
}
|
|
log_info("buf_init completed successfully");
|
|
|
|
format_thread = malloc(sizeof(pthread_t));
|
|
pthread_create(format_thread, NULL, run_detect_format, NULL);
|
|
return RK_SUCCESS;
|
|
}
|
|
|
|
// static int32_t venc_set_param(int32_t bitrate, int32_t max_bitrate, int32_t width, int32_t height)
|
|
// {
|
|
|
|
// VENC_CHN_ATTR_S stAttr;
|
|
// populate_venc_attr(&stAttr, bitrate, max_bitrate, width, height);
|
|
// VENC_CHN_PARAM_S stParam;
|
|
// memset(&stParam, 0, sizeof(VENC_CHN_PARAM_S));
|
|
|
|
// RK_MPI_VENC_StopRecvFrame(VENC_CHANNEL);
|
|
|
|
// int32_t ret = RK_MPI_VENC_SetChnParam(VENC_CHANNEL, &stAttr);
|
|
// if (ret < 0)
|
|
// {
|
|
// RK_LOGE("error RK_MPI_VENC_SetChnParam, %d", ret);
|
|
// return ret;
|
|
// }
|
|
// VENC_RECV_PIC_PARAM_S stRecvParam;
|
|
// memset(&stRecvParam, 0, sizeof(VENC_RECV_PIC_PARAM_S));
|
|
// stRecvParam.s32RecvPicNum = -1;
|
|
// ret = RK_MPI_VENC_StartRecvFrame(VENC_CHANNEL, &stRecvParam);
|
|
// if (ret < 0)
|
|
// {
|
|
// RK_LOGE("error RK_MPI_VENC_StartRecvFrame, %d", ret);
|
|
// return ret;
|
|
// }
|
|
|
|
// return RK_SUCCESS;
|
|
// }
|
|
|
|
/**
|
|
* @brief Continuously reads encoded video streams and sends them over unix socket.
|
|
*
|
|
* @param arg Unused parameter (void pointer for thread compatibility)
|
|
* @return NULL Always returns NULL
|
|
*/
|
|
static void *venc_read_stream(void *arg)
|
|
{
|
|
(void)arg;
|
|
void *pData = RK_NULL;
|
|
int loopCount = 0;
|
|
int s32Ret;
|
|
|
|
VENC_STREAM_S stFrame;
|
|
stFrame.pstPack = malloc(sizeof(VENC_PACK_S));
|
|
while (venc_running)
|
|
{
|
|
log_trace("RK_MPI_VENC_GetStream");
|
|
s32Ret = RK_MPI_VENC_GetStream(VENC_CHANNEL, &stFrame, 200); // blocks max 200ms
|
|
if (s32Ret == RK_SUCCESS)
|
|
{
|
|
RK_U64 nowUs = get_us();
|
|
log_trace("chn:0, loopCount:%d enc->seq:%d wd:%d pts=%llu delay=%lldus",
|
|
loopCount, stFrame.u32Seq, stFrame.pstPack->u32Len,
|
|
stFrame.pstPack->u64PTS, nowUs - stFrame.pstPack->u64PTS);
|
|
pData = RK_MPI_MB_Handle2VirAddr(stFrame.pstPack->pMbBlk);
|
|
video_send_frame(pData, (ssize_t)stFrame.pstPack->u32Len);
|
|
s32Ret = RK_MPI_VENC_ReleaseStream(VENC_CHANNEL, &stFrame);
|
|
if (s32Ret != RK_SUCCESS)
|
|
{
|
|
log_error("RK_MPI_VENC_ReleaseStream fail %x", s32Ret);
|
|
}
|
|
loopCount++;
|
|
}
|
|
else
|
|
{
|
|
if (s32Ret == RK_ERR_VENC_BUF_EMPTY)
|
|
{
|
|
continue;
|
|
}
|
|
log_error("RK_MPI_VENC_GetStream fail %x", s32Ret);
|
|
break;
|
|
}
|
|
}
|
|
log_info("exiting venc_read_stream");
|
|
free(stFrame.pstPack);
|
|
return NULL;
|
|
}
|
|
|
|
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;
|
|
pthread_mutex_t streaming_stopped_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
pthread_t *streaming_thread = NULL;
|
|
pthread_mutex_t streaming_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
bool get_streaming_flag()
|
|
{
|
|
log_info("getting streaming flag");
|
|
pthread_mutex_lock(&streaming_mutex);
|
|
bool flag = streaming_flag;
|
|
pthread_mutex_unlock(&streaming_mutex);
|
|
return flag;
|
|
}
|
|
|
|
void set_streaming_flag(bool flag)
|
|
{
|
|
log_info("setting streaming flag to %d", flag);
|
|
|
|
pthread_mutex_lock(&streaming_mutex);
|
|
streaming_flag = flag;
|
|
pthread_mutex_unlock(&streaming_mutex);
|
|
|
|
video_send_format_report();
|
|
}
|
|
|
|
void set_streaming_stopped(bool stopped)
|
|
{
|
|
pthread_mutex_lock(&streaming_stopped_mutex);
|
|
streaming_stopped = stopped;
|
|
pthread_mutex_unlock(&streaming_stopped_mutex);
|
|
|
|
video_send_format_report();
|
|
}
|
|
|
|
bool get_streaming_stopped()
|
|
{
|
|
pthread_mutex_lock(&streaming_stopped_mutex);
|
|
bool stopped = streaming_stopped;
|
|
pthread_mutex_unlock(&streaming_stopped_mutex);
|
|
return stopped;
|
|
}
|
|
|
|
void write_buffer_to_file(const uint8_t *buffer, size_t length, const char *filename)
|
|
{
|
|
FILE *file = fopen(filename, "wb");
|
|
fwrite(buffer, 1, length, file);
|
|
fclose(file);
|
|
}
|
|
|
|
void *run_video_stream(void *arg)
|
|
{
|
|
enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
|
|
|
log_info("running video stream");
|
|
|
|
set_streaming_stopped(false);
|
|
while (streaming_flag)
|
|
{
|
|
if (detected_signal == false)
|
|
{
|
|
usleep(10000); // Reduced to 10ms for better responsiveness to streaming_flag changes
|
|
continue;
|
|
}
|
|
|
|
int video_dev_fd = open(VIDEO_DEV, O_RDWR);
|
|
if (video_dev_fd < 0)
|
|
{
|
|
log_error("failed to open video capture device %s: %s", VIDEO_DEV, strerror(errno));
|
|
usleep(1000000);
|
|
continue;
|
|
}
|
|
log_info("opened video capture device %s", VIDEO_DEV);
|
|
|
|
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;
|
|
fmt.fmt.pix_mp.width = width;
|
|
fmt.fmt.pix_mp.height = height;
|
|
fmt.fmt.pix_mp.pixelformat = V4L2_PIX_FMT_YUYV;
|
|
fmt.fmt.pix_mp.field = V4L2_FIELD_ANY;
|
|
set_v4l2_yuv_colorimetry(&fmt.fmt.pix_mp, height);
|
|
|
|
if (ioctl(video_dev_fd, VIDIOC_S_FMT, &fmt) < 0)
|
|
{
|
|
log_error("Set format fail: %s", strerror(errno));
|
|
usleep(100000); // Sleep for 100 milliseconds
|
|
close(video_dev_fd);
|
|
continue;
|
|
}
|
|
log_info("capture colorimetry: colorspace=%u ycbcr_enc=%u quantization=%u xfer=%u",
|
|
fmt.fmt.pix_mp.colorspace, fmt.fmt.pix_mp.ycbcr_enc,
|
|
fmt.fmt.pix_mp.quantization, fmt.fmt.pix_mp.xfer_func);
|
|
|
|
struct v4l2_buffer buf;
|
|
|
|
struct v4l2_requestbuffers req;
|
|
req.count = input_buffer_count;
|
|
req.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
|
req.memory = V4L2_MEMORY_DMABUF;
|
|
|
|
if (ioctl(video_dev_fd, VIDIOC_REQBUFS, &req) < 0)
|
|
{
|
|
log_error("VIDIOC_REQBUFS failed: %s", strerror(errno));
|
|
close(video_dev_fd);
|
|
return (void *)errno;
|
|
}
|
|
log_info("VIDIOC_REQBUFS successful");
|
|
|
|
struct buffer buffers[3] = {};
|
|
log_info("allocated buffers");
|
|
|
|
for (int i = 0; i < input_buffer_count; i++)
|
|
{
|
|
struct v4l2_plane *planes_buffer = &buffers[i].plane_buffer;
|
|
memset(planes_buffer, 0, sizeof(struct v4l2_plane));
|
|
|
|
memset(&buf, 0, sizeof(buf));
|
|
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
|
buf.memory = V4L2_MEMORY_DMABUF;
|
|
buf.m.planes = planes_buffer;
|
|
buf.length = 1;
|
|
buf.index = i;
|
|
|
|
if (-1 == ioctl(video_dev_fd, VIDIOC_QUERYBUF, &buf))
|
|
{
|
|
log_error("VIDIOC_QUERYBUF failed: %s", strerror(errno));
|
|
req.count = i;
|
|
close(video_dev_fd);
|
|
return (void *)errno;
|
|
}
|
|
log_info("VIDIOC_QUERYBUF successful for buffer %d", i);
|
|
|
|
log_info("plane: length = %d", planes_buffer->length);
|
|
log_info("plane: offset = %d", planes_buffer->m.mem_offset);
|
|
|
|
MB_BLK blk = RK_MPI_MB_GetMB(memPool, (planes_buffer)->length, RK_TRUE);
|
|
if (blk == NULL)
|
|
{
|
|
log_error("get mb blk failed!");
|
|
close(video_dev_fd);
|
|
return (void *)errno;
|
|
}
|
|
log_info("Got memory block for buffer %d", i);
|
|
|
|
buffers[i].mb_blk = blk;
|
|
|
|
RK_S32 buf_fd = (RK_MPI_MB_Handle2Fd(blk));
|
|
if (buf_fd < 0)
|
|
{
|
|
log_error("RK_MPI_MB_Handle2Fd failed!");
|
|
close(video_dev_fd);
|
|
return (void *)errno;
|
|
}
|
|
log_info("Converted memory block to file descriptor for buffer %d", i);
|
|
planes_buffer->m.fd = buf_fd;
|
|
}
|
|
|
|
for (int i = 0; i < input_buffer_count; ++i)
|
|
{
|
|
struct v4l2_buffer buf;
|
|
memset(&buf, 0, sizeof(buf));
|
|
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
|
buf.memory = V4L2_MEMORY_DMABUF;
|
|
buf.length = 1;
|
|
buf.index = i;
|
|
buf.m.planes = &buffers[i].plane_buffer;
|
|
if (ioctl(video_dev_fd, VIDIOC_QBUF, &buf) < 0)
|
|
{
|
|
log_error("VIDIOC_QBUF failed: %s", strerror(errno));
|
|
close(video_dev_fd);
|
|
return (void *)errno;
|
|
}
|
|
log_info("VIDIOC_QBUF successful for buffer %d", i);
|
|
}
|
|
|
|
if (ioctl(video_dev_fd, VIDIOC_STREAMON, &type) < 0)
|
|
{
|
|
log_error("VIDIOC_STREAMON failed: %s", strerror(errno));
|
|
close(video_dev_fd);
|
|
return (void *)errno;
|
|
}
|
|
|
|
struct v4l2_plane tmp_plane;
|
|
|
|
// Set VENC parameters
|
|
int32_t bitrate = calculate_bitrate(quality_factor, 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);
|
|
goto cleanup;
|
|
}
|
|
|
|
fd_set fds;
|
|
struct timeval tv;
|
|
int r;
|
|
uint32_t num = 0;
|
|
VIDEO_FRAME_INFO_S stFrame;
|
|
memset(&stFrame, 0, sizeof(VIDEO_FRAME_INFO_S));
|
|
stFrame.stVFrame.u32Width = width;
|
|
stFrame.stVFrame.u32Height = height;
|
|
stFrame.stVFrame.u32VirWidth = RK_ALIGN_16(width);
|
|
stFrame.stVFrame.u32VirHeight = RK_ALIGN_16(height);
|
|
stFrame.stVFrame.enField = VIDEO_FIELD_FRAME;
|
|
stFrame.stVFrame.enPixelFormat = RK_FMT_YUV422_YUYV;
|
|
stFrame.stVFrame.enVideoFormat = VIDEO_FORMAT_LINEAR;
|
|
stFrame.stVFrame.enCompressMode = COMPRESS_MODE_NONE;
|
|
stFrame.stVFrame.enDynamicRange = DYNAMIC_RANGE_SDR8;
|
|
stFrame.stVFrame.enColorGamut = color_gamut_for_height(height);
|
|
|
|
while (streaming_flag)
|
|
{
|
|
FD_ZERO(&fds);
|
|
FD_SET(video_dev_fd, &fds);
|
|
tv.tv_sec = 1;
|
|
tv.tv_usec = 0;
|
|
|
|
r = select(video_dev_fd + 1, &fds, NULL, NULL, &tv);
|
|
if (r == 0)
|
|
{
|
|
log_info("select timeout");
|
|
ensure_sleep_mode_disabled();
|
|
break;
|
|
}
|
|
if (r == -1)
|
|
{
|
|
if (errno == EINTR)
|
|
{
|
|
continue;
|
|
}
|
|
log_error("select in video streaming");
|
|
break;
|
|
}
|
|
memset(&buf, 0, sizeof(buf));
|
|
buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
|
buf.memory = V4L2_MEMORY_DMABUF;
|
|
buf.m.planes = &tmp_plane;
|
|
buf.length = 1;
|
|
if (ioctl(video_dev_fd, VIDIOC_DQBUF, &buf) < 0)
|
|
{
|
|
log_error("VIDIOC_DQBUF failed: %s", strerror(errno));
|
|
break;
|
|
}
|
|
log_trace("got frame, bytesused = %d", tmp_plane.bytesused);
|
|
MB_BLK blk = RK_MPI_MMZ_Fd2Handle(tmp_plane.m.fd);
|
|
assert(blk != RK_NULL);
|
|
stFrame.stVFrame.pMbBlk = blk;
|
|
stFrame.stVFrame.u32TimeRef = num;
|
|
stFrame.stVFrame.u64PTS = get_us();
|
|
bool retried = false;
|
|
retry_send_frame:
|
|
if (RK_MPI_VENC_SendFrame(VENC_CHANNEL, &stFrame, 2000) != RK_SUCCESS)
|
|
{
|
|
if (retried == true)
|
|
{
|
|
log_error("RK_MPI_VENC_SendFrame retry failed");
|
|
}
|
|
else
|
|
{
|
|
log_error("RK_MPI_VENC_SendFrame failed,retrying");
|
|
retried = true;
|
|
usleep(1000llu);
|
|
goto retry_send_frame;
|
|
}
|
|
}
|
|
|
|
num++;
|
|
|
|
if (ioctl(video_dev_fd, VIDIOC_QBUF, &buf) < 0)
|
|
log_error("failure VIDIOC_QBUF: %s", strerror(errno));
|
|
}
|
|
cleanup:
|
|
log_info("cleaning up video capture device %s", VIDEO_DEV);
|
|
if (ioctl(video_dev_fd, VIDIOC_STREAMOFF, &type) < 0)
|
|
{
|
|
log_error("VIDIOC_STREAMOFF failed: %s", strerror(errno));
|
|
}
|
|
|
|
// Explicitly free V4L2 buffer queue
|
|
struct v4l2_requestbuffers req_free;
|
|
memset(&req_free, 0, sizeof(req_free));
|
|
req_free.count = 0;
|
|
req_free.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE;
|
|
req_free.memory = V4L2_MEMORY_DMABUF;
|
|
|
|
if (ioctl(video_dev_fd, VIDIOC_REQBUFS, &req_free) < 0)
|
|
{
|
|
log_error("Failed to free V4L2 buffers: %s", strerror(errno));
|
|
}
|
|
|
|
venc_stop();
|
|
|
|
for (int i = 0; i < input_buffer_count; i++)
|
|
{
|
|
if (buffers[i].mb_blk != NULL)
|
|
{
|
|
RK_MPI_MB_ReleaseMB((buffers + i)->mb_blk);
|
|
}
|
|
}
|
|
|
|
log_info("closing video capture device %s", VIDEO_DEV);
|
|
close(video_dev_fd);
|
|
}
|
|
|
|
log_info("video stream thread exiting");
|
|
|
|
set_streaming_stopped(true);
|
|
|
|
return NULL;
|
|
}
|
|
|
|
void video_shutdown()
|
|
{
|
|
if (should_exit == true)
|
|
{
|
|
log_info("shutting down in progress already");
|
|
return;
|
|
}
|
|
video_stop_streaming();
|
|
|
|
should_exit = true;
|
|
if (sub_dev_fd > 0)
|
|
{
|
|
shutdown(sub_dev_fd, SHUT_RDWR);
|
|
log_info("Closed sub_dev_fd");
|
|
}
|
|
|
|
if (memPool != MB_INVALID_POOLID)
|
|
{
|
|
RK_MPI_MB_DestroyPool(memPool);
|
|
}
|
|
log_info("Destroyed memory pool");
|
|
|
|
pthread_mutex_destroy(&streaming_mutex);
|
|
log_info("Destroyed streaming mutex");
|
|
}
|
|
|
|
void video_start_streaming()
|
|
{
|
|
log_info("starting video streaming");
|
|
if (streaming_thread != NULL)
|
|
{
|
|
bool stopped = get_streaming_stopped();
|
|
if (stopped == true) {
|
|
log_error("video streaming already stopped but streaming_thread is not NULL");
|
|
assert(stopped == true);
|
|
}
|
|
log_warn("video streaming already started");
|
|
return;
|
|
}
|
|
|
|
pthread_t *new_thread = malloc(sizeof(pthread_t));
|
|
if (new_thread == NULL)
|
|
{
|
|
log_error("Failed to allocate memory for streaming thread");
|
|
return;
|
|
}
|
|
|
|
set_streaming_flag(true);
|
|
int result = pthread_create(new_thread, NULL, run_video_stream, NULL);
|
|
if (result != 0)
|
|
{
|
|
log_error("Failed to create streaming thread: %s", strerror(result));
|
|
set_streaming_flag(false);
|
|
free(new_thread);
|
|
return;
|
|
}
|
|
|
|
// Only set streaming_thread after successful creation
|
|
streaming_thread = new_thread;
|
|
}
|
|
|
|
bool wait_for_streaming_stopped()
|
|
{
|
|
int attempts = 0;
|
|
while (attempts < 30) {
|
|
if (get_streaming_stopped() == true) {
|
|
log_info("video streaming stopped after %d attempts", attempts);
|
|
return true;
|
|
}
|
|
usleep(100000); // 100ms
|
|
attempts++;
|
|
}
|
|
log_error("video streaming did not stop after 3s");
|
|
return false;
|
|
}
|
|
|
|
void video_stop_streaming()
|
|
{
|
|
if (streaming_thread == NULL) {
|
|
log_info("video streaming already stopped");
|
|
return;
|
|
}
|
|
|
|
log_info("stopping video streaming");
|
|
set_streaming_flag(false);
|
|
|
|
log_info("waiting for video streaming thread to exit");
|
|
wait_for_streaming_stopped();
|
|
|
|
pthread_join(*streaming_thread, NULL);
|
|
free(streaming_thread);
|
|
streaming_thread = NULL;
|
|
|
|
log_info("video streaming stopped");
|
|
}
|
|
|
|
uint8_t video_get_streaming_status() {
|
|
// streaming flag can be false when stopping streaming
|
|
if (get_streaming_flag() == true) return 1;
|
|
if (get_streaming_stopped() == false) return 2;
|
|
return 0;
|
|
}
|
|
|
|
void video_restart_streaming()
|
|
{
|
|
uint8_t streaming_status = video_get_streaming_status();
|
|
// 0 = stopped, 1 = running, 2 = stopping
|
|
|
|
if (streaming_status == 0)
|
|
{
|
|
log_info("will not restart video streaming because it's stopped");
|
|
return;
|
|
}
|
|
|
|
if (streaming_status == 1 || streaming_status == 2) {
|
|
video_stop_streaming();
|
|
}
|
|
|
|
if (!wait_for_streaming_stopped()) {
|
|
return;
|
|
}
|
|
|
|
video_start_streaming();
|
|
}
|
|
|
|
void *run_detect_format(void *arg)
|
|
{
|
|
struct v4l2_event_subscription sub;
|
|
struct v4l2_event ev;
|
|
struct v4l2_dv_timings dv_timings;
|
|
|
|
memset(&sub, 0, sizeof(sub));
|
|
sub.type = V4L2_EVENT_SOURCE_CHANGE;
|
|
if (ioctl(sub_dev_fd, VIDIOC_SUBSCRIBE_EVENT, &sub) == -1)
|
|
{
|
|
log_error("cannot subscribe to event");
|
|
goto exit;
|
|
}
|
|
|
|
while (!should_exit)
|
|
{
|
|
ensure_sleep_mode_disabled();
|
|
|
|
memset(&dv_timings, 0, sizeof(dv_timings));
|
|
if (ioctl(sub_dev_fd, VIDIOC_QUERY_DV_TIMINGS, &dv_timings) != 0)
|
|
{
|
|
detected_signal = false;
|
|
if (errno == ENOLINK)
|
|
{
|
|
// No timings could be detected because no signal was found.
|
|
log_info("HDMI status: no signal");
|
|
video_report_format(false, "no_signal", 0, 0, 0);
|
|
}
|
|
else if (errno == ENOLCK)
|
|
{
|
|
// The signal was unstable and the hardware could not lock on to it.
|
|
log_info("HDMI status: no lock");
|
|
video_report_format(false, "no_lock", 0, 0, 0);
|
|
}
|
|
else if (errno == ERANGE)
|
|
{
|
|
// Timings were found, but they are out of range of the hardware capabilities.
|
|
log_warn("HDMI status: out of range");
|
|
video_report_format(false, "out_of_range", 0, 0, 0);
|
|
}
|
|
else
|
|
{
|
|
log_error("error VIDIOC_QUERY_DV_TIMINGS: %s", strerror(errno));
|
|
sleep(1);
|
|
continue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
log_info("Active width: %d", dv_timings.bt.width);
|
|
log_info("Active height: %d", dv_timings.bt.height);
|
|
double frames_per_second = (double)dv_timings.bt.pixelclock /
|
|
((dv_timings.bt.height + dv_timings.bt.vfrontporch + dv_timings.bt.vsync +
|
|
dv_timings.bt.vbackporch) *
|
|
(dv_timings.bt.width + dv_timings.bt.hfrontporch + dv_timings.bt.hsync +
|
|
dv_timings.bt.hbackporch));
|
|
log_info("Frames per second: %.2f fps", frames_per_second);
|
|
|
|
// 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);
|
|
|
|
if (should_restart) {
|
|
log_info("restarting video streaming due to format change");
|
|
video_restart_streaming();
|
|
}
|
|
}
|
|
|
|
memset(&ev, 0, sizeof(ev));
|
|
if (ioctl(sub_dev_fd, VIDIOC_DQEVENT, &ev) != 0)
|
|
{
|
|
log_error("failed to VIDIOC_DQEVENT: %s", strerror(errno));
|
|
break;
|
|
}
|
|
log_info("New event of type %u", ev.type);
|
|
if (ev.type != V4L2_EVENT_SOURCE_CHANGE)
|
|
{
|
|
continue;
|
|
}
|
|
log_info("source change detected!");
|
|
}
|
|
exit:
|
|
close(sub_dev_fd);
|
|
return NULL;
|
|
}
|
|
|
|
|
|
void video_set_quality_factor(float factor)
|
|
{
|
|
quality_factor = factor;
|
|
|
|
// TODO: update venc bitrate without stopping streaming
|
|
video_restart_streaming();
|
|
}
|
|
|
|
float video_get_quality_factor() {
|
|
return quality_factor;
|
|
}
|
|
|
|
void video_set_codec_type(int type) {
|
|
codec_type = type;
|
|
}
|
|
|
|
int video_get_codec_type() {
|
|
return codec_type;
|
|
}
|