python-aidot-cameras 1.0.0rc2__tar.gz → 1.0.0rc3__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 (278) hide show
  1. {python_aidot_cameras-1.0.0rc2/python_aidot_cameras.egg-info → python_aidot_cameras-1.0.0rc3}/PKG-INFO +2 -1
  2. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/README.md +1 -0
  3. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/protocol.py +71 -1
  4. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/pyproject.toml +1 -1
  5. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3/python_aidot_cameras.egg-info}/PKG-INFO +2 -1
  6. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/python_aidot_cameras.egg-info/SOURCES.txt +1 -0
  7. python_aidot_cameras-1.0.0rc3/tests/test_direct_ts_keyframe_signal.py +140 -0
  8. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_direct_ts_serve.py +11 -0
  9. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/LICENSE +0 -0
  10. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/MANIFEST.in +0 -0
  11. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/__init__.py +0 -0
  12. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/__main__.py +0 -0
  13. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_upstream.py +0 -0
  14. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/__init__.py +0 -0
  15. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/LICENSE +0 -0
  16. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/__init__.py +0 -0
  17. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/clock.py +0 -0
  18. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/__init__.py +0 -0
  19. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/base.py +0 -0
  20. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/g711.py +0 -0
  21. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/g722.py +0 -0
  22. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/h264.py +0 -0
  23. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/opus.py +0 -0
  24. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/codecs/vpx.py +0 -0
  25. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/contrib/__init__.py +0 -0
  26. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/contrib/media.py +0 -0
  27. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/contrib/signaling.py +0 -0
  28. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/events.py +0 -0
  29. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/exceptions.py +0 -0
  30. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/jitterbuffer.py +0 -0
  31. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/mediastreams.py +0 -0
  32. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rate.py +0 -0
  33. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcconfiguration.py +0 -0
  34. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcdatachannel.py +0 -0
  35. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcdtlstransport.py +0 -0
  36. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcicetransport.py +0 -0
  37. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcpeerconnection.py +0 -0
  38. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcrtpparameters.py +0 -0
  39. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcrtpreceiver.py +0 -0
  40. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcrtpsender.py +0 -0
  41. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcrtptransceiver.py +0 -0
  42. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcsctptransport.py +0 -0
  43. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtcsessiondescription.py +0 -0
  44. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/rtp.py +0 -0
  45. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/sdp.py +0 -0
  46. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/stats.py +0 -0
  47. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/_vendor/aiortc/utils.py +0 -0
  48. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/ble_gateway.py +0 -0
  49. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/__init__.py +0 -0
  50. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/client.py +0 -0
  51. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/constants.py +0 -0
  52. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/controls.py +0 -0
  53. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/go2rtc.py +0 -0
  54. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/hwaccel.py +0 -0
  55. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/lan_control.py +0 -0
  56. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/models.py +0 -0
  57. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/playback.py +0 -0
  58. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sd_events.py +0 -0
  59. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sd_listing.py +0 -0
  60. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sd_playback.py +0 -0
  61. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sdes.py +0 -0
  62. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/sdes_open.py +0 -0
  63. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/tutk.py +0 -0
  64. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/webrtc.py +0 -0
  65. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/camera/webrtc_open.py +0 -0
  66. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/client.py +0 -0
  67. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/cloud_auth.py +0 -0
  68. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/const.py +0 -0
  69. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/credentials.py +0 -0
  70. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/crypto.py +0 -0
  71. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/device_client.py +0 -0
  72. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/discover.py +0 -0
  73. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/exceptions.py +0 -0
  74. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/g711.py +0 -0
  75. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/lan_retry.py +0 -0
  76. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/aidot_cameras/py.typed +0 -0
  77. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/python_aidot_cameras.egg-info/dependency_links.txt +0 -0
  78. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/python_aidot_cameras.egg-info/entry_points.txt +0 -0
  79. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/python_aidot_cameras.egg-info/requires.txt +0 -0
  80. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/python_aidot_cameras.egg-info/top_level.txt +0 -0
  81. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/setup.cfg +0 -0
  82. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_aioice_compat.py +0 -0
  83. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_alarm_event.py +0 -0
  84. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_answer_pt_kinds.py +0 -0
  85. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_answer_sdp_video_pt.py +0 -0
  86. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_answer_section_selection.py +0 -0
  87. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_audio_mux_offline.py +0 -0
  88. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_avio_ctrl_uses_the_session_dseq.py +0 -0
  89. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_avio_request.py +0 -0
  90. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_avio_response_parse.py +0 -0
  91. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_avio_response_router.py +0 -0
  92. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_backoff.py +0 -0
  93. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_battery_detection.py +0 -0
  94. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_battery_evidence_corroboration.py +0 -0
  95. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_battery_skip_turn.py +0 -0
  96. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_bitrate_campaign_arms.py +0 -0
  97. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_ble_gateway.py +0 -0
  98. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_bridge_state_sentinels.py +0 -0
  99. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_busy_backoff.py +0 -0
  100. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_canary_log_names_the_camera.py +0 -0
  101. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_cloud_count.py +0 -0
  102. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_cloud_plan.py +0 -0
  103. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_constants_imports_resolve.py +0 -0
  104. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_control_verdicts.py +0 -0
  105. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_deferred_hardening.py +0 -0
  106. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_device_cmd_seq_match.py +0 -0
  107. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_device_login_guard.py +0 -0
  108. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_device_user_info_cache.py +0 -0
  109. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dormant_camera_cleanup.py +0 -0
  110. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_drain_is_cancellable.py +0 -0
  111. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_mux_terminal_write.py +0 -0
  112. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_not_ready_burst.py +0 -0
  113. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_pinning.py +0 -0
  114. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_resolution_ack.py +0 -0
  115. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_serve_decode_skip.py +0 -0
  116. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_serve_ice_wait.py +0 -0
  117. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_serve_open_timeout.py +0 -0
  118. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_skip_signaling_wait.py +0 -0
  119. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_slow_probe.py +0 -0
  120. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_stdout_producer.py +0 -0
  121. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_dtls_video_presence.py +0 -0
  122. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_egress_guard.py +0 -0
  123. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_env_seams.py +0 -0
  124. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_feature_probe_needs_a_live_session.py +0 -0
  125. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_ffmpeg_signal_exit_is_not_a_crash.py +0 -0
  126. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_futile_keepalive.py +0 -0
  127. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_gate_requires_a_decoded_frame.py +0 -0
  128. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_go2rtc.py +0 -0
  129. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_go2rtc_cli.py +0 -0
  130. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_h264_decode_rate_limit.py +0 -0
  131. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_highport_nomination.py +0 -0
  132. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_hwaccel_probe.py +0 -0
  133. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_ice_config_cache.py +0 -0
  134. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_ice_peer_reflexive.py +0 -0
  135. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_idle_release_both_loops.py +0 -0
  136. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_keepalive_renew.py +0 -0
  137. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_key_restart_sdp.py +0 -0
  138. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_keyframe_prompter.py +0 -0
  139. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_lan_control.py +0 -0
  140. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_lan_retry_reaches_the_storming_devices.py +0 -0
  141. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_late_repeats_are_dropped.py +0 -0
  142. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_light_active_color_mode.py +0 -0
  143. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_live_session_carries_talk.py +0 -0
  144. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_live_stream_param.py +0 -0
  145. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_live_validate_policy.py +0 -0
  146. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_live_video_canary.py +0 -0
  147. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_liveplay_echo_timeout.py +0 -0
  148. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_local_only_controls.py +0 -0
  149. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_logging_caps.py +0 -0
  150. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_login_connect_deadline.py +0 -0
  151. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_login_reconnect_backoff.py +0 -0
  152. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_media_counted_only_when_plaintext.py +0 -0
  153. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_media_path_is_reported.py +0 -0
  154. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_media_socket_rcvbuf.py +0 -0
  155. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_media_wait_floor.py +0 -0
  156. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_motion_poll.py +0 -0
  157. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_mqtt_client_id_is_exact.py +0 -0
  158. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_mqtt_credential_refetch.py +0 -0
  159. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_mqtt_credential_self_heal.py +0 -0
  160. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_mqtt_password_not_persisted.py +0 -0
  161. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_mqtt_session_reconnect.py +0 -0
  162. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_mqtt_thread_not_orphaned.py +0 -0
  163. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_nack.py +0 -0
  164. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_nack_cannot_add_load_in_a_burst.py +0 -0
  165. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_narrow_pc_ice.py +0 -0
  166. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_no_key_material_in_logs.py +0 -0
  167. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_no_self_referential_go2rtc.py +0 -0
  168. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_no_undefined_names.py +0 -0
  169. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_offer_bandwidth_line.py +0 -0
  170. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_offer_keeps_rtcp_feedback.py +0 -0
  171. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_offline_keepalive_pause.py +0 -0
  172. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_on_frame_is_an_av_frame.py +0 -0
  173. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_open_fail_logger.py +0 -0
  174. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_open_gate_delay.py +0 -0
  175. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_peer_id_client_type.py +0 -0
  176. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_peerid_reuse.py +0 -0
  177. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_persistent_mqtt.py +0 -0
  178. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_persistent_mqtt_robustness.py +0 -0
  179. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_playback_tls.py +0 -0
  180. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_post_merge_hardening.py +0 -0
  181. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_ptz_probe_reads_what_it_is_told.py +0 -0
  182. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_public_signature_matches_impl.py +0 -0
  183. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_quality_campaign_in_session.py +0 -0
  184. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_recent_recordings.py +0 -0
  185. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_remb.py +0 -0
  186. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_remb_goes_out_the_relay_aware_sender.py +0 -0
  187. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_resent_frames_are_dropped.py +0 -0
  188. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_resolution_persists.py +0 -0
  189. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_restart_cycle.py +0 -0
  190. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_retry_policy.py +0 -0
  191. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_scripts_importable.py +0 -0
  192. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sctp_init_ack_tsn.py +0 -0
  193. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sctp_reassembly.py +0 -0
  194. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sctp_sack.py +0 -0
  195. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sd_card_presence.py +0 -0
  196. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sd_event_decode.py +0 -0
  197. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sd_event_probe_payloads.py +0 -0
  198. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sd_listing.py +0 -0
  199. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sd_playback.py +0 -0
  200. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_adaptive.py +0 -0
  201. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_avio_dispatch.py +0 -0
  202. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_bridge_break_gate.py +0 -0
  203. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_connection_mode.py +0 -0
  204. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_echo_wait_timeout.py +0 -0
  205. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_fast_liveplay.py +0 -0
  206. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_ffmpeg_stderr_log.py +0 -0
  207. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_first_media_stall_report.py +0 -0
  208. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_ice_teardown.py +0 -0
  209. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_idle_release.py +0 -0
  210. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_nack_bridge.py +0 -0
  211. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_offer_video_order.py +0 -0
  212. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_offer_video_pin.py +0 -0
  213. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_open_cleanup.py +0 -0
  214. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_sctp_abort.py +0 -0
  215. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_serve_audio.py +0 -0
  216. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_serve_cmd.py +0 -0
  217. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_serve_stderr_drain.py +0 -0
  218. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_session_cap.py +0 -0
  219. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_sprop.py +0 -0
  220. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_talk.py +0 -0
  221. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_teardown_exit_log.py +0 -0
  222. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_terminal_ack.py +0 -0
  223. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_terminal_ack_latency.py +0 -0
  224. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_tmmbr_bridge.py +0 -0
  225. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdes_watchdog.py +0 -0
  226. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdp_narrowing_properties.py +0 -0
  227. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sdp_payload_type_narrowing.py +0 -0
  228. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_security_hardening.py +0 -0
  229. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_self_peer_is_an_address_not_an_ip.py +0 -0
  230. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_serializable_login_info.py +0 -0
  231. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_serve_input_timeout.py +0 -0
  232. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_serve_loss_tolerance.py +0 -0
  233. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_serve_pt_respects_the_pin.py +0 -0
  234. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_serve_relay.py +0 -0
  235. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_serve_stderr_keeps_the_reason.py +0 -0
  236. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_session_stats.py +0 -0
  237. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_shared_house_seam.py +0 -0
  238. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_snapshot_cancellation.py +0 -0
  239. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_speak.py +0 -0
  240. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sprop_capture_gate.py +0 -0
  241. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sprop_staleness.py +0 -0
  242. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sprop_unstable_public.py +0 -0
  243. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_sprop_write_is_reported_truthfully.py +0 -0
  244. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_srtp_rx_rekey.py +0 -0
  245. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_srtp_tx_key_note.py +0 -0
  246. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stable_terminal_id.py +0 -0
  247. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stall_nudge_policy.py +0 -0
  248. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stall_report_says_what_the_answer_carried.py +0 -0
  249. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stall_report_says_whether_the_trigger_was_answered.py +0 -0
  250. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stall_report_separates_silence_from_undecryptable.py +0 -0
  251. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stall_report_survives_a_caller_giving_up.py +0 -0
  252. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stall_reports_reach_the_artifact.py +0 -0
  253. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stream_cap.py +0 -0
  254. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stream_drain_ownership.py +0 -0
  255. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stream_idle.py +0 -0
  256. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stream_slot_cap.py +0 -0
  257. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_stream_teardown.py +0 -0
  258. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_talk.py +0 -0
  259. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_talk_reports_a_speaker_that_never_opened.py +0 -0
  260. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_tap_to_mux_end_to_end.py +0 -0
  261. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_terminal_ack.py +0 -0
  262. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_tmmbr.py +0 -0
  263. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_token_file_robustness.py +0 -0
  264. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_token_refresh.py +0 -0
  265. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_turn_create_permission.py +0 -0
  266. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_tutk_path_is_out_of_reach.py +0 -0
  267. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_upstream_compat.py +0 -0
  268. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_validator_shares_one_token.py +0 -0
  269. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_video_profile_log.py +0 -0
  270. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_video_pts_dts.py +0 -0
  271. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_video_ts_unwrap.py +0 -0
  272. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_viewer_check_throttle.py +0 -0
  273. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_viewer_presence.py +0 -0
  274. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_viewer_present_push_mode.py +0 -0
  275. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_wait_or_event.py +0 -0
  276. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_wake_gate_camera_evidence.py +0 -0
  277. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/tests/test_wake_readiness_retry.py +0 -0
  278. {python_aidot_cameras-1.0.0rc2 → python_aidot_cameras-1.0.0rc3}/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.0rc2
