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:
watermeko
2026-08-24 12:06:38 +00:00
committed by Guoguo
parent 9e6d280826
commit 2ba45a2147
39 changed files with 536 additions and 179 deletions

View File

@@ -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 {

View File

@@ -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(&region)
}
}
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
}