mirror of
https://github.com/luckfox-eng29/kvm.git
synced 2026-05-28 09:01:22 +02:00
feat(video): add video rate control settings and UI integration
Signed-off-by: luckfox-eng29 <eng29@luckfox.com>
This commit is contained in:
128
jsonrpc.go
128
jsonrpc.go
@@ -236,6 +236,124 @@ func rpcSetStreamEncodecType(encodecType string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RcQpParams struct {
|
||||||
|
S32FirstFrameStartQp int `json:"s32FirstFrameStartQp"`
|
||||||
|
U32StepQp int `json:"u32StepQp"`
|
||||||
|
U32MinQp int `json:"u32MinQp"`
|
||||||
|
U32MaxQp int `json:"u32MaxQp"`
|
||||||
|
U32MinIQp int `json:"u32MinIQp"`
|
||||||
|
U32MaxIQp int `json:"u32MaxIQp"`
|
||||||
|
S32DeltIpQp int `json:"s32DeltIpQp"`
|
||||||
|
S32MaxReEncodeTimes int `json:"s32MaxReEncodeTimes"`
|
||||||
|
U32FrmMaxQp int `json:"u32FrmMaxQp"`
|
||||||
|
U32FrmMinQp int `json:"u32FrmMinQp"`
|
||||||
|
U32FrmMinIQp int `json:"u32FrmMinIQp"`
|
||||||
|
U32FrmMaxIQp int `json:"u32FrmMaxIQp"`
|
||||||
|
U32MotionStaticSwitchFrmQp int `json:"u32MotionStaticSwitchFrmQp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type VideoRcConfigParams struct {
|
||||||
|
H264 RcQpParams `json:"h264"`
|
||||||
|
H265 RcQpParams `json:"h265"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcSetVideoRc(params VideoRcConfigParams) error {
|
||||||
|
logger.Info().Interface("params", params).Msg("Setting video RC params")
|
||||||
|
rcParams := map[string]interface{}{
|
||||||
|
"h264": map[string]interface{}{
|
||||||
|
"s32FirstFrameStartQp": params.H264.S32FirstFrameStartQp,
|
||||||
|
"u32StepQp": params.H264.U32StepQp,
|
||||||
|
"u32MinQp": params.H264.U32MinQp,
|
||||||
|
"u32MaxQp": params.H264.U32MaxQp,
|
||||||
|
"u32MinIQp": params.H264.U32MinIQp,
|
||||||
|
"u32MaxIQp": params.H264.U32MaxIQp,
|
||||||
|
"s32DeltIpQp": params.H264.S32DeltIpQp,
|
||||||
|
"s32MaxReEncodeTimes": params.H264.S32MaxReEncodeTimes,
|
||||||
|
"u32FrmMaxQp": params.H264.U32FrmMaxQp,
|
||||||
|
"u32FrmMinQp": params.H264.U32FrmMinQp,
|
||||||
|
"u32FrmMinIQp": params.H264.U32FrmMinIQp,
|
||||||
|
"u32FrmMaxIQp": params.H264.U32FrmMaxIQp,
|
||||||
|
"u32MotionStaticSwitchFrmQp": params.H264.U32MotionStaticSwitchFrmQp,
|
||||||
|
},
|
||||||
|
"h265": map[string]interface{}{
|
||||||
|
"s32FirstFrameStartQp": params.H265.S32FirstFrameStartQp,
|
||||||
|
"u32StepQp": params.H265.U32StepQp,
|
||||||
|
"u32MinQp": params.H265.U32MinQp,
|
||||||
|
"u32MaxQp": params.H265.U32MaxQp,
|
||||||
|
"u32MinIQp": params.H265.U32MinIQp,
|
||||||
|
"u32MaxIQp": params.H265.U32MaxIQp,
|
||||||
|
"s32DeltIpQp": params.H265.S32DeltIpQp,
|
||||||
|
"s32MaxReEncodeTimes": params.H265.S32MaxReEncodeTimes,
|
||||||
|
"u32FrmMaxQp": params.H265.U32FrmMaxQp,
|
||||||
|
"u32FrmMinQp": params.H265.U32FrmMinQp,
|
||||||
|
"u32FrmMinIQp": params.H265.U32FrmMinIQp,
|
||||||
|
"u32FrmMaxIQp": params.H265.U32FrmMaxIQp,
|
||||||
|
"u32MotionStaticSwitchFrmQp": params.H265.U32MotionStaticSwitchFrmQp,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var _, err = CallCtrlAction("set_video_rc", rcParams)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func rpcGetVideoRc() (VideoRcConfigParams, error) {
|
||||||
|
resp, err := CallCtrlAction("get_video_rc", nil)
|
||||||
|
if err != nil {
|
||||||
|
return VideoRcConfigParams{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := resp.Result
|
||||||
|
if result == nil {
|
||||||
|
return VideoRcConfigParams{}, errors.New("invalid response format")
|
||||||
|
}
|
||||||
|
|
||||||
|
h264Map, _ := result["h264"].(map[string]interface{})
|
||||||
|
h265Map, _ := result["h265"].(map[string]interface{})
|
||||||
|
|
||||||
|
getInt := func(m map[string]interface{}, k string) int {
|
||||||
|
if v, ok := m[k].(float64); ok {
|
||||||
|
return int(v)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
getUint := func(m map[string]interface{}, k string) int {
|
||||||
|
return getInt(m, k)
|
||||||
|
}
|
||||||
|
|
||||||
|
rc := VideoRcConfigParams{
|
||||||
|
H264: RcQpParams{
|
||||||
|
S32FirstFrameStartQp: getInt(h264Map, "s32FirstFrameStartQp"),
|
||||||
|
U32StepQp: getUint(h264Map, "u32StepQp"),
|
||||||
|
U32MinQp: getUint(h264Map, "u32MinQp"),
|
||||||
|
U32MaxQp: getUint(h264Map, "u32MaxQp"),
|
||||||
|
U32MinIQp: getUint(h264Map, "u32MinIQp"),
|
||||||
|
U32MaxIQp: getUint(h264Map, "u32MaxIQp"),
|
||||||
|
S32DeltIpQp: getInt(h264Map, "s32DeltIpQp"),
|
||||||
|
S32MaxReEncodeTimes: getInt(h264Map, "s32MaxReEncodeTimes"),
|
||||||
|
U32FrmMaxQp: getUint(h264Map, "u32FrmMaxQp"),
|
||||||
|
U32FrmMinQp: getUint(h264Map, "u32FrmMinQp"),
|
||||||
|
U32FrmMinIQp: getUint(h264Map, "u32FrmMinIQp"),
|
||||||
|
U32FrmMaxIQp: getUint(h264Map, "u32FrmMaxIQp"),
|
||||||
|
U32MotionStaticSwitchFrmQp: getUint(h264Map, "u32MotionStaticSwitchFrmQp"),
|
||||||
|
},
|
||||||
|
H265: RcQpParams{
|
||||||
|
S32FirstFrameStartQp: getInt(h265Map, "s32FirstFrameStartQp"),
|
||||||
|
U32StepQp: getUint(h265Map, "u32StepQp"),
|
||||||
|
U32MinQp: getUint(h265Map, "u32MinQp"),
|
||||||
|
U32MaxQp: getUint(h265Map, "u32MaxQp"),
|
||||||
|
U32MinIQp: getUint(h265Map, "u32MinIQp"),
|
||||||
|
U32MaxIQp: getUint(h265Map, "u32MaxIQp"),
|
||||||
|
S32DeltIpQp: getInt(h265Map, "s32DeltIpQp"),
|
||||||
|
S32MaxReEncodeTimes: getInt(h265Map, "s32MaxReEncodeTimes"),
|
||||||
|
U32FrmMaxQp: getUint(h265Map, "u32FrmMaxQp"),
|
||||||
|
U32FrmMinQp: getUint(h265Map, "u32FrmMinQp"),
|
||||||
|
U32FrmMinIQp: getUint(h265Map, "u32FrmMinIQp"),
|
||||||
|
U32FrmMaxIQp: getUint(h265Map, "u32FrmMaxIQp"),
|
||||||
|
U32MotionStaticSwitchFrmQp: getUint(h265Map, "u32MotionStaticSwitchFrmQp"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return rc, nil
|
||||||
|
}
|
||||||
|
|
||||||
func rpcSetNpuAppStatus(enable bool) error {
|
func rpcSetNpuAppStatus(enable bool) error {
|
||||||
logger.Info().Bool("enable", enable).Msg("Setting NPU app status")
|
logger.Info().Bool("enable", enable).Msg("Setting NPU app status")
|
||||||
var _, err = CallCtrlAction("set_yolo_enable", map[string]interface{}{"enable": enable})
|
var _, err = CallCtrlAction("set_yolo_enable", map[string]interface{}{"enable": enable})
|
||||||
@@ -1441,7 +1559,7 @@ var rpcHandlers = map[string]RPCHandler{
|
|||||||
"resetSDStorage": {Func: rpcResetSDStorage},
|
"resetSDStorage": {Func: rpcResetSDStorage},
|
||||||
"mountSDStorage": {Func: rpcMountSDStorage},
|
"mountSDStorage": {Func: rpcMountSDStorage},
|
||||||
"unmountSDStorage": {Func: rpcUnmountSDStorage},
|
"unmountSDStorage": {Func: rpcUnmountSDStorage},
|
||||||
"formatSDStorage": {Func: rpcFormatSDStorage, Params: []string{"confirm"}},
|
"formatSDStorage": {Func: rpcFormatSDStorage, Params: []string{"confirm", "fsType"}},
|
||||||
"mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}},
|
"mountWithHTTP": {Func: rpcMountWithHTTP, Params: []string{"url", "mode"}},
|
||||||
"mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}},
|
"mountWithWebRTC": {Func: rpcMountWithWebRTC, Params: []string{"filename", "size", "mode"}},
|
||||||
"mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}},
|
"mountWithStorage": {Func: rpcMountWithStorage, Params: []string{"filename", "mode"}},
|
||||||
@@ -1527,8 +1645,16 @@ var rpcHandlers = map[string]RPCHandler{
|
|||||||
"stopCloudflared": {Func: rpcStopCloudflared},
|
"stopCloudflared": {Func: rpcStopCloudflared},
|
||||||
"getCloudflaredStatus": {Func: rpcGetCloudflaredStatus},
|
"getCloudflaredStatus": {Func: rpcGetCloudflaredStatus},
|
||||||
"getCloudflaredLog": {Func: rpcGetCloudflaredLog},
|
"getCloudflaredLog": {Func: rpcGetCloudflaredLog},
|
||||||
|
"getVpnToolSystemInfo": {Func: rpcGetVpnToolSystemInfo},
|
||||||
|
"getVpnToolStatus": {Func: rpcGetVpnToolStatus, Params: []string{"tool"}},
|
||||||
|
"listVpnToolReleases": {Func: rpcListVpnToolReleases, Params: []string{"tool"}},
|
||||||
|
"installVpnTool": {Func: rpcInstallVpnTool, Params: []string{"tool", "version", "assetName", "downloadURL"}},
|
||||||
|
"useVpnToolVersion": {Func: rpcUseVpnToolVersion, Params: []string{"tool", "version"}},
|
||||||
|
"uninstallVpnToolVersion": {Func: rpcUninstallVpnToolVersion, Params: []string{"tool", "version"}},
|
||||||
"getStreamEncodecType": {Func: rpcGetStreamEncodecType},
|
"getStreamEncodecType": {Func: rpcGetStreamEncodecType},
|
||||||
"setStreamEncodecType": {Func: rpcSetStreamEncodecType, Params: []string{"encodecType"}},
|
"setStreamEncodecType": {Func: rpcSetStreamEncodecType, Params: []string{"encodecType"}},
|
||||||
|
"setVideoRc": {Func: rpcSetVideoRc, Params: []string{"params"}},
|
||||||
|
"getVideoRc": {Func: rpcGetVideoRc},
|
||||||
"setNpuAppStatus": {Func: rpcSetNpuAppStatus, Params: []string{"enable"}},
|
"setNpuAppStatus": {Func: rpcSetNpuAppStatus, Params: []string{"enable"}},
|
||||||
"getNpuAppStatus": {Func: rpcGetNpuAppStatus},
|
"getNpuAppStatus": {Func: rpcGetNpuAppStatus},
|
||||||
"startWireguard": {Func: rpcStartWireguard, Params: []string{"configFile"}},
|
"startWireguard": {Func: rpcStartWireguard, Params: []string{"configFile"}},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Button as AntdButton , Slider , Checkbox, Select } from "antd";
|
import { Button as AntdButton , Slider , Checkbox, Select, Modal, InputNumber, Tabs, Typography } from "antd";
|
||||||
import { useReactAt } from "i18n-auto-extractor/react";
|
import { useReactAt } from "i18n-auto-extractor/react";
|
||||||
import { isMobile } from "react-device-detect";
|
import { isMobile } from "react-device-detect";
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ import { SettingsItem, SettingsItemNew } from "@components/Settings/SettingsView
|
|||||||
|
|
||||||
import notifications from "../../../notifications";
|
import notifications from "../../../notifications";
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -45,6 +46,133 @@ const streamQualityOptions = [
|
|||||||
{ value: "0.1", label: "Low" },
|
{ value: "0.1", label: "Low" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
type RcQpParams = {
|
||||||
|
s32FirstFrameStartQp: number;
|
||||||
|
u32StepQp: number;
|
||||||
|
u32MinQp: number;
|
||||||
|
u32MaxQp: number;
|
||||||
|
u32MinIQp: number;
|
||||||
|
u32MaxIQp: number;
|
||||||
|
s32DeltIpQp: number;
|
||||||
|
s32MaxReEncodeTimes: number;
|
||||||
|
u32FrmMaxQp: number;
|
||||||
|
u32FrmMinQp: number;
|
||||||
|
u32FrmMinIQp: number;
|
||||||
|
u32FrmMaxIQp: number;
|
||||||
|
u32MotionStaticSwitchFrmQp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type VideoRcConfig = {
|
||||||
|
h264: RcQpParams;
|
||||||
|
h265: RcQpParams;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RcSliderValues = {
|
||||||
|
stepQp: number;
|
||||||
|
minQp: number;
|
||||||
|
minIQp: number;
|
||||||
|
deltIpQp: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RcSliderState = {
|
||||||
|
h264: RcSliderValues;
|
||||||
|
h265: RcSliderValues;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clamp = (value: number, min: number, max: number) =>
|
||||||
|
Math.min(max, Math.max(min, value));
|
||||||
|
|
||||||
|
const sliderValueToNumber = (value: number | number[]) =>
|
||||||
|
Array.isArray(value) ? value[0] : value;
|
||||||
|
|
||||||
|
const DEFAULT_RC_CODEC: RcQpParams = {
|
||||||
|
s32FirstFrameStartQp: 0,
|
||||||
|
u32StepQp: 48,
|
||||||
|
u32MinQp: 48,
|
||||||
|
u32MaxQp: 51,
|
||||||
|
u32MinIQp: 48,
|
||||||
|
u32MaxIQp: 51,
|
||||||
|
s32DeltIpQp: 7,
|
||||||
|
s32MaxReEncodeTimes: 2,
|
||||||
|
u32FrmMaxQp: 51,
|
||||||
|
u32FrmMinQp: 48,
|
||||||
|
u32FrmMinIQp: 51,
|
||||||
|
u32FrmMaxIQp: 48,
|
||||||
|
u32MotionStaticSwitchFrmQp: 50,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_VIDEO_RC_CONFIG: VideoRcConfig = {
|
||||||
|
h264: { ...DEFAULT_RC_CODEC },
|
||||||
|
h265: { ...DEFAULT_RC_CODEC },
|
||||||
|
};
|
||||||
|
|
||||||
|
const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
|
||||||
|
typeof value === "object" && value !== null;
|
||||||
|
|
||||||
|
const toVideoRcConfigOrDefault = (value: unknown): VideoRcConfig => {
|
||||||
|
if (!isObjectRecord(value)) return DEFAULT_VIDEO_RC_CONFIG;
|
||||||
|
if (!("h264" in value) || !("h265" in value)) return DEFAULT_VIDEO_RC_CONFIG;
|
||||||
|
return value as VideoRcConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
const RC_LIMITS = {
|
||||||
|
stepQp: { min: 1, max: 51 },
|
||||||
|
maxQp: { min: 1, max: 51 },
|
||||||
|
maxIQp: { min: 1, max: 51 },
|
||||||
|
deltIpQp: { min: -7, max: 7 },
|
||||||
|
maxReEncodeTimes: { min: 0, max: 3 },
|
||||||
|
frmQp: { min: 1, max: 51 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeRcCodec = (codec: RcQpParams): RcQpParams => {
|
||||||
|
const maxQp = clamp(Number(codec.u32MaxQp), RC_LIMITS.maxQp.min, RC_LIMITS.maxQp.max);
|
||||||
|
const maxIQp = clamp(Number(codec.u32MaxIQp), RC_LIMITS.maxIQp.min, RC_LIMITS.maxIQp.max);
|
||||||
|
const minQp = clamp(Number(codec.u32MinQp), RC_LIMITS.maxQp.min, maxQp);
|
||||||
|
const minIQp = clamp(Number(codec.u32MinIQp), RC_LIMITS.maxIQp.min, maxIQp);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...codec,
|
||||||
|
s32FirstFrameStartQp: clamp(Number(codec.s32FirstFrameStartQp), RC_LIMITS.frmQp.min, RC_LIMITS.frmQp.max),
|
||||||
|
u32StepQp: clamp(Number(codec.u32StepQp), RC_LIMITS.stepQp.min, RC_LIMITS.stepQp.max),
|
||||||
|
u32MaxQp: maxQp,
|
||||||
|
u32MinQp: minQp,
|
||||||
|
u32MaxIQp: maxIQp,
|
||||||
|
u32MinIQp: minIQp,
|
||||||
|
s32DeltIpQp: clamp(Number(codec.s32DeltIpQp), RC_LIMITS.deltIpQp.min, RC_LIMITS.deltIpQp.max),
|
||||||
|
s32MaxReEncodeTimes: clamp(
|
||||||
|
Number(codec.s32MaxReEncodeTimes),
|
||||||
|
RC_LIMITS.maxReEncodeTimes.min,
|
||||||
|
RC_LIMITS.maxReEncodeTimes.max,
|
||||||
|
),
|
||||||
|
u32FrmMaxQp: clamp(Number(codec.u32FrmMaxQp), RC_LIMITS.frmQp.min, RC_LIMITS.frmQp.max),
|
||||||
|
u32FrmMinQp: clamp(Number(codec.u32FrmMinQp), RC_LIMITS.frmQp.min, RC_LIMITS.frmQp.max),
|
||||||
|
u32FrmMinIQp: clamp(Number(codec.u32FrmMinIQp), RC_LIMITS.frmQp.min, RC_LIMITS.frmQp.max),
|
||||||
|
u32FrmMaxIQp: clamp(Number(codec.u32FrmMaxIQp), RC_LIMITS.frmQp.min, RC_LIMITS.frmQp.max),
|
||||||
|
u32MotionStaticSwitchFrmQp: clamp(
|
||||||
|
Number(codec.u32MotionStaticSwitchFrmQp),
|
||||||
|
RC_LIMITS.frmQp.min,
|
||||||
|
RC_LIMITS.frmQp.max,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeVideoRcConfig = (config: VideoRcConfig): VideoRcConfig => ({
|
||||||
|
h264: normalizeRcCodec(config.h264),
|
||||||
|
h265: normalizeRcCodec(config.h265),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sliderValuesFromCodec = (codec: RcQpParams): RcSliderValues => ({
|
||||||
|
stepQp: clamp(Number(codec.u32StepQp), 1, 50),
|
||||||
|
minQp: clamp(Number(codec.u32MinQp), 1, 50),
|
||||||
|
minIQp: clamp(Number(codec.u32MinIQp), 1, 50),
|
||||||
|
deltIpQp: clamp(Number(codec.s32DeltIpQp), -7, 7),
|
||||||
|
});
|
||||||
|
|
||||||
|
const sliderStateFromConfig = (config: VideoRcConfig): RcSliderState => ({
|
||||||
|
h264: sliderValuesFromCodec(config.h264),
|
||||||
|
h265: sliderValuesFromCodec(config.h265),
|
||||||
|
});
|
||||||
|
|
||||||
export default function SettingsVideoSide() {
|
export default function SettingsVideoSide() {
|
||||||
const { $at } = useReactAt();
|
const { $at } = useReactAt();
|
||||||
const [send] = useJsonRpc();
|
const [send] = useJsonRpc();
|
||||||
@@ -54,6 +182,12 @@ export default function SettingsVideoSide() {
|
|||||||
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
|
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
|
||||||
const [edid, setEdid] = useState<string | null>(null);
|
const [edid, setEdid] = useState<string | null>(null);
|
||||||
const [forceHpd, setForceHpd] = useState(false);
|
const [forceHpd, setForceHpd] = useState(false);
|
||||||
|
const [videoRcConfig, setVideoRcConfig] = useState<VideoRcConfig>(DEFAULT_VIDEO_RC_CONFIG);
|
||||||
|
const [rcSliderValues, setRcSliderValues] = useState<RcSliderState>(
|
||||||
|
sliderStateFromConfig(DEFAULT_VIDEO_RC_CONFIG),
|
||||||
|
);
|
||||||
|
const [showRcAdvanced, setShowRcAdvanced] = useState(false);
|
||||||
|
const [rcDraftConfig, setRcDraftConfig] = useState<VideoRcConfig | null>(null);
|
||||||
|
|
||||||
// Video enhancement settings from store
|
// Video enhancement settings from store
|
||||||
const videoSaturation = useSettingsStore(state => state.videoSaturation);
|
const videoSaturation = useSettingsStore(state => state.videoSaturation);
|
||||||
@@ -63,6 +197,129 @@ export default function SettingsVideoSide() {
|
|||||||
const videoContrast = useSettingsStore(state => state.videoContrast);
|
const videoContrast = useSettingsStore(state => state.videoContrast);
|
||||||
const setVideoContrast = useSettingsStore(state => state.setVideoContrast);
|
const setVideoContrast = useSettingsStore(state => state.setVideoContrast);
|
||||||
|
|
||||||
|
const currentCodec: "h264" | "h265" = streamEncodecType === "hevc" ? "h265" : "h264";
|
||||||
|
const currentSliders = rcSliderValues[currentCodec];
|
||||||
|
|
||||||
|
const applySliderToCodec = (codec: RcQpParams, sliders: RcSliderValues): RcQpParams => ({
|
||||||
|
...codec,
|
||||||
|
u32StepQp: sliders.stepQp,
|
||||||
|
u32MinQp: sliders.minQp,
|
||||||
|
u32MinIQp: sliders.minIQp,
|
||||||
|
s32DeltIpQp: sliders.deltIpQp,
|
||||||
|
});
|
||||||
|
|
||||||
|
const applyRcBasicConfig = () => {
|
||||||
|
const nextRcConfig = normalizeVideoRcConfig({
|
||||||
|
...videoRcConfig,
|
||||||
|
[currentCodec]: applySliderToCodec(videoRcConfig[currentCodec], currentSliders),
|
||||||
|
});
|
||||||
|
send("setVideoRc", { params: nextRcConfig }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`Failed to set video RC: ${resp.error.data || "Unknown error"}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setVideoRcConfig(nextRcConfig);
|
||||||
|
setRcDraftConfig(nextRcConfig);
|
||||||
|
setRcSliderValues(sliderStateFromConfig(nextRcConfig));
|
||||||
|
notifications.success("Video RC updated");
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateRcDraftField = (
|
||||||
|
codec: "h264" | "h265",
|
||||||
|
field: keyof RcQpParams,
|
||||||
|
value: number,
|
||||||
|
) => {
|
||||||
|
setRcDraftConfig(prev => {
|
||||||
|
if (!prev) return prev;
|
||||||
|
const nextCodec = { ...prev[codec], [field]: value };
|
||||||
|
const normalizedCodec = normalizeRcCodec(nextCodec);
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[codec]: normalizedCodec,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const openRcAdvancedModal = () => {
|
||||||
|
const nextDraft = normalizeVideoRcConfig({
|
||||||
|
...videoRcConfig,
|
||||||
|
[currentCodec]: applySliderToCodec(videoRcConfig[currentCodec], currentSliders),
|
||||||
|
});
|
||||||
|
setRcDraftConfig(nextDraft);
|
||||||
|
setShowRcAdvanced(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyRcAdvancedConfig = () => {
|
||||||
|
if (!rcDraftConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const normalized = normalizeVideoRcConfig(rcDraftConfig);
|
||||||
|
send("setVideoRc", { params: normalized }, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`Failed to set video RC: ${resp.error.data || "Unknown error"}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setVideoRcConfig(normalized);
|
||||||
|
setRcDraftConfig(normalized);
|
||||||
|
setRcSliderValues(sliderStateFromConfig(normalized));
|
||||||
|
notifications.success("Video RC updated");
|
||||||
|
setShowRcAdvanced(false);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRcAdvancedForm = (codec: "h264" | "h265") => {
|
||||||
|
const current = rcDraftConfig?.[codec];
|
||||||
|
if (!current) return null;
|
||||||
|
|
||||||
|
const fields: Array<{
|
||||||
|
key: keyof RcQpParams;
|
||||||
|
label: string;
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
|
}> = [
|
||||||
|
{ key: "s32FirstFrameStartQp", label: "FirstFrameStartQp", min: 1, max: 51 },
|
||||||
|
{ key: "u32StepQp", label: "StepQp", min: 1, max: 51 },
|
||||||
|
{ key: "u32MaxQp", label: "MaxQp", min: 1, max: 51 },
|
||||||
|
{ key: "u32MinQp", label: "MinQp", min: 1, max: Number(current.u32MaxQp) || 51 },
|
||||||
|
{ key: "u32MaxIQp", label: "MaxIQp", min: 1, max: 51 },
|
||||||
|
{ key: "u32MinIQp", label: "MinIQp", min: 1, max: Number(current.u32MaxIQp) || 51 },
|
||||||
|
{ key: "s32DeltIpQp", label: "DeltIpQp", min: -7, max: 7 },
|
||||||
|
{ key: "s32MaxReEncodeTimes", label: "MaxReEncodeTimes", min: 0, max: 3 },
|
||||||
|
{ key: "u32FrmMaxQp", label: "FrmMaxQp", min: 1, max: 51 },
|
||||||
|
{ key: "u32FrmMinQp", label: "FrmMinQp", min: 1, max: 51 },
|
||||||
|
{ key: "u32FrmMaxIQp", label: "FrmMaxIQp", min: 1, max: 51 },
|
||||||
|
{ key: "u32FrmMinIQp", label: "FrmMinIQp", min: 1, max: 51 },
|
||||||
|
{ key: "u32MotionStaticSwitchFrmQp", label: "MotionStaticSwitchFrmQp", min: 1, max: 51 },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 gap-3">
|
||||||
|
{fields.map(field => (
|
||||||
|
<div key={`${codec}-${field.key}`} className="flex items-center justify-between gap-3">
|
||||||
|
<Text className="text-xs">{field.label}</Text>
|
||||||
|
<InputNumber
|
||||||
|
size="small"
|
||||||
|
value={current[field.key]}
|
||||||
|
min={field.min}
|
||||||
|
max={field.max}
|
||||||
|
step={1}
|
||||||
|
style={{ width: 140 }}
|
||||||
|
onChange={val => {
|
||||||
|
if (typeof val !== "number") return;
|
||||||
|
const safeValue =
|
||||||
|
field.min !== undefined && field.max !== undefined
|
||||||
|
? clamp(Number(val), field.min, field.max)
|
||||||
|
: Number(val);
|
||||||
|
updateRcDraftField(codec, field.key, safeValue);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
send("getNpuAppStatus", {}, resp => {
|
send("getNpuAppStatus", {}, resp => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
@@ -79,6 +336,20 @@ export default function SettingsVideoSide() {
|
|||||||
setStreamQuality(String(resp.result));
|
setStreamQuality(String(resp.result));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
send("getVideoRc", {}, resp => {
|
||||||
|
if ("error" in resp) {
|
||||||
|
notifications.error(`Failed to get video RC: ${resp.error.data || "Unknown error"}`);
|
||||||
|
const fallbackRc = normalizeVideoRcConfig(DEFAULT_VIDEO_RC_CONFIG);
|
||||||
|
setVideoRcConfig(fallbackRc);
|
||||||
|
setRcSliderValues(sliderStateFromConfig(fallbackRc));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rc = normalizeVideoRcConfig(toVideoRcConfigOrDefault(resp.result));
|
||||||
|
setVideoRcConfig(rc);
|
||||||
|
setRcSliderValues(sliderStateFromConfig(rc));
|
||||||
|
});
|
||||||
|
|
||||||
send("getEDID", {}, resp => {
|
send("getEDID", {}, resp => {
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(`Failed to get EDID: ${resp.error.data || "Unknown error"}`);
|
notifications.error(`Failed to get EDID: ${resp.error.data || "Unknown error"}`);
|
||||||
@@ -218,6 +489,116 @@ export default function SettingsVideoSide() {
|
|||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
|
|
||||||
|
<SettingsItem
|
||||||
|
title={$at("RC Control")}
|
||||||
|
description={$at("Adjust rate control QP settings for better balance between quality and bitrate")}
|
||||||
|
/>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItemNew
|
||||||
|
title={$at("StepQp")}
|
||||||
|
description={String(currentSliders.stepQp)}
|
||||||
|
className={"flex-col w-full h-[40px]"}
|
||||||
|
>
|
||||||
|
<Slider
|
||||||
|
min={1}
|
||||||
|
max={51}
|
||||||
|
step={1}
|
||||||
|
value={currentSliders.stepQp}
|
||||||
|
onChange={value => {
|
||||||
|
const nextStepQp = clamp(sliderValueToNumber(value), 1, 50);
|
||||||
|
setRcSliderValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
[currentCodec]: {
|
||||||
|
...prev[currentCodec],
|
||||||
|
stepQp: nextStepQp,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
className={"w-full"}
|
||||||
|
/>
|
||||||
|
</SettingsItemNew>
|
||||||
|
|
||||||
|
<SettingsItemNew
|
||||||
|
title={$at("MinQp")}
|
||||||
|
description={String(currentSliders.minQp)}
|
||||||
|
className={"flex-col w-full h-[40px]"}
|
||||||
|
>
|
||||||
|
<Slider
|
||||||
|
min={1}
|
||||||
|
max={50}
|
||||||
|
step={1}
|
||||||
|
value={currentSliders.minQp}
|
||||||
|
onChange={value => {
|
||||||
|
const nextMinQp = clamp(sliderValueToNumber(value), 1, 50);
|
||||||
|
setRcSliderValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
[currentCodec]: {
|
||||||
|
...prev[currentCodec],
|
||||||
|
minQp: nextMinQp,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
className={"w-full"}
|
||||||
|
/>
|
||||||
|
</SettingsItemNew>
|
||||||
|
|
||||||
|
<SettingsItemNew
|
||||||
|
title={$at("MinIQp")}
|
||||||
|
description={String(currentSliders.minIQp)}
|
||||||
|
className={"flex-col w-full h-[40px]"}
|
||||||
|
>
|
||||||
|
<Slider
|
||||||
|
min={1}
|
||||||
|
max={50}
|
||||||
|
step={1}
|
||||||
|
value={currentSliders.minIQp}
|
||||||
|
onChange={value => {
|
||||||
|
const nextMinIQp = clamp(sliderValueToNumber(value), 1, 50);
|
||||||
|
setRcSliderValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
[currentCodec]: {
|
||||||
|
...prev[currentCodec],
|
||||||
|
minIQp: nextMinIQp,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
className={"w-full"}
|
||||||
|
/>
|
||||||
|
</SettingsItemNew>
|
||||||
|
|
||||||
|
<SettingsItemNew
|
||||||
|
title={$at("DetlpQp")}
|
||||||
|
description={String(currentSliders.deltIpQp)}
|
||||||
|
className={"flex-col w-full h-[40px]"}
|
||||||
|
>
|
||||||
|
<Slider
|
||||||
|
min={-7}
|
||||||
|
max={7}
|
||||||
|
step={1}
|
||||||
|
value={currentSliders.deltIpQp}
|
||||||
|
onChange={value => {
|
||||||
|
const nextDeltIpQp = clamp(sliderValueToNumber(value), -7, 7);
|
||||||
|
setRcSliderValues(prev => ({
|
||||||
|
...prev,
|
||||||
|
[currentCodec]: {
|
||||||
|
...prev[currentCodec],
|
||||||
|
deltIpQp: nextDeltIpQp,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}}
|
||||||
|
className={"w-full"}
|
||||||
|
/>
|
||||||
|
</SettingsItemNew>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2">
|
||||||
|
<AntdButton onClick={openRcAdvancedModal}>
|
||||||
|
{$at("Advanced")}
|
||||||
|
</AntdButton>
|
||||||
|
<AntdButton type="primary" onClick={applyRcBasicConfig}>
|
||||||
|
{$at("Apply")}
|
||||||
|
</AntdButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title={$at("NPU Application")}
|
title={$at("NPU Application")}
|
||||||
@@ -362,25 +743,7 @@ export default function SettingsVideoSide() {
|
|||||||
}}
|
}}
|
||||||
options={[...edids, { value: "custom", label: "Custom" }]}
|
options={[...edids, { value: "custom", label: "Custom" }]}
|
||||||
/>
|
/>
|
||||||
{/* options={[...edids, { value: "custom", label: "Custom" }]}*/}
|
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
{/*<SelectMenuBasic*/}
|
|
||||||
{/* size="SM"*/}
|
|
||||||
{/* label=""*/}
|
|
||||||
{/* fullWidth*/}
|
|
||||||
{/* value={customEdidValue ? "custom" : edid || "asd"}*/}
|
|
||||||
{/* onChange={e => {*/}
|
|
||||||
{/* console.log(e.target.value)*/}
|
|
||||||
{/* if (e.target.value === "custom") {*/}
|
|
||||||
{/* setEdid("custom");*/}
|
|
||||||
{/* setCustomEdidValue("");*/}
|
|
||||||
{/* } else {*/}
|
|
||||||
{/* setCustomEdidValue(null);*/}
|
|
||||||
{/* handleEDIDChange(e.target.value as string);*/}
|
|
||||||
{/* }*/}
|
|
||||||
{/* }}*/}
|
|
||||||
{/* options={[...edids, { value: "custom", label: "Custom" }]}*/}
|
|
||||||
{/*/>*/}
|
|
||||||
|
|
||||||
{customEdidValue !== null && (
|
{customEdidValue !== null && (
|
||||||
<>
|
<>
|
||||||
@@ -418,6 +781,39 @@ export default function SettingsVideoSide() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={"h-[10vh]"}></div>
|
<div className={"h-[10vh]"}></div>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
title={
|
||||||
|
<Text strong style={{ fontSize: "16px" }}>
|
||||||
|
{$at("RC Advanced Config")}
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
open={showRcAdvanced}
|
||||||
|
onCancel={() => setShowRcAdvanced(false)}
|
||||||
|
onOk={applyRcAdvancedConfig}
|
||||||
|
okText={$at("Apply")}
|
||||||
|
cancelText={$at("Cancel")}
|
||||||
|
maskClosable={true}
|
||||||
|
keyboard={true}
|
||||||
|
width={520}
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
padding: "20px 24px",
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
borderBottom: "1px solid #f0f0f0",
|
||||||
|
padding: "16px 24px",
|
||||||
|
marginBottom: 0,
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Tabs
|
||||||
|
items={[
|
||||||
|
{ key: "h264", label: "H264", children: renderRcAdvancedForm("h264") },
|
||||||
|
{ key: "h265", label: "H265", children: renderRcAdvancedForm("h265") },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user