3
+ Version: 1.0.0rc3
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
@@ -418,6 +418,7 @@ audio, idle release, the sprop cache path) are documented in
418
418
  | `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all - same lever as `AIDOT_SDES_SKIP_TURN_PREALLOC`, and battery cameras keep the relay regardless because a cloud-woken camera has no other path back), or `relay` (EXPERIMENTAL, and on the reference fleet it does not steer: even with `c=`/`m=` moved to the relay allocation and the WAN permission pre-installed, an A001064 and an A001513 both dialed our host address directly, learning it from our own ICE probes - forcing the relay for real would mean suppressing every direct-path outbound, which is unbuilt). Per-open `sdes_connection_mode` beats the env. Every session reports the path its media actually took as `media_stats().media_path` (`direct`/`relay`). | unset (`auto`) |
419
419
  | `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
420
420
  | `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
421
+ | `AIDOT_SDES_LIVEPLAY_ECHO_S` | How long to wait for the broker to echo our own `livePlayReq` back before sending `webrtcReq`, in seconds (`0` disables the wait). The wait was 5.0 s on the models excluded from fast-liveplay and is pure latency: across 22 h of one deployment it ran 169 times and timed out 169 times, never once ending early, with no inbound `livePlayReq` among 5000+ messages the cameras and broker did send. The code proceeds on timeout anyway, so it never changed behaviour, only delayed it. Measured on an A001064: time to first media 11534 ms -> 6819 ms. Still honoured, so a broker that does echo short-circuits it. | 0.25 s (1.5 s for fast-liveplay models) |
421
422
  | `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
