python-aidot-cameras 0.5.7__tar.gz → 0.5.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_aidot_cameras-0.5.7/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.5.9}/PKG-INFO +1 -1
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/pyproject.toml +1 -1
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/src/aidot/device_client.py +129 -4
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9/src/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/src/python_aidot_cameras.egg-info/SOURCES.txt +2 -0
- python_aidot_cameras-0.5.9/tests/test_no_undefined_names.py +44 -0
- python_aidot_cameras-0.5.9/tests/test_sdes_sprop.py +111 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/LICENSE +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/README.md +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/setup.cfg +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/src/aidot/__init__.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/src/aidot/aes_utils.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/src/aidot/client.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/src/aidot/const.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/src/aidot/credentials.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/src/aidot/discover.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/src/aidot/exceptions.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/src/aidot/g711.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/src/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/src/python_aidot_cameras.egg-info/requires.txt +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/src/python_aidot_cameras.egg-info/top_level.txt +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/tests/test_alarm_event.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/tests/test_highport_nomination.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/tests/test_motion_poll.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/tests/test_sdes_talk.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/tests/test_sdes_watchdog.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/tests/test_speak.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/tests/test_stream_cap.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/tests/test_talk.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/tests/test_terminal_ack.py +0 -0
- {python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9}/tests/test_token_refresh.py +0 -0
{python_aidot_cameras-0.5.7/src/python_aidot_cameras.egg-info → python_aidot_cameras-0.5.9}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.9
|
|
4
4
|
Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
|
|
5
5
|
Author-email: cbrightly <chris.brightly@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-aidot-cameras"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.9"
|
|
8
8
|
description = "Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -1984,6 +1984,105 @@ def _get_stream_slots() -> "asyncio.Semaphore":
|
|
|
1984
1984
|
return _STREAM_SLOTS
|
|
1985
1985
|
|
|
1986
1986
|
|
|
1987
|
+
# Where captured SPS/PPS are cached. Override with AIDOT_SPROP_DIR so HA can
|
|
1988
|
+
# point it at persistent storage (the core container's ~ is ephemeral; without a
|
|
1989
|
+
# persistent dir the cache simply re-bootstraps from the first stream each boot).
|
|
1990
|
+
_SPROP_DIR = os.environ.get("AIDOT_SPROP_DIR") or os.path.join(
|
|
1991
|
+
os.path.expanduser("~"), ".config", "aidot", "sprop")
|
|
1992
|
+
|
|
1993
|
+
|
|
1994
|
+
def _extract_param_sets_from_rtp(pkt: bytes) -> dict:
|
|
1995
|
+
"""Pull H.264 SPS (NAL type 7) / PPS (type 8) out of one RTP packet.
|
|
1996
|
+
|
|
1997
|
+
Handles single-NAL-unit packets and STAP-A (type 24) aggregation - SPS/PPS
|
|
1998
|
+
are small and the camera sends them either as their own packets or in a
|
|
1999
|
+
STAP-A at the head of an IDR. FU-A fragments are ignored (param sets are not
|
|
2000
|
+
fragmented in practice). Returns ``{7: sps_bytes, 8: pps_bytes}`` for those
|
|
2001
|
+
found (each value is the NAL unit *including* its header byte). Pure /
|
|
2002
|
+
unit-testable: parses the RTP header length defensively."""
|
|
2003
|
+
if len(pkt) < 13:
|
|
2004
|
+
return {}
|
|
2005
|
+
cc = pkt[0] & 0x0F
|
|
2006
|
+
ext = (pkt[0] >> 4) & 0x01
|
|
2007
|
+
off = 12 + 4 * cc
|
|
2008
|
+
if ext and len(pkt) >= off + 4:
|
|
2009
|
+
ext_words = int.from_bytes(pkt[off + 2:off + 4], "big")
|
|
2010
|
+
off += 4 + 4 * ext_words
|
|
2011
|
+
payload = pkt[off:]
|
|
2012
|
+
if not payload:
|
|
2013
|
+
return {}
|
|
2014
|
+
out: dict = {}
|
|
2015
|
+
ntype = payload[0] & 0x1F
|
|
2016
|
+
if ntype == 24: # STAP-A: [hdr] then [ (size16)(nal) ]...
|
|
2017
|
+
i = 1
|
|
2018
|
+
while i + 2 <= len(payload):
|
|
2019
|
+
size = int.from_bytes(payload[i:i + 2], "big")
|
|
2020
|
+
i += 2
|
|
2021
|
+
nal = payload[i:i + size]
|
|
2022
|
+
i += size
|
|
2023
|
+
if nal:
|
|
2024
|
+
t = nal[0] & 0x1F
|
|
2025
|
+
if t in (7, 8):
|
|
2026
|
+
out[t] = nal
|
|
2027
|
+
elif ntype in (7, 8): # single NAL unit packet
|
|
2028
|
+
out[ntype] = payload
|
|
2029
|
+
return out
|
|
2030
|
+
|
|
2031
|
+
|
|
2032
|
+
def _build_sprop(sps: bytes, pps: bytes) -> str:
|
|
2033
|
+
"""``sprop-parameter-sets`` value: base64(SPS NAL),base64(PPS NAL)."""
|
|
2034
|
+
import base64 as _b64
|
|
2035
|
+
return _b64.b64encode(sps).decode() + "," + _b64.b64encode(pps).decode()
|
|
2036
|
+
|
|
2037
|
+
|
|
2038
|
+
def _sprop_cache_path(devid: str) -> str:
|
|
2039
|
+
return os.path.join(_SPROP_DIR, f"{devid}.sprop")
|
|
2040
|
+
|
|
2041
|
+
|
|
2042
|
+
def _load_sprop(devid: str) -> "Optional[str]":
|
|
2043
|
+
"""Cached ``sprop-parameter-sets`` for ``devid`` (captured from a prior
|
|
2044
|
+
stream), or None. Fail-safe: any error -> None (SDP omits sprop = today's
|
|
2045
|
+
behaviour)."""
|
|
2046
|
+
try:
|
|
2047
|
+
with open(_sprop_cache_path(devid), "r") as fh:
|
|
2048
|
+
s = fh.read().strip()
|
|
2049
|
+
return s or None
|
|
2050
|
+
except OSError:
|
|
2051
|
+
return None
|
|
2052
|
+
|
|
2053
|
+
|
|
2054
|
+
def _save_sprop(devid: str, sprop: str) -> None:
|
|
2055
|
+
"""Persist the ``sprop-parameter-sets`` string for ``devid`` (best-effort)."""
|
|
2056
|
+
try:
|
|
2057
|
+
os.makedirs(_SPROP_DIR, exist_ok=True)
|
|
2058
|
+
tmp = _sprop_cache_path(devid) + ".tmp"
|
|
2059
|
+
with open(tmp, "w") as fh:
|
|
2060
|
+
fh.write(sprop)
|
|
2061
|
+
os.replace(tmp, _sprop_cache_path(devid))
|
|
2062
|
+
except OSError as exc:
|
|
2063
|
+
# Surface (don't swallow): if the cache dir isn't writable the whole
|
|
2064
|
+
# sprop feature is silently inert. AIDOT_SPROP_DIR can redirect it.
|
|
2065
|
+
_LOGGER.warning("sprop cache write failed (%s): %s — set AIDOT_SPROP_DIR "
|
|
2066
|
+
"to a writable path", _SPROP_DIR, exc)
|
|
2067
|
+
|
|
2068
|
+
|
|
2069
|
+
def _inject_sprop(sdp: str, devid: str) -> str:
|
|
2070
|
+
"""Append the cached ``sprop-parameter-sets`` to the ffmpeg-input SDP's
|
|
2071
|
+
``a=fmtp:96`` line so ffmpeg inits the H.264 decoder out-of-band (robust to
|
|
2072
|
+
in-band SPS loss). No-op if nothing is cached yet or sprop is already
|
|
2073
|
+
present. Applied at every ffmpeg-input SDP write so both the serve path and
|
|
2074
|
+
the snapshot/file path benefit."""
|
|
2075
|
+
sprop = _load_sprop(devid)
|
|
2076
|
+
if not sprop or "sprop-parameter-sets=" in sdp:
|
|
2077
|
+
return sdp
|
|
2078
|
+
out = []
|
|
2079
|
+
for ln in sdp.split("\r\n"):
|
|
2080
|
+
if ln.startswith("a=fmtp:96") and "sprop-parameter-sets" not in ln:
|
|
2081
|
+
ln = ln + ";sprop-parameter-sets=" + sprop
|
|
2082
|
+
out.append(ln)
|
|
2083
|
+
return "\r\n".join(out)
|
|
2084
|
+
|
|
2085
|
+
|
|
1987
2086
|
def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
|
|
1988
2087
|
"""Mux tapped video (H.264 copy) + audio (PCMA->AAC) to ``out_fileobj`` as
|
|
1989
2088
|
RTP-timestamped MPEG-TS. Runs in a worker thread; the serve's ffmpeg reads
|
|
@@ -9224,7 +9323,7 @@ class DeviceClient(object):
|
|
|
9224
9323
|
)
|
|
9225
9324
|
|
|
9226
9325
|
sdp_path = await asyncio.get_running_loop().run_in_executor(
|
|
9227
|
-
None, _make_sdp_tempfile, ffmpeg_sdp)
|
|
9326
|
+
None, _make_sdp_tempfile, _inject_sprop(ffmpeg_sdp, self.device_id))
|
|
9228
9327
|
|
|
9229
9328
|
# --- Send webrtcReq BEFORE releasing reservation sockets ------------- #
|
|
9230
9329
|
# ICE cameras (e.g. LK.IPC.A001064) send STUN binding requests to our
|
|
@@ -10178,7 +10277,7 @@ class DeviceClient(object):
|
|
|
10178
10277
|
)
|
|
10179
10278
|
try:
|
|
10180
10279
|
await asyncio.get_running_loop().run_in_executor(
|
|
10181
|
-
None, _write_text_file, sdp_path, _updated_sdp)
|
|
10280
|
+
None, _write_text_file, sdp_path, _inject_sprop(_updated_sdp, self.device_id))
|
|
10182
10281
|
except Exception as _sdp_exc:
|
|
10183
10282
|
_LOGGER.warning("_open_sdes_stream: could not rewrite SDP: %s", _sdp_exc)
|
|
10184
10283
|
|
|
@@ -10423,6 +10522,7 @@ class DeviceClient(object):
|
|
|
10423
10522
|
# TUTK cameras use plain RTP/AVP (no SRTP): the bridge synthesizes
|
|
10424
10523
|
# standard RTP packets from TUTK SFrames and forwards without crypto.
|
|
10425
10524
|
_ts_br = int(time.time())
|
|
10525
|
+
# (Out-of-band SPS/PPS is injected at the write below via _inject_sprop.)
|
|
10426
10526
|
if _use_plain_rtp:
|
|
10427
10527
|
_br_sdp = (
|
|
10428
10528
|
"v=0\r\n"
|
|
@@ -10467,7 +10567,7 @@ class DeviceClient(object):
|
|
|
10467
10567
|
)
|
|
10468
10568
|
try:
|
|
10469
10569
|
await asyncio.get_running_loop().run_in_executor(
|
|
10470
|
-
None, _write_text_file, sdp_path, _br_sdp)
|
|
10570
|
+
None, _write_text_file, sdp_path, _inject_sprop(_br_sdp, self.device_id))
|
|
10471
10571
|
except Exception as _br_sdp_exc:
|
|
10472
10572
|
_LOGGER.warning("bridge: could not rewrite SDP: %s", _br_sdp_exc)
|
|
10473
10573
|
|
|
@@ -11548,6 +11648,26 @@ class DeviceClient(object):
|
|
|
11548
11648
|
f" ssrc=0x{_ssrc_d:08x}"
|
|
11549
11649
|
f" pt={_pt}"
|
|
11550
11650
|
)
|
|
11651
|
+
# Capture this camera's SPS/PPS once so future streams
|
|
11652
|
+
# can inject sprop-parameter-sets (out-of-band decoder
|
|
11653
|
+
# init, robust to in-band SPS loss). Parses only until
|
|
11654
|
+
# both are seen; then _sprop_done short-circuits.
|
|
11655
|
+
if (_kind == "video"
|
|
11656
|
+
and not getattr(_bridge_fn, "_sprop_done", False)):
|
|
11657
|
+
_ps = _extract_param_sets_from_rtp(_fwd_pkt)
|
|
11658
|
+
if _ps:
|
|
11659
|
+
_psc = getattr(_bridge_fn, "_ps_cache", None)
|
|
11660
|
+
if _psc is None:
|
|
11661
|
+
_psc = {}
|
|
11662
|
+
_bridge_fn._ps_cache = _psc
|
|
11663
|
+
_psc.update(_ps)
|
|
11664
|
+
if 7 in _psc and 8 in _psc:
|
|
11665
|
+
_sprop_new = _build_sprop(_psc[7], _psc[8])
|
|
11666
|
+
if _sprop_new != _load_sprop(self.device_id):
|
|
11667
|
+
_save_sprop(self.device_id, _sprop_new)
|
|
11668
|
+
_status("bridge: cached sprop-parameter"
|
|
11669
|
+
f"-sets for {self.device_id}")
|
|
11670
|
+
_bridge_fn._sprop_done = True
|
|
11551
11671
|
# Rebase RTP timestamps to start near 0. Camera picks a
|
|
11552
11672
|
# random starting timestamp (RFC 3550 §5.1); the 90 kHz
|
|
11553
11673
|
# video clock can be near 2^32 and wraps, producing huge
|
|
@@ -11981,8 +12101,13 @@ class DeviceClient(object):
|
|
|
11981
12101
|
"a=rtcp-mux\r\n"
|
|
11982
12102
|
)
|
|
11983
12103
|
try:
|
|
12104
|
+
# Re-apply the cached sprop-parameter-sets here too:
|
|
12105
|
+
# this serve-restart rewrite is the SDP the keepalive
|
|
12106
|
+
# ffmpeg actually reads on every watchdog cycle, so
|
|
12107
|
+
# without this the out-of-band SPS is lost on restart
|
|
12108
|
+
# (the failure persisted live even for cached cameras).
|
|
11984
12109
|
with open(sdp_path, "w") as _f2:
|
|
11985
|
-
_f2.write(_new_sdp)
|
|
12110
|
+
_f2.write(_inject_sprop(_new_sdp, self.device_id))
|
|
11986
12111
|
except Exception as _sdp_exc2:
|
|
11987
12112
|
_LOGGER.warning("could not rewrite SDP for restart: %s", _sdp_exc2)
|
|
11988
12113
|
proc = subprocess.Popen(
|
{python_aidot_cameras-0.5.7 → python_aidot_cameras-0.5.9/src/python_aidot_cameras.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-aidot-cameras
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.9
|
|
4
4
|
Summary: Control AiDot/Leedarson WiFi lights and cameras (WebRTC streaming, two-way audio, PTZ, controls)
|
|
5
5
|
Author-email: cbrightly <chris.brightly@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -18,6 +18,8 @@ src/python_aidot_cameras.egg-info/top_level.txt
|
|
|
18
18
|
tests/test_alarm_event.py
|
|
19
19
|
tests/test_highport_nomination.py
|
|
20
20
|
tests/test_motion_poll.py
|
|
21
|
+
tests/test_no_undefined_names.py
|
|
22
|
+
tests/test_sdes_sprop.py
|
|
21
23
|
tests/test_sdes_talk.py
|
|
22
24
|
tests/test_sdes_watchdog.py
|
|
23
25
|
tests/test_speak.py
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Guard against NameError-class regressions in device_client.py.
|
|
2
|
+
|
|
3
|
+
`ast.parse`/`py_compile` only catch syntax errors, not undefined names used at
|
|
4
|
+
runtime (e.g. a variable referenced in one method but defined only in another).
|
|
5
|
+
That class of bug shipped once (0.5.5/0.5.6: `_fast_connect` referenced in
|
|
6
|
+
`_open_sdes_stream` but defined only in `_async_open_webrtc_stream_impl`,
|
|
7
|
+
breaking every SDES camera open). This test runs pyflakes and fails on any
|
|
8
|
+
undefined name except a small allowlist of intentional closure patterns.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pathlib
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import pytest
|
|
16
|
+
|
|
17
|
+
_SRC = (
|
|
18
|
+
pathlib.Path(__file__).resolve().parent.parent
|
|
19
|
+
/ "src" / "aidot" / "device_client.py"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Names pyflakes can't resolve because they're bound in an inner runtime scope
|
|
23
|
+
# (closure) and referenced from a sibling scope; verified working in production
|
|
24
|
+
# and already marked `# noqa: F821` in the source.
|
|
25
|
+
_ALLOWLIST = {"_enc_c8_sctp"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_device_client_has_no_undefined_names():
|
|
29
|
+
try:
|
|
30
|
+
proc = subprocess.run(
|
|
31
|
+
[sys.executable, "-m", "pyflakes", str(_SRC)],
|
|
32
|
+
capture_output=True, text=True,
|
|
33
|
+
)
|
|
34
|
+
except FileNotFoundError: # pragma: no cover
|
|
35
|
+
pytest.skip("pyflakes not available")
|
|
36
|
+
if "No module named pyflakes" in proc.stderr: # pragma: no cover
|
|
37
|
+
pytest.skip("pyflakes not installed")
|
|
38
|
+
|
|
39
|
+
undefined = [
|
|
40
|
+
line for line in proc.stdout.splitlines()
|
|
41
|
+
if "undefined name" in line
|
|
42
|
+
and not any(name in line for name in _ALLOWLIST)
|
|
43
|
+
]
|
|
44
|
+
assert not undefined, "Undefined names (NameError risk):\n" + "\n".join(undefined)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Unit tests for the SDES out-of-band SPS/PPS (sprop-parameter-sets) helpers.
|
|
2
|
+
|
|
3
|
+
A lost in-band SPS wedges ffmpeg ("could not find codec parameters"); injecting
|
|
4
|
+
the camera's SPS/PPS into the ffmpeg SDP as sprop-parameter-sets initializes the
|
|
5
|
+
decoder out-of-band so the loss no longer matters. These pure helpers capture
|
|
6
|
+
the param sets from RTP, format the sprop value, and cache it per camera.
|
|
7
|
+
"""
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
import tempfile
|
|
11
|
+
|
|
12
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
13
|
+
|
|
14
|
+
import aidot.device_client as dc
|
|
15
|
+
from aidot.device_client import (
|
|
16
|
+
_extract_param_sets_from_rtp,
|
|
17
|
+
_build_sprop,
|
|
18
|
+
_load_sprop,
|
|
19
|
+
_save_sprop,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Real Driveway (A001513) parameter sets captured live 2026-06-08.
|
|
23
|
+
SPS = bytes.fromhex("27640033ad00ce8050079a6a020203e0000003002000000303c6f207d00bbffff814")
|
|
24
|
+
PPS = bytes.fromhex("28ee3cb0")
|
|
25
|
+
SPROP = "J2QAM60AzoBQB5pqAgID4AAAAwAgAAADA8byB9ALv//4FA==,KO48sA=="
|
|
26
|
+
|
|
27
|
+
RTP = b"\x80\x60\x00\x01\x00\x00\x00\x00\x12\x34\x56\x78" # V2, no CC/ext, 12B
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ---- _build_sprop ----------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
def test_build_sprop_matches_live_vector():
|
|
33
|
+
assert _build_sprop(SPS, PPS) == SPROP
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---- _extract_param_sets_from_rtp -----------------------------------------
|
|
37
|
+
|
|
38
|
+
def test_single_nal_sps():
|
|
39
|
+
assert _extract_param_sets_from_rtp(RTP + SPS) == {7: SPS}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_single_nal_pps():
|
|
43
|
+
assert _extract_param_sets_from_rtp(RTP + PPS) == {8: PPS}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_stap_a_carries_both():
|
|
47
|
+
stap = bytes([0x78]) # 0x78 & 0x1f == 24 (STAP-A)
|
|
48
|
+
stap += len(SPS).to_bytes(2, "big") + SPS
|
|
49
|
+
stap += len(PPS).to_bytes(2, "big") + PPS
|
|
50
|
+
assert _extract_param_sets_from_rtp(RTP + stap) == {7: SPS, 8: PPS}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_slice_packet_yields_nothing():
|
|
54
|
+
# NAL type 1 (non-IDR slice) - not a parameter set
|
|
55
|
+
assert _extract_param_sets_from_rtp(RTP + bytes([0x61, 0x00, 0x11])) == {}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_short_or_empty_packet_safe():
|
|
59
|
+
assert _extract_param_sets_from_rtp(b"") == {}
|
|
60
|
+
assert _extract_param_sets_from_rtp(b"\x80\x60\x00") == {}
|
|
61
|
+
assert _extract_param_sets_from_rtp(RTP) == {} # header only, no payload
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_csrc_header_offset_handled():
|
|
65
|
+
# CC=2 -> 12 + 8 bytes of CSRC before the payload
|
|
66
|
+
hdr = b"\x82\x60\x00\x01\x00\x00\x00\x00\x12\x34\x56\x78" + b"\xaa\xaa\xaa\xaa\xbb\xbb\xbb\xbb"
|
|
67
|
+
assert _extract_param_sets_from_rtp(hdr + SPS) == {7: SPS}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---- cache roundtrip -------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
def test_cache_roundtrip(monkeypatch):
|
|
73
|
+
d = tempfile.mkdtemp()
|
|
74
|
+
monkeypatch.setattr(dc, "_SPROP_DIR", d)
|
|
75
|
+
devid = "abc123"
|
|
76
|
+
assert _load_sprop(devid) is None # nothing cached yet
|
|
77
|
+
_save_sprop(devid, SPROP)
|
|
78
|
+
assert _load_sprop(devid) == SPROP # persists + reads back
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_load_missing_is_none(monkeypatch):
|
|
82
|
+
monkeypatch.setattr(dc, "_SPROP_DIR", "/nonexistent/path/xyz")
|
|
83
|
+
assert _load_sprop("whatever") is None # fail-safe, no raise
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
import traceback
|
|
88
|
+
|
|
89
|
+
class _MP: # tiny monkeypatch shim so this runs without pytest
|
|
90
|
+
def __init__(self): self._undo = []
|
|
91
|
+
def setattr(self, obj, name, val):
|
|
92
|
+
self._undo.append((obj, name, getattr(obj, name)))
|
|
93
|
+
setattr(obj, name, val)
|
|
94
|
+
def undo(self):
|
|
95
|
+
for obj, name, val in reversed(self._undo):
|
|
96
|
+
setattr(obj, name, val)
|
|
97
|
+
|
|
98
|
+
_fail = 0
|
|
99
|
+
for _k, _v in sorted(globals().items()):
|
|
100
|
+
if _k.startswith("test_"):
|
|
101
|
+
mp = _MP()
|
|
102
|
+
try:
|
|
103
|
+
_v(mp) if "monkeypatch" in _v.__code__.co_varnames else _v()
|
|
104
|
+
print(f"PASS {_k}")
|
|
105
|
+
except Exception:
|
|
106
|
+
_fail += 1
|
|
107
|
+
print(f"FAIL {_k}")
|
|
108
|
+
traceback.print_exc()
|
|
109
|
+
finally:
|
|
110
|
+
mp.undo()
|
|
111
|
+
raise SystemExit(1 if _fail else 0)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|