python-aidot-cameras 1.0.0b32__tar.gz → 1.0.0b33__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.
Files changed (270) hide show
  1. {python_aidot_cameras-1.0.0b32/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0b33}/PKG-INFO +1 -1
  2. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/sdes_open.py +166 -4
  3. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/pyproject.toml +1 -1
  4. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33/python_aidot_cameras.egg-info}/PKG-INFO +1 -1
  5. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
  6. python_aidot_cameras-1.0.0b33/tests/test_answer_pt_kinds.py +141 -0
  7. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/LICENSE +0 -0
  8. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/MANIFEST.in +0 -0
  9. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/README.md +0 -0
  10. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/__init__.py +0 -0
  11. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/__main__.py +0 -0
  12. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_upstream.py +0 -0
  13. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/__init__.py +0 -0
  14. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  15. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  16. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  17. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  18. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  19. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  20. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  21. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  22. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  23. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  24. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  25. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  26. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  27. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  28. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  29. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  30. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  31. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  32. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  33. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  34. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  35. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  36. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  37. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  38. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  39. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  40. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  41. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  42. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  43. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  44. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  45. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  46. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  47. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/ble_gateway.py +0 -0
  48. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/__init__.py +0 -0
  49. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/client.py +0 -0
  50. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/constants.py +0 -0
  51. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/controls.py +0 -0
  52. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/go2rtc.py +0 -0
  53. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/hwaccel.py +0 -0
  54. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/lan_control.py +0 -0
  55. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/models.py +0 -0
  56. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/playback.py +0 -0
  57. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/protocol.py +0 -0
  58. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/sd_events.py +0 -0
  59. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/sd_listing.py +0 -0
  60. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/sd_playback.py +0 -0
  61. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/sdes.py +0 -0
  62. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/tutk.py +0 -0
  63. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/webrtc.py +0 -0
  64. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/camera/webrtc_open.py +0 -0
  65. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/client.py +0 -0
  66. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/cloud_auth.py +0 -0
  67. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/const.py +0 -0
  68. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/credentials.py +0 -0
  69. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/crypto.py +0 -0
  70. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/device_client.py +0 -0
  71. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/discover.py +0 -0
  72. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/exceptions.py +0 -0
  73. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/g711.py +0 -0
  74. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/lan_retry.py +0 -0
  75. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/aidot_cameras/py.typed +0 -0
  76. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  77. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  78. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/python_aidot_cameras.egg-info/requires.txt +0 -0
  79. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  80. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/setup.cfg +0 -0
  81. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_aioice_compat.py +0 -0
  82. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_alarm_event.py +0 -0
  83. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_answer_sdp_video_pt.py +0 -0
  84. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_answer_section_selection.py +0 -0
  85. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_audio_mux_offline.py +0 -0
  86. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  87. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_avio_request.py +0 -0
  88. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_avio_response_parse.py +0 -0
  89. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_avio_response_router.py +0 -0
  90. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_backoff.py +0 -0
  91. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_battery_detection.py +0 -0
  92. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_battery_evidence_corroboration.py +0 -0
  93. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_battery_skip_turn.py +0 -0
  94. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_bitrate_campaign_arms.py +0 -0
  95. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_ble_gateway.py +0 -0
  96. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_bridge_state_sentinels.py +0 -0
  97. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_busy_backoff.py +0 -0
  98. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_canary_log_names_the_camera.py +0 -0
  99. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_cloud_count.py +0 -0
  100. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_cloud_plan.py +0 -0
  101. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_constants_imports_resolve.py +0 -0
  102. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_control_verdicts.py +0 -0
  103. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_deferred_hardening.py +0 -0
  104. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_device_cmd_seq_match.py +0 -0
  105. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_device_login_guard.py +0 -0
  106. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_device_user_info_cache.py +0 -0
  107. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_direct_ts_serve.py +0 -0
  108. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_dormant_camera_cleanup.py +0 -0
  109. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_drain_is_cancellable.py +0 -0
  110. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_mux_terminal_write.py +0 -0
  111. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_not_ready_burst.py +0 -0
  112. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_pinning.py +0 -0
  113. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_resolution_ack.py +0 -0
  114. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_serve_decode_skip.py +0 -0
  115. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_serve_ice_wait.py +0 -0
  116. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_serve_open_timeout.py +0 -0
  117. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_skip_signaling_wait.py +0 -0
  118. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_slow_probe.py +0 -0
  119. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_stdout_producer.py +0 -0
  120. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_dtls_video_presence.py +0 -0
  121. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_egress_guard.py +0 -0
  122. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_env_seams.py +0 -0
  123. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  124. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  125. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_futile_keepalive.py +0 -0
  126. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  127. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_go2rtc.py +0 -0
  128. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_go2rtc_cli.py +0 -0
  129. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_h264_decode_rate_limit.py +0 -0
  130. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_highport_nomination.py +0 -0
  131. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_hwaccel_probe.py +0 -0
  132. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_ice_config_cache.py +0 -0
  133. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_ice_peer_reflexive.py +0 -0
  134. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_idle_release_both_loops.py +0 -0
  135. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_keepalive_renew.py +0 -0
  136. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_key_restart_sdp.py +0 -0
  137. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_keyframe_prompter.py +0 -0
  138. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_lan_control.py +0 -0
  139. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  140. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_late_repeats_are_dropped.py +0 -0
  141. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_light_active_color_mode.py +0 -0
  142. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_live_session_carries_talk.py +0 -0
  143. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_live_stream_param.py +0 -0
  144. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_live_validate_policy.py +0 -0
  145. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_live_video_canary.py +0 -0
  146. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_local_only_controls.py +0 -0
  147. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_logging_caps.py +0 -0
  148. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_login_connect_deadline.py +0 -0
  149. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_login_reconnect_backoff.py +0 -0
  150. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_media_counted_only_when_plaintext.py +0 -0
  151. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_media_path_is_reported.py +0 -0
  152. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_media_socket_rcvbuf.py +0 -0
  153. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_media_wait_floor.py +0 -0
  154. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_motion_poll.py +0 -0
  155. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_mqtt_client_id_is_exact.py +0 -0
  156. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_mqtt_credential_refetch.py +0 -0
  157. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_mqtt_credential_self_heal.py +0 -0
  158. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_mqtt_password_not_persisted.py +0 -0
  159. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_mqtt_session_reconnect.py +0 -0
  160. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  161. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_nack.py +0 -0
  162. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  163. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_narrow_pc_ice.py +0 -0
  164. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_no_key_material_in_logs.py +0 -0
  165. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_no_self_referential_go2rtc.py +0 -0
  166. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_no_undefined_names.py +0 -0
  167. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_offer_bandwidth_line.py +0 -0
  168. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  169. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_offline_keepalive_pause.py +0 -0
  170. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_on_frame_is_an_av_frame.py +0 -0
  171. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_open_fail_logger.py +0 -0
  172. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_open_gate_delay.py +0 -0
  173. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_peerid_reuse.py +0 -0
  174. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_persistent_mqtt.py +0 -0
  175. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_persistent_mqtt_robustness.py +0 -0
  176. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_playback_tls.py +0 -0
  177. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_post_merge_hardening.py +0 -0
  178. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  179. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_public_signature_matches_impl.py +0 -0
  180. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_quality_campaign_in_session.py +0 -0
  181. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_recent_recordings.py +0 -0
  182. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_remb.py +0 -0
  183. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  184. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_resent_frames_are_dropped.py +0 -0
  185. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_resolution_persists.py +0 -0
  186. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_restart_cycle.py +0 -0
  187. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_retry_policy.py +0 -0
  188. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_scripts_importable.py +0 -0
  189. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sctp_init_ack_tsn.py +0 -0
  190. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sd_card_presence.py +0 -0
  191. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sd_event_decode.py +0 -0
  192. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sd_event_probe_payloads.py +0 -0
  193. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sd_listing.py +0 -0
  194. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sd_playback.py +0 -0
  195. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_adaptive.py +0 -0
  196. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_avio_dispatch.py +0 -0
  197. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_bridge_break_gate.py +0 -0
  198. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_connection_mode.py +0 -0
  199. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_echo_wait_timeout.py +0 -0
  200. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_fast_liveplay.py +0 -0
  201. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  202. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_first_media_stall_report.py +0 -0
  203. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_idle_release.py +0 -0
  204. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_nack_bridge.py +0 -0
  205. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_offer_video_order.py +0 -0
  206. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_offer_video_pin.py +0 -0
  207. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_open_cleanup.py +0 -0
  208. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_sctp_abort.py +0 -0
  209. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_serve_audio.py +0 -0
  210. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_serve_cmd.py +0 -0
  211. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_serve_stderr_drain.py +0 -0
  212. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_sprop.py +0 -0
  213. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_talk.py +0 -0
  214. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_teardown_exit_log.py +0 -0
  215. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_terminal_ack.py +0 -0
  216. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_terminal_ack_latency.py +0 -0
  217. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_tmmbr_bridge.py +0 -0
  218. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdes_watchdog.py +0 -0
  219. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdp_narrowing_properties.py +0 -0
  220. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sdp_payload_type_narrowing.py +0 -0
  221. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_security_hardening.py +0 -0
  222. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  223. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_serializable_login_info.py +0 -0
  224. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_serve_input_timeout.py +0 -0
  225. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_serve_loss_tolerance.py +0 -0
  226. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_serve_pt_respects_the_pin.py +0 -0
  227. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_serve_relay.py +0 -0
  228. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  229. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_session_stats.py +0 -0
  230. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_shared_house_seam.py +0 -0
  231. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_snapshot_cancellation.py +0 -0
  232. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_speak.py +0 -0
  233. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sprop_capture_gate.py +0 -0
  234. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sprop_staleness.py +0 -0
  235. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sprop_unstable_public.py +0 -0
  236. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  237. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_srtp_rx_rekey.py +0 -0
  238. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_srtp_tx_key_note.py +0 -0
  239. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_stall_nudge_policy.py +0 -0
  240. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  241. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  242. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  243. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  244. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  245. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_stream_cap.py +0 -0
  246. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_stream_drain_ownership.py +0 -0
  247. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_stream_idle.py +0 -0
  248. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_stream_slot_cap.py +0 -0
  249. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_stream_teardown.py +0 -0
  250. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_talk.py +0 -0
  251. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  252. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_tap_to_mux_end_to_end.py +0 -0
  253. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_terminal_ack.py +0 -0
  254. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_tmmbr.py +0 -0
  255. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_token_file_robustness.py +0 -0
  256. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_token_refresh.py +0 -0
  257. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_turn_create_permission.py +0 -0
  258. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  259. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_upstream_compat.py +0 -0
  260. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_validator_shares_one_token.py +0 -0
  261. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_video_profile_log.py +0 -0
  262. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_video_pts_dts.py +0 -0
  263. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_video_ts_unwrap.py +0 -0
  264. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_viewer_check_throttle.py +0 -0
  265. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_viewer_presence.py +0 -0
  266. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_viewer_present_push_mode.py +0 -0
  267. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_wait_or_event.py +0 -0
  268. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_wake_gate_camera_evidence.py +0 -0
  269. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_wake_readiness_retry.py +0 -0
  270. {python_aidot_cameras-1.0.0b32 → python_aidot_cameras-1.0.0b33}/tests/test_webrtc_answer_race.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 1.0.0b32