422
423
  | `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
423
424
  | `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
@@ -378,6 +378,7 @@ audio, idle release, the sprop cache path) are documented in
378
378
  | `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all - same lever as `AIDOT_SDES_SKIP_TURN_PREALLOC`, and battery cameras keep the relay regardless because a cloud-woken camera has no other path back), or `relay` (EXPERIMENTAL, and on the reference fleet it does not steer: even with `c=`/`m=` moved to the relay allocation and the WAN permission pre-installed, an A001064 and an A001513 both dialed our host address directly, learning it from our own ICE probes - forcing the relay for real would mean suppressing every direct-path outbound, which is unbuilt). Per-open `sdes_connection_mode` beats the env. Every session reports the path its media actually took as `media_stats().media_path` (`direct`/`relay`). | unset (`auto`) |
379
379
  | `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
380
380
  | `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
381
+ | `AIDOT_SDES_LIVEPLAY_ECHO_S` | How long to wait for the broker to echo our own `livePlayReq` back before sending `webrtcReq`, in seconds (`0` disables the wait). The wait was 5.0 s on the models excluded from fast-liveplay and is pure latency: across 22 h of one deployment it ran 169 times and timed out 169 times, never once ending early, with no inbound `livePlayReq` among 5000+ messages the cameras and broker did send. The code proceeds on timeout anyway, so it never changed behaviour, only delayed it. Measured on an A001064: time to first media 11534 ms -> 6819 ms. Still honoured, so a broker that does echo short-circuits it. | 0.25 s (1.5 s for fast-liveplay models) |
381
382
  | `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
