From 7865b42274aa0fda9af4a6139f516e15afc4ce2b Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Sat, 25 Jul 2026 11:41:37 -0700 Subject: [PATCH 1/3] Fix SS 5.5 camera dimensions when video-format is present. SecuritySpy 5.5 emits video-format on the v5 schema; treating that as v6 left Width/Height at 0 and broke scaled RTSP clips. Co-authored-by: Cursor --- cameras_test.go | 32 ++++++++++++++++++++++++++++++++ cameras_types.go | 22 +++++++++++++++++++++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/cameras_test.go b/cameras_test.go index b8b58f3..ef7f9b5 100644 --- a/cameras_test.go +++ b/cameras_test.go @@ -20,6 +20,38 @@ func TestUnmarshalXMLCameraSchedule(t *testing.T) { asert.Equal(3, s.ID, "the data was not unmarshalled properly") } +// SS 5.5 keeps v5 width/height tags but also emits video-format (a v6-era field). +func TestUnmarshalXMLCameraSS55WithVideoFormat(t *testing.T) { + t.Parallel() + + const camXML = ` + 1 + yes + 3072 + 1728 + armed + armed + armed + yes + Mailbox + Dahua Technology + H.265 + AAC + ` + + var cam securityspy.Camera + require.NoError(t, xml.Unmarshal([]byte(camXML), &cam)) + require.Equal(t, "Mailbox", cam.Name) + require.Equal(t, 3072, cam.Width) + require.Equal(t, 1728, cam.Height) + require.Equal(t, "H.265", cam.VideoFormat) + require.Equal(t, "AAC", cam.AudioFormat) + require.True(t, cam.ModeM.Val) + require.True(t, cam.HasAudio.Val) + require.Equal(t, "Dahua Technology", cam.DeviceName) + require.Equal(t, "h265", cam.PreferredVCodec()) +} + func TestAll(t *testing.T) { t.Parallel() asert := assert.New(t) diff --git a/cameras_types.go b/cameras_types.go index 7db13b2..bfae61b 100644 --- a/cameras_types.go +++ b/cameras_types.go @@ -344,9 +344,11 @@ func (c *Camera) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { return fmt.Errorf("decoding camera xml: %w", err) } + // SS 5.5+ emits video-format / audio-format on the v5 schema; do not treat + // those alone as v6 (that left Width/Height at 0 from empty video-width tags). isV6 := raw.WidthV6 != 0 || raw.HeightV6 != 0 || raw.CapturePathV6 != "" || raw.DeviceNameV6 != "" || raw.ModeCV6.Txt != "" || raw.HasAudioV6.Txt != "" || - raw.PTZV6 != nil || raw.VideoFormat != "" || raw.StoragePathSet() + raw.PTZV6 != nil || raw.StoragePathSet() c.Number = raw.Number c.Connected = raw.Connected @@ -440,6 +442,14 @@ func (c *Camera) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { c.TLrecordAudio = raw.TLrecordAudio } + // Prefer the other schema's dimensions when the chosen branch left them unset. + if c.Width == 0 { + c.Width = firstNonZero(raw.WidthV5, raw.WidthV6) + } + if c.Height == 0 { + c.Height = firstNonZero(raw.HeightV5, raw.HeightV6) + } + c.NetworkAudio = c.AudioNetwork c.ActionSoundCam = raw.ActionSoundCam c.ActionSoundMac = raw.ActionSoundMac @@ -487,3 +497,13 @@ func firstNonEmpty(values ...string) string { return "" } + +func firstNonZero(values ...int) int { + for _, v := range values { + if v != 0 { + return v + } + } + + return 0 +} From 35b1a7da7cd8b4634083f4d8c41f0dfe0c03d03c Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Sat, 25 Jul 2026 13:43:08 -0700 Subject: [PATCH 2/3] Simplify camera schema detection for SS5 vs SS6. Drop the redundant StoragePathSet helper (same as CapturePathV6) and keep isV6 focused on v6-exclusive tags so shared fields like video-format stay safe. Co-authored-by: Cursor --- cameras_types.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/cameras_types.go b/cameras_types.go index bfae61b..31fcad1 100644 --- a/cameras_types.go +++ b/cameras_types.go @@ -344,12 +344,6 @@ func (c *Camera) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { return fmt.Errorf("decoding camera xml: %w", err) } - // SS 5.5+ emits video-format / audio-format on the v5 schema; do not treat - // those alone as v6 (that left Width/Height at 0 from empty video-width tags). - isV6 := raw.WidthV6 != 0 || raw.HeightV6 != 0 || raw.CapturePathV6 != "" || - raw.DeviceNameV6 != "" || raw.ModeCV6.Txt != "" || raw.HasAudioV6.Txt != "" || - raw.PTZV6 != nil || raw.StoragePathSet() - c.Number = raw.Number c.Connected = raw.Connected c.Name = raw.Name @@ -365,7 +359,7 @@ func (c *Camera) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { c.Overlay = raw.Overlay c.OverlayText = firstNonEmpty(raw.OverlayTextV6, raw.OverlayText) - if isV6 { + if raw.isV6() { c.Width = raw.WidthV6 c.Height = raw.HeightV6 c.ModeC = raw.ModeCV6 @@ -446,6 +440,7 @@ func (c *Camera) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { if c.Width == 0 { c.Width = firstNonZero(raw.WidthV5, raw.WidthV6) } + if c.Height == 0 { c.Height = firstNonZero(raw.HeightV5, raw.HeightV6) } @@ -484,8 +479,14 @@ func (c *Camera) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { return nil } -func (x *cameraXML) StoragePathSet() bool { - return x.CapturePathV6 != "" +// isV6 reports whether this camera element used SecuritySpy 6+ tags +// (video-width/height, storage-path, device-name, cc-mode, has-audio, ptz-features). +// Shared fields like video-format also appear on SS 5.5's v5 schema, so they +// must not be used as the sole signal. +func (x *cameraXML) isV6() bool { + return x.WidthV6 != 0 || x.HeightV6 != 0 || x.CapturePathV6 != "" || + x.DeviceNameV6 != "" || x.ModeCV6.Txt != "" || x.HasAudioV6.Txt != "" || + x.PTZV6 != nil } func firstNonEmpty(values ...string) string { From aa11f7e8cdaaf3d8a3dfb02700dae6197e7ee043 Mon Sep 17 00:00:00 2001 From: David Newhall II Date: Sat, 25 Jul 2026 13:43:43 -0700 Subject: [PATCH 3/3] Drop unused cyclop nolint on UnmarshalXML. Co-authored-by: Cursor --- cameras_types.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cameras_types.go b/cameras_types.go index 31fcad1..fdadb05 100644 --- a/cameras_types.go +++ b/cameras_types.go @@ -337,7 +337,7 @@ type cameraXML struct { // UnmarshalXML decodes v5 or v6 ++systemInfo camera elements into Camera. // -//nolint:cyclop,funlen // dual schema mapping +//nolint:funlen // dual schema mapping func (c *Camera) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { var raw cameraXML if err := d.DecodeElement(&raw, &start); err != nil {