3
+ Version: 1.0.0b33
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
@@ -746,6 +746,77 @@ def video_pt_from_answer_sdp(sdp_text: str) -> Optional[int]:
746
746
  return None
747
747
 
748
748
 
749
+ def answer_pt_kinds(sdp_text: str) -> dict:
750
+ """Map each payload type in the camera's answer to the kind that owns it.
751
+
752
+ The bridge demuxes a BUNDLEd stream by RTP payload type, and until this
753
+ existed it did so from a fixed table: 96/97/98 video, 0/8 audio. That
754
+ table is wrong for this fleet. Measured 2026-08-26 across 107 A001064
755
+ opens, **15 of them (14%) negotiated H265 on payload type 0** -- announced
756
+ correctly in the answer as ``m=video ... 0`` with ``a=rtpmap:0 H265/90000``
757
+ -- and then sent 2668 full-size (1222 B) video packets on pt=0 while audio
758
+ ran normally on pt=8. The fixed table posted every one of those video
759
+ packets to ffmpeg's AUDIO loopback, so no video was ever observed, the 75 s
760
+ first-media wait ran out, and the serve launched into an empty stream and
761
+ exited: 82 s wasted, then a full reopen. The other 92 opens answered
762
+ ``m=video ... 96`` and worked. Perfect separation, both directions.
763
+
764
+ The answer is the authority on this and is already in hand: PT numbering is
765
+ per-m-section by definition, so a section's fmt list names the kind of every
766
+ payload type in it. ``tests/test_answer_section_selection.py`` records the
767
+ same renumbering on the A000088 (PT 0 carrying H265), and the DTLS path
768
+ already selects sections by content for it; the SDES bridge was the one
769
+ place still trusting a static tuple.
770
+
771
+ A payload type claimed by BOTH an audio and a video section is dropped from
772
+ the map rather than guessed at -- an ambiguous answer is not better evidence
773
+ than the fallback table, and silently preferring one section would be the
774
+ same class of assumption this function exists to remove. Malformed m= lines
775
+ are skipped, never raised on: this feeds a media path.
776
+ """
777
+ kinds: dict = {}
778
+ dropped: set = set()
779
+ for raw in (sdp_text or "").splitlines():
780
+ line = raw.strip()
781
+ if not line.startswith("m=audio") and not line.startswith("m=video"):
782
+ continue
783
+ parts = line.split()
784
+ if len(parts) < 4:
785
+ continue
786
+ kind = "video" if line.startswith("m=video") else "audio"
787
+ for tok in parts[3:]:
788
+ try:
789
+ pt = int(tok)
790
+ except ValueError:
791
+ continue
792
+ if not 0 <= pt <= 127:
793
+ continue
794
+ if kinds.get(pt, kind) != kind:
795
+ dropped.add(pt)
796
+ kinds[pt] = kind
797
+ for pt in dropped:
798
+ kinds.pop(pt, None)
799
+ return kinds
800
+
801
+
802
+ def rewrite_rtp_payload_type(pkt: bytes, pt: int) -> bytes:
803
+ """Return ``pkt`` with its RTP payload type replaced, marker bit preserved.
804
+
805
+ Used only where the bridge itself decrypted the packet (``_use_plain_rtp``),
806
+ so what is being edited is plaintext on its way to our own loopback -- never
807
+ a packet ffmpeg will authenticate, where a changed byte would fail the SRTP
808
+ auth tag. RTP byte 1 is ``M(1) | PT(7)`` (RFC 3550), so only the low seven
809
+ bits move.
810
+
811
+ A packet too short to hold that byte is returned untouched rather than
812
+ rejected: this is the media path, and a malformed packet is ffmpeg's to
813
+ discard, not the bridge's to raise on.
814
+ """
815
+ if len(pkt) < 2:
816
+ return pkt
817
+ return pkt[:1] + bytes([(pkt[1] & 0x80) | (pt & 0x7F)]) + pkt[2:]
818
+
819
+
749
820
  #: Video payload types the SDES answer template advertises (H264, H265).