382
383
  | `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
383
384
  | `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
@@ -1058,6 +1058,11 @@ class _DirectTsServer:
1058
1058
  self._pmt: "Optional[bytes]" = None
1059
1059
  self._pmt_pid: "Optional[int]" = None
1060
1060
  self._synced = False # has the current consumer been given a start
1061
+ # Set by the mux immediately before it writes a video keyframe, so a new
1062
+ # consumer can be spliced where the picture is actually decodable.
1063
+ self._kf_pending = False
1064
+ self._kf_ever = False
1065
+ self._waiting_since: "Optional[float]" = None
1061
1066
 
1062
1067
  @property
1063
1068
  def port(self) -> int:
@@ -1141,6 +1146,51 @@ class _DirectTsServer:
1141
1146
  except (IndexError, ValueError):
1142
1147
  return
1143
1148
 
1149
+ #: How long a new consumer may wait for a keyframe signal before the old
1150
+ #: PID-blind random-access test is allowed to start it anyway. The signal is
1151
+ #: the correct trigger, but a stream that never sends one must not hang
1152
+ #: forever - gating solely on a video-pid flag served ZERO bytes when tried
1153
+ #: (2026-08-29), because the muxer does not set it.
1154
+ _LEGACY_SYNC_AFTER_S = 5.0
1155
+
1156
+ def mark_keyframe(self) -> None:
1157
+ """Tell the server the next bytes written begin a video keyframe.
1158
+
1159
+ The mux knows this and used to throw it away: it pulls ``(data, ts, kf)``
1160
+ off the queue and ``kf`` came from ``_h264_has_keyframe`` at the tap.
1161
+ Everything downstream then tried to rediscover it from the container,
1162
+ which does not carry it - PyAV's mpegts muxer does not set
1163
+ ``random_access_indicator`` on the video PID in this stream, so a sync
1164
+ gated on that flag never fires at all.
1165
+
1166
+ Splicing a consumer anywhere else hands it video mid-GOP referencing an
1167
+ SPS/PPS it never received, which decodes as "non-existing PPS 0
1168
+ referenced": a video track that never produces a frame.
1169
+ """
1170
+ if not self._kf_ever:
1171
+ _LOGGER.debug("direct-ts: first keyframe signal reached the server")
1172
+ self._kf_ever = True
1173
+ self._kf_pending = True
1174
+
1175
+ def _legacy_sync_ok(self, pkt: bytes) -> bool:
1176
+ """The pre-signal behaviour, allowed only after a grace period.
1177
+
1178
+ Kept because a source that never calls ``mark_keyframe`` must still be
1179
+ servable. Deliberately NOT the first choice: this test accepts a
1180
+ random-access point on ANY pid, and AAC audio sets that flag ~50x/s, so
1181
+ before the signal existed a late joiner was spliced onto audio and never
1182
+ received a decodable picture.
1183
+ """
1184
+ if self._kf_ever:
1185
+ return False # this source signals; wait for the real thing
1186
+ if not self._is_random_access(pkt):
1187
+ return False
1188
+ now = time.monotonic()
1189
+ if self._waiting_since is None:
1190
+ self._waiting_since = now
1191
+ return False
1192
+ return (now - self._waiting_since) >= self._LEGACY_SYNC_AFTER_S
1193
+
1144
1194
  def write(self, b: bytes) -> int:
