mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
feat(web): enhance configurable input regions
Make input-region calibration work across MJPEG and H.264 video modes, including robust four-sided black-border detection and initial-selection edge snapping. Persist multiple reusable manual regions, keep manual and original-resolution choices mutually exclusive, migrate legacy settings, and localize the new controls for every supported language.
This commit is contained in:
@@ -163,6 +163,17 @@ type InputRegion struct {
|
||||
Height int `json:"height"`
|
||||
Resolutions []OriginalResolution `json:"resolutions,omitempty"`
|
||||
SelectedResolution string `json:"selectedResolution"`
|
||||
Regions []ManualRegion `json:"regions,omitempty"`
|
||||
SelectedRegion string `json:"selectedRegion"`
|
||||
}
|
||||
|
||||
type ManualRegion struct {
|
||||
FrameWidth int `json:"frameWidth"`
|
||||
FrameHeight int `json:"frameHeight"`
|
||||
Left int `json:"left"`
|
||||
Top int `json:"top"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
type OriginalResolution struct {
|
||||
@@ -180,6 +191,8 @@ type SetInputRegionReq struct {
|
||||
Height *int `json:"height,omitempty"`
|
||||
Resolutions *[]OriginalResolution `json:"resolutions,omitempty"`
|
||||
SelectedResolution *string `json:"selectedResolution,omitempty"`
|
||||
Regions *[]ManualRegion `json:"regions,omitempty"`
|
||||
SelectedRegion *string `json:"selectedRegion,omitempty"`
|
||||
}
|
||||
|
||||
type GetInputRegionRsp struct {
|
||||
|
||||
@@ -99,7 +99,20 @@ func (s *Service) SetInputRegion(c *gin.Context) {
|
||||
if req.SelectedResolution != nil {
|
||||
region.SelectedResolution = *req.SelectedResolution
|
||||
}
|
||||
if err := validateSelectedResolution(region.SelectedResolution, region.Resolutions); err != nil {
|
||||
if req.Regions != nil {
|
||||
if err := validateManualRegions(*req.Regions); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid manual regions")
|
||||
return
|
||||
}
|
||||
region.Regions = *req.Regions
|
||||
if len(region.Regions) == 0 {
|
||||
clearLegacyManualRegion(region)
|
||||
}
|
||||
}
|
||||
if req.SelectedRegion != nil {
|
||||
region.SelectedRegion = *req.SelectedRegion
|
||||
}
|
||||
if err := validateSelections(*region); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid selected resolution")
|
||||
return
|
||||
}
|
||||
@@ -127,6 +140,8 @@ func (s *Service) SetInputRegion(c *gin.Context) {
|
||||
if previous != nil {
|
||||
region.Resolutions = previous.Resolutions
|
||||
region.SelectedResolution = previous.SelectedResolution
|
||||
region.Regions = previous.Regions
|
||||
region.SelectedRegion = previous.SelectedRegion
|
||||
}
|
||||
if req.Resolutions != nil {
|
||||
if err := validateOriginalResolutions(*req.Resolutions); err != nil {
|
||||
@@ -138,7 +153,20 @@ func (s *Service) SetInputRegion(c *gin.Context) {
|
||||
if req.SelectedResolution != nil {
|
||||
region.SelectedResolution = *req.SelectedResolution
|
||||
}
|
||||
if err := validateSelectedResolution(region.SelectedResolution, region.Resolutions); err != nil {
|
||||
if req.Regions != nil {
|
||||
if err := validateManualRegions(*req.Regions); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid manual regions")
|
||||
return
|
||||
}
|
||||
region.Regions = *req.Regions
|
||||
if len(region.Regions) == 0 {
|
||||
clearLegacyManualRegion(®ion)
|
||||
}
|
||||
}
|
||||
if req.SelectedRegion != nil {
|
||||
region.SelectedRegion = *req.SelectedRegion
|
||||
}
|
||||
if err := validateSelections(region); err != nil {
|
||||
rsp.ErrRsp(c, -1, "invalid selected resolution")
|
||||
return
|
||||
}
|
||||
@@ -176,10 +204,68 @@ func validateSelectedResolution(selected string, resolutions []proto.OriginalRes
|
||||
return errors.New("selected resolution not found")
|
||||
}
|
||||
|
||||
func validateManualRegions(regions []proto.ManualRegion) error {
|
||||
seen := make(map[string]struct{}, len(regions))
|
||||
for _, region := range regions {
|
||||
if err := validateManualRegion(region); err != nil {
|
||||
return err
|
||||
}
|
||||
key := manualRegionKey(region)
|
||||
if _, ok := seen[key]; ok {
|
||||
return errors.New("duplicate manual region")
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateManualRegion(region proto.ManualRegion) error {
|
||||
return validateInputRegion(proto.InputRegion{
|
||||
Mode: "manual",
|
||||
FrameWidth: region.FrameWidth,
|
||||
FrameHeight: region.FrameHeight,
|
||||
Left: region.Left,
|
||||
Top: region.Top,
|
||||
Width: region.Width,
|
||||
Height: region.Height,
|
||||
})
|
||||
}
|
||||
|
||||
func validateSelections(region proto.InputRegion) error {
|
||||
if region.SelectedResolution != "" && region.SelectedRegion != "" {
|
||||
return errors.New("only one input region source may be selected")
|
||||
}
|
||||
if err := validateSelectedResolution(region.SelectedResolution, region.Resolutions); err != nil {
|
||||
return err
|
||||
}
|
||||
if region.SelectedRegion == "" {
|
||||
return nil
|
||||
}
|
||||
for _, item := range region.Regions {
|
||||
if region.SelectedRegion == manualRegionKey(item) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errors.New("selected manual region not found")
|
||||
}
|
||||
|
||||
func resolutionKey(resolution proto.OriginalResolution) string {
|
||||
return fmt.Sprintf("%dx%d", resolution.Width, resolution.Height)
|
||||
}
|
||||
|
||||
func manualRegionKey(region proto.ManualRegion) string {
|
||||
return fmt.Sprintf("%dx%d", region.Width, region.Height)
|
||||
}
|
||||
|
||||
func clearLegacyManualRegion(region *proto.InputRegion) {
|
||||
region.FrameWidth = 0
|
||||
region.FrameHeight = 0
|
||||
region.Left = 0
|
||||
region.Top = 0
|
||||
region.Width = 0
|
||||
region.Height = 0
|
||||
}
|
||||
|
||||
func inputRegionFromRequest(req proto.SetInputRegionReq) (proto.InputRegion, error) {
|
||||
if req.FrameWidth == nil || req.FrameHeight == nil || req.Left == nil ||
|
||||
req.Top == nil || req.Width == nil || req.Height == nil {
|
||||
@@ -236,9 +322,29 @@ func readInputRegion(path string) (*proto.InputRegion, error) {
|
||||
if region.Mode == "" {
|
||||
region.Mode = "manual"
|
||||
}
|
||||
if len(region.Regions) == 0 && region.FrameWidth > 0 && region.FrameHeight > 0 {
|
||||
manual := proto.ManualRegion{
|
||||
FrameWidth: region.FrameWidth,
|
||||
FrameHeight: region.FrameHeight,
|
||||
Left: region.Left,
|
||||
Top: region.Top,
|
||||
Width: region.Width,
|
||||
Height: region.Height,
|
||||
}
|
||||
region.Regions = []proto.ManualRegion{manual}
|
||||
if region.SelectedResolution == "" {
|
||||
region.SelectedRegion = manualRegionKey(manual)
|
||||
}
|
||||
}
|
||||
if err := validateInputRegion(*region); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err := validateManualRegions(region.Regions); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
if err := validateSelections(*region); err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return region, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user