750
821
  _SDES_ANSWER_VIDEO_PTS = (96, 97)
751
822
 
@@ -4038,6 +4109,10 @@ class _SdesOpenMixin:
4038
4109
  # list for the same reason as _first_video_pt: the serve-restart path
4039
4110
  # reads it from a nested scope.
4040
4111
  _answer_video_pt: list = [None]
4112
+ # PT -> "video"/"audio" from the camera's answer, read by the
4113
+ # bridge demux. Empty until the answer lands (the bridge starts
4114
+ # first), which is exactly when the static-tuple fallback applies.
4115
+ _answer_pt_kinds: dict = {}
4041
4116
  # Same for audio: the ffmpeg SDP advertises BOTH PCMU (0) and PCMA (8),
4042
4117
  # and ffmpeg binds the depacketizer to the first one listed - so the line
4043
4118
  # has to be narrowed to the payload type the camera actually sends, the
@@ -5598,7 +5673,55 @@ class _SdesOpenMixin:
5598
5673
  _LOGGER.debug("camera %s: swallowed exception in %s", getattr(self, "device_id", "?"), '_bridge_fn', exc_info=True)
5599
5674
  continue
5600
5675
  _pt = _pt_byte & 0x7F
5601
- if _pt in (96, 97, 98):
5676
+ # The ANSWER decides which payload type is which
5677
+ # kind; the tuples below are only the fallback for a
5678
+ # packet that beats the answer in. See
5679
+ # answer_pt_kinds: this camera negotiates H265 on
5680
+ # pt=0 in ~14% of sessions and the fixed tuples sent
5681
+ # every one of those video streams to the AUDIO
5682
+ # loopback.
5683
+ _kind_answered = _answer_pt_kinds.get(_pt)
5684
+ _pt_out = _pt
5685
+ if _kind_answered == "video":
5686
+ _btgt, _lo_target, _kind = (
5687
+ _lo_video_port, _lo_v, "video"
5688
+ )
5689
+ # The bridge starts before the answer lands, so
5690
+ # a video packet on an unexpected PT can beat the
5691
+ # map in by a few hundred ms and latch as the
5692
+ # session's "first audio". Left alone that
5693
+ # narrows the serve's audio line to a payload
5694
+ # type no audio uses, and the mpegts mux then
5695
+ # withholds PAT/PMT waiting for a stream that
5696
+ # never produces - costing the picture, not just
5697
+ # the sound. Undo it and let real audio latch.
5698
+ if _first_audio_pt[0] == _pt:
5699
+ _first_audio_pt[0] = None
5700
+ _br_first_audio_logged = False
5701
+ _status(
5702
+ "bridge: pt=%d re-classified as video"
5703
+ " from the answer - clearing the audio"
5704
+ " latch it took first" % _pt)
5705
+ # Our serve SDP describes video as 96=H264 /
5706
+ # 97=H265 only, so a camera-numbered payload
5707
+ # type we do not advertise must be translated
5708
+ # into our numbering before ffmpeg sees it -
5709
+ # the same translation video_pt_from_answer_sdp
5710
+ # already performs for the SDP side ("the
5711
+ # camera's own numbering need not agree").
5712
+ # Gated on _use_plain_rtp because only there is
5713
+ # the bridge the decryptor and the packet
5714
+ # reaching ffmpeg plaintext; rewriting a byte
5715
+ # ffmpeg will itself authenticate breaks SRTP.
5716
+ if (_pt not in _SDP_VIDEO_PTS
5717
+ and _use_plain_rtp
5718
+ and _answer_video_pt[0] is not None):
5719
+ _pt_out = int(_answer_video_pt[0])
5720
+ elif _kind_answered == "audio":
5721
+ _btgt, _lo_target, _kind = (
5722
+ _lo_audio_port, _lo_a, "audio"
5723
+ )
5724
+ elif _pt in (96, 97, 98):
5602
5725
  _btgt, _lo_target, _kind = (
5603
5726
  _lo_video_port, _lo_v, "video"
5604
5727
  )