1145
1195
  data = self._tail + b if self._tail else b
1146
1196
  out = bytearray()
@@ -1164,7 +1214,11 @@ class _DirectTsServer:
1164
1214
  self._pmt = pkt
1165
1215
  if self._synced:
1166
1216
  out += pkt
1167
- elif self._is_random_access(pkt):
1217
+ elif self._kf_pending or self._legacy_sync_ok(pkt):
1218
+ _LOGGER.debug(
1219
+ "direct-ts: spliced a consumer via=%s signals=%s pid=%s",
1220
+ "keyframe-signal" if self._kf_pending else "legacy-rai",
1221
+ self._kf_ever, pid)
1168
1222
  # Start this consumer here: tables first, then the keyframe.
1169
1223
  if self._pat is not None:
1170
1224
  out += self._pat
@@ -1172,6 +1226,8 @@ class _DirectTsServer:
1172
1226
  out += self._pmt
1173
1227
  out += pkt
1174
1228
  self._synced = True
1229
+ self._kf_pending = False
1230
+ self._waiting_since = None
1175
1231
  self._tail = data[i:] if i < n else b""
1176
1232
 
1177
1233
  with self._lock:
@@ -1568,6 +1624,16 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
1568
1624
  def close(self):
1569
1625
  self._f.close()
1570
1626
 
1627
+ def mark_keyframe(self):
1628
+ """Forward the keyframe signal when the sink understands it.
1629
+
1630
+ The sink is a _DirectTsServer on the direct path and an os pipe to
1631
+ ffmpeg otherwise; only the former can use this.
1632
+ """
1633
+ fn = getattr(self._f, "mark_keyframe", None)
1634
+ if fn is not None:
1635
+ fn()
1636
+
1571
1637
  wsink = _FailFlagWriter(out_fileobj)
1572
1638
  try:
1573
1639
  # max_interleave_delta (default 10s of stream time): with an AAC stream
@@ -1659,6 +1725,10 @@ def _dtls_av_mux_run(vq, aq, out_fileobj, progress, stop_flag) -> None:
1659
1725
  pkt.stream = vs
1660
1726
  pkt.pts, pkt.dts = video_pts_dts(_ts_state, _rel, _slack)
1661
1727
  pkt.time_base = Fraction(1, 90000)
1728
+ if kf:
1729
+ # Announce it BEFORE muxing: the bytes this produces are where a
1730
+ # new consumer can safely be spliced, and only we know that.
1731
+ wsink.mark_keyframe()
1662
1732
  try:
1663
1733
  out.mux(pkt)
