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"`
|
Height int `json:"height"`
|
||||||
Resolutions []OriginalResolution `json:"resolutions,omitempty"`
|
Resolutions []OriginalResolution `json:"resolutions,omitempty"`
|
||||||
SelectedResolution string `json:"selectedResolution"`
|
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 {
|
type OriginalResolution struct {
|
||||||
@@ -180,6 +191,8 @@ type SetInputRegionReq struct {
|
|||||||
Height *int `json:"height,omitempty"`
|
Height *int `json:"height,omitempty"`
|
||||||
Resolutions *[]OriginalResolution `json:"resolutions,omitempty"`
|
Resolutions *[]OriginalResolution `json:"resolutions,omitempty"`
|
||||||
SelectedResolution *string `json:"selectedResolution,omitempty"`
|
SelectedResolution *string `json:"selectedResolution,omitempty"`
|
||||||
|
Regions *[]ManualRegion `json:"regions,omitempty"`
|
||||||
|
SelectedRegion *string `json:"selectedRegion,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetInputRegionRsp struct {
|
type GetInputRegionRsp struct {
|
||||||
|
|||||||
@@ -99,7 +99,20 @@ func (s *Service) SetInputRegion(c *gin.Context) {
|
|||||||
if req.SelectedResolution != nil {
|
if req.SelectedResolution != nil {
|
||||||
region.SelectedResolution = *req.SelectedResolution
|
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")
|
rsp.ErrRsp(c, -1, "invalid selected resolution")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -127,6 +140,8 @@ func (s *Service) SetInputRegion(c *gin.Context) {
|
|||||||
if previous != nil {
|
if previous != nil {
|
||||||
region.Resolutions = previous.Resolutions
|
region.Resolutions = previous.Resolutions
|
||||||
region.SelectedResolution = previous.SelectedResolution
|
region.SelectedResolution = previous.SelectedResolution
|
||||||
|
region.Regions = previous.Regions
|
||||||
|
region.SelectedRegion = previous.SelectedRegion
|
||||||
}
|
}
|
||||||
if req.Resolutions != nil {
|
if req.Resolutions != nil {
|
||||||
if err := validateOriginalResolutions(*req.Resolutions); err != nil {
|
if err := validateOriginalResolutions(*req.Resolutions); err != nil {
|
||||||
@@ -138,7 +153,20 @@ func (s *Service) SetInputRegion(c *gin.Context) {
|
|||||||
if req.SelectedResolution != nil {
|
if req.SelectedResolution != nil {
|
||||||
region.SelectedResolution = *req.SelectedResolution
|
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")
|
rsp.ErrRsp(c, -1, "invalid selected resolution")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -176,10 +204,68 @@ func validateSelectedResolution(selected string, resolutions []proto.OriginalRes
|
|||||||
return errors.New("selected resolution not found")
|
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 {
|
func resolutionKey(resolution proto.OriginalResolution) string {
|
||||||
return fmt.Sprintf("%dx%d", resolution.Width, resolution.Height)
|
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) {
|
func inputRegionFromRequest(req proto.SetInputRegionReq) (proto.InputRegion, error) {
|
||||||
if req.FrameWidth == nil || req.FrameHeight == nil || req.Left == nil ||
|
if req.FrameWidth == nil || req.FrameHeight == nil || req.Left == nil ||
|
||||||
req.Top == nil || req.Width == nil || req.Height == nil {
|
req.Top == nil || req.Width == nil || req.Height == nil {
|
||||||
@@ -236,9 +322,29 @@ func readInputRegion(path string) (*proto.InputRegion, error) {
|
|||||||
if region.Mode == "" {
|
if region.Mode == "" {
|
||||||
region.Mode = "manual"
|
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 {
|
if err := validateInputRegion(*region); err != nil {
|
||||||
return nil, 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
|
return region, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -48,11 +48,17 @@ export function setInputRegion(region: InputRegion) {
|
|||||||
return http.post('/api/vm/input-region', { mode: 'manual', ...region });
|
return http.post('/api/vm/input-region', { mode: 'manual', ...region });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setInputRegionConfig(region: InputRegion, selectedResolution: string) {
|
export function setInputRegionConfig(
|
||||||
|
region: InputRegion,
|
||||||
|
regions: InputRegion[],
|
||||||
|
selectedRegion: string
|
||||||
|
) {
|
||||||
return http.post('/api/vm/input-region', {
|
return http.post('/api/vm/input-region', {
|
||||||
mode: 'manual',
|
mode: 'manual',
|
||||||
...region,
|
...region,
|
||||||
selectedResolution
|
regions,
|
||||||
|
selectedRegion,
|
||||||
|
selectedResolution: ''
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,17 +72,36 @@ export function setOriginalResolutions(mode: ControlRegionMode, resolutions: Ori
|
|||||||
|
|
||||||
export function setOriginalResolutionConfig(
|
export function setOriginalResolutionConfig(
|
||||||
resolutions: OriginalResolution[],
|
resolutions: OriginalResolution[],
|
||||||
selectedResolution: string
|
selectedResolution: string,
|
||||||
|
selectedRegion: string
|
||||||
) {
|
) {
|
||||||
return http.post('/api/vm/input-region', {
|
return http.post('/api/vm/input-region', {
|
||||||
mode: 'manual',
|
mode: 'manual',
|
||||||
resolutions,
|
resolutions,
|
||||||
selectedResolution
|
selectedResolution,
|
||||||
|
selectedRegion
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setSelectedOriginalResolution(selectedResolution: string) {
|
export function setSelectedOriginalResolution(selectedResolution: string) {
|
||||||
return http.post('/api/vm/input-region', { mode: 'manual', selectedResolution });
|
return http.post('/api/vm/input-region', {
|
||||||
|
mode: 'manual',
|
||||||
|
selectedResolution,
|
||||||
|
selectedRegion: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setManualRegions(
|
||||||
|
regions: InputRegion[],
|
||||||
|
selectedRegion: string,
|
||||||
|
selectedResolution = ''
|
||||||
|
) {
|
||||||
|
return http.post('/api/vm/input-region', {
|
||||||
|
mode: 'manual',
|
||||||
|
regions,
|
||||||
|
selectedRegion,
|
||||||
|
selectedResolution
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// get memory limit
|
// get memory limit
|
||||||
|
|||||||
@@ -73,9 +73,10 @@ const ca = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
"El calibratge pot fallar quan l'aplicació de l'usuari tingui un fons completament negre.",
|
"El calibratge pot fallar quan l'aplicació de l'usuari tingui un fons completament negre.",
|
||||||
manual: 'Manual',
|
manual: 'Manual',
|
||||||
|
selectedResolution: "Resolució de l'àrea seleccionada",
|
||||||
|
unused: 'No utilitzada',
|
||||||
originalResolution: 'Resolució original',
|
originalResolution: 'Resolució original',
|
||||||
selectResolution: 'Selecciona la resolució original',
|
selectResolution: 'Selecciona la resolució original',
|
||||||
useSelectedArea: "Utilitza la resolució de l'àrea seleccionada",
|
|
||||||
addResolution: 'Afegeix una resolució personalitzada',
|
addResolution: 'Afegeix una resolució personalitzada',
|
||||||
add: 'Afegeix',
|
add: 'Afegeix',
|
||||||
duplicateResolution: 'Aquesta resolució ja existeix.',
|
duplicateResolution: 'Aquesta resolució ja existeix.',
|
||||||
|
|||||||
@@ -73,9 +73,10 @@ const cz = {
|
|||||||
auto: 'Automaticky',
|
auto: 'Automaticky',
|
||||||
autoWarning: 'Kalibrace může selhat, pokud má uživatelská aplikace zcela černé pozadí.',
|
autoWarning: 'Kalibrace může selhat, pokud má uživatelská aplikace zcela černé pozadí.',
|
||||||
manual: 'Ručně',
|
manual: 'Ručně',
|
||||||
|
selectedResolution: 'Rozlišení vybrané oblasti',
|
||||||
|
unused: 'Nepoužito',
|
||||||
originalResolution: 'Původní rozlišení',
|
originalResolution: 'Původní rozlišení',
|
||||||
selectResolution: 'Vyberte původní rozlišení',
|
selectResolution: 'Vyberte původní rozlišení',
|
||||||
useSelectedArea: 'Použít rozlišení vybrané oblasti',
|
|
||||||
addResolution: 'Přidat vlastní rozlišení',
|
addResolution: 'Přidat vlastní rozlišení',
|
||||||
add: 'Přidat',
|
add: 'Přidat',
|
||||||
duplicateResolution: 'Toto rozlišení již existuje.',
|
duplicateResolution: 'Toto rozlišení již existuje.',
|
||||||
|
|||||||
@@ -73,9 +73,10 @@ const da = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'Kalibreringen kan mislykkes, hvis brugerprogrammet har en helt sort baggrund.',
|
'Kalibreringen kan mislykkes, hvis brugerprogrammet har en helt sort baggrund.',
|
||||||
manual: 'Manuel',
|
manual: 'Manuel',
|
||||||
|
selectedResolution: 'Valgt områdeopløsning',
|
||||||
|
unused: 'Ikke i brug',
|
||||||
originalResolution: 'Oprindelig opløsning',
|
originalResolution: 'Oprindelig opløsning',
|
||||||
selectResolution: 'Vælg oprindelig opløsning',
|
selectResolution: 'Vælg oprindelig opløsning',
|
||||||
useSelectedArea: 'Brug det valgte områdes opløsning',
|
|
||||||
addResolution: 'Tilføj brugerdefineret opløsning',
|
addResolution: 'Tilføj brugerdefineret opløsning',
|
||||||
add: 'Tilføj',
|
add: 'Tilføj',
|
||||||
duplicateResolution: 'Denne opløsning findes allerede.',
|
duplicateResolution: 'Denne opløsning findes allerede.',
|
||||||
|
|||||||
@@ -76,9 +76,10 @@ const de = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'Die Kalibrierung kann fehlschlagen, wenn die Benutzeranwendung einen vollständig schwarzen Hintergrund hat.',
|
'Die Kalibrierung kann fehlschlagen, wenn die Benutzeranwendung einen vollständig schwarzen Hintergrund hat.',
|
||||||
manual: 'Manuell',
|
manual: 'Manuell',
|
||||||
|
selectedResolution: 'Auflösung des ausgewählten Bereichs',
|
||||||
|
unused: 'Nicht verwendet',
|
||||||
originalResolution: 'Originalauflösung',
|
originalResolution: 'Originalauflösung',
|
||||||
selectResolution: 'Originalauflösung auswählen',
|
selectResolution: 'Originalauflösung auswählen',
|
||||||
useSelectedArea: 'Auflösung des ausgewählten Bereichs verwenden',
|
|
||||||
addResolution: 'Benutzerdefinierte Auflösung hinzufügen',
|
addResolution: 'Benutzerdefinierte Auflösung hinzufügen',
|
||||||
add: 'Hinzufügen',
|
add: 'Hinzufügen',
|
||||||
duplicateResolution: 'Diese Auflösung ist bereits vorhanden.',
|
duplicateResolution: 'Diese Auflösung ist bereits vorhanden.',
|
||||||
|
|||||||
@@ -74,9 +74,10 @@ const en = {
|
|||||||
auto: 'Auto',
|
auto: 'Auto',
|
||||||
autoWarning: 'Calibration may fail when the user application has a pure black background.',
|
autoWarning: 'Calibration may fail when the user application has a pure black background.',
|
||||||
manual: 'Manual',
|
manual: 'Manual',
|
||||||
|
selectedResolution: 'Selected Area Resolution',
|
||||||
|
unused: 'Not used',
|
||||||
originalResolution: 'Original Resolution',
|
originalResolution: 'Original Resolution',
|
||||||
selectResolution: 'Select original resolution',
|
selectResolution: 'Select original resolution',
|
||||||
useSelectedArea: 'Use selected area resolution',
|
|
||||||
addResolution: 'Add custom resolution',
|
addResolution: 'Add custom resolution',
|
||||||
add: 'Add',
|
add: 'Add',
|
||||||
duplicateResolution: 'This resolution already exists.',
|
duplicateResolution: 'This resolution already exists.',
|
||||||
|
|||||||
@@ -74,9 +74,10 @@ const es = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'La calibración puede fallar si la aplicación del usuario tiene un fondo completamente negro.',
|
'La calibración puede fallar si la aplicación del usuario tiene un fondo completamente negro.',
|
||||||
manual: 'Manual',
|
manual: 'Manual',
|
||||||
|
selectedResolution: 'Resolución del área seleccionada',
|
||||||
|
unused: 'Sin usar',
|
||||||
originalResolution: 'Resolución original',
|
originalResolution: 'Resolución original',
|
||||||
selectResolution: 'Seleccionar resolución original',
|
selectResolution: 'Seleccionar resolución original',
|
||||||
useSelectedArea: 'Usar la resolución del área seleccionada',
|
|
||||||
addResolution: 'Añadir resolución personalizada',
|
addResolution: 'Añadir resolución personalizada',
|
||||||
add: 'Añadir',
|
add: 'Añadir',
|
||||||
duplicateResolution: 'Esta resolución ya existe.',
|
duplicateResolution: 'Esta resolución ya existe.',
|
||||||
|
|||||||
@@ -76,9 +76,10 @@ const fr = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
"L'étalonnage peut échouer si l'application utilisateur présente un arrière-plan entièrement noir.",
|
"L'étalonnage peut échouer si l'application utilisateur présente un arrière-plan entièrement noir.",
|
||||||
manual: 'Manuel',
|
manual: 'Manuel',
|
||||||
|
selectedResolution: 'Résolution de la zone sélectionnée',
|
||||||
|
unused: 'Non utilisée',
|
||||||
originalResolution: "Résolution d'origine",
|
originalResolution: "Résolution d'origine",
|
||||||
selectResolution: "Sélectionner la résolution d'origine",
|
selectResolution: "Sélectionner la résolution d'origine",
|
||||||
useSelectedArea: 'Utiliser la résolution de la zone sélectionnée',
|
|
||||||
addResolution: 'Ajouter une résolution personnalisée',
|
addResolution: 'Ajouter une résolution personnalisée',
|
||||||
add: 'Ajouter',
|
add: 'Ajouter',
|
||||||
duplicateResolution: 'Cette résolution existe déjà.',
|
duplicateResolution: 'Cette résolution existe déjà.',
|
||||||
|
|||||||
@@ -75,9 +75,10 @@ const hu = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'A kalibrálás sikertelen lehet, ha a felhasználói alkalmazás háttere teljesen fekete.',
|
'A kalibrálás sikertelen lehet, ha a felhasználói alkalmazás háttere teljesen fekete.',
|
||||||
manual: 'Kézi',
|
manual: 'Kézi',
|
||||||
|
selectedResolution: 'Kijelölt terület felbontása',
|
||||||
|
unused: 'Nincs használatban',
|
||||||
originalResolution: 'Eredeti felbontás',
|
originalResolution: 'Eredeti felbontás',
|
||||||
selectResolution: 'Válassza ki az eredeti felbontást',
|
selectResolution: 'Válassza ki az eredeti felbontást',
|
||||||
useSelectedArea: 'A kijelölt terület felbontásának használata',
|
|
||||||
addResolution: 'Egyéni felbontás hozzáadása',
|
addResolution: 'Egyéni felbontás hozzáadása',
|
||||||
add: 'Hozzáadás',
|
add: 'Hozzáadás',
|
||||||
duplicateResolution: 'Ez a felbontás már létezik.',
|
duplicateResolution: 'Ez a felbontás már létezik.',
|
||||||
|
|||||||
@@ -73,9 +73,10 @@ const id = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'Kalibrasi mungkin gagal jika aplikasi pengguna menggunakan latar belakang hitam pekat.',
|
'Kalibrasi mungkin gagal jika aplikasi pengguna menggunakan latar belakang hitam pekat.',
|
||||||
manual: 'Manual',
|
manual: 'Manual',
|
||||||
|
selectedResolution: 'Resolusi Area Terpilih',
|
||||||
|
unused: 'Tidak digunakan',
|
||||||
originalResolution: 'Resolusi Asli',
|
originalResolution: 'Resolusi Asli',
|
||||||
selectResolution: 'Pilih resolusi asli',
|
selectResolution: 'Pilih resolusi asli',
|
||||||
useSelectedArea: 'Gunakan resolusi area yang dipilih',
|
|
||||||
addResolution: 'Tambahkan resolusi khusus',
|
addResolution: 'Tambahkan resolusi khusus',
|
||||||
add: 'Tambah',
|
add: 'Tambah',
|
||||||
duplicateResolution: 'Resolusi ini sudah ada.',
|
duplicateResolution: 'Resolusi ini sudah ada.',
|
||||||
|
|||||||
@@ -75,9 +75,10 @@ const it = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
"La calibrazione potrebbe non riuscire se l'applicazione utente ha uno sfondo completamente nero.",
|
"La calibrazione potrebbe non riuscire se l'applicazione utente ha uno sfondo completamente nero.",
|
||||||
manual: 'Manuale',
|
manual: 'Manuale',
|
||||||
|
selectedResolution: 'Risoluzione area selezionata',
|
||||||
|
unused: 'Non utilizzata',
|
||||||
originalResolution: 'Risoluzione originale',
|
originalResolution: 'Risoluzione originale',
|
||||||
selectResolution: 'Seleziona la risoluzione originale',
|
selectResolution: 'Seleziona la risoluzione originale',
|
||||||
useSelectedArea: "Usa la risoluzione dell'area selezionata",
|
|
||||||
addResolution: 'Aggiungi una risoluzione personalizzata',
|
addResolution: 'Aggiungi una risoluzione personalizzata',
|
||||||
add: 'Aggiungi',
|
add: 'Aggiungi',
|
||||||
duplicateResolution: 'Questa risoluzione esiste già.',
|
duplicateResolution: 'Questa risoluzione esiste già.',
|
||||||
|
|||||||
@@ -73,9 +73,10 @@ const ja = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'ユーザーアプリケーションの背景が完全な黒の場合、補正に失敗することがあります。',
|
'ユーザーアプリケーションの背景が完全な黒の場合、補正に失敗することがあります。',
|
||||||
manual: '手動',
|
manual: '手動',
|
||||||
|
selectedResolution: '選択領域の解像度',
|
||||||
|
unused: '未使用',
|
||||||
originalResolution: '元の解像度',
|
originalResolution: '元の解像度',
|
||||||
selectResolution: '元の解像度を選択',
|
selectResolution: '元の解像度を選択',
|
||||||
useSelectedArea: '選択領域の解像度を使用',
|
|
||||||
addResolution: 'カスタム解像度を追加',
|
addResolution: 'カスタム解像度を追加',
|
||||||
add: '追加',
|
add: '追加',
|
||||||
duplicateResolution: 'この解像度はすでに存在します。',
|
duplicateResolution: 'この解像度はすでに存在します。',
|
||||||
|
|||||||
@@ -71,9 +71,10 @@ const ko = {
|
|||||||
auto: '자동',
|
auto: '자동',
|
||||||
autoWarning: '사용자 애플리케이션의 배경이 완전히 검은색이면 보정에 실패할 수 있습니다.',
|
autoWarning: '사용자 애플리케이션의 배경이 완전히 검은색이면 보정에 실패할 수 있습니다.',
|
||||||
manual: '수동',
|
manual: '수동',
|
||||||
|
selectedResolution: '선택 영역 해상도',
|
||||||
|
unused: '사용 안 함',
|
||||||
originalResolution: '원본 해상도',
|
originalResolution: '원본 해상도',
|
||||||
selectResolution: '원본 해상도 선택',
|
selectResolution: '원본 해상도 선택',
|
||||||
useSelectedArea: '선택한 영역의 해상도 사용',
|
|
||||||
addResolution: '사용자 지정 해상도 추가',
|
addResolution: '사용자 지정 해상도 추가',
|
||||||
add: '추가',
|
add: '추가',
|
||||||
duplicateResolution: '이미 존재하는 해상도입니다.',
|
duplicateResolution: '이미 존재하는 해상도입니다.',
|
||||||
|
|||||||
@@ -74,9 +74,10 @@ const nb = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'Kalibreringen kan mislykkes hvis brukerprogrammet har en helt svart bakgrunn.',
|
'Kalibreringen kan mislykkes hvis brukerprogrammet har en helt svart bakgrunn.',
|
||||||
manual: 'Manuell',
|
manual: 'Manuell',
|
||||||
|
selectedResolution: 'Oppløsning for valgt område',
|
||||||
|
unused: 'Ikke i bruk',
|
||||||
originalResolution: 'Opprinnelig oppløsning',
|
originalResolution: 'Opprinnelig oppløsning',
|
||||||
selectResolution: 'Velg opprinnelig oppløsning',
|
selectResolution: 'Velg opprinnelig oppløsning',
|
||||||
useSelectedArea: 'Bruk oppløsningen til det valgte området',
|
|
||||||
addResolution: 'Legg til egendefinert oppløsning',
|
addResolution: 'Legg til egendefinert oppløsning',
|
||||||
add: 'Legg til',
|
add: 'Legg til',
|
||||||
duplicateResolution: 'Denne oppløsningen finnes allerede.',
|
duplicateResolution: 'Denne oppløsningen finnes allerede.',
|
||||||
|
|||||||
@@ -75,9 +75,10 @@ const nl = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'De kalibratie kan mislukken als de gebruikerstoepassing een volledig zwarte achtergrond heeft.',
|
'De kalibratie kan mislukken als de gebruikerstoepassing een volledig zwarte achtergrond heeft.',
|
||||||
manual: 'Handmatig',
|
manual: 'Handmatig',
|
||||||
|
selectedResolution: 'Resolutie van geselecteerd gebied',
|
||||||
|
unused: 'Niet gebruikt',
|
||||||
originalResolution: 'Oorspronkelijke resolutie',
|
originalResolution: 'Oorspronkelijke resolutie',
|
||||||
selectResolution: 'Oorspronkelijke resolutie selecteren',
|
selectResolution: 'Oorspronkelijke resolutie selecteren',
|
||||||
useSelectedArea: 'Resolutie van geselecteerd gebied gebruiken',
|
|
||||||
addResolution: 'Aangepaste resolutie toevoegen',
|
addResolution: 'Aangepaste resolutie toevoegen',
|
||||||
add: 'Toevoegen',
|
add: 'Toevoegen',
|
||||||
duplicateResolution: 'Deze resolutie bestaat al.',
|
duplicateResolution: 'Deze resolutie bestaat al.',
|
||||||
|
|||||||
@@ -74,9 +74,10 @@ const pl = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'Kalibracja może się nie powieść, gdy aplikacja użytkownika ma całkowicie czarne tło.',
|
'Kalibracja może się nie powieść, gdy aplikacja użytkownika ma całkowicie czarne tło.',
|
||||||
manual: 'Ręczna',
|
manual: 'Ręczna',
|
||||||
|
selectedResolution: 'Rozdzielczość zaznaczonego obszaru',
|
||||||
|
unused: 'Nieużywane',
|
||||||
originalResolution: 'Oryginalna rozdzielczość',
|
originalResolution: 'Oryginalna rozdzielczość',
|
||||||
selectResolution: 'Wybierz oryginalną rozdzielczość',
|
selectResolution: 'Wybierz oryginalną rozdzielczość',
|
||||||
useSelectedArea: 'Użyj rozdzielczości zaznaczonego obszaru',
|
|
||||||
addResolution: 'Dodaj niestandardową rozdzielczość',
|
addResolution: 'Dodaj niestandardową rozdzielczość',
|
||||||
add: 'Dodaj',
|
add: 'Dodaj',
|
||||||
duplicateResolution: 'Ta rozdzielczość już istnieje.',
|
duplicateResolution: 'Ta rozdzielczość już istnieje.',
|
||||||
|
|||||||
@@ -73,9 +73,10 @@ const pt_br = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'A calibração pode falhar se o aplicativo do usuário tiver um fundo totalmente preto.',
|
'A calibração pode falhar se o aplicativo do usuário tiver um fundo totalmente preto.',
|
||||||
manual: 'Manual',
|
manual: 'Manual',
|
||||||
|
selectedResolution: 'Resolução da área selecionada',
|
||||||
|
unused: 'Não utilizado',
|
||||||
originalResolution: 'Resolução original',
|
originalResolution: 'Resolução original',
|
||||||
selectResolution: 'Selecionar resolução original',
|
selectResolution: 'Selecionar resolução original',
|
||||||
useSelectedArea: 'Usar a resolução da área selecionada',
|
|
||||||
addResolution: 'Adicionar resolução personalizada',
|
addResolution: 'Adicionar resolução personalizada',
|
||||||
add: 'Adicionar',
|
add: 'Adicionar',
|
||||||
duplicateResolution: 'Esta resolução já existe.',
|
duplicateResolution: 'Esta resolução já existe.',
|
||||||
|
|||||||
@@ -73,9 +73,10 @@ const ru = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'Калибровка может завершиться неудачей, если фон пользовательского приложения полностью чёрный.',
|
'Калибровка может завершиться неудачей, если фон пользовательского приложения полностью чёрный.',
|
||||||
manual: 'Вручную',
|
manual: 'Вручную',
|
||||||
|
selectedResolution: 'Разрешение выбранной области',
|
||||||
|
unused: 'Не используется',
|
||||||
originalResolution: 'Исходное разрешение',
|
originalResolution: 'Исходное разрешение',
|
||||||
selectResolution: 'Выберите исходное разрешение',
|
selectResolution: 'Выберите исходное разрешение',
|
||||||
useSelectedArea: 'Использовать разрешение выбранной области',
|
|
||||||
addResolution: 'Добавить пользовательское разрешение',
|
addResolution: 'Добавить пользовательское разрешение',
|
||||||
add: 'Добавить',
|
add: 'Добавить',
|
||||||
duplicateResolution: 'Такое разрешение уже существует.',
|
duplicateResolution: 'Такое разрешение уже существует.',
|
||||||
|
|||||||
@@ -70,9 +70,10 @@ const se = {
|
|||||||
auto: 'Automatisk',
|
auto: 'Automatisk',
|
||||||
autoWarning: 'Kalibreringen kan misslyckas om användarprogrammet har en helsvart bakgrund.',
|
autoWarning: 'Kalibreringen kan misslyckas om användarprogrammet har en helsvart bakgrund.',
|
||||||
manual: 'Manuell',
|
manual: 'Manuell',
|
||||||
|
selectedResolution: 'Valt områdesupplösning',
|
||||||
|
unused: 'Används inte',
|
||||||
originalResolution: 'Ursprunglig upplösning',
|
originalResolution: 'Ursprunglig upplösning',
|
||||||
selectResolution: 'Välj ursprunglig upplösning',
|
selectResolution: 'Välj ursprunglig upplösning',
|
||||||
useSelectedArea: 'Använd upplösningen för det valda området',
|
|
||||||
addResolution: 'Lägg till anpassad upplösning',
|
addResolution: 'Lägg till anpassad upplösning',
|
||||||
add: 'Lägg till',
|
add: 'Lägg till',
|
||||||
duplicateResolution: 'Den här upplösningen finns redan.',
|
duplicateResolution: 'Den här upplösningen finns redan.',
|
||||||
|
|||||||
@@ -70,9 +70,10 @@ const th = {
|
|||||||
auto: 'อัตโนมัติ',
|
auto: 'อัตโนมัติ',
|
||||||
autoWarning: 'การปรับเทียบอาจล้มเหลวเมื่อแอปพลิเคชันของผู้ใช้มีพื้นหลังสีดำสนิท',
|
autoWarning: 'การปรับเทียบอาจล้มเหลวเมื่อแอปพลิเคชันของผู้ใช้มีพื้นหลังสีดำสนิท',
|
||||||
manual: 'กำหนดเอง',
|
manual: 'กำหนดเอง',
|
||||||
|
selectedResolution: 'ความละเอียดพื้นที่ที่เลือก',
|
||||||
|
unused: 'ไม่ได้ใช้',
|
||||||
originalResolution: 'ความละเอียดต้นฉบับ',
|
originalResolution: 'ความละเอียดต้นฉบับ',
|
||||||
selectResolution: 'เลือกความละเอียดต้นฉบับ',
|
selectResolution: 'เลือกความละเอียดต้นฉบับ',
|
||||||
useSelectedArea: 'ใช้ความละเอียดของพื้นที่ที่เลือก',
|
|
||||||
addResolution: 'เพิ่มความละเอียดแบบกำหนดเอง',
|
addResolution: 'เพิ่มความละเอียดแบบกำหนดเอง',
|
||||||
add: 'เพิ่ม',
|
add: 'เพิ่ม',
|
||||||
duplicateResolution: 'มีความละเอียดนี้อยู่แล้ว',
|
duplicateResolution: 'มีความละเอียดนี้อยู่แล้ว',
|
||||||
|
|||||||
@@ -74,9 +74,10 @@ const tr = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'Kullanıcı uygulamasının arka planı tamamen siyah olduğunda kalibrasyon başarısız olabilir.',
|
'Kullanıcı uygulamasının arka planı tamamen siyah olduğunda kalibrasyon başarısız olabilir.',
|
||||||
manual: 'Manuel',
|
manual: 'Manuel',
|
||||||
|
selectedResolution: 'Seçili Alan Çözünürlüğü',
|
||||||
|
unused: 'Kullanılmıyor',
|
||||||
originalResolution: 'Orijinal Çözünürlük',
|
originalResolution: 'Orijinal Çözünürlük',
|
||||||
selectResolution: 'Orijinal çözünürlüğü seçin',
|
selectResolution: 'Orijinal çözünürlüğü seçin',
|
||||||
useSelectedArea: 'Seçili alanın çözünürlüğünü kullan',
|
|
||||||
addResolution: 'Özel çözünürlük ekle',
|
addResolution: 'Özel çözünürlük ekle',
|
||||||
add: 'Ekle',
|
add: 'Ekle',
|
||||||
duplicateResolution: 'Bu çözünürlük zaten mevcut.',
|
duplicateResolution: 'Bu çözünürlük zaten mevcut.',
|
||||||
|
|||||||
@@ -74,9 +74,10 @@ const uk = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'Калібрування може завершитися невдало, якщо користувацька програма має повністю чорне тло.',
|
'Калібрування може завершитися невдало, якщо користувацька програма має повністю чорне тло.',
|
||||||
manual: 'Вручну',
|
manual: 'Вручну',
|
||||||
|
selectedResolution: 'Роздільна здатність вибраної області',
|
||||||
|
unused: 'Не використовується',
|
||||||
originalResolution: 'Початкова роздільна здатність',
|
originalResolution: 'Початкова роздільна здатність',
|
||||||
selectResolution: 'Виберіть початкову роздільну здатність',
|
selectResolution: 'Виберіть початкову роздільну здатність',
|
||||||
useSelectedArea: 'Використати роздільну здатність вибраної області',
|
|
||||||
addResolution: 'Додати власну роздільну здатність',
|
addResolution: 'Додати власну роздільну здатність',
|
||||||
add: 'Додати',
|
add: 'Додати',
|
||||||
duplicateResolution: 'Така роздільна здатність уже існує.',
|
duplicateResolution: 'Така роздільна здатність уже існує.',
|
||||||
|
|||||||
@@ -73,9 +73,10 @@ const vi = {
|
|||||||
autoWarning:
|
autoWarning:
|
||||||
'Hiệu chỉnh có thể thất bại khi ứng dụng của người dùng có nền hoàn toàn màu đen.',
|
'Hiệu chỉnh có thể thất bại khi ứng dụng của người dùng có nền hoàn toàn màu đen.',
|
||||||
manual: 'Thủ công',
|
manual: 'Thủ công',
|
||||||
|
selectedResolution: 'Độ phân giải vùng đã chọn',
|
||||||
|
unused: 'Không sử dụng',
|
||||||
originalResolution: 'Độ phân giải gốc',
|
originalResolution: 'Độ phân giải gốc',
|
||||||
selectResolution: 'Chọn độ phân giải gốc',
|
selectResolution: 'Chọn độ phân giải gốc',
|
||||||
useSelectedArea: 'Sử dụng độ phân giải của vùng đã chọn',
|
|
||||||
addResolution: 'Thêm độ phân giải tùy chỉnh',
|
addResolution: 'Thêm độ phân giải tùy chỉnh',
|
||||||
add: 'Thêm',
|
add: 'Thêm',
|
||||||
duplicateResolution: 'Độ phân giải này đã tồn tại.',
|
duplicateResolution: 'Độ phân giải này đã tồn tại.',
|
||||||
|
|||||||
@@ -72,9 +72,10 @@ const zh = {
|
|||||||
auto: '自动',
|
auto: '自动',
|
||||||
autoWarning: '若用户程序为纯黑色背景,则可能校准出错。',
|
autoWarning: '若用户程序为纯黑色背景,则可能校准出错。',
|
||||||
manual: '手动',
|
manual: '手动',
|
||||||
|
selectedResolution: '框选分辨率',
|
||||||
|
unused: '未使用',
|
||||||
originalResolution: '原始分辨率',
|
originalResolution: '原始分辨率',
|
||||||
selectResolution: '选择原始分辨率',
|
selectResolution: '选择原始分辨率',
|
||||||
useSelectedArea: '使用框选分辨率',
|
|
||||||
addResolution: '添加自定义分辨率',
|
addResolution: '添加自定义分辨率',
|
||||||
add: '添加',
|
add: '添加',
|
||||||
duplicateResolution: '该分辨率已存在。',
|
duplicateResolution: '该分辨率已存在。',
|
||||||
|
|||||||
@@ -69,9 +69,10 @@ const zh_tw = {
|
|||||||
auto: '自動',
|
auto: '自動',
|
||||||
autoWarning: '當使用者應用程式使用全黑背景時,校正可能會失敗。',
|
autoWarning: '當使用者應用程式使用全黑背景時,校正可能會失敗。',
|
||||||
manual: '手動',
|
manual: '手動',
|
||||||
|
selectedResolution: '框選解析度',
|
||||||
|
unused: '未使用',
|
||||||
originalResolution: '原始解析度',
|
originalResolution: '原始解析度',
|
||||||
selectResolution: '選擇原始解析度',
|
selectResolution: '選擇原始解析度',
|
||||||
useSelectedArea: '使用所選區域的解析度',
|
|
||||||
addResolution: '新增自訂解析度',
|
addResolution: '新增自訂解析度',
|
||||||
add: '新增',
|
add: '新增',
|
||||||
duplicateResolution: '此解析度已存在。',
|
duplicateResolution: '此解析度已存在。',
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ export const resolutionAtom = atom<Resolution | null>(null);
|
|||||||
// currently effective absolute mouse input region
|
// currently effective absolute mouse input region
|
||||||
export const inputRegionAtom = atom<InputRegion | null>(null);
|
export const inputRegionAtom = atom<InputRegion | null>(null);
|
||||||
export const manualInputRegionAtom = atom<InputRegion | null>(null);
|
export const manualInputRegionAtom = atom<InputRegion | null>(null);
|
||||||
|
export const manualRegionsAtom = atom<InputRegion[]>([]);
|
||||||
|
export const selectedManualRegionAtom = atom<string>('');
|
||||||
export const selectedOriginalResolutionAtom = atom<string>('');
|
export const selectedOriginalResolutionAtom = atom<string>('');
|
||||||
|
|
||||||
// device-level control region mode; disabled by default
|
// device-level control region mode; disabled by default
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ import {
|
|||||||
controlRegionModeAtom,
|
controlRegionModeAtom,
|
||||||
inputRegionAtom,
|
inputRegionAtom,
|
||||||
manualInputRegionAtom,
|
manualInputRegionAtom,
|
||||||
|
manualRegionsAtom,
|
||||||
resolutionAtom,
|
resolutionAtom,
|
||||||
|
selectedManualRegionAtom,
|
||||||
selectedOriginalResolutionAtom,
|
selectedOriginalResolutionAtom,
|
||||||
videoModeAtom
|
videoModeAtom
|
||||||
} from '@/jotai/screen.ts';
|
} from '@/jotai/screen.ts';
|
||||||
@@ -42,14 +44,11 @@ function getVideoMode() {
|
|||||||
const defaultVideoMode = window.RTCPeerConnection ? 'h264' : 'mjpeg';
|
const defaultVideoMode = window.RTCPeerConnection ? 'h264' : 'mjpeg';
|
||||||
|
|
||||||
const cookieVideoMode = storage.getVideoMode();
|
const cookieVideoMode = storage.getVideoMode();
|
||||||
if (cookieVideoMode) {
|
if (!cookieVideoMode || (cookieVideoMode === 'direct' && !window.VideoDecoder)) {
|
||||||
if (cookieVideoMode === 'direct' && !window.VideoDecoder) {
|
return defaultVideoMode;
|
||||||
return defaultVideoMode;
|
|
||||||
}
|
|
||||||
return cookieVideoMode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return defaultVideoMode;
|
return ['direct', 'h264', 'mjpeg'].includes(cookieVideoMode) ? cookieVideoMode : defaultVideoMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Desktop = () => {
|
export const Desktop = () => {
|
||||||
@@ -64,6 +63,8 @@ export const Desktop = () => {
|
|||||||
const [inputRegion, setInputRegion] = useAtom(inputRegionAtom);
|
const [inputRegion, setInputRegion] = useAtom(inputRegionAtom);
|
||||||
const [controlRegionMode, setControlRegionModeState] = useAtom(controlRegionModeAtom);
|
const [controlRegionMode, setControlRegionModeState] = useAtom(controlRegionModeAtom);
|
||||||
const setManualInputRegion = useSetAtom(manualInputRegionAtom);
|
const setManualInputRegion = useSetAtom(manualInputRegionAtom);
|
||||||
|
const setManualRegions = useSetAtom(manualRegionsAtom);
|
||||||
|
const setSelectedManualRegion = useSetAtom(selectedManualRegionAtom);
|
||||||
const setSelectedOriginalResolution = useSetAtom(selectedOriginalResolutionAtom);
|
const setSelectedOriginalResolution = useSetAtom(selectedOriginalResolutionAtom);
|
||||||
const isPicoclawChatOpen = useAtomValue(picoclawChatOpenAtom);
|
const isPicoclawChatOpen = useAtomValue(picoclawChatOpenAtom);
|
||||||
|
|
||||||
@@ -76,6 +77,8 @@ export const Desktop = () => {
|
|||||||
setResolution(res);
|
setResolution(res);
|
||||||
setInputRegion(null);
|
setInputRegion(null);
|
||||||
setManualInputRegion(null);
|
setManualInputRegion(null);
|
||||||
|
setManualRegions([]);
|
||||||
|
setSelectedManualRegion('');
|
||||||
setSelectedOriginalResolution('');
|
setSelectedOriginalResolution('');
|
||||||
setControlRegionModeState('off');
|
setControlRegionModeState('off');
|
||||||
|
|
||||||
@@ -87,15 +90,24 @@ export const Desktop = () => {
|
|||||||
? (config as InputRegion)
|
? (config as InputRegion)
|
||||||
: null;
|
: null;
|
||||||
const selectedResolution = config?.selectedResolution || '';
|
const selectedResolution = config?.selectedResolution || '';
|
||||||
|
const regions = config?.regions || [];
|
||||||
|
const selectedRegion = config?.selectedRegion || '';
|
||||||
|
const selectedManualRegion = regions.find(
|
||||||
|
(region) => `${region.width}x${region.height}` === selectedRegion
|
||||||
|
);
|
||||||
setManualInputRegion(manualRegion);
|
setManualInputRegion(manualRegion);
|
||||||
|
setManualRegions(regions);
|
||||||
|
setSelectedManualRegion(selectedRegion);
|
||||||
setSelectedOriginalResolution(selectedResolution);
|
setSelectedOriginalResolution(selectedResolution);
|
||||||
setInputRegion(mode === 'manual' && !selectedResolution ? manualRegion : null);
|
setInputRegion(mode === 'manual' && selectedRegion ? selectedManualRegion || null : null);
|
||||||
setControlRegionModeState(mode);
|
setControlRegionModeState(mode);
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setControlRegionModeState('off');
|
setControlRegionModeState('off');
|
||||||
setInputRegion(null);
|
setInputRegion(null);
|
||||||
setManualInputRegion(null);
|
setManualInputRegion(null);
|
||||||
|
setManualRegions([]);
|
||||||
|
setSelectedManualRegion('');
|
||||||
setSelectedOriginalResolution('');
|
setSelectedOriginalResolution('');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,7 +119,9 @@ export const Desktop = () => {
|
|||||||
setControlRegionModeState,
|
setControlRegionModeState,
|
||||||
setInputRegion,
|
setInputRegion,
|
||||||
setManualInputRegion,
|
setManualInputRegion,
|
||||||
|
setManualRegions,
|
||||||
setResolution,
|
setResolution,
|
||||||
|
setSelectedManualRegion,
|
||||||
setSelectedOriginalResolution,
|
setSelectedOriginalResolution,
|
||||||
setVideoMode
|
setVideoMode
|
||||||
]);
|
]);
|
||||||
@@ -135,7 +149,7 @@ export const Desktop = () => {
|
|||||||
clearTimeout(validationTimer);
|
clearTimeout(validationTimer);
|
||||||
}
|
}
|
||||||
validationTimer = setTimeout(() => {
|
validationTimer = setTimeout(() => {
|
||||||
const mediaSize = getMediaSize(target);
|
const mediaSize = getMediaSize(target, resolution);
|
||||||
if (!mediaSize || !isMediaReady(target) || isInputRegionCompatible(region, mediaSize)) {
|
if (!mediaSize || !isMediaReady(target) || isInputRegionCompatible(region, mediaSize)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,3 +11,23 @@ export const BitRateMap = new Map([
|
|||||||
[3, 2000],
|
[3, 2000],
|
||||||
[4, 1000]
|
[4, 1000]
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
export function getQualityMap(videoMode: string) {
|
||||||
|
if (videoMode === 'mjpeg') {
|
||||||
|
return QualityMap;
|
||||||
|
}
|
||||||
|
if (videoMode === 'direct' || videoMode === 'h264') {
|
||||||
|
return BitRateMap;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getScreenType(videoMode: string) {
|
||||||
|
if (videoMode === 'mjpeg') {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (videoMode === 'direct' || videoMode === 'h264') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import * as ls from '@/lib/localstorage';
|
|||||||
import { resolutionAtom, videoModeAtom } from '@/jotai/screen.ts';
|
import { resolutionAtom, videoModeAtom } from '@/jotai/screen.ts';
|
||||||
import { MenuItem } from '@/components/menu-item.tsx';
|
import { MenuItem } from '@/components/menu-item.tsx';
|
||||||
|
|
||||||
import { BitRateMap, QualityMap } from './constants.ts';
|
import { getQualityMap, getScreenType } from './constants.ts';
|
||||||
import { Fps } from './fps';
|
import { Fps } from './fps';
|
||||||
import { FrameDetect } from './frame-detect';
|
import { FrameDetect } from './frame-detect';
|
||||||
import { Gop } from './gop.tsx';
|
import { Gop } from './gop.tsx';
|
||||||
@@ -23,12 +23,16 @@ export const Screen = () => {
|
|||||||
|
|
||||||
const videoMode = useAtomValue(videoModeAtom);
|
const videoMode = useAtomValue(videoModeAtom);
|
||||||
const resolution = useAtomValue(resolutionAtom);
|
const resolution = useAtomValue(resolutionAtom);
|
||||||
|
const isMjpeg = videoMode === 'mjpeg';
|
||||||
const [fps, setFps] = useState(30);
|
const [fps, setFps] = useState(30);
|
||||||
const [quality, setQuality] = useState(2);
|
const [quality, setQuality] = useState(2);
|
||||||
const [gop, setGop] = useState(30);
|
const [gop, setGop] = useState(30);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateScreen('type', videoMode === 'mjpeg' ? 0 : 1);
|
const screenType = getScreenType(videoMode);
|
||||||
|
if (screenType !== null) {
|
||||||
|
updateScreen('type', screenType);
|
||||||
|
}
|
||||||
updateScreen('resolution', resolution!.height);
|
updateScreen('resolution', resolution!.height);
|
||||||
updateQuality();
|
updateQuality();
|
||||||
updateFps();
|
updateFps();
|
||||||
@@ -40,7 +44,9 @@ export const Screen = () => {
|
|||||||
if (!cookieQuality) return;
|
if (!cookieQuality) return;
|
||||||
|
|
||||||
const key = cookieQuality >= 1 && cookieQuality <= 4 ? cookieQuality : 2;
|
const key = cookieQuality >= 1 && cookieQuality <= 4 ? cookieQuality : 2;
|
||||||
const value = videoMode === 'mjpeg' ? QualityMap.get(key)! : BitRateMap.get(key)!;
|
const qualityMap = getQualityMap(videoMode);
|
||||||
|
const value = qualityMap?.get(key);
|
||||||
|
if (value === undefined) return;
|
||||||
|
|
||||||
const rsp = await updateScreen('quality', value);
|
const rsp = await updateScreen('quality', value);
|
||||||
if (rsp.code === 0) {
|
if (rsp.code === 0) {
|
||||||
@@ -75,8 +81,8 @@ export const Screen = () => {
|
|||||||
<Quality quality={quality} setQuality={setQuality} />
|
<Quality quality={quality} setQuality={setQuality} />
|
||||||
<Fps fps={fps} setFps={setFps} />
|
<Fps fps={fps} setFps={setFps} />
|
||||||
<Scale />
|
<Scale />
|
||||||
{videoMode !== 'mjpeg' && <Gop gop={gop} setGop={setGop} />}
|
{!isMjpeg && <Gop gop={gop} setGop={setGop} />}
|
||||||
{videoMode === 'mjpeg' && <FrameDetect />}
|
{isMjpeg && <FrameDetect />}
|
||||||
<Reset />
|
<Reset />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,12 +12,13 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
Typography
|
Typography
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
import { useAtom, useSetAtom } from 'jotai';
|
||||||
import { ScanSearchIcon } from 'lucide-react';
|
import { ScanSearchIcon } from 'lucide-react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getInputRegion,
|
getInputRegion,
|
||||||
|
setManualRegions as saveManualRegions,
|
||||||
setControlRegionMode,
|
setControlRegionMode,
|
||||||
setOriginalResolutionConfig,
|
setOriginalResolutionConfig,
|
||||||
setSelectedOriginalResolution
|
setSelectedOriginalResolution
|
||||||
@@ -34,6 +35,8 @@ import {
|
|||||||
inputRegionAtom,
|
inputRegionAtom,
|
||||||
inputRegionSelectingAtom,
|
inputRegionSelectingAtom,
|
||||||
manualInputRegionAtom,
|
manualInputRegionAtom,
|
||||||
|
manualRegionsAtom,
|
||||||
|
selectedManualRegionAtom,
|
||||||
selectedOriginalResolutionAtom
|
selectedOriginalResolutionAtom
|
||||||
} from '@/jotai/screen.ts';
|
} from '@/jotai/screen.ts';
|
||||||
import { menuCloseSignalAtom } from '@/jotai/settings.ts';
|
import { menuCloseSignalAtom } from '@/jotai/settings.ts';
|
||||||
@@ -44,9 +47,12 @@ const resolutionKey = ({ width, height }: ResolutionPreset) => `${width}x${heigh
|
|||||||
export const OriginalResolution = () => {
|
export const OriginalResolution = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const setInputRegion = useSetAtom(inputRegionAtom);
|
const setInputRegion = useSetAtom(inputRegionAtom);
|
||||||
const manualInputRegion = useAtomValue(manualInputRegionAtom);
|
|
||||||
const setManualInputRegion = useSetAtom(manualInputRegionAtom);
|
const setManualInputRegion = useSetAtom(manualInputRegionAtom);
|
||||||
const [selectedResolution, setSelectedResolution] = useAtom(selectedOriginalResolutionAtom);
|
const [manualRegions, setManualRegions] = useAtom(manualRegionsAtom);
|
||||||
|
const [selectedManualRegion, setSelectedManualRegion] = useAtom(selectedManualRegionAtom);
|
||||||
|
const [selectedOriginalResolution, setSelectedOriginalResolutionState] = useAtom(
|
||||||
|
selectedOriginalResolutionAtom
|
||||||
|
);
|
||||||
const [mode, setMode] = useAtom(controlRegionModeAtom);
|
const [mode, setMode] = useAtom(controlRegionModeAtom);
|
||||||
const setSelecting = useSetAtom(inputRegionSelectingAtom);
|
const setSelecting = useSetAtom(inputRegionSelectingAtom);
|
||||||
const setKeyboardLock = useSetAtom(keyboardLockAtom);
|
const setKeyboardLock = useSetAtom(keyboardLockAtom);
|
||||||
@@ -74,7 +80,9 @@ export const OriginalResolution = () => {
|
|||||||
if (rsp.code === 0) {
|
if (rsp.code === 0) {
|
||||||
const config = rsp.data as ControlRegionConfig;
|
const config = rsp.data as ControlRegionConfig;
|
||||||
setResolutions(config?.resolutions || []);
|
setResolutions(config?.resolutions || []);
|
||||||
setSelectedResolution(config?.selectedResolution || '');
|
setManualRegions(config?.regions || []);
|
||||||
|
setSelectedManualRegion(config?.selectedRegion || '');
|
||||||
|
setSelectedOriginalResolutionState(config?.selectedResolution || '');
|
||||||
setManualInputRegion(
|
setManualInputRegion(
|
||||||
isValidInputRegion(config as InputRegion) ? (config as InputRegion) : null
|
isValidInputRegion(config as InputRegion) ? (config as InputRegion) : null
|
||||||
);
|
);
|
||||||
@@ -82,18 +90,41 @@ export const OriginalResolution = () => {
|
|||||||
return rsp;
|
return rsp;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyResolution(key: string) {
|
async function applyOriginalResolution(key: string) {
|
||||||
const rsp = await setSelectedOriginalResolution(key);
|
const rsp = await setSelectedOriginalResolution(key);
|
||||||
if (rsp.code !== 0) {
|
if (rsp.code !== 0) {
|
||||||
messageApi.error(t('screen.controlRegion.saveFailed'));
|
messageApi.error(t('screen.controlRegion.saveFailed'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedResolution(key);
|
setSelectedOriginalResolutionState(key);
|
||||||
|
setSelectedManualRegion('');
|
||||||
|
if (!key) setInputRegion(null);
|
||||||
setIsPopoverOpen(true);
|
setIsPopoverOpen(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveResolutions(next: ResolutionPreset[], nextSelected = selectedResolution) {
|
async function applyManualRegion(key: string) {
|
||||||
const rsp = await setOriginalResolutionConfig(next, nextSelected);
|
const rsp = await saveManualRegions(manualRegions, key);
|
||||||
|
if (rsp.code !== 0) {
|
||||||
|
messageApi.error(t('screen.controlRegion.saveFailed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedManualRegion(key);
|
||||||
|
setSelectedOriginalResolutionState('');
|
||||||
|
setInputRegion(
|
||||||
|
manualRegions.find((region) => `${region.width}x${region.height}` === key) || null
|
||||||
|
);
|
||||||
|
setIsPopoverOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveResolutions(
|
||||||
|
next: ResolutionPreset[],
|
||||||
|
nextSelected = selectedOriginalResolution
|
||||||
|
) {
|
||||||
|
const rsp = await setOriginalResolutionConfig(
|
||||||
|
next,
|
||||||
|
nextSelected,
|
||||||
|
nextSelected ? '' : selectedManualRegion
|
||||||
|
);
|
||||||
if (rsp.code !== 0) {
|
if (rsp.code !== 0) {
|
||||||
messageApi.error(t('screen.controlRegion.saveFailed'));
|
messageApi.error(t('screen.controlRegion.saveFailed'));
|
||||||
return false;
|
return false;
|
||||||
@@ -122,7 +153,7 @@ export const OriginalResolution = () => {
|
|||||||
async function deleteResolution(index: number) {
|
async function deleteResolution(index: number) {
|
||||||
if (index < 0 || index >= resolutions.length) return;
|
if (index < 0 || index >= resolutions.length) return;
|
||||||
const deleted = resolutionKey(resolutions[index]);
|
const deleted = resolutionKey(resolutions[index]);
|
||||||
const nextSelected = selectedResolution === deleted ? '' : selectedResolution;
|
const nextSelected = selectedOriginalResolution === deleted ? '' : selectedOriginalResolution;
|
||||||
if (
|
if (
|
||||||
await saveResolutions(
|
await saveResolutions(
|
||||||
resolutions.filter((_, itemIndex) => itemIndex !== index),
|
resolutions.filter((_, itemIndex) => itemIndex !== index),
|
||||||
@@ -130,12 +161,29 @@ export const OriginalResolution = () => {
|
|||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (!nextSelected) {
|
if (!nextSelected) {
|
||||||
setSelectedResolution('');
|
setSelectedOriginalResolutionState('');
|
||||||
setInputRegion(manualInputRegion);
|
setInputRegion(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteManualRegion(index: number) {
|
||||||
|
if (index < 0 || index >= manualRegions.length) return;
|
||||||
|
const deleted = `${manualRegions[index].width}x${manualRegions[index].height}`;
|
||||||
|
const nextSelected = selectedManualRegion === deleted ? '' : selectedManualRegion;
|
||||||
|
const next = manualRegions.filter((_, itemIndex) => itemIndex !== index);
|
||||||
|
const rsp = await saveManualRegions(next, nextSelected, selectedOriginalResolution);
|
||||||
|
if (rsp.code !== 0) {
|
||||||
|
messageApi.error(t('screen.controlRegion.saveFailed'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setManualRegions(next);
|
||||||
|
if (!nextSelected) {
|
||||||
|
setSelectedManualRegion('');
|
||||||
|
setInputRegion(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function updateMode(nextMode: ControlRegionMode) {
|
async function updateMode(nextMode: ControlRegionMode) {
|
||||||
const rsp = await setControlRegionMode(nextMode);
|
const rsp = await setControlRegionMode(nextMode);
|
||||||
if (rsp.code !== 0) {
|
if (rsp.code !== 0) {
|
||||||
@@ -148,9 +196,14 @@ export const OriginalResolution = () => {
|
|||||||
const manualRegion = isValidInputRegion(config.data as InputRegion)
|
const manualRegion = isValidInputRegion(config.data as InputRegion)
|
||||||
? (config.data as InputRegion)
|
? (config.data as InputRegion)
|
||||||
: null;
|
: null;
|
||||||
|
const selectedRegion = (config.data as ControlRegionConfig)?.selectedRegion || '';
|
||||||
|
const selectedResolution = (config.data as ControlRegionConfig)?.selectedResolution || '';
|
||||||
|
const regions = (config.data as ControlRegionConfig)?.regions || [];
|
||||||
setManualInputRegion(manualRegion);
|
setManualInputRegion(manualRegion);
|
||||||
setInputRegion(
|
setInputRegion(
|
||||||
(config.data as ControlRegionConfig)?.selectedResolution ? null : manualRegion
|
selectedResolution
|
||||||
|
? null
|
||||||
|
: regions.find((region) => `${region.width}x${region.height}` === selectedRegion) || null
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setInputRegion(null);
|
setInputRegion(null);
|
||||||
@@ -195,19 +248,58 @@ export const OriginalResolution = () => {
|
|||||||
<Button block type="primary" icon={<ScanSearchIcon size={14} />} onClick={selectArea}>
|
<Button block type="primary" icon={<ScanSearchIcon size={14} />} onClick={selectArea}>
|
||||||
{t('screen.controlRegion.select')}
|
{t('screen.controlRegion.select')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Typography.Text>{t('screen.controlRegion.selectedResolution')}</Typography.Text>
|
||||||
|
<Select
|
||||||
|
className="w-full"
|
||||||
|
value={selectedManualRegion}
|
||||||
|
options={[
|
||||||
|
{ label: t('screen.controlRegion.unused'), value: '' },
|
||||||
|
...manualRegions.map((region) => ({
|
||||||
|
label: `${region.width}x${region.height}`,
|
||||||
|
value: `${region.width}x${region.height}`
|
||||||
|
}))
|
||||||
|
]}
|
||||||
|
optionRender={(option) => (
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span>{option.label}</span>
|
||||||
|
{option.value !== '' && (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
danger
|
||||||
|
size="small"
|
||||||
|
icon={<DeleteOutlined />}
|
||||||
|
onMouseDown={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
}}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
deleteManualRegion(
|
||||||
|
manualRegions.findIndex(
|
||||||
|
(region) => `${region.width}x${region.height}` === option.value
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
onChange={applyManualRegion}
|
||||||
|
/>
|
||||||
<Typography.Text>{t('screen.controlRegion.originalResolution')}</Typography.Text>
|
<Typography.Text>{t('screen.controlRegion.originalResolution')}</Typography.Text>
|
||||||
<Space.Compact block>
|
<Space.Compact block>
|
||||||
<Select
|
<Select
|
||||||
className="w-full"
|
className="w-full"
|
||||||
value={selectedResolution}
|
value={selectedOriginalResolution}
|
||||||
placeholder={t('screen.controlRegion.selectResolution')}
|
placeholder={t('screen.controlRegion.selectResolution')}
|
||||||
getPopupContainer={(trigger) => trigger.parentElement || document.body}
|
getPopupContainer={(trigger) => trigger.parentElement || document.body}
|
||||||
options={resolutions
|
options={[
|
||||||
.map((resolution) => ({
|
{ label: t('screen.controlRegion.unused'), value: '' },
|
||||||
|
...resolutions.map((resolution) => ({
|
||||||
label: resolutionKey(resolution),
|
label: resolutionKey(resolution),
|
||||||
value: resolutionKey(resolution)
|
value: resolutionKey(resolution)
|
||||||
}))
|
}))
|
||||||
.concat([{ label: t('screen.controlRegion.useSelectedArea'), value: '' }])}
|
]}
|
||||||
optionRender={(option) => (
|
optionRender={(option) => (
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span>{option.label}</span>
|
<span>{option.label}</span>
|
||||||
@@ -233,7 +325,7 @@ export const OriginalResolution = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
onChange={applyResolution}
|
onChange={applyOriginalResolution}
|
||||||
/>
|
/>
|
||||||
<Tooltip title={t('screen.controlRegion.addResolution')}>
|
<Tooltip title={t('screen.controlRegion.addResolution')}>
|
||||||
<Button icon={<PlusOutlined />} onClick={() => setIsAddOpen(true)} />
|
<Button icon={<PlusOutlined />} onClick={() => setIsAddOpen(true)} />
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { updateScreen } from '@/api/vm';
|
|||||||
import { setQuality as setCookie } from '@/lib/localstorage.ts';
|
import { setQuality as setCookie } from '@/lib/localstorage.ts';
|
||||||
import { videoModeAtom } from '@/jotai/screen.ts';
|
import { videoModeAtom } from '@/jotai/screen.ts';
|
||||||
|
|
||||||
import { BitRateMap, QualityMap } from './constants.ts';
|
import { getQualityMap } from './constants.ts';
|
||||||
|
|
||||||
type QualityProps = {
|
type QualityProps = {
|
||||||
quality: number;
|
quality: number;
|
||||||
@@ -26,7 +26,11 @@ export const Quality = ({ quality, setQuality }: QualityProps) => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
async function update(key: number) {
|
async function update(key: number) {
|
||||||
const value = videoMode === 'mjpeg' ? QualityMap.get(key)! : BitRateMap.get(key)!;
|
const qualityMap = getQualityMap(videoMode);
|
||||||
|
const value = qualityMap?.get(key);
|
||||||
|
if (value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const rsp = await updateScreen('quality', value);
|
const rsp = await updateScreen('quality', value);
|
||||||
if (rsp.code !== 0) {
|
if (rsp.code !== 0) {
|
||||||
|
|||||||
@@ -4,14 +4,16 @@ import { useAtomValue, useSetAtom } from 'jotai';
|
|||||||
import {
|
import {
|
||||||
controlRegionModeAtom,
|
controlRegionModeAtom,
|
||||||
inputRegionAtom,
|
inputRegionAtom,
|
||||||
inputRegionSelectingAtom
|
inputRegionSelectingAtom,
|
||||||
|
resolutionAtom
|
||||||
} from '@/jotai/screen.ts';
|
} from '@/jotai/screen.ts';
|
||||||
|
|
||||||
import { detectFrameContent, getMediaSize, isMediaReady } from './geometry.ts';
|
import { detectFrameContent, getMediaSize } from './geometry.ts';
|
||||||
|
|
||||||
export const AutoRegion = () => {
|
export const AutoRegion = () => {
|
||||||
const mode = useAtomValue(controlRegionModeAtom);
|
const mode = useAtomValue(controlRegionModeAtom);
|
||||||
const selecting = useAtomValue(inputRegionSelectingAtom);
|
const selecting = useAtomValue(inputRegionSelectingAtom);
|
||||||
|
const resolution = useAtomValue(resolutionAtom);
|
||||||
const setInputRegion = useSetAtom(inputRegionAtom);
|
const setInputRegion = useSetAtom(inputRegionAtom);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -19,18 +21,25 @@ export const AutoRegion = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const screen = document.getElementById('screen');
|
|
||||||
if (!screen) return;
|
|
||||||
const target = screen;
|
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
|
let target: Element | null = null;
|
||||||
let candidate = '';
|
let candidate = '';
|
||||||
let confirmations = 0;
|
let confirmations = 0;
|
||||||
|
|
||||||
const detect = () => {
|
const detect = () => {
|
||||||
if (stopped || !isMediaReady(target)) return;
|
if (stopped) return;
|
||||||
const mediaSize = getMediaSize(target);
|
|
||||||
|
const screen = document.getElementById('screen');
|
||||||
|
if (!screen) return;
|
||||||
|
if (screen !== target) {
|
||||||
|
target = screen;
|
||||||
|
candidate = '';
|
||||||
|
confirmations = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mediaSize = getMediaSize(screen, resolution);
|
||||||
if (!mediaSize) return;
|
if (!mediaSize) return;
|
||||||
const content = detectFrameContent(target, mediaSize);
|
const content = detectFrameContent(screen, mediaSize);
|
||||||
const key = [
|
const key = [
|
||||||
mediaSize.width,
|
mediaSize.width,
|
||||||
mediaSize.height,
|
mediaSize.height,
|
||||||
@@ -65,7 +74,7 @@ export const AutoRegion = () => {
|
|||||||
stopped = true;
|
stopped = true;
|
||||||
window.clearInterval(timer);
|
window.clearInterval(timer);
|
||||||
};
|
};
|
||||||
}, [mode, selecting, setInputRegion]);
|
}, [mode, resolution, selecting, setInputRegion]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode !== 'auto') setInputRegion(null);
|
if (mode !== 'auto') setInputRegion(null);
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export type FrameContent = {
|
|||||||
export type RenderedMediaRect = FrameContent;
|
export type RenderedMediaRect = FrameContent;
|
||||||
|
|
||||||
const aspectRatioTolerance = 0.01;
|
const aspectRatioTolerance = 0.01;
|
||||||
|
const mediaWidthAttribute = 'data-media-width';
|
||||||
|
const mediaHeightAttribute = 'data-media-height';
|
||||||
|
const blackPixelThreshold = 32;
|
||||||
|
const darkSampleRatio = 0.75;
|
||||||
|
|
||||||
export const fullFrameContent = (mediaSize: MediaSize): FrameContent => ({
|
export const fullFrameContent = (mediaSize: MediaSize): FrameContent => ({
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -23,7 +27,13 @@ export const fullFrameContent = (mediaSize: MediaSize): FrameContent => ({
|
|||||||
height: mediaSize.height
|
height: mediaSize.height
|
||||||
});
|
});
|
||||||
|
|
||||||
export function getMediaSize(screen: Element): MediaSize | null {
|
export function getMediaSize(screen: Element, fallback?: MediaSize | null): MediaSize | null {
|
||||||
|
const dataWidth = Number(screen.getAttribute(mediaWidthAttribute));
|
||||||
|
const dataHeight = Number(screen.getAttribute(mediaHeightAttribute));
|
||||||
|
if (dataWidth > 0 && dataHeight > 0) {
|
||||||
|
return { width: dataWidth, height: dataHeight };
|
||||||
|
}
|
||||||
|
|
||||||
if (screen instanceof HTMLVideoElement && screen.videoWidth > 0 && screen.videoHeight > 0) {
|
if (screen instanceof HTMLVideoElement && screen.videoWidth > 0 && screen.videoHeight > 0) {
|
||||||
return { width: screen.videoWidth, height: screen.videoHeight };
|
return { width: screen.videoWidth, height: screen.videoHeight };
|
||||||
}
|
}
|
||||||
@@ -32,28 +42,20 @@ export function getMediaSize(screen: Element): MediaSize | null {
|
|||||||
return { width: screen.naturalWidth, height: screen.naturalHeight };
|
return { width: screen.naturalWidth, height: screen.naturalHeight };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screen instanceof HTMLCanvasElement) {
|
return fallback && fallback.width > 0 && fallback.height > 0 ? fallback : null;
|
||||||
const width = Number(screen.dataset.mediaWidth);
|
|
||||||
const height = Number(screen.dataset.mediaHeight);
|
|
||||||
if (width > 0 && height > 0) {
|
|
||||||
return { width, height };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isMediaReady(screen: Element) {
|
export function isMediaReady(screen: Element) {
|
||||||
if (screen instanceof HTMLVideoElement) {
|
if (screen instanceof HTMLVideoElement) {
|
||||||
return screen.readyState >= 2 && screen.videoWidth > 0 && screen.videoHeight > 0;
|
return screen.readyState >= 2 && !!getMediaSize(screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screen instanceof HTMLImageElement) {
|
if (screen instanceof HTMLImageElement) {
|
||||||
return screen.complete && screen.naturalWidth > 0 && screen.naturalHeight > 0;
|
return !!getMediaSize(screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screen instanceof HTMLCanvasElement) {
|
if (screen instanceof HTMLCanvasElement) {
|
||||||
return Number(screen.dataset.mediaWidth) > 0 && Number(screen.dataset.mediaHeight) > 0;
|
return !!getMediaSize(screen);
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
@@ -73,20 +75,33 @@ export function detectFrameContent(screen: Element, mediaSize: MediaSize): Frame
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const pixels = context.getImageData(0, 0, width, height).data;
|
const pixels = context.getImageData(0, 0, width, height).data;
|
||||||
const left = findBorderInset(pixels, width, height, true, 'horizontal');
|
let top = findBorderInset(pixels, width, height, true, 'vertical');
|
||||||
const right = findBorderInset(pixels, width, height, false, 'horizontal');
|
let bottom = findBorderInset(pixels, width, height, false, 'vertical');
|
||||||
const top = findBorderInset(pixels, width, height, true, 'vertical');
|
let verticalBorder = isSymmetricBorder(top, bottom, height);
|
||||||
const bottom = findBorderInset(pixels, width, height, false, 'vertical');
|
const left = findBorderInset(
|
||||||
const horizontalBorder =
|
pixels,
|
||||||
left >= Math.max(4, Math.round(width * 0.02)) &&
|
width,
|
||||||
right >= Math.max(4, Math.round(width * 0.02)) &&
|
height,
|
||||||
left + right < width * 0.4 &&
|
true,
|
||||||
Math.abs(left - right) <= Math.max(4, Math.round(width * 0.05));
|
'horizontal',
|
||||||
const verticalBorder =
|
verticalBorder ? top : 0,
|
||||||
top >= Math.max(4, Math.round(height * 0.02)) &&
|
verticalBorder ? height - bottom : height
|
||||||
bottom >= Math.max(4, Math.round(height * 0.02)) &&
|
);
|
||||||
top + bottom < height * 0.4 &&
|
const right = findBorderInset(
|
||||||
Math.abs(top - bottom) <= Math.max(4, Math.round(height * 0.05));
|
pixels,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
false,
|
||||||
|
'horizontal',
|
||||||
|
verticalBorder ? top : 0,
|
||||||
|
verticalBorder ? height - bottom : height
|
||||||
|
);
|
||||||
|
const horizontalBorder = isSymmetricBorder(left, right, width);
|
||||||
|
if (horizontalBorder) {
|
||||||
|
top = findBorderInset(pixels, width, height, true, 'vertical', left, width - right);
|
||||||
|
bottom = findBorderInset(pixels, width, height, false, 'vertical', left, width - right);
|
||||||
|
verticalBorder = isSymmetricBorder(top, bottom, height);
|
||||||
|
}
|
||||||
const frameLeft = horizontalBorder ? (left / width) * mediaSize.width : 0;
|
const frameLeft = horizontalBorder ? (left / width) * mediaSize.width : 0;
|
||||||
const frameRight = horizontalBorder ? (right / width) * mediaSize.width : 0;
|
const frameRight = horizontalBorder ? (right / width) * mediaSize.width : 0;
|
||||||
const frameTop = verticalBorder ? (top / height) * mediaSize.height : 0;
|
const frameTop = verticalBorder ? (top / height) * mediaSize.height : 0;
|
||||||
@@ -128,28 +143,43 @@ function findBorderInset(
|
|||||||
width: number,
|
width: number,
|
||||||
height: number,
|
height: number,
|
||||||
fromStart: boolean,
|
fromStart: boolean,
|
||||||
axis: 'horizontal' | 'vertical'
|
axis: 'horizontal' | 'vertical',
|
||||||
|
sampleStart = 0,
|
||||||
|
sampleEnd = axis === 'horizontal' ? height : width
|
||||||
) {
|
) {
|
||||||
const scanSize = axis === 'horizontal' ? width : height;
|
const scanSize = axis === 'horizontal' ? width : height;
|
||||||
const sampleSize = axis === 'horizontal' ? height : width;
|
const sampleSize = Math.max(1, sampleEnd - sampleStart);
|
||||||
const lines = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9].map((ratio) =>
|
const lines = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9].map(
|
||||||
Math.round((sampleSize - 1) * ratio)
|
(ratio) => sampleStart + Math.round((sampleSize - 1) * ratio)
|
||||||
);
|
);
|
||||||
const limit = Math.floor(scanSize * 0.45);
|
const limit = Math.floor(scanSize * 0.45);
|
||||||
let inset = 0;
|
let inset = 0;
|
||||||
for (let offset = 0; offset < limit; offset++) {
|
for (let offset = 0; offset < limit; offset++) {
|
||||||
const black = lines.every((line) => {
|
const darkSamples = lines.filter((line) => {
|
||||||
const x = axis === 'horizontal' ? (fromStart ? offset : width - offset - 1) : line;
|
const x = axis === 'horizontal' ? (fromStart ? offset : width - offset - 1) : line;
|
||||||
const y = axis === 'horizontal' ? line : fromStart ? offset : height - offset - 1;
|
const y = axis === 'horizontal' ? line : fromStart ? offset : height - offset - 1;
|
||||||
const index = (y * width + x) * 4;
|
const index = (y * width + x) * 4;
|
||||||
return pixels[index] === 0 && pixels[index + 1] === 0 && pixels[index + 2] === 0;
|
return (
|
||||||
});
|
pixels[index] <= blackPixelThreshold &&
|
||||||
|
pixels[index + 1] <= blackPixelThreshold &&
|
||||||
|
pixels[index + 2] <= blackPixelThreshold
|
||||||
|
);
|
||||||
|
}).length;
|
||||||
|
const black = darkSamples / lines.length >= darkSampleRatio;
|
||||||
if (!black) break;
|
if (!black) break;
|
||||||
inset = offset + 1;
|
inset = offset + 1;
|
||||||
}
|
}
|
||||||
return inset;
|
return inset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSymmetricBorder(start: number, end: number, size: number) {
|
||||||
|
return (
|
||||||
|
start >= Math.max(4, Math.round(size * 0.02)) &&
|
||||||
|
end >= Math.max(4, Math.round(size * 0.02)) &&
|
||||||
|
Math.abs(start - end) <= Math.max(4, Math.round(size * 0.05))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function getRenderedMediaRect(
|
export function getRenderedMediaRect(
|
||||||
elementRect: DOMRect,
|
elementRect: DOMRect,
|
||||||
mediaSize: MediaSize
|
mediaSize: MediaSize
|
||||||
|
|||||||
@@ -17,5 +17,9 @@ export const Screen = () => {
|
|||||||
return <H264Direct />;
|
return <H264Direct />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <H264Webrtc />;
|
if (videoMode === 'h264') {
|
||||||
|
return <H264Webrtc />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import {
|
|||||||
inputRegionAtom,
|
inputRegionAtom,
|
||||||
inputRegionSelectingAtom,
|
inputRegionSelectingAtom,
|
||||||
manualInputRegionAtom,
|
manualInputRegionAtom,
|
||||||
|
manualRegionsAtom,
|
||||||
|
resolutionAtom,
|
||||||
|
selectedManualRegionAtom,
|
||||||
selectedOriginalResolutionAtom,
|
selectedOriginalResolutionAtom,
|
||||||
videoScaleAtom
|
videoScaleAtom
|
||||||
} from '@/jotai/screen.ts';
|
} from '@/jotai/screen.ts';
|
||||||
@@ -75,6 +78,7 @@ const resizeHandles: Array<{
|
|||||||
];
|
];
|
||||||
|
|
||||||
const minimumSelectionSize = 4;
|
const minimumSelectionSize = 4;
|
||||||
|
const edgeSnapDistance = 8;
|
||||||
|
|
||||||
export const InputRegionOverlay = () => {
|
export const InputRegionOverlay = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -82,6 +86,9 @@ export const InputRegionOverlay = () => {
|
|||||||
const [selecting, setSelecting] = useAtom(inputRegionSelectingAtom);
|
const [selecting, setSelecting] = useAtom(inputRegionSelectingAtom);
|
||||||
const [, setInputRegionState] = useAtom(inputRegionAtom);
|
const [, setInputRegionState] = useAtom(inputRegionAtom);
|
||||||
const setManualInputRegion = useSetAtom(manualInputRegionAtom);
|
const setManualInputRegion = useSetAtom(manualInputRegionAtom);
|
||||||
|
const [manualRegions, setManualRegions] = useAtom(manualRegionsAtom);
|
||||||
|
const resolution = useAtomValue(resolutionAtom);
|
||||||
|
const setSelectedManualRegion = useSetAtom(selectedManualRegionAtom);
|
||||||
const setSelectedOriginalResolution = useSetAtom(selectedOriginalResolutionAtom);
|
const setSelectedOriginalResolution = useSetAtom(selectedOriginalResolutionAtom);
|
||||||
const videoScale = useAtomValue(videoScaleAtom);
|
const videoScale = useAtomValue(videoScaleAtom);
|
||||||
const setVideoScale = useSetAtom(videoScaleAtom);
|
const setVideoScale = useSetAtom(videoScaleAtom);
|
||||||
@@ -151,17 +158,15 @@ export const InputRegionOverlay = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const screen = document.getElementById('screen');
|
|
||||||
if (!screen) {
|
|
||||||
setFrameRect(null);
|
|
||||||
setMediaSize(null);
|
|
||||||
frameRectRef.current = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const target = screen;
|
|
||||||
|
|
||||||
function updateFrame() {
|
function updateFrame() {
|
||||||
const nextMediaSize = getMediaSize(target);
|
const target = document.getElementById('screen');
|
||||||
|
if (!target) {
|
||||||
|
setFrameRect(null);
|
||||||
|
setMediaSize(null);
|
||||||
|
frameRectRef.current = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const nextMediaSize = getMediaSize(target, resolution);
|
||||||
const bounds = target.getBoundingClientRect();
|
const bounds = target.getBoundingClientRect();
|
||||||
if (!nextMediaSize || bounds.width <= 0 || bounds.height <= 0) {
|
if (!nextMediaSize || bounds.width <= 0 || bounds.height <= 0) {
|
||||||
setFrameRect(null);
|
setFrameRect(null);
|
||||||
@@ -192,31 +197,16 @@ export const InputRegionOverlay = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateFrame();
|
updateFrame();
|
||||||
const resizeObserver = new ResizeObserver(updateFrame);
|
|
||||||
resizeObserver.observe(target);
|
|
||||||
const mutationObserver = new MutationObserver(updateFrame);
|
|
||||||
mutationObserver.observe(target, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['data-media-width', 'data-media-height']
|
|
||||||
});
|
|
||||||
window.addEventListener('resize', updateFrame);
|
window.addEventListener('resize', updateFrame);
|
||||||
window.addEventListener('scroll', updateFrame, true);
|
window.addEventListener('scroll', updateFrame, true);
|
||||||
target.addEventListener('load', updateFrame);
|
const timer = window.setInterval(updateFrame, 250);
|
||||||
target.addEventListener('loadedmetadata', updateFrame);
|
|
||||||
target.addEventListener('canplay', updateFrame);
|
|
||||||
target.addEventListener('resize', updateFrame);
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
resizeObserver.disconnect();
|
|
||||||
mutationObserver.disconnect();
|
|
||||||
window.removeEventListener('resize', updateFrame);
|
window.removeEventListener('resize', updateFrame);
|
||||||
window.removeEventListener('scroll', updateFrame, true);
|
window.removeEventListener('scroll', updateFrame, true);
|
||||||
target.removeEventListener('load', updateFrame);
|
window.clearInterval(timer);
|
||||||
target.removeEventListener('loadedmetadata', updateFrame);
|
|
||||||
target.removeEventListener('canplay', updateFrame);
|
|
||||||
target.removeEventListener('resize', updateFrame);
|
|
||||||
};
|
};
|
||||||
}, [selecting, videoScale]);
|
}, [resolution, selecting, videoScale]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selecting) {
|
if (!selecting) {
|
||||||
@@ -345,12 +335,20 @@ export const InputRegionOverlay = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const right = frameRect.left + frameRect.width;
|
||||||
|
const bottom = frameRect.top + frameRect.height;
|
||||||
return {
|
return {
|
||||||
x: Math.max(frameRect.left, Math.min(frameRect.left + frameRect.width, x)),
|
x: snapToEdge(Math.max(frameRect.left, Math.min(right, x)), frameRect.left, right),
|
||||||
y: Math.max(frameRect.top, Math.min(frameRect.top + frameRect.height, y))
|
y: snapToEdge(Math.max(frameRect.top, Math.min(bottom, y)), frameRect.top, bottom)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function snapToEdge(value: number, start: number, end: number) {
|
||||||
|
if (Math.abs(value - start) <= edgeSnapDistance) return start;
|
||||||
|
if (Math.abs(value - end) <= edgeSnapDistance) return end;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
function updateSelection(x: number, y: number) {
|
function updateSelection(x: number, y: number) {
|
||||||
const start = dragStartRef.current;
|
const start = dragStartRef.current;
|
||||||
const point = clampPoint(x, y);
|
const point = clampPoint(x, y);
|
||||||
@@ -448,17 +446,19 @@ export const InputRegionOverlay = () => {
|
|||||||
setCursorPoint(point);
|
setCursorPoint(point);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePointerDown(event: PointerEvent<HTMLDivElement>) {
|
function updateHoverCursorPoint(x: number, y: number) {
|
||||||
if (
|
if (!frameRect || selection) {
|
||||||
!frameRect ||
|
updateCursorPoint(x, y);
|
||||||
event.clientX < frameRect.left ||
|
|
||||||
event.clientX > frameRect.left + frameRect.width ||
|
|
||||||
event.clientY < frameRect.top ||
|
|
||||||
event.clientY > frameRect.top + frameRect.height
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const point = clampPoint(x, y);
|
||||||
|
if (point) {
|
||||||
|
updateCursorPoint(point.x, point.y, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePointerDown(event: PointerEvent<HTMLDivElement>) {
|
||||||
const point = clampPoint(event.clientX, event.clientY);
|
const point = clampPoint(event.clientX, event.clientY);
|
||||||
if (!point) {
|
if (!point) {
|
||||||
return;
|
return;
|
||||||
@@ -480,11 +480,14 @@ export const InputRegionOverlay = () => {
|
|||||||
setCursorPoint(null);
|
setCursorPoint(null);
|
||||||
} else if (resizeRef.current) {
|
} else if (resizeRef.current) {
|
||||||
updateResizedSelection(event.clientX, event.clientY);
|
updateResizedSelection(event.clientX, event.clientY);
|
||||||
} else {
|
} else if (dragging) {
|
||||||
updateCursorPoint(event.clientX, event.clientY, dragging);
|
const point = clampPoint(event.clientX, event.clientY);
|
||||||
if (dragging) {
|
if (point) {
|
||||||
updateSelection(event.clientX, event.clientY);
|
updateCursorPoint(point.x, point.y, true);
|
||||||
|
updateSelection(point.x, point.y);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
updateHoverCursorPoint(event.clientX, event.clientY);
|
||||||
}
|
}
|
||||||
if (moveRef.current) {
|
if (moveRef.current) {
|
||||||
updateMovedSelection(event.clientX, event.clientY);
|
updateMovedSelection(event.clientX, event.clientY);
|
||||||
@@ -575,14 +578,20 @@ export const InputRegionOverlay = () => {
|
|||||||
width: Math.max(1, right - left),
|
width: Math.max(1, right - left),
|
||||||
height: Math.max(1, bottom - top)
|
height: Math.max(1, bottom - top)
|
||||||
};
|
};
|
||||||
|
const key = `${region.width}x${region.height}`;
|
||||||
|
const regions = manualRegions.some((item) => `${item.width}x${item.height}` === key)
|
||||||
|
? manualRegions.map((item) => (`${item.width}x${item.height}` === key ? region : item))
|
||||||
|
: [...manualRegions, region];
|
||||||
|
|
||||||
const rsp = await setInputRegionConfig(region, '');
|
const rsp = await setInputRegionConfig(region, regions, key);
|
||||||
if (rsp.code !== 0) {
|
if (rsp.code !== 0) {
|
||||||
setError(t('screen.controlRegion.saveFailed'));
|
setError(t('screen.controlRegion.saveFailed'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setManualInputRegion(region);
|
setManualInputRegion(region);
|
||||||
|
setManualRegions(regions);
|
||||||
|
setSelectedManualRegion(key);
|
||||||
setSelectedOriginalResolution('');
|
setSelectedOriginalResolution('');
|
||||||
setInputRegionState(region);
|
setInputRegionState(region);
|
||||||
cursorPointRef.current = null;
|
cursorPointRef.current = null;
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { useAtomValue, useSetAtom } from 'jotai';
|
|||||||
import {
|
import {
|
||||||
controlRegionModeAtom,
|
controlRegionModeAtom,
|
||||||
inputRegionAtom,
|
inputRegionAtom,
|
||||||
manualInputRegionAtom,
|
manualRegionsAtom,
|
||||||
|
resolutionAtom,
|
||||||
|
selectedManualRegionAtom,
|
||||||
selectedOriginalResolutionAtom
|
selectedOriginalResolutionAtom
|
||||||
} from '@/jotai/screen.ts';
|
} from '@/jotai/screen.ts';
|
||||||
|
|
||||||
@@ -12,52 +14,47 @@ import { getCenteredInputRegionByAspectRatio, getMediaSize } from './geometry.ts
|
|||||||
|
|
||||||
export const ManualRegion = () => {
|
export const ManualRegion = () => {
|
||||||
const mode = useAtomValue(controlRegionModeAtom);
|
const mode = useAtomValue(controlRegionModeAtom);
|
||||||
const selected = useAtomValue(selectedOriginalResolutionAtom);
|
const selectedOriginal = useAtomValue(selectedOriginalResolutionAtom);
|
||||||
const manualRegion = useAtomValue(manualInputRegionAtom);
|
const selectedManual = useAtomValue(selectedManualRegionAtom);
|
||||||
|
const manualRegions = useAtomValue(manualRegionsAtom);
|
||||||
|
const resolution = useAtomValue(resolutionAtom);
|
||||||
const setInputRegion = useSetAtom(inputRegionAtom);
|
const setInputRegion = useSetAtom(inputRegionAtom);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode !== 'manual') return;
|
if (mode !== 'manual') return;
|
||||||
if (!selected) {
|
if (selectedManual) {
|
||||||
setInputRegion(manualRegion);
|
setInputRegion(
|
||||||
|
manualRegions.find((region) => `${region.width}x${region.height}` === selectedManual) ||
|
||||||
|
null
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [width, height] = selected.split('x').map(Number);
|
if (!selectedOriginal) {
|
||||||
if (manualRegion?.width === width && manualRegion.height === height) {
|
|
||||||
setInputRegion(manualRegion);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const screen = document.getElementById('screen');
|
|
||||||
if (!screen || !width || !height) {
|
|
||||||
setInputRegion(null);
|
setInputRegion(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const target = screen;
|
|
||||||
|
const [width, height] = selectedOriginal.split('x').map(Number);
|
||||||
|
if (!width || !height) {
|
||||||
|
setInputRegion(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const update = () => {
|
const update = () => {
|
||||||
const mediaSize = getMediaSize(target);
|
const target = document.getElementById('screen');
|
||||||
|
if (!target) return;
|
||||||
|
const mediaSize = getMediaSize(target, resolution);
|
||||||
setInputRegion(
|
setInputRegion(
|
||||||
mediaSize ? getCenteredInputRegionByAspectRatio(width, height, mediaSize) : null
|
mediaSize ? getCenteredInputRegionByAspectRatio(width, height, mediaSize) : null
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
update();
|
update();
|
||||||
const observer = new MutationObserver(update);
|
const timer = window.setInterval(update, 250);
|
||||||
observer.observe(target, {
|
|
||||||
attributes: true,
|
|
||||||
attributeFilter: ['data-media-width', 'data-media-height']
|
|
||||||
});
|
|
||||||
target.addEventListener('load', update);
|
|
||||||
target.addEventListener('loadedmetadata', update);
|
|
||||||
target.addEventListener('canplay', update);
|
|
||||||
target.addEventListener('resize', update);
|
|
||||||
return () => {
|
return () => {
|
||||||
observer.disconnect();
|
window.clearInterval(timer);
|
||||||
target.removeEventListener('load', update);
|
|
||||||
target.removeEventListener('loadedmetadata', update);
|
|
||||||
target.removeEventListener('canplay', update);
|
|
||||||
target.removeEventListener('resize', update);
|
|
||||||
};
|
};
|
||||||
}, [manualRegion, mode, selected, setInputRegion]);
|
}, [manualRegions, mode, resolution, selectedManual, selectedOriginal, setInputRegion]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -20,4 +20,6 @@ export type ControlRegionConfig = Partial<InputRegion> & {
|
|||||||
mode: ControlRegionMode;
|
mode: ControlRegionMode;
|
||||||
resolutions?: OriginalResolution[];
|
resolutions?: OriginalResolution[];
|
||||||
selectedResolution?: string;
|
selectedResolution?: string;
|
||||||
|
regions?: InputRegion[];
|
||||||
|
selectedRegion?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user