@@ -5658,15 +5781,22 @@ class _SdesOpenMixin:
5658
5781
  )
5659
5782
  elif _kind == "video" and not _br_first_video_logged:
5660
5783
  _br_first_video_logged = True
5661
- _first_video_pt[0] = _pt
5662
- _status(f"bridge: first video RTP pt={_pt}")
5784
+ # _pt_out, not _pt: everything downstream (the
5785
+ # serve SDP narrowing, _serve_video_pt's
5786
+ # "observed beats pinned") speaks OUR numbering,
5787
+ # and the wire PT is preserved in the log line.
5788
+ _first_video_pt[0] = _pt_out
5789
+ _status(
5790
+ f"bridge: first video RTP pt={_pt_out}"
5791
+ + (f" (camera numbered it {_pt})"
5792
+ if _pt_out != _pt else ""))
5663
5793
  # At INFO, not DEBUG: this is the one line that
5664
5794
  # makes a bitrate figure comparable to another
5665
5795
  # one, and it is emitted once per session.
5666
5796
  _LOGGER.info(
5667
5797
  "camera %s: video profile %s",
5668
5798
  getattr(self, "device_id", "?"),
5669
- describe_video_profile(_pt),
5799
+ describe_video_profile(_pt_out),
5670
5800
  )
5671
5801
  # Camera answers BUNDLE (all media on one 5-tuple),