1664
1734
  progress[0] = _t.monotonic()
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-aidot-cameras"
7
- version = "1.0.0rc2"
7
+ version = "1.0.0rc3"
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.0rc2
3
+ Version: 1.0.0rc3
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
@@ -418,6 +418,7 @@ audio, idle release, the sprop cache path) are documented in
418
418
  | `AIDOT_SDES_CONNECTION_MODE` | Which media path the SDES offer proposes: `auto` (default - every reachable candidate, and ICE priority prefers the LAN with the relay as last resort; measured on the full fleet, six of seven cameras stream direct and only the unit with no route to us rides the relay), `lan` (no relay at all - same lever as `AIDOT_SDES_SKIP_TURN_PREALLOC`, and battery cameras keep the relay regardless because a cloud-woken camera has no other path back), or `relay` (EXPERIMENTAL, and on the reference fleet it does not steer: even with `c=`/`m=` moved to the relay allocation and the WAN permission pre-installed, an A001064 and an A001513 both dialed our host address directly, learning it from our own ICE probes - forcing the relay for real would mean suppressing every direct-path outbound, which is unbuilt). Per-open `sdes_connection_mode` beats the env. Every session reports the path its media actually took as `media_stats().media_path` (`direct`/`relay`). | unset (`auto`) |
419
419
  | `AIDOT_SDES_ADAPTIVE` | Adaptive fast-with-fallback for the SDES keepalive loop: try the fast path first and fall back to the full relay path if a fast attempt delivers no media. A per-device cache skips the fast attempt on later views once it has failed. Truthy value enables. Ignored for battery cameras, where the fast path cannot win and its short grace truncates the cold start. | unset (off) |
420
420
  | `AIDOT_SDES_FAST_LIVEPLAY` | Don't block on the `livePlayResp` wait for eligible SDES cameras (~4.5 s faster cold start). Role-reversal models (A001064 PTZ) always excluded for correctness. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
421
+ | `AIDOT_SDES_LIVEPLAY_ECHO_S` | How long to wait for the broker to echo our own `livePlayReq` back before sending `webrtcReq`, in seconds (`0` disables the wait). The wait was 5.0 s on the models excluded from fast-liveplay and is pure latency: across 22 h of one deployment it ran 169 times and timed out 169 times, never once ending early, with no inbound `livePlayReq` among 5000+ messages the cameras and broker did send. The code proceeds on timeout anyway, so it never changed behaviour, only delayed it. Measured on an A001064: time to first media 11534 ms -> 6819 ms. Still honoured, so a broker that does echo short-circuits it. | 0.25 s (1.5 s for fast-liveplay models) |
421
422
  | `AIDOT_SDES_NACK` | Ask the camera to resend video RTP packets that never arrived (RTCP Generic NACK). A camera losing packets on a weak link otherwise delivers truncated H.264 slices, which a browser's WebRTC decoder conceals but Media Source Extensions treats as fatal. Measured on an A001064 at ~1-2% loss: 98.4% of losses recovered against none at all without it. Costs nothing on a clean link, where no requests are generated. **On by default**; set to `0`/`false`/`no`/`off` to disable. | enabled (on) |
422
423
  | `AIDOT_SDES_VIDEO_PT` | Pin the SDES offer to ONE video codec by payload type, so the camera cannot choose. The offer advertises 96 (H264) and 97 (H265) and the camera decides which to send; on an A001064 that means the same request comes back h264 1280x720 most sessions and hevc 2560x1440 occasionally, at a third of the bitrate. Set to `96` for H264 only (measured h264 720p in 4 of 4 sessions). **Do not set it to `97`** - an H265-only offer returned no video at all in 3 of 3 rounds; narrowing to H265 removes the option rather than selecting it. | unset (both offered) |
423
424
  | `AIDOT_SDES_VIDEO_PT_ORDER` | Reorder the SDES offer's video codec list without narrowing it, as a comma-separated preference list (`97,96`). RFC 3264 makes the `m=video` payload-type list a preference list, most-preferred first, and ours has always read `96 97` - not by decision, it has simply never been varied. Whatever is named leads and the rest is appended, so this can express a preference and can never produce a narrowed or empty video m-line. **Experimental and untested on hardware:** whether the camera acts on the order is the open question, so the default is deliberately unchanged. **Tested on hardware 2026-08-23 and it does not select the codec.** Eight sessions on an A001064 in blocks of two: H265 came back in 2 of 4 with the order reversed and 2 of 4 without it, receipts confirming the reordered list reached the SDP. The motive was real - an H265 session costs about half an H264 one (766-774 vs 1597-1685 Kbps) - but the camera picks the codec itself, roughly a coin flip, and RFC 3264 preference order does not move it. | unset (`96 97`) |
@@ -102,6 +102,7 @@ tests/test_deferred_hardening.py
102
102
  tests/test_device_cmd_seq_match.py
103
103
  tests/test_device_login_guard.py
104
104
  tests/test_device_user_info_cache.py
105
+ tests/test_direct_ts_keyframe_signal.py
105
106
  tests/test_direct_ts_serve.py
106
107
  tests/test_dormant_camera_cleanup.py