5672
5802
  # so the talk destination is the same address as
@@ -5891,6 +6021,9 @@ class _SdesOpenMixin:
5891
6021
  if _nk_late:
5892
6022
  _bridge_fn._nack_late_drops = getattr(
5893
6023
  _bridge_fn, '_nack_late_drops', 0) + 1
6024
+ if _pt_out != _pt and _decrypted:
6025
+ _fwd_pkt = rewrite_rtp_payload_type(
6026
+ _fwd_pkt, _pt_out)
5894
6027
  try:
5895
6028
  if not _nk_late:
5896
6029
  _lo_target.sendto(
@@ -6177,6 +6310,7 @@ class _SdesOpenMixin:
6177
6310
  _early_nominated = bool(_cam_ice_ufrag and _cam_ice_pwd and _cam_ice_cands)
6178
6311
  _media_deadline = time.monotonic() + _FIRST_MEDIA_WAIT_S
6179
6312
  _media_wait_started = time.monotonic()
6313
+ _pt_kinds_read = False
6180
6314
 
6181
6315
  def _report_first_media_stall(_waited_s, _cancelled=False):
6182
6316
  """Emit the one line that says why nothing arrived.
@@ -6242,6 +6376,34 @@ class _SdesOpenMixin:
6242
6376
  _status(f"camera refused: ack {_code} {_desc}"
6243
6377
  " - terminal, abandoning the first-media wait")
6244
6378
  raise AidotCameraBusy(_code, _desc)
6379
+ if (not _pt_kinds_read and answer_fut is not None
6380
+ and answer_fut.done() and not answer_fut.cancelled()):
6381
+ # One-shot, here rather than in the answer-logging block
6382
+ # further down: that block runs AFTER this wait, which is
6383
+ # 75 s too late for the demux that the map exists to fix.
6384
+ _pt_kinds_read = True
6385
+ try:
6386
+ if answer_fut.exception() is None:
6387
+ _pk_sdp = (
6388
+ answer_fut.result() or {}).get("sdp", "") or ""
6389
+ if _pk_sdp:
6390
+ _answer_pt_kinds.update(answer_pt_kinds(_pk_sdp))
6391
+ _answer_video_pt[0] = video_pt_from_answer_sdp(
6392
+ _pk_sdp)
6393
+ _vpts = sorted(
6394
+ pt for pt, k in _answer_pt_kinds.items()
6395
+ if k == "video")
6396
+ if any(pt not in _SDP_VIDEO_PTS for pt in _vpts):
6397
+ _status(
6398
+ "answer numbers video on payload"
6399
+ " type(s) %s - demuxing by the answer,"
6400
+ " serving as our pt=%s"
6401
+ % (_vpts, _answer_video_pt[0]))
6402
+ except Exception:
6403
+ _LOGGER.debug(
6404
+ "camera %s: swallowed exception in %s",
6405
+ getattr(self, "device_id", "?"),
6406
+ 'answer_pt_kinds', exc_info=True)
6245
6407
  if (not _early_nominated and answer_fut is not None
6246
6408
  and answer_fut.done() and not answer_fut.cancelled()):
6247
6409
  # Read-only peek: the real await below still consumes this
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "1.0.0b32"
7
+ version = "1.0.0b33"
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"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-aidot-cameras
3
- Version: 1.0.0b32
3
+ Version: 1.0.0b33
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
@@ -77,6 +77,7 @@ python_aidot_cameras.egg-info/requires.txt
77
77
  python_aidot_cameras.egg-info/top_level.txt
78
78
  tests/test_aioice_compat.py
79
79
  tests/test_alarm_event.py
80
+ tests/test_answer_pt_kinds.py
80
81
  tests/test_answer_sdp_video_pt.py
81
82
  tests/test_answer_section_selection.py
82
83
  tests/test_audio_mux_offline.py
@@ -0,0 +1,141 @@
1
+ """Demuxing the camera's media by the ANSWER's payload numbering, not a table.
2
+
3
+ The bridge splits a BUNDLEd stream into ffmpeg's audio and video loopbacks by
4
+ RTP payload type. It used to do that from a fixed table -- 96/97/98 video,
5
+ 0/8 audio -- and that table is wrong for this fleet.
6
+
7
+ Measured 2026-08-26 across 107 A001064 opens on the live camera: 15 of them
8
+ (14%) negotiated H265 on payload type 0, announced it correctly in the answer
9
+ as ``m=video ... 0`` with ``a=rtpmap:0 H265/90000``, and then sent 2668
10
+ full-size (1222 B) video packets on pt=0 while audio ran normally on pt=8
11
+ (verified on the wire, tshark by SSRC: two streams, 0x29d9/pt=0 video and
12
+ 0x29d7/pt=8 audio). The fixed table posted every one of those video packets to
13
+ the AUDIO loopback, so no video was ever observed, the 75 s first-media wait
14
+ expired, and the serve launched into an empty stream and exited -- 82 s, then a
15
+ full reopen. The other 92 opens answered ``m=video ... 96`` and worked. Perfect
16
+ separation in both directions.
17
+
18
+ The same renumbering is already on record for the A000088 in
19
+ ``test_answer_section_selection.py`` (PT 0 carrying H265), where the DTLS path
20
+ handles it by selecting sections by content. The SDES bridge was the one place
21
+ left trusting a static tuple.
22
+
23
+ The m-lines below are transcribed from the live log; the ``a=rtpmap:0
24
+ H265/90000`` line is inferred from ``video_pt_from_answer_sdp`` returning 97 on
25
+ every one of those 15 sessions (logged as "the camera's answer (97)"), which is
26
+ reachable only through the H265 branch of ``_SDP_VIDEO_PT_BY_CODEC``.
27
+ """
28
+
29
+ import os
30
+ import sys
31
+
32
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
33
+
34
+ from aidot_cameras.camera.sdes_open import (
35
+ answer_pt_kinds,
36
+ rewrite_rtp_payload_type,
37
+ video_pt_from_answer_sdp,
38
+ )
39
+
40
+ # The 92-of-107 shape: ordinary 3-section answer, video on 96.
41
+ _HEALTHY = (
42
+ "v=0\r\n"
43
+ "m=audio 9 RTP/SAVPF 8\r\n"
44
+ "a=rtpmap:8 PCMA/8000\r\n"
45
+ "m=video 9 RTP/SAVPF 96\r\n"
46
+ "a=rtpmap:96 H264/90000\r\n"
47
+ "m=application 9 SCTP webrtc-datachannel\r\n"
48
+ )
49
+
50
+ # The 15-of-107 shape: a leading video section numbering H265 as payload type 0.
51
+ _PT0_VIDEO = (
52
+ "v=0\r\n"
53
+ "m=video 9 RTP/SAVPF 0\r\n"
54
+ "a=rtpmap:0 H265/90000\r\n"
55
+ "m=audio 9 RTP/SAVPF 8\r\n"
56
+ "a=rtpmap:8 PCMA/8000\r\n"
57
+ "m=video 9 RTP/SAVPF 96\r\n"
58
+ "a=rtpmap:96 H264/90000\r\n"
59
+ "m=application 9 SCTP webrtc-datachannel\r\n"
60
+ )
61
+
62
+ # A000088 H265-capable firmware: legitimately four sections, reordered. A
63
+ # predicate keyed on the section COUNT would break this model, which works.
64
+ _A000088_REORDERED = (
65
+ "v=0\r\n"
66
+ "m=video 9 UDP/TLS/RTP/SAVPF 0\r\n"
67
+ "a=rtpmap:0 H265/90000\r\n"
68
+ "m=audio 9 UDP/TLS/RTP/SAVPF 8\r\n"
69
+ "a=rtpmap:8 PCMA/8000\r\n"
70
+ "m=video 9 UDP/TLS/RTP/SAVPF 101 102\r\n"
71
+ "a=rtpmap:101 H264/90000\r\n"
72
+ "m=application 9 DTLS/SCTP 5000\r\n"
73
+ )
74
+
75
+
76
+ def test_healthy_answer_maps_kinds():
77
+ assert answer_pt_kinds(_HEALTHY) == {8: "audio", 96: "video"}
78
+
79
+
80
+ def test_pt0_video_is_reported_as_video_not_audio():
81
+ # The whole defect in one assertion: without the answer, pt=0 reads as PCMU.
82
+ kinds = answer_pt_kinds(_PT0_VIDEO)
83
+ assert kinds[0] == "video"
84
+ assert kinds[8] == "audio"
85
+ assert kinds[96] == "video"
86
+
87
+
88
+ def test_pt0_video_answer_resolves_to_our_h265_payload_type():
89
+ # What the bridge translates the wire PT into before ffmpeg sees it.
90
+ assert video_pt_from_answer_sdp(_PT0_VIDEO) == 97
91
+
92
+
93
+ def test_a000088_reordered_four_sections_still_map_correctly():
94
+ kinds = answer_pt_kinds(_A000088_REORDERED)
95
+ assert kinds[0] == "video"
96
+ assert kinds[8] == "audio"
97
+ assert kinds[101] == "video"
98
+ assert kinds[102] == "video"
99
+
100
+
101
+ def test_payload_type_claimed_by_both_kinds_is_dropped():
102
+ # Ambiguity is not better evidence than the fallback table, so the map
103
+ # declines to answer for that payload type rather than guessing.
104
+ ambiguous = (
105
+ "m=video 9 RTP/SAVPF 0\r\n"
106
+ "m=audio 9 RTP/SAVPF 0 8\r\n"
107
+ )
108
+ kinds = answer_pt_kinds(ambiguous)
109
+ assert 0 not in kinds
110
+ assert kinds[8] == "audio"
111
+
112
+
113
+ def test_malformed_and_empty_answers_do_not_raise():
114
+ assert answer_pt_kinds("") == {}
115
+ # Out of contract, but the media path must not raise on it either.
116
+ assert answer_pt_kinds(None) == {} # type: ignore[arg-type]
117
+ assert answer_pt_kinds("m=video\r\nm=audio 9\r\n") == {}
118
+ # Non-numeric and out-of-range payload tokens are skipped, not raised on.
119
+ assert answer_pt_kinds("m=video 9 RTP/SAVPF x 300 96\r\n") == {96: "video"}
120
+
121
+
122
+ def test_rewrite_payload_type_preserves_the_marker_bit():
123
+ # byte1 = M(1) | PT(7). 0x80 is marker set + pt 0; 0x00 is marker clear.
124
+ marked = bytes([0x80, 0x80]) + b"rest"
125
+ assert rewrite_rtp_payload_type(marked, 97) == bytes([0x80, 0xE1]) + b"rest"
126
+ unmarked = bytes([0x80, 0x00]) + b"rest"
127
+ assert rewrite_rtp_payload_type(unmarked, 97) == bytes([0x80, 0x61]) + b"rest"
128
+
129
+
130
+ def test_rewrite_payload_type_leaves_the_rest_of_the_packet_alone():
131
+ pkt = bytes([0x80, 0x00]) + bytes(range(2, 40))
132
+ out = rewrite_rtp_payload_type(pkt, 96)
133
+ assert out[0] == pkt[0]
134
+ assert out[2:] == pkt[2:]
135
+ assert out[1] & 0x7F == 96
136
+
137
+
138
+ def test_rewrite_payload_type_ignores_a_runt_packet():
139
+ # Media path: a malformed packet is ffmpeg's to discard, not ours to raise.
140
+ assert rewrite_rtp_payload_type(b"", 97) == b""
141
+ assert rewrite_rtp_payload_type(b"\x80", 97) == b"\x80"