107
108
  tests/test_drain_is_cancellable.py
@@ -0,0 +1,140 @@
1
+ """A late joiner is spliced where the mux says a keyframe starts.
2
+
3
+ `_DirectTsServer` holds a new consumer until it can hand over a decodable start.
4
+ It used to decide that from the container: any TS packet with the adaptation
5
+ field's random_access_indicator set. That flag is per-PID, and AAC audio sets it
6
+ roughly 50x/s against a video keyframe every ~2 s, so a late joiner was spliced
7
+ onto audio and received video mid-GOP against an SPS/PPS it never got -
8
+ "non-existing PPS 0 referenced", a video track that never produces a frame.
9
+ Only a consumer present from byte 0 escaped, and go2rtc pulls lazily when a
10
+ viewer connects, so it is always late.
11
+
12
+ Gating on the flag being set on the VIDEO pid instead was tried on hardware
13
+ (2026-08-29) and served ZERO bytes: PyAV's mpegts muxer does not set it on video
14
+ in this stream, so the condition never became true. That attempt was reverted.
15
+
16
+ The mux already knows. It pulls ``(data, ts, kf)`` off the queue, where ``kf``
17
+ came from ``_h264_has_keyframe`` at the tap, and then threw it away. It now
18
+ announces it, and the legacy container test survives only as a bounded fallback
19
+ for a source that never signals - so a stream that does not use the signal can
20
+ still be served rather than hanging forever.
21
+ """
22
+ import os
23
+ import sys
24
+ import threading
25
+
26
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
27
+
28
+ from aidot_cameras.camera.protocol import _DirectTsServer
29
+
30
+ VIDEO_PID = 0x0100
31
+ AUDIO_PID = 0x0101
32
+
33
+
34
+ def _ts(pid: int, *, rai: bool = False) -> bytes:
35
+ """One 188-byte TS packet, optionally flagging a random-access point."""
36
+ hdr = bytearray(4)
37
+ hdr[0] = 0x47
38
+ hdr[1] = (pid >> 8) & 0x1F
39
+ hdr[2] = pid & 0xFF
40
+ hdr[3] = 0x30 if rai else 0x10 # adaptation+payload, or payload only
41
+ body = bytes([1, 0x40]) if rai else b"" # adaptation_field_length=1, RAI
42
+ pkt = bytes(hdr) + body + b"\xde\xad"
43
+ return pkt + b"\xff" * (188 - len(pkt))
44
+
45
+
46
+ def _server():
47
+ srv = _DirectTsServer.__new__(_DirectTsServer)
48
+ srv._tail = b""
49
+ srv._pat = b"P" * 188
50
+ srv._pmt = b"M" * 188
51
+ srv._pmt_pid = 0x1000
52
+ srv._synced = False
53
+ srv._kf_pending = False
54
+ srv._kf_ever = False
55
+ srv._waiting_since = None
56
+ srv._lock = threading.Lock()
57
+ sent = bytearray()
58
+
59
+ class _Sock:
60
+ def sendall(self, b):
61
+ sent.extend(b)
62
+
63
+ srv._client = _Sock()
64
+ return srv, sent
65
+
66
+
67
+ def test_audio_random_access_does_not_splice_a_signalling_source():
68
+ """The defect: audio RAI used to start the consumer mid-video-GOP."""
69
+ srv, sent = _server()
70
+ srv.mark_keyframe() # source signals, so it is trusted
71
+ srv._kf_pending = False # ...but no keyframe pending yet
72
+ srv.write(_ts(AUDIO_PID, rai=True) * 20)
73
+ assert not srv._synced
74
+ assert bytes(sent) == b""
75
+
76
+
77
+ def test_keyframe_signal_splices_with_tables_first():
78
+ srv, sent = _server()
79
+ srv.write(_ts(AUDIO_PID, rai=True) * 3)
80
+ assert not srv._synced
81
+ srv.mark_keyframe()
82
+ srv.write(_ts(VIDEO_PID))
83
+ assert srv._synced
84
+ out = bytes(sent)
85
+ assert out[:188] == b"P" * 188, "PAT first"
86
+ assert out[188:376] == b"M" * 188, "then PMT"
87
+ assert len(out) == 3 * 188, "then the keyframe's packet"
88
+
89
+
90
+ def test_pending_flag_is_consumed_so_a_later_consumer_waits_again():
91
+ srv, _ = _server()
92
+ srv.mark_keyframe()
93
+ srv.write(_ts(VIDEO_PID))
94
+ assert srv._synced and not srv._kf_pending
95
+
96
+
97
+ def test_everything_flows_once_synced():
98
+ srv, sent = _server()
99
+ srv.mark_keyframe()
100
+ srv.write(_ts(VIDEO_PID))
101
+ before = len(bytes(sent))
102
+ srv.write(_ts(AUDIO_PID) + _ts(VIDEO_PID))
103
+ assert len(bytes(sent)) == before + 2 * 188
104
+
105
+
106
+ def test_non_signalling_source_still_starts_after_the_grace_period():
107
+ """A writer that never calls mark_keyframe must not hang forever.
108
+
109
+ This is the regression that gating solely on a video-pid flag caused:
110
+ zero bytes, which is worse than the bug it was fixing.
111
+ """
112
+ srv, sent = _server()
113
+ srv.write(_ts(AUDIO_PID, rai=True))
114
+ assert not srv._synced, "must wait out the grace period first"
115
+ srv._waiting_since -= _DirectTsServer._LEGACY_SYNC_AFTER_S + 1
116
+ srv.write(_ts(AUDIO_PID, rai=True))
117
+ assert srv._synced, "a non-signalling source must still be servable"
118
+ assert bytes(sent), "and must actually receive bytes"
119
+
120
+
121
+ def test_a_signalling_source_never_falls_back():
122
+ """Once a source has signalled, the container test must never win."""
123
+ srv, _ = _server()
124
+ srv.mark_keyframe()
125
+ srv._kf_pending = False
126
+ srv._waiting_since = 0.0 # grace long expired
127
+ assert srv._legacy_sync_ok(_ts(AUDIO_PID, rai=True)) is False
128
+
129
+
130
+ def test_mux_forwards_the_signal_only_to_sinks_that_understand_it():
131
+ """The fail-flag wrapper must not raise on an os pipe, which has no hook."""
132
+ import inspect
133
+
134
+ from aidot_cameras.camera import protocol
135
+
136
+ src = inspect.getsource(protocol._dtls_av_mux_run)
137
+ assert "wsink.mark_keyframe()" in src, "the mux must announce keyframes"
138
+ assert 'getattr(self._f, "mark_keyframe", None)' in src, (
139
+ "forwarding must be optional - the ffmpeg path's sink is a pipe"
140
+ )
@@ -39,6 +39,13 @@ this server or in go2rtc: a `proc.returncode` dereference when there is no
39
39
  ffmpeg process, binding an ephemeral port instead of the advertised one when no
40
40
  relay holds it, and the missing join-awareness below. The server itself
41
41
  describes cleanly against real go2rtc (`rc: 0, h264,video`).
42
+
43
+ These drive the server directly, so they must announce keyframes the way the
44
+ mux does (`mark_keyframe`). The container's random_access_indicator is no longer
45
+ the primary trigger: it is per-PID, AAC audio sets it ~50x/s, and syncing on it
46
+ spliced a late joiner onto audio with video mid-GOP. It survives only as a
47
+ bounded fallback for a writer that never signals - covered in
48
+ test_direct_ts_keyframe_signal.py.
42
49
  """
43
50
  import os
44
51
  import socket
@@ -133,6 +140,7 @@ def test_a_consumer_gets_an_http_response_then_the_media():
133
140
  if srv.has_consumer():
134
141
  break
135
142
  time.sleep(0.05)
143
+ srv.mark_keyframe() # the mux announces it
136
144
  srv.write(_ts(0x100, pusi=1, rai=1)) # keyframe: releases the join
137
145
  for _ in range(10):
138
146
  srv.write(_ts(0x100, pusi=1))
@@ -255,6 +263,7 @@ def test_a_late_consumer_gets_tables_then_a_keyframe():
255
263
  sock, head = _get(srv.port, read_bytes=1)
256
264
  assert _wait_consumer(srv)
257
265
  body = head.partition(b"\r\n\r\n")[2]
266
+ srv.mark_keyframe() # the mux announces it
258
267
  srv.write(_ts(0x100, pusi=1, rai=1)) # the next keyframe
259
268
  srv.write(_ts(0x100, pusi=1))
260
269
  body += _drain(sock, n=4)
@@ -276,6 +285,7 @@ def test_once_synced_it_keeps_forwarding():
276
285
  srv.write(_pat()); srv.write(_ts(0x1000, pusi=1))
277
286
  sock, head = _get(srv.port, read_bytes=1)
278
287
  assert _wait_consumer(srv)
288
+ srv.mark_keyframe()
279
289
  srv.write(_ts(0x100, pusi=1, rai=1))
280
290
  for _ in range(6):
281
291
  srv.write(_ts(0x100, pusi=1))
@@ -295,6 +305,7 @@ def test_a_reconnecting_consumer_resyncs():
295
305
  srv.write(_pat()); srv.write(_ts(0x1000, pusi=1))
296
306
  s1, _ = _get(srv.port, read_bytes=1)
297
307
  assert _wait_consumer(srv)
308
+ srv.mark_keyframe()
298
309
  srv.write(_ts(0x100, pusi=1, rai=1))
299
310
  s1.close()
300
311
  time.sleep(0.3)