From 8c745d18e0cc62fde12c07830bfc2b62bda8c440 Mon Sep 17 00:00:00 2001 From: Breadway Date: Mon, 3 Aug 2026 09:07:21 +0800 Subject: [PATCH] Implement Cast Streaming mirroring, DLNA casting, daemon+GUI, and breadd integration Builds out the full v1 scope: a vendored+patched openscreen subset for low-latency Cast Streaming (Mirroring receiver 0F5096E8) alongside the existing Cast V2/HLS and new DLNA/AVTransport casting paths, breadcastd's Idle/Casting state machine with a private IPC socket, the breadcast GTK4 popup as a thin IPC client, and bread.cast.*/bread.command.cast.* breadd integration (device discovery, start/stop, mirroring lifecycle events). Also adds bakery/systemd/Forgejo CI packaging. Validated end-to-end against a real Chromecast/Google TV: negotiated Cast Streaming session, live pipeline playback, and daemon+GUI click-to-cast/ stop through the actual popup. --- .forgejo/workflows/beta-release.yml | 79 + .forgejo/workflows/dev-release.yml | 79 + .forgejo/workflows/release.yml | 58 + .gitignore | 34 + CONTRIBUTING.md | 91 + Cargo.lock | 2589 +++++++++++++++++ Cargo.toml | 28 + EVENTS.md | 44 + LICENSE | 21 + README.md | 97 + bakery.toml | 34 + breadcast-caststream-sys/Cargo.toml | 16 + breadcast-caststream-sys/build.rs | 60 + breadcast-caststream-sys/src/facade.cc | 297 ++ breadcast-caststream-sys/src/facade.h | 123 + breadcast-caststream-sys/src/lib.rs | 133 + .../src/message_port_bridge.cc | 35 + .../src/message_port_bridge.h | 58 + breadcast-caststream-sys/src/session.cc | 181 ++ breadcast-caststream-sys/src/session.h | 102 + breadcast-caststream-sys/tests/smoke.rs | 91 + .../vendor/openscreen/LICENSE | 27 + .../vendor/openscreen/PATCHES.md | 64 + .../vendor/openscreen/PINNED_COMMIT | 1 + .../vendor/openscreen/build/build_config.h | 19 + .../vendor/openscreen/build/buildflag.h | 17 + .../cast/common/public/message_port.h | 52 + .../cast/streaming/capture_configs.h | 81 + .../streaming/impl/bandwidth_estimator.cc | 155 + .../cast/streaming/impl/bandwidth_estimator.h | 168 ++ .../streaming/impl/clock_drift_smoother.cc | 79 + .../streaming/impl/clock_drift_smoother.h | 58 + .../streaming/impl/clock_offset_estimator.h | 71 + .../impl/clock_offset_estimator_impl.cc | 222 ++ .../impl/clock_offset_estimator_impl.h | 136 + .../streaming/impl/compound_rtcp_parser.cc | 479 +++ .../streaming/impl/compound_rtcp_parser.h | 135 + .../cast/streaming/impl/expanded_value_base.h | 174 ++ .../cast/streaming/impl/frame_crypto.cc | 106 + .../cast/streaming/impl/frame_crypto.h | 78 + .../cast/streaming/impl/message_constants.h | 15 + .../cast/streaming/impl/ntp_time.cc | 54 + .../openscreen/cast/streaming/impl/ntp_time.h | 73 + .../cast/streaming/impl/packet_util.cc | 39 + .../cast/streaming/impl/packet_util.h | 60 + .../cast/streaming/impl/rtcp_common.cc | 241 ++ .../cast/streaming/impl/rtcp_common.h | 203 ++ .../cast/streaming/impl/rtcp_session.cc | 25 + .../cast/streaming/impl/rtcp_session.h | 43 + .../cast/streaming/impl/rtp_defines.cc | 113 + .../cast/streaming/impl/rtp_defines.h | 382 +++ .../cast/streaming/impl/rtp_packetizer.cc | 133 + .../cast/streaming/impl/rtp_packetizer.h | 79 + .../cast/streaming/impl/sender_impl.cc | 686 +++++ .../cast/streaming/impl/sender_impl.h | 252 ++ .../streaming/impl/sender_report_builder.cc | 79 + .../streaming/impl/sender_report_builder.h | 50 + .../streaming/impl/statistics_analyzer.cc | 573 ++++ .../cast/streaming/impl/statistics_analyzer.h | 208 ++ .../streaming/impl/statistics_collector.cc | 74 + .../streaming/impl/statistics_collector.h | 64 + .../cast/streaming/impl/statistics_common.cc | 115 + .../cast/streaming/impl/statistics_common.h | 223 ++ .../streaming/impl/statistics_dispatcher.cc | 163 ++ .../streaming/impl/statistics_dispatcher.h | 58 + .../cast/streaming/message_fields.cc | 47 + .../cast/streaming/message_fields.h | 59 + .../cast/streaming/public/answer_messages.cc | 498 ++++ .../cast/streaming/public/answer_messages.h | 122 + .../public/capture_recommendations.cc | 157 + .../public/capture_recommendations.h | 151 + .../cast/streaming/public/constants.cc | 57 + .../cast/streaming/public/constants.h | 122 + .../cast/streaming/public/encoded_frame.cc | 60 + .../cast/streaming/public/encoded_frame.h | 119 + .../cast/streaming/public/environment.cc | 171 ++ .../cast/streaming/public/environment.h | 164 ++ .../cast/streaming/public/frame_id.cc | 20 + .../cast/streaming/public/frame_id.h | 121 + .../cast/streaming/public/offer_messages.cc | 487 ++++ .../cast/streaming/public/offer_messages.h | 115 + .../cast/streaming/public/receiver_message.cc | 300 ++ .../cast/streaming/public/receiver_message.h | 117 + .../cast/streaming/public/sender.cc | 12 + .../openscreen/cast/streaming/public/sender.h | 173 ++ .../cast/streaming/public/session_config.cc | 54 + .../cast/streaming/public/session_config.h | 71 + .../streaming/public/session_messenger.cc | 388 +++ .../cast/streaming/public/session_messenger.h | 168 ++ .../cast/streaming/public/statistics.cc | 183 ++ .../cast/streaming/public/statistics.h | 195 ++ .../openscreen/cast/streaming/resolution.cc | 141 + .../openscreen/cast/streaming/resolution.h | 67 + .../openscreen/cast/streaming/rtp_time.cc | 23 + .../openscreen/cast/streaming/rtp_time.h | 257 ++ .../cast/streaming/sender_message.cc | 128 + .../cast/streaming/sender_message.h | 55 + .../cast/streaming/sender_packet_router.cc | 273 ++ .../cast/streaming/sender_packet_router.h | 203 ++ .../vendor/openscreen/cast/streaming/ssrc.cc | 42 + .../vendor/openscreen/cast/streaming/ssrc.h | 37 + .../vendor/openscreen/patches/compat_shims.cc | 26 + .../vendor/openscreen/patches/compat_shims.h | 35 + .../openscreen/platform/api/connection.h | 54 + .../vendor/openscreen/platform/api/export.h | 26 + .../vendor/openscreen/platform/api/logging.h | 62 + .../platform/api/network_interface.h | 24 + .../openscreen/platform/api/task_runner.h | 64 + .../platform/api/task_runner_deleter.cc | 23 + .../platform/api/task_runner_deleter.h | 64 + .../vendor/openscreen/platform/api/time.h | 37 + .../openscreen/platform/api/tls_connection.cc | 12 + .../openscreen/platform/api/tls_connection.h | 29 + .../platform/api/tls_connection_factory.cc | 14 + .../platform/api/tls_connection_factory.h | 82 + .../openscreen/platform/api/trace_event.cc | 61 + .../openscreen/platform/api/trace_event.h | 75 + .../platform/api/trace_logging_platform.cc | 13 + .../platform/api/trace_logging_platform.h | 52 + .../openscreen/platform/api/udp_socket.cc | 14 + .../openscreen/platform/api/udp_socket.h | 140 + .../platform/base/compiler_specific.h | 20 + .../vendor/openscreen/platform/base/error.cc | 306 ++ .../vendor/openscreen/platform/base/error.h | 432 +++ .../platform/base/interface_info.cc | 98 + .../openscreen/platform/base/interface_info.h | 86 + .../openscreen/platform/base/ip_address.cc | 343 +++ .../openscreen/platform/base/ip_address.h | 209 ++ .../openscreen/platform/base/location.cc | 54 + .../openscreen/platform/base/location.h | 65 + .../vendor/openscreen/platform/base/span.h | 39 + .../platform/base/tls_connect_options.h | 20 + .../platform/base/tls_credentials.cc | 22 + .../platform/base/tls_credentials.h | 33 + .../platform/base/tls_listen_options.h | 19 + .../platform/base/trace_logging_activation.cc | 72 + .../platform/base/trace_logging_activation.h | 58 + .../platform/base/trace_logging_types.cc | 62 + .../platform/base/trace_logging_types.h | 71 + .../platform/base/trivial_clock_traits.cc | 55 + .../platform/base/trivial_clock_traits.h | 98 + .../openscreen/platform/base/type_util.h | 24 + .../openscreen/platform/base/udp_packet.cc | 30 + .../openscreen/platform/base/udp_packet.h | 54 + .../vendor/openscreen/platform/impl/logging.h | 32 + .../openscreen/platform/impl/logging_posix.cc | 161 + .../openscreen/platform/impl/logging_test.h | 24 + .../platform/impl/network_interface.cc | 31 + .../platform/impl/network_interface.h | 23 + .../platform/impl/network_interface_linux.cc | 384 +++ .../platform/impl/platform_client_posix.cc | 137 + .../platform/impl/platform_client_posix.h | 130 + .../openscreen/platform/impl/scoped_pipe.h | 71 + .../platform/impl/socket_address_posix.cc | 129 + .../platform/impl/socket_address_posix.h | 66 + .../openscreen/platform/impl/socket_handle.h | 27 + .../platform/impl/socket_handle_posix.cc | 22 + .../platform/impl/socket_handle_posix.h | 19 + .../platform/impl/socket_handle_waiter.cc | 170 ++ .../platform/impl/socket_handle_waiter.h | 151 + .../impl/socket_handle_waiter_posix.cc | 102 + .../impl/socket_handle_waiter_posix.h | 45 + .../openscreen/platform/impl/socket_state.h | 37 + .../openscreen/platform/impl/task_runner.cc | 197 ++ .../openscreen/platform/impl/task_runner.h | 150 + .../impl/text_trace_logging_platform.cc | 72 + .../impl/text_trace_logging_platform.h | 30 + .../vendor/openscreen/platform/impl/time.cc | 49 + .../openscreen/platform/impl/timeval_posix.cc | 22 + .../openscreen/platform/impl/timeval_posix.h | 18 + .../platform/impl/udp_socket_posix.cc | 665 +++++ .../platform/impl/udp_socket_posix.h | 90 + .../platform/impl/udp_socket_reader_posix.cc | 78 + .../platform/impl/udp_socket_reader_posix.h | 82 + .../vendor/openscreen/util/alarm.cc | 131 + .../vendor/openscreen/util/alarm.h | 109 + .../vendor/openscreen/util/base64.cc | 65 + .../vendor/openscreen/util/base64.h | 32 + .../vendor/openscreen/util/big_endian.cc | 47 + .../vendor/openscreen/util/big_endian.h | 255 ++ .../vendor/openscreen/util/bit_vector.cc | 34 + .../vendor/openscreen/util/bit_vector.h | 66 + .../vendor/openscreen/util/chrono_helpers.h | 50 + .../openscreen/util/crypto/openssl_util.cc | 62 + .../openscreen/util/crypto/openssl_util.h | 61 + .../openscreen/util/crypto/random_bytes.cc | 23 + .../openscreen/util/crypto/random_bytes.h | 20 + .../vendor/openscreen/util/enum_name_table.h | 51 + .../vendor/openscreen/util/flat_map.h | 66 + .../vendor/openscreen/util/hashing.h | 55 + .../vendor/openscreen/util/integer_division.h | 67 + .../openscreen/util/json/json_helpers.h | 205 ++ .../util/json/json_serialization.cc | 62 + .../openscreen/util/json/json_serialization.h | 24 + .../vendor/openscreen/util/json/json_value.cc | 43 + .../vendor/openscreen/util/json/json_value.h | 29 + .../vendor/openscreen/util/no_destructor.h | 79 + .../vendor/openscreen/util/osp_logging.h | 145 + .../vendor/openscreen/util/raw_ptr.h | 441 +++ .../vendor/openscreen/util/raw_ref.h | 122 + .../vendor/openscreen/util/read_file.cc | 34 + .../vendor/openscreen/util/read_file.h | 17 + .../vendor/openscreen/util/saturate_cast.h | 145 + .../openscreen/util/scoped_wake_lock.cc | 12 + .../vendor/openscreen/util/scoped_wake_lock.h | 46 + .../vendor/openscreen/util/simple_fraction.cc | 52 + .../vendor/openscreen/util/simple_fraction.h | 73 + .../vendor/openscreen/util/std_util.cc | 20 + .../vendor/openscreen/util/std_util.h | 104 + .../vendor/openscreen/util/string_parse.h | 32 + .../vendor/openscreen/util/string_util.cc | 160 + .../vendor/openscreen/util/string_util.h | 142 + .../vendor/openscreen/util/stringprintf.cc | 29 + .../vendor/openscreen/util/stringprintf.h | 33 + .../openscreen/util/thread_annotations.h | 50 + .../vendor/openscreen/util/trace_logging.h | 282 ++ .../util/trace_logging/macro_support.h | 84 + .../trace_logging/scoped_trace_operations.cc | 160 + .../trace_logging/scoped_trace_operations.h | 224 ++ .../vendor/openscreen/util/uuid.cc | 127 + .../vendor/openscreen/util/uuid.h | 81 + .../vendor/openscreen/util/weak_ptr.h | 217 ++ breadcast-core/Cargo.toml | 63 + breadcast-core/src/capture/mod.rs | 3 + breadcast-core/src/capture/portal.rs | 144 + breadcast-core/src/cast_sender.rs | 335 +++ breadcast-core/src/caststream.rs | 292 ++ breadcast-core/src/device.rs | 14 + breadcast-core/src/discovery.rs | 186 ++ breadcast-core/src/dlna/device.rs | 16 + breadcast-core/src/dlna/discovery.rs | 131 + breadcast-core/src/dlna/mod.rs | 23 + breadcast-core/src/dlna/session.rs | 122 + breadcast-core/src/examples/apple_hls_test.rs | 36 + breadcast-core/src/examples/capture_test.rs | 22 + .../src/examples/cast_stream_test.rs | 202 ++ breadcast-core/src/examples/cast_test.rs | 95 + breadcast-core/src/examples/discover.rs | 25 + breadcast-core/src/examples/dlna_discover.rs | 27 + .../src/examples/dlna_mirror_test.rs | 107 + breadcast-core/src/examples/mirror_test.rs | 134 + breadcast-core/src/examples/video_test.rs | 44 + breadcast-core/src/http_server.rs | 266 ++ breadcast-core/src/ipc.rs | 86 + breadcast-core/src/lib.rs | 17 + breadcast-core/src/net.rs | 61 + breadcast-core/src/pipeline/mod.rs | 312 ++ breadcast/Cargo.toml | 20 + breadcast/src/css.rs | 62 + breadcast/src/ipc_client.rs | 68 + breadcast/src/main.rs | 202 ++ breadcastd/Cargo.toml | 24 + breadcastd/src/bread_events.rs | 93 + breadcastd/src/cast_mirror.rs | 202 ++ breadcastd/src/daemon.rs | 221 ++ breadcastd/src/dlna_mirror.rs | 139 + breadcastd/src/ipc.rs | 175 ++ breadcastd/src/main.rs | 122 + contrib/binds.json | 6 + contrib/breadcastd.service | 19 + contrib/hyprland.conf | 18 + vendor/rust_cast-0.21.0/.gitignore | 8 + vendor/rust_cast-0.21.0/.husky/commit-msg | 1 + vendor/rust_cast-0.21.0/.husky/pre-push | 30 + vendor/rust_cast-0.21.0/Cargo.toml | 41 + vendor/rust_cast-0.21.0/LICENSE | 21 + vendor/rust_cast-0.21.0/PATCHES.md | 37 + vendor/rust_cast-0.21.0/README.md | 102 + vendor/rust_cast-0.21.0/build.rs | 22 + vendor/rust_cast-0.21.0/rustfmt.toml | 2 + .../src/cast/authority_keys.rs | 306 ++ .../rust_cast-0.21.0/src/cast/cast_channel.rs | 1555 ++++++++++ vendor/rust_cast-0.21.0/src/cast/mod.rs | 3 + vendor/rust_cast-0.21.0/src/cast/proxies.rs | 529 ++++ .../src/channels/connection.rs | 113 + .../src/channels/heartbeat.rs | 110 + vendor/rust_cast-0.21.0/src/channels/media.rs | 1643 +++++++++++ vendor/rust_cast-0.21.0/src/channels/mod.rs | 4 + .../rust_cast-0.21.0/src/channels/receiver.rs | 518 ++++ vendor/rust_cast-0.21.0/src/errors.rs | 69 + vendor/rust_cast-0.21.0/src/lib.rs | 571 ++++ .../rust_cast-0.21.0/src/message_manager.rs | 347 +++ vendor/rust_cast-0.21.0/src/utils.rs | 29 + 283 files changed, 36788 insertions(+) create mode 100644 .forgejo/workflows/beta-release.yml create mode 100644 .forgejo/workflows/dev-release.yml create mode 100644 .forgejo/workflows/release.yml create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 EVENTS.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 bakery.toml create mode 100644 breadcast-caststream-sys/Cargo.toml create mode 100644 breadcast-caststream-sys/build.rs create mode 100644 breadcast-caststream-sys/src/facade.cc create mode 100644 breadcast-caststream-sys/src/facade.h create mode 100644 breadcast-caststream-sys/src/lib.rs create mode 100644 breadcast-caststream-sys/src/message_port_bridge.cc create mode 100644 breadcast-caststream-sys/src/message_port_bridge.h create mode 100644 breadcast-caststream-sys/src/session.cc create mode 100644 breadcast-caststream-sys/src/session.h create mode 100644 breadcast-caststream-sys/tests/smoke.rs create mode 100644 breadcast-caststream-sys/vendor/openscreen/LICENSE create mode 100644 breadcast-caststream-sys/vendor/openscreen/PATCHES.md create mode 100644 breadcast-caststream-sys/vendor/openscreen/PINNED_COMMIT create mode 100644 breadcast-caststream-sys/vendor/openscreen/build/build_config.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/build/buildflag.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/common/public/message_port.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/capture_configs.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/bandwidth_estimator.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/bandwidth_estimator.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_drift_smoother.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_drift_smoother.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_offset_estimator.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_offset_estimator_impl.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_offset_estimator_impl.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/compound_rtcp_parser.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/compound_rtcp_parser.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/expanded_value_base.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/frame_crypto.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/frame_crypto.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/message_constants.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/ntp_time.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/ntp_time.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/packet_util.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/packet_util.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_common.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_common.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_session.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_session.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_defines.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_defines.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_packetizer.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_packetizer.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_impl.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_impl.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_report_builder.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_report_builder.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_analyzer.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_analyzer.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_collector.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_collector.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_common.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_common.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_dispatcher.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_dispatcher.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/message_fields.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/message_fields.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/answer_messages.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/answer_messages.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/capture_recommendations.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/capture_recommendations.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/constants.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/constants.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/encoded_frame.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/encoded_frame.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/environment.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/environment.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/frame_id.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/frame_id.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/offer_messages.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/offer_messages.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/receiver_message.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/receiver_message.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/sender.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/sender.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_config.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_config.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_messenger.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_messenger.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/statistics.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/statistics.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/resolution.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/resolution.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/rtp_time.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/rtp_time.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_message.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_message.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_packet_router.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_packet_router.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/ssrc.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/cast/streaming/ssrc.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/patches/compat_shims.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/patches/compat_shims.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/connection.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/export.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/logging.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/network_interface.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/task_runner.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/task_runner_deleter.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/task_runner_deleter.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/time.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection_factory.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection_factory.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/trace_event.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/trace_event.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/trace_logging_platform.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/trace_logging_platform.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/udp_socket.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/api/udp_socket.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/compiler_specific.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/error.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/error.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/interface_info.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/interface_info.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/ip_address.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/ip_address.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/location.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/location.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/span.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/tls_connect_options.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/tls_credentials.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/tls_credentials.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/tls_listen_options.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_activation.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_activation.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_types.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_types.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/trivial_clock_traits.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/trivial_clock_traits.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/type_util.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/udp_packet.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/base/udp_packet.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/logging.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/logging_posix.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/logging_test.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/network_interface.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/network_interface.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/network_interface_linux.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/platform_client_posix.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/platform_client_posix.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/scoped_pipe.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_address_posix.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_address_posix.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_posix.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_posix.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter_posix.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter_posix.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_state.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/task_runner.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/task_runner.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/text_trace_logging_platform.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/text_trace_logging_platform.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/time.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/timeval_posix.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/timeval_posix.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_posix.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_posix.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_reader_posix.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_reader_posix.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/alarm.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/alarm.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/base64.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/base64.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/big_endian.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/big_endian.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/bit_vector.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/bit_vector.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/chrono_helpers.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/crypto/openssl_util.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/crypto/openssl_util.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/crypto/random_bytes.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/crypto/random_bytes.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/enum_name_table.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/flat_map.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/hashing.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/integer_division.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/json/json_helpers.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/json/json_serialization.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/json/json_serialization.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/json/json_value.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/json/json_value.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/no_destructor.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/osp_logging.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/raw_ptr.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/raw_ref.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/read_file.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/read_file.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/saturate_cast.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/scoped_wake_lock.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/scoped_wake_lock.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/simple_fraction.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/simple_fraction.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/std_util.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/std_util.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/string_parse.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/string_util.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/string_util.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/stringprintf.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/stringprintf.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/thread_annotations.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/trace_logging.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/trace_logging/macro_support.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/trace_logging/scoped_trace_operations.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/trace_logging/scoped_trace_operations.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/uuid.cc create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/uuid.h create mode 100644 breadcast-caststream-sys/vendor/openscreen/util/weak_ptr.h create mode 100644 breadcast-core/Cargo.toml create mode 100644 breadcast-core/src/capture/mod.rs create mode 100644 breadcast-core/src/capture/portal.rs create mode 100644 breadcast-core/src/cast_sender.rs create mode 100644 breadcast-core/src/caststream.rs create mode 100644 breadcast-core/src/device.rs create mode 100644 breadcast-core/src/discovery.rs create mode 100644 breadcast-core/src/dlna/device.rs create mode 100644 breadcast-core/src/dlna/discovery.rs create mode 100644 breadcast-core/src/dlna/mod.rs create mode 100644 breadcast-core/src/dlna/session.rs create mode 100644 breadcast-core/src/examples/apple_hls_test.rs create mode 100644 breadcast-core/src/examples/capture_test.rs create mode 100644 breadcast-core/src/examples/cast_stream_test.rs create mode 100644 breadcast-core/src/examples/cast_test.rs create mode 100644 breadcast-core/src/examples/discover.rs create mode 100644 breadcast-core/src/examples/dlna_discover.rs create mode 100644 breadcast-core/src/examples/dlna_mirror_test.rs create mode 100644 breadcast-core/src/examples/mirror_test.rs create mode 100644 breadcast-core/src/examples/video_test.rs create mode 100644 breadcast-core/src/http_server.rs create mode 100644 breadcast-core/src/ipc.rs create mode 100644 breadcast-core/src/lib.rs create mode 100644 breadcast-core/src/net.rs create mode 100644 breadcast-core/src/pipeline/mod.rs create mode 100644 breadcast/Cargo.toml create mode 100644 breadcast/src/css.rs create mode 100644 breadcast/src/ipc_client.rs create mode 100644 breadcast/src/main.rs create mode 100644 breadcastd/Cargo.toml create mode 100644 breadcastd/src/bread_events.rs create mode 100644 breadcastd/src/cast_mirror.rs create mode 100644 breadcastd/src/daemon.rs create mode 100644 breadcastd/src/dlna_mirror.rs create mode 100644 breadcastd/src/ipc.rs create mode 100644 breadcastd/src/main.rs create mode 100644 contrib/binds.json create mode 100644 contrib/breadcastd.service create mode 100644 contrib/hyprland.conf create mode 100644 vendor/rust_cast-0.21.0/.gitignore create mode 100755 vendor/rust_cast-0.21.0/.husky/commit-msg create mode 100755 vendor/rust_cast-0.21.0/.husky/pre-push create mode 100644 vendor/rust_cast-0.21.0/Cargo.toml create mode 100644 vendor/rust_cast-0.21.0/LICENSE create mode 100644 vendor/rust_cast-0.21.0/PATCHES.md create mode 100644 vendor/rust_cast-0.21.0/README.md create mode 100644 vendor/rust_cast-0.21.0/build.rs create mode 100644 vendor/rust_cast-0.21.0/rustfmt.toml create mode 100644 vendor/rust_cast-0.21.0/src/cast/authority_keys.rs create mode 100644 vendor/rust_cast-0.21.0/src/cast/cast_channel.rs create mode 100644 vendor/rust_cast-0.21.0/src/cast/mod.rs create mode 100644 vendor/rust_cast-0.21.0/src/cast/proxies.rs create mode 100644 vendor/rust_cast-0.21.0/src/channels/connection.rs create mode 100644 vendor/rust_cast-0.21.0/src/channels/heartbeat.rs create mode 100644 vendor/rust_cast-0.21.0/src/channels/media.rs create mode 100644 vendor/rust_cast-0.21.0/src/channels/mod.rs create mode 100644 vendor/rust_cast-0.21.0/src/channels/receiver.rs create mode 100644 vendor/rust_cast-0.21.0/src/errors.rs create mode 100644 vendor/rust_cast-0.21.0/src/lib.rs create mode 100644 vendor/rust_cast-0.21.0/src/message_manager.rs create mode 100644 vendor/rust_cast-0.21.0/src/utils.rs diff --git a/.forgejo/workflows/beta-release.yml b/.forgejo/workflows/beta-release.yml new file mode 100644 index 0000000..56f53fe --- /dev/null +++ b/.forgejo/workflows/beta-release.yml @@ -0,0 +1,79 @@ +name: beta release + +# Publishes a beta-track build on every push to `beta` — a frozen +# stabilization branch cut from `dev` when ready to stabilize; only +# fix/ branches merged into `beta` should land here afterward. +# See bread-ecosystem's docs/release-channels.md for the three-track policy. +on: + push: + branches: ['beta'] + +jobs: + build: + runs-on: [self-hosted, hestia] + steps: + - name: checkout + run: | + set -euo pipefail + rm -rf src && mkdir src + git clone --branch beta --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src + + - name: build + run: cd src && cargo build --release --locked + + - name: compute beta version + run: | + set -euo pipefail + cd src + # Base the beta version off the latest published stable tag, + # not Cargo.toml — Cargo.toml can go stale relative to the last + # real release (seen in practice: breadbox/breadpad/breadcrumbs/ + # breadpaper), which would make a beta build sort as OLDER than + # what's already installed and bakery would correctly refuse it. + LATEST_TAG="$(git ls-remote --tags --refs \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \ + | awk -F/ '{print $NF}' | sed 's/^v//' | sort -V | tail -1)" + if [ -n "${LATEST_TAG}" ]; then + CUR="${LATEST_TAG}" + else + CUR="$(grep -m1 '^version' breadcast/Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" + fi + IFS='.' read -r MA MI PA <<< "${CUR}" + SHA="$(git rev-parse --short HEAD)" + TS="$(date -u +%Y%m%d%H%M%S)" + echo "VERSION=${MA}.${MI}.$((PA + 1))-beta.${TS}+${SHA}" >> "$GITHUB_ENV" + + - name: prepare artifacts + run: | + set -euo pipefail + PKG_DIR="/srv/breadway-dl/beta/breadcast/${VERSION}" + mkdir -p "${PKG_DIR}" + for bin in breadcast breadcastd; do + cp "src/target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" + strip "${PKG_DIR}/${bin}-x86_64" + sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ + > "${PKG_DIR}/${bin}-x86_64.sha256" + done + cp src/contrib/breadcastd.service "${PKG_DIR}/" + cp src/bakery.toml "${PKG_DIR}/bakery.toml" + ln -sfn "${VERSION}" "/srv/breadway-dl/beta/breadcast/latest" + + # No GitHub Release upload — beta, like dev, is only distributed via + # dl.breadway.dev/beta/. + - name: regenerate beta index.json + env: + MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} + run: | + set -euo pipefail + if [ -z "${MINISIGN_SEC_KEY:-}" ]; then + echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate beta index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the beta track)" + exit 1 + fi + rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true + # mktemp: a fixed clone path races when multiple repos' dev/beta + # workflows run close together on the same self-hosted runner. + ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" + git clone https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" + TRACK=beta bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" + rm -rf "${ECOSYSTEM_CI_DIR}" diff --git a/.forgejo/workflows/dev-release.yml b/.forgejo/workflows/dev-release.yml new file mode 100644 index 0000000..d8509ee --- /dev/null +++ b/.forgejo/workflows/dev-release.yml @@ -0,0 +1,79 @@ +name: dev release + +# Publishes a dev-track build on every push to `dev` — +# separate from release.yml's tag-triggered stable releases. See +# bread-ecosystem's docs/release-channels.md for the three-track policy +# this is part of. +on: + push: + branches: ['dev'] + +jobs: + build: + runs-on: [self-hosted, hestia] + steps: + - name: checkout + run: | + set -euo pipefail + rm -rf src && mkdir src + git clone --branch dev --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src + + - name: build + run: cd src && cargo build --release --locked + + - name: compute dev version + run: | + set -euo pipefail + cd src + # Base the dev version off the latest published stable tag, + # not Cargo.toml — Cargo.toml can go stale relative to the last + # real release (seen in practice: breadbox/breadpad/breadcrumbs/ + # breadpaper), which would make a dev build sort as OLDER than + # what's already installed and bakery would correctly refuse it. + LATEST_TAG="$(git ls-remote --tags --refs \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" 'v*' \ + | awk -F/ '{print $NF}' | sed 's/^v//' | sort -V | tail -1)" + if [ -n "${LATEST_TAG}" ]; then + CUR="${LATEST_TAG}" + else + CUR="$(grep -m1 '^version' breadcast/Cargo.toml | sed -E 's/.*"(.*)".*/\1/')" + fi + IFS='.' read -r MA MI PA <<< "${CUR}" + SHA="$(git rev-parse --short HEAD)" + TS="$(date -u +%Y%m%d%H%M%S)" + echo "VERSION=${MA}.${MI}.$((PA + 1))-dev.${TS}+${SHA}" >> "$GITHUB_ENV" + + - name: prepare artifacts + run: | + set -euo pipefail + PKG_DIR="/srv/breadway-dl/dev/breadcast/${VERSION}" + mkdir -p "${PKG_DIR}" + for bin in breadcast breadcastd; do + cp "src/target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" + strip "${PKG_DIR}/${bin}-x86_64" + sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ + > "${PKG_DIR}/${bin}-x86_64.sha256" + done + cp src/contrib/breadcastd.service "${PKG_DIR}/" + cp src/bakery.toml "${PKG_DIR}/bakery.toml" + ln -sfn "${VERSION}" "/srv/breadway-dl/dev/breadcast/latest" + + # No GitHub Release upload — dev, like the other non-stable track, + # is only distributed via dl.breadway.dev/dev/. + - name: regenerate dev index.json + env: + MINISIGN_SEC_KEY: ${{ secrets.BAKERY_MINISIGN_SEC_KEY_PATH }} + run: | + set -euo pipefail + if [ -z "${MINISIGN_SEC_KEY:-}" ]; then + echo "::error::BAKERY_MINISIGN_SEC_KEY_PATH secret not set — refusing to regenerate dev index.json unsigned (would leave a stale signature mismatched against fresh content and break bakery for everyone on the dev track)" + exit 1 + fi + rm -rf /tmp/bread-ecosystem-ci-* 2>/dev/null || true + # mktemp: a fixed clone path races when multiple repos' dev/beta + # workflows run close together on the same self-hosted runner. + ECOSYSTEM_CI_DIR="$(mktemp -d /tmp/bread-ecosystem-ci-XXXXXX)" + git clone --branch dev https://git.breadway.dev/Breadway/bread-ecosystem.git "${ECOSYSTEM_CI_DIR}" + TRACK=dev bash "${ECOSYSTEM_CI_DIR}/scripts/gen-index.sh" + rm -rf "${ECOSYSTEM_CI_DIR}" diff --git a/.forgejo/workflows/release.yml b/.forgejo/workflows/release.yml new file mode 100644 index 0000000..79a9960 --- /dev/null +++ b/.forgejo/workflows/release.yml @@ -0,0 +1,58 @@ +name: release + +on: + push: + tags: ["v*"] + +jobs: + build: + runs-on: [self-hosted, hestia] + steps: + - name: checkout + run: | + set -euo pipefail + rm -rf src && mkdir src + git clone --branch "${GITHUB_REF_NAME}" --depth 1 \ + "https://git.breadway.dev/${GITHUB_REPOSITORY}.git" src + + - name: build + run: cd src && cargo build --release --locked + + - name: prepare artifacts + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + PKG_DIR="/srv/breadway-dl/breadcast/${VERSION}" + mkdir -p "${PKG_DIR}" + for bin in breadcast breadcastd; do + cp "src/target/release/${bin}" "${PKG_DIR}/${bin}-x86_64" + strip "${PKG_DIR}/${bin}-x86_64" + sha256sum "${PKG_DIR}/${bin}-x86_64" | awk '{print $1}' \ + > "${PKG_DIR}/${bin}-x86_64.sha256" + done + cp src/contrib/breadcastd.service "${PKG_DIR}/" + cp src/bakery.toml "${PKG_DIR}/bakery.toml" + ln -sfn "${VERSION}" "/srv/breadway-dl/breadcast/latest" + + - name: regenerate index.json + run: | + set -euo pipefail + rm -rf /tmp/bread-ecosystem-ci + git clone https://git.breadway.dev/Breadway/bread-ecosystem.git /tmp/bread-ecosystem-ci + bash /tmp/bread-ecosystem-ci/scripts/gen-index.sh + + - name: upload to GitHub Release + env: + GH_TOKEN: ${{ secrets.GH_RELEASE_TOKEN }} + run: | + set -euo pipefail + VERSION="${GITHUB_REF_NAME#v}" + PKG_DIR="/srv/breadway-dl/breadcast/${VERSION}" + gh release create "${GITHUB_REF_NAME}" --repo Breadway/breadcast \ + --title "breadcast v${VERSION}" --generate-notes 2>/dev/null || true + gh release upload "${GITHUB_REF_NAME}" --repo Breadway/breadcast \ + "${PKG_DIR}/breadcast-x86_64" \ + "${PKG_DIR}/breadcastd-x86_64" \ + "${PKG_DIR}/breadcast-x86_64.sha256" \ + "${PKG_DIR}/breadcastd-x86_64.sha256" \ + --clobber diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d2803a --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# Rust build artifacts +target/ + +# Editor and IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS artifacts +.DS_Store +Thumbs.db +desktop.ini + +# Environment and secrets +.env +.env.* +*.env +*.pem +*.key +*.p12 +secrets/ + +# Log files +*.log +logs/ + +# Runtime files +*.sock +*.pid + +# Local hygiene notes (not for commit) +CLAUDE.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a8f1f84 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,91 @@ +# Contributing + +`breadcast` — cast your screen to any Chromecast/Google TV, for Hyprland +(daemon + GTK4 popup). + +Part of the bread ecosystem; this repo follows the same branch/release +workflow as every other ecosystem product. + +## Branches + +- **`main`** — release branch, always tag-ready. Nothing is committed to it + directly; it only moves forward via a `beta` merge (see below). +- **`dev`** — integration branch. All day-to-day work lands here first. + Every push to `dev` automatically builds and publishes a **dev-track** + build (see Tracks below) — use this to test your change in a real install + before it goes any further. +- **`beta`** — a frozen stabilization branch, cut from `dev` periodically. + Every push to `beta` automatically builds and publishes a **beta-track** + build. While a freeze is active, only fixes for issues found *in that + freeze* should land on `beta`. + +New work — features and bug fixes alike — goes on a short-lived branch: + +``` +feature/ +fix/ +``` + +Branch off `dev`, open a PR/push back into `dev` when ready. If you're fixing +something reported against an active `beta` freeze, branch off `beta` +instead, merge the fix there to unblock testers, and also forward the same +fix into `dev` so it doesn't quietly reappear next cycle. + +## The release cycle + +1. Work accumulates on `dev` via `feature/x` / `fix/x` branches. Each push + auto-publishes a dev build — install it with `bakery track set dev` and + `bakery update --all`, then report or fix anything broken with another + push to `dev`. +2. Once `dev` has gone roughly **a week** without new issues, `beta` is cut + fresh from `dev`'s current tip. This freezes it as the stabilization + target — `dev` keeps moving independently starting the next cycle. +3. `beta` is open for anyone to test: `bakery track set beta` and + `bakery update --all`. **File issues against anything you find on this + repo's Forgejo issue tracker.** Fixes land via `fix/` branches + merged into `beta`. +4. Once `beta` has gone roughly **a month** without new issues, it's merged + into `main` and tagged `vX.Y.Z` — that tag is what actually triggers the + stable release build. `beta` is then reset from `dev` to start the next + cycle. + +## Tracks, from a user's perspective + +``` +bakery track show # what you're currently on (defaults to stable) +bakery track set dev # or beta, or stable +bakery update --all # pull the latest build on your current track +``` + +| Track | What it is | Published from | +|--------|-----------|-----------------| +| `stable` | The last tagged release | `main`, on a `vX.Y.Z` tag push | +| `beta` | Current stabilization freeze | `beta`, on every push | +| `dev` | Bleeding edge | `dev`, on every push | + +Dev/beta versions are auto-computed (`X.Y.Z-dev.+` / +`-beta.…`) from the latest published stable tag, so they always sort as +newer than what you have installed — no manual version bumping needed when +pushing to `dev` or `beta`. + +## Local development + +```sh +cargo build --release --workspace +cargo test --release --workspace +``` + +## CI + +- `dev-release.yml` — triggered on push to `dev`. +- `beta-release.yml` — triggered on push to `beta`. +- `release.yml` — triggered on a `v*` tag push, cuts the actual stable release. + +All CI runs on a self-hosted runner; nothing runs automatically on plain +commits or PRs beyond the track builds above. See +[bread-ecosystem's docs/release-channels.md](https://git.breadway.dev/Breadway/bread-ecosystem/src/branch/main/docs/release-channels.md) +for the full policy, including how a new product gets wired onto these tracks. + +## Questions + +Open an issue on this repo's Forgejo tracker. diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..bb235a1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,2589 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "ascii" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" + +[[package]] +name = "ashpd" +version = "0.13.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8421aaa9644a5faf26735f258b669b15f063313ef8f8e2bdb28912a1a6f111" +dependencies = [ + "enumflags2", + "futures-util", + "getrandom 0.4.3", + "serde", + "serde_repr", + "tokio", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atomic_refcell" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e4227379beff4205943696e6c3e0cd809bacdf3f0edd6e3dd153e2269571a4" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "aws-lc-rs" +version = "1.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bread-shared" +version = "0.7.0" +source = "git+https://git.breadway.dev/Breadway/bread?tag=v0.7.0#22e34e2cf2202305d7960759dfccb54dc79f948b" +dependencies = [ + "dirs", + "serde", + "serde_json", + "toml 0.8.23", +] + +[[package]] +name = "bread-theme" +version = "0.3.1" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886" +dependencies = [ + "dirs", + "gtk4", + "serde", + "serde_json", +] + +[[package]] +name = "bread-utils" +version = "0.3.1" +source = "git+https://git.breadway.dev/Breadway/bread-ecosystem?tag=v0.7.1#db2fa3c4b4c1e6933bc5cf62a236d05972fdc886" +dependencies = [ + "bread-shared", + "dirs", + "gtk4", + "gtk4-layer-shell", + "serde", + "serde_json", +] + +[[package]] +name = "breadcast" +version = "0.1.0" +dependencies = [ + "anyhow", + "bread-theme", + "bread-utils", + "breadcast-core", + "gtk4", + "gtk4-layer-shell", + "serde_json", +] + +[[package]] +name = "breadcast-caststream-sys" +version = "0.1.0" +dependencies = [ + "cc", + "pkg-config", + "tracing-subscriber", +] + +[[package]] +name = "breadcast-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "ashpd", + "breadcast-caststream-sys", + "futures-util", + "gstreamer", + "gstreamer-app", + "gstreamer-video", + "mdns-sd", + "rupnp", + "rust_cast", + "serde", + "serde_json", + "tiny_http", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "breadcastd" +version = "0.1.0" +dependencies = [ + "anyhow", + "bread-utils", + "breadcast-core", + "gstreamer", + "gstreamer-app", + "rust_cast", + "serde", + "serde_json", + "tokio", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cairo-rs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95" +dependencies = [ + "bitflags", + "cairo-sys-rs", + "glib", + "libc", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "cc" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-expr" +version = "0.20.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chunked_transfer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flume" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e139bc46ca777eb5efaf62df0ab8cc5fd400866427e56c68b22e414e53bd3be" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" + +[[package]] +name = "futures-executor" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + +[[package]] +name = "futures-task" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" + +[[package]] +name = "futures-util" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +dependencies = [ + "futures-core", + "futures-macro", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk4" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d81e2a6c6ecba2aab60633a98df1868b03fa0bfdce8105edc27c1bccf71f0e39" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk4-sys", + "gio", + "gl", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk4-sys" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d8f608d8d7d229975c4d0d026f5d3071598c4ddab3c5262b0a31840fec78d13" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "genawaiter" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" +dependencies = [ + "futures-core", + "genawaiter-macro", +] + +[[package]] +name = "genawaiter-macro" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "gio" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b3e1f669909c326b9413bde5a742097b8c90a7d78f45326db13668984769ded" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "pin-project-lite", + "smallvec", +] + +[[package]] +name = "gio-sys" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353fdc7da7cd16da916104b1e0e4e7de380ec9c8aaa20d4d742d66310ab4b0d5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "windows-sys 0.61.2", +] + +[[package]] +name = "gl" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a94edab108827d67608095e269cf862e60d920f144a5026d3dbcfd8b877fb404" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glib" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddbcf514bd1881fc1b960e4e52b4e82873f4da3bceddbd58d42827b508888100" +dependencies = [ + "bitflags", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "smallvec", +] + +[[package]] +name = "glib-macros" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "glib-sys" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030967459f9f676851872c6304adea7825c6d462ec9b72554c733cf0c5952233" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "gobject-sys" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "graphene-rs" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb856b9c558971c3f13ab692358926da710b046932a4e087aedcc35b040d7dff" +dependencies = [ + "glib", + "graphene-sys", +] + +[[package]] +name = "graphene-sys" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7ffdfde88f3570d3705e0d8a2433e036d387a1f2930bbf47eafcb5f569fd04" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gsk4" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b867be1c5f14dcb8f552c0eff6e9a9b1da5f8b43943e8efc3a63c889d84952ff" +dependencies = [ + "cairo-rs", + "gdk4", + "glib", + "graphene-rs", + "gsk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gsk4-sys" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b7c7eb2e681ee896646cfb8872b431f24d09f53ba9283289d9b10caa6707088" +dependencies = [ + "cairo-sys-rs", + "gdk4-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gstreamer" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab4527e1b9bae8d29ce137bde5b8eec8ae8f78f13ad00fc6e70cbe227d6ad027" +dependencies = [ + "cfg-if", + "futures-channel", + "futures-core", + "futures-util", + "glib", + "gstreamer-sys", + "itertools", + "kstring", + "libc", + "muldiv", + "num-integer", + "num-rational", + "option-operations", + "pastey", + "pin-project-lite", + "smallvec", + "thiserror 2.0.19", +] + +[[package]] +name = "gstreamer-app" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97f8ae9238c2352398dcc084de28df3f7099af216ac6c160b52318d23f25c010" +dependencies = [ + "futures-core", + "futures-sink", + "glib", + "gstreamer", + "gstreamer-app-sys", + "gstreamer-base", + "libc", +] + +[[package]] +name = "gstreamer-app-sys" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a74a8211e5d7df2f45b612c284ddf56b92bdf4e879e8ed72e7c46dd0842e158" +dependencies = [ + "glib-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-base" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c91c94a4d3047d05dd6e1f6d91c74f61f56384c7ea1c9d0c1051572eeeb0138d" +dependencies = [ + "atomic_refcell", + "cfg-if", + "glib", + "gstreamer", + "gstreamer-base-sys", + "libc", +] + +[[package]] +name = "gstreamer-base-sys" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fbbc623dc066908ba10c43d629c21096508dea04796592a206c4edd864e37" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-sys" +version = "0.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "533fa8d28fc830eafccbcfcfddb390563ea5d3a351af2c3aab99e197e5f5b1ba" +dependencies = [ + "cfg-if", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gstreamer-video" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7907cb8a73edc98cf279e5de7370bb2c245b7bedf623ba8c7e59648cd4739133" +dependencies = [ + "cfg-if", + "futures-channel", + "glib", + "gstreamer", + "gstreamer-base", + "gstreamer-video-sys", + "libc", + "thiserror 2.0.19", +] + +[[package]] +name = "gstreamer-video-sys" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f6753d53c9a5e274f33441bee2c7dabef82edefb39605b75c834d24af89e7e" +dependencies = [ + "glib-sys", + "gobject-sys", + "gstreamer-base-sys", + "gstreamer-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk4" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98a0a0466484f64b07b5b8184d43fa46be78eb0b8e04ae4e179af31d770b76d9" +dependencies = [ + "cairo-rs", + "field-offset", + "futures-channel", + "gdk-pixbuf", + "gdk4", + "gio", + "glib", + "graphene-rs", + "gsk4", + "gtk4-macros", + "gtk4-sys", + "libc", + "pango", +] + +[[package]] +name = "gtk4-layer-shell" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4069987ff4793699511a251028cc336b438e46565b463f111250148d574752a" +dependencies = [ + "bitflags", + "gdk4", + "glib", + "glib-sys", + "gtk4", + "gtk4-layer-shell-sys", + "libc", +] + +[[package]] +name = "gtk4-layer-shell-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f566a5ec5bcc454e7fcf2ab76930887ced5365afce12c1e5201bb296b95f1b9" +dependencies = [ + "gdk4-sys", + "glib-sys", + "gtk4-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk4-macros" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ac7179400a36a04de039c24206bb841c5596992b907b43b23ee8d5bdc40d00e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "gtk4-sys" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f954786af0b1984425c4446b77f5ff6594346181316be3f850caab1c6f01" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk4-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "graphene-sys", + "gsk4-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "if-addrs" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b2eeee38fef3aa9b4cc5f1beea8a2444fc00e7377cafae396de3f5c2065e24" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "if-addrs" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a05c691e1fae256cf7013d99dad472dc52d5543322761f83ec8d47eab40d2b" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itertools" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kstring" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b609e7ca5ea38f093c20a4a102335b247221c9643b7a6bc3510f196f99499a9e" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libredox" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "mdns-sd" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86dbb9f00c8c367f75ed3a775d3eb31d0375a72f58275ef64a1bc53c255a2ce2" +dependencies = [ + "fastrand", + "flume", + "if-addrs 0.15.0", + "log", + "mio", + "socket-pktinfo", + "socket2", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "muldiv" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956787520e75e9bd233246045d19f42fb73242759cc57fba9611d940ae96d4b0" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "option-operations" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aca39cf52b03268400c16eeb9b56382ea3c3353409309b63f5c8f0b1faf42754" +dependencies = [ + "pastey", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.22.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d800d8d0de2ad5d0fb046f5344dbaba14a003cf3dd27cc21d85893d35ea316c" +dependencies = [ + "gio", + "glib", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pastey" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee67f1008b1ba2321834326597b8e186293b049a023cdef258527550b9935b4" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.13+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-codegen" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d3976825c0014bbd2f3b34f0001876604fe87e0c86cd8fa54251530f1544ace" +dependencies = [ + "anyhow", + "once_cell", + "protobuf", + "protobuf-parse", + "regex", + "tempfile", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-parse" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4aeaa1f2460f1d348eeaeed86aea999ce98c1bded6f089ff8514c9d9dbdc973" +dependencies = [ + "anyhow", + "indexmap", + "log", + "protobuf", + "protobuf-support", + "tempfile", + "thiserror 1.0.69", + "which", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "roxmltree" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" + +[[package]] +name = "rupnp" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adade717fef50ae742ee4341495dfb1f78b2bc39e50ad8a6d485ffb81c1edbf6" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "genawaiter", + "http", + "http-body-util", + "hyper", + "hyper-util", + "if-addrs 0.13.4", + "roxmltree", + "ssdp-client", + "tokio", +] + +[[package]] +name = "rust_cast" +version = "0.21.0" +dependencies = [ + "byteorder", + "log", + "protobuf", + "protobuf-codegen", + "rustls", + "rustls-native-certs", + "serde", + "serde_json", + "thiserror 2.0.19", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +dependencies = [ + "aws-lc-rs", + "log", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket-pktinfo" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612942246d0cc239cfd83af1dfd39be47f649208a3524e5e9da651910128e0ac" +dependencies = [ + "libc", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" +dependencies = [ + "lock_api", +] + +[[package]] +name = "ssdp-client" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f4048b05716cdb270e1e5960d9dd4d428e7ceaa939ee5348a84f6e43d52df1" +dependencies = [ + "futures-core", + "genawaiter", + "log", + "tokio", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "system-deps" +version = "7.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7" +dependencies = [ + "cfg-expr", + "heck", + "pkg-config", + "toml 1.1.4+spec-1.1.0", + "version-compare", +] + +[[package]] +name = "target-lexicon" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl 2.0.19", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiny_http" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389915df6413a2e74fb181895f933386023c71110878cd0825588928e64cdc82" +dependencies = [ + "ascii", + "chunked_transfer", + "httpdate", + "log", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml" +version = "1.1.4+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.4", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.4", +] + +[[package]] +name = "toml_parser" +version = "1.1.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" +dependencies = [ + "winnow 1.0.4", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "toml_writer" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "zbus" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe18fb60dc696039e738717b76eaea21e7a4489bbb1885020b43c94236d7e98a" +dependencies = [ + "async-broadcast", + "async-recursion", + "async-trait", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tokio", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.4", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe96480bed92df2b442a1a30df364e12d08eed03aeb061f2b8dc6afb2be91119" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8bf88b4a3ff53e883001e0e0115b297a9d53c31b9c1edd2bfdd853e3428624e" +dependencies = [ + "serde", + "winnow 1.0.4", + "zvariant", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zvariant" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee2a0bcd2a907786a456fff45aaaaf54c9ba5f50b71ae9ec1a4edd200c94911" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.4", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a708216a18780796770bfe3f4739c7c83a3e8f789b755534bbbc06e4e23e12" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90cb9383f9b45290407a1258b202d3f8f01db719eb60b4e4055c6375af4fc7c7" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.119", + "winnow 1.0.4", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..87633a3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,28 @@ +[workspace] +members = ["breadcast-core", "breadcastd", "breadcast", "breadcast-caststream-sys"] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2021" +license = "MIT" +authors = ["Breadway "] + +[workspace.dependencies] +anyhow = "1" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tracing = "0.1" +tracing-subscriber = "0.3" +tokio = { version = "1", features = ["rt-multi-thread", "net", "sync", "time", "macros", "io-util"] } + +[profile.release] +lto = "thin" +codegen-units = 1 +strip = "symbols" + +# Adds CastDevice::send_message() -- a point-to-point send on an arbitrary +# namespace, needed for the Cast Streaming OFFER/ANSWER exchange. See +# vendor/rust_cast-0.21.0/PATCHES.md. +[patch.crates-io] +rust_cast = { path = "vendor/rust_cast-0.21.0" } diff --git a/EVENTS.md b/EVENTS.md new file mode 100644 index 0000000..d198d1a --- /dev/null +++ b/EVENTS.md @@ -0,0 +1,44 @@ +# breadcast — bread event integration + +breadcast is a standalone screen-mirroring app: it works exactly the same +with or without `breadd` running. When breadd *is* present, `breadcastd` +publishes events into the shared bread automation fabric and listens for a +small set of commands. See the parent `bread` repo's `Documentation.md` — +specifically its "Namespaces" and "Integrating a bread\* app" sections — for +the general convention this follows. + +App id: **`cast`**. Transport: `bread-utils`'s `bread_client` module +(feature `bread-client`) — `breadcastd` links it directly, since it's a +long-running process for both the command-subscription half and the +discovery-driven emit half. + +## Events published (`bread.cast.*`) + +| Event | Data | When | +|-------|------|------| +| `bread.cast.device_found` | `{ "id": "", "name": "", "model": "", "protocol": "cast" \| "dlna" }` | A Chromecast/Google TV (mDNS) or DLNA/UPnP media renderer (SSDP) is discovered (or re-resolved) on the LAN. Fires on every re-resolution, not just the first sighting — treat it as an upsert keyed by `id`, not an append-only log. `id` is protocol-specific and only unique *within* a protocol — a Cast device's mDNS id and a DLNA device's description URL share no namespace. | +| `bread.cast.mirroring_started` | `{ "device_id": "", "device_name": "", "protocol": "cast" \| "dlna" }` | A mirroring session successfully started, whether triggered by `bread.command.cast.start`, the `breadcast` GTK popup, or (once wired) any other IPC client. | +| `bread.cast.mirroring_stopped` | `{}` | A mirroring session ended, whether via an explicit stop (command, IPC, or GTK popup) or unprompted (the portal picker's "stop sharing", the receiver dropping the connection, a DLNA renderer stopping playback from its own remote). There is no separate "stopped by whom" distinction in this event — `breadcastd`'s own logs have that detail if needed. | +| `bread.cast.mirroring_failed` | `{ "device_id": "", "error": "" }` | A `start_cast`/`bread.command.cast.start` attempt failed before a session was established (device unreachable, portal capture denied, negotiation timeout, renderer rejected the stream, etc). | + +## Commands honored (`bread.command.cast.*`) + +| Command | Data | Effect | +|---------|------|--------| +| `bread.command.cast.start` | `{ "device_id": "" }` | Starts mirroring to the given device (looked up across both the Cast and DLNA device lists — same `device_id` a `list_devices`/`device_list_changed` payload reports). Fire-and-forget: the outcome shows up as `bread.cast.mirroring_started`/`.failed`, not a reply to this command. No-ops (logged) if already casting. | +| `bread.command.cast.stop` | none | Stops the active mirroring session, if any. No-ops if already idle. | + +Both commands are handled identically regardless of whether the session was +started from here, the `breadcast` GTK popup, or the IPC socket directly — +`breadcastd`'s daemon actor has exactly one notion of "the active session" +(see `breadcastd/src/daemon.rs`), not one per control surface. + +## Fail-safe behavior + +- If breadd isn't installed or isn't running, `emit` is a silent no-op + (`BreadClient::emit` never blocks or errors the caller) and the + command subscription simply never receives anything — breadcastd's actual + discovery functionality is entirely unaffected either way. +- If breadd restarts, the command subscription reconnects automatically + (`BreadClient::subscribe`'s background thread has its own backoff loop); + no restart of breadcastd is needed. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..373e4ee --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Breadway + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..62a4451 --- /dev/null +++ b/README.md @@ -0,0 +1,97 @@ +# breadcast + +Cast your screen to any Chromecast/Google TV, for Hyprland. It consists of +two binaries: + +- **`breadcastd`** — a background daemon that discovers Cast devices on the + LAN and (once built — see Status) owns the screen-capture/encode/serve + pipeline and the live Cast V2 session. +- **`breadcast`** — a GTK4 Layer Shell popup for picking a device and + starting/stopping a cast. + +## Status + +This is early: device discovery (mDNS) and the Cast V2 sender (real device +control — connect, launch the receiver, load media) are built and validated +against real Chromecast/Google TV hardware. The actual screen-mirroring +pipeline (portal-based screen capture → GPU-accelerated encode → HLS → +local HTTP server) and the GTK4 device-picker UI are not built yet. +`breadcastd` today only does discovery and optional breadd event +publishing; `breadcast` is a stub binary. See `CLAUDE.md`'s Status section +for more detail. + +## Requirements + +- Rust toolchain (edition 2021) +- GTK 4.12+ and `gtk4-layer-shell` (once the UI lands) +- GStreamer + `gst-plugin-pipewire`, `gst-plugins-bad` (VA-API `va` plugin), + `gst-plugin-hlssink3` (once the capture pipeline lands) +- Hyprland (or any Wayland compositor with Layer Shell and the + `xdg-desktop-portal` ScreenCast interface) + +## Build + +```sh +git clone https://git.breadway.dev/breadway/breadcast +cd breadcast +cargo build --release +``` + +The compiled binaries are at `target/release/breadcast` and +`target/release/breadcastd`. + +## Try device discovery today + +```sh +cargo run -p breadcast-core --example discover +``` + +Prints Chromecast/Google TV devices as they appear/disappear on the LAN. + +## Install + +Copy the binaries to somewhere on your `$PATH`, e.g.: + +```sh +cp target/release/breadcast target/release/breadcastd ~/.local/bin/ +``` + +### systemd user service + +A unit file is provided in `contrib/`: + +```sh +cp contrib/breadcastd.service ~/.config/systemd/user/ +systemctl --user daemon-reload +systemctl --user enable --now breadcastd +``` + +### Hyprland keybind + +For stock Hyprland, add the contents of `contrib/hyprland.conf` to your +`hyprland.conf`: + +``` +layerrule = blur, breadcast +layerrule = ignorezero, breadcast + +bind = $mainMod, C, exec, breadcast +``` + +On BOS, Hyprland config is Lua+JSON-driven instead — see +`contrib/binds.json` for the equivalent keybind entry to merge into your +`binds.json` by hand (there's no per-app self-registration mechanism yet). +There is currently no BOS-native equivalent for the `layerrule` blur lines. + +## bread event integration + +`breadcastd` optionally publishes into the shared bread automation fabric +(`bread.cast.*`) when `breadd` is running, and works identically without +it. See `EVENTS.md` for the full, honest-about-scope contract — most of the +eventually-planned events (mirroring start/stop) aren't implemented yet, +since the mirroring pipeline itself isn't built yet. + +## Theming + +`breadcast` will inherit its colour palette from `bread-theme`, matching +the rest of the ecosystem, once its GTK4 UI is built. diff --git a/bakery.toml b/bakery.toml new file mode 100644 index 0000000..4ffac28 --- /dev/null +++ b/bakery.toml @@ -0,0 +1,34 @@ +name = "breadcast" +description = "Cast your screen to any Chromecast/Google TV or DLNA renderer — daemon + GTK4 popup" +binaries = ["breadcast", "breadcastd"] +# gst-plugin-va: the `vah264enc` element both encode pipelines use (see +# breadcast-core/src/pipeline/mod.rs) -- a separate Arch package from +# gst-plugins-bad itself, not bundled into it. jsoncpp/openssl: runtime +# shared-library deps of breadcastd itself (not just a build dep of +# breadcast-caststream-sys), confirmed via `ldd target/release/breadcastd` +# showing libjsoncpp.so/libcrypto.so -- the vendored openscreen Cast +# Streaming code (see breadcast-caststream-sys/vendor/openscreen) links +# dynamically against the system's jsoncpp/libcrypto rather than vendoring +# them too. +system_deps = [ + "gtk4", + "gtk4-layer-shell", + "gstreamer", + "gst-plugin-pipewire", + "gst-plugins-bad", + "gst-plugin-hlssink3", + "gst-plugin-va", + "jsoncpp", + "openssl", +] +optional_system_deps = ["hyprland", "xdg-desktop-portal-hyprland"] +bread_deps = [] + +[[service]] +unit = "breadcastd.service" +enable = true + +[install] +post_install = [ + "systemctl --user is-active --quiet breadcastd || systemctl --user start breadcastd", +] diff --git a/breadcast-caststream-sys/Cargo.toml b/breadcast-caststream-sys/Cargo.toml new file mode 100644 index 0000000..0e75299 --- /dev/null +++ b/breadcast-caststream-sys/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "breadcast-caststream-sys" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +description = "Low-level FFI bindings to a pruned, vendored openscreen Cast Streaming sender (see vendor/openscreen/PATCHES.md)" +links = "breadcast_caststream" +build = "build.rs" + +[build-dependencies] +cc = "1" +pkg-config = "0.3" + +[dev-dependencies] +tracing-subscriber = { workspace = true } diff --git a/breadcast-caststream-sys/build.rs b/breadcast-caststream-sys/build.rs new file mode 100644 index 0000000..0c21d86 --- /dev/null +++ b/breadcast-caststream-sys/build.rs @@ -0,0 +1,60 @@ +use std::path::{Path, PathBuf}; + +fn collect_cc_files(dir: &Path, out: &mut Vec) { + for entry in std::fs::read_dir(dir).unwrap_or_else(|e| { + panic!("failed to read {}: {e}", dir.display()); + }) { + let entry = entry.unwrap(); + let path = entry.path(); + if path.is_dir() { + collect_cc_files(&path, out); + } else if path.extension().is_some_and(|ext| ext == "cc") { + out.push(path); + } + } +} + +fn main() { + let vendor = Path::new("vendor/openscreen"); + let jsoncpp = pkg_config::probe_library("jsoncpp") + .expect("jsoncpp not found (pacman: jsoncpp / apt: libjsoncpp-dev)"); + // Just libcrypto, not the full "openssl" .pc -- this vendored subset of + // openscreen never touches libssl (no TLS; see vendor/openscreen/PATCHES.md). + let libcrypto = pkg_config::probe_library("libcrypto").expect("libcrypto not found"); + + let mut sources = Vec::new(); + collect_cc_files(vendor, &mut sources); + sources.push(PathBuf::from("src/message_port_bridge.cc")); + sources.push(PathBuf::from("src/session.cc")); + sources.push(PathBuf::from("src/facade.cc")); + + let mut build = cc::Build::new(); + build + .cpp(true) + .std("c++20") + .include(vendor) + .include("src") + // Force-included into every translation unit -- see + // vendor/openscreen/patches/compat_shims.h for what this papers over + // (a couple of missing includes and BoringSSL-only APIs system + // OpenSSL doesn't expose). + .flag(format!("-include{}", vendor.join("patches/compat_shims.h").display())) + // This is a vendored, pruned third-party subset (see + // vendor/openscreen/PATCHES.md) -- warnings in it aren't + // breadcast's to fix, and upstream builds it with -w itself for the + // same reason (see BoringSSL's "internal_config" in its own BUILD.gn). + .warnings(false); + + for path in jsoncpp.include_paths.iter().chain(libcrypto.include_paths.iter()) { + build.include(path); + } + + for source in &sources { + println!("cargo:rerun-if-changed={}", source.display()); + build.file(source); + } + println!("cargo:rerun-if-changed=src/facade.h"); + println!("cargo:rerun-if-changed=vendor/openscreen/PATCHES.md"); + + build.compile("breadcast_caststream"); +} diff --git a/breadcast-caststream-sys/src/facade.cc b/breadcast-caststream-sys/src/facade.cc new file mode 100644 index 0000000..753c231 --- /dev/null +++ b/breadcast-caststream-sys/src/facade.cc @@ -0,0 +1,297 @@ +#include "facade.h" + +#include +#include +#include +#include +#include + +#include "cast/streaming/public/encoded_frame.h" +#include "cast/streaming/public/environment.h" +#include "cast/streaming/rtp_time.h" +#include "message_port_bridge.h" +#include "platform/api/time.h" +#include "platform/base/ip_address.h" +#include "platform/impl/platform_client_posix.h" +#include "platform/impl/task_runner.h" +#include "session.h" + +namespace { + +// How often to refresh the atomics polled by +// breadcast_caststream_sender_{needs_key_frame,estimated_bandwidth_bps}. +// These are advisory (encoder bitrate/keyframe hints), so a small amount of +// staleness is fine -- this just needs to be fast enough not to be the +// bottleneck in reacting to network conditions. +constexpr std::chrono::milliseconds kPollInterval(100); + +// Used as the pre-negotiation default so the very first frames (sent before +// any RTCP feedback exists to compute a real estimate from) aren't wildly +// over-encoded. Conservative middle-of-the-road value; real estimates from +// BandwidthEstimator::ComputeNetworkBandwidth() take over once available. +constexpr int32_t kDefaultBandwidthEstimateBps = 2 * 1000 * 1000; + +} // namespace + +struct CastStreamSender { + std::unique_ptr environment; + std::unique_ptr message_port; + std::unique_ptr session; + + // Set once, the first time a frame is enqueued after negotiation + // completes; used to compute monotonically-increasing RTP timestamps + // relative to session start. + bool have_origin = false; + int64_t origin_capture_time_us = 0; + + // negotiated uses acquire/release so that once + // breadcast_caststream_sender_enqueue_frame observes it true (from an + // arbitrary caller thread), `session->video_sender()` is guaranteed + // already set -- see OnNegotiatedTrampoline below. + std::atomic negotiated{false}; + std::atomic needs_key_frame{true}; + std::atomic estimated_bandwidth_bps{kDefaultBandwidthEstimateBps}; + + // The caller's own user_data + callbacks, as passed to `_create`. Not + // called directly -- session.h/message_port_bridge.h are instead given + // trampolines below (with `this` as their user_data) so this struct can + // update its own bookkeeping (e.g. `negotiated`) before forwarding. + void* rust_user_data = nullptr; + BreadcastOnNegotiatedFn rust_on_negotiated = nullptr; + BreadcastOnErrorFn rust_on_error = nullptr; + BreadcastOnPictureLostFn rust_on_picture_lost = nullptr; + + void SchedulePoll() { + environment->task_runner().PostTaskWithDelay( + [this] { + if (session && session->video_sender()) { + needs_key_frame.store(session->video_sender()->NeedsKeyFrame(), + std::memory_order_relaxed); + const int bps = session->GetEstimatedBandwidthBps(); + if (bps > 0) { + estimated_bandwidth_bps.store(bps, std::memory_order_relaxed); + } + } + SchedulePoll(); + }, + kPollInterval); + } +}; + +namespace { + +void OnNegotiatedTrampoline(void* user_data) { + auto* sender = static_cast(user_data); + sender->negotiated.store(true, std::memory_order_release); + if (sender->rust_on_negotiated) { + sender->rust_on_negotiated(sender->rust_user_data); + } +} + +void OnErrorTrampoline(void* user_data, const char* message, size_t message_len) { + auto* sender = static_cast(user_data); + if (sender->rust_on_error) { + sender->rust_on_error(sender->rust_user_data, message, message_len); + } +} + +void OnPictureLostTrampoline(void* user_data) { + auto* sender = static_cast(user_data); + if (sender->rust_on_picture_lost) { + sender->rust_on_picture_lost(sender->rust_user_data); + } +} + +} // namespace + +extern "C" { + +CastStreamSender* breadcast_caststream_sender_create( + const char* remote_ip, + size_t remote_ip_len, + const char* local_source_id, + size_t local_source_id_len, + const char* receiver_id, + size_t receiver_id_len, + int32_t width, + int32_t height, + int32_t max_bitrate_bps, + int32_t max_frame_rate_numerator, + int32_t max_frame_rate_denominator, + void* user_data, + BreadcastPostMessageFn post_message, + BreadcastOnNegotiatedFn on_negotiated, + BreadcastOnErrorFn on_error, + BreadcastOnPictureLostFn on_picture_lost) { + auto address_result = + openscreen::IPAddress::Parse(std::string(remote_ip, remote_ip_len)); + if (!address_result) { + return nullptr; + } + + auto handle = std::make_unique(); + handle->rust_user_data = user_data; + handle->rust_on_negotiated = on_negotiated; + handle->rust_on_error = on_error; + handle->rust_on_picture_lost = on_picture_lost; + + // Spins up openscreen's TaskRunner + networking threads. Safe to call more + // than once process-wide only if ShutDown() was called first -- breadcast + // only ever has one active cast-streaming session at a time, so this + // assumption (baked into PlatformClientPosix's own singleton design) holds. + openscreen::PlatformClientPosix::Create(std::chrono::milliseconds(50)); + openscreen::TaskRunner& task_runner = + openscreen::PlatformClientPosix::GetInstance()->GetTaskRunner(); + + breadcast_caststream::VideoParams params; + params.width = width; + params.height = height; + params.max_bit_rate = max_bitrate_bps; + params.max_frame_rate_numerator = max_frame_rate_numerator; + params.max_frame_rate_denominator = max_frame_rate_denominator; + + breadcast_caststream::SessionCallbacks callbacks; + callbacks.user_data = handle.get(); + callbacks.on_negotiated = &OnNegotiatedTrampoline; + callbacks.on_error = &OnErrorTrampoline; + callbacks.on_picture_lost = &OnPictureLostTrampoline; + + const openscreen::IPAddress remote_address = address_result.value(); + std::string local_source_id_str(local_source_id, local_source_id_len); + std::string receiver_id_str(receiver_id, receiver_id_len); + + // Environment's constructor synchronously creates and binds a UdpSocket, + // whose posix implementation asserts it's only ever touched from the + // TaskRunner thread (see udp_socket_posix.cc) -- so construction has to + // happen there too, not on this (arbitrary caller's) thread. + CastStreamSender* handle_ptr = handle.get(); + std::promise constructed; + std::future constructed_future = constructed.get_future(); + task_runner.PostTask([handle_ptr, &task_runner, ¶ms, &callbacks, remote_address, + local_source_id_str, receiver_id_str, post_message, user_data, + &constructed] { + handle_ptr->environment = + std::make_unique(&openscreen::Clock::now, task_runner); + handle_ptr->message_port = + std::make_unique(user_data, post_message); + handle_ptr->session = std::make_unique( + *handle_ptr->environment, *handle_ptr->message_port, remote_address, + local_source_id_str, receiver_id_str, params, callbacks); + constructed.set_value(); + }); + constructed_future.wait(); + + return handle.release(); +} + +void breadcast_caststream_sender_negotiate(CastStreamSender* sender) { + sender->environment->task_runner().PostTask([sender] { + sender->session->Negotiate(); + sender->SchedulePoll(); + }); +} + +void breadcast_caststream_sender_on_message(CastStreamSender* sender, + const char* source_id, + size_t source_id_len, + const char* message_namespace, + size_t message_namespace_len, + const char* message, + size_t message_len) { + auto source = std::make_shared(source_id, source_id_len); + auto ns = std::make_shared(message_namespace, message_namespace_len); + auto body = std::make_shared(message, message_len); + sender->environment->task_runner().PostTask([sender, source, ns, body] { + sender->message_port->DeliverMessage(*source, *ns, *body); + }); +} + +int32_t breadcast_caststream_sender_enqueue_frame(CastStreamSender* sender, + const uint8_t* data, + size_t data_len, + int32_t is_key_frame, + int64_t capture_time_us) { + if (!sender->negotiated.load(std::memory_order_acquire)) { + return -1; + } + + // Copied here (not captured as a borrowed span) because PostTask defers + // execution -- `data` is only guaranteed valid for the duration of this + // call, per facade.h's documented contract. + auto owned_data = std::make_shared>(data, data + data_len); + const bool is_key = is_key_frame != 0; + + sender->environment->task_runner().PostTask([sender, owned_data, is_key, capture_time_us] { + using namespace openscreen; + using namespace openscreen::cast; + + Sender* video_sender = sender->session->video_sender(); + if (!video_sender) { + return; + } + + if (!sender->have_origin) { + sender->have_origin = true; + sender->origin_capture_time_us = capture_time_us; + } + + const FrameId frame_id = video_sender->GetNextFrameId(); + const FrameId referenced_frame_id = + is_key || frame_id == FrameId::first() ? frame_id : frame_id - 1; + + // breadcast's encoder (vah264enc via GStreamer) is assumed to produce a + // simple linear IPPP GOP structure (no B-frames / no multi-reference), + // so "depends on the immediately preceding frame" is a correct + // reference, not just an approximation. + const int64_t elapsed_us = capture_time_us - sender->origin_capture_time_us; + const RtpTimeTicks rtp_timestamp = RtpTimeTicks::FromTimeSinceOrigin( + std::chrono::microseconds(elapsed_us), video_sender->config().rtp_timebase); + + const EncodedFrame::Dependency dependency = + is_key ? EncodedFrame::Dependency::kKeyFrame : EncodedFrame::Dependency::kDependent; + EncodedFrame frame(dependency, frame_id, referenced_frame_id, rtp_timestamp, Clock::now(), + /*new_playout_delay=*/std::chrono::milliseconds::zero(), + ByteView(owned_data->data(), owned_data->size())); + + // EnqueueFrame()'s result (e.g. MAX_DURATION_IN_FLIGHT under backpressure) + // isn't propagated to the caller: by the time this runs, enqueue_frame() + // has already returned 0 synchronously (this call is posted, not + // immediate -- see facade.h's threading contract). Backpressure here just + // means this one frame is dropped; the encoder finds out indirectly via + // needs_key_frame()/estimated_bandwidth_bps() polling. + (void)video_sender->EnqueueFrame(frame); + }); + + return 0; +} + +int32_t breadcast_caststream_sender_needs_key_frame(CastStreamSender* sender) { + return sender->needs_key_frame.load(std::memory_order_relaxed) ? 1 : 0; +} + +int32_t breadcast_caststream_sender_estimated_bandwidth_bps(CastStreamSender* sender) { + return sender->estimated_bandwidth_bps.load(std::memory_order_relaxed); +} + +void breadcast_caststream_sender_destroy(CastStreamSender* sender) { + if (!sender) { + return; + } + // These must be torn down on the TaskRunner thread (they hold raw + // references into it and into `environment`), so hop over there and block + // until it's done before shutting the TaskRunner itself down. + std::promise done; + std::future done_future = done.get_future(); + sender->environment->task_runner().PostTask([sender, &done] { + sender->session.reset(); + sender->message_port.reset(); + sender->environment.reset(); + done.set_value(); + }); + done_future.wait(); + + openscreen::PlatformClientPosix::ShutDown(); + delete sender; +} + +} // extern "C" diff --git a/breadcast-caststream-sys/src/facade.h b/breadcast-caststream-sys/src/facade.h new file mode 100644 index 0000000..f472d24 --- /dev/null +++ b/breadcast-caststream-sys/src/facade.h @@ -0,0 +1,123 @@ +// The extern "C" surface breadcast-caststream-sys's build.rs compiles and +// the Rust side (src/lib.rs) declares `extern "C"` bindings for. +// +// Threading contract: `breadcast_caststream_sender_create` spins up +// openscreen's own TaskRunner + networking threads internally (via +// PlatformClientPosix) -- callers don't manage those. All +// `breadcast_caststream_sender_*` functions taking a `CastStreamSender*` are +// safe to call from any single thread (they internally marshal onto the +// TaskRunner thread via TaskRunner::PostTask, which is documented +// thread-safe) -- but the *callbacks* passed to `_create` fire FROM that +// TaskRunner thread, not the caller's thread. This mirrors the existing +// single-io-thread actor pattern breadcast-core's CastSession already uses +// for CASTV2: the Rust wrapper is expected to run one dedicated thread that +// owns the CastStreamSender and treats callback invocations as arriving from +// a foreign thread (e.g. hands them off over an mpsc channel), exactly like +// CastSession's io loop already does for rust_cast's own callbacks. +#ifndef BREADCAST_CASTSTREAM_FACADE_H_ +#define BREADCAST_CASTSTREAM_FACADE_H_ + +#include +#include + +extern "C" { + +typedef struct CastStreamSender CastStreamSender; + +// Forwards an outbound OFFER/ANSWER-exchange message to Rust for sending +// over the existing CASTV2 urn:x-cast:com.google.cast.webrtc channel. All +// buffers are borrowed for the duration of the call only. +typedef void (*BreadcastPostMessageFn)(void* user_data, + const char* destination_id, + size_t destination_id_len, + const char* message_namespace, + size_t message_namespace_len, + const char* message, + size_t message_len); + +// Fired once OFFER/ANSWER negotiation succeeds and the video sender is +// ready to accept frames. +typedef void (*BreadcastOnNegotiatedFn)(void* user_data); + +// Fired on a negotiation or session error. `message` is borrowed for the +// duration of the call only. +typedef void (*BreadcastOnErrorFn)(void* user_data, + const char* message, + size_t message_len); + +// Fired when the receiver reports picture loss and wants a key frame ASAP +// (a push notification; see also breadcast_caststream_sender_needs_key_frame +// for the pull-style equivalent, which also catches this condition). +typedef void (*BreadcastOnPictureLostFn)(void* user_data); + +// Creates a session and starts openscreen's TaskRunner/networking threads. +// `remote_ip` is the receiver's IP address (the same one rust_cast already +// connected to for the CASTV2 control channel); `local_source_id`/ +// `receiver_id` are the CASTV2 source/destination IDs to use when sending +// messages over the webrtc namespace (already known to the Rust caller from +// its existing CASTV2 session). Returns null on failure (e.g. invalid IP or +// failure to bind the local UDP socket). +CastStreamSender* breadcast_caststream_sender_create( + const char* remote_ip, + size_t remote_ip_len, + const char* local_source_id, + size_t local_source_id_len, + const char* receiver_id, + size_t receiver_id_len, + int32_t width, + int32_t height, + int32_t max_bitrate_bps, + int32_t max_frame_rate_numerator, + int32_t max_frame_rate_denominator, + void* user_data, + BreadcastPostMessageFn post_message, + BreadcastOnNegotiatedFn on_negotiated, + BreadcastOnErrorFn on_error, + BreadcastOnPictureLostFn on_picture_lost); + +// Sends the OFFER and begins waiting for an ANSWER. +void breadcast_caststream_sender_negotiate(CastStreamSender* sender); + +// Delivers a message received on the webrtc namespace (e.g. the ANSWER) +// into the session. All buffers are copied before this returns. +void breadcast_caststream_sender_on_message(CastStreamSender* sender, + const char* source_id, + size_t source_id_len, + const char* message_namespace, + size_t message_namespace_len, + const char* message, + size_t message_len); + +// Enqueues one encoded video access unit (Annex-B H.264) for sending. +// `data` is copied before this returns, so the caller may reuse/free its +// buffer immediately after. `capture_time_us` is only used to derive the +// RTP timestamp's relative spacing between frames (it does not need to be +// wall-clock-accurate, just monotonically increasing and proportional to +// real elapsed time between frames). Returns 0 if queued, nonzero if the +// session isn't negotiated yet or the frame was rejected (e.g. too large, +// or the in-flight queue is full -- the caller should back off encoding +// when this happens rather than treating it as fatal). +int32_t breadcast_caststream_sender_enqueue_frame(CastStreamSender* sender, + const uint8_t* data, + size_t data_len, + int32_t is_key_frame, + int64_t capture_time_us); + +// True (nonzero) if the receiver wants a key frame as soon as possible. +// Safe to poll frequently; cheap, non-blocking, lock-free. +int32_t breadcast_caststream_sender_needs_key_frame(CastStreamSender* sender); + +// Best-effort current bandwidth estimate in bits per second. Safe to poll +// frequently; cheap, non-blocking, lock-free. Intended to drive the video +// encoder's target bitrate (this vendored subset of openscreen only does +// flow control, not congestion control -- see Sender's class comment in +// vendor/openscreen/cast/streaming/public/sender.h). +int32_t breadcast_caststream_sender_estimated_bandwidth_bps(CastStreamSender* sender); + +// Tears down the session and stops openscreen's internal threads. Blocks +// until shutdown completes. +void breadcast_caststream_sender_destroy(CastStreamSender* sender); + +} // extern "C" + +#endif // BREADCAST_CASTSTREAM_FACADE_H_ diff --git a/breadcast-caststream-sys/src/lib.rs b/breadcast-caststream-sys/src/lib.rs new file mode 100644 index 0000000..cefd9e3 --- /dev/null +++ b/breadcast-caststream-sys/src/lib.rs @@ -0,0 +1,133 @@ +//! Raw FFI bindings to `src/facade.h`/`src/facade.cc`, which wrap a pruned, +//! vendored subset of `chromium/openscreen`'s Cast Streaming sender (see +//! `vendor/openscreen/PATCHES.md`). This crate is intentionally low-level and +//! unsafe -- see `breadcast-caststream` (not this crate) for the ergonomic, +//! thread-safe wrapper most callers should use instead. +//! +//! # Threading contract +//! +//! `sender_create` spins up openscreen's own TaskRunner + networking threads +//! internally; callers don't manage those. Every `sender_*` function taking +//! a `*mut CastStreamSender` is safe to call from any thread (calls are +//! internally marshaled onto the TaskRunner thread). The callbacks passed to +//! `sender_create`, however, fire FROM that TaskRunner thread, not the +//! caller's thread -- see `facade.h`'s doc comment for the full contract, +//! which mirrors the single-io-thread actor pattern breadcast-core's +//! `CastSession` already uses for CASTV2. + +use std::ffi::{c_char, c_void}; + +#[repr(C)] +pub struct CastStreamSender { + _private: [u8; 0], +} + +// Safety: every function below is documented (facade.h) as safe to call +// from any thread; only the callbacks fire cross-thread, and those are +// plain `extern "C" fn` pointers rather than captured state, so there is no +// non-Send/Sync data hanging off `*mut CastStreamSender` itself. +unsafe impl Send for CastStreamSender {} + +pub type PostMessageFn = extern "C" fn( + user_data: *mut c_void, + destination_id: *const c_char, + destination_id_len: usize, + message_namespace: *const c_char, + message_namespace_len: usize, + message: *const c_char, + message_len: usize, +); + +pub type OnNegotiatedFn = extern "C" fn(user_data: *mut c_void); + +pub type OnErrorFn = + extern "C" fn(user_data: *mut c_void, message: *const c_char, message_len: usize); + +pub type OnPictureLostFn = extern "C" fn(user_data: *mut c_void); + +unsafe extern "C" { + /// Returns null on failure (e.g. an unparseable `remote_ip`, or the + /// local UDP socket failed to bind). + /// + /// # Safety + /// `remote_ip`/`local_source_id`/`receiver_id` must each point to + /// `_len` valid, readable bytes for the duration of this call. + /// `post_message`/`on_negotiated`/`on_error`/`on_picture_lost` must be + /// valid to call for as long as the returned sender is alive (i.e. + /// until `sender_destroy` returns). `user_data` is passed back + /// unmodified to every callback and may be null. + pub fn breadcast_caststream_sender_create( + remote_ip: *const c_char, + remote_ip_len: usize, + local_source_id: *const c_char, + local_source_id_len: usize, + receiver_id: *const c_char, + receiver_id_len: usize, + width: i32, + height: i32, + max_bitrate_bps: i32, + max_frame_rate_numerator: i32, + max_frame_rate_denominator: i32, + user_data: *mut c_void, + post_message: PostMessageFn, + on_negotiated: OnNegotiatedFn, + on_error: OnErrorFn, + on_picture_lost: OnPictureLostFn, + ) -> *mut CastStreamSender; + + /// # Safety + /// `sender` must be a live pointer returned by `sender_create` and not + /// yet passed to `sender_destroy`. + pub fn breadcast_caststream_sender_negotiate(sender: *mut CastStreamSender); + + /// Delivers an inbound message (e.g. the receiver's ANSWER) received on + /// the CASTV2 `urn:x-cast:com.google.cast.webrtc` namespace into the + /// session. All buffers are copied before this returns. + /// + /// # Safety + /// `sender` must be live. `source_id`/`message_namespace`/`message` must + /// each point to `_len` valid, readable bytes for the duration of this + /// call only. + pub fn breadcast_caststream_sender_on_message( + sender: *mut CastStreamSender, + source_id: *const c_char, + source_id_len: usize, + message_namespace: *const c_char, + message_namespace_len: usize, + message: *const c_char, + message_len: usize, + ); + + /// Enqueues one encoded video access unit (Annex-B H.264) for sending. + /// Returns 0 if queued, nonzero if not negotiated yet. + /// + /// # Safety + /// `sender` must be live. `data` must point to `data_len` valid, + /// readable bytes for the duration of this call only (it is copied + /// before this returns). + pub fn breadcast_caststream_sender_enqueue_frame( + sender: *mut CastStreamSender, + data: *const u8, + data_len: usize, + is_key_frame: i32, + capture_time_us: i64, + ) -> i32; + + /// # Safety + /// `sender` must be live. + pub fn breadcast_caststream_sender_needs_key_frame(sender: *mut CastStreamSender) -> i32; + + /// # Safety + /// `sender` must be live. + pub fn breadcast_caststream_sender_estimated_bandwidth_bps( + sender: *mut CastStreamSender, + ) -> i32; + + /// Tears down the session and blocks until openscreen's internal + /// threads stop. `sender` must not be used again after this call. + /// + /// # Safety + /// `sender` must be a live pointer returned by `sender_create`, not + /// already passed to this function. + pub fn breadcast_caststream_sender_destroy(sender: *mut CastStreamSender); +} diff --git a/breadcast-caststream-sys/src/message_port_bridge.cc b/breadcast-caststream-sys/src/message_port_bridge.cc new file mode 100644 index 0000000..aac43c4 --- /dev/null +++ b/breadcast-caststream-sys/src/message_port_bridge.cc @@ -0,0 +1,35 @@ +#include "message_port_bridge.h" + +namespace breadcast_caststream { + +MessagePortBridge::MessagePortBridge(void* user_data, + PostMessageCallback post_message) + : user_data_(user_data), post_message_(post_message) {} + +MessagePortBridge::~MessagePortBridge() = default; + +void MessagePortBridge::DeliverMessage(const std::string& source_id, + const std::string& message_namespace, + const std::string& message) { + if (client_) { + client_->OnMessage(source_id, message_namespace, message); + } +} + +void MessagePortBridge::SetClient(Client& client) { + client_ = &client; +} + +void MessagePortBridge::ResetClient() { + client_ = nullptr; +} + +void MessagePortBridge::PostMessage(const std::string& destination_id, + const std::string& message_namespace, + const std::string& message) { + post_message_(user_data_, destination_id.data(), destination_id.size(), + message_namespace.data(), message_namespace.size(), + message.data(), message.size()); +} + +} // namespace breadcast_caststream diff --git a/breadcast-caststream-sys/src/message_port_bridge.h b/breadcast-caststream-sys/src/message_port_bridge.h new file mode 100644 index 0000000..eefc8cb --- /dev/null +++ b/breadcast-caststream-sys/src/message_port_bridge.h @@ -0,0 +1,58 @@ +// A openscreen::cast::MessagePort implementation that forwards outbound +// messages to a Rust-provided callback (which sends them over the existing +// rust_cast-managed CASTV2 TLS channel, in the +// urn:x-cast:com.google.cast.webrtc namespace) and accepts inbound messages +// via DeliverMessage(), called by Rust when a reply arrives on that +// channel. This is what lets openscreen's own SenderSessionMessenger/ +// SenderPacketRouter run OFFER/ANSWER negotiation without this crate +// needing its own TLS stack -- see ../vendor/openscreen/PATCHES.md. +#ifndef BREADCAST_CASTSTREAM_MESSAGE_PORT_BRIDGE_H_ +#define BREADCAST_CASTSTREAM_MESSAGE_PORT_BRIDGE_H_ + +#include +#include +#include + +#include "cast/common/public/message_port.h" + +namespace breadcast_caststream { + +// Matches the PostMessageFn typedef in facade.h. +using PostMessageCallback = void (*)(void* user_data, + const char* destination_id, + size_t destination_id_len, + const char* message_namespace, + size_t message_namespace_len, + const char* message, + size_t message_len); + +class MessagePortBridge final : public openscreen::cast::MessagePort { + public: + MessagePortBridge(void* user_data, PostMessageCallback post_message); + ~MessagePortBridge() override; + + // Called by facade.cc's FFI entry point when Rust has received a message + // on the webrtc namespace for us. Safe to call from any thread; the + // caller is responsible for making sure this only actually touches + // `client_` while running on the Environment's TaskRunner thread (facade.cc + // marshals this via TaskRunner::PostTask before calling here). + void DeliverMessage(const std::string& source_id, + const std::string& message_namespace, + const std::string& message); + + // openscreen::cast::MessagePort implementation. + void SetClient(Client& client) override; + void ResetClient() override; + void PostMessage(const std::string& destination_id, + const std::string& message_namespace, + const std::string& message) override; + + private: + void* const user_data_; + const PostMessageCallback post_message_; + Client* client_ = nullptr; +}; + +} // namespace breadcast_caststream + +#endif // BREADCAST_CASTSTREAM_MESSAGE_PORT_BRIDGE_H_ diff --git a/breadcast-caststream-sys/src/session.cc b/breadcast-caststream-sys/src/session.cc new file mode 100644 index 0000000..bde4770 --- /dev/null +++ b/breadcast-caststream-sys/src/session.cc @@ -0,0 +1,181 @@ +#include "session.h" + +#include + +#include "cast/streaming/impl/rtp_defines.h" +#include "cast/streaming/message_fields.h" +#include "cast/streaming/public/constants.h" +#include "cast/streaming/public/session_config.h" +#include "cast/streaming/sender_message.h" +#include "util/crypto/random_bytes.h" + +namespace breadcast_caststream { + +namespace { + +using openscreen::Error; +using openscreen::ErrorOr; +using openscreen::GenerateRandomBytes16; +using openscreen::cast::AudioStream; +using openscreen::cast::CastMode; +using openscreen::cast::GenerateSsrc; +using openscreen::cast::GetPayloadType; +using openscreen::cast::kMinVideoHeight; +using openscreen::cast::kMinVideoWidth; +using openscreen::cast::kRtpVideoTimebase; +using openscreen::cast::Offer; +using openscreen::cast::ReceiverMessage; +using openscreen::cast::Resolution; +using openscreen::cast::SenderMessage; +using openscreen::cast::SessionConfig; +using openscreen::cast::Stream; +using openscreen::cast::ToStreamType; +using openscreen::cast::VideoCodec; +using openscreen::cast::VideoStream; + +// breadcast always mirrors exactly one video stream at index 0 -- there is +// no audio stream in this integration (breadcast's capture pipeline is +// video-only), so the index scheme sender_session.cc uses for interleaving +// audio-then-video streams collapses to just "index 0 is the video stream." +constexpr int kVideoStreamIndex = 0; + +VideoStream BuildVideoStream(const VideoParams& params, + bool use_android_rtp_hack) { + Stream stream; + stream.index = kVideoStreamIndex; + stream.type = Stream::Type::kVideoSource; + stream.channels = 1; + stream.rtp_payload_type = GetPayloadType(VideoCodec::kH264, use_android_rtp_hack); + stream.ssrc = GenerateSsrc(/*higher_priority=*/false); + stream.target_delay = openscreen::cast::kDefaultTargetPlayoutDelay; + stream.aes_key = GenerateRandomBytes16(); + stream.aes_iv_mask = GenerateRandomBytes16(); + stream.receiver_rtcp_event_log = true; + stream.rtp_timebase = kRtpVideoTimebase; + + VideoStream video_stream; + video_stream.stream = std::move(stream); + video_stream.codec = VideoCodec::kH264; + video_stream.max_frame_rate = openscreen::SimpleFraction{ + params.max_frame_rate_numerator, params.max_frame_rate_denominator}; + video_stream.max_bit_rate = (params.max_bit_rate >= openscreen::cast::kDefaultVideoMinBitRate) + ? params.max_bit_rate + : openscreen::cast::kDefaultVideoMaxBitRate; + video_stream.resolutions.push_back(Resolution{ + std::max(params.width, kMinVideoWidth), std::max(params.height, kMinVideoHeight)}); + return video_stream; +} + +} // namespace + +MirroringSenderSession::MirroringSenderSession( + openscreen::cast::Environment& environment, + openscreen::cast::MessagePort& message_port, + openscreen::IPAddress remote_address, + std::string local_source_id, + std::string receiver_id, + VideoParams params, + SessionCallbacks callbacks) + : environment_(environment), + remote_address_(remote_address), + receiver_id_(std::move(receiver_id)), + params_(params), + callbacks_(callbacks), + messenger_( + message_port, + std::move(local_source_id), + receiver_id_, + [this](Error error) { ReportError(error.message()); }, + environment.task_runner()), + packet_router_(environment) {} + +MirroringSenderSession::~MirroringSenderSession() { + if (video_sender_) { + video_sender_->SetObserver(nullptr); + } +} + +void MirroringSenderSession::Negotiate() { + // use_android_rtp_hack defaults on upstream too (crbug.com/631828) -- + // Google TV devices are Android TV under the hood, so this is left on. + constexpr bool kUseAndroidRtpHack = true; + + Offer offer; + offer.cast_mode = CastMode::kMirroring; + offer.video_streams.push_back(BuildVideoStream(params_, kUseAndroidRtpHack)); + pending_offer_ = offer; + + const Error result = messenger_.SendRequest( + SenderMessage{SenderMessage::Type::kOffer, ++sequence_number_, + /*valid=*/true, std::move(offer)}, + ReceiverMessage::Type::kAnswer, + [this](ErrorOr message) { OnAnswer(std::move(message)); }); + if (!result.ok()) { + ReportError(result.message()); + } +} + +void MirroringSenderSession::OnAnswer(ErrorOr message) { + if (!message) { + ReportError(message.error().message()); + return; + } + if (!message.value().valid || message.value().type != ReceiverMessage::Type::kAnswer) { + ReportError("Receiver sent an invalid or unexpected ANSWER response"); + return; + } + + const auto& answer = std::get(message.value().body); + if (answer.send_indexes.empty() || answer.ssrcs.empty()) { + ReportError("ANSWER selected no streams"); + return; + } + + environment_.set_remote_endpoint( + openscreen::IPEndpoint{remote_address_, static_cast(answer.udp_port)}); + + const openscreen::cast::VideoStream& stream = pending_offer_.video_streams[0]; + const openscreen::cast::RtpPayloadType payload_type = stream.stream.rtp_payload_type; + + SessionConfig config{stream.stream.ssrc, + answer.ssrcs[0], + stream.stream.rtp_timebase, + stream.stream.channels, + stream.stream.target_delay, + stream.stream.aes_key, + stream.stream.aes_iv_mask, + /*is_pli_enabled=*/true, + ToStreamType(payload_type, /*use_android_rtp_hack=*/true)}; + if (!config.IsValid()) { + ReportError("Derived an invalid SessionConfig from the ANSWER"); + return; + } + + video_sender_ = std::make_unique( + environment_, packet_router_, std::move(config), payload_type); + video_sender_->SetObserver(this); + + if (callbacks_.on_negotiated) { + callbacks_.on_negotiated(callbacks_.user_data); + } +} + +int MirroringSenderSession::GetEstimatedBandwidthBps() const { + return packet_router_.ComputeNetworkBandwidth(); +} + +void MirroringSenderSession::OnFrameCanceled(openscreen::cast::FrameId) {} + +void MirroringSenderSession::OnPictureLost() { + if (callbacks_.on_picture_lost) { + callbacks_.on_picture_lost(callbacks_.user_data); + } +} + +void MirroringSenderSession::ReportError(const std::string& message) { + if (callbacks_.on_error) { + callbacks_.on_error(callbacks_.user_data, message.data(), message.size()); + } +} + +} // namespace breadcast_caststream diff --git a/breadcast-caststream-sys/src/session.h b/breadcast-caststream-sys/src/session.h new file mode 100644 index 0000000..46dd784 --- /dev/null +++ b/breadcast-caststream-sys/src/session.h @@ -0,0 +1,102 @@ +// A minimal, video-only Cast Streaming sender negotiation driver. +// +// This deliberately does NOT use openscreen's own +// cast::SenderSession -- that class bundles mirroring negotiation together +// with RPC/remoting/input support, which pulls in protobuf +// (input.pb.h/remoting.pb.h) for no benefit here (breadcast only ever does +// one-way video mirroring). Instead, this reimplements just the +// OFFER-building and ANSWER-handling logic SenderSession itself uses +// internally (see CreateMirroringOffer/StartNegotiation/SelectSenders in +// upstream's public/sender_session.cc), built directly on +// SenderSessionMessenger + Offer/Answer/SessionConfig + SenderImpl. See +// ../vendor/openscreen/PATCHES.md. +#ifndef BREADCAST_CASTSTREAM_SESSION_H_ +#define BREADCAST_CASTSTREAM_SESSION_H_ + +#include +#include +#include + +#include "cast/streaming/impl/sender_impl.h" +#include "cast/streaming/public/environment.h" +#include "cast/streaming/public/offer_messages.h" +#include "cast/streaming/public/receiver_message.h" +#include "cast/streaming/public/sender.h" +#include "cast/streaming/public/session_messenger.h" +#include "cast/streaming/sender_packet_router.h" +#include "platform/base/ip_address.h" + +namespace breadcast_caststream { + +struct VideoParams { + int width = 1920; + int height = 1080; + int max_bit_rate = 8 * 1000 * 1000; + int max_frame_rate_numerator = 30; + int max_frame_rate_denominator = 1; +}; + +struct SessionCallbacks { + void* user_data = nullptr; + void (*on_negotiated)(void* user_data) = nullptr; + void (*on_error)(void* user_data, const char* message, size_t message_len) = + nullptr; + void (*on_picture_lost)(void* user_data) = nullptr; +}; + +// Owns the OFFER/ANSWER exchange and, once negotiated, the resulting video +// Sender. All methods (other than the constructor) must be called on +// `environment`'s TaskRunner thread -- facade.cc is responsible for +// marshaling calls onto it via TaskRunner::PostTask, matching the threading +// contract the rest of openscreen's Environment/SenderPacketRouter/Sender +// classes already assume. +class MirroringSenderSession final : public openscreen::cast::Sender::Observer { + public: + MirroringSenderSession(openscreen::cast::Environment& environment, + openscreen::cast::MessagePort& message_port, + openscreen::IPAddress remote_address, + std::string local_source_id, + std::string receiver_id, + VideoParams params, + SessionCallbacks callbacks); + ~MirroringSenderSession() override; + + MirroringSenderSession(const MirroringSenderSession&) = delete; + MirroringSenderSession& operator=(const MirroringSenderSession&) = delete; + + // Sends the OFFER and begins waiting for an ANSWER. `callbacks.on_negotiated` + // or `callbacks.on_error` will be called once the exchange completes. + void Negotiate(); + + // Valid only after `on_negotiated` has fired. + openscreen::cast::Sender* video_sender() { return video_sender_.get(); } + + // Best-effort current bandwidth estimate in bits per second, or a + // conservative default before enough RTCP feedback has arrived. + int GetEstimatedBandwidthBps() const; + + // Sender::Observer implementation. + void OnFrameCanceled(openscreen::cast::FrameId frame_id) override; + void OnPictureLost() override; + + private: + void OnAnswer(openscreen::ErrorOr message); + void ReportError(const std::string& message); + + openscreen::cast::Environment& environment_; + const openscreen::IPAddress remote_address_; + const std::string receiver_id_; + const VideoParams params_; + const SessionCallbacks callbacks_; + + openscreen::cast::SenderSessionMessenger messenger_; + openscreen::cast::SenderPacketRouter packet_router_; + + int sequence_number_ = 0; + openscreen::cast::Offer pending_offer_; + std::unique_ptr video_sender_; +}; + +} // namespace breadcast_caststream + +#endif // BREADCAST_CASTSTREAM_SESSION_H_ diff --git a/breadcast-caststream-sys/tests/smoke.rs b/breadcast-caststream-sys/tests/smoke.rs new file mode 100644 index 0000000..55466de --- /dev/null +++ b/breadcast-caststream-sys/tests/smoke.rs @@ -0,0 +1,91 @@ +//! Exercises the raw FFI end-to-end from Rust: create a sender, kick off +//! negotiation, and confirm the C++ side calls back out through +//! `post_message` with a real Cast Streaming OFFER -- without needing an +//! actual receiver on the network (nothing here waits for an ANSWER). + +use std::ffi::c_void; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Mutex; +use std::time::{Duration, Instant}; + +use breadcast_caststream_sys::{ + breadcast_caststream_sender_create, breadcast_caststream_sender_destroy, + breadcast_caststream_sender_estimated_bandwidth_bps, + breadcast_caststream_sender_needs_key_frame, breadcast_caststream_sender_negotiate, +}; + +static GOT_OFFER: AtomicBool = AtomicBool::new(false); +static LAST_OFFER: Mutex> = Mutex::new(Vec::new()); + +extern "C" fn post_message( + _user_data: *mut c_void, + _destination_id: *const std::ffi::c_char, + _destination_id_len: usize, + _message_namespace: *const std::ffi::c_char, + _message_namespace_len: usize, + message: *const std::ffi::c_char, + message_len: usize, +) { + let bytes = unsafe { std::slice::from_raw_parts(message as *const u8, message_len) }; + *LAST_OFFER.lock().unwrap() = bytes.to_vec(); + GOT_OFFER.store(true, Ordering::Release); +} + +extern "C" fn on_negotiated(_user_data: *mut c_void) {} + +extern "C" fn on_error(_user_data: *mut c_void, message: *const std::ffi::c_char, message_len: usize) { + let bytes = unsafe { std::slice::from_raw_parts(message as *const u8, message_len) }; + panic!("session error: {}", String::from_utf8_lossy(bytes)); +} + +extern "C" fn on_picture_lost(_user_data: *mut c_void) {} + +#[test] +fn create_negotiate_sends_offer_and_tears_down_cleanly() { + let remote_ip = "192.0.2.1"; + let local_source_id = "sender-0"; + let receiver_id = "receiver-0"; + + let sender = unsafe { + breadcast_caststream_sender_create( + remote_ip.as_ptr() as *const _, + remote_ip.len(), + local_source_id.as_ptr() as *const _, + local_source_id.len(), + receiver_id.as_ptr() as *const _, + receiver_id.len(), + 1920, + 1080, + 8_000_000, + 30, + 1, + std::ptr::null_mut(), + post_message, + on_negotiated, + on_error, + on_picture_lost, + ) + }; + assert!(!sender.is_null(), "sender_create returned null"); + + unsafe { breadcast_caststream_sender_negotiate(sender) }; + + let deadline = Instant::now() + Duration::from_secs(2); + while !GOT_OFFER.load(Ordering::Acquire) && Instant::now() < deadline { + std::thread::sleep(Duration::from_millis(10)); + } + assert!(GOT_OFFER.load(Ordering::Acquire), "never received OFFER via post_message"); + + let offer = String::from_utf8(LAST_OFFER.lock().unwrap().clone()).unwrap(); + assert!(offer.contains("\"type\":\"OFFER\""), "unexpected payload: {offer}"); + assert!(offer.contains("\"codecName\":\"h264\""), "unexpected payload: {offer}"); + + // Before negotiation with a real receiver completes, these should still + // return sane defaults rather than garbage/crashing. + let needs_key = unsafe { breadcast_caststream_sender_needs_key_frame(sender) }; + let bandwidth = unsafe { breadcast_caststream_sender_estimated_bandwidth_bps(sender) }; + assert_eq!(needs_key, 1); + assert!(bandwidth > 0); + + unsafe { breadcast_caststream_sender_destroy(sender) }; +} diff --git a/breadcast-caststream-sys/vendor/openscreen/LICENSE b/breadcast-caststream-sys/vendor/openscreen/LICENSE new file mode 100644 index 0000000..eda81b0 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/LICENSE @@ -0,0 +1,27 @@ +// Copyright 2018 The Chromium Authors +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following disclaimer +// in the documentation and/or other materials provided with the +// distribution. +// * Neither the name of Google LLC nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/breadcast-caststream-sys/vendor/openscreen/PATCHES.md b/breadcast-caststream-sys/vendor/openscreen/PATCHES.md new file mode 100644 index 0000000..c25ec95 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/PATCHES.md @@ -0,0 +1,64 @@ +# Vendoring notes + +This directory is a **pruned subset** of [chromium/openscreen](https://chromium.googlesource.com/openscreen), +pinned at the commit in `PINNED_COMMIT`, licensed BSD-3-Clause (see `LICENSE`). +It contains only the files needed for the Cast Streaming *sender* data path +(RTP/RTCP send, OFFER/ANSWER message types, AES frame encryption) plus the +POSIX platform/util plumbing that path needs — not the receiver, not OSP +discovery, not remoting/RPC, not TLS (breadcast's TLS CASTV2 control channel +is handled by the existing `rust_cast`-based Rust code; this vendored code +only drives the UDP RTP/RTCP data path once `rust_cast` has already +negotiated OFFER/ANSWER and we know where to send packets). + +It is compiled directly via the `cc` crate in `../build.rs`, not GN/Ninja — +there is no build system here to regenerate anything from. + +## Rolling the pin + +There are no release/API-stability guarantees upstream. To update: +1. Re-run the file selection against the new commit's `cast/streaming/BUILD.gn` + (`:common` + `:sender` targets, minus `public/sender_session.*` and + `public/rpc_messenger.*` — see "What's excluded" below) plus + `platform/BUILD.gn`'s `:base`/`:api`/`:standalone_impl` targets. +2. Re-apply the patches below (they're small; check if upstream has since + fixed the same problem and the patch can be dropped). +3. Update `PINNED_COMMIT` and rebuild. + +## What's excluded and why + +- **`public/sender_session.{h,cc}`, `public/rpc_messenger.{h,cc}`, + `public/receiver_session.*`, receiver-side files, `remoting.proto`/ + `input.proto`** — `SenderSession` bundles mirroring negotiation together + with RPC/remoting/input support, which pulls in protobuf + (`input.pb.h`/`remoting.pb.h`) for no benefit here (breadcast only ever + does one-way video mirroring). Instead, `../src/session.cc` drives + `Offer`/`Answer`/`SessionConfig` directly — logic adapted from + `sender_session.cc`'s `CreateMirroringOffer`/`StartNegotiation`/ + `SelectSenders`, minus everything RPC/remoting/audio/input-related. +- **All TLS support** (`platform/impl/tls_*`, `platform/impl/stream_socket*`) — + unused; see above. +- **`util/crypto/{certificate_utils,digest_sign,pem_helpers,rsa_private_key, + secure_hash,sha2}.*`** — only needed for OSP discovery / X.509 certificate + handling, not the RTP data path. +- **`util/scoped_wake_lock_mac.cc`** — macOS-only. + +## Local patches (not upstream) + +1. **`build/build_config.h`, `build/buildflag.h`** — stand-ins for Chromium's + GN-generated versions of these headers. Only define what the 3 callers + here actually check (`IS_POSIX`, `IS_LINUX`, `IS_APPLE`, `IS_ANDROID`), + hardcoded for Linux. +2. **`patches/aes_ctr128_compat.cc`** — `frame_crypto.cc` calls + `AES_ctr128_encrypt()`, a BoringSSL convenience wrapper not in system + OpenSSL's public headers. Reimplemented from scratch (standard CTR mode + over `AES_encrypt()`, which OpenSSL does still expose). +3. **`platform/impl/platform_client_posix.{h,cc}`** — stripped the + `TlsDataRouterPosix` member/accessor (see "All TLS support" above). +4. **`util/crypto/openssl_util.{h,cc}`** — dropped `SSLErrorCodeToError()`/ + `GetSSLError()`, which reference BoringSSL's `SSL_error_description()` + (not in system OpenSSL). Unused for the same reason as (3). +5. **`util/base64.cc`** — upstream implements this on + `third_party/modp_b64`, which isn't fetched by a plain shallow clone + (pulled in separately via gclient/DEPS in a full Chromium checkout). + Reimplemented on `EVP_EncodeBlock`/`EVP_DecodeBlock` from system OpenSSL + instead, same public interface. diff --git a/breadcast-caststream-sys/vendor/openscreen/PINNED_COMMIT b/breadcast-caststream-sys/vendor/openscreen/PINNED_COMMIT new file mode 100644 index 0000000..9cdd872 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/PINNED_COMMIT @@ -0,0 +1 @@ +920129874c16365415a8a52fdbbc492d7623dc11 diff --git a/breadcast-caststream-sys/vendor/openscreen/build/build_config.h b/breadcast-caststream-sys/vendor/openscreen/build/build_config.h new file mode 100644 index 0000000..518d616 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/build/build_config.h @@ -0,0 +1,19 @@ +// Minimal stand-in for Chromium's build/build_config.h, providing just the +// BUILDFLAG(IS_*) values this vendored subset of openscreen actually checks +// (see ip_address.cc, logging_posix.cc, udp_socket_posix.cc). breadcast only +// ever builds this for Linux, so everything else is hardcoded off. Not part +// of upstream openscreen. +#ifndef BUILD_BUILD_CONFIG_H_ +#define BUILD_BUILD_CONFIG_H_ + +#include "build/buildflag.h" + +#define BUILDFLAG_IS_LINUX() (1) +#define BUILDFLAG_IS_POSIX() (1) +#define BUILDFLAG_IS_APPLE() (0) +#define BUILDFLAG_IS_ANDROID() (0) +#define BUILDFLAG_IS_WIN() (0) +#define BUILDFLAG_IS_CHROMEOS() (0) +#define BUILDFLAG_IS_IOS() (0) + +#endif // BUILD_BUILD_CONFIG_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/build/buildflag.h b/breadcast-caststream-sys/vendor/openscreen/build/buildflag.h new file mode 100644 index 0000000..64d9fde --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/build/buildflag.h @@ -0,0 +1,17 @@ +// Minimal stand-in for Chromium's generated build/buildflag.h. +// +// The real header is produced per-target by GN's buildflag_header() rule, +// which generates a `BUILDFLAG_()` macro per flag; BUILDFLAG(flag) +// then token-pastes "BUILDFLAG_" with the flag name and calls it. Since this +// vendored subset is compiled directly with the `cc` crate rather than +// GN/Ninja, build_config.h defines those BUILDFLAG_() macros directly +// for the Linux target breadcast always builds for. Not part of upstream +// openscreen. +#ifndef BUILD_BUILDFLAG_H_ +#define BUILD_BUILDFLAG_H_ + +#define BUILDFLAG_CAT_INDIRECT(a, b) a##b +#define BUILDFLAG_CAT(a, b) BUILDFLAG_CAT_INDIRECT(a, b) +#define BUILDFLAG(flag) (BUILDFLAG_CAT(BUILDFLAG_, flag)()) + +#endif // BUILD_BUILDFLAG_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/common/public/message_port.h b/breadcast-caststream-sys/vendor/openscreen/cast/common/public/message_port.h new file mode 100644 index 0000000..74f49f2 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/common/public/message_port.h @@ -0,0 +1,52 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_COMMON_PUBLIC_MESSAGE_PORT_H_ +#define CAST_COMMON_PUBLIC_MESSAGE_PORT_H_ + +#include + +#include "platform/base/error.h" + +namespace openscreen::cast { + +// This interface is intended to provide an abstraction for communicating +// cast messages across a pipe with guaranteed delivery. This is used to +// decouple the cast streaming receiver and sender sessions from the +// network implementation. +class MessagePort { + public: + class Client { + public: + // Called whenever a message arrives on the message port. + virtual void OnMessage(const std::string& source_id, + const std::string& message_namespace, + const std::string& message) = 0; + + // Called whenever an error occurs on the message port. + virtual void OnError(const Error& error) = 0; + + // Clients should expose a unique identifier used as the "source" of + // all messages sent on this message port. + virtual const std::string& source_id() = 0; + + protected: + virtual ~Client() = default; + }; + + virtual ~MessagePort() = default; + + // Set or reset the `MessagePort::Client` for this instance. + virtual void SetClient(Client& client) = 0; + virtual void ResetClient() = 0; + + // Sends a message to a given `destination_id`. + virtual void PostMessage(const std::string& destination_id, + const std::string& message_namespace, + const std::string& message) = 0; +}; + +} // namespace openscreen::cast + +#endif // CAST_COMMON_PUBLIC_MESSAGE_PORT_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/capture_configs.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/capture_configs.h new file mode 100644 index 0000000..2971023 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/capture_configs.h @@ -0,0 +1,81 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_CAPTURE_CONFIGS_H_ +#define CAST_STREAMING_CAPTURE_CONFIGS_H_ + +#include +#include + +#include "cast/streaming/public/constants.h" +#include "cast/streaming/resolution.h" +#include "util/simple_fraction.h" + +namespace openscreen::cast { + +// A configuration set that can be used by the sender to capture audio, and the +// receiver to playback audio. Used by Cast Streaming to provide an offer to the +// receiver. +struct AudioCaptureConfig { + // Audio codec represented by this configuration. + AudioCodec codec = AudioCodec::kOpus; + + // Number of channels used by this configuration. + int channels = kDefaultAudioChannels; + + // Average bit rate in bits per second used by this configuration. A value + // of "zero" suggests that the bitrate should be automatically selected by + // the sender. + int bit_rate = 0; + + // Sample rate for audio RTP timebase. + int sample_rate = kDefaultAudioSampleRate; + + // Target playout delay in milliseconds. + std::chrono::milliseconds target_playout_delay = kDefaultTargetPlayoutDelay; + + // The codec parameter for this configuration. Honors the format laid out + // in RFC 6381: https://datatracker.ietf.org/doc/html/rfc6381 + // NOTE: the "profiles" parameter is not supported in our implementation. + std::string codec_parameter; +}; + +// A configuration set that can be used by the sender to capture video, as +// well as the receiver to playback video. Used by Cast Streaming to provide an +// offer to the receiver. +struct VideoCaptureConfig { + // Video codec represented by this configuration. + VideoCodec codec = VideoCodec::kVp8; + + // Maximum frame rate in frames per second. + // For simple cases, the frame rate may be provided by simply setting the + // number to the desired value, e.g. 30 or 60FPS. Some common frame rates like + // 23.98 FPS (for NTSC compatibility) are represented as fractions, in this + // case 24000/1001. + SimpleFraction max_frame_rate{kDefaultFrameRate, 1}; + + // Number specifying the maximum bit rate for this stream. A value of + // zero means that the maximum bit rate should be automatically selected by + // the sender. + int max_bit_rate = 0; + + // Resolutions to be offered to the receiver. At least one resolution + // must be provided. + std::vector resolutions; + + // Target playout delay in milliseconds. + std::chrono::milliseconds target_playout_delay = kDefaultTargetPlayoutDelay; + + // The codec parameter for this configuration. Honors the format laid out + // in RFC 6381: https://datatracker.ietf.org/doc/html/rfc6381. + // VP8 and VP9 codec parameter versions are defined here: + // https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter#webm + // https://www.webmproject.org/vp9/mp4/#codecs-parameter-string + // NOTE: the "profiles" parameter is not supported in our implementation. + std::string codec_parameter; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_CAPTURE_CONFIGS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/bandwidth_estimator.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/bandwidth_estimator.cc new file mode 100644 index 0000000..75788af --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/bandwidth_estimator.cc @@ -0,0 +1,155 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/bandwidth_estimator.h" + +#include + +#include "util/osp_logging.h" +#include "util/saturate_cast.h" + +namespace openscreen::cast { + +using clock_operators::operator<<; + +namespace { + +// Converts units from `bytes` per `time_window` number of Clock ticks into +// bits-per-second. +int ToClampedBitsPerSecond(int32_t bytes, Clock::duration time_window) { + OSP_CHECK_GT(time_window, Clock::duration::zero()); + + // Divide `bytes` by `time_window` and scale the units to bits per second. + constexpr int64_t kBitsPerByte = 8; + constexpr int64_t kClockTicksPerSecond = + Clock::to_duration(std::chrono::seconds(1)).count(); + const int64_t bits = bytes * kBitsPerByte; + const int64_t bits_per_second = + (bits * kClockTicksPerSecond) / time_window.count(); + return saturate_cast(bits_per_second); +} + +} // namespace + +BandwidthEstimator::BandwidthEstimator(int max_packets_per_timeslice, + Clock::duration timeslice_duration, + Clock::time_point start_time) + : max_packets_per_history_window_(max_packets_per_timeslice * + kNumTimeslices), + history_window_(timeslice_duration * kNumTimeslices), + burst_history_(timeslice_duration, start_time), + feedback_history_(timeslice_duration, start_time) { + OSP_CHECK_GT(max_packets_per_timeslice, 0); + OSP_CHECK_GT(timeslice_duration, Clock::duration::zero()); +} + +BandwidthEstimator::~BandwidthEstimator() = default; + +void BandwidthEstimator::OnBurstComplete(int num_packets_sent, + Clock::time_point when) { + OSP_CHECK_GE(num_packets_sent, 0); + burst_history_.Accumulate(num_packets_sent, when); +} + +void BandwidthEstimator::OnRtcpReceived( + Clock::time_point arrival_time, + Clock::duration estimated_round_trip_time) { + OSP_CHECK_GE(estimated_round_trip_time, Clock::duration::zero()); + // Move forward the feedback history tracking timeline to include the latest + // moment a packet could have left the Sender. + feedback_history_.AdvanceToIncludeTime(arrival_time - + estimated_round_trip_time); +} + +void BandwidthEstimator::OnPayloadReceived( + int payload_bytes_acknowledged, + Clock::time_point ack_arrival_time, + Clock::duration estimated_round_trip_time) { + OSP_CHECK_GE(payload_bytes_acknowledged, 0); + OSP_CHECK_LT(ack_arrival_time, Clock::time_point::max()); + OSP_CHECK_GE(estimated_round_trip_time, Clock::duration::zero()); + // Track the bytes in terms of when the last packet was sent. + feedback_history_.Accumulate(payload_bytes_acknowledged, + ack_arrival_time - estimated_round_trip_time); +} + +int BandwidthEstimator::ComputeNetworkBandwidth() const { + // Determine whether the `burst_history_` time window overlaps with the + // `feedback_history_` time window by at least half. The time windows don't + // have to overlap entirely because the calculations are averaging all the + // measurements (i.e., recent typical behavior). Though, they should overlap + // by "enough" so that the measurements correlate "enough." + const Clock::time_point overlap_begin = + std::max(burst_history_.begin_time(), feedback_history_.begin_time()); + const Clock::time_point overlap_end = + std::min(burst_history_.end_time(), feedback_history_.end_time()); + if ((overlap_end - overlap_begin) < (history_window_ / 2)) { + return 0; + } + + const int32_t num_packets_transmitted = burst_history_.Sum(); + if (num_packets_transmitted <= 0) { + // Cannot estimate because there have been no transmissions recently. + return 0; + } + const Clock::duration transmit_duration = history_window_ * + num_packets_transmitted / + max_packets_per_history_window_; + const int32_t num_bytes_received = feedback_history_.Sum(); + return ToClampedBitsPerSecond(num_bytes_received, transmit_duration); +} + +// static +constexpr int BandwidthEstimator::kNumTimeslices; + +BandwidthEstimator::FlowTracker::FlowTracker(Clock::duration timeslice_duration, + Clock::time_point begin_time) + : timeslice_duration_(timeslice_duration), begin_time_(begin_time) {} + +BandwidthEstimator::FlowTracker::~FlowTracker() = default; + +void BandwidthEstimator::FlowTracker::AdvanceToIncludeTime( + Clock::time_point until) { + if (until < end_time()) { + return; // Not advancing. + } + + // Step forward in time, at timeslice granularity. + const int64_t num_periods = 1 + (until - end_time()) / timeslice_duration_; + begin_time_ += num_periods * timeslice_duration_; + + // Shift the ring elements, discarding N oldest timeslices, and creating N new + // ones initialized to zero. + const int shift_count = std::min(num_periods, kNumTimeslices); + for (int i = 0; i < shift_count; ++i) { + history_ring_[tail_++] = 0; + } +} + +void BandwidthEstimator::FlowTracker::Accumulate(int32_t amount, + Clock::time_point when) { + if (when < begin_time_) { + return; // Ignore a data point that is already too old. + } + + AdvanceToIncludeTime(when); + + // Because of the AdvanceToIncludeTime() call just made, the offset/index + // calculations here are guaranteed to point to a valid element in the + // `history_ring_`. + const int64_t offset_from_first = (when - begin_time_) / timeslice_duration_; + const index_mod_256_t ring_index = tail_ + offset_from_first; + int32_t& timeslice = history_ring_[ring_index]; + timeslice = saturate_cast(int64_t{timeslice} + amount); +} + +int32_t BandwidthEstimator::FlowTracker::Sum() const { + int64_t result = 0; + for (int32_t amount : history_ring_) { + result += amount; + } + return saturate_cast(result); +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/bandwidth_estimator.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/bandwidth_estimator.h new file mode 100644 index 0000000..8ee2c92 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/bandwidth_estimator.h @@ -0,0 +1,168 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_BANDWIDTH_ESTIMATOR_H_ +#define CAST_STREAMING_IMPL_BANDWIDTH_ESTIMATOR_H_ + +#include + +#include + +#include "platform/api/time.h" + +namespace openscreen::cast { + +// Tracks send attempts and successful receives, and then computes a total +// network bandwith estimate. +// +// Two metrics are tracked by the BandwidthEstimator, over a "recent history" +// time window: +// +// 1. The number of packets sent during bursts (see SenderPacketRouter for +// explanation of what a "burst" is). These track when the network was +// actually in-use for transmission and the magnitude of each burst. When +// computing bandwidth, the estimator assumes the timeslices where the +// network was not in-use could have been used to send even more bytes at +// the same rate. +// +// 2. Successful receipt of payload bytes over time, or a lack thereof. +// Packets that include acknowledgements from the Receivers are providing +// proof of the successful receipt of payload bytes. All other packets +// provide proof of network connectivity over time, and are used to +// identify periods of time where nothing was received. +// +// The BandwidthEstimator assumes a simplified model for streaming over the +// network. The model does not include any detailed knowledge about things like +// protocol overhead, packet re-transmits, parasitic bufferring, network +// reliability, etc. Instead, it automatically accounts for all such things by +// looking at what's actually leaving the Senders and what's actually making it +// to the Receivers. +// +// This simplified model does produce some known inaccuracies in the resulting +// estimations. If no data has recently been transmitted (or been received), +// estimations cannot be provided. If the transmission rate is near (or +// exceeding) the network's capacity, the estimations will be very accurate. In +// between those two extremes, the logic will tend to under-estimate the +// network's capacity. However, those under-estimates will still be far larger +// than the current transmission rate. +// +// Thus, these estimates can be used effectively as a control signal for +// congestion control in upstream code modules. The logic computing the media's +// encoding target bitrate should be adjusted in realtime using a TCP-like +// congestion control algorithm: +// +// 1. When the estimated bitrate is less than the current encoding target +// bitrate, aggressively and immediately decrease the encoding bitrate. +// +// 2. When the estimated bitrate is more than the current encoding target +// bitrate, gradually increase the encoding bitrate (up to the maximum +// that is reasonable for the application). +class BandwidthEstimator { + public: + // `max_packets_per_timeslice` and `timeslice_duration` should match the burst + // configuration in SenderPacketRouter. `start_time` should be a recent + // point-in-time before the first packet is sent. + BandwidthEstimator(int max_packets_per_timeslice, + Clock::duration timeslice_duration, + Clock::time_point start_time); + + ~BandwidthEstimator(); + + // Returns the duration of the fixed, recent-history time window over which + // data flows are being tracked. + Clock::duration history_window() const { return history_window_; } + + // Records `when` burst-sending was active or inactive. For the active case, + // `num_packets_sent` should include all network packets sent, including + // non-payload packets (since both affect the modeled utilization/capacity). + // For the inactive case, this method should be called with zero for + // `num_packets_sent`. + void OnBurstComplete(int num_packets_sent, Clock::time_point when); + + // Records when a RTCP packet was received. It's important for Senders to call + // this any time a packet comes in from the Receivers, even if no payload is + // being acknowledged, since the time windows of "nothing successfully + // received" is also important information to track. + void OnRtcpReceived(Clock::time_point arrival_time, + Clock::duration estimated_round_trip_time); + + // Records that some number of payload bytes has been acknowledged (i.e., + // successfully received). + void OnPayloadReceived(int payload_bytes_acknowledged, + Clock::time_point ack_arrival_time, + Clock::duration estimated_round_trip_time); + + // Computes the current network bandwith estimate. Returns 0 if this cannot be + // determined due to a lack of sufficiently-recent data. + int ComputeNetworkBandwidth() const; + + private: + // FlowTracker (below) manages a ring buffer of size 256. It simplifies the + // index calculations to use an integer data type where all arithmetic is mod + // 256. + using index_mod_256_t = uint8_t; + static constexpr int kNumTimeslices = + static_cast(std::numeric_limits::max()) + 1; + + // Tracks volume (e.g., the total number of payload bytes) over a fixed + // recent-history time window. The time window is divided up into a number of + // identical timeslices, each of which represents the total number of bytes + // that flowed during a certain period of time. The data is accumulated in + // ring buffer elements so that old data points drop-off as newer ones (that + // move the history window forward) are added. + class FlowTracker { + public: + FlowTracker(Clock::duration timeslice_duration, + Clock::time_point begin_time); + ~FlowTracker(); + + Clock::time_point begin_time() const { return begin_time_; } + Clock::time_point end_time() const { + return begin_time_ + timeslice_duration_ * kNumTimeslices; + } + + // Advance the end of the time window being tracked such that the + // most-recent timeslice includes `until`. Too-old timeslices are dropped + // and new ones are initialized to a zero amount. + void AdvanceToIncludeTime(Clock::time_point until); + + // Accumulate the given `amount` into the timeslice that includes `when`. + void Accumulate(int32_t amount, Clock::time_point when); + + // Return the sum of all the amounts in recent history. This clamps to the + // valid range of int32_t, if necessary. + int32_t Sum() const; + + private: + const Clock::duration timeslice_duration_; + + // The beginning of the oldest timeslice in the recent-history time window, + // the one pointed to by `tail_`. + Clock::time_point begin_time_; + + // A ring buffer tracking the accumulated amount for each timeslice. + int32_t history_ring_[kNumTimeslices]{}; + + // The index of the oldest timeslice in the `history_ring_`. This can also + // be thought of, equivalently, as the index just after the most-recent + // timeslice. + index_mod_256_t tail_ = 0; + }; + + // The maximum number of packet sends that could possibly be attempted during + // the recent-history time window. + const int max_packets_per_history_window_; + + // The range of time being tracked. + const Clock::duration history_window_; + + // History tracking for send attempts, and success feeback. These timeseries + // are in terms of when packets have left the Senders. + FlowTracker burst_history_; + FlowTracker feedback_history_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_BANDWIDTH_ESTIMATOR_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_drift_smoother.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_drift_smoother.cc new file mode 100644 index 0000000..90cd10c --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_drift_smoother.cc @@ -0,0 +1,79 @@ +// Copyright 2014 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/clock_drift_smoother.h" + +#include + +#include "util/chrono_helpers.h" +#include "util/osp_logging.h" +#include "util/saturate_cast.h" + +namespace openscreen::cast { +namespace { + +constexpr Clock::time_point kNullTime = Clock::time_point::min(); +} + +using clock_operators::operator<<; + +ClockDriftSmoother::ClockDriftSmoother(Clock::duration time_constant) + : time_constant_(time_constant), + last_update_time_(kNullTime), + estimated_tick_offset_(0.0) { + OSP_CHECK(time_constant_ > decltype(time_constant_)::zero()); +} + +ClockDriftSmoother::~ClockDriftSmoother() = default; + +std::optional ClockDriftSmoother::Current() const { + if (last_update_time_ == kNullTime) { + return std::nullopt; + } + return Clock::duration( + rounded_saturate_cast(estimated_tick_offset_)); +} + +void ClockDriftSmoother::Reset(Clock::time_point now, + Clock::duration measured_offset) { + OSP_CHECK_NE(now, kNullTime); + last_update_time_ = now; + estimated_tick_offset_ = static_cast(measured_offset.count()); +} + +void ClockDriftSmoother::Update(Clock::time_point now, + Clock::duration measured_offset) { + OSP_CHECK_NE(now, kNullTime); + if (last_update_time_ == kNullTime) { + Reset(now, measured_offset); + return; + } + + if (now < last_update_time_) { + // `now` is not monotonically non-decreasing. + OSP_NOTREACHED(); + } + + const double elapsed_ticks = + static_cast((now - last_update_time_).count()); + last_update_time_ = now; + + // This is a standard exponential moving average (EMA) filter. + // The alpha value is calculated such that the filter has the desired time + // constant. + const double alpha = 1.0 - std::exp(-elapsed_ticks / time_constant_.count()); + estimated_tick_offset_ = + alpha * static_cast(measured_offset.count()) + + (1.0 - alpha) * estimated_tick_offset_; + + const auto current = Current(); + OSP_VLOG << "Local clock is ahead of the remote clock by: measured = " + << measured_offset << ", " + << "filtered = " << (current ? ToString(*current) : "null") << "."; +} + +// static +constexpr std::chrono::seconds ClockDriftSmoother::kDefaultTimeConstant; + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_drift_smoother.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_drift_smoother.h new file mode 100644 index 0000000..4ea8bd3 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_drift_smoother.h @@ -0,0 +1,58 @@ +// Copyright 2014 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_CLOCK_DRIFT_SMOOTHER_H_ +#define CAST_STREAMING_IMPL_CLOCK_DRIFT_SMOOTHER_H_ + +#include +#include + +#include "platform/api/time.h" + +namespace openscreen::cast { + +// Tracks the jitter and drift between clocks, providing a smoothed offset. +// Internally, a Simple IIR filter is used to maintain a running average that +// moves at a rate based on the passage of time. +class ClockDriftSmoother { + public: + // `time_constant` is the amount of time an impulse signal takes to decay by + // ~62.6%. Interpretation: If the value passed to several Update() calls is + // held constant for T seconds, then the running average will have moved + // towards the value by ~62.6% from where it started. + explicit ClockDriftSmoother(Clock::duration time_constant); + ~ClockDriftSmoother(); + + // Returns the current offset. Will be std::nullopt if no values have been + // set yet (via Reset() or Update()). + std::optional Current() const; + + // Discard all history and reset to exactly `offset`, measured `now`. + void Reset(Clock::time_point now, Clock::duration offset); + + // Update the current offset, which was measured `now`. The weighting that + // `measured_offset` will have on the running average is influenced by how + // much time has passed since the last call to this method (or Reset()). + // `now` should be monotonically non-decreasing over successive calls of this + // method. + void Update(Clock::time_point now, Clock::duration measured_offset); + + // A time constant suitable for most use cases, where the clocks are expected + // to drift very little with respect to each other, and the jitter caused by + // clock imprecision is effectively canceled out. + static constexpr std::chrono::seconds kDefaultTimeConstant{30}; + + private: + const std::chrono::duration time_constant_; + + // The time at which `estimated_tick_offset_` was last updated. + Clock::time_point last_update_time_; + + // The current estimated offset, as number of Clock::duration ticks. + double estimated_tick_offset_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_CLOCK_DRIFT_SMOOTHER_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_offset_estimator.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_offset_estimator.h new file mode 100644 index 0000000..be51abf --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_offset_estimator.h @@ -0,0 +1,71 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_CLOCK_OFFSET_ESTIMATOR_H_ +#define CAST_STREAMING_IMPL_CLOCK_OFFSET_ESTIMATOR_H_ + +#include +#include + +#include "cast/streaming/impl/statistics_common.h" +#include "cast/streaming/public/statistics.h" +#include "platform/base/trivial_clock_traits.h" + +namespace openscreen::cast { + +// Used to estimate the offset between the Sender and Receiver clocks. +class ClockOffsetEstimator { + public: + static std::unique_ptr Create(); + + virtual ~ClockOffsetEstimator() {} + + // TODO(issuetracker.google.com/298085631): these should be in a separate + // header, like Chrome's raw event subscriber pattern. + // See: //media/cast/logging/raw_event_subscriber.h + virtual void OnFrameEvent(const FrameEvent& frame_event) = 0; + virtual void OnPacketEvent(const PacketEvent& packet_event) = 0; + + // Estimates the clock offset between the sender and the receiver. + // + // This is calculated by solving a system of two linear equations with two + // unknowns: the clock offset and the network latency. The two equations are + // derived from two round-trip time measurements. + // + // Let's define: + // - latency: the one-way network latency. + // - offset: the clock offset, where Clock_Receiver(t) = Clock_Sender(t) + + // offset. + // + // The estimator measures two bounds: + // + // 1. packet_bound (sender -> receiver): + // delta = TS_receiver - TS_sender + // = (TS_sender + latency + offset) - TS_sender + // = latency + offset + // + // 2. frame_bound (receiver -> sender): + // delta = TS_sender - TS_receiver + // = (TS_receiver + latency - offset) - TS_receiver + // = latency - offset + // + // The offset is then isolated by the formula: + // (packet_bound - frame_bound) / 2 = + // ( (latency + offset) - (latency - offset) ) / 2 = + // (2 * offset) / 2 = offset + virtual std::optional GetEstimatedOffset() const = 0; + + // Estimates the one-way network latency. + // This uses the same bounds as GetEstimatedOffset(). + // + // The latency is isolated by the formula: + // (packet_bound + frame_bound) / 2 = + // ( (latency + offset) + (latency - offset) ) / 2 = (2 * latency) / 2 = + // latency + virtual std::optional GetEstimatedLatency() const = 0; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_CLOCK_OFFSET_ESTIMATOR_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_offset_estimator_impl.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_offset_estimator_impl.cc new file mode 100644 index 0000000..49a7002 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_offset_estimator_impl.cc @@ -0,0 +1,222 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/clock_offset_estimator_impl.h" + +#include +#include +#include +#include + +#include "platform/base/trivial_clock_traits.h" +#include "util/chrono_helpers.h" + +namespace openscreen::cast { +namespace { + +// This should be large enough so that we can collect all 3 events before +// the entry gets removed from the map. +constexpr size_t kMaxEventTimesMapSize = 500; + +// Bitwise merging of values to produce an ordered key for entries in the +// BoundCalculator::events_ map. Since std::map is sorted by key value, we +// ensure that the Packet ID is first (since the RTP timestamp may roll over +// eventually). +// +// 0 1 2 3 4 5 6 +// 0 2 4 6 8 0 2 4 6 8 0 2 4 6 8 0 2 4 6 8 0 2 4 6 8 0 2 4 6 8 0 2 4 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Packet ID | RTP Timestamp |*| (is_audio) +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +uint64_t MakeEventKey(RtpTimeTicks rtp, uint16_t packet_id, bool audio) { + return (static_cast(packet_id) << 48) | + (static_cast(rtp.lower_32_bits()) << 1) | + static_cast(audio ? 1 : 0); +} + +} // namespace + +std::unique_ptr ClockOffsetEstimator::Create() { + return std::make_unique(); +} + +ClockOffsetEstimatorImpl::ClockOffsetEstimatorImpl() = default; +ClockOffsetEstimatorImpl::ClockOffsetEstimatorImpl( + ClockOffsetEstimatorImpl&&) noexcept = default; +ClockOffsetEstimatorImpl& ClockOffsetEstimatorImpl::operator=( + ClockOffsetEstimatorImpl&&) = default; +ClockOffsetEstimatorImpl::~ClockOffsetEstimatorImpl() = default; + +void ClockOffsetEstimatorImpl::OnFrameEvent(const FrameEvent& frame_event) { + switch (frame_event.type) { + case StatisticsEvent::Type::kFrameAckSent: + frame_bound_.SetSent( + frame_event.rtp_timestamp, 0, + frame_event.media_type == StatisticsEvent::MediaType::kAudio, + frame_event.timestamp); + break; + case StatisticsEvent::Type::kFrameAckReceived: + frame_bound_.SetReceived( + frame_event.rtp_timestamp, 0, + frame_event.media_type == StatisticsEvent::MediaType::kAudio, + frame_event.timestamp); + break; + default: + // Ignored + break; + } +} + +void ClockOffsetEstimatorImpl::OnPacketEvent(const PacketEvent& packet_event) { + switch (packet_event.type) { + case StatisticsEvent::Type::kPacketSentToNetwork: + packet_bound_.SetSent( + packet_event.rtp_timestamp, packet_event.packet_id, + packet_event.media_type == StatisticsEvent::MediaType::kAudio, + packet_event.timestamp); + break; + case StatisticsEvent::Type::kPacketReceived: + packet_bound_.SetReceived( + packet_event.rtp_timestamp, packet_event.packet_id, + packet_event.media_type == StatisticsEvent::MediaType::kAudio, + packet_event.timestamp); + break; + default: + // Ignored + break; + } +} + +bool ClockOffsetEstimatorImpl::GetReceiverOffsetBounds( + Clock::duration& frame_bound, + Clock::duration& packet_bound) const { + if (!frame_bound_.has_bound() || !packet_bound_.has_bound()) { + return false; + } + + frame_bound = -frame_bound_.bound(); + packet_bound = packet_bound_.bound(); + + return true; +} + +std::optional ClockOffsetEstimatorImpl::GetEstimatedOffset() + const { + Clock::duration frame_bound; + Clock::duration packet_bound; + if (!GetReceiverOffsetBounds(frame_bound, packet_bound)) { + return {}; + } + return (packet_bound + frame_bound) / 2; +} + +std::optional ClockOffsetEstimatorImpl::GetEstimatedLatency() + const { + Clock::duration frame_bound; + Clock::duration packet_bound; + if (!GetReceiverOffsetBounds(frame_bound, packet_bound)) { + return {}; + } + return (packet_bound - frame_bound) / 2; +} + +ClockOffsetEstimatorImpl::KalmanFilter::KalmanFilter( + Clock::duration process_noise, + Clock::duration measurement_noise) + : q_nanos_squared_( + static_cast(std::chrono::nanoseconds(process_noise).count()) * + std::chrono::nanoseconds(process_noise).count()), + r_nanos_squared_( + static_cast( + std::chrono::nanoseconds(measurement_noise).count()) * + std::chrono::nanoseconds(measurement_noise).count()) {} + +void ClockOffsetEstimatorImpl::KalmanFilter::Update( + Clock::duration measurement) { + if (!has_estimate_) { + // First measurement, initialize the state. + estimated_latency_ = measurement; + error_covariance_nanos_squared_ = r_nanos_squared_; + has_estimate_ = true; + return; + } + + // --- 1. PREDICT --- + // The predicted state is the same as the previous state. + // The uncertainty (covariance) increases by the process noise. + const double predicted_error_covariance = + error_covariance_nanos_squared_ + q_nanos_squared_; + + // --- 2. UPDATE --- + // Calculate Kalman Gain. + const double kalman_gain = predicted_error_covariance / + (predicted_error_covariance + r_nanos_squared_); + + // Update the estimate with the new measurement. + const double measurement_nanos = + static_cast(std::chrono::nanoseconds(measurement).count()); + const double estimate_nanos = + static_cast(std::chrono::nanoseconds(estimated_latency_).count()); + const double new_estimate_nanos = + estimate_nanos + kalman_gain * (measurement_nanos - estimate_nanos); + estimated_latency_ = + std::chrono::duration_cast(std::chrono::nanoseconds( + static_cast(new_estimate_nanos))); + + // Update the error covariance. + error_covariance_nanos_squared_ = + (1.0 - kalman_gain) * predicted_error_covariance; +} + +ClockOffsetEstimatorImpl::BoundCalculator::BoundCalculator() + : filter_(kProcessNoise, kMeasurementNoise) {} + +ClockOffsetEstimatorImpl::BoundCalculator::BoundCalculator( + BoundCalculator&&) noexcept = default; +ClockOffsetEstimatorImpl::BoundCalculator& +ClockOffsetEstimatorImpl::BoundCalculator::operator=(BoundCalculator&&) = + default; +ClockOffsetEstimatorImpl::BoundCalculator::~BoundCalculator() = default; + +void ClockOffsetEstimatorImpl::BoundCalculator::SetSent(RtpTimeTicks rtp, + uint16_t packet_id, + bool audio, + Clock::time_point t) { + const uint64_t key = MakeEventKey(rtp, packet_id, audio); + events_[key].first = t; + CheckUpdate(key); +} + +void ClockOffsetEstimatorImpl::BoundCalculator::SetReceived( + RtpTimeTicks rtp, + uint16_t packet_id, + bool audio, + Clock::time_point t) { + const uint64_t key = MakeEventKey(rtp, packet_id, audio); + events_[key].second = t; + CheckUpdate(key); +} + +void ClockOffsetEstimatorImpl::BoundCalculator::UpdateBound( + Clock::time_point sent, + Clock::time_point received) { + filter_.Update(received - sent); +} + +void ClockOffsetEstimatorImpl::BoundCalculator::CheckUpdate(uint64_t key) { + const TimeTickPair& ticks = events_[key]; + if (ticks.first && ticks.second) { + UpdateBound(ticks.first.value(), ticks.second.value()); + events_.erase(key); + return; + } + + if (events_.size() > kMaxEventTimesMapSize) { + // We can make use of the fact that std::map sorts by key and just erase + // the first entry. + events_.erase(events_.begin()); + } +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_offset_estimator_impl.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_offset_estimator_impl.h new file mode 100644 index 0000000..5bf9422 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/clock_offset_estimator_impl.h @@ -0,0 +1,136 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_CLOCK_OFFSET_ESTIMATOR_IMPL_H_ +#define CAST_STREAMING_IMPL_CLOCK_OFFSET_ESTIMATOR_IMPL_H_ + +#include +#include + +#include +#include +#include + +#include "cast/streaming/impl/clock_offset_estimator.h" +#include "cast/streaming/impl/statistics_common.h" +#include "cast/streaming/rtp_time.h" +#include "platform/base/trivial_clock_traits.h" +#include "util/chrono_helpers.h" + +namespace openscreen::cast { + +// This implementation listens to two pairs of events: +// 1. FrameAckSent / FrameAckReceived (receiver->sender) +// 2. PacketSentToNetwork / PacketReceived (sender->receiver) +// +// There is a causal relationship between these events in that these events +// must happen in order. This class obtains the lower and upper bounds for +// the offset by taking the difference of timestamps. +class ClockOffsetEstimatorImpl final : public ClockOffsetEstimator { + public: + ClockOffsetEstimatorImpl(); + ClockOffsetEstimatorImpl(ClockOffsetEstimatorImpl&&) noexcept; + ClockOffsetEstimatorImpl(const ClockOffsetEstimatorImpl&) = delete; + ClockOffsetEstimatorImpl& operator=(ClockOffsetEstimatorImpl&&); + ClockOffsetEstimatorImpl& operator=(const ClockOffsetEstimatorImpl&) = delete; + ~ClockOffsetEstimatorImpl() final; + + void OnFrameEvent(const FrameEvent& frame_event) final; + void OnPacketEvent(const PacketEvent& packet_event) final; + + bool GetReceiverOffsetBounds(Clock::duration& frame_bound, + Clock::duration& packet_bound) const; + + // ClockOffsetEstimator overrides. + std::optional GetEstimatedOffset() const final; + std::optional GetEstimatedLatency() const final; + + private: + // These values are chosen based on common network conditions. + // + // Q (process_noise): We expect latency to drift by up to 5ms between + // measurements. + static constexpr Clock::duration kProcessNoise = milliseconds(5); + // + // R (measurement_noise): We expect jitter of up to 30ms. + static constexpr Clock::duration kMeasurementNoise = milliseconds(30); + + // Simplified 1D Kalman Filter for latency estimation. + class KalmanFilter { + public: + // Q: process_noise - Represents the expected variance of the latency + // itself between time steps. A higher value makes the filter adapt + // more quickly to real changes in latency. + // R: measurement_noise - Represents the variance of the measurement + // noise (jitter). A higher value makes the filter trust its own + // prediction more and smooth out noisy measurements. + KalmanFilter(Clock::duration process_noise, + Clock::duration measurement_noise); + KalmanFilter(KalmanFilter&&) noexcept = default; + KalmanFilter& operator=(KalmanFilter&&) = default; + + Clock::duration GetEstimate() const { return estimated_latency_; } + bool HasEstimate() const { return has_estimate_; } + void Update(Clock::duration measurement); + + private: + double q_nanos_squared_; + double r_nanos_squared_; + + bool has_estimate_ = false; + Clock::duration estimated_latency_{}; + double error_covariance_nanos_squared_ = 0.0; + }; + + // This helper uses the difference between sent and received event + // to calculate an upper bound on the difference between the clocks + // on the sender and receiver. Note that this difference can take + // very large positive or negative values, but the smaller value is + // always the better estimate, since a receive event cannot possibly + // happen before a send event. Note that we use this to calculate + // both upper and lower bounds by reversing the sender/receiver + // relationship. + class BoundCalculator { + public: + typedef std::pair, + std::optional> + TimeTickPair; + typedef std::map EventMap; + + BoundCalculator(); + BoundCalculator(BoundCalculator&&) noexcept; + BoundCalculator(const BoundCalculator&) = delete; + BoundCalculator& operator=(BoundCalculator&&); + BoundCalculator& operator=(const BoundCalculator&) = delete; + ~BoundCalculator(); + bool has_bound() const { return filter_.HasEstimate(); } + Clock::duration bound() const { return filter_.GetEstimate(); } + + void SetSent(RtpTimeTicks rtp, + uint16_t packet_id, + bool audio, + Clock::time_point t); + + void SetReceived(RtpTimeTicks rtp, + uint16_t packet_id, + bool audio, + Clock::time_point t); + + private: + void UpdateBound(Clock::time_point a, Clock::time_point b); + void CheckUpdate(uint64_t key); + + private: + EventMap events_; + KalmanFilter filter_; + }; + + // Fixed size storage to store event times for recent frames and packets. + BoundCalculator packet_bound_; + BoundCalculator frame_bound_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_CLOCK_OFFSET_ESTIMATOR_IMPL_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/compound_rtcp_parser.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/compound_rtcp_parser.cc new file mode 100644 index 0000000..50fbe24 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/compound_rtcp_parser.cc @@ -0,0 +1,479 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/compound_rtcp_parser.h" + +#include +#include + +#include "cast/streaming/impl/packet_util.h" +#include "cast/streaming/impl/rtcp_session.h" +#include "cast/streaming/impl/statistics_common.h" +#include "util/chrono_helpers.h" +#include "util/osp_logging.h" +#include "util/std_util.h" + +namespace openscreen::cast { + +namespace { + +// Use the Clock's minimum time value (an impossible value, waaaaay before epoch +// time) to represent unset time_point values. +constexpr auto kNullTimePoint = Clock::time_point::min(); + +// Some receivers send time sync requests (that we ignore). +constexpr uint32_t kTimeSyncRequestName = + ('T' << 24) + ('I' << 16) + ('M' << 8) + 'E'; + +// Canonicalizes the just-parsed list of packet-specific NACKs so that the +// CompoundRtcpParser::Client can make several simplifying assumptions when +// processing the results. +void CanonicalizePacketNackVector(std::vector* packets) { + // First, sort all elements. The sort order is the normal lexicographical + // ordering, with one exception: The special kAllPacketsLost packet_id value + // should be treated as coming before all others. This special sort order + // allows the filtering algorithm below to be simpler, and only require one + // pass; and the final result will be the normal lexicographically-sorted + // output the CompoundRtcpParser::Client expects. + std::sort(packets->begin(), packets->end(), + [](const PacketNack& a, const PacketNack& b) { + // Since the comparator is a hot code path, use a simple modular + // arithmetic trick in lieu of extra branching: When comparing the + // tuples, map all packet_id values to packet_id + 1, mod 0x10000. + // This results in the desired sorting behavior since + // kAllPacketsLost (0xffff) wraps-around to 0x0000, and all other + // values become N + 1. + static_assert(static_cast(kAllPacketsLost + 1) < + FramePacketId{0x0000 + 1}, + "comparison requires integer wrap-around"); + return PacketNack{a.frame_id, + static_cast(a.packet_id + 1)} < + PacketNack{b.frame_id, + static_cast(b.packet_id + 1)}; + }); + + // De-duplicate elements. Two possible cases: + // + // 1. Identical elements (same FrameId+FramePacketId). + // 2. If there are any elements with kAllPacketsLost as the packet ID, + // prune-out all other elements having the same frame ID, as they are + // redundant. + // + // This is done by walking forwards over the sorted vector and deciding which + // elements to keep. Those that are kept are stacked-up at the front of the + // vector. After the "to-keep" pass, the vector is truncated to remove the + // left-over garbage at the end. + auto have_it = packets->begin(); + if (have_it != packets->end()) { + auto kept_it = have_it; // Always keep the first element. + for (++have_it; have_it != packets->end(); ++have_it) { + if (have_it->frame_id != kept_it->frame_id || + (kept_it->packet_id != kAllPacketsLost && + have_it->packet_id != kept_it->packet_id)) { // Keep it. + ++kept_it; + *kept_it = *have_it; + } + } + packets->erase(++kept_it, packets->end()); + } +} + +} // namespace + +CompoundRtcpParser::CompoundRtcpParser(RtcpSession& session, + CompoundRtcpParser::Client& client) + : session_(session), + client_(client), + latest_receiver_timestamp_(kNullTimePoint) {} + +CompoundRtcpParser::~CompoundRtcpParser() = default; + +bool CompoundRtcpParser::Parse(ByteView buffer, FrameId max_feedback_frame_id) { + // These will contain the results from the various ParseXYZ() methods. None of + // the results will be dispatched to the Client until the entire parse + // succeeds. + Clock::time_point receiver_reference_time = kNullTimePoint; + std::optional receiver_report; + std::vector log_messages; + FrameId checkpoint_frame_id; + milliseconds target_playout_delay{}; + std::vector received_frames; + std::vector packet_nacks; + bool picture_loss_indicator = false; + + // The data contained in `buffer` can be a "compound packet," which means that + // it can be the concatenation of multiple RTCP packets. The loop here + // processes each one-by-one. + while (!buffer.empty()) { + const auto header = RtcpCommonHeader::Parse(buffer); + if (!header) { + return false; + } + buffer = buffer.subspan(kRtcpCommonHeaderSize); + if (static_cast(buffer.size()) < header->payload_size) { + return false; + } + ByteView payload = buffer.subspan(0, header->payload_size); + buffer = buffer.subspan(header->payload_size); + + switch (header->packet_type) { + case RtcpPacketType::kReceiverReport: + if (!ParseReceiverReport(payload, header->with.report_count, + receiver_report)) { + return false; + } + break; + + case RtcpPacketType::kApplicationDefined: + if (!ParseApplicationDefined(header->with.subtype, payload, + log_messages)) { + return false; + } + break; + + case RtcpPacketType::kPayloadSpecific: + switch (header->with.subtype) { + case RtcpSubtype::kPictureLossIndicator: + if (!ParsePictureLossIndicator(payload, picture_loss_indicator)) { + return false; + } + break; + case RtcpSubtype::kFeedback: + if (!ParseFeedback(payload, max_feedback_frame_id, + &checkpoint_frame_id, &target_playout_delay, + &received_frames, packet_nacks)) { + return false; + } + break; + default: + // Ignore: Unimplemented or not part of the Cast Streaming spec. + break; + } + break; + + case RtcpPacketType::kExtendedReports: + if (!ParseExtendedReports(payload, receiver_reference_time)) { + return false; + } + break; + + default: + // Ignored, unimplemented or not part of the Cast Streaming spec. + break; + } + } + + // A well-behaved Cast Streaming Receiver will always include a reference time + // report. This essentially "timestamps" the RTCP packets just parsed. + // However, the spec does not explicitly require this be included. When it is + // present, improve the stability of the system by ignoring stale/out-of-order + // RTCP packets. + if (receiver_reference_time != kNullTimePoint) { + // If the packet is out-of-order (e.g., it got delayed/shuffled when going + // through the network), just ignore it. Since RTCP packets always include + // all the necessary current state from the peer, dropping them does not + // mean important signals will be lost. In fact, it can actually be harmful + // to process compound RTCP packets out-of-order. + if (latest_receiver_timestamp_ != kNullTimePoint && + receiver_reference_time < latest_receiver_timestamp_) { + return true; + } + latest_receiver_timestamp_ = receiver_reference_time; + client_->OnReceiverReferenceTimeAdvanced(latest_receiver_timestamp_); + } + + // At this point, the packet is known to be well-formed. Dispatch events of + // interest to the Client. + if (receiver_report) { + client_->OnReceiverReport(*receiver_report); + } + if (!log_messages.empty()) { + client_->OnCastReceiverFrameLogMessages(std::move(log_messages)); + } + if (!checkpoint_frame_id.is_null()) { + client_->OnReceiverCheckpoint(checkpoint_frame_id, target_playout_delay); + } + if (!received_frames.empty()) { + OSP_DCHECK(AreElementsSortedAndUnique(received_frames)); + client_->OnReceiverHasFrames(std::move(received_frames)); + } + CanonicalizePacketNackVector(&packet_nacks); + if (!packet_nacks.empty()) { + client_->OnReceiverIsMissingPackets(std::move(packet_nacks)); + } + if (picture_loss_indicator) { + client_->OnReceiverIndicatesPictureLoss(); + } + + return true; +} + +bool CompoundRtcpParser::ParseReceiverReport( + ByteView in, + int num_report_blocks, + std::optional& receiver_report) { + if (in.size() < kRtcpReceiverReportSize) { + return false; + } + if (ConsumeField(in) == session_->receiver_ssrc()) { + receiver_report = RtcpReportBlock::ParseOne(in, num_report_blocks, + session_->sender_ssrc()); + } + return true; +} + +bool CompoundRtcpParser::ParseApplicationDefined( + RtcpSubtype subtype, + ByteView in, + std::vector& messages) { + if (in.size() < 2 * sizeof(uint32_t)) { + return false; + } + + const uint32_t sender_ssrc = ConsumeField(in); + const uint32_t name = ConsumeField(in); + + // Just ignore events that aren't intended for us. + if (sender_ssrc != session_->receiver_ssrc()) { + return true; + } + if (name != kCastName) { + // We ignore time sync requests but don't throw an error for them. + return name == kTimeSyncRequestName; + } + if (subtype == RtcpSubtype::kReceiverLog) { + return ParseFrameLogMessages(in, messages); + } + return true; +} + +bool CompoundRtcpParser::ParseFrameLogMessages( + ByteView in, + std::vector& messages) { + while (!in.empty()) { + if (in.size() < kRtcpReceiverFrameLogMessageHeaderSize) { + messages.clear(); + return false; + } + const uint32_t truncated_rtp_timestamp = ConsumeField(in); + const uint32_t data = ConsumeField(in); + + // The 24 least significant bits contain the event timestamp, which is + // offset from when the first packet was sent. + const uint32_t raw_timestamp = data & 0xFFFFFF; + const Clock::time_point event_timestamp_base = + session_->start_time() + milliseconds(raw_timestamp); + + // The 8 most significant bits contain the number of events. + // NOTE: at least one event is required, so a value of "0" over the wire + // actually means there is one event. + const size_t num_events = 1u + static_cast(data >> 24); + + const RtpTimeTicks frame_log_rtp_timestamp = + latest_frame_log_rtp_timestamp_.Expand(truncated_rtp_timestamp); + RtcpReceiverFrameLogMessage frame_log_message{.rtp_timestamp = + frame_log_rtp_timestamp}; + + for (size_t event = 0; event < num_events; ++event) { + if (in.size() < kRtcpReceiverFrameLogMessageBlockSize) { + messages.clear(); + return false; + } + + const uint16_t delay_delta_or_packet_id = ConsumeField(in); + const uint16_t event_type_and_timestamp_delta = + ConsumeField(in); + + // Skip unknown event types, they are not useful. + const auto event_type = + StatisticsEvent::FromWireType(static_cast( + event_type_and_timestamp_delta >> 12)); + if (event_type == StatisticsEvent::Type::kUnknown) { + continue; + } + + RtcpReceiverEventLogMessage event_log{ + .type = event_type, + .timestamp = event_timestamp_base + + milliseconds(event_type_and_timestamp_delta & 0xFFF)}; + if (event_type == StatisticsEvent::Type::kPacketReceived) { + event_log.packet_id = delay_delta_or_packet_id; + } else { + event_log.delay = + milliseconds(static_cast(delay_delta_or_packet_id)); + } + frame_log_message.messages.emplace_back(std::move(event_log)); + } + latest_frame_log_rtp_timestamp_ = frame_log_rtp_timestamp; + messages.emplace_back(std::move(frame_log_message)); + } + + return true; +} + +bool CompoundRtcpParser::ParseFeedback(ByteView in, + FrameId max_feedback_frame_id, + FrameId* checkpoint_frame_id, + milliseconds* target_playout_delay, + std::vector* received_frames, + std::vector& packet_nacks) { + OSP_CHECK(!max_feedback_frame_id.is_null()); + + if (static_cast(in.size()) < kRtcpFeedbackHeaderSize) { + return false; + } + if (ConsumeField(in) != session_->receiver_ssrc() || + ConsumeField(in) != session_->sender_ssrc()) { + return true; // Ignore report from mismatched SSRC(s). + } + if (ConsumeField(in) != kRtcpCastIdentifierWord) { + return false; + } + + const FrameId feedback_frame_id = + max_feedback_frame_id.ExpandLessThanOrEqual(ConsumeField(in)); + const int loss_field_count = ConsumeField(in); + const auto playout_delay = milliseconds(ConsumeField(in)); + // Don't process feedback that would move the checkpoint backwards. The Client + // makes assumptions about what frame data and other tracking state can be + // discarded based on a monotonically non-decreasing checkpoint FrameId. + if (!checkpoint_frame_id->is_null() && + *checkpoint_frame_id > feedback_frame_id) { + return true; + } + *checkpoint_frame_id = feedback_frame_id; + *target_playout_delay = playout_delay; + received_frames->clear(); + packet_nacks.clear(); + if (static_cast(in.size()) < + (kRtcpFeedbackLossFieldSize * loss_field_count)) { + return false; + } + + // Parse the NACKs. + for (int i = 0; i < loss_field_count; ++i) { + const FrameId frame_id = + feedback_frame_id.ExpandGreaterThan(ConsumeField(in)); + FramePacketId packet_id = ConsumeField(in); + uint8_t bits = ConsumeField(in); + packet_nacks.push_back(PacketNack{frame_id, packet_id}); + + if (packet_id != kAllPacketsLost) { + // Translate each set bit in the bit vector into another missing + // FramePacketId. + while (bits) { + ++packet_id; + if (bits & 1) { + packet_nacks.push_back(PacketNack{frame_id, packet_id}); + } + bits >>= 1; + } + } + } + + // Parse the optional CST2 feedback (frame-level ACKs). + if (static_cast(in.size()) < kRtcpFeedbackAckHeaderSize || + ConsumeField(in) != kRtcpCst2IdentifierWord) { + // Optional CST2 extended feedback is not present. For backwards- + // compatibility reasons, do not consider any extra "garbage" in the packet + // that doesn't match 'CST2' as corrupted input. + return true; + } + // Skip over the "Feedback Count" field. It's currently unused, though it + // might be useful for event tracing later... + in = in.subspan(sizeof(uint8_t)); + const int ack_bitvector_octet_count = ConsumeField(in); + if (static_cast(in.size()) < ack_bitvector_octet_count) { + return false; + } + // Translate each set bit in the bit vector into a FrameId. See the + // explanation of this wire format in rtp_defines.h for where the "plus two" + // comes from. + FrameId starting_frame_id = feedback_frame_id + 2; + for (int i = 0; i < ack_bitvector_octet_count; ++i) { + uint8_t bits = ConsumeField(in); + FrameId frame_id = starting_frame_id; + while (bits) { + if (bits & 1) { + received_frames->push_back(frame_id); + } + ++frame_id; + bits >>= 1; + } + constexpr int kBitsPerOctet = 8; + starting_frame_id += kBitsPerOctet; + } + + return true; +} + +bool CompoundRtcpParser::ParseExtendedReports( + ByteView in, + Clock::time_point& receiver_reference_time) { + if (static_cast(in.size()) < kRtcpExtendedReportHeaderSize) { + return false; + } + if (ConsumeField(in) != session_->receiver_ssrc()) { + return true; // Ignore report from unknown receiver. + } + + while (!in.empty()) { + // All extended report types have the same 4-byte subheader. + if (static_cast(in.size()) < kRtcpExtendedReportBlockHeaderSize) { + return false; + } + const uint8_t block_type = ConsumeField(in); + in = in.subspan(sizeof(uint8_t)); // Skip the "reserved" byte. + const int block_data_size = + static_cast(ConsumeField(in)) * 4; + if (static_cast(in.size()) < block_data_size) { + return false; + } + if (block_type == kRtcpReceiverReferenceTimeReportBlockType) { + if (block_data_size != sizeof(uint64_t)) { + return false; // Length field must always be 2 words. + } + receiver_reference_time = session_->ntp_converter().ToLocalTime( + ReadBigEndian(in.data())); + } else { + // Ignore any other type of extended report. + } + in = in.subspan(block_data_size); + } + + return true; +} + +bool CompoundRtcpParser::ParsePictureLossIndicator( + ByteView in, + bool& picture_loss_indicator) { + if (static_cast(in.size()) < kRtcpPictureLossIndicatorHeaderSize) { + return false; + } + // Only set the flag if the PLI is from the Receiver and to this Sender. + if (ConsumeField(in) == session_->receiver_ssrc() && + ConsumeField(in) == session_->sender_ssrc()) { + picture_loss_indicator = true; + } + return true; +} + +CompoundRtcpParser::Client::Client() = default; +CompoundRtcpParser::Client::~Client() = default; +void CompoundRtcpParser::Client::OnReceiverReferenceTimeAdvanced( + Clock::time_point reference_time) {} +void CompoundRtcpParser::Client::OnReceiverReport( + const RtcpReportBlock& receiver_report) {} +void CompoundRtcpParser::Client::OnCastReceiverFrameLogMessages( + std::vector messages) {} +void CompoundRtcpParser::Client::OnReceiverIndicatesPictureLoss() {} +void CompoundRtcpParser::Client::OnReceiverCheckpoint( + FrameId frame_id, + milliseconds playout_delay) {} +void CompoundRtcpParser::Client::OnReceiverHasFrames( + std::vector acks) {} +void CompoundRtcpParser::Client::OnReceiverIsMissingPackets( + std::vector nacks) {} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/compound_rtcp_parser.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/compound_rtcp_parser.h new file mode 100644 index 0000000..38809ac --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/compound_rtcp_parser.h @@ -0,0 +1,135 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_COMPOUND_RTCP_PARSER_H_ +#define CAST_STREAMING_IMPL_COMPOUND_RTCP_PARSER_H_ + +#include +#include +#include + +#include "cast/streaming/impl/rtcp_common.h" +#include "cast/streaming/impl/rtp_defines.h" +#include "cast/streaming/public/frame_id.h" +#include "platform/base/span.h" +#include "util/raw_ref.h" + +namespace openscreen::cast { + +class RtcpSession; + +// Parses compound RTCP packets from a Receiver, invoking client callbacks when +// information of interest to a Sender (in the current process) is encountered. +class CompoundRtcpParser { + public: + // Callback interface used while parsing RTCP packets of interest to a Sender. + // The implementation must take into account: + // + // 1. Some/All of the data could be stale, as it only reflects the state of + // the Receiver at the time the packet was generated. A significant + // amount of time may have passed, depending on how long it took the + // packet to reach this local instance over the network. + // 2. The data shouldn't necessarily be trusted blindly: Some may be + // inconsistent (e.g., the same frame being ACKed and NACKed; or a frame + // that has not been sent yet is being NACKed). While that would indicate + // a badly-behaving Receiver, the Sender should be robust to such things. + class Client { + public: + Client(); + + // Called when a Receiver Reference Time Report has been parsed. + virtual void OnReceiverReferenceTimeAdvanced( + Clock::time_point reference_time); + + // Called when a Receiver Report with a Report Block has been parsed. + virtual void OnReceiverReport(const RtcpReportBlock& receiver_report); + + // Called when a group of Cast Receiver frame log messages has been parsed. + virtual void OnCastReceiverFrameLogMessages( + std::vector messages); + + // Called when the Receiver has encountered an unrecoverable error in + // decoding the data. The Sender should provide a key frame as soon as + // possible. + virtual void OnReceiverIndicatesPictureLoss(); + + // Called when the Receiver indicates that all of the packets for all frames + // up to and including `frame_id` have been successfully received (or + // otherwise do not need to be re-transmitted). The `playout_delay` is the + // Receiver's current end-to-end target playout delay setting, which should + // reflect any changes the Sender has made by using the "Cast Adaptive + // Latency Extension" in RTP packets. + virtual void OnReceiverCheckpoint(FrameId frame_id, + std::chrono::milliseconds playout_delay); + + // Called to indicate the Receiver has successfully received all of the + // packets for each of the given `acks`. The argument's elements are in + // monotonically increasing order. + virtual void OnReceiverHasFrames(std::vector acks); + + // Called to indicate the Receiver is missing certain specific packets for + // certain specific frames. Any elements where the packet_id is + // kAllPacketsLost indicates that all the packets are missing for a frame. + // The argument's elements are in monotonically increasing order. + virtual void OnReceiverIsMissingPackets(std::vector nacks); + + protected: + virtual ~Client(); + }; + + // `session` and `client` must be non-null and must outlive the + // CompoundRtcpParser instance. + CompoundRtcpParser(RtcpSession& session, Client& client); + ~CompoundRtcpParser(); + + // Parses the packet, invoking the Client callback methods when appropriate. + // Returns true if the `packet` was well-formed, or false if it was corrupt. + // Note that none of the Client callback methods will be invoked until a + // packet is known to be well-formed. + // + // `max_feedback_frame_id` is the maximum-valued FrameId that could possibly + // be ACKnowledged by the Receiver, if there is Cast Feedback in the `packet`. + // This is needed for expanding truncated frame IDs correctly. + bool Parse(ByteView packet, FrameId max_feedback_frame_id); + + private: + // These return true if the input was well-formed, and false if it was + // invalid/corrupt. The true/false value does NOT indicate whether the data + // contained within was ignored. Output arguments are only modified if the + // input contained the relevant field(s). + bool ParseReceiverReport(ByteView in, + int num_report_blocks, + std::optional& receiver_report); + bool ParseApplicationDefined( + RtcpSubtype subtype, + ByteView in, + std::vector& messages); + bool ParseFrameLogMessages( + ByteView in, + std::vector& messages); + bool ParseFeedback(ByteView in, + FrameId max_feedback_frame_id, + FrameId* checkpoint_frame_id, + std::chrono::milliseconds* target_playout_delay, + std::vector* received_frames, + std::vector& packet_nacks); + bool ParseExtendedReports(ByteView in, + Clock::time_point& receiver_reference_time); + bool ParsePictureLossIndicator(ByteView in, bool& picture_loss_indicator); + + const raw_ref session_; + const raw_ref client_; + + // Tracks the latest timestamp seen from any Receiver Reference Time Report, + // and uses this to ignore stale RTCP packets that arrived out-of-order and/or + // late from the network. + Clock::time_point latest_receiver_timestamp_; + + // Tracks the last parsed RTP timestamp seen from any Cast receiver frame log. + RtpTimeTicks latest_frame_log_rtp_timestamp_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_COMPOUND_RTCP_PARSER_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/expanded_value_base.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/expanded_value_base.h new file mode 100644 index 0000000..08af3e8 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/expanded_value_base.h @@ -0,0 +1,174 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_EXPANDED_VALUE_BASE_H_ +#define CAST_STREAMING_IMPL_EXPANDED_VALUE_BASE_H_ + +#include + +#include + +#include "util/osp_logging.h" + +namespace openscreen::cast { + +// Abstract base template class for common "sequence value" data types such as +// RtpTimeTicks, FrameId, or PacketId which generally increment/decrement in +// predictable amounts as media is streamed, and which often need to be reliably +// truncated and re-expanded for over-the-wire transmission. +// +// FullWidthInteger should be a signed integer POD type that is of sufficiently +// high width (in bits) such that it is never expected to under/overflow during +// the longest reasonable length of continuous system operation. Subclass is +// the class inheriting the common functionality provided in this template, and +// is used to provide operator overloads. The Subclass must friend this class +// to enable these operator overloads. +// +// Please see RtpTimeTicks and unit test code for examples of how to define +// Subclasses and add features specific to their concrete data type, and how to +// use data types derived from ExpandedValueBase. For example, a RtpTimeTicks +// adds math operators consisting of the meaningful and valid set of operations +// allowed for doing "time math." On the other hand, FrameId only adds math +// operators for incrementing/decrementing since multiplication and division are +// meaningless. +template +class ExpandedValueBase { + static_assert(std::numeric_limits::is_signed, + "FullWidthInteger must be a signed integer."); + static_assert(std::numeric_limits::is_integer, + "FullWidthInteger must be a signed integer."); + + public: + // Methods that return the lower bits of this value. This should only be used + // for serializing/wire-formatting, and not to subvert the restricted set of + // operators allowed on this data type. + constexpr uint8_t lower_8_bits() const { + return static_cast(value_); + } + constexpr uint16_t lower_16_bits() const { + return static_cast(value_); + } + constexpr uint32_t lower_32_bits() const { + return static_cast(value_); + } + + // Compute the greatest value less than or equal to `this` value whose lower + // bits are those of `x`. The purpose of this method is to re-instantiate an + // original value from its truncated form, usually when deserializing + // off-the-wire, when `this` value is known to be the greatest possible valid + // value. + // + // Use case example: Start with an original 32-bit value of 0x000001fe (510 + // decimal) and truncate, throwing away its upper 24 bits: 0xfe. Now, send + // this truncated value over-the-wire to a peer who needs to expand it back to + // the original 32-bit value. The peer knows that the greatest possible valid + // value is 0x00000202 (514 decimal). This method will initially attempt to + // just concatenate the upper 24 bits of |this->value_| with |x| (the 8-bit + // value), and get a result of 0x000002fe (766 decimal). However, this is + // greater than |this->value_|, so the upper 24 bits are subtracted by one to + // get 0x000001fe, which is the original value. + template + Subclass ExpandLessThanOrEqual(ShortUnsigned x) const { + static_assert(!std::numeric_limits::is_signed, + "`x` must be an unsigned integer."); + static_assert(std::numeric_limits::is_integer, + "`x` must be an unsigned integer."); + static_assert(sizeof(ShortUnsigned) <= sizeof(FullWidthInteger), + "`x` must fit within the FullWidthInteger."); + + if (sizeof(ShortUnsigned) < sizeof(FullWidthInteger)) { + // Initially, the `result` is composed of upper bits from `value_` and + // lower bits from `x`. + const FullWidthInteger short_max = + std::numeric_limits::max(); + FullWidthInteger result = (value_ & ~short_max) | x; + + // If the `result` is larger than `value_`, decrement the upper bits by + // one. In other words, `x` must always be interpreted as a truncated + // version of a value less than or equal to `value_`. + if (result > value_) + result -= short_max + 1; + + return Subclass(result); + } else { + // Debug builds: Ensure the highest bit is not set (which would cause + // overflow when casting to the signed integer). + OSP_CHECK_EQ( + static_cast(0), + x & (static_cast(1) << ((sizeof(x) * 8) - 1))); + return Subclass(x); + } + } + + // Compute the smallest value greater than `this` value whose lower bits are + // those of `x`. + template + Subclass ExpandGreaterThan(ShortUnsigned x) const { + const Subclass maximum_possible_result( + value_ + std::numeric_limits::max() + 1); + return maximum_possible_result.ExpandLessThanOrEqual(x); + } + + // Compute the value closest to `this` value whose lower bits are those of + // `x`. The result is always within `max_distance_for_expansion()` of `this` + // value. The purpose of this method is to re-instantiate an original value + // from its truncated form, usually when deserializing off-the-wire. See + // comments for ExpandLessThanOrEqual() above for further explanation. + template + Subclass Expand(ShortUnsigned x) const { + const Subclass maximum_possible_result( + value_ + max_distance_for_expansion()); + return maximum_possible_result.ExpandLessThanOrEqual(x); + } + + // Comparison operators. + constexpr bool operator==(const ExpandedValueBase& rhs) const { + return value_ == rhs.value_; + } + constexpr bool operator!=(const ExpandedValueBase& rhs) const { + return value_ != rhs.value_; + } + constexpr bool operator<(const ExpandedValueBase& rhs) const { + return value_ < rhs.value_; + } + constexpr bool operator>(const ExpandedValueBase& rhs) const { + return value_ > rhs.value_; + } + constexpr bool operator<=(const ExpandedValueBase& rhs) const { + return value_ <= rhs.value_; + } + constexpr bool operator>=(const ExpandedValueBase& rhs) const { + return value_ >= rhs.value_; + } + + // (De)Serialize for transmission over IPC. Do not use these to subvert the + // valid set of operators allowed by this class or its Subclass. + uint64_t SerializeForIPC() const { + static_assert(sizeof(uint64_t) >= sizeof(FullWidthInteger), + "Cannot serialize FullWidthInteger into an uint64_t."); + return static_cast(value_); + } + static Subclass DeserializeForIPC(uint64_t serialized) { + return Subclass(static_cast(serialized)); + } + + // Design limit: Values that are truncated to the ShortUnsigned type must be + // no more than this maximum distance from each other in order to ensure the + // original value can be determined correctly. + template + static constexpr FullWidthInteger max_distance_for_expansion() { + return std::numeric_limits::max() / 2; + } + + protected: + // Only subclasses are permitted to instantiate directly. + constexpr explicit ExpandedValueBase(FullWidthInteger value) + : value_(value) {} + + FullWidthInteger value_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_EXPANDED_VALUE_BASE_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/frame_crypto.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/frame_crypto.cc new file mode 100644 index 0000000..37f64c7 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/frame_crypto.cc @@ -0,0 +1,106 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/frame_crypto.h" + +#include +#include + +#include "openssl/crypto.h" +#include "openssl/err.h" +#include "openssl/rand.h" +#include "platform/base/span.h" +#include "util/big_endian.h" +#include "util/crypto/openssl_util.h" +#include "util/crypto/random_bytes.h" +#include "util/osp_logging.h" + +namespace openscreen::cast { + +EncryptedFrame::EncryptedFrame() { + data = owned_data_; +} + +EncryptedFrame::~EncryptedFrame() = default; + +EncryptedFrame::EncryptedFrame(EncryptedFrame&& other) noexcept + : EncodedFrame(static_cast(other)), + owned_data_(std::move(other.owned_data_)) { + data = owned_data_; + other.data = ByteView(); +} + +EncryptedFrame& EncryptedFrame::operator=(EncryptedFrame&& other) { + this->EncodedFrame::operator=(static_cast(other)); + owned_data_ = std::move(other.owned_data_); + data = owned_data_; + other.data = ByteView(); + return *this; +} + +FrameCrypto::FrameCrypto(const std::array& aes_key, + const std::array& cast_iv_mask) + : aes_key_{}, cast_iv_mask_(cast_iv_mask) { + // Ensure that the library has been initialized. CRYPTO_library_init() may be + // safely called multiple times during the life of a process. + CRYPTO_library_init(); + + // Initialize the 244-byte AES_KEY struct once, here at construction time. The + // const_cast<> is reasonable as this is a one-time-ctor-initialized value + // that will remain constant from here onward. + const int return_code = AES_set_encrypt_key( + aes_key.data(), aes_key.size() * 8, const_cast(&aes_key_)); + if (return_code != 0) { + ClearOpenSSLERRStack(CURRENT_LOCATION); + OSP_LOG_FATAL << "Failure when setting encryption key; unsafe to continue."; + OSP_NOTREACHED(); + } +} + +FrameCrypto::~FrameCrypto() = default; + +EncryptedFrame FrameCrypto::Encrypt(const EncodedFrame& encoded_frame) const { + EncryptedFrame result; + encoded_frame.CopyMetadataTo(&result); + result.owned_data_.resize(encoded_frame.data.size()); + result.data = result.owned_data_; + Crypt(encoded_frame.frame_id, {&encoded_frame.data, 1}, result.owned_data_); + return result; +} + +void FrameCrypto::Decrypt(FrameId frame_id, + ChunkList chunks, + ByteBuffer out) const { + Crypt(frame_id, chunks, out); +} + +void FrameCrypto::Crypt(FrameId frame_id, + ChunkList chunks, + ByteBuffer out) const { + OSP_CHECK(!frame_id.is_null()); + + // Compute the AES nonce for Cast Streaming payload encryption, which is based + // on the `frame_id`. + std::array aes_nonce{}; + static_assert(AES_BLOCK_SIZE == sizeof(aes_nonce), + "AES_BLOCK_SIZE is not 16 bytes."); + WriteBigEndian(frame_id.lower_32_bits(), aes_nonce.data() + 8); + for (size_t i = 0; i < aes_nonce.size(); ++i) { + aes_nonce[i] ^= cast_iv_mask_[i]; + } + + std::array ecount_buf{}; + unsigned int block_offset = 0; + size_t out_offset = 0; + for (ByteView chunk : chunks) { + OSP_CHECK_LE(out_offset + chunk.size(), out.size()); + AES_ctr128_encrypt(chunk.data(), out.data() + out_offset, chunk.size(), + &aes_key_, aes_nonce.data(), ecount_buf.data(), + &block_offset); + out_offset += chunk.size(); + } + OSP_CHECK_EQ(out_offset, out.size()); +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/frame_crypto.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/frame_crypto.h new file mode 100644 index 0000000..4804d5f --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/frame_crypto.h @@ -0,0 +1,78 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_FRAME_CRYPTO_H_ +#define CAST_STREAMING_IMPL_FRAME_CRYPTO_H_ + +#include +#include + +#include +#include + +#include "cast/streaming/public/encoded_frame.h" +#include "openssl/aes.h" +#include "platform/base/span.h" + +namespace openscreen::cast { + +class FrameCrypto; + +// A subclass of EncodedFrame that represents an EncodedFrame with encrypted +// payload data, and owns the buffer storing the encrypted payload data. Use +// FrameCrypto (below) to explicitly convert between EncryptedFrames and +// EncodedFrames. +struct EncryptedFrame : public EncodedFrame { + EncryptedFrame(); + ~EncryptedFrame(); + EncryptedFrame(EncryptedFrame&&) noexcept; + EncryptedFrame& operator=(EncryptedFrame&&); + + protected: + // Since only FrameCrypto is trusted to generate the + // payload data, it is allowed direct access to the storage. + friend class FrameCrypto; + + // Note: EncodedFrame::data must be updated whenever any mutations are + // performed on this member! + std::vector owned_data_; +}; + +// Encrypts EncodedFrames before sending, or decrypts EncryptedFrames that have +// been received. +class FrameCrypto { + public: + using ChunkList = std::span; + + // Construct with the given 16-bytes AES key and IV mask. Both arguments + // should be randomly-generated for each new streaming session. + // GenerateRandomBytes() can be used to create them. + FrameCrypto(const std::array& aes_key, + const std::array& cast_iv_mask); + + ~FrameCrypto(); + + EncryptedFrame Encrypt(const EncodedFrame& encoded_frame) const; + + // Decrypts `chunks` into `out`. `out` must have a sufficiently-sized + // data buffer. + void Decrypt(FrameId frame_id, ChunkList chunks, ByteBuffer out) const; + + private: + // The 244-byte AES_KEY struct, derived from the `aes_key` passed to the ctor, + // and initialized by boringssl's AES_set_encrypt_key() function. + const AES_KEY aes_key_; + + // Random bytes used in the custom heuristic to generate a different + // initialization vector for each frame. + const std::array cast_iv_mask_; + + // AES-CTR is symmetric. Thus, the "meat" of both Encrypt() and Decrypt() is + // the same. + void Crypt(FrameId frame_id, ChunkList chunks, ByteBuffer out) const; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_FRAME_CRYPTO_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/message_constants.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/message_constants.h new file mode 100644 index 0000000..8634add --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/message_constants.h @@ -0,0 +1,15 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_MESSAGE_CONSTANTS_H_ +#define CAST_STREAMING_IMPL_MESSAGE_CONSTANTS_H_ + +namespace openscreen::cast { + +// RTP extension strings. +inline constexpr char kInputEventsRtpExtension[] = "input_events"; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_MESSAGE_CONSTANTS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/ntp_time.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/ntp_time.cc new file mode 100644 index 0000000..d078d43 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/ntp_time.cc @@ -0,0 +1,54 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/ntp_time.h" + +#include "util/osp_logging.h" + +namespace openscreen::cast { + +namespace { + +// The number of seconds between 1 January 1900 and 1 January 1970. +constexpr NtpSeconds kTimeBetweenNtpEpochAndUnixEpoch(2208988800); + +} // namespace + +NtpTimeConverter::NtpTimeConverter(Clock::time_point now, + std::chrono::seconds since_unix_epoch) + : start_time_(now), + since_ntp_epoch_( + std::chrono::duration_cast(since_unix_epoch) + + kTimeBetweenNtpEpochAndUnixEpoch) {} + +NtpTimeConverter::~NtpTimeConverter() = default; + +NtpTimestamp NtpTimeConverter::ToNtpTimestamp( + Clock::time_point time_point) const { + const Clock::duration time_since_start = time_point - start_time_; + const auto whole_seconds = + std::chrono::duration_cast(time_since_start); + const auto remainder = + std::chrono::duration_cast(time_since_start - whole_seconds); + return AssembleNtpTimestamp(since_ntp_epoch_ + whole_seconds, remainder); +} + +Clock::time_point NtpTimeConverter::ToLocalTime(NtpTimestamp timestamp) const { + auto ntp_seconds = NtpSecondsPart(timestamp); + // Year 2036 wrap-around check: If the NTP timestamp appears to be a + // point-in-time before 1970, assume the 2036 wrap-around has occurred, and + // adjust to compensate. + if (ntp_seconds <= kTimeBetweenNtpEpochAndUnixEpoch) { + constexpr NtpSeconds kNtpSecondsPerEra{INT64_C(1) << 32}; + ntp_seconds += kNtpSecondsPerEra; + } + + const auto whole_seconds = ntp_seconds - since_ntp_epoch_; + const auto seconds_since_start = + Clock::to_duration(whole_seconds) + start_time_; + const auto remainder = Clock::to_duration(NtpFractionPart(timestamp)); + return seconds_since_start + remainder; +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/ntp_time.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/ntp_time.h new file mode 100644 index 0000000..7258fed --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/ntp_time.h @@ -0,0 +1,73 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_NTP_TIME_H_ +#define CAST_STREAMING_IMPL_NTP_TIME_H_ + +#include + +#include "platform/api/time.h" + +namespace openscreen::cast { + +// NTP timestamps are 64-bit timestamps that consist of two 32-bit parts: 1) The +// number of seconds since 1 January 1900; and 2) The fraction of the second, +// where 0 maps to 0x00000000 and each unit increment represents another 2^-32 +// seconds. +// +// Note that it is part of the design of NTP for the seconds part to roll around +// on 7 February 2036. +using NtpTimestamp = uint64_t; + +// NTP fixed-point time math: Declare two std::chrono::duration types with the +// bit-width necessary to reliably perform all conversions to/from NTP format. +using NtpSeconds = std::chrono::duration; +using NtpFraction = + std::chrono::duration>; + +constexpr NtpSeconds NtpSecondsPart(NtpTimestamp timestamp) { + return NtpSeconds(timestamp >> 32); +} + +constexpr NtpFraction NtpFractionPart(NtpTimestamp timestamp) { + return NtpFraction(timestamp & 0xffffffff); +} + +constexpr NtpTimestamp AssembleNtpTimestamp(NtpSeconds seconds, + NtpFraction fraction) { + return (static_cast(seconds.count()) << 32) | + static_cast(fraction.count()); +} + +// Converts between Clock::time_points and NtpTimestamps. The class is +// instantiated with the current Clock time and the current wall clock time, and +// these are used to determine a fixed origin reference point for all +// conversions. Thus, to avoid introducing unintended timing-related behaviors, +// only one NtpTimeConverter instance should be used for converting all the NTP +// timestamps in the same streaming session. +class NtpTimeConverter { + public: + NtpTimeConverter( + Clock::time_point now, + std::chrono::seconds since_unix_epoch = GetWallTimeSinceUnixEpoch()); + ~NtpTimeConverter(); + + NtpTimestamp ToNtpTimestamp(Clock::time_point time_point) const; + Clock::time_point ToLocalTime(NtpTimestamp timestamp) const; + + private: + // The time point on the platform clock's timeline that corresponds to + // approximately the same time point on the NTP timeline. Note that it is + // acceptable for the granularity of the NTP seconds value to be whole seconds + // here: Both a Cast Streaming Sender and Receiver will assume their clocks + // can be off (with respect to each other) by even a large amount; and all + // that matters is that time ticks forward at a reasonable pace from some + // initial point. + const Clock::time_point start_time_; + const NtpSeconds since_ntp_epoch_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_NTP_TIME_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/packet_util.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/packet_util.cc new file mode 100644 index 0000000..129061d --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/packet_util.cc @@ -0,0 +1,39 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/packet_util.h" + +#include "cast/streaming/impl/rtcp_common.h" +#include "cast/streaming/impl/rtp_defines.h" + +namespace openscreen::cast { + +std::pair InspectPacketForRouting(ByteView packet) { + // Check for RTP packets first, since they are more frequent. + if (packet.size() >= kRtpPacketMinValidSize && + packet[0] == kRtpRequiredFirstByte && + IsRtpPayloadType(packet[1] & kRtpPayloadTypeMask)) { + constexpr int kOffsetToSsrcField = 8; + return std::make_pair( + ApparentPacketType::RTP, + Ssrc{ReadBigEndian(packet.data() + kOffsetToSsrcField)}); + } + + // While RTCP packets are valid if they consist of just the RTCP Common + // Header, all the RTCP packet types processed by this implementation will + // also have a SSRC field immediately following the header. This is important + // for routing the packet to the correct parser instance. + constexpr int kRtcpPacketMinAcceptableSize = + kRtcpCommonHeaderSize + sizeof(uint32_t); + if (packet.size() >= kRtcpPacketMinAcceptableSize && + RtcpCommonHeader::Parse(packet).has_value()) { + return std::make_pair( + ApparentPacketType::RTCP, + Ssrc{ReadBigEndian(packet.data() + kRtcpCommonHeaderSize)}); + } + + return std::make_pair(ApparentPacketType::UNKNOWN, Ssrc{0}); +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/packet_util.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/packet_util.h new file mode 100644 index 0000000..f9d3771 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/packet_util.h @@ -0,0 +1,60 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_PACKET_UTIL_H_ +#define CAST_STREAMING_IMPL_PACKET_UTIL_H_ + +#include + +#include "cast/streaming/ssrc.h" +#include "platform/base/span.h" +#include "util/big_endian.h" +#include "util/osp_logging.h" + +namespace openscreen::cast { + +// Reads a field from the start of the given span and advances the span to point +// just after the field. +template +inline Integer ConsumeField(ByteView& in) { + OSP_CHECK_GE(in.size(), sizeof(Integer)); + const Integer result = ReadBigEndian(in.data()); + in = in.subspan(sizeof(Integer)); + return result; +} + +// Writes a field at the start of the given span and advances the span to point +// just after the field. +template +inline void AppendField(Integer value, ByteBuffer& out) { + WriteBigEndian(value, out.data()); + out = out.subspan(sizeof(Integer)); +} + +// Returns a bitmask for a field having the given number of bits. For example, +// FieldBitmask(5) returns 0b00011111. +template +constexpr Integer FieldBitmask(unsigned field_size_in_bits) { + return (Integer{1} << field_size_in_bits) - 1; +} + +// Reserves `num_bytes` from the beginning of the given span, returning the +// reserved space. +inline ByteBuffer ReserveSpace(int num_bytes, ByteBuffer& out) { + const ByteBuffer reserved = out.subspan(0, num_bytes); + out = out.subspan(num_bytes); + return reserved; +} + +// Performs a quick-scan of the packet data for the purposes of routing it to an +// appropriate parser. Identifies whether the packet is a RTP packet, RTCP +// packet, or unknown; and provides the originator's SSRC. This only performs a +// very quick scan of the packet data, and does not guarantee that a full parse +// will later succeed. +enum class ApparentPacketType { UNKNOWN, RTP, RTCP }; +std::pair InspectPacketForRouting(ByteView packet); + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_PACKET_UTIL_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_common.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_common.cc new file mode 100644 index 0000000..78f0287 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_common.cc @@ -0,0 +1,241 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/rtcp_common.h" + +#include +#include + +#include "cast/streaming/impl/packet_util.h" +#include "util/saturate_cast.h" + +namespace openscreen::cast { + +RtcpCommonHeader::RtcpCommonHeader() = default; +RtcpCommonHeader::~RtcpCommonHeader() = default; + +void RtcpCommonHeader::AppendFields(ByteBuffer& buffer) const { + OSP_CHECK_GE(buffer.size(), kRtcpCommonHeaderSize); + + uint8_t byte0 = kRtcpRequiredVersionAndPaddingBits + << kRtcpReportCountFieldNumBits; + switch (packet_type) { + case RtcpPacketType::kSenderReport: + case RtcpPacketType::kReceiverReport: + OSP_CHECK_LE(with.report_count, + FieldBitmask(kRtcpReportCountFieldNumBits)); + byte0 |= with.report_count; + break; + case RtcpPacketType::kSourceDescription: + OSP_UNIMPLEMENTED(); + break; + case RtcpPacketType::kApplicationDefined: + case RtcpPacketType::kPayloadSpecific: + switch (with.subtype) { + case RtcpSubtype::kPictureLossIndicator: + case RtcpSubtype::kFeedback: + case RtcpSubtype::kReceiverLog: + byte0 |= static_cast(with.subtype); + break; + + // We should not be creating application or payload specific packets + // with an unknown or null subtype -- they will just be ignored. + case RtcpSubtype::kNull: + OSP_NOTREACHED(); + } + break; + case RtcpPacketType::kExtendedReports: + break; + case RtcpPacketType::kNull: + OSP_NOTREACHED(); + } + AppendField(byte0, buffer); + + AppendField(static_cast(packet_type), buffer); + + // The size of the packet must be evenly divisible by the 32-bit word size. + OSP_CHECK_EQ(0, payload_size % sizeof(uint32_t)); + AppendField(payload_size / sizeof(uint32_t), buffer); +} + +// static +std::optional RtcpCommonHeader::Parse(ByteView buffer) { + if (buffer.size() < kRtcpCommonHeaderSize) { + return std::nullopt; + } + + const uint8_t byte0 = ConsumeField(buffer); + if ((byte0 >> kRtcpReportCountFieldNumBits) != + kRtcpRequiredVersionAndPaddingBits) { + return std::nullopt; + } + const uint8_t report_count_or_subtype = + byte0 & FieldBitmask(kRtcpReportCountFieldNumBits); + + const uint8_t byte1 = ConsumeField(buffer); + if (!IsRtcpPacketType(byte1)) { + return std::nullopt; + } + + // Optionally set `header.with.report_count` or `header.with.subtype`, + // depending on the packet type. + RtcpCommonHeader header; + header.packet_type = static_cast(byte1); + switch (header.packet_type) { + case RtcpPacketType::kSenderReport: + case RtcpPacketType::kReceiverReport: + header.with.report_count = report_count_or_subtype; + break; + case RtcpPacketType::kApplicationDefined: + case RtcpPacketType::kPayloadSpecific: + switch (static_cast(report_count_or_subtype)) { + case RtcpSubtype::kPictureLossIndicator: + case RtcpSubtype::kReceiverLog: + case RtcpSubtype::kFeedback: + header.with.subtype = + static_cast(report_count_or_subtype); + break; + default: // Unknown subtype. + header.with.subtype = RtcpSubtype::kNull; + break; + } + break; + default: + // Neither `header.with.report_count` nor `header.with.subtype` are used. + break; + } + + header.payload_size = + static_cast(ConsumeField(buffer)) * sizeof(uint32_t); + + return header; +} + +RtcpReportBlock::RtcpReportBlock() = default; +RtcpReportBlock::~RtcpReportBlock() = default; + +void RtcpReportBlock::AppendFields(ByteBuffer& buffer) const { + OSP_CHECK_GE(buffer.size(), kRtcpReportBlockSize); + + AppendField(ssrc, buffer); + OSP_CHECK_GE(packet_fraction_lost_numerator, + std::numeric_limits::min()); + OSP_CHECK_LE(packet_fraction_lost_numerator, + std::numeric_limits::max()); + OSP_CHECK_GE(cumulative_packets_lost, 0); + OSP_CHECK_LE(cumulative_packets_lost, + FieldBitmask(kRtcpCumulativePacketsFieldNumBits)); + AppendField( + (static_cast(packet_fraction_lost_numerator) + << kRtcpCumulativePacketsFieldNumBits) | + (static_cast(cumulative_packets_lost) & + FieldBitmask(kRtcpCumulativePacketsFieldNumBits)), + buffer); + AppendField(extended_high_sequence_number, buffer); + const int64_t jitter_ticks = jitter / RtpTimeDelta::FromTicks(1); + OSP_CHECK_GE(jitter_ticks, 0); + OSP_CHECK_LE(jitter_ticks, int64_t{std::numeric_limits::max()}); + AppendField(jitter_ticks, buffer); + AppendField(last_status_report_id, buffer); + const int64_t delay_ticks = delay_since_last_report.count(); + OSP_CHECK_GE(delay_ticks, 0); + OSP_CHECK_LE(delay_ticks, int64_t{std::numeric_limits::max()}); + AppendField(delay_ticks, buffer); +} + +void RtcpReportBlock::SetPacketFractionLostNumerator( + int64_t num_apparently_sent, + int64_t num_received) { + if (num_apparently_sent <= 0) { + packet_fraction_lost_numerator = 0; + return; + } + // The following computes the fraction of packets lost as "one minus + // `num_received` divided by `num_apparently_sent`" and scales by 256 (the + // kPacketFractionLostDenominator). It's valid for `num_received` to be + // greater than `num_apparently_sent` in some cases (e.g., if duplicate + // packets were received from the network). + const int64_t numerator = + ((num_apparently_sent - num_received) * kPacketFractionLostDenominator) / + num_apparently_sent; + // Since the value must be in the range [0,255], just do a saturate_cast + // to the uint8_t type to clamp. + packet_fraction_lost_numerator = saturate_cast(numerator); +} + +void RtcpReportBlock::SetCumulativePacketsLost(int64_t num_apparently_sent, + int64_t num_received) { + const int64_t num_lost = num_apparently_sent - num_received; + // Clamp to valid range supported by the wire format (and RTP spec). + // + // Note that `num_lost` can be negative if duplicate packets were received. + // The RFC spec (https://tools.ietf.org/html/rfc3550#section-6.4.1) states + // this should result in a clamped, "zero loss" value. + cumulative_packets_lost = static_cast( + std::min(std::max(num_lost, 0), + FieldBitmask(kRtcpCumulativePacketsFieldNumBits))); +} + +void RtcpReportBlock::SetDelaySinceLastReport( + Clock::duration local_clock_delay) { + // Clamp to valid range supported by the wire format (and RTP spec). The + // bounds checking is done in terms of Clock::duration, since doing the checks + // after the duration_cast may allow overflow to occur in the duration_cast + // math (well, only for unusually large inputs). + constexpr Delay kMaxValidReportedDelay(std::numeric_limits::max()); + constexpr auto kMaxValidLocalClockDelay = + Clock::to_duration(kMaxValidReportedDelay); + if (local_clock_delay > kMaxValidLocalClockDelay) { + delay_since_last_report = kMaxValidReportedDelay; + return; + } + if (local_clock_delay <= Clock::duration::zero()) { + delay_since_last_report = Delay::zero(); + return; + } + + // If this point is reached, then the `local_clock_delay` is representable as + // a Delay within the valid range. + delay_since_last_report = + std::chrono::duration_cast(local_clock_delay); +} + +// static +std::optional RtcpReportBlock::ParseOne(ByteView buffer, + int report_count, + Ssrc ssrc) { + if (static_cast(buffer.size()) < (kRtcpReportBlockSize * report_count)) { + return std::nullopt; + } + + std::optional result; + for (int block = 0; block < report_count; ++block) { + if (ConsumeField(buffer) != ssrc) { + // Skip-over report block meant for some other recipient. + buffer = buffer.subspan(kRtcpReportBlockSize - sizeof(uint32_t)); + continue; + } + + RtcpReportBlock& report_block = result.emplace(); + report_block.ssrc = ssrc; + const auto second_word = ConsumeField(buffer); + report_block.packet_fraction_lost_numerator = + second_word >> kRtcpCumulativePacketsFieldNumBits; + report_block.cumulative_packets_lost = + second_word & + FieldBitmask(kRtcpCumulativePacketsFieldNumBits); + report_block.extended_high_sequence_number = ConsumeField(buffer); + report_block.jitter = + RtpTimeDelta::FromTicks(ConsumeField(buffer)); + report_block.last_status_report_id = ConsumeField(buffer); + report_block.delay_since_last_report = + RtcpReportBlock::Delay(ConsumeField(buffer)); + } + return result; +} + +RtcpSenderReport::RtcpSenderReport() = default; +RtcpSenderReport::~RtcpSenderReport() = default; + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_common.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_common.h new file mode 100644 index 0000000..7f13d1e --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_common.h @@ -0,0 +1,203 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_RTCP_COMMON_H_ +#define CAST_STREAMING_IMPL_RTCP_COMMON_H_ + +#include + +#include +#include +#include + +#include "cast/streaming/impl/ntp_time.h" +#include "cast/streaming/impl/rtp_defines.h" +#include "cast/streaming/impl/statistics_common.h" +#include "cast/streaming/public/frame_id.h" +#include "cast/streaming/rtp_time.h" +#include "cast/streaming/ssrc.h" +#include "platform/base/span.h" + +namespace openscreen::cast { + +struct RtcpCommonHeader { + RtcpCommonHeader(); + ~RtcpCommonHeader(); + + RtcpPacketType packet_type = RtcpPacketType::kNull; + + union { + // The number of report blocks if `packet_type` is kSenderReport or + // kReceiverReport. + int report_count; + + // Indicates the type of an application-defined message if `packet_type` is + // kApplicationDefined or kPayloadSpecific. + RtcpSubtype subtype; + + // Otherwise, not used. + } with{0}; + + // The size (in bytes) of the RTCP packet, not including the header. + int payload_size = 0; + + // Serializes this header into the first `kRtcpCommonHeaderSize` bytes of the + // given `buffer` and adjusts `buffer` to point to the first byte after it. + void AppendFields(ByteBuffer& buffer) const; + + // Parse from the 4-byte wire format in `buffer`. Returns nullopt if the data + // is corrupt. + static std::optional Parse(ByteView buffer); +}; + +// The middle 32-bits of the 64-bit NtpTimestamp field from the Sender Reports. +// This is used as an opaque identifier that the Receiver will use in its +// reports to refer to specific previous Sender Reports. +using StatusReportId = uint32_t; +constexpr StatusReportId ToStatusReportId(NtpTimestamp ntp_timestamp) { + return static_cast(ntp_timestamp >> 16); +} + +// One of these is optionally included with a Sender Report or a Receiver +// Report. See: https://tools.ietf.org/html/rfc3550#section-6.4.1 +struct RtcpReportBlock { + RtcpReportBlock(); + ~RtcpReportBlock(); + + // The intended recipient of this report block. + Ssrc ssrc = 0; + + // The fraction of RTP packets lost since the last report, specified as a + // variable numerator and fixed denominator. The numerator will always be in + // the range [0,255] since, semantically: + // + // a. Negative values are impossible. + // b. Values greater than 255 would indicate 100% packet loss, and so a + // report block would not be generated in the first place. + int packet_fraction_lost_numerator = 0; + static constexpr int kPacketFractionLostDenominator = 256; + + // The total number of RTP packets lost since the start of the session. This + // value will always be in the range [0,2^24-1], as the wire format only + // provides 24 bits; so, wrap-around is possible. + int cumulative_packets_lost = 0; + + // The highest sequence number received in any RTP packet. Wrap-around is + // possible. + uint32_t extended_high_sequence_number = 0; + + // An estimate of the recent variance in RTP packet arrival times. + RtpTimeDelta jitter; + + // The last Status Report received. + StatusReportId last_status_report_id{}; + + // The delay between when the peer received the most-recent Status Report and + // when this report was sent. The timebase is 65536 ticks per second and, + // because of the wire format, this value will always be in the range + // [0,65536) seconds. + using Delay = std::chrono::duration>; + Delay delay_since_last_report{}; + + // Convenience helper to compute/assign the `packet_fraction_lost_numerator`, + // based on the `num_apparently_sent` and `num_received` packet counts since + // the last report was sent. + void SetPacketFractionLostNumerator(int64_t num_apparently_sent, + int64_t num_received); + + // Convenience helper to compute/assign the `cumulative_packets_lost`, based + // on the `num_apparently_sent` and `num_received` packet counts since the + // start of the entire session. + void SetCumulativePacketsLost(int64_t num_apparently_sent, + int64_t num_received); + + // Convenience helper to convert the given `local_clock_delay` to the + // RtcpReportBlock::Delay timebase, then clamp and assign it to + // `delay_since_last_report`. + void SetDelaySinceLastReport(Clock::duration local_clock_delay); + + // Serializes this report block in the first `kRtcpReportBlockSize` bytes of + // the given `buffer` and adjusts `buffer` to point to the first byte after + // it. + void AppendFields(ByteBuffer& buffer) const; + + // Scans the wire-format report blocks in `buffer`, searching for one with the + // matching `ssrc` and, if found, returns the parse result. Returns nullopt if + // the data is corrupt or no report block with the matching SSRC was found. + static std::optional ParseOne(ByteView buffer, + int report_count, + Ssrc ssrc); +}; + +struct RtcpSenderReport { + RtcpSenderReport(); + ~RtcpSenderReport(); + + // The point-in-time at which this report was sent, according to both: 1) the + // common reference clock shared by all RTP streams; 2) the RTP timestamp on + // the media capture/playout timeline. Together, these are used by a Receiver + // to achieve A/V synchronization across RTP streams for playout. + Clock::time_point reference_time{}; + RtpTimeTicks rtp_timestamp; + + // The total number of RTP packets transmitted since the start of the session + // (wrap-around is possible). + uint32_t send_packet_count = 0; + + // The total number of payload bytes transmitted in RTP packets since the + // start of the session (wrap-around is possible). + uint32_t send_octet_count = 0; + + // The report block, if present. While the RTCP spec allows for zero or + // multiple reports, Cast Streaming only uses zero or one. + std::optional report_block; +}; + +// A pair of IDs that refers to a specific missing packet within a frame. If +// `packet_id` is kAllPacketsLost, then it represents all the packets of a +// frame. +struct PacketNack { + FrameId frame_id; + FramePacketId packet_id; + + constexpr bool operator==(const PacketNack& other) const { + return frame_id == other.frame_id && packet_id == other.packet_id; + } + constexpr bool operator!=(const PacketNack& other) const { + return frame_id != other.frame_id || packet_id != other.packet_id; + } + constexpr bool operator<(const PacketNack& other) const { + return (frame_id < other.frame_id) || + (frame_id == other.frame_id && packet_id < other.packet_id); + } +}; + +// Statistics events sent from the receiver over RTCP. +struct RtcpReceiverEventLogMessage { + // The statistics event type, may be either a receiver side frame event or + // packet event. + StatisticsEvent::Type type; + + // The time at which this event occurred. + Clock::time_point timestamp; + + // Only set for frame played out events. + // If this value is zero the frame is rendered on time. + // If this value is positive it means the frame is rendered late. + // If this value is negative it means the frame is rendered early. + Clock::duration delay; + + // Only set for packet events. + // The ID of the packet associated with this event. + FramePacketId packet_id; +}; + +struct RtcpReceiverFrameLogMessage { + RtpTimeTicks rtp_timestamp; + std::vector messages; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_RTCP_COMMON_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_session.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_session.cc new file mode 100644 index 0000000..3acef68 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_session.cc @@ -0,0 +1,25 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/rtcp_session.h" + +#include "util/osp_logging.h" + +namespace openscreen::cast { + +RtcpSession::RtcpSession(Ssrc sender_ssrc, + Ssrc receiver_ssrc, + Clock::time_point start_time) + : sender_ssrc_(sender_ssrc), + receiver_ssrc_(receiver_ssrc), + start_time_(start_time), + ntp_converter_(start_time) { + OSP_CHECK_NE(sender_ssrc_, kNullSsrc); + OSP_CHECK_NE(receiver_ssrc_, kNullSsrc); + OSP_CHECK_NE(sender_ssrc_, receiver_ssrc_); +} + +RtcpSession::~RtcpSession() = default; + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_session.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_session.h new file mode 100644 index 0000000..f358f2e --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtcp_session.h @@ -0,0 +1,43 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_RTCP_SESSION_H_ +#define CAST_STREAMING_IMPL_RTCP_SESSION_H_ + +#include "cast/streaming/impl/ntp_time.h" +#include "cast/streaming/ssrc.h" + +namespace openscreen::cast { + +// Session-level configuration and shared components for the RTCP messaging +// associated with a single Cast RTP stream. Multiple packet serialization and +// parsing components share a single RtcpSession instance for data consistency. +class RtcpSession { + public: + // `start_time` should be the current time, as it is used by NtpTimeConverter + // to set a fixed reference point between the local Clock and current "real + // world" wall time. + RtcpSession(Ssrc sender_ssrc, + Ssrc receiver_ssrc, + Clock::time_point start_time); + ~RtcpSession(); + + Ssrc sender_ssrc() const { return sender_ssrc_; } + Ssrc receiver_ssrc() const { return receiver_ssrc_; } + const NtpTimeConverter& ntp_converter() const { return ntp_converter_; } + Clock::time_point start_time() const { return start_time_; } + + private: + const Ssrc sender_ssrc_; + const Ssrc receiver_ssrc_; + + Clock::time_point start_time_; + + // Translates between system time (internal format) and NTP (wire format). + NtpTimeConverter ntp_converter_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_RTCP_SESSION_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_defines.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_defines.cc new file mode 100644 index 0000000..ba432c5 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_defines.cc @@ -0,0 +1,113 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/rtp_defines.h" + +#include "util/osp_logging.h" + +namespace openscreen::cast { + +RtpPayloadType GetPayloadType(AudioCodec codec, bool use_android_rtp_hack) { + if (use_android_rtp_hack) { + return RtpPayloadType::kAudioHackForAndroidTV; + } + + switch (codec) { + case AudioCodec::kAac: + return RtpPayloadType::kAudioAac; + case AudioCodec::kOpus: + return RtpPayloadType::kAudioOpus; + case AudioCodec::kNotSpecified: + return RtpPayloadType::kAudioVarious; + default: + OSP_NOTREACHED(); + } +} + +RtpPayloadType GetPayloadType(VideoCodec codec, bool use_android_rtp_hack) { + if (use_android_rtp_hack) { + return RtpPayloadType::kVideoHackForAndroidTV; + } + switch (codec) { + // VP8 and VP9 share the same payload type. + case VideoCodec::kVp9: + case VideoCodec::kVp8: + return RtpPayloadType::kVideoVp8; + + // H264 and HEVC/H265 share the same payload type. + case VideoCodec::kHevc: // fallthrough + case VideoCodec::kH264: + return RtpPayloadType::kVideoH264; + + case VideoCodec::kAv1: + return RtpPayloadType::kVideoAv1; + + case VideoCodec::kNotSpecified: + return RtpPayloadType::kVideoVarious; + + default: + OSP_NOTREACHED(); + } +} + +StreamType ToStreamType(RtpPayloadType type, bool use_android_rtp_hack) { + if (use_android_rtp_hack) { + if (type == RtpPayloadType::kAudioHackForAndroidTV) { + return StreamType::kAudio; + } + if (type == RtpPayloadType::kVideoHackForAndroidTV) { + return StreamType::kVideo; + } + } + + if (RtpPayloadType::kAudioFirst <= type && + type <= RtpPayloadType::kAudioLast) { + return StreamType::kAudio; + } + if (RtpPayloadType::kVideoFirst <= type && + type <= RtpPayloadType::kVideoLast) { + return StreamType::kVideo; + } + return StreamType::kUnknown; +} + +bool IsRtpPayloadType(uint8_t raw_byte) { + switch (static_cast(raw_byte)) { + case RtpPayloadType::kAudioOpus: + case RtpPayloadType::kAudioAac: + case RtpPayloadType::kAudioPcm16: + case RtpPayloadType::kAudioVarious: + case RtpPayloadType::kVideoVp8: + case RtpPayloadType::kVideoH264: + case RtpPayloadType::kVideoVp9: + case RtpPayloadType::kVideoAv1: + case RtpPayloadType::kVideoVarious: + case RtpPayloadType::kAudioHackForAndroidTV: + // Note: RtpPayloadType::kVideoHackForAndroidTV has the same value as + // kAudioOpus. + return true; + + case RtpPayloadType::kNull: + break; + } + return false; +} + +bool IsRtcpPacketType(uint8_t raw_byte) { + switch (static_cast(raw_byte)) { + case RtcpPacketType::kSenderReport: + case RtcpPacketType::kReceiverReport: + case RtcpPacketType::kSourceDescription: + case RtcpPacketType::kApplicationDefined: + case RtcpPacketType::kPayloadSpecific: + case RtcpPacketType::kExtendedReports: + return true; + + case RtcpPacketType::kNull: + break; + } + return false; +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_defines.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_defines.h new file mode 100644 index 0000000..e1d82b8 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_defines.h @@ -0,0 +1,382 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_RTP_DEFINES_H_ +#define CAST_STREAMING_IMPL_RTP_DEFINES_H_ + +#include + +#include "cast/streaming/public/constants.h" + +namespace openscreen::cast { + +// Note: Cast Streaming uses a subset of the messages in the RTP/RTCP +// specification, but also adds some of its own extensions. See: +// https://tools.ietf.org/html/rfc3550 + +// Uniquely identifies one packet within a frame. These are sequence numbers, +// starting at 0. Each Cast RTP packet also includes the "last ID" so that a +// receiver always knows the range of valid FramePacketIds for a given frame. +using FramePacketId = uint16_t; + +// A special FramePacketId value meant to represent "all packets lost" in Cast +// RTCP Feedback messages. +inline constexpr FramePacketId kAllPacketsLost = 0xffff; +inline constexpr FramePacketId kMaxAllowedFramePacketId = kAllPacketsLost - 1; + +// The maximum size of any RTP or RTCP packet, in bytes. The calculation below +// is: Standard Ethernet MTU bytes minus IP header bytes minus UDP header bytes. +// The remainder is available for RTP/RTCP packet data (header + payload). +// +// A nice explanation of this: https://jvns.ca/blog/2017/02/07/mtu/ +// +// Constants are provided here for UDP over IPv4 and IPv6 on Ethernet. Other +// transports and network mediums will need additional consideration, alternate +// calculations. Note that MTU is dynamic, depending on the path the packets +// take between two endpoints (the 1500 here is just a commonly-used value for +// LAN Ethernet). +inline constexpr int kMaxRtpPacketSizeForIpv4UdpOnEthernet = 1500 - 20 - 8; +inline constexpr int kMaxRtpPacketSizeForIpv6UdpOnEthernet = 1500 - 40 - 8; + +// The Cast RTP packet header: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ ^ +// |V=2|P|X| CC=0 |M| PT | sequence number | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+RTP +// + RTP timestamp |Spec +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | +// + synchronization source (SSRC) identifier | v +// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ +// |K|R| EXT count | FID | PID | ^ +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+Cast +// | Max PID | optional fields, extensions, Spec +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ then payload... v +// +// Byte 0: Version 2, no padding, no RTP extensions, no CSRCs. +// Byte 1: Marker bit indicates whether this is the last packet, followed by a +// 7-bit payload type. +// Byte 12: Key Frame bit, followed by "RFID will be provided" bit, followed by +// 6 bits specifying the number of extensions that will be provided. + +// The minimum-possible valid size of a Cast RTP packet (i.e., no optional +// fields, extensions, nor payload). +inline constexpr int kRtpPacketMinValidSize = 18; + +// All Cast RTP packets must carry the version 2 flag, not use padding, not use +// RTP extensions, and have zero CSRCs. +inline constexpr uint8_t kRtpRequiredFirstByte = 0b10000000; + +// Bitmasks to isolate fields within byte 2 of the Cast RTP header. +inline constexpr uint8_t kRtpMarkerBitMask = 0b10000000; +inline constexpr uint8_t kRtpPayloadTypeMask = 0b01111111; + +// Describes the content being transported over RTP streams. These are Cast +// Streaming specific assignments, within the "dynamic" range provided by +// IANA. Note that this Cast Streaming implementation does not manipulate +// already-encoded data, and so these payload types are only "informative" in +// purpose and can be used to check for corruption while parsing packets. +enum class RtpPayloadType : uint8_t { + kNull = 0, + + kAudioFirst = 96, + kAudioOpus = 96, + kAudioAac = 97, + kAudioPcm16 = 98, + kAudioVarious = 99, // Codec being used is not fixed. + kAudioLast = kAudioVarious, + + kVideoFirst = 100, + kVideoVp8 = 100, + kVideoH264 = 101, + kVideoVarious = 102, // Codec being used is not fixed. + kVideoVp9 = 103, + kVideoAv1 = 104, + kVideoLast = kVideoAv1, + + // Some AndroidTV receivers require the payload type for audio to be 127, and + // video to be 96; regardless of the codecs actually being used. This is + // definitely out-of-spec, and inconsistent with the audio versus video range + // of values, but must be taken into account for backwards-compatibility. + kAudioHackForAndroidTV = 127, + kVideoHackForAndroidTV = 96, +}; + +// Returns the stream type associated with the RTP payload type. +StreamType ToStreamType(RtpPayloadType type, bool use_android_rtp_hack); + +// Setting `use_android_rtp_hack` to true means that we match the legacy Chrome +// sender's behavior of always sending the audio and video hacks for AndroidTV, +// as some legacy android receivers require these. +// TODO(issuetracker.google.com/184438154): we need to figure out what receivers +// need this still, if any. The hack should be removed when possible. +RtpPayloadType GetPayloadType(AudioCodec codec, bool use_android_rtp_hack); +RtpPayloadType GetPayloadType(VideoCodec codec, bool use_android_rtp_hack); + +// Returns true if the `raw_byte` can be type-casted to a RtpPayloadType, and is +// also not RtpPayloadType::kNull. The caller should mask the byte, to select +// the lower 7 bits, if applicable. +bool IsRtpPayloadType(uint8_t raw_byte); + +// Bitmasks to isolate fields within byte 12 of the Cast RTP header. +inline constexpr uint8_t kRtpKeyFrameBitMask = 0b10000000; +inline constexpr uint8_t kRtpHasReferenceFrameIdBitMask = 0b01000000; +inline constexpr uint8_t kRtpExtensionCountMask = 0b00111111; + +// Cast extensions. This implementation supports only the Adaptive Latency +// extension, and ignores all others: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | TYPE = 1 | Ext data SIZE = 2 |Playout Delay (unsigned millis)| +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// +// The Adaptive Latency extension permits changing the fixed end-to-end playout +// delay of a single RTP stream. +inline constexpr uint8_t kAdaptiveLatencyRtpExtensionType = 1; +inline constexpr int kNumExtensionDataSizeFieldBits = 10; + +// RTCP Common Header: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |V=2|P|RC/Subtyp| Packet Type | Length | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +inline constexpr int kRtcpCommonHeaderSize = 4; +// All RTCP packets must carry the version 2 flag and not use padding. +inline constexpr uint8_t kRtcpRequiredVersionAndPaddingBits = 0b100; +inline constexpr int kRtcpReportCountFieldNumBits = 5; + +// https://www.iana.org/assignments/rtp-parameters/rtp-parameters.xhtml +enum class RtcpPacketType : uint8_t { + kNull = 0, + + kSenderReport = 200, + kReceiverReport = 201, + kSourceDescription = 202, + kApplicationDefined = 204, + kPayloadSpecific = 206, + kExtendedReports = 207, +}; + +// Returns true if the `raw_byte` can be type-casted to a RtcpPacketType, and is +// also not RtcpPacketType::kNull. +bool IsRtcpPacketType(uint8_t raw_byte); + +// Supported subtype values in the RTCP Common Header when the packet type is +// kApplicationDefined or kPayloadSpecific. +enum class RtcpSubtype : uint8_t { + kNull = 0, + + kPictureLossIndicator = 1, + kReceiverLog = 2, + kFeedback = 15, +}; + +// RTCP Sender Report: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Sender | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | | +// | NTP Timestamp | +// | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | RTP Timestamp | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Sender's Packet Count | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Sender's Octet Count | +// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ +// ...Followed by zero or more "Report Blocks"... +inline constexpr int kRtcpSenderReportSize = 24; + +// RTCP Receiver Report: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Receiver | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// ...Followed by zero or more "Report Blocks"... +inline constexpr int kRtcpReceiverReportSize = 4; + +// RTCP Report Block. For Cast Streaming, zero or one of these accompanies a +// Sender or Receiver Report, which is different than the RTCP spec (which +// allows zero or more). +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | "To" SSRC | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Fraction Lost | Cumulative Number of Packets Lost | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | [32-bit extended] Highest Sequence Number Received | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Interarrival Jitter Mean Absolute Deviation (in RTP Timebase) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Middle 32-bits of NTP Timestamp from last Sender Report | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Delay since last Sender Report (1/65536 sec timebase) | +// +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+ +inline constexpr int kRtcpReportBlockSize = 24; +inline constexpr int kRtcpCumulativePacketsFieldNumBits = 24; + +// Cast Feedback Message: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Receiver | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Sender | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Unique identifier 'C' 'A' 'S' 'T' | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | CkPt Frame ID | # Loss Fields | Current Playout Delay (msec) | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +inline constexpr int kRtcpFeedbackHeaderSize = 16; +inline constexpr uint32_t kRtcpCastIdentifierWord = + (uint32_t{'C'} << 24) | (uint32_t{'A'} << 16) | (uint32_t{'S'} << 8) | + uint32_t{'T'}; +// +// "Checkpoint Frame ID" indicates that all frames prior to and including this +// one have been fully received. Unfortunately, the Frame ID is truncated to its +// lower 8 bits in the packet, and 8 bits is not really enough: If a RTCP packet +// is received very late (e.g., more than 1.2 seconds late for 100 FPS audio), +// the Checkpoint Frame ID here will be mis-interpreted as representing a +// higher-numbered frame than what was intended. This could make the sender's +// tracking of "completely received" frames inconsistent, and Cast Streaming +// would live-lock. However, this design issue has been baked into the spec and +// millions of deployments over several years, and so there's no changing it +// now. See kMaxUnackedFrames in constants.h. +// +// "# Loss fields" indicates the number of packet-level NACK words, 0 to 255: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | w/in Frame ID | Lost Frame Packet ID | PID BitVector | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +inline constexpr int kRtcpFeedbackLossFieldSize = 4; +// +// "Within Frame ID" is a truncated-to-8-bits frame ID field and, when +// bit-expanded should always be interpreted to represent a value greater than +// the Checkpoint Frame ID. "Lost Frame Packet ID" is either a specific packet +// (within the frame) that has not been received, or kAllPacketsLost to indicate +// none the packets for the frame have been received yet. In the former case, +// "PID Bit Vector" then represents which of the next 8 packets are also +// missing. +// +// Finally, all of the above is optionally followed by a frame-level ACK bit +// vector: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Unique identifier 'C' 'S' 'T' '2' | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// |Feedback Count | # BVectOctets | ACK BitVect (2 to 254 bytes)... +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ → zero-padded to word boundary +inline constexpr int kRtcpFeedbackAckHeaderSize = 6; +inline constexpr uint32_t kRtcpCst2IdentifierWord = + (uint32_t{'C'} << 24) | (uint32_t{'S'} << 16) | (uint32_t{'T'} << 8) | + uint32_t{'2'}; +inline constexpr int kRtcpMinAckBitVectorOctets = 2; +inline constexpr int kRtcpMaxAckBitVectorOctets = 254; +// +// "Feedback Count" is a wrap-around counter indicating the number of Cast +// Feedbacks that have been sent before this one. "# Bit Vector Octets" +// indicates the number of bytes of ACK bit vector following. Cast RTCP +// alignment/padding requirements (to 4-byte boundaries) dictates the following +// rules for generating the ACK bit vector: +// +// 1. There must be at least 2 bytes of ACK bit vector, if only to pad the 6 +// byte header with two more bytes. +// 2. If more than 2 bytes are needed, they must be added 4 at a time to +// maintain the 4-byte alignment of the overall RTCP packet. +// 3. The total number of octets may not exceed 255; but, because of #2, 254 +// is effectively the limit. +// 4. The first bit in the first octet represents "Checkpoint Frame ID" plus +// two. "Plus two" and not "plus one" because otherwise the "Checkpoint +// Frame ID" should have been a greater value! + +// RTCP Extended Report: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Report Author | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +inline constexpr int kRtcpExtendedReportHeaderSize = 4; +// +// ...followed by zero or more Blocks: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Block Type | Reserved = 0 | Block Length | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | ..."Block Length" words of report data... | +// + + +// + + +inline constexpr int kRtcpExtendedReportBlockHeaderSize = 4; +// +// Cast Streaming only uses Receiver Reference Time Reports: +// https://tools.ietf.org/html/rfc3611#section-4.4. So, the entire block would +// be: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Block Type=4 | Reserved = 0 | Block Length = 2 | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | NTP Timestamp | +// | | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +inline constexpr uint8_t kRtcpReceiverReferenceTimeReportBlockType = 4; +inline constexpr int kRtcpReceiverReferenceTimeReportBlockSize = 8; + +// Cast Picture Loss Indicator Message: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Receiver | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | SSRC of Sender | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +inline constexpr int kRtcpPictureLossIndicatorHeaderSize = 8; + +// The Cast Receiver RTCP frame log message is an application specific +// extension that contains receiver side statistics about the Receiver Session. +// The message format is: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | RTP Timestamp | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Event Count | Event Timestamp | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +inline constexpr int kRtcpReceiverFrameLogMessageHeaderSize = 8; +// +// Followed by a list of zero or more event blocks: +// +// 0 1 2 3 +// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +// | Delay Delta or Packet ID | Type | Event Timestamp | +// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ +inline constexpr int kRtcpReceiverFrameLogMessageBlockSize = 4; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_RTP_DEFINES_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_packetizer.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_packetizer.cc new file mode 100644 index 0000000..0e9f21e --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_packetizer.cc @@ -0,0 +1,133 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/rtp_packetizer.h" + +#include +#include +#include + +#include "cast/streaming/impl/packet_util.h" +#include "platform/api/time.h" +#include "util/big_endian.h" +#include "util/integer_division.h" +#include "util/osp_logging.h" + +namespace openscreen::cast { + +namespace { + +// Returns a random sequence number to start with. The reason for using a random +// number instead of zero is unclear, but this has existed both in several +// versions of the Cast Streaming spec and in other implementations for many +// years. +uint16_t GenerateRandomSequenceNumberStart() { + // Use a statically-allocated generator, instantiated upon first use, and + // seeded with the current time tick count. This generator was chosen because + // it is light-weight and does not need to produce unguessable (nor + // crypto-secure) values. + static std::minstd_rand generator(static_cast( + Clock::now().time_since_epoch().count())); + + return std::uniform_int_distribution()(generator); +} + +} // namespace + +RtpPacketizer::RtpPacketizer(RtpPayloadType payload_type, + Ssrc sender_ssrc, + int max_packet_size) + : payload_type_7bits_(static_cast(payload_type)), + sender_ssrc_(sender_ssrc), + max_packet_size_(max_packet_size), + sequence_number_(GenerateRandomSequenceNumberStart()) { + OSP_CHECK(IsRtpPayloadType(payload_type_7bits_)); + OSP_CHECK_GT(max_packet_size_, kMaxRtpHeaderSize); +} + +RtpPacketizer::~RtpPacketizer() = default; + +ByteBuffer RtpPacketizer::GeneratePacket(const EncryptedFrame& frame, + FramePacketId packet_id, + ByteBuffer buffer) { + OSP_CHECK_GE(static_cast(buffer.size()), max_packet_size_); + + const int num_packets = ComputeNumberOfPackets(frame); + OSP_CHECK_GT(num_packets, 0); + OSP_CHECK_LT(int{packet_id}, num_packets); + const bool is_last_packet = int{packet_id} == (num_packets - 1); + + // Compute the size of this packet, which is the number of bytes of header + // plus the number of bytes of payload. Note that the optional Adaptive + // Latency information is only added to the first packet. + int packet_size = kBaseRtpHeaderSize; + const bool include_adaptive_latency_change = + (packet_id == 0 && + frame.new_playout_delay > std::chrono::milliseconds(0)); + if (include_adaptive_latency_change) { + OSP_CHECK_LE(frame.new_playout_delay.count(), + int{std::numeric_limits::max()}); + packet_size += kAdaptiveLatencyHeaderSize; + } + int data_chunk_size = max_payload_size(); + const int data_chunk_start = data_chunk_size * int{packet_id}; + if (is_last_packet) { + data_chunk_size = static_cast(frame.data.size()) - data_chunk_start; + } + packet_size += data_chunk_size; + OSP_CHECK_LE(packet_size, max_packet_size_); + const ByteBuffer packet(buffer.data(), packet_size); + + // RTP Header. + AppendField(kRtpRequiredFirstByte, buffer); + AppendField( + (is_last_packet ? kRtpMarkerBitMask : 0) | payload_type_7bits_, buffer); + AppendField(sequence_number_++, buffer); + AppendField(frame.rtp_timestamp.lower_32_bits(), buffer); + AppendField(sender_ssrc_, buffer); + + // Cast Header. + AppendField( + ((frame.dependency == EncodedFrame::Dependency::kKeyFrame) + ? kRtpKeyFrameBitMask + : 0) | + kRtpHasReferenceFrameIdBitMask | + (include_adaptive_latency_change ? 1 : 0), + buffer); + AppendField(frame.frame_id.lower_8_bits(), buffer); + AppendField(packet_id, buffer); + AppendField(num_packets - 1, buffer); + AppendField(frame.referenced_frame_id.lower_8_bits(), buffer); + + // Extension of Cast Header for Adaptive Latency change. + if (include_adaptive_latency_change) { + AppendField( + (kAdaptiveLatencyRtpExtensionType << kNumExtensionDataSizeFieldBits) | + sizeof(uint16_t), + buffer); + AppendField(frame.new_playout_delay.count(), buffer); + } + + // Copy the encrypted payload data into the packet. + auto data_chunk = frame.data.subspan(data_chunk_start, data_chunk_size); + std::copy(data_chunk.begin(), data_chunk.end(), buffer.data()); + + return packet; +} + +int RtpPacketizer::ComputeNumberOfPackets(const EncryptedFrame& frame) const { + // The total number of packets is computed by assuming the payload will be + // split-up across as few packets as possible. + int num_packets = DividePositivesRoundingUp( + static_cast(frame.data.size()), max_payload_size()); + // Edge case: There must always be at least one packet, even when there are no + // payload bytes. Some audio codecs, for example, use zero bytes to represent + // a period of silence. + num_packets = std::max(1, num_packets); + + // Ensure that the entire range of FramePacketIds can be represented. + return num_packets <= int{kMaxAllowedFramePacketId} ? num_packets : -1; +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_packetizer.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_packetizer.h new file mode 100644 index 0000000..71b13dc --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/rtp_packetizer.h @@ -0,0 +1,79 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_RTP_PACKETIZER_H_ +#define CAST_STREAMING_IMPL_RTP_PACKETIZER_H_ + +#include + +#include "cast/streaming/impl/frame_crypto.h" +#include "cast/streaming/impl/rtp_defines.h" +#include "cast/streaming/ssrc.h" +#include "platform/base/span.h" + +namespace openscreen::cast { + +// Transforms a logical sequence of EncryptedFrames into RTP packets for +// transmission. A single instance of RtpPacketizer should be used for all the +// frames in a Cast RTP stream having the same SSRC. +class RtpPacketizer { + public: + // `payload_type` describes the type of the media content for the RTP stream + // from the sender having the given `sender_ssrc`. + // + // The `max_packet_size` argument depends on the optimal over-the-wire size of + // packets for the network medium being used. See discussion in rtp_defines.h + // for further info. + RtpPacketizer(RtpPayloadType payload_type, + Ssrc sender_ssrc, + int max_packet_size); + + ~RtpPacketizer(); + + // Wire-format one of the RTP packets for the given frame, which must only be + // transmitted once. This method should be called in the same sequence that + // packets will be transmitted. This also means that, if a packet needs to be + // re-transmitted, this method should be called to generate it again. Returns + // the subspan of `buffer` that contains the packet. `buffer` must be at least + // as large as the `max_packet_size` passed to the constructor. + ByteBuffer GeneratePacket(const EncryptedFrame& frame, + FramePacketId packet_id, + ByteBuffer buffer); + + // Given `frame`, compute the total number of packets over which the whole + // frame will be split-up. Returns -1 if the frame is too large and cannot be + // packetized. + int ComputeNumberOfPackets(const EncryptedFrame& frame) const; + + // See rtp_defines.h for wire-format diagram. + static constexpr int kBaseRtpHeaderSize = + // Plus one byte, because this implementation always includes the 8-bit + // Reference Frame ID field. + kRtpPacketMinValidSize + 1; + static constexpr int kAdaptiveLatencyHeaderSize = 4; + static constexpr int kMaxRtpHeaderSize = + kBaseRtpHeaderSize + kAdaptiveLatencyHeaderSize; + + private: + int max_payload_size() const { + // Start with the configured max packet size, then subtract reserved space + // for packet header fields. The rest can be allocated to the payload. + return max_packet_size_ - kMaxRtpHeaderSize; + } + + // The validated ctor RtpPayloadType arg, in wire-format form. + const uint8_t payload_type_7bits_; + + const Ssrc sender_ssrc_; + const int max_packet_size_; + + // Incremented each time GeneratePacket() is called. Every packet, even those + // re-transmitted, must have different sequence numbers (within wrap-around + // concerns) per the RTP spec. + uint16_t sequence_number_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_RTP_PACKETIZER_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_impl.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_impl.cc new file mode 100644 index 0000000..4b08012 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_impl.cc @@ -0,0 +1,686 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/sender_impl.h" + +#include +#include +#include +#include + +#include "cast/streaming/impl/rtp_defines.h" +#include "cast/streaming/impl/statistics_common.h" +#include "cast/streaming/public/session_config.h" +#include "platform/base/trivial_clock_traits.h" +#include "util/chrono_helpers.h" +#include "util/osp_logging.h" +#include "util/std_util.h" +#include "util/string_util.h" +#include "util/trace_logging.h" + +namespace openscreen::cast { +namespace { + +// The minimum amount of media the Sender keeps in-flight, regardless of the +// measured network round-trip time. This keeps the encoder pipeline flowing on +// low-latency networks (roughly two video frames at 30 FPS). See +// crbug.com/498035450. +constexpr Clock::duration kMinSenderInFlight = + Clock::to_duration(milliseconds(66)); + +} // namespace + +using clock_operators::operator<<; + +SenderImpl::SenderImpl(Environment& environment, + SenderPacketRouter& packet_router, + SessionConfig config, + RtpPayloadType rtp_payload_type) + : config_(config), + packet_router_(packet_router), + rtcp_session_(config.sender_ssrc, + config.receiver_ssrc, + environment.now()), + rtcp_parser_(rtcp_session_, *this), + sender_report_builder_(rtcp_session_), + rtp_packetizer_(rtp_payload_type, + config.sender_ssrc, + packet_router_->max_packet_size()), + rtp_timebase_(config.rtp_timebase), + crypto_(config.aes_secret_key, config.aes_iv_mask), + statistics_dispatcher_(environment), + target_playout_delay_(config.target_playout_delay) { + OSP_CHECK_NE(rtcp_session_.sender_ssrc(), rtcp_session_.receiver_ssrc()); + OSP_CHECK_GT(rtp_timebase_, 0); + OSP_CHECK_GT(target_playout_delay_, milliseconds::zero()); + + pending_sender_report_.reference_time = SenderPacketRouter::kNever; + + packet_router_->OnSenderCreated(rtcp_session_.receiver_ssrc(), this); +} + +SenderImpl::~SenderImpl() { + packet_router_->OnSenderDestroyed(rtcp_session_.receiver_ssrc()); +} + +void SenderImpl::SetObserver(openscreen::cast::Sender::Observer* observer) { + OSP_CHECK_NE(observer_, observer); + observer_ = observer; +} + +size_t SenderImpl::GetInFlightFrameCount() const { + return num_frames_in_flight_; +} + +Clock::duration SenderImpl::GetInFlightMediaDuration( + RtpTimeTicks next_frame_rtp_timestamp) const { + if (num_frames_in_flight_ == 0) { + return Clock::duration::zero(); // No frames are currently in-flight. + } + + const PendingFrameSlot& oldest_slot = get_slot_for(checkpoint_frame_id_ + 1); + // Note: The oldest slot's frame cannot have been canceled because the + // protocol does not allow ACK'ing this particular frame without also moving + // the checkpoint forward. See "CST2 feedback" discussion in rtp_defines.h. + OSP_CHECK(oldest_slot.is_active_for_frame(checkpoint_frame_id_ + 1)); + + return (next_frame_rtp_timestamp - oldest_slot.frame->rtp_timestamp) + .ToDuration(rtp_timebase_); +} + +Clock::duration SenderImpl::GetMaxInFlightMediaDuration() const { + // The Sender keeps only enough media in-flight to drive the loss-detection + // and retransmit feedback loop, which takes on the order of two network + // round-trips (one to detect a loss via NACK, one to retransmit). A small + // floor (`kMinSenderInFlight`) keeps the encoder pipeline flowing on + // low-latency networks where 2*RTT is negligible. + // + // The result is capped at a third of the playout delay window so that the + // majority of the budget is reserved for the Receiver, which needs buffer to + // absorb NACK retransmissions. Bounding the Sender this way also makes it + // drop frames earlier during congestion (saving bandwidth and CPU) rather + // than over-buffering. See crbug.com/498035450. + // + // Note: the upper bound is held at or above `kMinSenderInFlight` so the + // std::clamp() bounds remain well-ordered even for very small playout delays. + const Clock::duration max_in_flight = std::max( + kMinSenderInFlight, Clock::to_duration(target_playout_delay_) / 3); + return std::clamp(round_trip_time_ * 2, kMinSenderInFlight, max_in_flight); +} + +bool SenderImpl::NeedsKeyFrame() const { + return last_enqueued_key_frame_id_ <= picture_lost_at_frame_id_; +} + +FrameId SenderImpl::GetNextFrameId() const { + return last_enqueued_frame_id_ + 1; +} + +Clock::duration SenderImpl::GetCurrentRoundTripTime() const { + return round_trip_time_; +} + +openscreen::cast::Sender::EnqueueFrameResult SenderImpl::EnqueueFrame( + const EncodedFrame& frame) { + // Assume the fields of the `frame` have all been set correctly, with + // monotonically increasing timestamps and a valid pointer to the data. + OSP_CHECK_EQ(frame.frame_id, GetNextFrameId()); + OSP_CHECK_GE(frame.referenced_frame_id, FrameId::first()); + if (frame.frame_id != FrameId::first()) { + OSP_CHECK_GT(frame.rtp_timestamp, pending_sender_report_.rtp_timestamp); + if (frame.reference_time <= pending_sender_report_.reference_time) { + OSP_DLOG_WARN << "Frame " << frame.frame_id + << " has non-monotonic reference_time: " + << frame.reference_time + << " <= " << pending_sender_report_.reference_time; + } + } + OSP_CHECK(frame.data.data()); + + const auto capture_begin_time = + (frame.capture_begin_time > Clock::time_point::min()) + ? frame.capture_begin_time + : Clock::now(); + + TRACE_FLOW_BEGIN_WITH_TIME(TraceCategory::kSender, "Frame.Capture", + frame.frame_id, capture_begin_time); + + if (frame.capture_end_time > Clock::time_point::min()) { + TRACE_FLOW_STEP_WITH_TIME(TraceCategory::kSender, "Frame.Capture.End", + frame.frame_id, frame.capture_end_time); + } + + TRACE_FLOW_STEP(TraceCategory::kSender, "Frame.Encode.End", frame.frame_id); + + // Check whether enqueuing the frame would exceed the design limit for the + // span of FrameIds. Even if `num_frames_in_flight_` is less than + // kMaxUnackedFrames, it's the span of FrameIds that is restricted. + if ((frame.frame_id - checkpoint_frame_id_) > kMaxUnackedFrames) { + return REACHED_ID_SPAN_LIMIT; + } + + // Check whether enqueuing the frame would exceed the current maximum media + // duration limit. + if (GetInFlightMediaDuration(frame.rtp_timestamp) > + GetMaxInFlightMediaDuration()) { + return MAX_DURATION_IN_FLIGHT; + } + + // Encrypt the frame and initialize the slot tracking its sending. + PendingFrameSlot& slot = get_slot_for(frame.frame_id); + OSP_CHECK(!slot.frame); + slot.frame = crypto_.Encrypt(frame); + const int packet_count = rtp_packetizer_.ComputeNumberOfPackets(*slot.frame); + if (packet_count <= 0) { + slot.frame.reset(); + return PAYLOAD_TOO_LARGE; + } + slot.send_flags.Resize(packet_count, BitVector::SET); + slot.packet_sent_times.assign(packet_count, SenderPacketRouter::kNever); + + // Officially record the "enqueue." + ++num_frames_in_flight_; + last_enqueued_frame_id_ = slot.frame->frame_id; + OSP_CHECK_LE( + num_frames_in_flight_, + static_cast(last_enqueued_frame_id_ - checkpoint_frame_id_)); + if (slot.frame->dependency == EncodedFrame::Dependency::kKeyFrame) { + last_enqueued_key_frame_id_ = slot.frame->frame_id; + } + TRACE_FLOW_STEP(TraceCategory::kSender, "Frame.Enqueued", frame.frame_id); + + // Update the target playout delay, if necessary. + if (slot.frame->new_playout_delay > milliseconds::zero()) { + target_playout_delay_ = slot.frame->new_playout_delay; + playout_delay_change_at_frame_id_ = slot.frame->frame_id; + } + + // Update the lip-sync information for the next Sender Report, ensuring that + // the reference time is monotonically increasing. + pending_sender_report_.reference_time = + frame.frame_id == FrameId::first() + ? slot.frame->reference_time + : std::max(slot.frame->reference_time, + pending_sender_report_.reference_time); + pending_sender_report_.rtp_timestamp = slot.frame->rtp_timestamp; + + // If the round trip time hasn't been computed yet, immediately send a RTCP + // packet (i.e., before the RTP packets are sent). The RTCP packet will + // provide a Sender Report which contains the required lip-sync information + // the Receiver needs for timing the media playout. + // + // Detail: Working backwards, if the round trip time is not known, then this + // Sender has never processed a Receiver Report. Thus, the Receiver has never + // provided a Receiver Report, which it can only do after having processed a + // Sender Report from this Sender. Thus, this Sender really needs to send + // that, right now! + if (round_trip_time_ == Clock::duration::zero()) { + packet_router_->RequestRtcpSend(rtcp_session_.receiver_ssrc()); + } + + // Re-activate RTP sending if it was suspended. + packet_router_->RequestRtpSend(rtcp_session_.receiver_ssrc()); + statistics_dispatcher_.DispatchEnqueueEvents(config_.stream_type, frame); + + return OK; +} + +void SenderImpl::CancelInFlightData() { + TRACE_DEFAULT_SCOPED1( + TraceCategory::kSender, "frames_in_flight", + std::to_string(last_enqueued_frame_id_ - checkpoint_frame_id_)); + + while (checkpoint_frame_id_ < last_enqueued_frame_id_) { + ++checkpoint_frame_id_; + CancelPendingFrame(checkpoint_frame_id_, /*was_acked*/ false); + } + DispatchCancellations(); +} + +void SenderImpl::ReportFrameDropEvent(FrameId frame_id, + RtpTimeTicks rtp_timestamp, + Clock::time_point drop_time) { + statistics_dispatcher_.DispatchFrameDropEvent(config_.stream_type, frame_id, + rtp_timestamp, drop_time); +} + +void SenderImpl::OnReceivedRtcpPacket(Clock::time_point arrival_time, + ByteView packet) { + rtcp_packet_arrival_time_ = arrival_time; + // This call to Parse() invoke zero or more of the OnReceiverXYZ() methods in + // the current call stack: + if (rtcp_parser_.Parse(packet, last_enqueued_frame_id_)) { + packet_router_->OnRtcpReceived(arrival_time, round_trip_time_); + } +} + +ByteBuffer SenderImpl::GetRtcpPacketForImmediateSend( + Clock::time_point send_time, + ByteBuffer buffer) { + if (pending_sender_report_.reference_time == SenderPacketRouter::kNever) { + // Cannot send a report if one is not available (i.e., a frame has never + // been enqueued). + return buffer.subspan(0, 0); + } + + // The Sender Report to be sent is a snapshot of the "pending Sender Report," + // but with its timestamp fields modified. First, the reference time is set to + // the RTCP packet's send time. Then, the corresponding RTP timestamp is + // translated to match (for lip-sync). + RtcpSenderReport sender_report = pending_sender_report_; + sender_report.reference_time = send_time; + sender_report.rtp_timestamp += RtpTimeDelta::FromDuration( + sender_report.reference_time - pending_sender_report_.reference_time, + rtp_timebase_); + + return sender_report_builder_.BuildPacket(sender_report, buffer).first; +} + +ByteBuffer SenderImpl::GetRtpPacketForImmediateSend(Clock::time_point send_time, + ByteBuffer buffer) { + ChosenPacket chosen = ChooseNextRtpPacketNeedingSend(); + + // If no packets need sending (i.e., all packets have been sent at least once + // and do not need to be re-sent yet), check whether a Kickstart packet should + // be sent. It's possible that there has been complete packet loss of some + // frames, and the Receiver may not be aware of the existence of the latest + // frame(s). Kickstarting is the only way the Receiver can discover the newer + // frames it doesn't know about. + if (!chosen) { + const ChosenPacketAndWhen kickstart = ChooseKickstartPacket(); + if (kickstart.when > send_time) { + // Nothing to send, so return "empty" signal to the packet router. The + // packet router will suspend RTP sending until this Sender explicitly + // resumes it. + return buffer.subspan(0, 0); + } + chosen = kickstart; + OSP_CHECK(chosen); + } + + const ByteBuffer result = rtp_packetizer_.GeneratePacket( + *chosen.slot->frame, chosen.packet_id, buffer); + chosen.slot->send_flags.Clear(chosen.packet_id); + chosen.slot->packet_sent_times[chosen.packet_id] = send_time; + + ++pending_sender_report_.send_packet_count; + // According to RFC3550, the octet count does not include the RTP header. The + // following is just a good approximation, however, because the header size + // will very infrequently be 4 bytes greater (see + // RtpPacketizer::kAdaptiveLatencyHeaderSize). No known Cast Streaming + // Receiver implementations use this for anything, and so this should be fine. + const int approximate_octet_count = + static_cast(result.size()) - RtpPacketizer::kBaseRtpHeaderSize; + OSP_CHECK_GE(approximate_octet_count, 0); + pending_sender_report_.send_octet_count += approximate_octet_count; + + return result; +} + +Clock::time_point SenderImpl::GetRtpResumeTime() { + if (ChooseNextRtpPacketNeedingSend()) { + return Alarm::kImmediately; + } + return ChooseKickstartPacket().when; +} + +RtpTimeTicks SenderImpl::GetLastRtpTimestamp() const { + return {}; +} + +StreamType SenderImpl::GetStreamType() const { + return config_.stream_type; +} + +void SenderImpl::OnReceiverReferenceTimeAdvanced( + Clock::time_point reference_time) { + // Not used. +} + +// static +Clock::duration SenderImpl::SmoothRoundTripTime(Clock::duration estimate, + Clock::duration measurement) { + // Measurements typically have high variance, so smooth them with an + // exponentially-weighted moving average. The filter is asymmetric ("fast + // attack, slow decay"): it reacts quickly to upward spikes so the Sender + // notices congestion onset promptly and backs off, but decays slowly on the + // way down so a single low sample doesn't collapse the estimate. See + // crbug.com/498036656. + if (estimate == Clock::duration::zero()) { + return measurement; + } + if (measurement > estimate) { + // Spike / congestion onset: give the new measurement half the weight so the + // estimate climbs quickly. + return (estimate + measurement) / 2; + } + // Recovery: give the new measurement 1/8 weight (and the old estimate 7/8) to + // de-noise, since downward measurements are typically the network settling + // rather than a sustained improvement. + constexpr int kInertia = 7; + return (kInertia * estimate + measurement) / (kInertia + 1); +} + +void SenderImpl::OnReceiverReport(const RtcpReportBlock& receiver_report) { + OSP_CHECK_NE(rtcp_packet_arrival_time_, SenderPacketRouter::kNever); + + const Clock::duration total_delay = + rtcp_packet_arrival_time_ - + sender_report_builder_.GetRecentReportTime( + receiver_report.last_status_report_id, rtcp_packet_arrival_time_); + const auto non_network_delay = + Clock::to_duration(receiver_report.delay_since_last_report); + + // Round trip time measurement: This is the time elapsed since the Sender + // Report was sent, minus the time the Receiver did other stuff before sending + // the Receiver Report back. + // + // If the round trip time seems to be less than or equal to zero, assume clock + // imprecision by one or both peers caused a bad value to be calculated. The + // true value is likely very close to zero (i.e., this is ideal network + // behavior); and so just represent this as 75 µs, an optimistic + // wired-Ethernet LAN ping time. + constexpr auto kNearZeroRoundTripTime = Clock::to_duration(microseconds(75)); + static_assert(kNearZeroRoundTripTime > Clock::duration::zero(), + "More precision in Clock::duration needed!"); + const Clock::duration measurement = + std::max(total_delay - non_network_delay, kNearZeroRoundTripTime); + + // Validate the measurement by using the current target playout delay as a + // "reasonable upper-bound." It's certainly possible that the actual network + // round-trip time could exceed the target playout delay, but that would mean + // the current network performance is totally inadequate for streaming anyway. + // We cap the measurement here instead of ignoring it so the Sender still + // backs off its estimates during severe network congestion. + Clock::duration clamped_measurement = measurement; + if (clamped_measurement > target_playout_delay_) { + OSP_LOG_WARN << "Capping round-trip time measurement (" << measurement + << ") to the current target playout delay (" + << target_playout_delay_ << ")."; + clamped_measurement = target_playout_delay_; + } + + round_trip_time_ = SmoothRoundTripTime(round_trip_time_, clamped_measurement); + TRACE_SCOPED1(TraceCategory::kSender, "UpdatedRoundTripTime", + "round_trip_time", ToString(round_trip_time_)); +} + +void SenderImpl::OnCastReceiverFrameLogMessages( + std::vector messages) { + statistics_dispatcher_.DispatchFrameLogMessages(config_.stream_type, + messages); +} + +void SenderImpl::OnReceiverIndicatesPictureLoss() { + TRACE_DEFAULT_SCOPED1(TraceCategory::kSender, "last_received_frame_id", + picture_lost_at_frame_id_.ToString()); + // The Receiver will continue the PLI notifications until it has received a + // key frame. Thus, if a key frame is already in-flight, don't make a state + // change that would cause this Sender to force another expensive key frame. + if (checkpoint_frame_id_ < last_enqueued_key_frame_id_) { + return; + } + + picture_lost_at_frame_id_ = checkpoint_frame_id_; + + if (observer_) { + observer_->OnPictureLost(); + } + + // Note: It may seem that all pending frames should be canceled until + // EnqueueFrame() is called with a key frame. However: + // + // 1. The Receiver should still be the main authority on what frames/packets + // are being ACK'ed and NACK'ed. + // + // 2. It may be desirable for the Receiver to be "limping along" in the + // meantime. For example, video may be corrupted but mostly watchable, + // and so it's best for the Sender to continue sending the non-key frames + // until the Receiver indicates otherwise. +} + +void SenderImpl::OnReceiverCheckpoint(FrameId frame_id, + milliseconds playout_delay) { + TRACE_DEFAULT_SCOPED2(TraceCategory::kSender, "frame_id", frame_id.ToString(), + "playout_delay", ToString(playout_delay)); + if (frame_id > last_enqueued_frame_id_) { + TRACE_SET_RESULT(Error::Code::kParameterOutOfRange); + OSP_LOG_ERROR + << "Ignoring checkpoint for " << latest_expected_frame_id_ + << " because this Sender could not have sent any frames after " + << last_enqueued_frame_id_ << '.'; + return; + } + // CompoundRtcpParser should guarantee this: + OSP_CHECK_GE(playout_delay, milliseconds::zero()); + while (checkpoint_frame_id_ < frame_id) { + ++checkpoint_frame_id_; + PendingFrameSlot& slot = get_slot_for(checkpoint_frame_id_); + if (slot.is_active_for_frame(checkpoint_frame_id_)) { + const RtpTimeTicks rtp_timestamp = slot.frame->rtp_timestamp; + statistics_dispatcher_.DispatchAckEvent( + config_.stream_type, rtp_timestamp, checkpoint_frame_id_); + CancelPendingFrame(checkpoint_frame_id_, /*was_acked*/ true); + + TRACE_FLOW_STEP(TraceCategory::kSender, "Frame.Acked", + checkpoint_frame_id_); + } + } + latest_expected_frame_id_ = std::max(latest_expected_frame_id_, frame_id); + DispatchCancellations(); + + if (playout_delay != target_playout_delay_ && + frame_id >= playout_delay_change_at_frame_id_) { + OSP_LOG_WARN << "Sender's target playout delay (" << target_playout_delay_ + << ") disagrees with the Receiver's (" << playout_delay << ")"; + } +} + +void SenderImpl::OnReceiverHasFrames(std::vector acks) { + OSP_DCHECK(!acks.empty() && AreElementsSortedAndUnique(acks)); + TRACE_DEFAULT_SCOPED1(TraceCategory::kSender, "frame_ids", + string_util::Join(acks)); + + if (acks.back() > last_enqueued_frame_id_) { + TRACE_SET_RESULT(Error::Code::kParameterOutOfRange); + OSP_LOG_ERROR << "Ignoring individual frame ACKs: ACKing frame " + << latest_expected_frame_id_ + << " is invalid because this Sender could not have sent any " + "frames after " + << last_enqueued_frame_id_ << '.'; + return; + } + + for (FrameId id : acks) { + TRACE_FLOW_STEP(TraceCategory::kSender, "Frame.Acked", id); + PendingFrameSlot& slot = get_slot_for(id); + if (slot.is_active_for_frame(id)) { + const RtpTimeTicks rtp_timestamp = slot.frame->rtp_timestamp; + statistics_dispatcher_.DispatchAckEvent(config_.stream_type, + rtp_timestamp, id); + } + CancelPendingFrame(id, /*was_acked*/ true); + } + latest_expected_frame_id_ = std::max(latest_expected_frame_id_, acks.back()); + DispatchCancellations(); +} + +void SenderImpl::OnReceiverIsMissingPackets(std::vector nacks) { + TRACE_DEFAULT_SCOPED1(TraceCategory::kSender, "number_of_packets", + std::to_string(nacks.size())); + OSP_DCHECK(!nacks.empty() && AreElementsSortedAndUnique(nacks)); + OSP_CHECK_NE(rtcp_packet_arrival_time_, SenderPacketRouter::kNever); + + // This is a point-in-time threshold that indicates whether each NACK will + // trigger a packet retransmit. The threshold is based on the network round + // trip time because a Receiver's NACK may have been issued while the needed + // packet was in-flight from the Sender. In such cases, the Receiver's NACK is + // likely stale and this Sender should not redundantly re-transmit the packet + // again. + const Clock::time_point too_recent_a_send_time = + rtcp_packet_arrival_time_ - round_trip_time_; + + // Iterate over all the NACKs... + bool need_to_send = false; + for (auto nack_it = nacks.begin(); nack_it != nacks.end();) { + // Find the slot associated with the NACK's frame ID. + const FrameId frame_id = nack_it->frame_id; + PendingFrameSlot* slot = nullptr; + if (frame_id <= last_enqueued_frame_id_) { + PendingFrameSlot& candidate_slot = get_slot_for(frame_id); + if (candidate_slot.is_active_for_frame(frame_id)) { + slot = &candidate_slot; + } + } + + // If no slot was found (i.e., the NACK is invalid) for the frame, skip-over + // all other NACKs for the same frame. While it seems to be a bug that the + // Receiver would attempt to NACK a frame that does not yet exist, this can + // happen in rare cases where RTCP packets arrive out-of-order (i.e., the + // network shuffled them). + if (!slot) { + TRACE_SCOPED1(TraceCategory::kSender, "MissingNackSlot", "frame_id", + frame_id.ToString()); + for (++nack_it; nack_it != nacks.end() && nack_it->frame_id == frame_id; + ++nack_it) { + } + continue; + } + + latest_expected_frame_id_ = std::max(latest_expected_frame_id_, frame_id); + + const auto HandleIndividualNack = [&](FramePacketId packet_id) { + if (slot->packet_sent_times[packet_id] <= too_recent_a_send_time) { + slot->send_flags.Set(packet_id); + need_to_send = true; + } + }; + const FramePacketId range_end = slot->packet_sent_times.size(); + if (nack_it->packet_id == kAllPacketsLost) { + for (FramePacketId packet_id = 0; packet_id < range_end; ++packet_id) { + HandleIndividualNack(packet_id); + } + ++nack_it; + } else { + do { + if (nack_it->packet_id < range_end) { + HandleIndividualNack(nack_it->packet_id); + } else { + OSP_LOG_WARN + << "Ignoring NACK for packet that doesn't exist in frame " + << frame_id << ": " << static_cast(nack_it->packet_id); + } + ++nack_it; + } while (nack_it != nacks.end() && nack_it->frame_id == frame_id); + } + } + + if (need_to_send) { + packet_router_->RequestRtpSend(rtcp_session_.receiver_ssrc()); + } +} + +SenderImpl::ChosenPacket SenderImpl::ChooseNextRtpPacketNeedingSend() { + // Find the oldest packet needing to be sent (or re-sent). + for (FrameId frame_id = checkpoint_frame_id_ + 1; + frame_id <= last_enqueued_frame_id_; ++frame_id) { + PendingFrameSlot& slot = get_slot_for(frame_id); + if (!slot.is_active_for_frame(frame_id)) { + continue; // Frame was canceled. None of its packets need to be sent. + } + const FramePacketId packet_id = slot.send_flags.FindFirstSet(); + if (packet_id < slot.send_flags.size()) { + return {&slot, packet_id}; + } + } + + return {}; // Nothing needs to be sent. +} + +SenderImpl::ChosenPacketAndWhen SenderImpl::ChooseKickstartPacket() { + if (latest_expected_frame_id_ >= last_enqueued_frame_id_) { + // Since the Receiver must know about all of the frames currently queued, no + // Kickstart packet is necessary. + return {}; + } + + // The Kickstart packet is always in the last-enqueued frame, so that the + // Receiver will know about every frame the Sender has. However, which packet + // should be chosen? Any would do, since all packets contain the frame's total + // packet count. For historical reasons, all sender implementations have + // always just sent the last packet; and so that tradition is continued here. + ChosenPacketAndWhen chosen; + chosen.slot = &get_slot_for(last_enqueued_frame_id_); + // Note: This frame cannot have been canceled since + // `latest_expected_frame_id_` hasn't yet reached this point. + OSP_CHECK(chosen.slot->is_active_for_frame(last_enqueued_frame_id_)); + chosen.packet_id = chosen.slot->send_flags.size() - 1; + + const Clock::time_point time_last_sent = + chosen.slot->packet_sent_times[chosen.packet_id]; + // Sanity-check: This method should not be called to choose a packet while + // there are still unsent packets. + OSP_CHECK_NE(time_last_sent, SenderPacketRouter::kNever); + + // The desired Kickstart interval is a fraction of the total + // `target_playout_delay_`. The reason for the specific ratio here is based on + // lost knowledge (from legacy implementations); but it makes sense (i.e., to + // be a good "network citizen") to be less aggressive for larger playout delay + // windows, and more aggressive for shorter ones to avoid too-late packet + // arrivals. + using kWaitFraction = std::ratio<1, 20>; + const Clock::duration desired_kickstart_interval = + Clock::to_duration(target_playout_delay_) * kWaitFraction::num / + kWaitFraction::den; + // The actual interval used is increased, if current network performance + // warrants waiting longer. Don't send a Kickstart packet until no NACKs + // have been received for two network round-trip periods. + constexpr int kLowerBoundRoundTrips = 2; + const Clock::duration kickstart_interval = std::max( + desired_kickstart_interval, round_trip_time_ * kLowerBoundRoundTrips); + chosen.when = time_last_sent + kickstart_interval; + + return chosen; +} + +void SenderImpl::CancelPendingFrame(FrameId frame_id, bool was_acked) { + TRACE_FLOW_END(TraceCategory::kSender, "Frame.Cancelled", frame_id); + + PendingFrameSlot& slot = get_slot_for(frame_id); + if (!slot.is_active_for_frame(frame_id)) { + return; // Frame was already canceled. + } + + if (was_acked) { + packet_router_->OnPayloadReceived( + slot.frame->data.size(), rtcp_packet_arrival_time_, round_trip_time_); + } + + slot.frame.reset(); + OSP_CHECK_GT(num_frames_in_flight_, 0); + --num_frames_in_flight_; + if (observer_) { + pending_cancellations_.emplace_back(frame_id); + } +} + +void SenderImpl::DispatchCancellations() { + if (observer_) { + for (const FrameId id : pending_cancellations_) { + observer_->OnFrameCanceled(id); + } + } + pending_cancellations_.clear(); + + // At this point, there should either be no frames in flight, or the frame + // immediately after `checkpoint_frame_id_` must be valid. + OSP_DCHECK((num_frames_in_flight_ == 0) || + get_slot_for(checkpoint_frame_id_ + 1) + .is_active_for_frame(checkpoint_frame_id_ + 1)); +} + +SenderImpl::PendingFrameSlot::PendingFrameSlot() = default; +SenderImpl::PendingFrameSlot::~PendingFrameSlot() = default; + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_impl.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_impl.h new file mode 100644 index 0000000..49dd148 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_impl.h @@ -0,0 +1,252 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_SENDER_IMPL_H_ +#define CAST_STREAMING_IMPL_SENDER_IMPL_H_ + +#include + +#include +#include +#include +#include + +#include "cast/streaming/impl/compound_rtcp_parser.h" +#include "cast/streaming/impl/frame_crypto.h" +#include "cast/streaming/impl/rtcp_common.h" +#include "cast/streaming/impl/rtp_defines.h" +#include "cast/streaming/impl/rtp_packetizer.h" +#include "cast/streaming/impl/sender_report_builder.h" +#include "cast/streaming/impl/statistics_dispatcher.h" +#include "cast/streaming/public/constants.h" +#include "cast/streaming/public/frame_id.h" +#include "cast/streaming/public/sender.h" +#include "cast/streaming/public/session_config.h" +#include "cast/streaming/rtp_time.h" +#include "cast/streaming/sender_packet_router.h" +#include "platform/api/time.h" +#include "platform/base/span.h" +#include "util/bit_vector.h" +#include "util/raw_ptr.h" +#include "util/raw_ref.h" + +namespace openscreen::cast { + +class Environment; + +// The Cast Streaming Sender, a peer corresponding to some Cast Streaming +// Receiver at the other end of a network link. See class level comments for +// Receiver for a high-level overview. +class SenderImpl final : public Sender, + public SenderPacketRouter::Sender, + public CompoundRtcpParser::Client { + public: + // Constructs a Sender that attaches to the given `environment`-provided + // resources and `packet_router`. The `config` contains the settings that were + // agreed-upon by both sides from the OFFER/ANSWER exchange (i.e., the part of + // the overall end-to-end connection process that occurs before Cast Streaming + // is started). The `rtp_payload_type` does not affect the behavior of this + // Sender. It is simply passed along to a Receiver in the RTP packet stream. + SenderImpl(Environment& environment, + SenderPacketRouter& packet_router, + SessionConfig config, + RtpPayloadType rtp_payload_type); + + ~SenderImpl() final; + + // Sender overrides. + const SessionConfig& config() const override { return config_; } + void SetObserver(Observer* observer) override; + size_t GetInFlightFrameCount() const override; + Clock::duration GetInFlightMediaDuration( + RtpTimeTicks next_frame_rtp_timestamp) const override; + Clock::duration GetMaxInFlightMediaDuration() const override; + bool NeedsKeyFrame() const override; + FrameId GetNextFrameId() const override; + Clock::duration GetCurrentRoundTripTime() const override; + [[nodiscard]] EnqueueFrameResult EnqueueFrame( + const EncodedFrame& frame) override; + void CancelInFlightData() override; + void ReportFrameDropEvent(FrameId frame_id, + RtpTimeTicks rtp_timestamp, + Clock::time_point drop_time) override; + + // Smooths a new round-trip-time `measurement` into the running `estimate` + // using an asymmetric "fast attack, slow decay" filter: the estimate climbs + // quickly on an upward spike (so the Sender notices congestion onset) but + // decays slowly (so a single low sample does not collapse it). A zero + // `estimate` adopts the measurement directly. Static and exposed for testing. + static Clock::duration SmoothRoundTripTime(Clock::duration estimate, + Clock::duration measurement); + + private: + // Tracking/Storage for frames that are ready-to-send, and until they are + // fully received at the other end. + struct PendingFrameSlot { + // The frame to send, or nullopt if this slot is not in use. + std::optional frame; + + // Represents which packets need to be sent. Elements are indexed by + // FramePacketId. A set bit means a packet needs to be sent (or re-sent). + BitVector send_flags; + + // The time when each of the packets was last sent, or + // `SenderPacketRouter::kNever` if the packet has not been sent yet. + // Elements are indexed by FramePacketId. This is used to avoid + // re-transmitting any given packet too frequently. + std::vector packet_sent_times; + + PendingFrameSlot(); + ~PendingFrameSlot(); + + bool is_active_for_frame(FrameId frame_id) const { + return frame && frame->frame_id == frame_id; + } + }; + + // Return value from the ChooseXYZ() helper methods. + struct ChosenPacket { + raw_ptr slot = nullptr; + FramePacketId packet_id{}; + + explicit operator bool() const { return !!slot; } + }; + + // An extension of ChosenPacket that also includes the point-in-time when the + // packet should be sent. + struct ChosenPacketAndWhen : public ChosenPacket { + Clock::time_point when = SenderPacketRouter::kNever; + }; + + // SenderPacketRouter::Sender implementation. + void OnReceivedRtcpPacket(Clock::time_point arrival_time, + ByteView packet) final; + ByteBuffer GetRtcpPacketForImmediateSend(Clock::time_point send_time, + ByteBuffer buffer) final; + ByteBuffer GetRtpPacketForImmediateSend(Clock::time_point send_time, + ByteBuffer buffer) final; + Clock::time_point GetRtpResumeTime() final; + RtpTimeTicks GetLastRtpTimestamp() const final; + StreamType GetStreamType() const final; + + // CompoundRtcpParser::Client implementation. + void OnReceiverReferenceTimeAdvanced(Clock::time_point reference_time) final; + void OnReceiverReport(const RtcpReportBlock& receiver_report) final; + void OnCastReceiverFrameLogMessages( + std::vector messages) final; + void OnReceiverIndicatesPictureLoss() final; + void OnReceiverCheckpoint(FrameId frame_id, + std::chrono::milliseconds playout_delay) final; + void OnReceiverHasFrames(std::vector acks) final; + void OnReceiverIsMissingPackets(std::vector nacks) final; + + // Helper to choose which packet to send, from those that have been flagged as + // "need to send." Returns a "false" result if nothing needs to be sent. + ChosenPacket ChooseNextRtpPacketNeedingSend(); + + // Helper that returns the packet that should be used to kick-start the + // Receiver, and the time at which the packet should be sent. Returns a kNever + // result if kick-starting is not needed. + ChosenPacketAndWhen ChooseKickstartPacket(); + + // Cancels sending (or resending) the given frame once it is known to have + // been either: + // 1. Cancelled by the sender (was_acked must be false); + // 2. Fully received based on the ACK feedback in a receiver RTCP report + // (was_acked must be true); + // 3. The receiver sent a checkpoint frame ID (was_acked must be true). + // + // This clears the corresponding entry in `pending_frames_` and + // adds `frame_id` to the list of pending cancellations to be dispatched as + // part of DispatchCancellations(). + // + // NOTE: Every frame_id ends up being "cancelled" at least once. + void CancelPendingFrame(FrameId frame_id, bool was_acked); + + // Must be called after one or a series of CancelPendingFrame() calls in order + // to notify the observer, if any, about cancellations. + void DispatchCancellations(); + + // Inline helper to return the slot that would contain the tracking info for + // the given `frame_id`. + const PendingFrameSlot& get_slot_for(FrameId frame_id) const { + return pending_frames_[(frame_id - FrameId::first()) % + pending_frames_.size()]; + } + PendingFrameSlot& get_slot_for(FrameId frame_id) { + return pending_frames_[(frame_id - FrameId::first()) % + pending_frames_.size()]; + } + + const SessionConfig config_; + const raw_ref packet_router_; + RtcpSession rtcp_session_; + CompoundRtcpParser rtcp_parser_; + SenderReportBuilder sender_report_builder_; + RtpPacketizer rtp_packetizer_; + const int rtp_timebase_; + FrameCrypto crypto_; + StatisticsDispatcher statistics_dispatcher_; + + // Ring buffer of PendingFrameSlots. The frame having FrameId x will always + // be slotted at position x % pending_frames_.size(). Use get_slot_for() to + // access the correct slot for a given FrameId. + std::array pending_frames_ = {}; + + // A count of the number of frames in-flight (i.e., the number of active + // entries in `pending_frames_`). + size_t num_frames_in_flight_ = 0; + + // The ID of the last frame enqueued. + FrameId last_enqueued_frame_id_ = FrameId::leader(); + + // Indicates that all of the packets for all frames up to and including this + // FrameId have been successfully received (or otherwise do not need to be + // re-transmitted). + FrameId checkpoint_frame_id_ = FrameId::leader(); + + // The ID of the latest frame the Receiver seems to be aware of. + FrameId latest_expected_frame_id_ = FrameId::leader(); + + // The target playout delay for the last-enqueued frame. This is auto-updated + // when a frame is enqueued that changes the delay. + std::chrono::milliseconds target_playout_delay_; + FrameId playout_delay_change_at_frame_id_ = FrameId::first(); + + // The exact arrival time of the last RTCP packet. + Clock::time_point rtcp_packet_arrival_time_ = SenderPacketRouter::kNever; + + // The near-term average round trip time. This is updated with each Sender + // Report → Receiver Report round trip. This is initially zero, indicating the + // round trip time has not been measured yet. + Clock::duration round_trip_time_ = {}; + + // Maintain current stats in a Sender Report that is ready for sending at any + // time. This includes up-to-date lip-sync information, and packet and byte + // count stats. + RtcpSenderReport pending_sender_report_; + + // These are used to determine whether a key frame needs to be sent to the + // Receiver. When the Receiver provides a picture loss notification, the + // current checkpoint frame ID is stored in `picture_lost_at_frame_id_`. Then, + // while `last_enqueued_key_frame_id_` is less than or equal to + // `picture_lost_at_frame_id_`, the Sender knows it still needs to send a key + // frame to resolve the picture loss condition. In all other cases, the + // Receiver is either in a good state or is in the process of receiving the + // key frame that will make that happen. + FrameId picture_lost_at_frame_id_ = FrameId::leader(); + FrameId last_enqueued_key_frame_id_ = FrameId::leader(); + + // The current observer (optional). + raw_ptr observer_ = nullptr; + + // Because the observer may take action when frames are cancelled, such as + // calling APIs like EnqueueFrame(), `this` must be in a good state before + // the observer is notified of any pending frame cancellations. + std::vector pending_cancellations_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_SENDER_IMPL_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_report_builder.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_report_builder.cc new file mode 100644 index 0000000..df7ecff --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_report_builder.cc @@ -0,0 +1,79 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/sender_report_builder.h" + +#include "cast/streaming/impl/packet_util.h" +#include "util/osp_logging.h" + +namespace openscreen::cast { + +SenderReportBuilder::SenderReportBuilder(RtcpSession& session) + : session_(session) {} + +SenderReportBuilder::~SenderReportBuilder() = default; + +std::pair SenderReportBuilder::BuildPacket( + const RtcpSenderReport& sender_report, + ByteBuffer buffer) const { + OSP_CHECK_GE(buffer.size(), kRequiredBufferSize); + + uint8_t* const packet_begin = buffer.data(); + + RtcpCommonHeader header; + header.packet_type = RtcpPacketType::kSenderReport; + header.payload_size = kRtcpSenderReportSize; + if (sender_report.report_block) { + header.with.report_count = 1; + header.payload_size += kRtcpReportBlockSize; + } else { + header.with.report_count = 0; + } + header.AppendFields(buffer); + + AppendField(session_->sender_ssrc(), buffer); + const NtpTimestamp ntp_timestamp = + session_->ntp_converter().ToNtpTimestamp(sender_report.reference_time); + AppendField(ntp_timestamp, buffer); + AppendField(sender_report.rtp_timestamp.lower_32_bits(), buffer); + AppendField(sender_report.send_packet_count, buffer); + AppendField(sender_report.send_octet_count, buffer); + if (sender_report.report_block) { + sender_report.report_block->AppendFields(buffer); + } + + uint8_t* const packet_end = buffer.data(); + return std::make_pair(ByteBuffer(packet_begin, packet_end - packet_begin), + ToStatusReportId(ntp_timestamp)); +} + +Clock::time_point SenderReportBuilder::GetRecentReportTime( + StatusReportId report_id, + Clock::time_point on_or_before) const { + // Assumption: The `report_id` is the middle 32 bits of a 64-bit NtpTimestamp. + static_assert(ToStatusReportId(NtpTimestamp{0x0192a3b4c5d6e7f8}) == + StatusReportId{0xa3b4c5d6}, + "FIXME: ToStatusReportId() implementation changed."); + + // Compute the maximum possible NtpTimestamp. Then, use its uppermost 16 bits + // and the 32 bits from the report_id to produce a reconstructed NtpTimestamp. + const NtpTimestamp max_timestamp = + session_->ntp_converter().ToNtpTimestamp(on_or_before); + // max_timestamp: HH...... + // report_id: LLLL + // ↓↓ ↙↙↙↙ + // reconstructed: HHLLLL00 + NtpTimestamp reconstructed = (max_timestamp & (uint64_t{0xffff} << 48)) | + (static_cast(report_id) << 16); + // If the reconstructed timestamp is greater than the maximum one, rollover + // of the lower 48 bits occurred. Subtract one from the upper 16 bits to + // rectify that. + if (reconstructed > max_timestamp) { + reconstructed -= uint64_t{1} << 48; + } + + return session_->ntp_converter().ToLocalTime(reconstructed); +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_report_builder.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_report_builder.h new file mode 100644 index 0000000..94bac6c --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/sender_report_builder.h @@ -0,0 +1,50 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_SENDER_REPORT_BUILDER_H_ +#define CAST_STREAMING_IMPL_SENDER_REPORT_BUILDER_H_ + +#include + +#include + +#include "cast/streaming/impl/rtcp_common.h" +#include "cast/streaming/impl/rtcp_session.h" +#include "cast/streaming/impl/rtp_defines.h" +#include "platform/api/time.h" +#include "platform/base/span.h" +#include "util/raw_ref.h" + +namespace openscreen::cast { + +// Builds RTCP packets containing one Sender Report. +class SenderReportBuilder { + public: + explicit SenderReportBuilder(RtcpSession& session); + ~SenderReportBuilder(); + + // Serializes the given `sender_report` as a RTCP packet and writes it to + // `buffer` (which must be kRequiredBufferSize in size). Returns the subspan + // of `buffer` that contains the result and a StatusReportId the receiver + // might use in its own reports to reference this specific report. + std::pair BuildPacket( + const RtcpSenderReport& sender_report, + ByteBuffer buffer) const; + + // Returns the approximate reference time from a recently-built Sender Report, + // based on the given `report_id` and maximum possible reference time. + Clock::time_point GetRecentReportTime(StatusReportId report_id, + Clock::time_point on_or_before) const; + + // The required size (in bytes) of the buffer passed to BuildPacket(). + static constexpr int kRequiredBufferSize = + kRtcpCommonHeaderSize + kRtcpSenderReportSize + kRtcpReportBlockSize; + + private: + const raw_ref session_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_SENDER_REPORT_BUILDER_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_analyzer.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_analyzer.cc new file mode 100644 index 0000000..bd2fc9f --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_analyzer.cc @@ -0,0 +1,573 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/statistics_analyzer.h" + +#include + +#include "cast/streaming/impl/statistics_common.h" +#include "platform/base/trivial_clock_traits.h" +#include "util/chrono_helpers.h" + +namespace openscreen::cast { + +using openscreen::clock_operators::operator<<; + +namespace { + +constexpr Clock::duration kAnalysisInterval = std::chrono::milliseconds(500); + +constexpr size_t kMaxRecentPacketInfoMapSize = 1000; +constexpr size_t kMaxRecentFrameInfoMapSize = 200; + +constexpr int kDefaultMaxLatencyBucketMs = 800; +constexpr int kDefaultBucketWidthMs = 20; + +double InMilliseconds(Clock::duration duration) { + return static_cast(to_milliseconds(duration).count()); +} + +bool IsReceiverEvent(StatisticsEvent::Type event) { + return event == StatisticsEvent::Type::kFrameAckSent || + event == StatisticsEvent::Type::kFrameDecoded || + event == StatisticsEvent::Type::kFramePlayedOut || + event == StatisticsEvent::Type::kPacketReceived; +} + +} // namespace + +StatisticsAnalyzer::StatisticsAnalyzer( + SenderStatsClient* stats_client, + ClockNowFunctionPtr now, + TaskRunner& task_runner, + std::unique_ptr offset_estimator) + : stats_client_(stats_client), + offset_estimator_(std::move(offset_estimator)), + now_(now), + alarm_(now, task_runner), + start_time_(now()) { + statistics_collector_ = std::make_unique(now_); + InitHistograms(); +} + +StatisticsAnalyzer::~StatisticsAnalyzer() = default; + +void StatisticsAnalyzer::ScheduleAnalysis() { + Clock::time_point next_analysis_time = now_() + kAnalysisInterval; + alarm_.Schedule([this] { AnalyzeStatistics(); }, next_analysis_time); +} + +void StatisticsAnalyzer::InitHistograms() { + for (auto& histogram : histograms_.audio) { + histogram = + SimpleHistogram(0, kDefaultMaxLatencyBucketMs, kDefaultBucketWidthMs); + } + for (auto& histogram : histograms_.video) { + histogram = + SimpleHistogram(0, kDefaultMaxLatencyBucketMs, kDefaultBucketWidthMs); + } +} + +void StatisticsAnalyzer::AnalyzeStatistics() { + ProcessFrameEvents(statistics_collector_->TakeRecentFrameEvents()); + ProcessPacketEvents(statistics_collector_->TakeRecentPacketEvents()); + SendStatistics(); + ScheduleAnalysis(); +} + +void StatisticsAnalyzer::SendStatistics() { + if (!stats_client_) { + return; + } + + const Clock::time_point end_time = now_(); + stats_client_->OnStatisticsUpdated(SenderStats{ + .audio_statistics = + ConstructStatisticsList(end_time, StatisticsEvent::MediaType::kAudio), + .audio_histograms = histograms_.audio, + .video_statistics = + ConstructStatisticsList(end_time, StatisticsEvent::MediaType::kVideo), + .video_histograms = histograms_.video}); +} + +void StatisticsAnalyzer::ProcessFrameEvents( + const std::vector& frame_events) { + for (FrameEvent frame_event : frame_events) { + offset_estimator_->OnFrameEvent(frame_event); + + FrameStatsMap& frame_stats_map = frame_stats_.Get(frame_event.media_type); + auto it = frame_stats_map.find(frame_event.type); + if (it == frame_stats_map.end()) { + frame_stats_map.insert(std::make_pair( + frame_event.type, + FrameStatsAggregate{.event_counter = 1, + .sum_size = frame_event.size, + .sum_delay = frame_event.delay_delta})); + } else { + ++(it->second.event_counter); + it->second.sum_size += frame_event.size; + it->second.sum_delay += frame_event.delay_delta; + } + + RecordEventTimes(frame_event); + RecordFrameLatencies(frame_event); + } +} + +void StatisticsAnalyzer::ProcessPacketEvents( + const std::vector& packet_events) { + for (PacketEvent packet_event : packet_events) { + offset_estimator_->OnPacketEvent(packet_event); + + PacketStatsMap& packet_stats_map = + packet_stats_.Get(packet_event.media_type); + auto it = packet_stats_map.find(packet_event.type); + if (it == packet_stats_map.end()) { + packet_stats_map.insert( + std::make_pair(packet_event.type, + PacketStatsAggregate{.event_counter = 1, + .sum_size = packet_event.size})); + } else { + ++(it->second.event_counter); + it->second.sum_size += packet_event.size; + } + + RecordEventTimes(packet_event); + if (packet_event.type == StatisticsEvent::Type::kPacketSentToNetwork || + packet_event.type == StatisticsEvent::Type::kPacketReceived) { + RecordPacketLatencies(packet_event); + } else if (packet_event.type == + StatisticsEvent::Type::kPacketRetransmitted) { + // We only measure network latency for packets that are not retransmitted. + ErasePacketInfo(packet_event); + } + } +} + +void StatisticsAnalyzer::RecordFrameLatencies(const FrameEvent& frame_event) { + FrameInfoMap& frame_infos = recent_frame_infos_.Get(frame_event.media_type); + + // Event is too old, don't bother. + const bool map_is_full = frame_infos.size() == kMaxRecentFrameInfoMapSize; + if (map_is_full && frame_event.rtp_timestamp <= frame_infos.begin()->first) { + return; + } + + auto it = frame_infos.find(frame_event.rtp_timestamp); + if (it == frame_infos.end()) { + if (map_is_full) { + frame_infos.erase(frame_infos.begin()); + } + + auto emplace_result = + frame_infos.emplace(frame_event.rtp_timestamp, FrameInfo{}); + OSP_CHECK(emplace_result.second); + it = emplace_result.first; + } + + switch (frame_event.type) { + case StatisticsEvent::Type::kFrameCaptureBegin: + it->second.capture_begin_time = frame_event.timestamp; + break; + + case StatisticsEvent::Type::kFrameCaptureEnd: { + it->second.capture_end_time = frame_event.timestamp; + if (it->second.capture_begin_time != Clock::time_point::min()) { + const Clock::duration capture_latency = + frame_event.timestamp - it->second.capture_begin_time; + AddToLatencyAggregrate(StatisticType::kAvgCaptureLatencyMs, + capture_latency, frame_event.media_type); + AddToHistogram(HistogramType::kCaptureLatencyMs, frame_event.media_type, + InMilliseconds(capture_latency)); + } + } break; + + case StatisticsEvent::Type::kFrameEncoded: { + it->second.encode_end_time = frame_event.timestamp; + if (it->second.capture_end_time != Clock::time_point::min()) { + const Clock::duration encode_latency = + frame_event.timestamp - it->second.capture_end_time; + AddToLatencyAggregrate(StatisticType::kAvgEncodeTimeMs, encode_latency, + frame_event.media_type); + AddToHistogram(HistogramType::kEncodeTimeMs, frame_event.media_type, + InMilliseconds(encode_latency)); + } + } break; + + // Frame latency is the time from when the frame is encoded until the + // receiver ack for the frame is sent. + case StatisticsEvent::Type::kFrameAckSent: { + const auto adjusted_timestamp = + ToSenderTimestamp(frame_event.timestamp, frame_event.media_type); + if (!adjusted_timestamp) { + return; + } + + if (it->second.encode_end_time != Clock::time_point::min()) { + const Clock::duration frame_latency = + *adjusted_timestamp - it->second.encode_end_time; + AddToLatencyAggregrate(StatisticType::kAvgFrameLatencyMs, frame_latency, + frame_event.media_type); + } + } break; + + case StatisticsEvent::Type::kFramePlayedOut: { + const auto adjusted_timestamp = + ToSenderTimestamp(frame_event.timestamp, frame_event.media_type); + if (!adjusted_timestamp) { + return; + } + + if (it->second.capture_begin_time != Clock::time_point::min()) { + const Clock::duration e2e_latency = + *adjusted_timestamp - it->second.capture_begin_time; + AddToLatencyAggregrate(StatisticType::kAvgEndToEndLatencyMs, + e2e_latency, frame_event.media_type); + AddToHistogram(HistogramType::kEndToEndLatencyMs, + frame_event.media_type, InMilliseconds(e2e_latency)); + } + + // Positive delay means the frame is late. + if (frame_event.delay_delta > Clock::duration::zero()) { + session_stats_.Get(frame_event.media_type).late_frame_counter += 1; + AddToHistogram(HistogramType::kFrameLatenessMs, frame_event.media_type, + InMilliseconds(frame_event.delay_delta)); + } + } break; + + default: + break; + } +} + +void StatisticsAnalyzer::RecordPacketLatencies( + const PacketEvent& packet_event) { + FrameInfoMap& frame_infos = recent_frame_infos_.Get(packet_event.media_type); + + // Queueing latency is the time from when a frame is encoded to when the + // packet is first sent. + if (packet_event.type == StatisticsEvent::Type::kPacketSentToNetwork) { + const auto it = frame_infos.find(packet_event.rtp_timestamp); + + // We have an encode end time for a frame associated with this packet. + if (it != frame_infos.end()) { + const Clock::duration queueing_latency = + packet_event.timestamp - it->second.encode_end_time; + AddToLatencyAggregrate(StatisticType::kAvgQueueingLatencyMs, + queueing_latency, packet_event.media_type); + AddToHistogram(HistogramType::kQueueingLatencyMs, packet_event.media_type, + InMilliseconds(queueing_latency)); + } + } + + StatisticsAnalyzer::PacketKey key = + std::make_pair(packet_event.rtp_timestamp, packet_event.packet_id); + PacketInfoMap& packet_infos = + recent_packet_infos_.Get(packet_event.media_type); + + const auto it = packet_infos.find(key); + if (it == packet_infos.end()) { + packet_infos.insert( + std::make_pair(key, PacketInfo{.timestamp = packet_event.timestamp, + .type = packet_event.type})); + if (packet_infos.size() > kMaxRecentPacketInfoMapSize) { + packet_infos.erase(packet_infos.begin()); + } + } else { // We know when this packet was sent, and when it arrived. + PacketInfo value = it->second; + StatisticsEvent::Type recorded_type = value.type; + Clock::time_point packet_sent_time; + Clock::time_point packet_received_time; + if (recorded_type == StatisticsEvent::Type::kPacketSentToNetwork && + packet_event.type == StatisticsEvent::Type::kPacketReceived) { + packet_sent_time = value.timestamp; + packet_received_time = packet_event.timestamp; + } else if (recorded_type == StatisticsEvent::Type::kPacketReceived && + packet_event.type == + StatisticsEvent::Type::kPacketSentToNetwork) { + packet_sent_time = packet_event.timestamp; + packet_received_time = value.timestamp; + } else { + return; + } + + packet_infos.erase(it); + + // Use the offset estimator directly since we are trying to calculate the + // average network latency. + const std::optional receiver_offset = + offset_estimator_->GetEstimatedOffset(); + if (!receiver_offset) { + return; + } + packet_received_time -= *receiver_offset; + + const auto latency = packet_received_time - packet_sent_time; + AddToLatencyAggregrate(StatisticType::kAvgNetworkLatencyMs, latency, + packet_event.media_type); + AddToHistogram(HistogramType::kNetworkLatencyMs, packet_event.media_type, + InMilliseconds(latency)); + + // Packet latency is the time from when a frame is encoded until when the + // packet is received. + const auto frame_it = frame_infos.find(packet_event.rtp_timestamp); + if (frame_it != frame_infos.end()) { + const Clock::duration packet_latency = + packet_received_time - frame_it->second.encode_end_time; + AddToLatencyAggregrate(StatisticType::kAvgPacketLatencyMs, packet_latency, + packet_event.media_type); + AddToHistogram(HistogramType::kPacketLatencyMs, packet_event.media_type, + InMilliseconds(packet_latency)); + } + } +} + +void StatisticsAnalyzer::RecordEventTimes(const StatisticsEvent& event) { + SessionStats& session_stats = session_stats_.Get(event.media_type); + + Clock::time_point sender_timestamp = event.timestamp; + if (IsReceiverEvent(event.type)) { + const auto latency = offset_estimator_->GetEstimatedLatency(); + if (latency) { + const Clock::time_point estimated_sent_time = + event.received_timestamp - *latency; + session_stats.last_response_received_time = std::max( + session_stats.last_response_received_time, estimated_sent_time); + } + + const auto result = ToSenderTimestamp(event.timestamp, event.media_type); + if (!result) { + return; + } + sender_timestamp = *result; + } + + session_stats.first_event_time = + std::min(session_stats.first_event_time, sender_timestamp); + session_stats.last_event_time = + std::max(session_stats.last_event_time, sender_timestamp); +} + +void StatisticsAnalyzer::ErasePacketInfo(const PacketEvent& packet_event) { + const StatisticsAnalyzer::PacketKey key = + std::make_pair(packet_event.rtp_timestamp, packet_event.packet_id); + PacketInfoMap& packet_infos = + recent_packet_infos_.Get(packet_event.media_type); + packet_infos.erase(key); +} + +void StatisticsAnalyzer::AddToLatencyAggregrate( + StatisticType latency_stat, + Clock::duration latency_delta, + StatisticsEvent::MediaType media_type) { + LatencyStatsMap& latency_stats = latency_stats_.Get(media_type); + + auto it = latency_stats.find(latency_stat); + if (it == latency_stats.end()) { + latency_stats.insert(std::make_pair( + latency_stat, LatencyStatsAggregate{.data_point_counter = 1, + .sum_latency = latency_delta})); + } else { + ++(it->second.data_point_counter); + it->second.sum_latency += latency_delta; + } +} + +void StatisticsAnalyzer::AddToHistogram(HistogramType histogram, + StatisticsEvent::MediaType media_type, + int64_t sample) { + histograms_.Get(media_type)[static_cast(histogram)].Add(sample); +} + +SenderStats::StatisticsList StatisticsAnalyzer::ConstructStatisticsList( + Clock::time_point end_time, + StatisticsEvent::MediaType media_type) { + SenderStats::StatisticsList stats_list; + + PopulateFrameCountStat(StatisticsEvent::Type::kFrameDroppedByEncoder, + StatisticType::kNumFramesDroppedByEncoder, media_type, + stats_list); + + PopulateFrameCountStat(StatisticsEvent::Type::kFrameCaptureEnd, + StatisticType::kNumFramesCaptured, media_type, + stats_list); + + // kEnqueueFps + PopulateFpsStat(StatisticsEvent::Type::kFrameEncoded, + StatisticType::kEnqueueFps, media_type, end_time, stats_list); + + constexpr StatisticType kSupportedLatencyStats[] = { + StatisticType::kAvgEncodeTimeMs, StatisticType::kAvgCaptureLatencyMs, + StatisticType::kAvgQueueingLatencyMs, StatisticType::kAvgNetworkLatencyMs, + StatisticType::kAvgPacketLatencyMs, StatisticType::kAvgFrameLatencyMs, + StatisticType::kAvgEndToEndLatencyMs, + }; + for (StatisticType type : kSupportedLatencyStats) { + PopulateAvgLatencyStat(type, media_type, stats_list); + } + + // kEncodeRateKbps + PopulateFrameBitrateStat(StatisticsEvent::Type::kFrameEncoded, + StatisticType::kEncodeRateKbps, media_type, end_time, + stats_list); + + // kPacketTransmissionRateKbps + PopulatePacketBitrateStat(StatisticsEvent::Type::kPacketSentToNetwork, + StatisticType::kPacketTransmissionRateKbps, + media_type, end_time, stats_list); + + // kNumPacketsSent + PopulatePacketCountStat(StatisticsEvent::Type::kPacketSentToNetwork, + StatisticType::kNumPacketsSent, media_type, + stats_list); + + // kNumPacketsReceived + PopulatePacketCountStat(StatisticsEvent::Type::kPacketReceived, + StatisticType::kNumPacketsReceived, media_type, + stats_list); + + // kTimeSinceLastReceiverResponseMs + // kFirstEventTimeMs + // kLastEventTimeMs + // kNumLateFrames + PopulateSessionStats(media_type, end_time, stats_list); + + return stats_list; +} + +void StatisticsAnalyzer::PopulatePacketCountStat( + StatisticsEvent::Type event, + StatisticType stat, + StatisticsEvent::MediaType media_type, + SenderStats::StatisticsList& stats_list) { + PacketStatsMap& stats_map = packet_stats_.Get(media_type); + + auto it = stats_map.find(event); + if (it != stats_map.end()) { + stats_list[static_cast(stat)] = it->second.event_counter; + } +} + +void StatisticsAnalyzer::PopulateFrameCountStat( + StatisticsEvent::Type event, + StatisticType stat, + StatisticsEvent::MediaType media_type, + SenderStats::StatisticsList& stats_list) { + FrameStatsMap& stats_map = frame_stats_.Get(media_type); + + const auto it = stats_map.find(event); + if (it != stats_map.end()) { + stats_list[static_cast(stat)] = it->second.event_counter; + } +} + +void StatisticsAnalyzer::PopulateFpsStat( + StatisticsEvent::Type event, + StatisticType stat, + StatisticsEvent::MediaType media_type, + Clock::time_point end_time, + SenderStats::StatisticsList& stats_list) { + FrameStatsMap& stats_map = frame_stats_.Get(media_type); + + const auto it = stats_map.find(event); + if (it != stats_map.end()) { + const Clock::duration duration = end_time - start_time_; + if (duration != Clock::duration::zero()) { + const int count = it->second.event_counter; + const double fps = (count / InMilliseconds(duration)) * 1000; + stats_list[static_cast(stat)] = fps; + } + } +} + +void StatisticsAnalyzer::PopulateAvgLatencyStat( + StatisticType stat, + StatisticsEvent::MediaType media_type, + SenderStats::StatisticsList& stats_list + +) { + LatencyStatsMap& latency_map = latency_stats_.Get(media_type); + + const auto it = latency_map.find(stat); + if (it != latency_map.end() && it->second.data_point_counter > 0) { + const double avg_latency = + InMilliseconds(it->second.sum_latency) / it->second.data_point_counter; + stats_list[static_cast(stat)] = avg_latency; + } +} + +void StatisticsAnalyzer::PopulateFrameBitrateStat( + StatisticsEvent::Type event, + StatisticType stat, + StatisticsEvent::MediaType media_type, + Clock::time_point end_time, + SenderStats::StatisticsList& stats_list) { + FrameStatsMap& stats_map = frame_stats_.Get(media_type); + + const auto it = stats_map.find(event); + if (it != stats_map.end()) { + const Clock::duration duration = end_time - start_time_; + if (duration != Clock::duration::zero()) { + const double kbps = it->second.sum_size / InMilliseconds(duration) * 8; + stats_list[static_cast(stat)] = kbps; + } + } +} + +void StatisticsAnalyzer::PopulatePacketBitrateStat( + StatisticsEvent::Type event, + StatisticType stat, + StatisticsEvent::MediaType media_type, + Clock::time_point end_time, + SenderStats::StatisticsList& stats_list) { + PacketStatsMap& stats_map = packet_stats_.Get(media_type); + + auto it = stats_map.find(event); + if (it != stats_map.end()) { + const Clock::duration duration = end_time - start_time_; + if (duration != Clock::duration::zero()) { + const double kbps = it->second.sum_size / InMilliseconds(duration) * 8; + stats_list[static_cast(stat)] = kbps; + } + } +} + +void StatisticsAnalyzer::PopulateSessionStats( + StatisticsEvent::MediaType media_type, + Clock::time_point end_time, + SenderStats::StatisticsList& stats_list) { + SessionStats& session_stats = session_stats_.Get(media_type); + + if (session_stats.first_event_time != Clock::time_point::min()) { + stats_list[static_cast(StatisticType::kFirstEventTimeMs)] = + InMilliseconds(session_stats.first_event_time.time_since_epoch()); + } + + if (session_stats.last_event_time != Clock::time_point::min()) { + stats_list[static_cast(StatisticType::kLastEventTimeMs)] = + InMilliseconds(session_stats.last_event_time.time_since_epoch()); + } + + if (session_stats.last_response_received_time != Clock::time_point::min()) { + stats_list[static_cast( + StatisticType::kTimeSinceLastReceiverResponseMs)] = + InMilliseconds(end_time - session_stats.last_response_received_time); + } + + stats_list[static_cast(StatisticType::kNumLateFrames)] = + session_stats.late_frame_counter; +} + +std::optional StatisticsAnalyzer::ToSenderTimestamp( + Clock::time_point receiver_timestamp, + StatisticsEvent::MediaType media_type) const { + const std::optional receiver_offset = + offset_estimator_->GetEstimatedOffset(); + if (!receiver_offset) { + return {}; + } + return receiver_timestamp - *receiver_offset; +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_analyzer.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_analyzer.h new file mode 100644 index 0000000..0e23e3f --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_analyzer.h @@ -0,0 +1,208 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_STATISTICS_ANALYZER_H_ +#define CAST_STREAMING_IMPL_STATISTICS_ANALYZER_H_ + +#include +#include +#include +#include +#include + +#include "cast/streaming/impl/clock_offset_estimator.h" +#include "cast/streaming/impl/statistics_collector.h" +#include "cast/streaming/public/statistics.h" +#include "platform/api/time.h" +#include "util/alarm.h" +#include "util/raw_ptr.h" + +namespace openscreen::cast { + +class StatisticsAnalyzer { + public: + StatisticsAnalyzer(SenderStatsClient* stats_client, + ClockNowFunctionPtr now, + TaskRunner& task_runner, + std::unique_ptr offset_estimator); + ~StatisticsAnalyzer(); + + void ScheduleAnalysis(); + + // Get the statistics collector managed by this analyzer. + StatisticsCollector* statistics_collector() { + return statistics_collector_.get(); + } + + private: + struct FrameStatsAggregate { + int event_counter; + uint32_t sum_size; + Clock::duration sum_delay; + }; + + struct PacketStatsAggregate { + int event_counter; + uint32_t sum_size; + }; + + struct LatencyStatsAggregate { + int data_point_counter; + Clock::duration sum_latency; + }; + + struct FrameInfo { + Clock::time_point capture_begin_time = Clock::time_point::min(); + Clock::time_point capture_end_time = Clock::time_point::min(); + Clock::time_point encode_end_time = Clock::time_point::min(); + }; + + struct PacketInfo { + Clock::time_point timestamp; + StatisticsEvent::Type type; + }; + + struct SessionStats { + Clock::time_point first_event_time = Clock::time_point::max(); + Clock::time_point last_event_time = Clock::time_point::min(); + Clock::time_point last_response_received_time = Clock::time_point::min(); + int late_frame_counter = 0; + }; + + // Named std::pair equivalent for audio + video classes. + template + struct AVPair { + T audio; + T video; + + const T& Get(StatisticsEvent::MediaType media_type) const { + if (media_type == StatisticsEvent::MediaType::kAudio) { + return audio; + } + OSP_CHECK(media_type == StatisticsEvent::MediaType::kVideo); + return video; + } + T& Get(StatisticsEvent::MediaType media_type) { + return const_cast(const_cast(this)->Get(media_type)); + } + }; + + using FrameStatsMap = std::map; + using PacketStatsMap = std::map; + using LatencyStatsMap = std::map; + + using FrameInfoMap = std::map; + using PacketKey = std::pair; + using PacketInfoMap = std::map; + + // Initialize the stats histograms with the preferred min, max, and width. + void InitHistograms(); + + // Takes the Frame and Packet events from the `collector_`, and processes them + // into a form expected by `stats_client_`. Then sends the stats, and + // schedules a future analysis. + void AnalyzeStatistics(); + + // Constructs a stats list, and sends it to `stats_client_`; + void SendStatistics(); + + // Handles incoming stat events, and adds their infos to all of the proper + // stats maps / aggregates. + void ProcessFrameEvents(const std::vector& frame_events); + void ProcessPacketEvents(const std::vector& packet_events); + void RecordFrameLatencies(const FrameEvent& frame_event); + void RecordPacketLatencies(const PacketEvent& packet_event); + void RecordEventTimes(const StatisticsEvent& event); + void ErasePacketInfo(const PacketEvent& packet_event); + void AddToLatencyAggregrate(StatisticType latency_stat, + Clock::duration latency_delta, + StatisticsEvent::MediaType media_type); + void AddToHistogram(HistogramType histogram, + StatisticsEvent::MediaType media_type, + int64_t sample); + + // Creates a stats list, and populates the entries based on stored stats info + // / aggregates for each stat field. + SenderStats::StatisticsList ConstructStatisticsList( + Clock::time_point end_time, + StatisticsEvent::MediaType media_type); + + void PopulatePacketCountStat(StatisticsEvent::Type event, + StatisticType stat, + StatisticsEvent::MediaType media_type, + SenderStats::StatisticsList& stats_list); + + void PopulateFrameCountStat(StatisticsEvent::Type event, + StatisticType stat, + StatisticsEvent::MediaType media_type, + SenderStats::StatisticsList& stats_list); + + void PopulateFpsStat(StatisticsEvent::Type event, + StatisticType stat, + StatisticsEvent::MediaType media_type, + Clock::time_point end_time, + SenderStats::StatisticsList& stats_list); + + void PopulateAvgLatencyStat(StatisticType stat, + StatisticsEvent::MediaType media_type, + SenderStats::StatisticsList& stats_list); + + void PopulateFrameBitrateStat(StatisticsEvent::Type event, + StatisticType stat, + StatisticsEvent::MediaType media_type, + Clock::time_point end_time, + SenderStats::StatisticsList& stats_list); + + void PopulatePacketBitrateStat(StatisticsEvent::Type event, + StatisticType stat, + StatisticsEvent::MediaType media_type, + Clock::time_point end_time, + SenderStats::StatisticsList& stats_list); + + void PopulateSessionStats(StatisticsEvent::MediaType media_type, + Clock::time_point end_time, + SenderStats::StatisticsList& stats_list); + + // Calculates the offset between the sender and receiver clocks and returns + // the sender-side version of this receiver timestamp, if possible. + std::optional ToSenderTimestamp( + Clock::time_point receiver_timestamp, + StatisticsEvent::MediaType media_type) const; + + // The statistics client to which we report analyzed statistics. + const raw_ptr stats_client_; + + // The statistics collector from which we take the un-analyzed stats packets. + std::unique_ptr statistics_collector_; + + // Keeps track of the best-guess clock offset between the sender and receiver. + std::unique_ptr offset_estimator_; + + // Keep track of time and events for this analyzer. + ClockNowFunctionPtr now_; + Alarm alarm_; + Clock::time_point start_time_; + + // Maps of frame / packet infos used for stats that rely on seeing multiple + // events. For example, network latency is the calculated time difference + // between went a packet is sent, and when it is received. + AVPair recent_frame_infos_; + AVPair recent_packet_infos_; + + // Aggregate statistics. + AVPair frame_stats_; + AVPair packet_stats_; + AVPair latency_stats_; + + // Stats that relate to the entirety of the session. For example, total late + // frames, or time of last event. + AVPair session_stats_; + + // Histograms. + AVPair histograms_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_STATISTICS_ANALYZER_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_collector.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_collector.cc new file mode 100644 index 0000000..daed12c --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_collector.cc @@ -0,0 +1,74 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/statistics_collector.h" + +#include + +#include +#include + +#include "cast/streaming/public/environment.h" +#include "util/big_endian.h" + +namespace openscreen::cast { + +StatisticsCollector::StatisticsCollector(ClockNowFunctionPtr now) : now_(now) {} +StatisticsCollector::~StatisticsCollector() = default; + +void StatisticsCollector::CollectPacketSentEvent(ByteView packet, + PacketMetadata metadata) { + PacketEvent event; + + // Populate the new PacketEvent by parsing the wire-format `packet`. + event.timestamp = now_(); + event.type = StatisticsEvent::Type::kPacketSentToNetwork; + + BigEndianReader reader(packet.data(), packet.size()); + bool success = reader.Skip(4); + uint32_t truncated_rtp_timestamp = 0; + success &= reader.Read(&truncated_rtp_timestamp); + success &= reader.Skip(4); + + event.rtp_timestamp = metadata.rtp_timestamp.Expand(truncated_rtp_timestamp); + event.media_type = StatisticsEvent::ToMediaType(metadata.stream_type); + + success &= reader.Skip(2); + success &= reader.Read(&event.packet_id); + success &= reader.Read(&event.max_packet_id); + + // Check that the cast is safe. + // TODO(issuetracker.google.com/3576782): move to checked casts when ready. + static_assert(static_cast(std::numeric_limits::max()) <= + static_cast(std::numeric_limits::max()), + "invalid type cast assumption"); + OSP_CHECK_LE(packet.size(), + static_cast(std::numeric_limits::max())); + event.size = static_cast(packet.size()); + OSP_CHECK(success); + + recent_packet_events_.emplace_back(event); +} + +void StatisticsCollector::CollectPacketEvent(PacketEvent event) { + recent_packet_events_.emplace_back(event); +} + +void StatisticsCollector::CollectFrameEvent(FrameEvent event) { + recent_frame_events_.emplace_back(event); +} + +std::vector StatisticsCollector::TakeRecentPacketEvents() { + std::vector out; + recent_packet_events_.swap(out); + return out; +} + +std::vector StatisticsCollector::TakeRecentFrameEvents() { + std::vector out; + recent_frame_events_.swap(out); + return out; +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_collector.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_collector.h new file mode 100644 index 0000000..6067e5d --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_collector.h @@ -0,0 +1,64 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_STATISTICS_COLLECTOR_H_ +#define CAST_STREAMING_IMPL_STATISTICS_COLLECTOR_H_ + +#include + +#include "cast/streaming/impl/statistics_common.h" +#include "platform/api/time.h" +#include "platform/base/span.h" + +namespace openscreen::cast { + +// This POD struct contains helpful information about a given packet that is +// not stored directly on the packet itself. +struct PacketMetadata { + // The stream type (audio, video, unknown) of this packet. + StreamType stream_type; + + // The RTP timestamp associated with this packet. + RtpTimeTicks rtp_timestamp; +}; + +// This class is responsible for gathering packet and frame statistics using its +// Collect*() methods, that can then be taken by consumers using the Take*() +// methods. +class StatisticsCollector { + public: + explicit StatisticsCollector(ClockNowFunctionPtr now); + ~StatisticsCollector(); + + // Informs the collector that a packet has been sent. The collector will then + // generate a packet event that is then added to `recent_packet_events_`. + void CollectPacketSentEvent(ByteView packet, PacketMetadata metadata); + + // Informs the collector that a packet event has occurred. This event is then + // added to `recent_packet_events_`. + void CollectPacketEvent(PacketEvent event); + + // Informs the collector that a frame event has occurred. This event is then + // added to `recent_frame_events_`. + void CollectFrameEvent(FrameEvent event); + + // Returns the current collection of packet events stored in + // `recent_packet_events_`. After calling this method, `recent_packet_events_` + // is reset to an empty vector. + std::vector TakeRecentPacketEvents(); + + // Returns the current collection of frame events stored in + // `recent_frame_events_`. After calling this method, `recent_frame_events_` + // is reset to an empty vector. + std::vector TakeRecentFrameEvents(); + + private: + ClockNowFunctionPtr now_; + std::vector recent_packet_events_; + std::vector recent_frame_events_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_STATISTICS_COLLECTOR_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_common.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_common.cc new file mode 100644 index 0000000..a25f562 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_common.cc @@ -0,0 +1,115 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/statistics_common.h" + +#include "util/osp_logging.h" + +namespace openscreen::cast { + +// static +StatisticsEvent::Type StatisticsEvent::FromWireType(WireType wire_type) { + switch (wire_type) { + case WireType::kAudioAckSent: + case WireType::kVideoAckSent: + case WireType::kUnifiedAckSent: + return Type::kFrameAckSent; + + case WireType::kAudioPlayoutDelay: + case WireType::kVideoRenderDelay: + case WireType::kUnifiedRenderDelay: + return Type::kFramePlayedOut; + + case WireType::kAudioFrameDecoded: + case WireType::kVideoFrameDecoded: + case WireType::kUnifiedFrameDecoded: + return Type::kFrameDecoded; + + case WireType::kAudioPacketReceived: + case WireType::kVideoPacketReceived: + case WireType::kUnifiedPacketReceived: + return Type::kPacketReceived; + + default: + OSP_VLOG << "Unexpected RTCP log message received: " + << static_cast(wire_type); + return Type::kUnknown; + } +} + +// static +StatisticsEvent::WireType StatisticsEvent::ToWireType(Type type) { + switch (type) { + case Type::kUnknown: + return WireType::kUnknown; + + case Type::kFrameAckSent: + return WireType::kUnifiedAckSent; + + case Type::kFramePlayedOut: + return WireType::kUnifiedRenderDelay; + + case Type::kFrameDecoded: + return WireType::kUnifiedFrameDecoded; + + case Type::kPacketReceived: + return WireType::kUnifiedPacketReceived; + + default: + OSP_VLOG << "Unknown RTCP log message event type: " + << static_cast(type); + return WireType::kUnknown; + } +} + +// static +StatisticsEvent::MediaType StatisticsEvent::ToMediaType(StreamType type) { + switch (type) { + case StreamType::kUnknown: + return MediaType::kUnknown; + case StreamType::kAudio: + return MediaType::kAudio; + case StreamType::kVideo: + return MediaType::kVideo; + } + + OSP_NOTREACHED(); +} + +StatisticsEvent::StatisticsEvent(const StatisticsEvent& other) = default; +StatisticsEvent::StatisticsEvent(StatisticsEvent&& other) noexcept = default; +StatisticsEvent& StatisticsEvent::operator=(const StatisticsEvent& other) = + default; +StatisticsEvent& StatisticsEvent::operator=(StatisticsEvent&& other) = default; + +bool StatisticsEvent::operator==(const StatisticsEvent& other) const { + return frame_id == other.frame_id && type == other.type && + media_type == other.media_type && + rtp_timestamp == other.rtp_timestamp && size == other.size && + timestamp == other.timestamp && + received_timestamp == other.received_timestamp; +} + +FrameEvent::FrameEvent(const FrameEvent& other) = default; +FrameEvent::FrameEvent(FrameEvent&& other) noexcept = default; +FrameEvent& FrameEvent::operator=(const FrameEvent& other) = default; +FrameEvent& FrameEvent::operator=(FrameEvent&& other) = default; + +bool FrameEvent::operator==(const FrameEvent& other) const { + return StatisticsEvent::operator==(other) && width == other.width && + height == other.height && delay_delta == other.delay_delta && + key_frame == other.key_frame && target_bitrate == other.target_bitrate; +} + +PacketEvent::PacketEvent(const PacketEvent& other) = default; +PacketEvent::PacketEvent(PacketEvent&& other) noexcept = default; +PacketEvent& PacketEvent::operator=(const PacketEvent& other) = default; +PacketEvent& PacketEvent::operator=(PacketEvent&& other) = default; + +bool PacketEvent::operator==(const PacketEvent& other) const { + return StatisticsEvent::operator==(other) && packet_id == other.packet_id && + max_packet_id == other.max_packet_id; +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_common.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_common.h new file mode 100644 index 0000000..69f2dba --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_common.h @@ -0,0 +1,223 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_STATISTICS_COMMON_H_ +#define CAST_STREAMING_IMPL_STATISTICS_COMMON_H_ + +#include +#include + +#include "cast/streaming/public/constants.h" +#include "cast/streaming/public/frame_id.h" +#include "cast/streaming/rtp_time.h" +#include "platform/api/time.h" + +namespace openscreen::cast { + +struct StatisticsEvent { + enum class Type : int { + kUnknown = 0, + + // Sender side frame events. + kFrameCaptureBegin = 1, + kFrameCaptureEnd = 2, + kFrameEncoded = 3, + kFrameAckReceived = 4, + + // Receiver side frame events. + kFrameAckSent = 5, + kFrameDecoded = 6, + kFramePlayedOut = 7, + + // Sender side packet events. + kPacketSentToNetwork = 8, + kPacketRetransmitted = 9, + kPacketRtxRejected = 10, + + // Receiver side packet events. + kPacketReceived = 11, + kFrameDroppedByEncoder = 15, + + kNumOfEvents = kFrameDroppedByEncoder + 1 + }; + + // Serialized values for the statistics events for use by the RTCP builder + // and parser logic. *Do not modify existing values* since they are shared by + // both libcast-based devices as well as a variety of legacy implementations. + // + // NOTE: Events 1 to 8 have been replaced with events 11 to 14 (e.g. + // kAudioAckSent and kVideoAckSent merged into a single event kAckSent). + // Events 9 and 10 (to log duplicated packets) have been fully removed. Future + // events may reuse those values. + enum class WireType : uint8_t { + kUnknown = 0, + + // Legacy audio event types. + kAudioAckSent = 1, + kAudioPlayoutDelay = 2, + kAudioFrameDecoded = 3, + kAudioPacketReceived = 4, + + // Legacy video event types. + kVideoAckSent = 5, + kVideoRenderDelay = 6, + kVideoFrameDecoded = 7, + kVideoPacketReceived = 8, + + // New unified event types. + kUnifiedAckSent = 11, + kUnifiedRenderDelay = 12, + kUnifiedFrameDecoded = 13, + kUnifiedPacketReceived = 14, + + kNumOfEvents = kUnifiedPacketReceived + 1 + }; + + enum class MediaType : int { kUnknown = 0, kAudio = 1, kVideo = 2 }; + + static Type FromWireType(WireType wire_type); + static WireType ToWireType(Type type); + static MediaType ToMediaType(StreamType type); + + constexpr StatisticsEvent(FrameId frame_id, + Type type, + MediaType media_type, + RtpTimeTicks rtp_timestamp, + uint32_t size, + Clock::time_point timestamp, + Clock::time_point received_timestamp) + : frame_id(frame_id), + type(type), + media_type(media_type), + rtp_timestamp(rtp_timestamp), + size(size), + timestamp(timestamp), + received_timestamp(received_timestamp) {} + + constexpr StatisticsEvent() = default; + StatisticsEvent(const StatisticsEvent& other); + StatisticsEvent(StatisticsEvent&& other) noexcept; + StatisticsEvent& operator=(const StatisticsEvent& other); + StatisticsEvent& operator=(StatisticsEvent&& other); + ~StatisticsEvent() = default; + + bool operator==(const StatisticsEvent& other) const; + + // The frame this event is associated with. + FrameId frame_id; + + // The type of this frame event. + Type type = Type::kUnknown; + + // Whether this was audio or video (or unknown). + MediaType media_type = MediaType::kUnknown; + + // The RTP timestamp of the frame this event is associated with. + RtpTimeTicks rtp_timestamp; + + // Size of this packet, or the frame it is associated with. + // Note: we use uint32_t instead of size_t for byte count because this struct + // is sent over IPC which could span 32 & 64 bit processes. + uint32_t size = 0; + + // Time of event logged. + Clock::time_point timestamp; + + // Time that the event was received by the sender. Only set for receiver-side + // events. + Clock::time_point received_timestamp; +}; + +struct FrameEvent : public StatisticsEvent { + constexpr FrameEvent(FrameId frame_id_in, + Type type_in, + MediaType media_type_in, + RtpTimeTicks rtp_timestamp_in, + uint32_t size_in, + Clock::time_point timestamp_in, + Clock::time_point received_timestamp_in, + int width, + int height, + Clock::duration delay_delta, + bool key_frame, + int target_bitrate) + : StatisticsEvent(frame_id_in, + type_in, + media_type_in, + rtp_timestamp_in, + size_in, + timestamp_in, + received_timestamp_in), + width(width), + height(height), + delay_delta(delay_delta), + key_frame(key_frame), + target_bitrate(target_bitrate) {} + + constexpr FrameEvent() = default; + FrameEvent(const FrameEvent& other); + FrameEvent(FrameEvent&& other) noexcept; + FrameEvent& operator=(const FrameEvent& other); + FrameEvent& operator=(FrameEvent&& other); + ~FrameEvent() = default; + + bool operator==(const FrameEvent& other) const; + + // Resolution of the frame. Only set for video FRAME_CAPTURE_END events. + int width = 0; + int height = 0; + + // Only set for FRAME_PLAYOUT events. + // If this value is zero the frame is rendered on time. + // If this value is positive it means the frame is rendered late. + // If this value is negative it means the frame is rendered early. + Clock::duration delay_delta{}; + + // Whether the frame is a key frame. Only set for video FRAME_ENCODED event. + bool key_frame = false; + + // The requested target bitrate of the encoder at the time the frame is + // encoded. Only set for video FRAME_ENCODED event. + int target_bitrate = 0; +}; + +struct PacketEvent : public StatisticsEvent { + constexpr PacketEvent(FrameId frame_id_in, + Type type_in, + MediaType media_type_in, + RtpTimeTicks rtp_timestamp_in, + uint32_t size_in, + Clock::time_point timestamp_in, + Clock::time_point received_timestamp_in, + uint16_t packet_id, + uint16_t max_packet_id) + : StatisticsEvent(frame_id_in, + type_in, + media_type_in, + rtp_timestamp_in, + size_in, + timestamp_in, + received_timestamp_in), + packet_id(packet_id), + max_packet_id(max_packet_id) {} + + constexpr PacketEvent() = default; + PacketEvent(const PacketEvent& other); + PacketEvent(PacketEvent&& other) noexcept; + PacketEvent& operator=(const PacketEvent& other); + PacketEvent& operator=(PacketEvent&& other); + ~PacketEvent() = default; + + bool operator==(const PacketEvent& other) const; + + // The packet this event is associated with. + uint16_t packet_id = 0; + + // The highest packet ID seen so far at time of event. + uint16_t max_packet_id = 0; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_STATISTICS_COMMON_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_dispatcher.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_dispatcher.cc new file mode 100644 index 0000000..59ed5a9 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_dispatcher.cc @@ -0,0 +1,163 @@ +// Copyright 2025 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/impl/statistics_dispatcher.h" + +#include + +#include "cast/streaming/impl/rtcp_common.h" +#include "cast/streaming/impl/rtp_defines.h" +#include "cast/streaming/impl/statistics_collector.h" +#include "cast/streaming/impl/statistics_common.h" +#include "cast/streaming/public/encoded_frame.h" +#include "cast/streaming/public/environment.h" +#include "cast/streaming/public/session_config.h" +#include "platform/base/trivial_clock_traits.h" +#include "util/chrono_helpers.h" +#include "util/osp_logging.h" +#include "util/std_util.h" +#include "util/trace_logging.h" + +namespace openscreen::cast { + +using clock_operators::operator<<; + +StatisticsDispatcher::StatisticsDispatcher(Environment& environment) + : environment_(environment) {} +StatisticsDispatcher::~StatisticsDispatcher() = default; + +void StatisticsDispatcher::DispatchEnqueueEvents(StreamType stream_type, + const EncodedFrame& frame) { + if (!environment_->statistics_collector()) { + return; + } + const auto media_type = StatisticsEvent::ToMediaType(stream_type); + + // Submit a capture begin event. + FrameEvent capture_begin_event; + capture_begin_event.type = StatisticsEvent::Type::kFrameCaptureBegin; + capture_begin_event.media_type = media_type; + capture_begin_event.rtp_timestamp = frame.rtp_timestamp; + capture_begin_event.timestamp = + (frame.capture_begin_time > Clock::time_point::min()) + ? frame.capture_begin_time + : environment_->now(); + environment_->statistics_collector()->CollectFrameEvent( + std::move(capture_begin_event)); + + // Submit a capture end event. + FrameEvent capture_end_event; + capture_end_event.type = StatisticsEvent::Type::kFrameCaptureEnd; + capture_end_event.media_type = media_type; + capture_end_event.rtp_timestamp = frame.rtp_timestamp; + capture_end_event.timestamp = + (frame.capture_end_time > Clock::time_point::min()) + ? frame.capture_end_time + : environment_->now(); + environment_->statistics_collector()->CollectFrameEvent( + std::move(capture_end_event)); + + // Submit an encoded event. + FrameEvent encode_event; + encode_event.timestamp = environment_->now(); + encode_event.type = StatisticsEvent::Type::kFrameEncoded; + encode_event.media_type = media_type; + encode_event.rtp_timestamp = frame.rtp_timestamp; + encode_event.frame_id = frame.frame_id; + encode_event.size = static_cast(frame.data.size()); + encode_event.key_frame = + frame.dependency == openscreen::cast::EncodedFrame::Dependency::kKeyFrame; + + environment_->statistics_collector()->CollectFrameEvent( + std::move(encode_event)); +} + +void StatisticsDispatcher::DispatchAckEvent(StreamType stream_type, + RtpTimeTicks rtp_timestamp, + FrameId frame_id) { + if (!environment_->statistics_collector()) { + return; + } + + FrameEvent ack_event; + ack_event.timestamp = environment_->now(); + ack_event.type = StatisticsEvent::Type::kFrameAckReceived; + ack_event.media_type = StatisticsEvent::ToMediaType(stream_type); + ack_event.rtp_timestamp = rtp_timestamp; + ack_event.frame_id = frame_id; + + environment_->statistics_collector()->CollectFrameEvent(std::move(ack_event)); +} + +void StatisticsDispatcher::DispatchFrameDropEvent(StreamType stream_type, + FrameId frame_id, + RtpTimeTicks rtp_timestamp, + Clock::time_point drop_time) { + if (!environment_->statistics_collector()) { + return; + } + + FrameEvent drop_event; + drop_event.timestamp = drop_time; + drop_event.type = StatisticsEvent::Type::kFrameDroppedByEncoder; + drop_event.media_type = StatisticsEvent::ToMediaType(stream_type); + drop_event.rtp_timestamp = rtp_timestamp; + drop_event.frame_id = frame_id; + + environment_->statistics_collector()->CollectFrameEvent( + std::move(drop_event)); +} + +void StatisticsDispatcher::DispatchFrameLogMessages( + StreamType stream_type, + const std::vector& messages) { + if (!environment_->statistics_collector()) { + return; + } + + const Clock::time_point now = environment_->now(); + const auto media_type = StatisticsEvent::ToMediaType(stream_type); + for (const RtcpReceiverFrameLogMessage& log_message : messages) { + for (const RtcpReceiverEventLogMessage& event_message : + log_message.messages) { + switch (event_message.type) { + case StatisticsEvent::Type::kPacketReceived: { + PacketEvent event; + event.timestamp = event_message.timestamp; + event.received_timestamp = now; + event.type = event_message.type; + event.media_type = media_type; + event.rtp_timestamp = log_message.rtp_timestamp; + event.packet_id = event_message.packet_id; + environment_->statistics_collector()->CollectPacketEvent( + std::move(event)); + } break; + + case StatisticsEvent::Type::kFrameAckSent: + case StatisticsEvent::Type::kFrameDecoded: + case StatisticsEvent::Type::kFramePlayedOut: { + FrameEvent event; + event.timestamp = event_message.timestamp; + event.received_timestamp = now; + event.type = event_message.type; + event.media_type = media_type; + event.rtp_timestamp = log_message.rtp_timestamp; + if (event.type == StatisticsEvent::Type::kFramePlayedOut) { + event.delay_delta = event_message.delay; + } + environment_->statistics_collector()->CollectFrameEvent( + std::move(event)); + } break; + + default: + OSP_VLOG << "Received log message via RTCP that we did not expect, " + "StatisticsEvent::Type=" + << static_cast(event_message.type); + break; + } + } + } +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_dispatcher.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_dispatcher.h new file mode 100644 index 0000000..4ce3d2a --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/impl/statistics_dispatcher.h @@ -0,0 +1,58 @@ +// Copyright 2025 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_IMPL_STATISTICS_DISPATCHER_H_ +#define CAST_STREAMING_IMPL_STATISTICS_DISPATCHER_H_ + +#include + +#include "cast/streaming/impl/statistics_common.h" +#include "platform/api/time.h" +#include "platform/base/span.h" +#include "util/raw_ref.h" + +namespace openscreen::cast { + +class StatisticsCollector; +class Environment; +struct EncodedFrame; +struct RtcpReceiverFrameLogMessage; + +// This class is responsible for dispatching statistics events. +class StatisticsDispatcher { + public: + explicit StatisticsDispatcher(Environment& environment); + + StatisticsDispatcher(const StatisticsDispatcher&) = delete; + StatisticsDispatcher& operator=(const StatisticsDispatcher&) = delete; + StatisticsDispatcher(StatisticsDispatcher&&) noexcept = delete; + StatisticsDispatcher& operator=(StatisticsDispatcher&&) = delete; + ~StatisticsDispatcher(); + + // Dispatches enqueue events for a given frame. + void DispatchEnqueueEvents(StreamType stream_type, const EncodedFrame& frame); + + // Dispatches frame log messages. + void DispatchFrameLogMessages( + StreamType stream_type, + const std::vector& messages); + + // Dispatches an ack event. + void DispatchAckEvent(StreamType stream_type, + RtpTimeTicks rtp_timestamp, + FrameId frame_id); + + // Dispatches a frame dropped by encoder event. + void DispatchFrameDropEvent(StreamType stream_type, + FrameId frame_id, + RtpTimeTicks rtp_timestamp, + Clock::time_point drop_time); + + private: + const raw_ref environment_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_IMPL_STATISTICS_DISPATCHER_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/message_fields.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/message_fields.cc new file mode 100644 index 0000000..4602a69 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/message_fields.cc @@ -0,0 +1,47 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/message_fields.h" + +#include +#include + +#include "util/enum_name_table.h" +#include "util/osp_logging.h" + +namespace openscreen::cast { +namespace { + +constexpr EnumNameTable kAudioCodecNames{ + {{"aac", AudioCodec::kAac}, + {"opus", AudioCodec::kOpus}, + {"REMOTE_AUDIO", AudioCodec::kNotSpecified}}}; + +constexpr EnumNameTable kVideoCodecNames{ + {{"h264", VideoCodec::kH264}, + {"vp8", VideoCodec::kVp8}, + {"hevc", VideoCodec::kHevc}, + {"REMOTE_VIDEO", VideoCodec::kNotSpecified}, + {"vp9", VideoCodec::kVp9}, + {"av1", VideoCodec::kAv1}}}; + +} // namespace + +const char* CodecToString(AudioCodec codec) { + return GetEnumName(kAudioCodecNames, codec).value(); +} + +ErrorOr StringToAudioCodec(std::string_view name) { + return GetEnum(kAudioCodecNames, name); +} + +const char* CodecToString(VideoCodec codec) { + return GetEnumName(kVideoCodecNames, codec).value(); +} + +ErrorOr StringToVideoCodec(std::string_view name) { + return GetEnum(kVideoCodecNames, name); +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/message_fields.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/message_fields.h new file mode 100644 index 0000000..feba973 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/message_fields.h @@ -0,0 +1,59 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_MESSAGE_FIELDS_H_ +#define CAST_STREAMING_MESSAGE_FIELDS_H_ + +#include +#include + +#include "cast/streaming/public/constants.h" +#include "platform/base/error.h" + +namespace openscreen::cast { + +/// NOTE: Constants here are all taken from the Cast V2: Mirroring Control +/// Protocol specification. + +// Namespace for OFFER/ANSWER messages. +inline constexpr char kCastWebrtcNamespace[] = + "urn:x-cast:com.google.cast.webrtc"; +inline constexpr char kCastRemotingNamespace[] = + "urn:x-cast:com.google.cast.remoting"; + +// JSON message field values specific to the Sender Session. +inline constexpr char kMessageType[] = "type"; + +// List of OFFER message fields. +inline constexpr char kMessageTypeOffer[] = "OFFER"; +inline constexpr char kOfferMessageBody[] = "offer"; +inline constexpr char kSequenceNumber[] = "seqNum"; +inline constexpr char kCodecName[] = "codecName"; + +/// ANSWER message fields. +inline constexpr char kMessageTypeAnswer[] = "ANSWER"; +inline constexpr char kAnswerMessageBody[] = "answer"; +inline constexpr char kResult[] = "result"; +inline constexpr char kResultOk[] = "ok"; +inline constexpr char kResultError[] = "error"; +inline constexpr char kErrorMessageBody[] = "error"; +inline constexpr char kErrorCode[] = "code"; +inline constexpr char kErrorDescription[] = "description"; + +// Other message fields. +inline constexpr char kRpcMessageBody[] = "rpc"; +inline constexpr char kInputMessageBody[] = "input"; +inline constexpr char kCapabilitiesMessageBody[] = "capabilities"; +inline constexpr char kStatusMessageBody[] = "status"; + +// Conversion methods for codec message fields. +const char* CodecToString(AudioCodec codec); +ErrorOr StringToAudioCodec(std::string_view name); + +const char* CodecToString(VideoCodec codec); +ErrorOr StringToVideoCodec(std::string_view name); + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_MESSAGE_FIELDS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/answer_messages.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/answer_messages.cc new file mode 100644 index 0000000..25235be --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/answer_messages.cc @@ -0,0 +1,498 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/public/answer_messages.h" + +#include +#include + +#include "cast/streaming/public/constants.h" +#include "platform/base/error.h" +#include "util/enum_name_table.h" +#include "util/json/json_helpers.h" +#include "util/osp_logging.h" +#include "util/string_parse.h" +#include "util/string_util.h" +#include "util/stringprintf.h" + +namespace openscreen::cast { + +namespace { + +/// Constraint properties. +// Audio constraints. See properties below. +constexpr char kAudio[] = "audio"; +// Video constraints. See properties below. +constexpr char kVideo[] = "video"; + +// An optional field representing the minimum bits per second. If not specified +// by the receiver, the sender will use kDefaultAudioMinBitRate and +// kDefaultVideoMinBitRate, which represent the true operational minimum. +constexpr char kMinBitRate[] = "minBitRate"; + +// Maximum encoded bits per second. This is the lower of (1) the max capability +// of the decoder, or (2) the max data transfer rate. +constexpr char kMaxBitRate[] = "maxBitRate"; +// Maximum supported end-to-end latency, in milliseconds. Proportional to the +// size of the data buffers in the receiver. +constexpr char kMaxDelay[] = "maxDelay"; + +/// Video constraint properties. +// Maximum pixel rate (width * height * framerate). Is often less than +// multiplying the fields in maxDimensions. This field is used to set the +// maximum processing rate. +constexpr char kMaxPixelsPerSecond[] = "maxPixelsPerSecond"; +// Minimum dimensions. If omitted, the sender will assume a reasonable minimum +// with the same aspect ratio as maxDimensions, as close to 320*180 as possible. +// Should reflect the true operational minimum. +constexpr char kMinResolution[] = "minResolution"; +// Maximum dimensions, not necessarily ideal dimensions. +constexpr char kMaxDimensions[] = "maxDimensions"; + +/// Audio constraint properties. +// Maximum supported sampling frequency (not necessarily ideal). +constexpr char kMaxSampleRate[] = "maxSampleRate"; +// Maximum number of audio channels (1 is mono, 2 is stereo, etc.). +constexpr char kMaxChannels[] = "maxChannels"; + +/// Display description properties +// If this optional field is included in the ANSWER message, the receiver is +// attached to a fixed display that has the given dimensions and frame rate +// configuration. These may exceed, be the same, or be less than the values in +// constraints. If undefined, we assume the display is not fixed (e.g. a Google +// Hangouts UI panel). +constexpr char kDimensions[] = "dimensions"; +// An optional field. When missing and dimensions are specified, the sender +// will assume square pixels and the dimensions imply the aspect ratio of the +// fixed display. WHen present and dimensions are also specified, implies the +// pixels are not square. +constexpr char kAspectRatio[] = "aspectRatio"; +// The delimeter used for the aspect ratio format ("A:B"). +constexpr char kAspectRatioDelimiter = ':'; +// Sets the aspect ratio constraints. Value must be either "sender" or +// "receiver", see kScalingSender and kScalingReceiver below. +constexpr char kScaling[] = "scaling"; +// scaling = "sender" means that the sender must provide video frames of a fixed +// aspect ratio. In this case, the dimensions object must be passed or an error +// case will occur. +constexpr char kScalingSender[] = "sender"; +// scaling = "receiver" means that the sender may send arbitrarily sized frames, +// and the receiver will handle scaling and letterboxing as necessary. +constexpr char kScalingReceiver[] = "receiver"; + +/// Answer properties. +// A number specifying the UDP port used for all streams in this session. +// Must have a value between kUdpPortMin and kUdpPortMax. +constexpr char kUdpPort[] = "udpPort"; +constexpr int kUdpPortMin = 1; +constexpr int kUdpPortMax = 65535; +// Numbers specifying the indexes chosen from the offer message. +constexpr char kSendIndexes[] = "sendIndexes"; +// uint32_t values specifying the RTP SSRC values used to send the RTCP feedback +// of the stream indicated in kSendIndexes. +constexpr char kSsrcs[] = "ssrcs"; +// Provides detailed maximum and minimum capabilities of the receiver for +// processing the selected streams. The sender may alter video resolution and +// frame rate throughout the session, and the constraints here determine how +// much data volume is allowed. +constexpr char kConstraints[] = "constraints"; +// Provides details about the display on the receiver. +constexpr char kDisplay[] = "display"; +// std::optional array of numbers specifying the indexes of streams that will +// send event logs through RTCP. +constexpr char kReceiverRtcpEventLog[] = "receiverRtcpEventLog"; +// Optional array of numbers specifying the indexes of streams that will use +// DSCP values specified in the OFFER message for RTCP packets. +constexpr char kReceiverRtcpDscp[] = "receiverRtcpDscp"; +// If this optional field is present the receiver supports the specific +// RTP extensions (such as adaptive playout delay). +constexpr char kRtpExtensions[] = "rtpExtensions"; + +EnumNameTable kAspectRatioConstraintNames{ + {{kScalingReceiver, AspectRatioConstraint::kVariable}, + {kScalingSender, AspectRatioConstraint::kFixed}}}; + +Json::Value AspectRatioConstraintToJson(AspectRatioConstraint aspect_ratio) { + return Json::Value(GetEnumName(kAspectRatioConstraintNames, aspect_ratio) + .value(kScalingSender)); +} + +std::optional TryParseAspectRatioConstraint( + const Json::Value& value) { + std::string aspect_ratio; + if (!json::TryParseString(value, &aspect_ratio)) { + return std::nullopt; + } + + ErrorOr constraint = + GetEnum(kAspectRatioConstraintNames, aspect_ratio); + if (constraint.is_error()) { + return std::nullopt; + } + return constraint.value(); +} + +template +ErrorOr> ParseOptional(const Json::Value& value) { + if (!value) { + return std::optional{}; + } + auto out = T::TryParse(value); + if (out.is_error()) { + return out.error(); + } + return std::optional{std::move(out.value())}; +} + +} // namespace + +// static +ErrorOr AspectRatio::TryParse(const Json::Value& value) { + std::string parsed_value; + if (!json::TryParseString(value, &parsed_value)) { + return Error(Error::Code::kJsonParseError, "Invalid aspect ratio string"); + } + + std::vector fields = + string_util::Split(parsed_value, kAspectRatioDelimiter); + if (fields.size() != 2) { + return Error(Error::Code::kJsonParseError, "Invalid aspect ratio format"); + } + + AspectRatio out; + if (!string_parse::ParseAsciiNumber(fields[0], out.width) || + !string_parse::ParseAsciiNumber(fields[1], out.height)) { + return Error(Error::Code::kJsonParseError, "Invalid aspect ratio values"); + } + if (!out.IsValid()) { + return Error(Error::Code::kJsonParseError, "Invalid aspect ratio"); + } + return out; +} + +bool AspectRatio::IsValid() const { + return width > 0 && height > 0; +} + +// static +ErrorOr AudioConstraints::TryParse(const Json::Value& root) { + if (!root.isObject()) { + return Error(Error::Code::kJsonParseError, + "Audio constraints is not a JSON object"); + } + + AudioConstraints out; + if (!json::TryParseInt(root[kMaxSampleRate], &out.max_sample_rate) || + !json::TryParseInt(root[kMaxChannels], &out.max_channels) || + !json::TryParseInt(root[kMaxBitRate], &out.max_bit_rate)) { + return Error(Error::Code::kJsonParseError, "Invalid audio constraints"); + } + + std::chrono::milliseconds max_delay; + if (json::TryParseMilliseconds(root[kMaxDelay], &max_delay)) { + out.max_delay = max_delay; + } + + if (!json::TryParseInt(root[kMinBitRate], &out.min_bit_rate)) { + out.min_bit_rate = kDefaultAudioMinBitRate; + } + if (!out.IsValid()) { + return Error(Error::Code::kJsonParseError, "Invalid audio constraints"); + } + return out; +} + +Json::Value AudioConstraints::ToJson() const { + OSP_CHECK(IsValid()); + Json::Value root; + root[kMaxSampleRate] = max_sample_rate; + root[kMaxChannels] = max_channels; + root[kMinBitRate] = min_bit_rate; + root[kMaxBitRate] = max_bit_rate; + if (max_delay.has_value()) { + root[kMaxDelay] = Json::Value::Int64(max_delay->count()); + } + return root; +} + +bool AudioConstraints::IsValid() const { + return max_sample_rate > 0 && max_channels > 0 && min_bit_rate > 0 && + max_bit_rate >= min_bit_rate; +} + +// static +ErrorOr VideoConstraints::TryParse(const Json::Value& root) { + if (!root.isObject()) { + return Error(Error::Code::kJsonParseError, + "Video constraints is not a JSON object"); + } + + VideoConstraints out; + + auto max_dimensions = Dimensions::TryParse(root[kMaxDimensions]); + if (max_dimensions.is_error()) { + return max_dimensions.error(); + } + out.max_dimensions = std::move(max_dimensions.value()); + + if (!json::TryParseInt(root[kMaxBitRate], &out.max_bit_rate)) { + return Error(Error::Code::kJsonParseError, + "Invalid video constraints: missing maxBitRate"); + } + + auto min_resolution = ParseOptional(root[kMinResolution]); + if (min_resolution.is_error()) { + return min_resolution.error(); + } + out.min_resolution = std::move(min_resolution.value()); + + std::chrono::milliseconds max_delay; + if (json::TryParseMilliseconds(root[kMaxDelay], &max_delay)) { + out.max_delay = max_delay; + } + + double max_pixels_per_second; + if (json::TryParseDouble(root[kMaxPixelsPerSecond], &max_pixels_per_second)) { + out.max_pixels_per_second = max_pixels_per_second; + } + + if (!json::TryParseInt(root[kMinBitRate], &out.min_bit_rate)) { + out.min_bit_rate = kDefaultVideoMinBitRate; + } + if (!out.IsValid()) { + return Error(Error::Code::kJsonParseError, "Invalid video constraints"); + } + return out; +} + +bool VideoConstraints::IsValid() const { + return max_pixels_per_second > 0 && min_bit_rate > 0 && + max_bit_rate > min_bit_rate && + (!max_delay.has_value() || max_delay->count() > 0) && + max_dimensions.IsValid() && + (!min_resolution.has_value() || min_resolution->IsValid()) && + max_dimensions.frame_rate.numerator() > 0; +} + +Json::Value VideoConstraints::ToJson() const { + OSP_CHECK(IsValid()); + Json::Value root; + root[kMaxDimensions] = max_dimensions.ToJson(); + root[kMinBitRate] = min_bit_rate; + root[kMaxBitRate] = max_bit_rate; + if (max_pixels_per_second.has_value()) { + root[kMaxPixelsPerSecond] = max_pixels_per_second.value(); + } + + if (min_resolution.has_value()) { + root[kMinResolution] = min_resolution->ToJson(); + } + + if (max_delay.has_value()) { + root[kMaxDelay] = Json::Value::Int64(max_delay->count()); + } + return root; +} + +// static +ErrorOr Constraints::TryParse(const Json::Value& root) { + if (!root.isObject()) { + return Error(Error::Code::kJsonParseError, + "Constraints is not a JSON object"); + } + + Constraints out; + + auto audio = AudioConstraints::TryParse(root[kAudio]); + if (audio.is_error()) { + return audio.error(); + } + out.audio = std::move(audio.value()); + + auto video = VideoConstraints::TryParse(root[kVideo]); + if (video.is_error()) { + return video.error(); + } + out.video = std::move(video.value()); + + if (!out.IsValid()) { + return Error(Error::Code::kJsonParseError, "Invalid constraints"); + } + return out; +} + +bool Constraints::IsValid() const { + return audio.IsValid() && video.IsValid(); +} + +Json::Value Constraints::ToJson() const { + OSP_CHECK(IsValid()); + Json::Value root; + root[kAudio] = audio.ToJson(); + root[kVideo] = video.ToJson(); + return root; +} + +// static +ErrorOr DisplayDescription::TryParse( + const Json::Value& root) { + if (!root.isObject()) { + return Error(Error::Code::kJsonParseError, + "Display description is not a JSON object"); + } + + DisplayDescription out; + + auto dimensions = ParseOptional(root[kDimensions]); + if (dimensions.is_error()) { + return dimensions.error(); + } + out.dimensions = std::move(dimensions.value()); + + auto aspect_ratio = ParseOptional(root[kAspectRatio]); + if (aspect_ratio.is_error()) { + return aspect_ratio.error(); + } + out.aspect_ratio = std::move(aspect_ratio.value()); + + auto constraint = TryParseAspectRatioConstraint(root[kScaling]); + if (constraint.has_value()) { + out.aspect_ratio_constraint = constraint.value(); + } else { + out.aspect_ratio_constraint = std::nullopt; + } + + if (!out.IsValid()) { + return Error(Error::Code::kJsonParseError, "Invalid display description"); + } + return out; +} + +bool DisplayDescription::IsValid() const { + // At least one of the properties must be set, and if a property is set + // it must be valid. + if (aspect_ratio.has_value() && !aspect_ratio->IsValid()) { + return false; + } + + if (dimensions.has_value() && !dimensions->IsValid()) { + return false; + } + + // Sender behavior is undefined if the aspect ratio is fixed but no + // dimensions or aspect ratio are provided. + if (aspect_ratio_constraint.has_value() && + (aspect_ratio_constraint.value() == AspectRatioConstraint::kFixed) && + !dimensions.has_value() && !aspect_ratio.has_value()) { + return false; + } + return aspect_ratio.has_value() || dimensions.has_value() || + aspect_ratio_constraint.has_value(); +} + +Json::Value DisplayDescription::ToJson() const { + OSP_CHECK(IsValid()); + Json::Value root; + if (aspect_ratio.has_value()) { + root[kAspectRatio] = + StringFormat("{}{}{}", aspect_ratio->width, kAspectRatioDelimiter, + aspect_ratio->height); + } + if (dimensions.has_value()) { + root[kDimensions] = dimensions->ToJson(); + } + if (aspect_ratio_constraint.has_value()) { + root[kScaling] = + AspectRatioConstraintToJson(aspect_ratio_constraint.value()); + } + return root; +} + +ErrorOr Answer::TryParse(const Json::Value& root) { + if (!root.isObject()) { + return Error(Error::Code::kJsonParseError, "Answer is not a JSON object"); + } + + Answer out; + if (!json::TryParseInt(root[kUdpPort], &out.udp_port) || + !json::TryParseIntArray(root[kSendIndexes], &out.send_indexes) || + !json::TryParseUintArray(root[kSsrcs], &out.ssrcs)) { + return Error(Error::Code::kJsonParseError, + "Invalid answer: missing or invalid mandatory fields"); + } + + auto constraints = ParseOptional(root[kConstraints]); + if (constraints.is_error()) { + return constraints.error(); + } + out.constraints = std::move(constraints.value()); + + auto display = ParseOptional(root[kDisplay]); + if (display.is_error()) { + return display.error(); + } + out.display = std::move(display.value()); + + // These functions set to empty array if not present, so we can ignore + // the return value for optional values. + json::TryParseIntArray(root[kReceiverRtcpEventLog], + &out.receiver_rtcp_event_log); + json::TryParseIntArray(root[kReceiverRtcpDscp], &out.receiver_rtcp_dscp); + json::TryParseNestedStringArray(root[kRtpExtensions], &out.rtp_extensions); + + if (!out.IsValid()) { + return Error(Error::Code::kJsonParseError, "Invalid answer"); + } + return out; +} + +bool Answer::IsValid() const { + if (ssrcs.empty() || send_indexes.empty()) { + return false; + } + + // We don't know what the indexes used in the offer were here, so we just + // sanity check. + for (const int index : send_indexes) { + if (index < 0) { + return false; + } + } + if (constraints.has_value() && !constraints->IsValid()) { + return false; + } + if (display.has_value() && !display->IsValid()) { + return false; + } + return kUdpPortMin <= udp_port && udp_port <= kUdpPortMax; +} + +Json::Value Answer::ToJson() const { + OSP_CHECK(IsValid()); + Json::Value root; + if (constraints.has_value()) { + root[kConstraints] = constraints->ToJson(); + } + if (display.has_value()) { + root[kDisplay] = display->ToJson(); + } + root[kUdpPort] = udp_port; + root[kSendIndexes] = json::PrimitiveVectorToJson(send_indexes); + root[kSsrcs] = json::PrimitiveVectorToJson(ssrcs); + // Some sender do not handle empty array properly, so we omit these fields + // if they are empty. + if (!receiver_rtcp_event_log.empty()) { + root[kReceiverRtcpEventLog] = + json::PrimitiveVectorToJson(receiver_rtcp_event_log); + } + if (!receiver_rtcp_dscp.empty()) { + root[kReceiverRtcpDscp] = json::PrimitiveVectorToJson(receiver_rtcp_dscp); + } + if (!rtp_extensions.empty()) { + root[kRtpExtensions] = json::NestedStringArrayToJson(rtp_extensions); + } + return root; +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/answer_messages.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/answer_messages.h new file mode 100644 index 0000000..00949f6 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/answer_messages.h @@ -0,0 +1,122 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_PUBLIC_ANSWER_MESSAGES_H_ +#define CAST_STREAMING_PUBLIC_ANSWER_MESSAGES_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cast/streaming/resolution.h" +#include "cast/streaming/ssrc.h" +#include "json/value.h" +#include "platform/base/error.h" +#include "util/simple_fraction.h" + +namespace openscreen::cast { + +// For each of the below classes, though a number of methods are shared, the use +// of a shared base class has intentionally been avoided. This is to improve +// readability of the structs provided in this file by cutting down on the +// amount of obscuring boilerplate code. For each of the following struct +// definitions, the following method definitions are shared: +// (1) TryParse. Shall return a boolean indicating whether the out +// parameter is in a valid state after checking bounds and restrictions. +// (2) ToJson. Should return a proper JSON object. Assumes that IsValid() +// has been called already, OSP_CHECKs if not IsValid(). +// (3) IsValid. Used by both TryParse and ToJson to ensure that the +// object is in a good state. +struct AudioConstraints { + static ErrorOr TryParse(const Json::Value& value); + Json::Value ToJson() const; + bool IsValid() const; + + int max_sample_rate = 0; + int max_channels = 0; + int min_bit_rate = 0; // optional + int max_bit_rate = 0; + std::optional max_delay = {}; +}; + +struct VideoConstraints { + static ErrorOr TryParse(const Json::Value& value); + Json::Value ToJson() const; + bool IsValid() const; + + std::optional max_pixels_per_second = {}; + std::optional min_resolution = {}; + Dimensions max_dimensions = {}; + int min_bit_rate = 0; // optional + int max_bit_rate = 0; + std::optional max_delay = {}; +}; + +struct Constraints { + static ErrorOr TryParse(const Json::Value& value); + Json::Value ToJson() const; + bool IsValid() const; + + AudioConstraints audio; + VideoConstraints video; +}; + +// Decides whether the Sender scales and letterboxes content to 16:9, or if +// it may send video frames of any arbitrary size and the Receiver must +// handle the presentation details. +enum class AspectRatioConstraint : uint8_t { kVariable = 0, kFixed }; + +struct AspectRatio { + static ErrorOr TryParse(const Json::Value& value); + bool IsValid() const; + + bool operator==(const AspectRatio& other) const { + return width == other.width && height == other.height; + } + + int width = 0; + int height = 0; +}; + +struct DisplayDescription { + static ErrorOr TryParse(const Json::Value& value); + Json::Value ToJson() const; + bool IsValid() const; + + // May exceed, be the same, or less than those mentioned in the + // video constraints. + std::optional dimensions; + std::optional aspect_ratio = {}; + std::optional aspect_ratio_constraint = {}; +}; + +struct Answer { + static ErrorOr TryParse(const Json::Value& value); + Json::Value ToJson() const; + bool IsValid() const; + + int udp_port = 0; + std::vector send_indexes; + std::vector ssrcs; + + // Constraints and display descriptions are optional fields, and maybe null in + // the valid case. + std::optional constraints; + std::optional display; + std::vector receiver_rtcp_event_log; + std::vector receiver_rtcp_dscp; + + // RTP extensions should be empty, but not null. + std::vector> rtp_extensions = {}; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_PUBLIC_ANSWER_MESSAGES_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/capture_recommendations.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/capture_recommendations.cc new file mode 100644 index 0000000..fa0b6b6 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/capture_recommendations.cc @@ -0,0 +1,157 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/public/capture_recommendations.h" + +#include +#include + +#include "cast/streaming/public/answer_messages.h" +#include "util/osp_logging.h" + +namespace openscreen::cast { +namespace capture_recommendations { +namespace { + +void ApplyDisplay(const DisplayDescription& description, + Recommendations* recommendations) { + recommendations->video.supports_scaling = + (description.aspect_ratio_constraint && + (description.aspect_ratio_constraint.value() == + AspectRatioConstraint::kVariable)); + + // We should never exceed the display's resolution, since it will always + // force scaling. + if (description.dimensions) { + recommendations->video.maximum = description.dimensions.value(); + recommendations->video.bit_rate_limits.maximum = + recommendations->video.maximum.effective_bit_rate(); + + if (recommendations->video.maximum.width < + recommendations->video.minimum.width) { + recommendations->video.minimum = + recommendations->video.maximum.ToResolution(); + } + } + + // If the receiver gives us an aspect ratio that doesn't match the display + // resolution they give us, the behavior is undefined from the spec. + // Here we prioritize the aspect ratio, and the receiver can scale the frame + // as they wish. + double aspect_ratio = 0.0; + if (description.aspect_ratio) { + aspect_ratio = static_cast(description.aspect_ratio->width) / + description.aspect_ratio->height; + recommendations->video.maximum.width = + recommendations->video.maximum.height * aspect_ratio; + } else if (description.dimensions) { + aspect_ratio = static_cast(description.dimensions->width) / + description.dimensions->height; + } else { + return; + } + recommendations->video.minimum.width = + recommendations->video.minimum.height * aspect_ratio; +} + +void ApplyConstraints(const Constraints& constraints, + Recommendations* recommendations) { + // Audio has no fields in the display description, so we can safely + // ignore the current recommendations when setting values here. + if (constraints.audio.max_delay.has_value()) { + recommendations->audio.max_delay = constraints.audio.max_delay.value(); + } + recommendations->audio.max_channels = constraints.audio.max_channels; + recommendations->audio.max_sample_rate = constraints.audio.max_sample_rate; + + recommendations->audio.bit_rate_limits = BitRateLimits{ + std::max(constraints.audio.min_bit_rate, kDefaultAudioMinBitRate), + std::max(constraints.audio.max_bit_rate, kDefaultAudioMinBitRate)}; + + // With video, we take the intersection of values of the constraints and + // the display description. + if (constraints.video.max_delay.has_value()) { + recommendations->video.max_delay = constraints.video.max_delay.value(); + } + + if (constraints.video.max_pixels_per_second.has_value()) { + recommendations->video.max_pixels_per_second = + constraints.video.max_pixels_per_second.value(); + } + + recommendations->video.bit_rate_limits = + BitRateLimits{std::max(constraints.video.min_bit_rate, + recommendations->video.bit_rate_limits.minimum), + std::min(constraints.video.max_bit_rate, + recommendations->video.bit_rate_limits.maximum)}; + Dimensions dimensions = constraints.video.max_dimensions; + if (dimensions.width <= kDefaultMinResolution.width) { + recommendations->video.maximum = {kDefaultMinResolution.width, + kDefaultMinResolution.height, + kDefaultFrameRate}; + } else if (dimensions.width < recommendations->video.maximum.width) { + recommendations->video.maximum = std::move(dimensions); + } + + if (constraints.video.min_resolution) { + const Resolution& min = constraints.video.min_resolution->ToResolution(); + if (kDefaultMinResolution.width < min.width) { + recommendations->video.minimum = std::move(min); + } + } +} + +// The receiver's video constraints, even when each is individually valid, can +// intersect with the display description to produce an inverted range: a +// minimum bit rate above the display-limited maximum, or a minimum resolution +// larger than the display. (Audio cannot invert: AudioConstraints::IsValid() +// already requires max_bit_rate >= min_bit_rate.) Resolve any such +// contradiction in favor of the maximum, which reflects what the +// receiver/display can actually handle. +void ClampVideoToWellOrderedRanges(Video& video) { + video.bit_rate_limits.minimum = + std::min(video.bit_rate_limits.minimum, video.bit_rate_limits.maximum); + video.minimum.width = std::min(video.minimum.width, video.maximum.width); + video.minimum.height = + std::min(video.minimum.height, video.maximum.height); +} + +} // namespace + +bool BitRateLimits::operator==(const BitRateLimits& other) const { + return std::tie(minimum, maximum) == std::tie(other.minimum, other.maximum); +} + +bool Audio::operator==(const Audio& other) const { + return std::tie(bit_rate_limits, max_delay, max_channels, max_sample_rate) == + std::tie(other.bit_rate_limits, other.max_delay, other.max_channels, + other.max_sample_rate); +} + +bool Video::operator==(const Video& other) const { + return std::tie(bit_rate_limits, minimum, maximum, supports_scaling, + max_delay, max_pixels_per_second) == + std::tie(other.bit_rate_limits, other.minimum, other.maximum, + other.supports_scaling, other.max_delay, + other.max_pixels_per_second); +} + +bool Recommendations::operator==(const Recommendations& other) const { + return std::tie(audio, video) == std::tie(other.audio, other.video); +} + +Recommendations GetRecommendations(const Answer& answer) { + Recommendations recommendations; + if (answer.display.has_value() && answer.display->IsValid()) { + ApplyDisplay(answer.display.value(), &recommendations); + } + if (answer.constraints.has_value() && answer.constraints->IsValid()) { + ApplyConstraints(answer.constraints.value(), &recommendations); + } + ClampVideoToWellOrderedRanges(recommendations.video); + return recommendations; +} + +} // namespace capture_recommendations +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/capture_recommendations.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/capture_recommendations.h new file mode 100644 index 0000000..fabfa1d --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/capture_recommendations.h @@ -0,0 +1,151 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_PUBLIC_CAPTURE_RECOMMENDATIONS_H_ +#define CAST_STREAMING_PUBLIC_CAPTURE_RECOMMENDATIONS_H_ + +#include +#include +#include +#include + +#include "cast/streaming/public/constants.h" +#include "cast/streaming/resolution.h" +namespace openscreen::cast { + +struct Answer; + +// This namespace contains classes and functions to be used by senders for +// determining what constraints are recommended for the capture device, based on +// the limits reported by the receiver. +// +// A general note about recommendations: they are NOT maximum operational +// limits, instead they are targeted to provide a delightful cast experience. +// For example, if a receiver is connected to a 1080P display but cannot provide +// 1080P at a stable FPS with a good experience, 1080P will not be recommended. +namespace capture_recommendations { + +// Default maximum delay for both audio and video. Used if the sender fails +// to provide any constraints. +inline constexpr std::chrono::milliseconds kDefaultMaxDelayMs(400); + +// Bit rate limits, used for both audio and video streams. +struct BitRateLimits { + bool operator==(const BitRateLimits& other) const; + + // Minimum bit rate, in bits per second. + int minimum; + + // Maximum bit rate, in bits per second. + int maximum; +}; + +// The mirroring control protocol specifies 32kbps as the absolute minimum +// for audio. Depending on the type of audio content (narrowband, fullband, +// etc.) Opus specifically can perform very well at this bitrate. +// See: https://research.google/pubs/pub41650/ +inline constexpr int kDefaultAudioMinBitRate = 32 * 1000; + +// Opus generally sees little improvement above 192kbps, but some older codecs +// that we may consider supporting improve at up to 256kbps. +inline constexpr int kDefaultAudioMaxBitRate = 256 * 1000; +inline constexpr BitRateLimits kDefaultAudioBitRateLimits{ + kDefaultAudioMinBitRate, kDefaultAudioMaxBitRate}; + +// While generally audio should be captured at the maximum sample rate, 16kHz is +// the recommended absolute minimum. +inline constexpr int kDefaultAudioMinSampleRate = 16000; + +// Audio capture recommendations. Maximum delay is determined by buffer +// constraints, and capture bit rate may vary between limits as appropriate. +struct Audio { + bool operator==(const Audio& other) const; + + // Represents the recommended bit rate range. + BitRateLimits bit_rate_limits = kDefaultAudioBitRateLimits; + + // Represents the maximum audio delay, in milliseconds. + std::chrono::milliseconds max_delay = kDefaultMaxDelayMs; + + // Represents the maximum number of audio channels. + int max_channels = kDefaultAudioChannels; + + // Represents the maximum samples per second. + int max_sample_rate = kDefaultAudioSampleRate; + + // Represents the absolute minimum samples per second. Generally speaking, + // audio should be captured at the maximum samples per second rate. + int min_sample_rate = kDefaultAudioMinSampleRate; +}; + +// The minimum dimensions are as close as possible to low-definition +// television, factoring in the receiver's aspect ratio if provided. +inline constexpr Resolution kDefaultMinResolution{kMinVideoWidth, + kMinVideoHeight}; + +// Currently mirroring only supports 1080P. +inline constexpr Dimensions kDefaultMaxResolution{1920, 1080, + kDefaultFrameRate}; + +// The mirroring spec suggests 300kbps as the absolute minimum bitrate. +inline constexpr int kDefaultVideoMinBitRate = 300 * 1000; + +// The theoretical maximum pixels per second is the maximum bit rate +// divided by 8 (the max byte rate). In practice it should generally be +// less. +inline constexpr int kDefaultVideoMaxPixelsPerSecond = + kDefaultMaxResolution.effective_bit_rate() / 8; + +// Our default limits are merely the product of the minimum and maximum +// dimensions, and are only used if the receiver fails to give better +// constraint information. +inline constexpr BitRateLimits kDefaultVideoBitRateLimits{ + kDefaultVideoMinBitRate, kDefaultMaxResolution.effective_bit_rate()}; + +// Video capture recommendations. +struct Video { + bool operator==(const Video& other) const; + + // Represents the recommended bit rate range. + BitRateLimits bit_rate_limits = kDefaultVideoBitRateLimits; + + // Represents the recommended minimum resolution. + Resolution minimum = kDefaultMinResolution; + + // Represents the recommended maximum resolution. + Dimensions maximum = kDefaultMaxResolution; + + // Indicates whether the receiver can scale frames from a different aspect + // ratio, or if it needs to be done by the sender. Default is false, meaning + // that the sender is responsible for letterboxing. + bool supports_scaling = false; + + // Represents the maximum video delay, in milliseconds. + std::chrono::milliseconds max_delay = kDefaultMaxDelayMs; + + // Represents the maximum pixels per second, not necessarily correlated + // to bit rate. + int max_pixels_per_second = kDefaultVideoMaxPixelsPerSecond; +}; + +// Outputted recommendations for usage by capture devices. Note that we always +// return both audio and video (it is up to the sender to determine what +// streams actually get created). If the receiver doesn't give us any +// information for making recommendations, the defaults are used. +struct Recommendations { + bool operator==(const Recommendations& other) const; + + // Audio specific recommendations. + Audio audio; + + // Video specific recommendations. + Video video; +}; + +Recommendations GetRecommendations(const Answer& answer); + +} // namespace capture_recommendations +} // namespace openscreen::cast + +#endif // CAST_STREAMING_PUBLIC_CAPTURE_RECOMMENDATIONS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/constants.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/constants.cc new file mode 100644 index 0000000..a74eb38 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/constants.cc @@ -0,0 +1,57 @@ +// Copyright 2024 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/public/constants.h" + +#include + +#include "util/osp_logging.h" + +namespace openscreen::cast { + +std::ostream& operator<<(std::ostream& os, VideoCodec codec) { + const char* str = nullptr; + switch (codec) { + case VideoCodec::kH264: + str = "H264"; + break; + case VideoCodec::kVp8: + str = "VP8"; + break; + case VideoCodec::kHevc: + str = "HEVC"; + break; + case VideoCodec::kNotSpecified: + str = "NotSpecified"; + break; + case VideoCodec::kVp9: + str = "VP9"; + break; + case VideoCodec::kAv1: + str = "AV1"; + break; + default: + OSP_NOTREACHED(); + } + os << str; + return os; +} + +std::ostream& operator<<(std::ostream& os, CastMode mode) { + const char* str = nullptr; + switch (mode) { + case CastMode::kMirroring: + str = "mirroring"; + break; + case CastMode::kRemoting: + str = "remoting"; + break; + default: + OSP_NOTREACHED(); + } + os << str; + return os; +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/constants.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/constants.h new file mode 100644 index 0000000..f4e3736 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/constants.h @@ -0,0 +1,122 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_PUBLIC_CONSTANTS_H_ +#define CAST_STREAMING_PUBLIC_CONSTANTS_H_ + +//////////////////////////////////////////////////////////////////////////////// +// NOTE: This file should only contain constants that are reasonably globally +// used (i.e., by many modules, and in all or nearly all subdirs). Do NOT add +// non-POD constants, functions, interfaces, or any logic to this module, +// except for std::ostream operators on an as-needed basis. +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include + +namespace openscreen::cast { + +// Default target playout delay. The playout delay is the window of time between +// capture from the source until presentation at the receiver. +inline constexpr std::chrono::milliseconds kDefaultTargetPlayoutDelay(400); + +// Default UDP port, bound at the Receiver, for Cast Streaming. An +// implementation is required to use the port specified by the Receiver in its +// ANSWER control message, which may or may not match this port number here. +inline constexpr int kDefaultCastStreamingPort = 2344; + +// Default TCP port, bound at the TLS server socket level, for Cast Streaming. +// An implementation must use the port specified in the DNS-SD published record +// for connecting over TLS, which may or may not match this port number here. +inline constexpr int kDefaultCastPort = 8010; + +// Target number of milliseconds between the sending of RTCP reports. Both +// senders and receivers regularly send RTCP reports to their peer. +inline constexpr std::chrono::milliseconds kRtcpReportInterval(500); + +// This is an important system-wide constant. This limits how much history +// the implementation must retain in order to process the acknowledgements of +// past frames. +// +// This value is carefully choosen such that it fits in the 8-bits range for +// frame IDs. It is also less than half of the full 8-bits range such that +// logic can handle wrap around and compare two frame IDs meaningfully. +inline constexpr int kMaxUnackedFrames = 120; + +// The network must support a packet size of at least this many bytes. +inline constexpr int kRequiredNetworkPacketSize = 256; + +// The spec declares RTP timestamps must always have a timebase of 90000 ticks +// per second for video. +inline constexpr int kRtpVideoTimebase = 90000; + +// Minimum resolution is 320x240. +inline constexpr int kMinVideoHeight = 240; +inline constexpr int kMinVideoWidth = 320; + +// The default frame rate for capture options is 30FPS. +inline constexpr int kDefaultFrameRate = 30; + +// The mirroring spec suggests 300kbps as the absolute minimum bitrate. +inline constexpr int kDefaultVideoMinBitRate = 300 * 1000; + +// Default video max bitrate is based on 1080P @ 30FPS, which can be played back +// at good quality around 10mbps. +inline constexpr int kDefaultVideoMaxBitRate = 10 * 1000 * 1000; + +// The mirroring control protocol specifies 32kbps as the absolute minimum +// for audio. Depending on the type of audio content (narrowband, fullband, +// etc.) Opus specifically can perform very well at this bitrate. +// See: https://research.google/pubs/pub41650/ +inline constexpr int kDefaultAudioMinBitRate = 32 * 1000; + +// Opus generally sees little improvement above 192kbps, but some older codecs +// that we may consider supporting improve at up to 256kbps. +inline constexpr int kDefaultAudioMaxBitRate = 256 * 1000; + +// While generally audio should be captured at the maximum sample rate, 16kHz is +// the recommended absolute minimum. +inline constexpr int kDefaultAudioMinSampleRate = 16000; + +// The default audio sample rate is 48kHz, slightly higher than standard +// consumer audio. +inline constexpr int kDefaultAudioSampleRate = 48000; + +// The default audio number of channels is set to stereo. +inline constexpr int kDefaultAudioChannels = 2; + +// Default maximum delay for both audio and video. Used if the sender fails +// to provide any constraints. +inline constexpr std::chrono::milliseconds kDefaultMaxDelayMs(1500); + +// TODO(issuetracker.google.com/184189100): As part of updating remoting +// OFFER/ANSWER and capabilities exchange, remoting version should be updated +// to 3. +inline constexpr int kSupportedRemotingVersion = 2; + +// Used for RTCP message support. +constexpr uint32_t kCastName = ('C' << 24) + ('A' << 16) + ('S' << 8) + 'T'; + +// Codecs known and understood by cast senders and receivers. Note: receivers +// are required to implement the following codecs to be Cast V2 compliant: H264, +// VP8, AAC, Opus. Senders have to implement at least one codec from this +// list for audio or video to start a session. +// `kNotSpecified` is used in remoting to indicate that the stream is being +// remoted and is not specified as part of the OFFER message (indicated as +// "REMOTE_AUDIO" or "REMOTE_VIDEO"). +enum class AudioCodec { kAac, kOpus, kNotSpecified }; + +enum class VideoCodec { kH264, kVp8, kHevc, kNotSpecified, kVp9, kAv1 }; +std::ostream& operator<<(std::ostream& os, VideoCodec codec); + +// The type (audio, video, or unknown) of the stream. +enum class StreamType { kUnknown, kAudio, kVideo }; + +enum class CastMode : uint8_t { kMirroring, kRemoting }; +std::ostream& operator<<(std::ostream& os, CastMode mode); + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_PUBLIC_CONSTANTS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/encoded_frame.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/encoded_frame.cc new file mode 100644 index 0000000..68ec881 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/encoded_frame.cc @@ -0,0 +1,60 @@ +// Copyright 2014 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/public/encoded_frame.h" + +namespace openscreen::cast { + +EncodedFrame::EncodedFrame(Dependency dependency, + FrameId frame_id, + FrameId referenced_frame_id, + RtpTimeTicks rtp_timestamp, + Clock::time_point reference_time, + std::chrono::milliseconds new_playout_delay, + Clock::time_point capture_begin_time, + Clock::time_point capture_end_time, + ByteView data) + : dependency(dependency), + frame_id(frame_id), + referenced_frame_id(referenced_frame_id), + rtp_timestamp(rtp_timestamp), + reference_time(reference_time), + new_playout_delay(new_playout_delay), + capture_begin_time(capture_begin_time), + capture_end_time(capture_end_time), + data(data) {} + +EncodedFrame::EncodedFrame(Dependency dependency, + FrameId frame_id, + FrameId referenced_frame_id, + RtpTimeTicks rtp_timestamp, + Clock::time_point reference_time, + std::chrono::milliseconds new_playout_delay, + ByteView data) + : dependency(dependency), + frame_id(frame_id), + referenced_frame_id(referenced_frame_id), + rtp_timestamp(rtp_timestamp), + reference_time(reference_time), + new_playout_delay(new_playout_delay), + data(data) {} + +EncodedFrame::EncodedFrame() = default; +EncodedFrame::~EncodedFrame() = default; + +EncodedFrame::EncodedFrame(EncodedFrame&&) noexcept = default; +EncodedFrame& EncodedFrame::operator=(EncodedFrame&&) = default; + +void EncodedFrame::CopyMetadataTo(EncodedFrame* dest) const { + dest->dependency = this->dependency; + dest->frame_id = this->frame_id; + dest->referenced_frame_id = this->referenced_frame_id; + dest->rtp_timestamp = this->rtp_timestamp; + dest->reference_time = this->reference_time; + dest->new_playout_delay = this->new_playout_delay; + dest->capture_begin_time = this->capture_begin_time; + dest->capture_end_time = this->capture_end_time; +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/encoded_frame.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/encoded_frame.h new file mode 100644 index 0000000..c38e471 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/encoded_frame.h @@ -0,0 +1,119 @@ +// Copyright 2014 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_PUBLIC_ENCODED_FRAME_H_ +#define CAST_STREAMING_PUBLIC_ENCODED_FRAME_H_ + +#include + +#include +#include + +#include "cast/streaming/public/frame_id.h" +#include "cast/streaming/rtp_time.h" +#include "platform/api/time.h" +#include "platform/base/span.h" + +namespace openscreen::cast { + +// A combination of metadata and data for one encoded frame. This can contain +// audio data or video data or other. +struct EncodedFrame { + enum class Dependency : int8_t { + // "null" value, used to indicate whether `dependency` has been set. + kUnknown, + + // Not decodable without the reference frame indicated by + // `referenced_frame_id`. + kDependent, + + // Independently decodable. + kIndependent, + + // Independently decodable, and no future frames will depend on any frames + // before this one. + kKeyFrame, + }; + + EncodedFrame(Dependency dependency, + FrameId frame_id, + FrameId referenced_frame_id, + RtpTimeTicks rtp_timestamp, + Clock::time_point reference_time, + std::chrono::milliseconds new_playout_delay, + Clock::time_point capture_begin_time, + Clock::time_point capture_end_time, + ByteView data); + + // TODO(issuetracker.google.com/285905175): remove remaining optional fields + // (new_playout_delay) once Chrome provides the capture begin and end + // timestamps, so this constructor only provides the required fields. + EncodedFrame(Dependency dependency, + FrameId frame_id, + FrameId referenced_frame_id, + RtpTimeTicks rtp_timestamp, + Clock::time_point reference_time, + std::chrono::milliseconds new_playout_delay, + ByteView data); + EncodedFrame(); + EncodedFrame(const EncodedFrame&) = delete; + EncodedFrame& operator=(const EncodedFrame&) = delete; + EncodedFrame(EncodedFrame&&) noexcept; + EncodedFrame& operator=(EncodedFrame&&); + ~EncodedFrame(); + + // Copies all members except `data` to `dest`. Does not modify |dest->data|. + void CopyMetadataTo(EncodedFrame* dest) const; + + // This frame's dependency relationship with respect to other frames. + Dependency dependency = Dependency::kUnknown; + + // The label associated with this frame. Implies an ordering relative to + // other frames in the same stream. + FrameId frame_id; + + // The label associated with the frame upon which this frame depends. If + // this frame does not require any other frame in order to become decodable + // (e.g., key frames), `referenced_frame_id` must equal `frame_id`. + FrameId referenced_frame_id; + + // The stream timestamp, on the timeline of the signal data. For example, RTP + // timestamps for audio are usually defined as the total number of audio + // samples encoded in all prior frames. A playback system uses this value to + // detect gaps in the stream, and otherwise stretch the signal to gradually + // re-align towards playout targets when too much drift has occurred (see + // `reference_time`, below). + RtpTimeTicks rtp_timestamp; + + // The common reference clock timestamp for this frame. Over a sequence of + // frames, this time value is expected to drift with respect to the elapsed + // time implied by the RTP timestamps; and this may not necessarily increment + // with precise regularity. + // + // This value originates from a sender, and is the time at which the frame was + // captured/recorded. In the receiver context, this value is the computed + // target playout time, which is used for guiding the timing of presentation + // (see `rtp_timestamp`, above). It is also meant to be used to synchronize + // the presentation of multiple streams (e.g., audio and video), commonly + // known as "lip-sync." It is NOT meant to be a mandatory/exact playout time. + Clock::time_point reference_time; + + // Playout delay for this and all future frames. Used by the Adaptive + // Playout delay extension. Non-positive values means no change. + std::chrono::milliseconds new_playout_delay{}; + + // Video capture begin/end timestamps. If set to a value other than + // Clock::time_point::min(), used for improved statistics gathering. + Clock::time_point capture_begin_time = Clock::time_point::min(); + Clock::time_point capture_end_time = Clock::time_point::min(); + + // A buffer containing the encoded signal data for the frame. In the sender + // context, this points to the data to be sent. In the receiver context, this + // is set to the region of a client-provided buffer that was populated. + ByteView data; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_PUBLIC_ENCODED_FRAME_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/environment.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/environment.cc new file mode 100644 index 0000000..5299007 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/environment.cc @@ -0,0 +1,171 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/public/environment.h" + +#include +#include + +#include "cast/streaming/impl/rtp_defines.h" +#include "platform/api/task_runner.h" +#include "platform/base/span.h" +#include "util/osp_logging.h" + +namespace openscreen::cast { + +Environment::PacketConsumer::~PacketConsumer() = default; + +Environment::SocketSubscriber::~SocketSubscriber() = default; + +Environment::Environment(ClockNowFunctionPtr now_function, + TaskRunner& task_runner, + const IPEndpoint& local_endpoint) + : now_function_(now_function), task_runner_(task_runner) { + OSP_CHECK(now_function_); + ErrorOr> result = + UdpSocket::Create(*task_runner_, this, local_endpoint); + if (result.is_error()) { + OSP_LOG_ERROR << "Unable to create a UDP socket bound to " << local_endpoint + << ": " << result.error(); + return; + } + const_cast&>(socket_) = std::move(result.value()); + OSP_CHECK(socket_); + socket_->Bind(); +} + +Environment::~Environment() = default; + +IPEndpoint Environment::GetBoundLocalEndpoint() const { + if (socket_) { + return socket_->GetLocalEndpoint(); + } + return IPEndpoint{}; +} + +void Environment::SetSocketStateForTesting(SocketState state) { + state_ = state; + if (socket_subscriber_) { + switch (state_) { + case SocketState::kReady: + socket_subscriber_->OnSocketReady(); + break; + case SocketState::kInvalid: + socket_subscriber_->OnSocketInvalid(Error::Code::kSocketFailure); + break; + default: + break; + } + } +} + +void Environment::SetSocketSubscriber(SocketSubscriber* subscriber) { + socket_subscriber_ = subscriber; +} + +void Environment::SetStatisticsCollector(StatisticsCollector* collector) { + statistics_collector_ = collector; +} + +void Environment::ConsumeIncomingPackets(PacketConsumer* packet_consumer) { + OSP_CHECK(packet_consumer); + OSP_CHECK(!packet_consumer_); + packet_consumer_ = packet_consumer; +} + +void Environment::DropIncomingPackets() { + packet_consumer_ = nullptr; +} + +int Environment::GetMaxPacketSize() const { + // Return hard-coded values for UDP over wired Ethernet (which is a smaller + // MTU than typical defaults for UDP over 802.11 wireless). Performance would + // be more-optimized if the network were probed for the actual value. See + // discussion in rtp_defines.h. + switch (remote_endpoint_.address.version()) { + case IPAddress::Version::kV4: + return kMaxRtpPacketSizeForIpv4UdpOnEthernet; + case IPAddress::Version::kV6: + return kMaxRtpPacketSizeForIpv6UdpOnEthernet; + default: + OSP_NOTREACHED(); + } +} + +void Environment::SetDscp(UdpSocket::DscpMode mode) { + if (socket_) { + socket_->SetDscp(mode); + } +} + +void Environment::SendPacket(ByteView packet, PacketMetadata metadata) { + OSP_CHECK(remote_endpoint_.address); + OSP_CHECK_NE(remote_endpoint_.port, 0); + if (socket_) { + socket_->SendMessage(packet, remote_endpoint_); + } + if (statistics_collector_) { + statistics_collector_->CollectPacketSentEvent(packet, metadata); + } +} + +void Environment::OnBound(UdpSocket* socket) { + OSP_CHECK_EQ(socket, socket_.get()); + state_ = SocketState::kReady; + + if (socket_subscriber_) { + socket_subscriber_->OnSocketReady(); + } +} + +void Environment::OnError(UdpSocket* socket, const Error& error) { + OSP_CHECK_EQ(socket, socket_.get()); + // Usually OnError() is only called for non-recoverable Errors. However, + // OnSendError() and OnRead() delegate to this method, to handle their hard + // error cases as well. So, return early here if `error` is recoverable. + if (error.ok() || error.code() == Error::Code::kAgain) { + return; + } + + state_ = SocketState::kInvalid; + if (socket_subscriber_) { + socket_subscriber_->OnSocketInvalid(error); + } else { + // Default behavior when there are no subscribers. + OSP_LOG_ERROR << "For UDP socket bound to " << socket_->GetLocalEndpoint() + << ": " << error; + } +} + +void Environment::OnSendError(UdpSocket* socket, const Error& error) { + OnError(socket, error); +} + +void Environment::OnRead(UdpSocket* socket, + ErrorOr packet_or_error) { + if (!packet_consumer_) { + return; + } + + if (packet_or_error.is_error()) { + OnError(socket, packet_or_error.error()); + return; + } + + // Ideally, the arrival time would come from the operating system's network + // stack (e.g., by using the SO_TIMESTAMP sockopt on POSIX systems). However, + // there would still be the problem of mapping the timestamp to a value in + // terms of Clock::time_point. So, just sample the Clock here and call that + // the "arrival time." While this can add variance within the system, it + // should be minimal, assuming not too much time has elapsed between the + // actual packet receive event and the when this code here is executing. + const Clock::time_point arrival_time = now_function_(); + + UdpPacket packet = std::move(packet_or_error.value()); + packet_consumer_->OnReceivedPacket( + packet.source(), arrival_time, + std::move(static_cast&>(packet))); +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/environment.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/environment.h new file mode 100644 index 0000000..331fc8a --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/environment.h @@ -0,0 +1,164 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_PUBLIC_ENVIRONMENT_H_ +#define CAST_STREAMING_PUBLIC_ENVIRONMENT_H_ + +#include + +#include +#include +#include + +#include "cast/streaming/impl/statistics_collector.h" +#include "platform/api/time.h" +#include "platform/api/udp_socket.h" +#include "platform/base/ip_address.h" +#include "platform/base/span.h" +#include "util/raw_ptr.h" +#include "util/raw_ref.h" + +namespace openscreen::cast { + +// Provides the common environment for operating system resources shared by +// multiple components. +class Environment : public UdpSocket::Client { + public: + class PacketConsumer { + public: + virtual void OnReceivedPacket(const IPEndpoint& source, + Clock::time_point arrival_time, + std::vector packet) = 0; + + protected: + virtual ~PacketConsumer(); + }; + + // Consumers of the environment's UDP socket should be careful to check the + // socket's state before accessing its methods, especially + // GetBoundLocalEndpoint(). If the environment is `kStarting`, the + // local endpoint may not be set yet and will be zero initialized. + enum class SocketState { + // Socket is still initializing. Usually the UDP socket bind is + // the last piece. + kStarting, + + // The socket is ready for use and has been bound. + kReady, + + // The socket is either closed (normally or due to an error) or in an + // invalid state. Currently the environment does not create a new socket + // in this case, so to be used again the environment itself needs to be + // recreated. + kInvalid + }; + + // Classes concerned with the Environment's UDP socket state may inherit from + // `Subscriber` and then `Subscribe`. + class SocketSubscriber { + public: + // Event that occurs when the environment is ready for use. + virtual void OnSocketReady() = 0; + + // Event that occurs when the environment has experienced a fatal error. + virtual void OnSocketInvalid(const Error& error) = 0; + + protected: + virtual ~SocketSubscriber(); + }; + + // Construct with the given clock source and TaskRunner. Creates and + // internally-owns a UdpSocket, and immediately binds it to the given + // `local_endpoint`. Default behavior if `local_endpoint` is omitted is to + // bind to all available interfaces using IPv4. + Environment(ClockNowFunctionPtr now_function, + TaskRunner& task_runner, + const IPEndpoint& local_endpoint = IPEndpoint::kAnyV4()); + + ~Environment() override; + + ClockNowFunctionPtr now_function() const { return now_function_; } + Clock::time_point now() const { return now_function_(); } + TaskRunner& task_runner() const { return *task_runner_; } + + // Returns the local endpoint the socket is bound to, or the zero IPEndpoint + // if socket creation/binding failed. + // + // Note: This method is virtual to allow unit tests to fake that there really + // is a bound socket. + virtual IPEndpoint GetBoundLocalEndpoint() const; + + // Get/Set the remote endpoint. This is separate from the constructor because + // the remote endpoint is, in some cases, discovered only after receiving a + // packet. + const IPEndpoint& remote_endpoint() const { return remote_endpoint_; } + void set_remote_endpoint(const IPEndpoint& endpoint) { + remote_endpoint_ = endpoint; + } + + SocketState socket_state() const { return state_; } + void SetSocketStateForTesting(SocketState state); + + // Subscribe to socket changes. Callers can unsubscribe by passing + // nullptr. + void SetSocketSubscriber(SocketSubscriber* subscriber); + + // Subscribe to frame and packet events. Callers can unsubscribe by passing + // nullptr. Note that if the collector is destroyed before the environment, + // callers MUST unsubscribe to avoid an access exception. + void SetStatisticsCollector(StatisticsCollector* subscriber); + StatisticsCollector* statistics_collector() { + return statistics_collector_.get(); + } + + // Start/Resume delivery of incoming packets to the given `packet_consumer`. + // Delivery will continue until DropIncomingPackets() is called. + void ConsumeIncomingPackets(PacketConsumer* packet_consumer); + + // Stop delivery of incoming packets, dropping any that do come in. All + // internal references to the PacketConsumer that was provided in the last + // call to ConsumeIncomingPackets() are cleared. + void DropIncomingPackets(); + + // Returns the maximum packet size for the network. This will always return a + // value of at least kRequiredNetworkPacketSize. + int GetMaxPacketSize() const; + + // Sets the DSCP value for the underlying UDP socket. + void SetDscp(UdpSocket::DscpMode mode); + + // Sends the given `packet` to the remote endpoint, best-effort. + // set_remote_endpoint() must be called beforehand with a valid IPEndpoint. + // + // Note: This method is virtual to allow unit tests to intercept packets + // before they actually head-out through the socket. + virtual void SendPacket(ByteView packet, PacketMetadata metadata); + + private: + // UdpSocket::Client implementation. + void OnBound(UdpSocket* socket) final; + void OnError(UdpSocket* socket, const Error& error) final; + void OnSendError(UdpSocket* socket, const Error& error) final; + void OnRead(UdpSocket* socket, ErrorOr packet_or_error) final; + + ClockNowFunctionPtr now_function_; + const raw_ref task_runner_; + + // The UDP socket bound to the local endpoint that was passed into the + // constructor, or null if socket creation failed. + const std::unique_ptr socket_; + + // These are externally set/cleared. Behaviors are described in getter/setter + // method comments above. + IPEndpoint local_endpoint_{}; + IPEndpoint remote_endpoint_{}; + raw_ptr packet_consumer_ = nullptr; + SocketState state_ = SocketState::kStarting; + raw_ptr socket_subscriber_ = nullptr; + raw_ptr statistics_collector_ = nullptr; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_PUBLIC_ENVIRONMENT_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/frame_id.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/frame_id.cc new file mode 100644 index 0000000..6a4ac5c --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/frame_id.cc @@ -0,0 +1,20 @@ +// Copyright 2016 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/public/frame_id.h" + +namespace openscreen::cast { + +std::ostream& operator<<(std::ostream& out, const FrameId rhs) { + return out << rhs.ToString(); +} + +std::string FrameId::ToString() const { + if (is_null()) + return "F"; + + return "F" + std::to_string(value()); +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/frame_id.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/frame_id.h new file mode 100644 index 0000000..f17a0ad --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/frame_id.h @@ -0,0 +1,121 @@ +// Copyright 2016 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_PUBLIC_FRAME_ID_H_ +#define CAST_STREAMING_PUBLIC_FRAME_ID_H_ + +#include + +#include +#include +#include + +#include "cast/streaming/impl/expanded_value_base.h" + +namespace openscreen::cast { + +// Forward declaration (see below). +class FrameId; + +// Convenience operator overloads for logging. +std::ostream& operator<<(std::ostream& out, const FrameId rhs); + +// Unique identifier for a frame in a RTP media stream. FrameIds are truncated +// to 8-bit values in RTP and RTCP headers, and then expanded back by the other +// endpoint when parsing the headers. +// +// Usage example: +// +// // Distance/offset math. +// FrameId first = FrameId::first(); +// FrameId second = first + 1; +// FrameId third = second + 1; +// int64_t offset = third - first; +// FrameId fourth = second + offset; +// +// // Logging convenience. +// OSP_DLOG_INFO << "The current frame is " << fourth; +class FrameId : public ExpandedValueBase { + public: + // The "null" FrameId constructor. Represents a FrameId field that has not + // been set and/or a "not applicable" indicator. + constexpr FrameId() : FrameId(std::numeric_limits::min()) {} + + constexpr explicit FrameId(int64_t value) : ExpandedValueBase(value) {} + + // Allow copy construction and assignment. + constexpr FrameId(const FrameId&) = default; + constexpr FrameId& operator=(const FrameId&) = default; + + // Returns true if this is the special value representing null. + constexpr bool is_null() const { return *this == FrameId(); } + + // Distance operator. + int64_t operator-(FrameId rhs) const { + OSP_CHECK(!is_null()); + OSP_CHECK(!rhs.is_null()); + return value_ - rhs.value_; + } + + // Operators to compute advancement by incremental amounts. + constexpr FrameId operator+(int64_t rhs) const { + OSP_CHECK(!is_null()); + return FrameId(value_ + rhs); + } + constexpr FrameId operator-(int64_t rhs) const { + OSP_CHECK(!is_null()); + return FrameId(value_ - rhs); + } + constexpr FrameId& operator+=(int64_t rhs) { + OSP_CHECK(!is_null()); + return (*this = (*this + rhs)); + } + constexpr FrameId& operator-=(int64_t rhs) { + OSP_CHECK(!is_null()); + return (*this = (*this - rhs)); + } + constexpr FrameId& operator++() { + OSP_CHECK(!is_null()); + ++value_; + return *this; + } + constexpr FrameId& operator--() { + OSP_CHECK(!is_null()); + --value_; + return *this; + } + constexpr FrameId operator++(int) { + OSP_CHECK(!is_null()); + return FrameId(value_++); + } + constexpr FrameId operator--(int) { + OSP_CHECK(!is_null()); + return FrameId(value_--); + } + + // The identifier for the first frame in a stream. + static constexpr FrameId first() { return FrameId(0); } + + // A virtual identifier, representing the frame before the first. There should + // never actually be a frame streamed with this identifier. Instead, this is + // used in various components to represent a "not yet seen/processed the first + // frame" state. + // + // The name "leader" comes from the terminology used in tape reels, which + // refers to the non-data-carrying segment of tape before the recording + // begins. + static constexpr FrameId leader() { return FrameId(-1); } + + constexpr int64_t value() const { return value_; } + + std::string ToString() const; + + private: + friend class ExpandedValueBase; + friend std::ostream& operator<<(std::ostream& out, const FrameId rhs); +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_PUBLIC_FRAME_ID_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/offer_messages.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/offer_messages.cc new file mode 100644 index 0000000..0efe025 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/offer_messages.cc @@ -0,0 +1,487 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/public/offer_messages.h" + +#include + +#include +#include +#include +#include +#include +#include + +#include "cast/streaming/public/constants.h" +#include "platform/base/error.h" +#include "util/big_endian.h" +#include "util/enum_name_table.h" +#include "util/json/json_helpers.h" +#include "util/json/json_serialization.h" +#include "util/osp_logging.h" +#include "util/string_util.h" +#include "util/stringprintf.h" + +namespace openscreen::cast { + +namespace { + +constexpr char kSupportedStreams[] = "supportedStreams"; +constexpr char kAudioSourceType[] = "audio_source"; +constexpr char kVideoSourceType[] = "video_source"; +constexpr char kStreamType[] = "type"; + +[[nodiscard]] constexpr bool CodecParameterIsValid(VideoCodec codec, + std::string_view parameter) { + if (parameter.empty()) { + return true; + } + switch (codec) { + using enum VideoCodec; + case kVp8: + return parameter.starts_with("vp08"); + case kVp9: + return parameter.starts_with("vp09"); + case kAv1: + return parameter.starts_with("av01"); + case kHevc: + return parameter.starts_with("hev1"); + case kH264: + return parameter.starts_with("avc1"); + case kNotSpecified: + return false; + } + OSP_NOTREACHED(); +} + +bool CodecParameterIsValid(AudioCodec codec, + const std::string& codec_parameter) { + if (codec_parameter.empty()) { + return true; + } + switch (codec) { + case AudioCodec::kAac: + return codec_parameter.starts_with("mp4a."); + + // Opus doesn't use codec parameters. + case AudioCodec::kOpus: // fallthrough + case AudioCodec::kNotSpecified: + return false; + } + OSP_NOTREACHED(); +} + +EnumNameTable kCastModeNames{ + {{"mirroring", CastMode::kMirroring}, {"remoting", CastMode::kRemoting}}}; + +bool TryParseRtpPayloadType(const Json::Value& value, RtpPayloadType* out) { + int t; + if (!json::TryParseInt(value, &t)) { + return false; + } + + uint8_t t_small = t; + if (t_small != t || !IsRtpPayloadType(t_small)) { + return false; + } + + *out = static_cast(t_small); + return true; +} + +bool TryParseRtpTimebase(const Json::Value& value, int* out) { + std::string raw_timebase; + if (!json::TryParseString(value, &raw_timebase)) { + return false; + } + + // The spec demands a leading 1, so this isn't really a fraction. + const auto fraction = SimpleFraction::FromString(raw_timebase); + if (fraction.is_error() || !fraction.value().is_positive() || + fraction.value().numerator() != 1) { + return false; + } + + *out = fraction.value().denominator(); + return true; +} + +// For a hex byte, the conversion is 4 bits to 1 character, e.g. +// 0b11110001 becomes F1, so 1 byte is two characters. +constexpr int kHexDigitsPerByte = 2; +constexpr int kAesBytesSize = 16; +constexpr int kAesStringLength = kAesBytesSize * kHexDigitsPerByte; +bool TryParseAesHexBytes(const Json::Value& value, + std::array* out) { + std::string hex_string; + if (!json::TryParseString(value, &hex_string)) { + return false; + } + + constexpr int kHexDigitsPerScanField = 16; + constexpr int kNumScanFields = kAesStringLength / kHexDigitsPerScanField; + uint64_t quads[kNumScanFields]; + int chars_scanned; + if (hex_string.size() == kAesStringLength && + sscanf(hex_string.c_str(), "%16" SCNx64 "%16" SCNx64 "%n", &quads[0], + &quads[1], &chars_scanned) == kNumScanFields && + chars_scanned == kAesStringLength && + std::none_of(hex_string.begin(), hex_string.end(), + [](char c) { return std::isspace(c); })) { + WriteBigEndian(quads[0], out->data()); + WriteBigEndian(quads[1], out->data() + 8); + return true; + } + + return false; +} + +std::string_view ToString(Stream::Type type) { + switch (type) { + case Stream::Type::kAudioSource: + return kAudioSourceType; + case Stream::Type::kVideoSource: + return kVideoSourceType; + default: { + OSP_NOTREACHED(); + } + } +} + +bool TryParseResolutions(const Json::Value& value, + std::vector* out) { + out->clear(); + + // Some legacy senders don't provide resolutions, so just return empty. + if (!value.isArray() || value.empty()) { + return false; + } + + for (Json::ArrayIndex i = 0; i < value.size(); ++i) { + auto resolution = Resolution::TryParse(value[i]); + if (resolution.is_error()) { + out->clear(); + return false; + } + out->push_back(std::move(resolution.value())); + } + + return true; +} + +} // namespace + +ErrorOr Stream::TryParse(const Json::Value& value, Stream::Type type) { + if (!value.isObject()) { + return Error(Error::Code::kJsonParseError, "Stream is not a JSON object"); + } + + Stream out; + out.type = type; + + if (!json::TryParseInt(value["index"], &out.index) || + !json::TryParseUint(value["ssrc"], &out.ssrc) || + !TryParseRtpPayloadType(value["rtpPayloadType"], &out.rtp_payload_type) || + !TryParseRtpTimebase(value["timeBase"], &out.rtp_timebase)) { + return Error(Error::Code::kJsonParseError, + "Offer stream has missing or invalid mandatory field"); + } + + if (!json::TryParseInt(value["channels"], &out.channels)) { + out.channels = out.type == Stream::Type::kAudioSource + ? kDefaultNumAudioChannels + : kDefaultNumVideoChannels; + } else if (out.channels <= 0) { + return Error(Error::Code::kJsonParseError, "Invalid channel count"); + } + + if (!TryParseAesHexBytes(value["aesKey"], &out.aes_key) || + !TryParseAesHexBytes(value["aesIvMask"], &out.aes_iv_mask)) { + return Error(Error::Code::kUnencryptedOffer, + "Offer stream must have both a valid aesKey and aesIvMask"); + } + if (out.rtp_timebase < + std::min(kDefaultAudioMinSampleRate, kRtpVideoTimebase) || + out.rtp_timebase > kRtpVideoTimebase) { + return Error(Error::Code::kJsonParseError, "rtp_timebase (sample rate)"); + } + + out.target_delay = kDefaultTargetPlayoutDelay; + int target_delay; + if (json::TryParseInt(value["targetDelay"], &target_delay)) { + auto d = std::chrono::milliseconds(target_delay); + if (kMinTargetPlayoutDelay <= d && d <= kMaxTargetPlayoutDelay) { + out.target_delay = d; + } + } + + json::TryParseBool(value["receiverRtcpEventLog"], + &out.receiver_rtcp_event_log); + int dscp_value; + if (json::TryParseInt(value["receiverRtcpDscp"], &dscp_value)) { + // DSCP values are clamped to [0, 63]. + if (dscp_value < 0 || dscp_value > 63) { + return Error(Error::Code::kJsonParseError, + "receiverRtcpDscp (invalid DSCP value)"); + } + out.receiver_rtcp_dscp = dscp_value; + } + + json::TryParseStringArray(value["rtpExtensions"], &out.rtp_extensions); + json::TryParseString(value["codecParameter"], &out.codec_parameter); + + return out; +} + +Json::Value Stream::ToJson() const { + OSP_CHECK(IsValid()); + + Json::Value root; + root["index"] = index; + root["type"] = std::string(ToString(type)); + root["channels"] = channels; + root["rtpPayloadType"] = static_cast(rtp_payload_type); + // rtpProfile is technically required by the spec, although it is always set + // to cast. We set it here to be compliant with all spec implementers. + root["rtpProfile"] = "cast"; + static_assert(sizeof(ssrc) <= sizeof(Json::UInt), + "this code assumes Ssrc fits in a Json::UInt"); + root["ssrc"] = static_cast(ssrc); + root["targetDelay"] = static_cast(target_delay.count()); + root["aesKey"] = HexEncode(aes_key.data(), aes_key.size()); + root["aesIvMask"] = HexEncode(aes_iv_mask.data(), aes_iv_mask.size()); + root["receiverRtcpEventLog"] = receiver_rtcp_event_log; + if (receiver_rtcp_dscp.has_value()) { + root["receiverRtcpDscp"] = receiver_rtcp_dscp.value(); + } + root["timeBase"] = "1/" + std::to_string(rtp_timebase); + root["codecParameter"] = codec_parameter; + if (!rtp_extensions.empty()) { + root["rtpExtensions"] = json::PrimitiveVectorToJson(rtp_extensions); + } + return root; +} + +bool Stream::IsValid() const { + return channels >= 1 && index >= 0 && target_delay.count() > 0 && + target_delay.count() <= std::numeric_limits::max() && + rtp_timebase >= 1; +} + +ErrorOr AudioStream::TryParse(const Json::Value& value) { + if (!value.isObject()) { + return Error(Error::Code::kJsonParseError, + "Audio stream is not a JSON object"); + } + + auto stream_or_error = Stream::TryParse(value, Stream::Type::kAudioSource); + if (stream_or_error.is_error()) { + return stream_or_error.error(); + } + + AudioStream out; + out.stream = std::move(stream_or_error.value()); + + std::string codec_name; + if (!json::TryParseInt(value["bitRate"], &out.bit_rate) || out.bit_rate < 0 || + !json::TryParseString(value[kCodecName], &codec_name)) { + return Error(Error::Code::kJsonParseError, "Invalid audio stream field"); + } + ErrorOr codec = StringToAudioCodec(codec_name); + if (!codec) { + return Error(Error::Code::kUnknownCodec, + "Codec is not known, can't use stream"); + } + out.codec = codec.value(); + if (!CodecParameterIsValid(codec.value(), out.stream.codec_parameter)) { + return Error(Error::Code::kInvalidCodecParameter, + StringFormat("Invalid audio codec parameter ({} for codec {})", + out.stream.codec_parameter.c_str(), + CodecToString(codec.value()))); + } + return out; +} + +Json::Value AudioStream::ToJson() const { + OSP_CHECK(IsValid()); + + Json::Value out = stream.ToJson(); + out[kCodecName] = CodecToString(codec); + out["bitRate"] = bit_rate; + return out; +} + +bool AudioStream::IsValid() const { + return bit_rate >= 0 && stream.IsValid(); +} + +ErrorOr VideoStream::TryParse(const Json::Value& value) { + if (!value.isObject()) { + return Error(Error::Code::kJsonParseError, + "Video stream is not a JSON object"); + } + + auto stream_or_error = Stream::TryParse(value, Stream::Type::kVideoSource); + if (stream_or_error.is_error()) { + return stream_or_error.error(); + } + + VideoStream out; + out.stream = std::move(stream_or_error.value()); + + std::string codec_name; + if (!json::TryParseString(value[kCodecName], &codec_name)) { + return Error(Error::Code::kJsonParseError, "Video stream missing codec"); + } + ErrorOr codec = StringToVideoCodec(codec_name); + if (!codec) { + return Error(Error::Code::kUnknownCodec, + "Codec is not known, can't use stream"); + } + out.codec = codec.value(); + if (!CodecParameterIsValid(codec.value(), out.stream.codec_parameter)) { + return Error(Error::Code::kInvalidCodecParameter, + StringFormat("Invalid video codec parameter ({} for codec {})", + out.stream.codec_parameter.c_str(), + CodecToString(codec.value()))); + } + + out.max_frame_rate = SimpleFraction{kDefaultMaxFrameRate, 1}; + std::string raw_max_frame_rate; + if (json::TryParseString(value["maxFrameRate"], &raw_max_frame_rate)) { + auto parsed = SimpleFraction::FromString(raw_max_frame_rate); + if (parsed.is_value() && parsed.value().is_positive()) { + out.max_frame_rate = parsed.value(); + } + } + + TryParseResolutions(value["resolutions"], &out.resolutions); + json::TryParseString(value["profile"], &out.profile); + json::TryParseString(value["protection"], &out.protection); + json::TryParseString(value["level"], &out.level); + json::TryParseString(value["errorRecoveryMode"], &out.error_recovery_mode); + if (!json::TryParseInt(value["maxBitRate"], &out.max_bit_rate)) { + out.max_bit_rate = 4 << 20; + } + + return out; +} + +Json::Value VideoStream::ToJson() const { + OSP_CHECK(IsValid()); + + Json::Value out = stream.ToJson(); + out["codecName"] = CodecToString(codec); + out["maxFrameRate"] = max_frame_rate.ToString(); + out["maxBitRate"] = max_bit_rate; + out["protection"] = protection; + out["profile"] = profile; + out["level"] = level; + out["errorRecoveryMode"] = error_recovery_mode; + + Json::Value rs; + for (auto resolution : resolutions) { + rs.append(resolution.ToJson()); + } + out["resolutions"] = std::move(rs); + return out; +} + +bool VideoStream::IsValid() const { + return max_bit_rate > 0 && max_frame_rate.is_positive(); +} + +// static +ErrorOr Offer::TryParse(const Json::Value& root) { + if (!root.isObject()) { + return Error(Error::Code::kJsonParseError, "null offer"); + } + const ErrorOr cast_mode = + GetEnum(kCastModeNames, root["castMode"].asString()); + Json::Value supported_streams = root[kSupportedStreams]; + if (!supported_streams.isArray()) { + return Error(Error::Code::kJsonParseError, "supported streams in offer"); + } + + std::vector audio_streams; + std::vector video_streams; + + using Dscp = std::optional; + std::optional receiver_rtcp_dscp; + for (Json::ArrayIndex i = 0; i < supported_streams.size(); ++i) { + const Json::Value& fields = supported_streams[i]; + std::string type; + if (!json::TryParseString(fields[kStreamType], &type)) { + return Error(Error::Code::kJsonParseError, "Missing stream type"); + } + + Error error = Error::None(); + if (type == kAudioSourceType) { + auto stream_or_error = AudioStream::TryParse(fields); + if (stream_or_error.is_value()) { + auto stream = std::move(stream_or_error.value()); + if (!receiver_rtcp_dscp) { + receiver_rtcp_dscp.emplace(stream.stream.receiver_rtcp_dscp); + } else if (stream.stream.receiver_rtcp_dscp != *receiver_rtcp_dscp) { + return Error(Error::Code::kJsonParseError, + "Mixed DSCP values in offer"); + } + audio_streams.push_back(std::move(stream)); + } else { + error = stream_or_error.error(); + } + } else if (type == kVideoSourceType) { + auto stream_or_error = VideoStream::TryParse(fields); + if (stream_or_error.is_value()) { + auto stream = std::move(stream_or_error.value()); + if (!receiver_rtcp_dscp) { + receiver_rtcp_dscp.emplace(stream.stream.receiver_rtcp_dscp); + } else if (stream.stream.receiver_rtcp_dscp != *receiver_rtcp_dscp) { + return Error(Error::Code::kJsonParseError, + "Mixed DSCP values in offer"); + } + video_streams.push_back(std::move(stream)); + } else { + error = stream_or_error.error(); + } + } + + if (!error.ok()) { + if (error.code() == Error::Code::kUnknownCodec) { + OSP_VLOG << "Dropping audio stream due to unknown codec: " << error; + continue; + } else { + return error; + } + } + } + + return Offer{cast_mode.value(CastMode::kMirroring), std::move(audio_streams), + std::move(video_streams)}; +} + +Json::Value Offer::ToJson() const { + OSP_CHECK(IsValid()); + Json::Value root; + root["castMode"] = GetEnumName(kCastModeNames, cast_mode).value(); + Json::Value streams; + for (auto& stream : audio_streams) { + streams.append(stream.ToJson()); + } + + for (auto& stream : video_streams) { + streams.append(stream.ToJson()); + } + + root[kSupportedStreams] = std::move(streams); + return root; +} + +bool Offer::IsValid() const { + return std::ranges::all_of( + audio_streams, [](const AudioStream& a) { return a.IsValid(); }) && + std::ranges::all_of(video_streams, + [](const VideoStream& v) { return v.IsValid(); }); +} +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/offer_messages.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/offer_messages.h new file mode 100644 index 0000000..042f7ff --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/offer_messages.h @@ -0,0 +1,115 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_PUBLIC_OFFER_MESSAGES_H_ +#define CAST_STREAMING_PUBLIC_OFFER_MESSAGES_H_ + +#include +#include +#include + +#include "cast/streaming/impl/rtp_defines.h" +#include "cast/streaming/message_fields.h" +#include "cast/streaming/public/session_config.h" +#include "cast/streaming/resolution.h" +#include "json/value.h" +#include "platform/base/error.h" +#include "util/simple_fraction.h" + +// This file contains the implementation of the Cast V2 Mirroring Control +// Protocol offer object definition. +namespace openscreen::cast { + +// If the target delay provided by the sender is not bounded by +// [kMinTargetDelay, kMaxTargetDelay], it will be set to +// kDefaultTargetPlayoutDelay. +inline constexpr auto kMinTargetPlayoutDelay = std::chrono::milliseconds(0); +inline constexpr auto kMaxTargetPlayoutDelay = std::chrono::milliseconds(5000); + +// If the sender provides an invalid maximum frame rate, it ill +// be set to kDefaultMaxFrameRate. +inline constexpr int kDefaultMaxFrameRate = 30; + +inline constexpr int kDefaultNumVideoChannels = 1; +inline constexpr int kDefaultNumAudioChannels = 2; + +// A stream, as detailed by the CastV2 protocol spec, is a segment of an +// offer message specifically representing a configuration object for +// a codec and its related fields, such as maximum bit rate, time base, +// and other fields. +// Composed classes include AudioStream and VideoStream, which contain +// fields specific to audio and video respectively. +struct Stream { + enum class Type : uint8_t { kAudioSource, kVideoSource }; + + static ErrorOr TryParse(const Json::Value& root, Stream::Type type); + Json::Value ToJson() const; + bool IsValid() const; + + int index = 0; + Type type = {}; + + // Default channel count is 1, e.g. for video. + int channels = 0; + RtpPayloadType rtp_payload_type = {}; + Ssrc ssrc = {}; + std::chrono::milliseconds target_delay = {}; + + // AES Key and IV mask format is very strict: a 32 digit hex string that + // must be converted to a 16 digit byte array. + std::array aes_key = {}; + std::array aes_iv_mask = {}; + + // The event logs are generally recommended for use in gathering statistics + // for the sender session. + bool receiver_rtcp_event_log = true; + std::optional receiver_rtcp_dscp; + int rtp_timebase = 0; + + // The codec parameter field honors the format laid out in RFC 6381: + // https://datatracker.ietf.org/doc/html/rfc6381. + std::string codec_parameter; + + std::vector rtp_extensions; +}; + +struct AudioStream { + static ErrorOr TryParse(const Json::Value& root); + Json::Value ToJson() const; + bool IsValid() const; + + Stream stream; + AudioCodec codec = AudioCodec::kNotSpecified; + int bit_rate = 0; +}; + +struct VideoStream { + static ErrorOr TryParse(const Json::Value& root); + Json::Value ToJson() const; + bool IsValid() const; + + Stream stream; + VideoCodec codec = VideoCodec::kNotSpecified; + SimpleFraction max_frame_rate; + int max_bit_rate = 0; + std::string protection; + std::string profile; + std::string level; + std::vector resolutions; + std::string error_recovery_mode; +}; + +struct Offer { + static ErrorOr TryParse(const Json::Value& root); + Json::Value ToJson() const; + bool IsValid() const; + + CastMode cast_mode = CastMode::kMirroring; + std::vector audio_streams; + std::vector video_streams; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_PUBLIC_OFFER_MESSAGES_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/receiver_message.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/receiver_message.cc new file mode 100644 index 0000000..9654f66 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/receiver_message.cc @@ -0,0 +1,300 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/public/receiver_message.h" + +#include +#include + +#include "cast/streaming/message_fields.h" +#include "json/reader.h" +#include "json/writer.h" +#include "platform/base/error.h" +#include "util/base64.h" +#include "util/enum_name_table.h" +#include "util/json/json_helpers.h" +#include "util/json/json_serialization.h" +#include "util/osp_logging.h" +#include "util/string_util.h" +#include "util/stringprintf.h" + +namespace openscreen::cast { + +namespace { + +EnumNameTable kMessageTypeNames{ + {{kMessageTypeAnswer, ReceiverMessage::Type::kAnswer}, + {"CAPABILITIES_RESPONSE", ReceiverMessage::Type::kCapabilitiesResponse}, + {"RPC", ReceiverMessage::Type::kRpc}, + {"INPUT", ReceiverMessage::Type::kInput}}}; + +EnumNameTable kMediaCapabilityNames{ + {{"audio", MediaCapability::kAudio}, + {"aac", MediaCapability::kAac}, + {"opus", MediaCapability::kOpus}, + {"video", MediaCapability::kVideo}, + {"4k", MediaCapability::k4k}, + {"h264", MediaCapability::kH264}, + {"vp8", MediaCapability::kVp8}, + {"vp9", MediaCapability::kVp9}, + {"hevc", MediaCapability::kHevc}, + {"av1", MediaCapability::kAv1}}}; + +ReceiverMessage::Type GetMessageType(const Json::Value& root) { + std::string type; + if (!json::TryParseString(root[kMessageType], &type)) { + return ReceiverMessage::Type::kUnknown; + } + string_util::AsciiStrToUpper(type); + + ErrorOr parsed = GetEnum(kMessageTypeNames, type); + return parsed.value(ReceiverMessage::Type::kUnknown); +} + +bool TryParseCapability(const Json::Value& value, MediaCapability* out) { + std::string c; + if (!json::TryParseString(value, &c)) { + return false; + } + + const ErrorOr capability = GetEnum(kMediaCapabilityNames, c); + if (capability.is_error()) { + return false; + } + + *out = capability.value(); + return true; +} + +} // namespace + +ReceiverError::ReceiverError(int code, std::string_view description) + : code(code), description(description) { + if (code >= kOpenscreenErrorOffset) { + openscreen_code = static_cast(code - kOpenscreenErrorOffset); + } +} + +ReceiverError::ReceiverError(Error::Code code, std::string_view description) + : code(static_cast(code) + kOpenscreenErrorOffset), + openscreen_code(code), + description(description) {} + +ReceiverError::ReceiverError(const Error& error) + : code(static_cast(error.code()) + kOpenscreenErrorOffset), + openscreen_code(error.code()), + description(error.message()) {} + +ReceiverError::ReceiverError(const ReceiverError&) = default; +ReceiverError::ReceiverError(ReceiverError&&) noexcept = default; +ReceiverError& ReceiverError::operator=(const ReceiverError&) = default; +ReceiverError& ReceiverError::operator=(ReceiverError&&) = default; +ReceiverError::~ReceiverError() = default; + +// static +ErrorOr ReceiverError::Parse(const Json::Value& value) { + if (!value.isObject()) { + return Error(Error::Code::kParameterInvalid, + "Empty JSON in receiver error parsing"); + } + + int code; + std::string description; + if (!json::TryParseInt(value[kErrorCode], &code) || + !json::TryParseString(value[kErrorDescription], &description)) { + return Error::Code::kJsonParseError; + } + + return ReceiverError(code, description); +} + +Json::Value ReceiverError::ToJson() const { + Json::Value root; + root[kErrorCode] = openscreen_code ? static_cast(*openscreen_code) + + kOpenscreenErrorOffset + : code; + root[kErrorDescription] = description; + return root; +} + +Error ReceiverError::ToError() const { + if (openscreen_code) { + return Error(*openscreen_code, description); + } + + std::string full_description = StringFormat("Error code: {}, description: {}", + code, description.c_str()); + return Error(Error::Code::kUnknownError, std::move(full_description)); +} + +// static +ErrorOr ReceiverCapability::Parse( + const Json::Value& value) { + if (!value.isObject()) { + return Error(Error::Code::kParameterInvalid, + "Empty JSON in capabilities parsing"); + } + + int remoting_version; + if (!json::TryParseInt(value["remoting"], &remoting_version)) { + remoting_version = ReceiverCapability::kRemotingVersionUnknown; + } + + std::vector capabilities; + if (!json::TryParseArray( + value["mediaCaps"], TryParseCapability, &capabilities)) { + return Error(Error::Code::kJsonParseError, + "Failed to parse media capabilities"); + } + + return ReceiverCapability{remoting_version, std::move(capabilities)}; +} + +Json::Value ReceiverCapability::ToJson() const { + Json::Value root; + root["remoting"] = remoting_version; + Json::Value capabilities(Json::ValueType::arrayValue); + for (const auto& capability : media_capabilities) { + capabilities.append(GetEnumName(kMediaCapabilityNames, capability).value()); + } + root["mediaCaps"] = std::move(capabilities); + return root; +} + +// static +ErrorOr ReceiverMessage::Parse(const Json::Value& value) { + ReceiverMessage message; + if (!value.isObject()) { + return Error(Error::Code::kJsonParseError, "Invalid message body"); + } + + std::string result; + if (!json::TryParseString(value[kResult], &result)) { + result = kResultError; + } + + message.type = GetMessageType(value); + message.valid = + (result == kResultOk || message.type == ReceiverMessage::Type::kRpc || + message.type == ReceiverMessage::Type::kInput); + + if (message.type != ReceiverMessage::Type::kRpc && + message.type != ReceiverMessage::Type::kInput) { + if (!json::TryParseInt(value[kSequenceNumber], + &(message.sequence_number))) { + message.sequence_number = -1; + } + + // Sequence numbers must be non-negative. + if (message.sequence_number < 0) { + message.valid = false; + } + } + + if (!message.valid) { + ErrorOr error = + ReceiverError::Parse(value[kErrorMessageBody]); + if (error.is_value()) { + message.body = std::move(error.value()); + } + return message; + } + + switch (message.type) { + case Type::kAnswer: { + auto answer_or_error = + openscreen::cast::Answer::TryParse(value[kAnswerMessageBody]); + if (answer_or_error.is_value()) { + message.body = std::move(answer_or_error.value()); + message.valid = true; + } + } break; + + case Type::kCapabilitiesResponse: { + ErrorOr capability = + ReceiverCapability::Parse(value[kCapabilitiesMessageBody]); + if (capability.is_value()) { + message.body = std::move(capability.value()); + message.valid = true; + } + } break; + + case Type::kRpc: { + std::string encoded_rpc; + std::vector rpc; + if (json::TryParseString(value[kRpcMessageBody], &encoded_rpc) && + base64::Decode(encoded_rpc, &rpc)) { + message.body = std::move(rpc); + message.valid = true; + } + } break; + + case Type::kInput: { + std::string encoded_input; + std::vector input; + if (json::TryParseString(value[kInputMessageBody], &encoded_input) && + base64::Decode(encoded_input, &input)) { + message.body = std::move(input); + message.valid = true; + } + } break; + + default: + break; + } + + return message; +} + +ErrorOr ReceiverMessage::ToJson() const { + OSP_CHECK(type != ReceiverMessage::Type::kUnknown) + << "Trying to send an unknown message is a developer error"; + + Json::Value root; + root[kMessageType] = GetEnumName(kMessageTypeNames, type).value(); + if (sequence_number >= 0) { + root[kSequenceNumber] = sequence_number; + } + + switch (type) { + case ReceiverMessage::Type::kAnswer: + if (valid) { + root[kResult] = kResultOk; + root[kAnswerMessageBody] = std::get(body).ToJson(); + } else { + root[kResult] = kResultError; + root[kErrorMessageBody] = std::get(body).ToJson(); + } + break; + + case ReceiverMessage::Type::kCapabilitiesResponse: + if (valid) { + root[kResult] = kResultOk; + root[kCapabilitiesMessageBody] = + std::get(body).ToJson(); + } else { + root[kResult] = kResultError; + root[kErrorMessageBody] = std::get(body).ToJson(); + } + break; + + // NOTE: RPC messages do NOT have a result field. + case ReceiverMessage::Type::kRpc: + root[kRpcMessageBody] = + base64::Encode(std::get>(body)); + break; + + case ReceiverMessage::Type::kInput: + root[kInputMessageBody] = + base64::Encode(std::get>(body)); + break; + + default: + OSP_NOTREACHED(); + } + + return root; +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/receiver_message.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/receiver_message.h new file mode 100644 index 0000000..6bf5457 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/receiver_message.h @@ -0,0 +1,117 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_PUBLIC_RECEIVER_MESSAGE_H_ +#define CAST_STREAMING_PUBLIC_RECEIVER_MESSAGE_H_ + +#include +#include +#include +#include +#include +#include + +#include "cast/streaming/public/answer_messages.h" +#include "json/value.h" +#include "util/osp_logging.h" + +namespace openscreen::cast { + +enum class MediaCapability { + kAudio, + kAac, + kOpus, + kVideo, + k4k, + kH264, + kVp8, + kVp9, + kHevc, + kAv1 +}; + +struct ReceiverCapability { + static constexpr int kRemotingVersionUnknown = -1; + + Json::Value ToJson() const; + static ErrorOr Parse(const Json::Value& value); + + // The remoting version that the receiver uses. + int remoting_version = kRemotingVersionUnknown; + + // Set of capabilities (e.g., ac3, 4k, hevc, vp9, dolby_vision, etc.). + std::vector media_capabilities; +}; + +// To avoid collisions with legacy error values, all Open Screen receiver errors +// are offset. +struct ReceiverError { + explicit ReceiverError(int code, std::string_view description = ""); + explicit ReceiverError(Error::Code code, std::string_view description = ""); + explicit ReceiverError(const Error& error); + + ReceiverError(const ReceiverError&); + ReceiverError(ReceiverError&&) noexcept; + ReceiverError& operator=(const ReceiverError&); + ReceiverError& operator=(ReceiverError&&); + ~ReceiverError(); + + Json::Value ToJson() const; + static ErrorOr Parse(const Json::Value& value); + Error ToError() const; + + // All Open Screen errors are offset by a fixed value to avoid overlapping + // with legacy values. + static constexpr int kOpenscreenErrorOffset = 10000; + + // Raw error code. + int32_t code = -1; + + // Parsed openscreen::Error code. May be nullopt if not a match. + std::optional openscreen_code; + + // Error description. + std::string description; +}; + +struct ReceiverMessage { + public: + // Receiver response message type. + enum class Type { + // Unknown message type. + kUnknown, + + // Response to OFFER message. + kAnswer, + + // Response to GET_CAPABILITIES message. + kCapabilitiesResponse, + + // Rpc binary messages. The payload is base64-encoded. + kRpc, + + // Input-related binary messages. The payload is base64-encoded. + kInput, + }; + + static ErrorOr Parse(const Json::Value& value); + ErrorOr ToJson() const; + + Type type = Type::kUnknown; + + int32_t sequence_number = -1; + + bool valid = false; + + std::variant, // Binary-encoded protobuf message. + ReceiverCapability, + ReceiverError> + body; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_PUBLIC_RECEIVER_MESSAGE_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/sender.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/sender.cc new file mode 100644 index 0000000..a910b1e --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/sender.cc @@ -0,0 +1,12 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/public/sender.h" + +namespace openscreen::cast { + +Sender::Observer::~Observer() = default; +Sender::~Sender() = default; + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/sender.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/sender.h new file mode 100644 index 0000000..f6f34e4 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/sender.h @@ -0,0 +1,173 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_PUBLIC_SENDER_H_ +#define CAST_STREAMING_PUBLIC_SENDER_H_ + +#include + +#include + +#include "cast/streaming/public/encoded_frame.h" +#include "cast/streaming/public/frame_id.h" +#include "cast/streaming/public/session_config.h" +#include "cast/streaming/rtp_time.h" +#include "cast/streaming/ssrc.h" +#include "platform/api/time.h" + +namespace openscreen::cast { + +// The Cast Streaming Sender, a peer corresponding to some Cast Streaming +// Receiver at the other end of a network link. +// +// The Sender is the peer responsible for enqueuing EncodedFrames for streaming, +// guaranteeing their delivery to a Receiver, and handling feedback events from +// a Receiver. Some feedback events are used for managing the Sender's internal +// queue of in-flight frames, requesting network packet re-transmits, etc.; +// while others are exposed via the Sender's public interface. For example, +// sometimes the Receiver signals that it needs a a key frame to resolve a +// picture loss condition, and the modules upstream of the Sender (e.g., where +// encoding happens) should call NeedsKeyFrame() to check for, and handle that. +// +// There are usually one or two Senders in a streaming session, one for audio +// and one for video. Both senders work with the same SenderPacketRouter +// instance to schedule their transmission of packets, and provide the necessary +// metrics for estimating bandwidth utilization and availability. +// +// It is the responsibility of upstream code modules to handle congestion +// control. With respect to this Sender, that means the media encoding bit rate +// should be throttled based on network bandwidth availability. This Sender does +// not do any throttling, only flow-control. In other words, this Sender can +// only manage its in-flight queue of frames, and if that queue grows too large, +// it will eventually reject further enqueuing. +// +// General usage: A client should check the in-flight media duration frequently +// to decide when to pause encoding, to avoid wasting system resources on +// encoding frames that will likely be rejected by the Sender. The client should +// also frequently call NeedsKeyFrame() and, when this returns true, direct its +// encoder to produce a key frame soon. Finally, when using EnqueueFrame(), an +// EncodedFrame struct should be prepared with its frame_id field set to +// whatever GetNextFrameId() returns. Please see method comments for +// more-detailed usage info. +class Sender { + public: + // Interface for receiving notifications about events of possible interest. + class Observer { + public: + // Called when a frame was canceled, which may occur in the following cases: + // - The Receiver acknowledged successful receipt of the frame. + // - The Receiver decided to skip over the frame (e.g. it was too late). + // - The Sender decided to skip the frame (e.g. OnFrameCanceled() called). + // + // Note: Frame cancellations may occur out-of-order. + virtual void OnFrameCanceled(FrameId frame_id) = 0; + + // Called when a Receiver begins reporting picture loss, and there is no key + // frame currently enqueued in the Sender. The application should enqueue a + // key frame as soon as possible. + // + // This acts as a "push" notification, which is useful for immediately + // waking up an application that may be waiting for the next capture tick. + // For "pull" state checking inside a continuous encoding loop, see + // NeedsKeyFrame(). + virtual void OnPictureLost() = 0; + + protected: + virtual ~Observer(); + }; + + // Result codes for EnqueueFrame(). + enum EnqueueFrameResult { + // The frame has been queued for sending. + OK, + + // The frame's payload was too large. + PAYLOAD_TOO_LARGE, + + // The span of FrameIds is too large. + REACHED_ID_SPAN_LIMIT, + + // Too-large a media duration is in-flight. + MAX_DURATION_IN_FLIGHT, + }; + + virtual ~Sender(); + + // The session configuration for this sender. The configuration is generated + // from the offer/answer exchange, and includes critical information like the + // RTP timebase, SSRCs for sending and receiving, and the AES configuration. + virtual const SessionConfig& config() const = 0; + + // Sets an observer for receiving notifications. Call with nullptr to stop + // observing. + virtual void SetObserver(Observer* observer) = 0; + + // Returns the number of frames currently in-flight. This is only meant to be + // informative. Clients should use GetInFlightMediaDuration() to make + // throttling decisions. + virtual size_t GetInFlightFrameCount() const = 0; + + // Returns the total media duration of the frames currently in-flight, + // assuming the next not-yet-enqueued frame will have the given RTP timestamp. + // For a better user experience, the result should be compared to + // GetMaxInFlightMediaDuration(), and media encoding should be throttled down + // before additional EnqueueFrame() calls would cause this to reach the + // current maximum limit. + virtual Clock::duration GetInFlightMediaDuration( + RtpTimeTicks next_frame_rtp_timestamp) const = 0; + + // Return the maximum acceptable in-flight media duration, given the current + // target playout delay setting and end-to-end network/system conditions. + virtual Clock::duration GetMaxInFlightMediaDuration() const = 0; + + // Returns true if the Receiver requires a key frame. Note that this will + // return true until a key frame is accepted by EnqueueFrame(). Thus, when + // encoding is pipelined, care should be taken to instruct the encoder to + // produce just ONE forced key frame. + // + // This acts as a stateful "pull" check, which is useful for an encoder loop + // to poll right before processing the next image. For "push" notifications + // to wake up an idle application, see Observer::OnPictureLost(). + virtual bool NeedsKeyFrame() const = 0; + + // Returns the next FrameId, the one after the frame enqueued by the last call + // to EnqueueFrame(). Note that the next call to EnqueueFrame() assumes this + // frame ID be used. + virtual FrameId GetNextFrameId() const = 0; + + // Get the current round trip time, defined as the total time between when the + // sender report is sent and the receiver report is received. This value is + // updated with each receiver report using a weighted moving average of 1/8 + // for the new value and 7/8 for the previous value. Will be set to + // Clock::duration::zero() if no reports have been received yet. + // TODO(crbug.com/498036656): move to a more modern approach for estimating + // bandwidth. + virtual Clock::duration GetCurrentRoundTripTime() const = 0; + + // Enqueues the given `frame` for sending as soon as possible. Returns OK if + // the frame is accepted, and some time later Observer::OnFrameCanceled() will + // be called once it is no longer in-flight. + // + // All fields of the `frame` must be set to valid values: the `frame_id` must + // be the same as GetNextFrameId(); both the `rtp_timestamp` and + // `reference_time` fields must be monotonically increasing relative to the + // prior frame; and the frame's `data` pointer must be set. + [[nodiscard]] virtual EnqueueFrameResult EnqueueFrame( + const EncodedFrame& frame) = 0; + + // Causes all pending operations to discard data when they are processed + // later. This will notify observers by invoking OnFrameCanceled() for each + // canceled frame. + virtual void CancelInFlightData() = 0; + + // May be called by the consumer to report that a frame has been dropped. This + // is used to report drop statistics to the sender's statistics collector. + virtual void ReportFrameDropEvent(FrameId frame_id, + RtpTimeTicks rtp_timestamp, + Clock::time_point drop_time) = 0; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_PUBLIC_SENDER_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_config.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_config.cc new file mode 100644 index 0000000..e819b4f --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_config.cc @@ -0,0 +1,54 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/public/session_config.h" + +#include +#include + +namespace openscreen::cast { + +namespace { + +bool IsNonZero(uint8_t byte) { + return byte > 0; +} + +} // namespace + +SessionConfig::SessionConfig(Ssrc sender_ssrc, + Ssrc receiver_ssrc, + int rtp_timebase, + int channels, + std::chrono::milliseconds target_playout_delay, + std::array aes_secret_key, + std::array aes_iv_mask, + bool is_pli_enabled, + StreamType stream_type, + bool are_receiver_event_logs_enabled) + : sender_ssrc(sender_ssrc), + receiver_ssrc(receiver_ssrc), + rtp_timebase(rtp_timebase), + channels(channels), + target_playout_delay(target_playout_delay), + aes_secret_key(std::move(aes_secret_key)), + aes_iv_mask(std::move(aes_iv_mask)), + is_pli_enabled(is_pli_enabled), + stream_type(stream_type), + are_receiver_event_logs_enabled(are_receiver_event_logs_enabled) {} + +SessionConfig::SessionConfig(const SessionConfig& other) = default; +SessionConfig::SessionConfig(SessionConfig&& other) noexcept = default; +SessionConfig& SessionConfig::operator=(const SessionConfig& other) = default; +SessionConfig& SessionConfig::operator=(SessionConfig&& other) noexcept = + default; +SessionConfig::~SessionConfig() = default; + +bool SessionConfig::IsValid() const { + return sender_ssrc > 0 && receiver_ssrc > 0 && rtp_timebase > 0 && + channels > 0 && + std::any_of(aes_secret_key.begin(), aes_secret_key.end(), IsNonZero) && + std::any_of(aes_iv_mask.begin(), aes_iv_mask.end(), IsNonZero); +} +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_config.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_config.h new file mode 100644 index 0000000..e77f1fb --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_config.h @@ -0,0 +1,71 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_PUBLIC_SESSION_CONFIG_H_ +#define CAST_STREAMING_PUBLIC_SESSION_CONFIG_H_ + +#include +#include +#include + +#include "cast/streaming/public/constants.h" +#include "cast/streaming/ssrc.h" + +namespace openscreen::cast { + +// Common streaming configuration, established from the OFFER/ANSWER exchange, +// that the Sender and Receiver are both assuming. +struct SessionConfig final { + SessionConfig(Ssrc sender_ssrc, + Ssrc receiver_ssrc, + int rtp_timebase, + int channels, + std::chrono::milliseconds target_playout_delay, + std::array aes_secret_key, + std::array aes_iv_mask, + bool is_pli_enabled = false, + StreamType stream_type = StreamType::kUnknown, + bool are_receiver_event_logs_enabled = true); + SessionConfig(const SessionConfig& other); + SessionConfig(SessionConfig&& other) noexcept; + SessionConfig& operator=(const SessionConfig& other); + SessionConfig& operator=(SessionConfig&& other) noexcept; + ~SessionConfig(); + + bool IsValid() const; + + // The sender and receiver's SSRC identifiers. Note: SSRC identifiers + // are defined as unsigned 32 bit integers here: + // https://tools.ietf.org/html/rfc5576#page-5 + Ssrc sender_ssrc = 0; + Ssrc receiver_ssrc = 0; + + // RTP timebase: The number of RTP units advanced per second. For audio, + // this is the sampling rate. For video, this is 90 kHz by convention. + int rtp_timebase = 90000; + + // Number of channels. Must be 1 for video, for audio typically 2. + int channels = 1; + + // Initial target playout delay. + std::chrono::milliseconds target_playout_delay; + + // The AES-128 crypto key and initialization vector. + std::array aes_secret_key{}; + std::array aes_iv_mask{}; + + // Whether picture loss indication (PLI) should be used for this session. + bool is_pli_enabled = false; + + // The type (e.g. audio or video) of the stream. + StreamType stream_type = StreamType::kUnknown; + + // Whether RTCP event logs from the Receiver are enabled. These are used for + // generating statistics. It is recommended that this generally be true. + bool are_receiver_event_logs_enabled = true; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_PUBLIC_SESSION_CONFIG_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_messenger.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_messenger.cc new file mode 100644 index 0000000..9062694 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_messenger.cc @@ -0,0 +1,388 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/public/session_messenger.h" + +#include +#include +#include + +#include "cast/common/public/message_port.h" +#include "cast/streaming/message_fields.h" +#include "platform/base/trivial_clock_traits.h" +#include "util/json/json_helpers.h" +#include "util/json/json_serialization.h" +#include "util/osp_logging.h" +#include "util/string_util.h" + +namespace openscreen::cast { + +namespace { + +// Default timeout to receive a reply message in response to a request message +// sent by us. +constexpr std::chrono::milliseconds kReplyTimeout(4000); + +// Special character indicating message was sent to all receivers or senders. +constexpr char kAnyDestination[] = "*"; + +void ReplyIfTimedOut( + int sequence_number, + std::vector>* + replies) { + for (auto it = replies->begin(); it != replies->end(); ++it) { + if (it->first == sequence_number) { + OSP_VLOG << "Reply was an error with due to timeout for sequence number: " + << sequence_number; + + // We erase before handling the callback, since it may invalidate the + // replies vector. + SenderSessionMessenger::ReplyCallback callback = std::move(it->second); + replies->erase(it); + callback(Error(Error::Code::kMessageTimeout, + string_util::StrCat({"message timed out; max delay of ", + ToString(kReplyTimeout)}))); + return; + } + } +} + +} // namespace + +SessionMessenger::SessionMessenger(MessagePort& message_port, + std::string source_id, + ErrorCallback cb) + : message_port_(message_port), + source_id_(source_id), + error_callback_(std::move(cb)) { + OSP_CHECK(!source_id_.empty()); + message_port_->SetClient(*this); +} + +SessionMessenger::~SessionMessenger() { + message_port_->ResetClient(); +} + +Error SessionMessenger::SendMessage(const std::string& destination_id, + const std::string& namespace_, + const Json::Value& message_root) { + OSP_CHECK(namespace_ == kCastRemotingNamespace || + namespace_ == kCastWebrtcNamespace); + auto body_or_error = json::Stringify(message_root); + if (body_or_error.is_error()) { + return std::move(body_or_error.error()); + } + OSP_VLOG << "Sending message: DESTINATION[" << destination_id + << "], NAMESPACE[" << namespace_ << "], BODY:\n" + << body_or_error.value(); + message_port_->PostMessage(destination_id, namespace_, body_or_error.value()); + return Error::None(); +} + +void SessionMessenger::ReportError(const Error& error) { + error_callback_(error); +} + +SenderSessionMessenger::SenderSessionMessenger(MessagePort& message_port, + std::string source_id, + std::string receiver_id, + ErrorCallback cb, + TaskRunner& task_runner) + : SessionMessenger(message_port, std::move(source_id), std::move(cb)), + task_runner_(task_runner), + receiver_id_(std::move(receiver_id)) {} + +void SenderSessionMessenger::SetHandler(ReceiverMessage::Type type, + ReplyCallback cb) { + // Currently the only handlers allowed are for RPC and INPUT messages. + if (type == ReceiverMessage::Type::kRpc) { + rpc_callback_ = std::move(cb); + } else if (type == ReceiverMessage::Type::kInput) { + input_callback_ = std::move(cb); + } else { + OSP_NOTREACHED(); + } +} + +void SenderSessionMessenger::ResetHandler(ReceiverMessage::Type type) { + if (type == ReceiverMessage::Type::kRpc) { + rpc_callback_ = {}; + } else if (type == ReceiverMessage::Type::kInput) { + input_callback_ = {}; + } else { + OSP_NOTREACHED(); + } +} + +Error SenderSessionMessenger::SendOutboundMessage(SenderMessage message) { + const auto namespace_ = (message.type == SenderMessage::Type::kRpc || + message.type == SenderMessage::Type::kInput) + ? kCastRemotingNamespace + : kCastWebrtcNamespace; + + ErrorOr jsonified = message.ToJson(); + OSP_CHECK(jsonified.is_value()) << "Tried to send an invalid message"; + return SessionMessenger::SendMessage(receiver_id_, namespace_, + jsonified.value()); +} + +Error SenderSessionMessenger::SendRpcMessage(ByteView message) { + return SendOutboundMessage(SenderMessage{ + openscreen::cast::SenderMessage::Type::kRpc, + -1 /* sequence_number, unused by RPC messages */, true /* valid */, + std::vector(message.begin(), message.end())}); +} + +Error SenderSessionMessenger::SendInputMessage(ByteView message) { + return SendOutboundMessage(SenderMessage{ + openscreen::cast::SenderMessage::Type::kInput, + -1 /* sequence_number, unused by INPUT messages */, true /* valid */, + std::vector(message.begin(), message.end())}); +} + +Error SenderSessionMessenger::SendRequest(SenderMessage message, + ReceiverMessage::Type reply_type, + ReplyCallback cb) { + // RPC and INPUT messages are not meant to be request/reply. + OSP_CHECK(reply_type != ReceiverMessage::Type::kRpc); + OSP_CHECK(reply_type != ReceiverMessage::Type::kInput); + + if (!cb) { + return Error(Error::Code::kParameterInvalid, + "Must provide a reply callback"); + } + const Error error = SendOutboundMessage(message); + if (!error.ok()) { + return error; + } + + OSP_DCHECK(awaiting_replies_.find(message.sequence_number) == + awaiting_replies_.end()); + awaiting_replies_.emplace_back(message.sequence_number, std::move(cb)); + task_runner_->PostTaskWithDelay( + [self = weak_factory_.GetWeakPtr(), seq_num = message.sequence_number] { + if (self) { + ReplyIfTimedOut(seq_num, &self->awaiting_replies_); + } + }, + kReplyTimeout); + + return Error::None(); +} + +void SenderSessionMessenger::OnMessage(const std::string& source_id, + const std::string& message_namespace, + const std::string& message) { + if (source_id != receiver_id_ && source_id != kAnyDestination) { + OSP_DLOG_WARN << "Received message from unknown/incorrect Cast Receiver " + << source_id << ". Currently connected to " << receiver_id_; + return; + } + + if (message_namespace != kCastWebrtcNamespace && + message_namespace != kCastRemotingNamespace) { + OSP_DLOG_WARN << "Received message from unknown namespace: " + << message_namespace << ". Message was " << message; + return; + } + + ErrorOr message_body = json::Parse(message); + if (!message_body || !message_body.value().isObject()) { + ReportError(message_body.error()); + OSP_DLOG_WARN << "Received an invalid message: " << message; + return; + } + + // If the message is valid JSON and we don't understand it, there are two + // options: (1) it's an unknown type, or (2) the receiver filled out the + // message incorrectly. In the first case we can drop it, it's likely just + // unsupported. In the second case we might need it, so worth warning the + // client. + ErrorOr receiver_message = + ReceiverMessage::Parse(message_body.value()); + if (receiver_message.is_error()) { + ReportError(receiver_message.error()); + OSP_DLOG_WARN << "Received an invalid receiver message: " + << receiver_message.error(); + return; + } + + if (receiver_message.value().type == ReceiverMessage::Type::kRpc) { + if (rpc_callback_) { + rpc_callback_(receiver_message.value()); + } else { + OSP_DLOG_INFO << "Received RPC message but no callback, dropping"; + } + } else if (receiver_message.value().type == ReceiverMessage::Type::kInput) { + if (input_callback_) { + input_callback_(receiver_message.value()); + } else { + OSP_DLOG_INFO << "Received INPUT message but no callback, dropping"; + } + } else { + const int sequence_number = receiver_message.value().sequence_number; + auto it = awaiting_replies_.find(sequence_number); + if (it == awaiting_replies_.end()) { + OSP_DLOG_WARN << "Received a reply I wasn't waiting for: " + << sequence_number; + return; + } + + ReplyCallback callback = std::move(it->second); + awaiting_replies_.erase(it); + callback(std::move(receiver_message.value())); + } +} + +void SenderSessionMessenger::OnError(const Error& error) { + OSP_DLOG_WARN << "Received an error in the session messenger: " << error; + ReportError(error); +} + +ReceiverSessionMessenger::ReceiverSessionMessenger(MessagePort& message_port, + std::string source_id, + ErrorCallback cb) + : SessionMessenger(message_port, std::move(source_id), std::move(cb)) {} + +void ReceiverSessionMessenger::SetHandler(SenderMessage::Type type, + RequestCallback cb) { + OSP_DCHECK(callbacks_.find(type) == callbacks_.end()); + callbacks_.emplace_back(type, std::move(cb)); +} + +void ReceiverSessionMessenger::ResetHandler(SenderMessage::Type type) { + callbacks_.erase_key(type); +} + +Error ReceiverSessionMessenger::SendRpcMessage(const std::string& source_id, + ByteView message) { + return SendMessage( + source_id, + ReceiverMessage{ReceiverMessage::Type::kRpc, -1 /* sequence_number */, + true /* valid */, + std::vector(message.begin(), message.end())}); +} + +Error ReceiverSessionMessenger::SendInputMessage(const std::string& source_id, + ByteView message) { + return SendMessage( + source_id, + ReceiverMessage{ReceiverMessage::Type::kInput, -1 /* sequence_number */, + true /* valid */, + std::vector(message.begin(), message.end())}); +} + +Error ReceiverSessionMessenger::SendMessage(const std::string& source_id, + ReceiverMessage message) { + if (source_id.empty()) { + return Error(Error::Code::kInitializationFailure, + "Cannot send a message without a current source ID."); + } + + const auto namespace_ = (message.type == ReceiverMessage::Type::kRpc || + message.type == ReceiverMessage::Type::kInput) + ? kCastRemotingNamespace + : kCastWebrtcNamespace; + + ErrorOr message_json = message.ToJson(); + OSP_CHECK(message_json.is_value()) << "Tried to send an invalid message"; + return SessionMessenger::SendMessage(source_id, namespace_, + message_json.value()); +} + +void ReceiverSessionMessenger::SetCustomMessageHandler( + std::string_view message_namespace, + CustomMessageCallback cb) { + auto it = std::find_if(custom_message_handlers_.begin(), + custom_message_handlers_.end(), + [&message_namespace](const auto& pair) { + return pair.first == message_namespace; + }); + + if (!cb) { + if (it != custom_message_handlers_.end()) { + custom_message_handlers_.erase(it); + } + return; + } + + if (it != custom_message_handlers_.end()) { + OSP_LOG_ERROR << "Handler already exists for namespace: " + << message_namespace; + return; + } else { + custom_message_handlers_.emplace_back(std::string(message_namespace), + std::move(cb)); + } +} + +Error ReceiverSessionMessenger::SendMessage(std::string_view destination_id, + std::string_view message_namespace, + std::string_view message) { + message_port().PostMessage(std::string(destination_id), + std::string(message_namespace), + std::string(message)); + return Error::None(); +} + +void ReceiverSessionMessenger::OnMessage(const std::string& source_id, + const std::string& message_namespace, + const std::string& message) { + if (message_namespace != kCastWebrtcNamespace && + message_namespace != kCastRemotingNamespace) { + auto it = std::find_if(custom_message_handlers_.begin(), + custom_message_handlers_.end(), + [&message_namespace](const auto& pair) { + return pair.first == message_namespace; + }); + if (it != custom_message_handlers_.end()) { + it->second(source_id, message_namespace, message); + return; + } + OSP_DLOG_WARN << "Received message from unknown namespace: " + << message_namespace; + return; + } + + // If the message is bad JSON, the sender is in a funky state so we + // report an error. + ErrorOr message_body = json::Parse(message); + if (message_body.is_error() || !message_body.value().isObject()) { + ReportError(message_body.error()); + return; + } + + // If the message is valid JSON and we don't understand it, there are two + // options: (1) it's an unknown type, or (2) the sender filled out the message + // incorrectly. In the first case we can drop it, it's likely just + // unsupported. In the second case we might need it, so worth warning the + // client. + ErrorOr sender_message = + SenderMessage::Parse(message_body.value()); + if (sender_message.is_error()) { + ReportError(sender_message.error()); + OSP_DLOG_WARN << "Received an invalid sender message: " + << sender_message.error(); + return; + } + + if (sender_message.value().type == SenderMessage::Type::kOffer || + sender_message.value().type == SenderMessage::Type::kGetCapabilities) { + OSP_VLOG << "Received Message:\n" << message; + } + + auto it = callbacks_.find(sender_message.value().type); + if (it == callbacks_.end()) { + OSP_DLOG_INFO << "Received message without a callback, dropping"; + return; + } + it->second(source_id, sender_message.value()); +} + +void ReceiverSessionMessenger::OnError(const Error& error) { + OSP_DLOG_WARN << "Received an error in the session messenger: " << error; + ReportError(error); +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_messenger.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_messenger.h new file mode 100644 index 0000000..6488bd3 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/session_messenger.h @@ -0,0 +1,168 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_PUBLIC_SESSION_MESSENGER_H_ +#define CAST_STREAMING_PUBLIC_SESSION_MESSENGER_H_ + +#include +#include +#include +#include + +#include "cast/common/public/message_port.h" +#include "cast/streaming/public/answer_messages.h" +#include "cast/streaming/public/offer_messages.h" +#include "cast/streaming/public/receiver_message.h" +#include "cast/streaming/sender_message.h" +#include "json/value.h" +#include "platform/api/task_runner.h" +#include "platform/base/span.h" +#include "util/flat_map.h" +#include "util/raw_ref.h" +#include "util/weak_ptr.h" + +namespace openscreen::cast { + +// A message port interface designed specifically for use by the Receiver +// and Sender session classes. +class SessionMessenger : public MessagePort::Client { + public: + using ErrorCallback = std::function; + + SessionMessenger(MessagePort& message_port, + std::string source_id, + ErrorCallback cb); + ~SessionMessenger() override; + + MessagePort& message_port() { return *message_port_; } + + protected: + // Barebones message sending method shared by both children. + [[nodiscard]] Error SendMessage(const std::string& destination_id, + const std::string& namespace_, + const Json::Value& message_root); + + // Used to report errors in subclasses. + void ReportError(const Error& error); + + const std::string& source_id() override { return source_id_; } + + private: + const raw_ref message_port_; + const std::string source_id_; + ErrorCallback error_callback_; +}; + +// Message port interface designed to handle sending messages to and +// from a receiver. When possible, errors receiving messages are reported +// to the ReplyCallback passed to SendRequest(), otherwise errors are +// reported to the ErrorCallback passed in the constructor. +class SenderSessionMessenger final : public SessionMessenger { + public: + using ReplyCallback = std::function)>; + + SenderSessionMessenger(MessagePort& message_port, + std::string source_id, + std::string receiver_id, + ErrorCallback cb, + TaskRunner& task_runner); + + // Set receiver message handler. Note that this should only be + // applied for messages that don't have sequence numbers, like RPC + // and status messages. + void SetHandler(ReceiverMessage::Type type, ReplyCallback cb); + void ResetHandler(ReceiverMessage::Type type); + + // Send a message that doesn't require a reply. + [[nodiscard]] Error SendOutboundMessage(SenderMessage message); + + // Convenience method for sending a valid RPC message. + [[nodiscard]] Error SendRpcMessage(ByteView message); + + // Convenience method for sending a valid INPUT message. + [[nodiscard]] Error SendInputMessage(ByteView message); + + // Send a request (with optional reply callback). + [[nodiscard]] Error SendRequest(SenderMessage message, + ReceiverMessage::Type reply_type, + ReplyCallback cb); + + // MessagePort::Client overrides + void OnMessage(const std::string& source_id, + const std::string& message_namespace, + const std::string& message) override; + void OnError(const Error& error) override; + + private: + const raw_ref task_runner_; + + // This messenger should only be connected to one receiver, so `receiver_id_` + // should not change. + const std::string receiver_id_; + + // We keep a list here of replies we are expecting--if the reply is + // received for this sequence number, we call its respective callback, + // otherwise it is called after an internally specified timeout. + FlatMap awaiting_replies_; + + // Currently we can only set a handler for RPC messages, so no need for + // a flatmap here. + ReplyCallback rpc_callback_; + ReplyCallback input_callback_; + + WeakPtrFactory weak_factory_{this}; +}; + +// Message port interface designed for messaging to and from a sender. +class ReceiverSessionMessenger final : public SessionMessenger { + public: + using RequestCallback = + std::function; + ReceiverSessionMessenger(MessagePort& message_port, + std::string source_id, + ErrorCallback cb); + + // Set sender message handler. + void SetHandler(SenderMessage::Type type, RequestCallback cb); + void ResetHandler(SenderMessage::Type type); + + // Convenience method for sending a valid RPC message. + [[nodiscard]] Error SendRpcMessage(const std::string& source_id, + ByteView message); + + // Convenience method for sending a valid INPUT message. + [[nodiscard]] Error SendInputMessage(const std::string& source_id, + ByteView message); + + // Send a JSON message. + [[nodiscard]] Error SendMessage(const std::string& source_id, + ReceiverMessage message); + + // Send a raw string message to a custom namespace. + [[nodiscard]] Error SendMessage(std::string_view destination_id, + std::string_view message_namespace, + std::string_view message); + + using CustomMessageCallback = + std::function; + void SetCustomMessageHandler(std::string_view message_namespace, + CustomMessageCallback cb); + + // MessagePort::Client overrides + void OnMessage(const std::string& source_id, + const std::string& message_namespace, + const std::string& message) override; + void OnError(const Error& error) override; + + private: + FlatMap callbacks_; + std::vector> + custom_message_handlers_; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_PUBLIC_SESSION_MESSENGER_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/statistics.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/statistics.cc new file mode 100644 index 0000000..81667f6 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/statistics.cc @@ -0,0 +1,183 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/public/statistics.h" + +#include +#include +#include + +#include "util/enum_name_table.h" +#include "util/json/json_helpers.h" +#include "util/json/json_serialization.h" +#include "util/stringprintf.h" + +namespace openscreen::cast { + +namespace { + +template +Json::Value ToJson(const Type& t) { + return t.ToJson(); +} + +template <> +Json::Value ToJson(const double& t) { + return t; +} + +template +Json::Value ArrayToJson( + const std::array(Type::kNumTypes)>& list, + const EnumNameTable(Type::kNumTypes)>& names) { + Json::Value out; + for (size_t i = 0; i < list.size(); ++i) { + ErrorOr name = GetEnumName(names, static_cast(i)); + OSP_CHECK(name); + out[name.value()] = ToJson(list[i]); + } + return out; +} + +} // namespace + +// External linkage for unit test +extern const EnumNameTable(StatisticType::kNumTypes)> + kStatisticTypeNames = { + {{"EnqueueFps", StatisticType::kEnqueueFps}, + {"AvgCaptureLatencyMs", StatisticType::kAvgCaptureLatencyMs}, + {"AvgEncodeTimeMs", StatisticType::kAvgEncodeTimeMs}, + {"AvgQueueingLatencyMs", StatisticType::kAvgQueueingLatencyMs}, + {"AvgNetworkLatencyMs", StatisticType::kAvgNetworkLatencyMs}, + {"AvgPacketLatencyMs", StatisticType::kAvgPacketLatencyMs}, + {"AvgFrameLatencyMs", StatisticType::kAvgFrameLatencyMs}, + {"AvgEndToEndLatencyMs", StatisticType::kAvgEndToEndLatencyMs}, + {"EncodeRateKbps", StatisticType::kEncodeRateKbps}, + {"PacketTransmissionRateKbps", + StatisticType::kPacketTransmissionRateKbps}, + {"TimeSinceLastReceiverResponseMs", + StatisticType::kTimeSinceLastReceiverResponseMs}, + {"NumFramesCaptured", StatisticType::kNumFramesCaptured}, + {"NumFramesDroppedByEncoder", + StatisticType::kNumFramesDroppedByEncoder}, + {"NumLateFrames", StatisticType::kNumLateFrames}, + {"NumPacketsSent", StatisticType::kNumPacketsSent}, + {"NumPacketsReceived", StatisticType::kNumPacketsReceived}, + {"FirstEventTimeMs", StatisticType::kFirstEventTimeMs}, + {"LastEventTimeMs", StatisticType::kLastEventTimeMs}}}; + +// External linkage for unit test +extern const EnumNameTable(HistogramType::kNumTypes)> + kHistogramTypeNames = { + {{"CaptureLatencyMs", HistogramType::kCaptureLatencyMs}, + {"EncodeTimeMs", HistogramType::kEncodeTimeMs}, + {"QueueingLatencyMs", HistogramType::kQueueingLatencyMs}, + {"NetworkLatencyMs", HistogramType::kNetworkLatencyMs}, + {"PacketLatencyMs", HistogramType::kPacketLatencyMs}, + {"EndToEndLatencyMs", HistogramType::kEndToEndLatencyMs}, + {"FrameLatenessMs", HistogramType::kFrameLatenessMs}}}; + +SimpleHistogram::SimpleHistogram() = default; +SimpleHistogram::SimpleHistogram(int64_t min, int64_t max, int64_t width) + + : min(min), max(max), width(width), buckets((max - min) / width + 2) { + OSP_CHECK_GT(buckets.size(), 2u); + OSP_CHECK_EQ(0, (max - min) % width); +} + +SimpleHistogram::SimpleHistogram(const SimpleHistogram&) = default; +SimpleHistogram::SimpleHistogram(SimpleHistogram&&) noexcept = default; +SimpleHistogram& SimpleHistogram::operator=(const SimpleHistogram&) = default; +SimpleHistogram& SimpleHistogram::operator=(SimpleHistogram&&) = default; +SimpleHistogram::~SimpleHistogram() = default; + +bool SimpleHistogram::operator==(const SimpleHistogram& other) const { + return min == other.min && max == other.max && width == other.width && + buckets == other.buckets; +} + +void SimpleHistogram::Add(int64_t sample) { + if (sample < min) { + ++buckets.front(); + } else if (sample >= max) { + ++buckets.back(); + } else { + size_t index = 1 + (sample - min) / width; + OSP_CHECK_LT(index, buckets.size()); + ++buckets[index]; + } +} + +void SimpleHistogram::Reset() { + buckets.assign(buckets.size(), 0); +} + +Json::Value SimpleHistogram::ToJson() const { + // Nest the bucket values in an array instead of a dictionary, so we sort + // numerically instead of alphabetically. + Json::Value out(Json::ValueType::arrayValue); + for (size_t i = 0; i < buckets.size(); ++i) { + if (buckets[i] != 0) { + Json::Value entry; + entry[GetBucketName(i)] = buckets[i]; + out.append(entry); + } + } + return out; +} + +std::string SimpleHistogram::ToString() const { + return json::Stringify(ToJson()).value(); +} + +SimpleHistogram::SimpleHistogram(int64_t min, + int64_t max, + int64_t width, + std::vector buckets) + : SimpleHistogram(min, max, width) { + this->buckets = std::move(buckets); +} + +std::string SimpleHistogram::GetBucketName(size_t index) const { + if (index == 0) { + return "<" + std::to_string(min); + } + + if (index == buckets.size() - 1) { + return ">=" + std::to_string(max); + } + + // See the constructor comment for an example of how these bucket bounds + // are calculated. + const int bucket_min = min + width * (index - 1); + const int bucket_max = min + index * width - 1; + return StringFormat("{}-{}", bucket_min, bucket_max); +} + +Json::Value SenderStats::ToJson() const { + Json::Value out; + out["audio_statistics"] = ArrayToJson(audio_statistics, kStatisticTypeNames); + out["audio_histograms"] = ArrayToJson(audio_histograms, kHistogramTypeNames); + out["video_statistics"] = ArrayToJson(video_statistics, kStatisticTypeNames); + out["video_histograms"] = ArrayToJson(video_histograms, kHistogramTypeNames); + return out; +} + +std::string SenderStats::ToString() const { + return json::Stringify(ToJson()).value(); +} + +std::ostream& operator<<(std::ostream& out, const SenderStats& stats) { + return out << stats.ToString(); +} + +std::ostream& operator<<(std::ostream& out, const SimpleHistogram& histogram) { + return out << histogram.ToString(); +} + +SenderStatsClient::~SenderStatsClient() {} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/statistics.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/statistics.h new file mode 100644 index 0000000..660dfb1 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/statistics.h @@ -0,0 +1,195 @@ +// Copyright 2024 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_PUBLIC_STATISTICS_H_ +#define CAST_STREAMING_PUBLIC_STATISTICS_H_ + +#include +#include + +#include +#include +#include + +#include "cast/streaming/public/frame_id.h" +#include "cast/streaming/rtp_time.h" +#include "json/value.h" +#include "platform/api/time.h" + +namespace openscreen::cast { + +// This file must be updated whenever sender_stats.proto is updated. +enum class StatisticType { + // Frame enqueuing rate. + kEnqueueFps = 0, + + // Average capture latency in milliseconds. + kAvgCaptureLatencyMs, + + // Average encode duration in milliseconds. + kAvgEncodeTimeMs, + + // Duration from when a frame is encoded to when the packet is first + // sent. + kAvgQueueingLatencyMs, + + // Duration from when a packet is transmitted to when it is received. + // This measures latency from sender to receiver. + kAvgNetworkLatencyMs, + + // Duration from when a frame is encoded to when the packet is first + // received. + kAvgPacketLatencyMs, + + // Average latency between frame encoded and the moment when the frame + // is fully received. + kAvgFrameLatencyMs, + + // Duration from when a frame is captured to when it should be played out. + kAvgEndToEndLatencyMs, + + // Encode bitrate in kbps. + kEncodeRateKbps, + + // Packet transmission bitrate in kbps. + kPacketTransmissionRateKbps, + + // Duration in milliseconds since the estimated last time the receiver sent + // a response. + kTimeSinceLastReceiverResponseMs, + + // Number of frames captured. + kNumFramesCaptured, + + // Number of frames dropped by encoder. + kNumFramesDroppedByEncoder, + + // Number of late frames. + kNumLateFrames, + + // Number of packets that were sent. + kNumPacketsSent, + + // Number of packets that were received by receiver. + kNumPacketsReceived, + + // Unix time in milliseconds of first event since reset. + kFirstEventTimeMs, + + // Unix time in milliseconds of last event since reset. + kLastEventTimeMs, + + // The number of statistic types. + kNumTypes = kLastEventTimeMs + 1 +}; + +enum class HistogramType { + // Histogram representing the capture latency (in milliseconds). + kCaptureLatencyMs, + + // Histogram representing the encode time (in milliseconds). + kEncodeTimeMs, + + // Histogram representing the queueing latency (in milliseconds). + kQueueingLatencyMs, + + // Histogram representing the network latency (in milliseconds). + kNetworkLatencyMs, + + // Histogram representing the packet latency (in milliseconds). + kPacketLatencyMs, + + // Histogram representing the end to end latency (in milliseconds). + kEndToEndLatencyMs, + + // Histogram representing how late frames are (in milliseconds). + kFrameLatenessMs, + + // The number of histogram types. + kNumTypes = kFrameLatenessMs + 1 +}; + +struct SimpleHistogram { + // This will create N+2 buckets where N = (max - min) / width: + // Underflow bucket: < min + // Bucket 0: [min, min + width - 1] + // Bucket 1: [min + width, min + 2 * width - 1] + // ... + // Bucket N-1: [max - width, max - 1] + // Overflow bucket: >= max + // `min` must be less than `max`. + // `width` must divide `max - min` evenly. + SimpleHistogram(int64_t min, int64_t max, int64_t width); + + SimpleHistogram(); + SimpleHistogram(const SimpleHistogram&); + SimpleHistogram(SimpleHistogram&&) noexcept; + SimpleHistogram& operator=(const SimpleHistogram&); + SimpleHistogram& operator=(SimpleHistogram&&); + ~SimpleHistogram(); + + bool operator==(const SimpleHistogram&) const; + + void Add(int64_t sample); + void Reset(); + + Json::Value ToJson() const; + std::string ToString() const; + + int64_t min = 1; + int64_t max = 1; + int64_t width = 1; + std::vector buckets; + + private: + SimpleHistogram(int64_t min, + int64_t max, + int64_t width, + std::vector buckets); + + std::string GetBucketName(size_t index) const; +}; + +std::ostream& operator<<(std::ostream& out, const SimpleHistogram& histogram); + +struct SenderStats { + using StatisticsList = + std::array(StatisticType::kNumTypes)>; + using HistogramsList = + std::array(HistogramType::kNumTypes)>; + + // The current audio statistics. + StatisticsList audio_statistics = {}; + + // The current audio histograms. + HistogramsList audio_histograms = {}; + + // The current video statistics. + StatisticsList video_statistics = {}; + + // The current video histograms. + HistogramsList video_histograms = {}; + + Json::Value ToJson() const; + std::string ToString() const; +}; + +std::ostream& operator<<(std::ostream& out, const SenderStats& stats); + +// The consumer may provide a statistics client if they are interested in +// getting statistics about the ongoing session. +class SenderStatsClient { + public: + // Gets called regularly with updated statistics while they are being + // generated. + virtual void OnStatisticsUpdated(const SenderStats& updated_stats) = 0; + + protected: + virtual ~SenderStatsClient(); +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_PUBLIC_STATISTICS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/resolution.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/resolution.cc new file mode 100644 index 0000000..8727f84 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/resolution.cc @@ -0,0 +1,141 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/resolution.h" + +#include + +#include "cast/streaming/message_fields.h" +#include "platform/base/error.h" +#include "util/json/json_helpers.h" +#include "util/osp_logging.h" + +namespace openscreen::cast { + +namespace { + +/// Dimension properties. +// Width in pixels. +constexpr char kWidth[] = "width"; + +// Height in pixels. +constexpr char kHeight[] = "height"; + +// Frame rate as a rational decimal number or fraction. +// E.g. 30 and "3000/1001" are both valid representations. +constexpr char kFrameRate[] = "frameRate"; + +// Choice of epsilon for double comparison allows for proper comparison +// for both aspect ratios and frame rates. For frame rates, it is based on the +// broadcast rate of 29.97fps, which is actually 29.976. For aspect ratios, it +// allows for a one-pixel difference at a 4K resolution, we want it to be +// relatively high to avoid false negative comparison results. +bool FrameRateEquals(double a, double b) { + const double kEpsilonForFrameRateComparisons = .0001; + return std::abs(a - b) < kEpsilonForFrameRateComparisons; +} + +} // namespace + +ErrorOr Resolution::TryParse(const Json::Value& root) { + if (!root.isObject()) { + return Error(Error::Code::kJsonParseError, + "Resolution is not a JSON object"); + } + + Resolution out; + if (!json::TryParseInt(root[kWidth], &out.width) || + !json::TryParseInt(root[kHeight], &out.height)) { + return Error(Error::Code::kJsonParseError, "Invalid resolution"); + } + if (!out.IsValid()) { + return Error(Error::Code::kJsonParseError, "Invalid resolution values"); + } + return out; +} + +bool Resolution::IsValid() const { + return width > 0 && height > 0; +} + +Json::Value Resolution::ToJson() const { + OSP_CHECK(IsValid()); + Json::Value root; + root[kWidth] = width; + root[kHeight] = height; + + return root; +} + +bool Resolution::operator==(const Resolution& other) const { + return std::tie(width, height) == std::tie(other.width, other.height); +} + +bool Resolution::operator!=(const Resolution& other) const { + return !(*this == other); +} + +bool Resolution::IsSupersetOf(const Resolution& other) const { + return width >= other.width && height >= other.height; +} + +ErrorOr Dimensions::TryParse(const Json::Value& root) { + if (!root.isObject()) { + return Error(Error::Code::kJsonParseError, + "Dimensions is not a JSON object"); + } + + Dimensions out; + if (!json::TryParseInt(root[kWidth], &out.width) || + !json::TryParseInt(root[kHeight], &out.height)) { + return Error(Error::Code::kJsonParseError, "Invalid dimensions"); + } + + if (!root[kFrameRate].isNull()) { + if (!json::TryParseSimpleFraction(root[kFrameRate], &out.frame_rate)) { + return Error(Error::Code::kJsonParseError, "Invalid frame rate"); + } + } + + if (!out.IsValid()) { + return Error(Error::Code::kJsonParseError, "Invalid dimensions values"); + } + return out; +} + +bool Dimensions::IsValid() const { + return width > 0 && height > 0 && frame_rate.is_positive(); +} + +Json::Value Dimensions::ToJson() const { + OSP_CHECK(IsValid()); + Json::Value root; + root[kWidth] = width; + root[kHeight] = height; + root[kFrameRate] = frame_rate.ToString(); + + return root; +} + +bool Dimensions::operator==(const Dimensions& other) const { + return (std::tie(width, height) == std::tie(other.width, other.height) && + FrameRateEquals(static_cast(frame_rate), + static_cast(other.frame_rate))); +} + +bool Dimensions::operator!=(const Dimensions& other) const { + return !(*this == other); +} + +bool Dimensions::IsSupersetOf(const Dimensions& other) const { + if (static_cast(frame_rate) != + static_cast(other.frame_rate)) { + return static_cast(frame_rate) >= + static_cast(other.frame_rate); + } + + return ToResolution().IsSupersetOf(other.ToResolution()); +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/resolution.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/resolution.h new file mode 100644 index 0000000..038d82f --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/resolution.h @@ -0,0 +1,67 @@ +// Copyright 2021 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Resolutions and dimensions (resolutions with a frame rate) are used +// extensively throughout cast streaming. Since their serialization to and +// from JSON is stable and standard, we have a single place definition for +// these for use both in our public APIs and private messages. + +#ifndef CAST_STREAMING_RESOLUTION_H_ +#define CAST_STREAMING_RESOLUTION_H_ + +#include "json/value.h" +#include "util/simple_fraction.h" + +namespace openscreen::cast { + +// A resolution in pixels. +struct Resolution { + static ErrorOr TryParse(const Json::Value& value); + bool IsValid() const; + Json::Value ToJson() const; + + // Returns true if both `width` and `height` of this instance are greater than + // or equal to that of `other`. + bool IsSupersetOf(const Resolution& other) const; + + bool operator==(const Resolution& other) const; + bool operator!=(const Resolution& other) const; + + // Width and height in pixels. + int width = 0; + int height = 0; +}; + +// A resolution in pixels and a frame rate. +struct Dimensions { + static ErrorOr TryParse(const Json::Value& value); + bool IsValid() const; + Json::Value ToJson() const; + + // Returns true if all properties of this instance are greater than or equal + // to those of `other`. + bool IsSupersetOf(const Dimensions& other) const; + + bool operator==(const Dimensions& other) const; + bool operator!=(const Dimensions& other) const; + + // Get just the width and height fields (for comparisons). + constexpr Resolution ToResolution() const { return {width, height}; } + + // The effective bit rate is the width * height * frame rate. + constexpr int effective_bit_rate() const { + return width * height * static_cast(frame_rate); + } + + // Width and height in pixels. + int width = 0; + int height = 0; + + // `frame_rate` is the maximum maintainable frame rate. + SimpleFraction frame_rate{0, 1}; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_RESOLUTION_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/rtp_time.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/rtp_time.cc new file mode 100644 index 0000000..459c7c9 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/rtp_time.cc @@ -0,0 +1,23 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/rtp_time.h" + +#include + +namespace openscreen::cast { + +std::ostream& operator<<(std::ostream& out, const RtpTimeDelta rhs) { + if (rhs.value_ >= 0) + out << "RTP+"; + else + out << "RTP"; + return out << rhs.value_; +} + +std::ostream& operator<<(std::ostream& out, const RtpTimeTicks rhs) { + return out << "RTP@" << rhs.value_; +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/rtp_time.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/rtp_time.h new file mode 100644 index 0000000..40d61a4 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/rtp_time.h @@ -0,0 +1,257 @@ +// Copyright 2015 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_RTP_TIME_H_ +#define CAST_STREAMING_RTP_TIME_H_ + +#include + +#include +#include +#include +#include +#include + +#include "cast/streaming/impl/expanded_value_base.h" +#include "platform/api/time.h" +#include "util/saturate_cast.h" + +namespace openscreen::cast { + +// Forward declarations (see below). +class RtpTimeDelta; +class RtpTimeTicks; + +// Convenience operator overloads for logging. +std::ostream& operator<<(std::ostream& out, const RtpTimeDelta rhs); +std::ostream& operator<<(std::ostream& out, const RtpTimeTicks rhs); + +// The difference between two RtpTimeTicks values. This data type is modeled +// off of Chromium's base::TimeDelta, and used for performing compiler-checked +// arithmetic with RtpTimeTicks. +// +// This data type wraps a value, providing only the meaningful set of math +// operations that may be performed on the value. RtpTimeDeltas may be +// added/subtracted with other RtpTimeDeltas to produce a RtpTimeDelta holding +// the sum/difference. RtpTimeDeltas may also be multiplied or divided by +// integer amounts. Finally, RtpTimeDeltas may be divided by other +// RtpTimeDeltas to compute a number of periods (trunc'ed to an integer), or +// modulo each other to determine a time period remainder. +// +// The base class provides bit truncation/extension features for +// wire-formatting, and also the comparison operators. +// +// Usage example: +// +// // Time math. +// RtpTimeDelta zero; +// RtpTimeDelta one_second_later = +// zero + RtpTimeDelta::FromTicks(kAudioSamplingRate); +// RtpTimeDelta ten_seconds_later = one_second_later * 10; +// int64_t ten_periods = ten_seconds_later / one_second_later; +// +// // Logging convenience. +// OSP_DLOG_INFO << "The RTP time offset is " << ten_seconds_later; +// +// // Convert (approximately!) between RTP timebase and microsecond timebase: +// RtpTimeDelta nine_seconds_in_rtp = ten_seconds_later - one_second_later; +// using std::chrono::microseconds; +// microseconds nine_seconds_duration = +// nine_seconds_in_rtp.ToDuration(kAudioSamplingRate); +// RtpTimeDelta two_seconds_in_rtp = +// RtpTimeDelta::FromDuration(std::chrono::seconds(2), +// kAudioSamplingRate); +class RtpTimeDelta : public ExpandedValueBase { + public: + constexpr RtpTimeDelta() : ExpandedValueBase(0) {} + + // Arithmetic operators (with other deltas). + constexpr RtpTimeDelta operator+(RtpTimeDelta rhs) const { + return RtpTimeDelta(value_ + rhs.value_); + } + constexpr RtpTimeDelta operator-(RtpTimeDelta rhs) const { + return RtpTimeDelta(value_ - rhs.value_); + } + constexpr RtpTimeDelta& operator+=(RtpTimeDelta rhs) { + return (*this = (*this + rhs)); + } + constexpr RtpTimeDelta& operator-=(RtpTimeDelta rhs) { + return (*this = (*this - rhs)); + } + constexpr RtpTimeDelta operator-() const { return RtpTimeDelta(-value_); } + + // Multiplicative operators (with other deltas). + constexpr int64_t operator/(RtpTimeDelta rhs) const { + return value_ / rhs.value_; + } + constexpr RtpTimeDelta operator%(RtpTimeDelta rhs) const { + return RtpTimeDelta(value_ % rhs.value_); + } + constexpr RtpTimeDelta& operator%=(RtpTimeDelta rhs) { + return (*this = (*this % rhs)); + } + + // Multiplicative operators (with integer types). + template + constexpr RtpTimeDelta operator*(IntType rhs) const { + static_assert(std::numeric_limits::is_integer, + "|rhs| must be a POD integer type"); + return RtpTimeDelta(value_ * rhs); + } + template + constexpr RtpTimeDelta operator/(IntType rhs) const { + static_assert(std::numeric_limits::is_integer, + "|rhs| must be a POD integer type"); + return RtpTimeDelta(value_ / rhs); + } + template + constexpr RtpTimeDelta& operator*=(IntType rhs) { + return (*this = (*this * rhs)); + } + template + constexpr RtpTimeDelta& operator/=(IntType rhs) { + return (*this = (*this / rhs)); + } + + // Maps this RtpTimeDelta to an approximate std::chrono::duration using the + // given RTP timebase. Assumes a zero-valued Duration corresponds to a + // zero-valued RtpTimeDelta. + template + Duration ToDuration(int rtp_timebase) const { + OSP_CHECK_GT(rtp_timebase, 0); + constexpr Duration kOneSecond = + std::chrono::duration_cast(std::chrono::seconds(1)); + return Duration(ToNearestRepresentativeValue( + static_cast(value_) / rtp_timebase * kOneSecond.count())); + } + + // Maps the `duration` to an approximate RtpTimeDelta using the given RTP + // timebase. Assumes a zero-valued Duration corresponds to a zero-valued + // RtpTimeDelta. + template + static constexpr RtpTimeDelta FromDuration(Duration duration, + int rtp_timebase) { + constexpr Duration kOneSecond = + std::chrono::duration_cast(std::chrono::seconds(1)); + static_assert(kOneSecond > Duration::zero(), + "Duration is too coarse-grained to represent one second."); + return RtpTimeDelta(ToNearestRepresentativeValue( + static_cast(duration.count()) / kOneSecond.count() * + rtp_timebase)); + } + + // Construct a RtpTimeDelta from an exact number of ticks. + static constexpr RtpTimeDelta FromTicks(int64_t ticks) { + return RtpTimeDelta(ticks); + } + + private: + friend class ExpandedValueBase; + friend class RtpTimeTicks; + friend std::ostream& operator<<(std::ostream& out, const RtpTimeDelta rhs); + + constexpr explicit RtpTimeDelta(int64_t ticks) : ExpandedValueBase(ticks) {} + + constexpr int64_t value() const { return value_; } + + template + static std::enable_if_t::value, Rep> + ToNearestRepresentativeValue(double ticks) { + return Rep(ticks); + } + + template + static std::enable_if_t::value, Rep> + ToNearestRepresentativeValue(double ticks) { + return rounded_saturate_cast(ticks); + } +}; + +// A media timestamp whose timebase matches the periodicity of the content +// (e.g., for audio, the timebase would be the sampling frequency). This data +// type is modeled off of Chromium's base::TimeTicks. +// +// This data type wraps a value, providing only the meaningful set of math +// operations that may be performed on the value. The difference between two +// RtpTimeTicks is a RtpTimeDelta. Likewise, adding or subtracting a +// RtpTimeTicks with a RtpTimeDelta produces an off-set RtpTimeTicks. +// +// The base class provides bit truncation/extension features for +// wire-formatting, and also the comparison operators. +// +// Usage example: +// +// // Time math. +// RtpTimeTicks origin; +// RtpTimeTicks at_one_second = +// origin + RtpTimeDelta::FromTicks(kAudioSamplingRate); +// RtpTimeTicks at_two_seconds = +// at_one_second + RtpTimeDelta::FromTicks(kAudioSamplingRate); +// RtpTimeDelta elasped_in_between = at_two_seconds - at_one_second; +// RtpTimeDelta thrice_as_much_elasped = elasped_in_between * 3; +// RtpTimeTicks at_four_seconds = at_one_second + thrice_as_much_elasped; +// +// // Logging convenience. +// OSP_DLOG_INFO << "The RTP timestamp is " << at_four_seconds; +// +// // Convert (approximately!) between RTP timebase and stream time offsets in +// // microsecond timebase: +// using std::chrono::microseconds; +// microseconds four_seconds_since_stream_start = +// at_four_seconds.ToTimeSinceOrigin(kAudioSamplingRate); +// RtpTimeTicks at_three_seconds = RtpTimeDelta::FromTimeSinceOrigin( +// std::chrono::seconds(3), kAudioSamplingRate); +class RtpTimeTicks : public ExpandedValueBase { + public: + constexpr explicit RtpTimeTicks(int64_t value) : ExpandedValueBase(value) {} + constexpr RtpTimeTicks() : ExpandedValueBase(0) {} + + constexpr int64_t value() const { return value_; } + + // Compute the difference between two RtpTimeTickses. + constexpr RtpTimeDelta operator-(RtpTimeTicks rhs) const { + return RtpTimeDelta(value_ - rhs.value_); + } + + // Return a new RtpTimeTicks before or after this one. + constexpr RtpTimeTicks operator+(RtpTimeDelta rhs) const { + return RtpTimeTicks(value_ + rhs.value()); + } + constexpr RtpTimeTicks operator-(RtpTimeDelta rhs) const { + return RtpTimeTicks(value_ - rhs.value()); + } + constexpr RtpTimeTicks& operator+=(RtpTimeDelta rhs) { + return (*this = (*this + rhs)); + } + constexpr RtpTimeTicks& operator-=(RtpTimeDelta rhs) { + return (*this = (*this - rhs)); + } + + // Maps this RtpTimeTicks to an approximate std::chrono::duration representing + // the amount of time since the origin point (e.g., the start of a stream) + // using the given `rtp_timebase`. Assumes a zero-valued Duration corresponds + // to a zero-valued RtpTimeTicks. + template + Duration ToTimeSinceOrigin(int rtp_timebase) const { + return (*this - RtpTimeTicks()).ToDuration(rtp_timebase); + } + + // Maps the `time_since_origin` to an approximate RtpTimeTicks using the given + // RTP timebase. Assumes a zero-valued Duration corresponds to a zero-valued + // RtpTimeTicks. + template + static constexpr RtpTimeTicks FromTimeSinceOrigin(Duration time_since_origin, + int rtp_timebase) { + return RtpTimeTicks() + + RtpTimeDelta::FromDuration(time_since_origin, rtp_timebase); + } + + private: + friend class ExpandedValueBase; + friend std::ostream& operator<<(std::ostream& out, const RtpTimeTicks rhs); +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_RTP_TIME_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_message.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_message.cc new file mode 100644 index 0000000..1494af6 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_message.cc @@ -0,0 +1,128 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/sender_message.h" + +#include +#include + +#include "cast/streaming/message_fields.h" +#include "util/base64.h" +#include "util/enum_name_table.h" +#include "util/json/json_helpers.h" +#include "util/json/json_serialization.h" +#include "util/string_util.h" + +namespace openscreen::cast { + +namespace { + +EnumNameTable kMessageTypeNames{ + {{kMessageTypeOffer, SenderMessage::Type::kOffer}, + {"GET_CAPABILITIES", SenderMessage::Type::kGetCapabilities}, + {"RPC", SenderMessage::Type::kRpc}, + {"INPUT", SenderMessage::Type::kInput}}}; + +SenderMessage::Type GetMessageType(const Json::Value& root) { + std::string type; + if (!json::TryParseString(root[kMessageType], &type)) { + return SenderMessage::Type::kUnknown; + } + string_util::AsciiStrToUpper(type); + ErrorOr parsed = GetEnum(kMessageTypeNames, type); + + return parsed.value(SenderMessage::Type::kUnknown); +} + +} // namespace + +// static +ErrorOr SenderMessage::Parse(const Json::Value& value) { + if (!value.isObject()) { + return Error(Error::Code::kParameterInvalid, + "SenderMessage body is not a JSON object"); + } + + SenderMessage message; + if (!json::TryParseInt(value[kSequenceNumber], &(message.sequence_number))) { + message.sequence_number = -1; + } + + message.type = GetMessageType(value); + switch (message.type) { + case Type::kOffer: { + auto offer_or_error = Offer::TryParse(value[kOfferMessageBody]); + if (offer_or_error.is_value()) { + message.body = std::move(offer_or_error.value()); + message.valid = true; + } + } break; + + case Type::kRpc: { + std::string rpc_body; + std::vector rpc; + if (json::TryParseString(value[kRpcMessageBody], &rpc_body) && + base64::Decode(rpc_body, &rpc)) { + message.body = rpc; + message.valid = true; + } + } break; + + case Type::kInput: { + std::string input_body; + std::vector input; + if (json::TryParseString(value[kInputMessageBody], &input_body) && + base64::Decode(input_body, &input)) { + message.body = input; + message.valid = true; + } + } break; + + case Type::kGetCapabilities: + message.valid = true; + break; + + default: + break; + } + + return message; +} + +ErrorOr SenderMessage::ToJson() const { + OSP_CHECK(type != SenderMessage::Type::kUnknown) + << "Trying to send an unknown message is a developer error"; + + Json::Value root; + ErrorOr message_type = GetEnumName(kMessageTypeNames, type); + root[kMessageType] = message_type.value(); + if (sequence_number >= 0) { + root[kSequenceNumber] = sequence_number; + } + + switch (type) { + case SenderMessage::Type::kOffer: + root[kOfferMessageBody] = std::get(body).ToJson(); + break; + + case SenderMessage::Type::kRpc: + root[kRpcMessageBody] = + base64::Encode(std::get>(body)); + break; + + case SenderMessage::Type::kInput: + root[kInputMessageBody] = + base64::Encode(std::get>(body)); + break; + + case SenderMessage::Type::kGetCapabilities: + break; + + default: + OSP_NOTREACHED(); + } + return root; +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_message.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_message.h new file mode 100644 index 0000000..24a779b --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_message.h @@ -0,0 +1,55 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_SENDER_MESSAGE_H_ +#define CAST_STREAMING_SENDER_MESSAGE_H_ + +#include +#include +#include +#include + +#include "cast/streaming/public/offer_messages.h" +#include "json/value.h" +#include "platform/base/error.h" +#include "util/osp_logging.h" + +namespace openscreen::cast { + +struct SenderMessage { + public: + // Receiver response message type. + enum class Type { + // Unknown message type. + kUnknown, + + // OFFER request message. + kOffer, + + // GET_CAPABILITIES request message. + kGetCapabilities, + + // Rpc binary messages. The payload is base64-encoded. + kRpc, + + // Input-related binary messages. The payload is base64-encoded. + kInput, + }; + + static ErrorOr Parse(const Json::Value& value); + ErrorOr ToJson() const; + + Type type = Type::kUnknown; + int32_t sequence_number = -1; + bool valid = false; + std::variant, // Binary-encoded protobuf message. + Offer, + std::string> + body; +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_SENDER_MESSAGE_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_packet_router.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_packet_router.cc new file mode 100644 index 0000000..fab9928 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_packet_router.cc @@ -0,0 +1,273 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/sender_packet_router.h" + +#include +#include + +#include "cast/streaming/impl/packet_util.h" +#include "cast/streaming/public/constants.h" +#include "platform/base/span.h" +#include "util/chrono_helpers.h" +#include "util/osp_logging.h" +#include "util/saturate_cast.h" +#include "util/stringprintf.h" + +namespace openscreen::cast { + +using clock_operators::operator<<; + +SenderPacketRouter::SenderPacketRouter(Environment& environment, + int max_burst_bitrate) + : SenderPacketRouter( + environment, + ComputeMaxPacketsPerBurst(max_burst_bitrate, + environment.GetMaxPacketSize(), + kDefaultBurstInterval), + kDefaultBurstInterval) {} + +SenderPacketRouter::SenderPacketRouter(Environment& environment, + int max_packets_per_burst, + milliseconds burst_interval) + : BandwidthEstimator(max_packets_per_burst, + burst_interval, + environment.now()), + environment_(environment), + packet_buffer_size_(environment.GetMaxPacketSize()), + packet_buffer_(new uint8_t[packet_buffer_size_]), + max_packets_per_burst_(max_packets_per_burst), + burst_interval_(burst_interval), + max_burst_bitrate_(ComputeMaxBurstBitrate(packet_buffer_size_, + max_packets_per_burst_, + burst_interval_)), + alarm_(environment_->now_function(), environment_->task_runner()) { + OSP_CHECK_GT(packet_buffer_size_, kRequiredNetworkPacketSize); +} + +SenderPacketRouter::~SenderPacketRouter() { + OSP_CHECK(senders_.empty()); +} + +void SenderPacketRouter::OnSenderCreated(Ssrc receiver_ssrc, Sender* sender) { + OSP_CHECK(FindEntry(receiver_ssrc) == senders_.end()); + senders_.push_back(SenderEntry{receiver_ssrc, sender, kNever, kNever}); + + if (senders_.size() == 1) { + environment_->ConsumeIncomingPackets(this); + } else { + // Sort the list of Senders so that they are iterated in priority order. + std::sort(senders_.begin(), senders_.end()); + } +} + +void SenderPacketRouter::OnSenderDestroyed(Ssrc receiver_ssrc) { + const auto it = FindEntry(receiver_ssrc); + OSP_CHECK(it != senders_.end()); + senders_.erase(it); + + // If there are no longer any Senders, suspend receiving RTCP packets. + if (senders_.empty()) { + environment_->DropIncomingPackets(); + } +} + +void SenderPacketRouter::RequestRtcpSend(Ssrc receiver_ssrc) { + const auto it = FindEntry(receiver_ssrc); + OSP_CHECK(it != senders_.end()); + it->next_rtcp_send_time = Alarm::kImmediately; + ScheduleNextBurst(); +} + +void SenderPacketRouter::RequestRtpSend(Ssrc receiver_ssrc) { + const auto it = FindEntry(receiver_ssrc); + OSP_CHECK(it != senders_.end()); + it->next_rtp_send_time = Alarm::kImmediately; + ScheduleNextBurst(); +} + +void SenderPacketRouter::OnReceivedPacket(const IPEndpoint& source, + Clock::time_point arrival_time, + std::vector packet) { + // If the packet did not come from the expected endpoint, ignore it. + OSP_CHECK_NE(source.port, uint16_t{0}); + if (source != environment_->remote_endpoint()) { + return; + } + + // Determine which Sender to dispatch the packet to. Senders may only receive + // RTCP packets from Receivers. Log a warning containing a pretty-printed dump + // if the packet is not an RTCP packet. + const std::pair seems_like = + InspectPacketForRouting(packet); + if (seems_like.first != ApparentPacketType::RTCP) { + constexpr int kMaxPartiaHexDumpSize = 96; + const std::size_t encode_size = + std::min(packet.size(), static_cast(kMaxPartiaHexDumpSize)); + OSP_LOG_WARN << "UNKNOWN packet of " << packet.size() + << " bytes. Partial hex dump: " + << HexEncode(packet.data(), encode_size); + return; + } + const auto it = FindEntry(seems_like.second); + if (it != senders_.end()) { + it->sender->OnReceivedRtcpPacket(arrival_time, std::move(packet)); + } +} + +SenderPacketRouter::SenderEntries::iterator SenderPacketRouter::FindEntry( + Ssrc receiver_ssrc) { + return std::find_if(senders_.begin(), senders_.end(), + [receiver_ssrc](const SenderEntry& entry) { + return entry.receiver_ssrc == receiver_ssrc; + }); +} + +void SenderPacketRouter::ScheduleNextBurst() { + // Determine the next burst time by scanning for the earliest of the + // next-scheduled send times for each Sender. + const Clock::time_point earliest_allowed_burst_time = + last_burst_time_ + burst_interval_; + Clock::time_point next_burst_time = kNever; + for (const SenderEntry& entry : senders_) { + const auto next_send_time = + std::min(entry.next_rtcp_send_time, entry.next_rtp_send_time); + if (next_send_time >= next_burst_time) { + continue; + } + if (next_send_time <= earliest_allowed_burst_time) { + next_burst_time = earliest_allowed_burst_time; + // No need to continue, since `next_burst_time` cannot become any earlier. + break; + } + next_burst_time = next_send_time; + } + + // Schedule the alarm for the next burst time unless none of the Senders has + // anything to send. + if (next_burst_time == kNever) { + alarm_.Cancel(); + } else { + alarm_.Schedule([this] { SendBurstOfPackets(); }, next_burst_time); + } +} + +void SenderPacketRouter::SendBurstOfPackets() { + // Treat RTCP packets as "critical priority," and so there is no upper limit + // on the number to send. Practically, this will always be limited by the + // number of Senders; so, this won't be a huge number of packets. + const Clock::time_point burst_time = environment_->now(); + const int num_rtcp_packets_sent = SendJustTheRtcpPackets(burst_time); + // Now send all the RTP packets, up to the maximum number allowed in a burst. + // Higher priority Senders' RTP packets are sent first. + const int num_rtp_packets_sent = SendJustTheRtpPackets( + burst_time, max_packets_per_burst_ - num_rtcp_packets_sent); + last_burst_time_ = burst_time; + + BandwidthEstimator::OnBurstComplete( + num_rtcp_packets_sent + num_rtp_packets_sent, burst_time); + + ScheduleNextBurst(); +} + +int SenderPacketRouter::SendJustTheRtcpPackets(Clock::time_point send_time) { + int num_sent = 0; + for (SenderEntry& entry : senders_) { + if (entry.next_rtcp_send_time > send_time) { + continue; + } + + // Note: Only one RTCP packet is sent from the same Sender in the same + // burst. This is because RTCP packets are supposed to always contain the + // most up-to-date Sender state. Having multiple RTCP packets in the same + // burst would mean that all but the last one are old/irrelevant snapshots + // of Sender state, and this would just thrash/confuse the Receiver. + const ByteBuffer packet = entry.sender->GetRtcpPacketForImmediateSend( + send_time, ByteBuffer(packet_buffer_.get(), packet_buffer_size_)); + if (!packet.empty()) { + environment_->SendPacket( + ByteView(packet.data(), packet.size()), + PacketMetadata{.stream_type = entry.sender->GetStreamType(), + .rtp_timestamp = entry.sender->GetLastRtpTimestamp()}); + entry.next_rtcp_send_time = send_time + kRtcpReportInterval; + ++num_sent; + } + } + + return num_sent; +} + +int SenderPacketRouter::SendJustTheRtpPackets(Clock::time_point send_time, + int num_packets_to_send) { + int num_sent = 0; + for (SenderEntry& entry : senders_) { + if (num_sent >= num_packets_to_send) { + break; + } + if (entry.next_rtp_send_time > send_time) { + continue; + } + + for (; num_sent < num_packets_to_send; ++num_sent) { + const ByteBuffer packet = entry.sender->GetRtpPacketForImmediateSend( + send_time, ByteBuffer(packet_buffer_.get(), packet_buffer_size_)); + if (packet.empty()) { + break; + } + environment_->SendPacket( + ByteView(packet.data(), packet.size()), + PacketMetadata{.stream_type = entry.sender->GetStreamType(), + .rtp_timestamp = entry.sender->GetLastRtpTimestamp()}); + } + entry.next_rtp_send_time = entry.sender->GetRtpResumeTime(); + } + + return num_sent; +} + +namespace { +constexpr int kBitsPerByte = 8; +constexpr auto kOneSecondInMilliseconds = to_milliseconds(seconds(1)); +} // namespace + +// static +int SenderPacketRouter::ComputeMaxPacketsPerBurst(int max_burst_bitrate, + int packet_size, + milliseconds burst_interval) { + OSP_CHECK_GT(max_burst_bitrate, 0); + OSP_CHECK_GT(packet_size, 0); + OSP_CHECK_GT(burst_interval, milliseconds(0)); + OSP_CHECK_LE(burst_interval, kOneSecondInMilliseconds); + + const int max_packets_per_second = + max_burst_bitrate / kBitsPerByte / packet_size; + const int bursts_per_second = kOneSecondInMilliseconds / burst_interval; + return std::max(max_packets_per_second / bursts_per_second, 1); +} + +// static +int SenderPacketRouter::ComputeMaxBurstBitrate(int packet_size, + int max_packets_per_burst, + milliseconds burst_interval) { + OSP_CHECK_GT(packet_size, 0); + OSP_CHECK_GT(max_packets_per_burst, 0); + OSP_CHECK_GT(burst_interval, milliseconds(0)); + OSP_CHECK_LE(burst_interval, kOneSecondInMilliseconds); + + const int64_t max_bits_per_burst = + int64_t{packet_size} * kBitsPerByte * max_packets_per_burst; + const int bursts_per_second = kOneSecondInMilliseconds / burst_interval; + return saturate_cast(max_bits_per_burst * bursts_per_second); +} + +SenderPacketRouter::Sender::~Sender() = default; + +// static +constexpr int SenderPacketRouter::kDefaultMaxBurstBitrate; +// static +constexpr milliseconds SenderPacketRouter::kDefaultBurstInterval; +// static +constexpr Clock::time_point SenderPacketRouter::kNever; + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_packet_router.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_packet_router.h new file mode 100644 index 0000000..e89a8e0 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/sender_packet_router.h @@ -0,0 +1,203 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_SENDER_PACKET_ROUTER_H_ +#define CAST_STREAMING_SENDER_PACKET_ROUTER_H_ + +#include + +#include +#include +#include + +#include "cast/streaming/impl/bandwidth_estimator.h" +#include "cast/streaming/public/constants.h" +#include "cast/streaming/public/environment.h" +#include "cast/streaming/ssrc.h" +#include "platform/api/time.h" +#include "platform/base/span.h" +#include "util/alarm.h" +#include "util/raw_ptr.h" +#include "util/raw_ref.h" + +namespace openscreen::cast { + +// Manages network packet transmission for one or more Senders, directing each +// inbound packet to a specific Sender instance, pacing the transmission of +// outbound packets, and employing network bandwidth/availability monitoring and +// congestion control. +// +// Instead of just sending packets whenever they want, Senders must request +// transmission from the SenderPacketRouter. The router then calls-back to each +// Sender, in the near future, when it has allocated an available time slice for +// transmission. The Sender is allowed to decide, at that exact moment, which +// packet most needs to be sent. +// +// Pacing strategy: Packets are sent in bursts. This allows the platform +// (operating system) to collect many small packets into a short-term buffer, +// which allows for optimizations at the link layer. For example, multiple +// packets can be sent together as one larger transmission unit, and this can be +// critical for good performance over shared-medium networks (such as 802.11 +// WiFi). https://en.wikipedia.org/wiki/Frame-bursting +class SenderPacketRouter : public BandwidthEstimator, + public Environment::PacketConsumer { + public: + class Sender { + public: + // Called to provide the Sender with what looks like a RTCP packet meant for + // it specifically (among other Senders) to process. `arrival_time` + // indicates when the packet arrived (i.e., when it was received from the + // platform). + virtual void OnReceivedRtcpPacket(Clock::time_point arrival_time, + ByteView packet) = 0; + + // Populates the given `buffer` with a RTCP/RTP packet that will be sent + // immediately. Returns the portion of `buffer` contaning the packet, or an + // empty Span if nothing is ready to send. + virtual ByteBuffer GetRtcpPacketForImmediateSend( + Clock::time_point send_time, + ByteBuffer buffer) = 0; + virtual ByteBuffer GetRtpPacketForImmediateSend(Clock::time_point send_time, + ByteBuffer buffer) = 0; + + // Returns the point-in-time at which RTP sending should resume, or kNever + // if it should be suspended until an explicit call to RequestRtpSend(). The + // implementation may return a value on or before "now" to indicate an + // immediate resume is desired. + virtual Clock::time_point GetRtpResumeTime() = 0; + + // Returns the last logged RTP timestamp, for use in expanding truncated + // packet RTP timestamps for metrics purposes. + virtual RtpTimeTicks GetLastRtpTimestamp() const = 0; + + // Returns the type of stream that this sender is providing. + virtual StreamType GetStreamType() const = 0; + + protected: + virtual ~Sender(); + }; + + // Constructs an instance with default burst parameters appropriate for the + // given `max_burst_bitrate`. + explicit SenderPacketRouter(Environment& environment, + int max_burst_bitrate = kDefaultMaxBurstBitrate); + + // Constructs an instance with specific burst parameters. The maximum bitrate + // will be computed based on these (and Environment::GetMaxPacketSize()). + SenderPacketRouter(Environment& environment, + int max_packets_per_burst, + std::chrono::milliseconds burst_interval); + + ~SenderPacketRouter(); + + int max_packet_size() const { return packet_buffer_size_; } + int max_burst_bitrate() const { return max_burst_bitrate_; } + + // Called from a Sender constructor/destructor to register/deregister a Sender + // instance that processes RTP/RTCP packets from a Receiver having the given + // SSRC. + void OnSenderCreated(Ssrc receiver_ssrc, Sender* client); + void OnSenderDestroyed(Ssrc receiver_ssrc); + + // Requests an immediate send of a RTCP packet, and then RTCP sending will + // repeat at regular intervals (see kRtcpSendInterval) until the Sender is + // de-registered. + void RequestRtcpSend(Ssrc receiver_ssrc); + + // Requests an immediate send of a RTP packet. RTP sending will continue until + // the Sender stops providing packet data. + // + // See also: Sender::GetRtpResumeTime(). + void RequestRtpSend(Ssrc receiver_ssrc); + + // A reasonable default maximum bitrate for bursting. Congestion control + // should always be employed to limit the Senders' sustained/average outbound + // data volume for "fair" use of the network. + static constexpr int kDefaultMaxBurstBitrate = 24 << 20; // 24 megabits/sec + + // The minimum amount of time between burst-sends. The methodology by which + // this value was determined is lost knowledge, but is likely the result of + // experimentation with various network and operating system configurations. + // This value came from the original Chrome Cast Streaming implementation. + static constexpr std::chrono::milliseconds kDefaultBurstInterval{10}; + + // A special time_point value representing "never." + static constexpr Clock::time_point kNever = Clock::time_point::max(); + + private: + struct SenderEntry { + Ssrc receiver_ssrc; + raw_ptr sender; + Clock::time_point next_rtcp_send_time; + Clock::time_point next_rtp_send_time; + + // Entries are ordered by the transmission priority (high→low), as implied + // by their SSRC. See ssrc.h for details. + bool operator<(const SenderEntry& other) const { + return ComparePriority(receiver_ssrc, other.receiver_ssrc) < 0; + } + }; + + using SenderEntries = std::vector; + + // Environment::PacketConsumer implementation. + void OnReceivedPacket(const IPEndpoint& source, + Clock::time_point arrival_time, + std::vector packet) final; + + // Helper to return an iterator pointing to the entry corresponding to the + // given `receiver_ssrc`, or "end" if not found. + SenderEntries::iterator FindEntry(Ssrc receiver_ssrc); + + // Examine the next send time for all Senders, and decide whether to schedule + // a burst-send. + void ScheduleNextBurst(); + + // Performs a burst-send of packets. This is called whenever the Alarm fires. + void SendBurstOfPackets(); + + // Send an RTCP packet from each Sender that has one ready, and return the + // number of packets sent. + int SendJustTheRtcpPackets(Clock::time_point send_time); + + // Send zero or more RTP packets from each Sender, up to a maximum of + // `num_packets_to_send`, and return the number of packets sent. + int SendJustTheRtpPackets(Clock::time_point send_time, + int num_packets_to_send); + + // Returns the maximum number of packets to send in one burst, based on the + // given parameters. + static int ComputeMaxPacketsPerBurst( + int max_burst_bitrate, + int packet_size, + std::chrono::milliseconds burst_interval); + + // Returns the maximum bitrate inferred by the given parameters. + static int ComputeMaxBurstBitrate(int packet_size, + int max_packets_per_burst, + std::chrono::milliseconds burst_interval); + + const raw_ref environment_; + const int packet_buffer_size_; + const std::unique_ptr packet_buffer_; + const int max_packets_per_burst_; + const std::chrono::milliseconds burst_interval_; + const int max_burst_bitrate_; + + // Schedules the task that calls back into this SenderPacketRouter at a later + // time to send the next burst of packets. + Alarm alarm_; + + // The current list of Senders and their timing information. This is + // maintained in order of the priority implied by the Sender SSRC's. + SenderEntries senders_; + + // The last time a burst of packets was sent. This is used to determine the + // next burst time. + Clock::time_point last_burst_time_ = Clock::time_point::min(); +}; + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_SENDER_PACKET_ROUTER_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/ssrc.cc b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/ssrc.cc new file mode 100644 index 0000000..6b9bee9 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/ssrc.cc @@ -0,0 +1,42 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "cast/streaming/ssrc.h" + +#include + +#include "platform/api/time.h" + +namespace openscreen::cast { + +namespace { + +// These ranges are arbitrary, but have been used for several years (in prior +// implementations of Cast Streaming). +constexpr int kHigherPriorityMin = 1; +constexpr int kHigherPriorityMax = 50000; +constexpr int kNormalPriorityMin = 50001; +constexpr int kNormalPriorityMax = 100000; + +} // namespace + +Ssrc GenerateSsrc(bool higher_priority) { + // Use a statically-allocated generator, instantiated upon first use, and + // seeded with the current time tick count. This generator was chosen because + // it is light-weight and does not need to produce unguessable (nor + // crypto-secure) values. + static std::minstd_rand generator(static_cast( + Clock::now().time_since_epoch().count())); + + std::uniform_int_distribution distribution( + higher_priority ? kHigherPriorityMin : kNormalPriorityMin, + higher_priority ? kHigherPriorityMax : kNormalPriorityMax); + return static_cast(distribution(generator)); +} + +int ComparePriority(Ssrc ssrc_a, Ssrc ssrc_b) { + return static_cast(ssrc_a) - static_cast(ssrc_b); +} + +} // namespace openscreen::cast diff --git a/breadcast-caststream-sys/vendor/openscreen/cast/streaming/ssrc.h b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/ssrc.h new file mode 100644 index 0000000..874b5e7 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/cast/streaming/ssrc.h @@ -0,0 +1,37 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CAST_STREAMING_SSRC_H_ +#define CAST_STREAMING_SSRC_H_ + +#include + +namespace openscreen::cast { + +// A Synchronization Source is a 32-bit opaque identifier used in RTP packets +// for identifying the source (or recipient) of a logical sequence of encoded +// audio/video frames. In other words, an audio stream will have one sender SSRC +// and a video stream will have a different sender SSRC. +using Ssrc = uint32_t; + +// The "not set" or "null" value for the Ssrc type. +inline constexpr Ssrc kNullSsrc = 0; + +// Computes a new SSRC that will be used to uniquely identify an RTP stream. The +// `higher_priority` argument, if true, will generate an SSRC that causes the +// system to use a higher priority when scheduling data transmission. Generally, +// this is set to true for audio streams and false for video streams. +Ssrc GenerateSsrc(bool higher_priority); + +// Returns a value indicating how to prioritize data transmission for a stream +// with `ssrc_a` versus a stream with `ssrc_b`: +// +// ret < 0: Stream `ssrc_a` has higher priority. +// ret == 0: Equal priority. +// ret > 0: Stream `ssrc_b` has higher priority. +int ComparePriority(Ssrc ssrc_a, Ssrc ssrc_b); + +} // namespace openscreen::cast + +#endif // CAST_STREAMING_SSRC_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/patches/compat_shims.cc b/breadcast-caststream-sys/vendor/openscreen/patches/compat_shims.cc new file mode 100644 index 0000000..2f30c60 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/patches/compat_shims.cc @@ -0,0 +1,26 @@ +#include "patches/compat_shims.h" + +void AES_ctr128_encrypt(const unsigned char* in, + unsigned char* out, + size_t length, + const AES_KEY* key, + unsigned char ivec[AES_BLOCK_SIZE], + unsigned char ecount_buf[AES_BLOCK_SIZE], + unsigned int* num) { + unsigned int n = *num; + size_t l = 0; + while (l < length) { + if (n == 0) { + AES_encrypt(ivec, ecount_buf, key); + for (int i = AES_BLOCK_SIZE - 1; i >= 0; --i) { + if (++ivec[i]) { + break; + } + } + } + out[l] = static_cast(in[l] ^ ecount_buf[n]); + ++l; + n = (n + 1) % AES_BLOCK_SIZE; + } + *num = n; +} diff --git a/breadcast-caststream-sys/vendor/openscreen/patches/compat_shims.h b/breadcast-caststream-sys/vendor/openscreen/patches/compat_shims.h new file mode 100644 index 0000000..ab42e8e --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/patches/compat_shims.h @@ -0,0 +1,35 @@ +// Force-included (via -include, see ../build.rs) into every translation +// unit in this vendored openscreen subset. Papers over a few places where +// upstream code either relies on a BoringSSL-only API that system OpenSSL +// doesn't expose, or was written assuming an include that some other header +// in a full Chromium checkout happens to pull in transitively. Not part of +// upstream openscreen -- see ../PATCHES.md. +#ifndef PATCHES_COMPAT_SHIMS_H_ +#define PATCHES_COMPAT_SHIMS_H_ + +// cast/streaming/impl/frame_crypto.cc uses strlen-family functions without +// including itself. +#include + +#include + +// CRYPTO_library_init() was an OpenSSL 1.0.x-era macro/no-op that BoringSSL +// still defines for source compatibility; system OpenSSL 3.x has no such +// symbol (initialization there is automatic). frame_crypto.cc's call to it +// is a no-op on any OpenSSL version this new, so it's shimmed out entirely. +#define CRYPTO_library_init() ((void)0) + +// AES_ctr128_encrypt() is a BoringSSL convenience wrapper around AES-CTR +// that system OpenSSL's public headers don't expose. Implemented in +// compat_shims.cc using the standard CTR-mode algorithm (NIST SP 800-38A, +// big-endian 128-bit counter block) over AES_encrypt(), which OpenSSL does +// still expose. +extern "C" void AES_ctr128_encrypt(const unsigned char* in, + unsigned char* out, + size_t length, + const AES_KEY* key, + unsigned char ivec[AES_BLOCK_SIZE], + unsigned char ecount_buf[AES_BLOCK_SIZE], + unsigned int* num); + +#endif // PATCHES_COMPAT_SHIMS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/connection.h b/breadcast-caststream-sys/vendor/openscreen/platform/api/connection.h new file mode 100644 index 0000000..6789a6d --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/connection.h @@ -0,0 +1,54 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_API_CONNECTION_H_ +#define PLATFORM_API_CONNECTION_H_ + +#include +#include + +#include "platform/base/error.h" +#include "platform/base/ip_address.h" +#include "platform/base/span.h" + +namespace openscreen { + +// Represents a connection between two endpoints. This class provides an +// interface for sending and receiving byte data over a connection. +class Connection { + public: + // Client callbacks are run via the TaskRunner used by TlsConnectionFactory. + class Client { + public: + // Called when `connection` experiences an error, such as a read error. + virtual void OnError(Connection* connection, const Error& error) = 0; + + // Called when a `block` arrives on `connection`. + virtual void OnRead(Connection* connection, std::vector block) = 0; + + protected: + virtual ~Client() = default; + }; + + virtual ~Connection() = default; + + // Sets the Client associated with this instance. This should be called as + // soon as the factory provides a new Connection instance via + // TlsConnectionFactory::OnAccepted(), OnConnected() or CreateSocket(). + // Pass nullptr to unset the Client. + virtual void SetClient(Client* client) = 0; + + // Sends a message. Returns true iff the message will be sent. + [[nodiscard]] virtual bool Send(ByteView data) = 0; + + // Get the connected remote address. + virtual IPEndpoint GetRemoteEndpoint() const = 0; + + protected: + Connection() = default; +}; + +} // namespace openscreen + +#endif // PLATFORM_API_CONNECTION_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/export.h b/breadcast-caststream-sys/vendor/openscreen/platform/api/export.h new file mode 100644 index 0000000..50b0f6f --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/export.h @@ -0,0 +1,26 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_API_EXPORT_H_ +#define PLATFORM_API_EXPORT_H_ + +#if defined(WIN32) + +#if defined(OPENSCREEN_SHARED_IMPLEMENTATION) +#define OPENSCREEN_EXPORT __declspec(dllexport) +#else +#define OPENSCREEN_EXPORT __declspec(dllimport) +#endif // defined(OPENSCREEN_SHARED_IMPLEMENTATION) + +#else + +#if defined(OPENSCREEN_SHARED_IMPLEMENTATION) +#define OPENSCREEN_EXPORT __attribute__((visibility("default"))) +#else +#define OPENSCREEN_EXPORT +#endif // defined(OPENSCREEN_SHARED_IMPLEMENTATION) + +#endif // defined(WIN32) + +#endif // PLATFORM_API_EXPORT_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/logging.h b/breadcast-caststream-sys/vendor/openscreen/platform/api/logging.h new file mode 100644 index 0000000..0f2cac0 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/logging.h @@ -0,0 +1,62 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_API_LOGGING_H_ +#define PLATFORM_API_LOGGING_H_ + +#include + +namespace openscreen { + +enum class LogLevel { + // Very detailed information, often used for evaluating performance or + // debugging production issues in-the-wild. + kVerbose = 0, + + // Used occasionally to note events of interest, but not for indicating any + // problems. This is also used for general console messaging in Open Screen's + // standalone executables. + kInfo = 1, + + // Indicates a problem that may or may not lead to an operational failure. + kWarning = 2, + + // Indicates an operational failure that may or may not cause a component to + // stop working. + kError = 3, + + // Indicates a logic flaw, corruption, impossible/unanticipated situation, or + // operational failure so serious that Open Screen will soon call Break() to + // abort the current process. Examples: security/privacy risks, memory + // management issues, API contract violations. + kFatal = 4, +}; + +// Returns true if `level` is at or above the level where the embedder will +// record/emit log entries from the code in `file`. +bool IsLoggingOn(LogLevel level, const std::string_view file); + +// Record a log entry, consisting of its logging level, location and message. +// The embedder may filter-out entries according to its own policy, but this +// function will not be called if IsLoggingOn(level, file) returns false. +// Whenever `level` is kFatal, Open Screen will call Break() immediately after +// this returns. +// +// `message` is passed as a string stream to avoid unnecessary string copies. +// Embedders can call its rdbuf() or str() methods to access the log message. +void LogWithLevel(LogLevel level, + const char* file, + int line, + std::stringstream message); + +// Breaks into the debugger, if one is present. Otherwise, aborts the current +// process (i.e., this function should not return). In production builds, an +// embedder could invoke its infrastructure for performing "dumps," consisting +// of thread stack traces and other relevant process state information, before +// aborting the process. +[[noreturn]] void Break(); + +} // namespace openscreen + +#endif // PLATFORM_API_LOGGING_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/network_interface.h b/breadcast-caststream-sys/vendor/openscreen/platform/api/network_interface.h new file mode 100644 index 0000000..5a4d29e --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/network_interface.h @@ -0,0 +1,24 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_API_NETWORK_INTERFACE_H_ +#define PLATFORM_API_NETWORK_INTERFACE_H_ + +#include + +#include "platform/base/interface_info.h" + +namespace openscreen { + +// Returns an InterfaceInfo for each currently active network interface on the +// system. No two entries in this vector can have the same NetworkInterfaceIndex +// value. +// +// This can return an empty vector if there are no active network interfaces or +// an error occurred querying the system for them. +std::vector GetNetworkInterfaces(); + +} // namespace openscreen + +#endif // PLATFORM_API_NETWORK_INTERFACE_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/task_runner.h b/breadcast-caststream-sys/vendor/openscreen/platform/api/task_runner.h new file mode 100644 index 0000000..bf439fc --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/task_runner.h @@ -0,0 +1,64 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_API_TASK_RUNNER_H_ +#define PLATFORM_API_TASK_RUNNER_H_ + +#include +#include + +#include "platform/api/time.h" + +namespace openscreen { + +// A thread-safe API surface that allows for posting tasks. The underlying +// implementation may be single or multi-threaded, and all complication should +// be handled by the implementation class. The implementation must guarantee: +// (1) Tasks shall not overlap in time/CPU. +// (2) Tasks shall run sequentially, e.g. posting task A then B implies +// that A shall run before B. +// (3) If task A is posted before task B, then any mutation in A happens-before +// B runs (even if A and B run on different threads). +class TaskRunner { + public: + using Task = std::packaged_task; + + virtual ~TaskRunner() = default; + + // Takes any callable target (function, lambda-expression, std::bind result, + // etc.) that should be run at the first convenient time. + template + inline void PostTask(Functor f) { + PostPackagedTask(Task(std::move(f))); + } + + // Takes any callable target (function, lambda-expression, std::bind result, + // etc.) that should be run no sooner than `delay` time from now. Note that + // the Task might run after an additional delay, especially under heavier + // system load. There is no deadline concept. + template + inline void PostTaskWithDelay(Functor f, Clock::duration delay) { + PostPackagedTaskWithDelay(Task(std::move(f)), delay); + } + + // Implementations should provide the behavior explained in the comments above + // for PostTask[WithDelay](). Client code may also call these directly when + // passing an existing Task object. + virtual void PostPackagedTask(Task task) = 0; + virtual void PostPackagedTaskWithDelay(Task task, Clock::duration delay) = 0; + + // Return true if the calling thread is the thread that task runner is using + // to run tasks, false otherwise. + virtual bool IsRunningOnTaskRunner() = 0; + + // Posts a task to delete `object`. + template + void DeleteSoon(const T* object) { + PostTask([object] { delete static_cast(object); }); + } +}; + +} // namespace openscreen + +#endif // PLATFORM_API_TASK_RUNNER_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/task_runner_deleter.cc b/breadcast-caststream-sys/vendor/openscreen/platform/api/task_runner_deleter.cc new file mode 100644 index 0000000..d02137d --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/task_runner_deleter.cc @@ -0,0 +1,23 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/api/task_runner_deleter.h" + +namespace openscreen { + +TaskRunnerDeleter::TaskRunnerDeleter() = default; + +TaskRunnerDeleter::TaskRunnerDeleter(TaskRunner& task_runner) + : task_runner_(&task_runner) {} + +TaskRunnerDeleter::~TaskRunnerDeleter() = default; + +TaskRunnerDeleter::TaskRunnerDeleter(const TaskRunnerDeleter&) = default; +TaskRunnerDeleter& TaskRunnerDeleter::operator=(const TaskRunnerDeleter&) = + default; +TaskRunnerDeleter::TaskRunnerDeleter(TaskRunnerDeleter&&) noexcept = default; +TaskRunnerDeleter& TaskRunnerDeleter::operator=(TaskRunnerDeleter&&) noexcept = + default; + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/task_runner_deleter.h b/breadcast-caststream-sys/vendor/openscreen/platform/api/task_runner_deleter.h new file mode 100644 index 0000000..5aedce8 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/task_runner_deleter.h @@ -0,0 +1,64 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_API_TASK_RUNNER_DELETER_H_ +#define PLATFORM_API_TASK_RUNNER_DELETER_H_ + +#include +#include + +#include "platform/api/task_runner.h" + +namespace openscreen { + +// Helper that deletes an object on the provided TaskRunner. +// +// Usage with std::unique_ptr: +// +// std::unique_ptr some_foo; +// ... +// some_foo = TaskRunnerDeleter::MakeUnique( +// task_runner, foo_arg1, foo_arg2, ...); +struct TaskRunnerDeleter { + TaskRunnerDeleter(); + explicit TaskRunnerDeleter(TaskRunner& task_runner); + ~TaskRunnerDeleter(); + + TaskRunnerDeleter(const TaskRunnerDeleter&); + TaskRunnerDeleter& operator=(const TaskRunnerDeleter&); + TaskRunnerDeleter(TaskRunnerDeleter&&) noexcept; + TaskRunnerDeleter& operator=(TaskRunnerDeleter&&) noexcept; + + // For compatibility with std:: deleters. + template + void operator()(const T* ptr) { + if (task_runner_ && ptr) + task_runner_->DeleteSoon(ptr); + } + + template + static std::unique_ptr WrapUnique(TaskRunner& task_runner, + Type* t) { + return std::unique_ptr(t, TaskRunnerDeleter(task_runner)); + } + + template + static std::unique_ptr MakeUnique(TaskRunner& task_runner, + Args&&... args) { + return std::unique_ptr( + new Type(std::forward(args)...), + TaskRunnerDeleter(task_runner)); // NOLINT + } + +#if defined(__clang__) + [[clang::annotate("raw_ptr_exclusion")]] +#endif + TaskRunner* task_runner_ = nullptr; +}; + +} // namespace openscreen + +#endif // PLATFORM_API_TASK_RUNNER_DELETER_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/time.h b/breadcast-caststream-sys/vendor/openscreen/platform/api/time.h new file mode 100644 index 0000000..2f67715 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/time.h @@ -0,0 +1,37 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_API_TIME_H_ +#define PLATFORM_API_TIME_H_ + +#include + +#include "platform/base/trivial_clock_traits.h" + +namespace openscreen { + +// The "reasonably high-resolution" source of monotonic time from the embedder, +// exhibiting the traits described in TrivialClockTraits. This class is not +// instantiated. It only contains a static now() function. +// +// For example, the default platform implementation bases this on +// std::chrono::steady_clock or std::chrono::high_resolution_clock, but an +// embedder may choose to use a different source of time (e.g., the embedder's +// time library, a simulated time source, or a mock). +class Clock : public TrivialClockTraits { + public: + // Returns the current time. + static time_point now() noexcept; +}; + +// Returns the number of seconds since UNIX epoch (1 Jan 1970, midnight) +// according to the wall clock, which is subject to adjustments (e.g., via NTP). +// Note that this is NOT necessarily the same time source as Clock::now() above, +// and is NOT guaranteed to be monotonically non-decreasing; it is "calendar +// time." +std::chrono::seconds GetWallTimeSinceUnixEpoch() noexcept; + +} // namespace openscreen + +#endif // PLATFORM_API_TIME_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection.cc b/breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection.cc new file mode 100644 index 0000000..5404298 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection.cc @@ -0,0 +1,12 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/api/tls_connection.h" + +namespace openscreen { + +TlsConnection::TlsConnection() = default; + + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection.h b/breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection.h new file mode 100644 index 0000000..f749644 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection.h @@ -0,0 +1,29 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_API_TLS_CONNECTION_H_ +#define PLATFORM_API_TLS_CONNECTION_H_ + +#include +#include + +#include "platform/api/connection.h" +#include "platform/base/error.h" +#include "platform/base/ip_address.h" +#include "platform/base/span.h" + +namespace openscreen { + +class TlsConnection : public Connection { + public: + // Get the connected remote address. + virtual IPEndpoint GetRemoteEndpoint() const = 0; + + protected: + TlsConnection(); +}; + +} // namespace openscreen + +#endif // PLATFORM_API_TLS_CONNECTION_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection_factory.cc b/breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection_factory.cc new file mode 100644 index 0000000..3457a85 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection_factory.cc @@ -0,0 +1,14 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/api/tls_connection_factory.h" + +namespace openscreen { + +TlsConnectionFactory::TlsConnectionFactory() = default; +TlsConnectionFactory::~TlsConnectionFactory() = default; + +TlsConnectionFactory::Client::~Client() = default; + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection_factory.h b/breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection_factory.h new file mode 100644 index 0000000..0efda5b --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/tls_connection_factory.h @@ -0,0 +1,82 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_API_TLS_CONNECTION_FACTORY_H_ +#define PLATFORM_API_TLS_CONNECTION_FACTORY_H_ + +#include + +#include +#include + +#include "platform/base/ip_address.h" + +namespace openscreen { + +class TaskRunner; +class TlsConnection; +struct TlsConnectOptions; +struct TlsCredentials; +struct TlsListenOptions; + +// We expect a single factory to be able to handle an arbitrary number of +// calls using the same client and task runner. +class TlsConnectionFactory { + public: + // Client callbacks are ran on the provided TaskRunner. + class Client { + public: + // Provides a new `connection` that resulted from listening on the local + // socket. `der_x509_peer_cert` is the DER-encoded X509 certificate from the + // peer if present, or empty if the peer didn't provide one. + virtual void OnAccepted(TlsConnectionFactory* factory, + std::vector der_x509_peer_cert, + std::unique_ptr connection) = 0; + + // Provides a new `connection` that resulted from connecting to a remote + // endpoint. `der_x509_peer_cert` is the DER-encoded X509 certificate from + // the peer. + virtual void OnConnected(TlsConnectionFactory* factory, + std::vector der_x509_peer_cert, + std::unique_ptr connection) = 0; + + virtual void OnConnectionFailed(TlsConnectionFactory* factory, + const IPEndpoint& remote_address) = 0; + + // Called when a non-recoverable error occurs. + virtual void OnError(TlsConnectionFactory* factory, const Error& error) = 0; + + protected: + virtual ~Client(); + }; + + // The connection factory requires a client for yielding creation results + // asynchronously, as well as a task runner it can use to for running + // callbacks both on the factory and on created TlsConnection instances. + static std::unique_ptr CreateFactory( + Client& client, + TaskRunner& task_runner); + + virtual ~TlsConnectionFactory(); + + // Fires an OnConnected or OnConnectionFailed event. + virtual void Connect(const IPEndpoint& remote_address, + const TlsConnectOptions& options) = 0; + + // Set the TlsCredentials used for listening for new connections. Currently, + // having different certificates on different address is not supported. This + // must be called before the first call to Listen. + virtual void SetListenCredentials(const TlsCredentials& credentials) = 0; + + // Fires an OnAccepted or OnConnectionFailed event. + virtual void Listen(const IPEndpoint& local_address, + const TlsListenOptions& options) = 0; + + protected: + TlsConnectionFactory(); +}; + +} // namespace openscreen + +#endif // PLATFORM_API_TLS_CONNECTION_FACTORY_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/trace_event.cc b/breadcast-caststream-sys/vendor/openscreen/platform/api/trace_event.cc new file mode 100644 index 0000000..a30ccfc --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/trace_event.cc @@ -0,0 +1,61 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/api/trace_event.h" + +#include + +namespace openscreen { + +TraceEvent::TraceEvent(TraceCategory category, + Clock::time_point start_time, + const char* name, + const char* file_name, + uint32_t line_number) + : category(category), + start_time(start_time), + name(name), + file_name(file_name), + line_number(line_number) {} + +TraceEvent::TraceEvent() = default; +TraceEvent::TraceEvent(TraceEvent&&) noexcept = default; +TraceEvent::TraceEvent(const TraceEvent&) = default; +TraceEvent& TraceEvent::operator=(TraceEvent&&) = default; +TraceEvent& TraceEvent::operator=(const TraceEvent&) = default; +TraceEvent::~TraceEvent() = default; + +std::string TraceEvent::ToString() const { + std::ostringstream oss; + + oss << ids << " " << openscreen::ToString(category) << "::" << name << " <" + << file_name << ":" << line_number << ">"; + + // We only support two arguments in total. + if (!arguments.empty()) { + oss << " { " << arguments[0].first << ": " << arguments[0].second; + if (arguments.size() > 1) { + oss << ", " << arguments[1].first << ": " << arguments[1].second; + } + oss << " }"; + } + return oss.str(); +} + +void TraceEvent::TruncateStrings() { + for (auto& argument : arguments) { + if (argument.second.size() > kMaxStringLength) { + argument.second.resize(kMaxStringLength); + // Populate last three digits with ellipses to indicate that + // we truncated this string. + argument.second.replace(kMaxStringLength - 3, 3, "..."); + } + } +} + +std::ostream& operator<<(std::ostream& out, const TraceEvent& event) { + return out << event.ToString(); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/trace_event.h b/breadcast-caststream-sys/vendor/openscreen/platform/api/trace_event.h new file mode 100644 index 0000000..c9ce318 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/trace_event.h @@ -0,0 +1,75 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_API_TRACE_EVENT_H_ +#define PLATFORM_API_TRACE_EVENT_H_ + +#include +#include +#include + +#include "platform/api/time.h" +#include "platform/base/error.h" +#include "platform/base/trace_logging_activation.h" +#include "platform/base/trace_logging_types.h" + +namespace openscreen { + +// A collection of common properties of trace events. +struct TraceEvent { + // Constructor with only the required fields. + TraceEvent(TraceCategory category, + Clock::time_point start_time, + const char* name, + const char* file_name, + uint32_t line_number); + + TraceEvent(); + TraceEvent(TraceEvent&&) noexcept; + TraceEvent(const TraceEvent&); + TraceEvent& operator=(TraceEvent&&); + TraceEvent& operator=(const TraceEvent&); + ~TraceEvent(); + + std::string ToString() const; + + // May be called to truncate all std::strings on this object. + static const size_t kMaxStringLength = 1024; + void TruncateStrings(); + + // The category of this event. + TraceCategory category; + + // Timestamp for when the event was created. + Clock::time_point start_time; + + // Name of this operation. + const char* name = nullptr; + + // Name of the file the log was generated in. + const char* file_name = nullptr; + + // Line number the log was generated on. + uint32_t line_number = 0; + + // The trace ids of this event and its ancestors. + TraceIdHierarchy ids; + + // Flow IDs associated with this event. + std::vector flow_ids; + + // Optional result of the trace event. + Error::Code result = Error::Code::kNone; + + // Optional list of arguments. May contain 0, 1, or 2 arguments. + // Excess arguments will remain unused. + using Argument = std::pair; + std::vector arguments; +}; + +std::ostream& operator<<(std::ostream& out, const TraceEvent& event); + +} // namespace openscreen + +#endif // PLATFORM_API_TRACE_EVENT_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/trace_logging_platform.cc b/breadcast-caststream-sys/vendor/openscreen/platform/api/trace_logging_platform.cc new file mode 100644 index 0000000..c062fbb --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/trace_logging_platform.cc @@ -0,0 +1,13 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/api/trace_logging_platform.h" + +namespace openscreen { + +TraceLoggingPlatform::~TraceLoggingPlatform() = default; + +void TraceLoggingPlatform::LogFlow(TraceEvent event, FlowType type) {} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/trace_logging_platform.h b/breadcast-caststream-sys/vendor/openscreen/platform/api/trace_logging_platform.h new file mode 100644 index 0000000..5aae8a7 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/trace_logging_platform.h @@ -0,0 +1,52 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_API_TRACE_LOGGING_PLATFORM_H_ +#define PLATFORM_API_TRACE_LOGGING_PLATFORM_H_ + +#include +#include +#include + +#include "platform/api/time.h" +#include "platform/api/trace_event.h" +#include "platform/base/error.h" +#include "platform/base/trace_logging_activation.h" +#include "platform/base/trace_logging_types.h" + +namespace openscreen { + +// Optional platform API to support logging trace events from Open Screen. To +// use this, implement the TraceLoggingPlatform interface and call +// StartTracing() and StopTracing() to turn tracing on/off (see +// platform/base/trace_logging_activation.h). +// +// All methods must be thread-safe and re-entrant. +class TraceLoggingPlatform { + public: + virtual ~TraceLoggingPlatform(); + + // Determines whether trace logging is enabled for the given category. Note + // that if any categories are supported, this function should return "true" + // when called with TraceCategory::kAny. + virtual bool IsTraceLoggingEnabled(TraceCategory category) = 0; + + // Log a synchronous trace. + virtual void LogTrace(TraceEvent event, Clock::time_point end_time) = 0; + + // Log an asynchronous trace start. + virtual void LogAsyncStart(TraceEvent event) = 0; + + // Log an asynchronous trace end. + virtual void LogAsyncEnd(TraceEvent event) = 0; + + // Log a flow event. + // TODO(crbug.com/479316209): fast-follow: make non-optional once implemented + // in Chromium. + virtual void LogFlow(TraceEvent event, FlowType type); +}; + +} // namespace openscreen + +#endif // PLATFORM_API_TRACE_LOGGING_PLATFORM_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/udp_socket.cc b/breadcast-caststream-sys/vendor/openscreen/platform/api/udp_socket.cc new file mode 100644 index 0000000..c82005d --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/udp_socket.cc @@ -0,0 +1,14 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/api/udp_socket.h" + +namespace openscreen { + +UdpSocket::UdpSocket() = default; +UdpSocket::~UdpSocket() = default; + +UdpSocket::Client::~Client() = default; + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/api/udp_socket.h b/breadcast-caststream-sys/vendor/openscreen/platform/api/udp_socket.h new file mode 100644 index 0000000..a3f4773 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/api/udp_socket.h @@ -0,0 +1,140 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_API_UDP_SOCKET_H_ +#define PLATFORM_API_UDP_SOCKET_H_ + +#include // size_t +#include // uint8_t + +#include + +#include "platform/api/network_interface.h" +#include "platform/base/error.h" +#include "platform/base/ip_address.h" +#include "platform/base/span.h" +#include "platform/base/udp_packet.h" + +namespace openscreen { + +class TaskRunner; + +// An open UDP socket for sending/receiving datagrams to/from either specific +// endpoints or over IP multicast. +// +// Usage: The socket is created and opened by calling the Create() method. This +// returns a unique pointer that auto-closes/destroys the socket when it goes +// out-of-scope. +class UdpSocket { + public: + // Client for the UdpSocket class. + class Client { + public: + // Method called when the UDP socket is bound. Default implementation + // does nothing, as clients may not care about the socket bind state. + virtual void OnBound(UdpSocket* socket) {} + + // Method called on socket configuration operations when an error occurs. + // These specific APIs are: + // UdpSocket::Bind() + // UdpSocket::SetMulticastOutboundInterface(...) + // UdpSocket::JoinMulticastGroup(...) + // UdpSocket::SetDscp(...) + virtual void OnError(UdpSocket* socket, const Error& error) = 0; + + // Method called when an error occurs during a SendMessage call. + virtual void OnSendError(UdpSocket* socket, const Error& error) = 0; + + // Method called when a packet is read. + virtual void OnRead(UdpSocket* socket, ErrorOr packet) = 0; + + protected: + virtual ~Client(); + }; + + // Common, modern code points for use with DSCP. This list is non-inclusive, + // callers are encouraged to check validity of an integer code point by + // ensuring it is in the bounds of [kBestEffort, kMaxValue] inclusive. + // https://www.rfc-editor.org/rfc/rfc2474.html + enum class DscpMode : uint8_t { + // Best-effort, no differentiated treatment. + kBestEffort = 0, + + // Assured Forwarding code points. + // https://datatracker.ietf.org/doc/html/rfc2597#section-6 + kAF11 = 10, + kAF12 = 12, + kAF13 = 14, + kAF21 = 18, + kAF22 = 20, + kAF23 = 22, + kAF31 = 26, + kAF32 = 28, + kAF33 = 30, + kAF41 = 34, + kAF42 = 36, + kAF43 = 38, + + // Expedited Forwarding (EF) code point. + // https://www.rfc-editor.org/rfc/rfc3246.html + kEF = 46, + + // As a 6-bit value, DSCP ranges from [0, 63] inclusive. + kMaxValue = 63, + }; + + using Version = IPAddress::Version; + + // Creates a new, scoped UdpSocket within the IPv4 or IPv6 family. + // `local_endpoint` may be zero (see comments for Bind()). This method must be + // defined in the platform-level implementation. All `client` methods called + // will be queued on the provided `task_runner`. For this reason, the provided + // TaskRunner and Client must exist for the duration of the created socket's + // lifetime. + static ErrorOr> Create( + TaskRunner& task_runner, + Client* client, + const IPEndpoint& local_endpoint); + + virtual ~UdpSocket(); + + // Returns true if `socket` belongs to the IPv4/IPv6 address family. + virtual bool IsIPv4() const = 0; + virtual bool IsIPv6() const = 0; + + // Returns the current local endpoint's address and port. Initially, this will + // be the same as the value that was passed into Create(). However, it can + // later change after certain operations, such as Bind(), are executed. + virtual IPEndpoint GetLocalEndpoint() const = 0; + + // Binds to the address specified in the constructor. If the local endpoint's + // address is zero, the operating system will bind to all interfaces. If the + // local endpoint's port is zero, the operating system will automatically find + // a free local port and bind to it. Future calls to GetLocalEndpoint() will + // reflect the resolved port. + virtual void Bind() = 0; + + // Sets the device to use for outgoing multicast packets on the socket. + virtual void SetMulticastOutboundInterface(NetworkInterfaceIndex ifindex) = 0; + + // Joins to the multicast group at the given address, using the specified + // interface. + virtual void JoinMulticastGroup(const IPAddress& address, + NetworkInterfaceIndex ifindex) = 0; + + // Sends a message. If the message is not sent, Client::OnSendError() will be + // called to indicate this. Error::Code::kAgain indicates the operation would + // block, which can be expected during normal operation. + virtual void SendMessage(ByteView data, const IPEndpoint& dest) = 0; + + // Sets the DSCP value to use for all messages sent from this socket. + virtual void SetDscp(DscpMode mode) = 0; + + protected: + UdpSocket(); +}; + +} // namespace openscreen + +#endif // PLATFORM_API_UDP_SOCKET_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/compiler_specific.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/compiler_specific.h new file mode 100644 index 0000000..0c8b76a --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/compiler_specific.h @@ -0,0 +1,20 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_COMPILER_SPECIFIC_H_ +#define PLATFORM_BASE_COMPILER_SPECIFIC_H_ + +#ifdef NOINLINE +#define OSP_NOINLINE NOINLINE +#elif __has_cpp_attribute(clang::noinline) +#define OSP_NOINLINE [[clang::noinline]] +#elif __has_cpp_attribute(gnu::noinline) +#define OSP_NOINLINE [[gnu::noinline]] +#elif __has_cpp_attribute(msvc::noinline) +#define OSP_NOINLINE [[msvc::noinline]] +#else +#define OSP_NOINLINE __attribute__((noinline)) +#endif + +#endif // PLATFORM_BASE_COMPILER_SPECIFIC_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/error.cc b/breadcast-caststream-sys/vendor/openscreen/platform/base/error.cc new file mode 100644 index 0000000..f92ce98 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/error.cc @@ -0,0 +1,306 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/base/error.h" + +#include + +namespace openscreen { + +Error::Error() = default; + +Error::Error(const Error& error) = default; + +Error::Error(Error&& error) noexcept = default; + +Error::Error(Code code) : code_(code) {} + +Error::Error(Code code, const std::string& message) + : code_(code), message_(message) {} + +Error::Error(Code code, std::string&& message) + : code_(code), message_(std::move(message)) {} + +Error::~Error() = default; + +Error& Error::operator=(const Error& other) = default; + +Error& Error::operator=(Error&& other) = default; + +bool Error::operator==(const Error& other) const { + return code_ == other.code_ && message_ == other.message_; +} + +bool Error::operator!=(const Error& other) const { + return !(*this == other); +} + +bool Error::operator==(Code code) const { + return code_ == code; +} + +bool Error::operator!=(Code code) const { + return !(*this == code); +} + +std::ostream& operator<<(std::ostream& os, const Error::Code& code) { + if (code == Error::Code::kNone) { + return os << "Success"; + } + os << "Failure: "; + switch (code) { + case Error::Code::kAgain: + return os << "Transient"; + case Error::Code::kCborParsing: + return os << "CborParsing"; + case Error::Code::kCborEncoding: + return os << "CborEncoding"; + case Error::Code::kCborIncompleteMessage: + return os << "CborIncompleteMessage"; + case Error::Code::kCborInvalidMessage: + return os << "CborInvalidMessage"; + case Error::Code::kCborInvalidResponseId: + return os << "CborInvalidResponseId"; + case Error::Code::kNoAvailableReceivers: + return os << "NoAvailableReceivers"; + case Error::Code::kRequestCancelled: + return os << "RequestCancelled"; + case Error::Code::kNoPresentationFound: + return os << "NoPresentationFound"; + case Error::Code::kPreviousStartInProgress: + return os << "PreviousStartInProgress"; + case Error::Code::kUnknownStartError: + return os << "UnknownStartError"; + case Error::Code::kUnknownRequestId: + return os << "UnknownRequestId"; + case Error::Code::kAddressInUse: + return os << "AddressInUse"; + case Error::Code::kDomainNameTooLong: + return os << "DomainNameTooLong"; + case Error::Code::kDomainNameLabelTooLong: + return os << "DomainNameLabelTooLong"; + case Error::Code::kIOFailure: + return os << "IOFailure"; + case Error::Code::kInitializationFailure: + return os << "InitializationFailure"; + case Error::Code::kInvalidIPV4Address: + return os << "InvalidIPV4Address"; + case Error::Code::kInvalidIPV6Address: + return os << "InvalidIPV6Address"; + case Error::Code::kConnectionFailed: + return os << "ConnectionFailed"; + case Error::Code::kSocketOptionSettingFailure: + return os << "SocketOptionSettingFailure"; + case Error::Code::kSocketAcceptFailure: + return os << "SocketAcceptFailure"; + case Error::Code::kSocketBindFailure: + return os << "SocketBindFailure"; + case Error::Code::kSocketClosedFailure: + return os << "SocketClosedFailure"; + case Error::Code::kSocketConnectFailure: + return os << "SocketConnectFailure"; + case Error::Code::kSocketInvalidState: + return os << "SocketInvalidState"; + case Error::Code::kSocketListenFailure: + return os << "SocketListenFailure"; + case Error::Code::kSocketReadFailure: + return os << "SocketReadFailure"; + case Error::Code::kSocketSendFailure: + return os << "SocketSendFailure"; + case Error::Code::kMdnsRegisterFailure: + return os << "MdnsRegisterFailure"; + case Error::Code::kMdnsReadFailure: + return os << "MdnsReadFailure"; + case Error::Code::kMdnsNonConformingFailure: + return os << "kMdnsNonConformingFailure"; + case Error::Code::kParseError: + return os << "ParseError"; + case Error::Code::kUnknownMessageType: + return os << "UnknownMessageType"; + case Error::Code::kNoActiveConnection: + return os << "NoActiveConnection"; + case Error::Code::kAlreadyClosed: + return os << "AlreadyClosed"; + case Error::Code::kNoStartedPresentation: + return os << "NoStartedPresentation"; + case Error::Code::kPresentationAlreadyStarted: + return os << "PresentationAlreadyStarted"; + case Error::Code::kInvalidConnectionState: + return os << "InvalidConnectionState"; + case Error::Code::kJsonParseError: + return os << "JsonParseError"; + case Error::Code::kJsonWriteError: + return os << "JsonWriteError"; + case Error::Code::kFatalSSLError: + return os << "FatalSSLError"; + case Error::Code::kRSAKeyGenerationFailure: + return os << "RSAKeyGenerationFailure"; + case Error::Code::kRSAKeyParseError: + return os << "RSAKeyParseError"; + case Error::Code::kEVPInitializationError: + return os << "EVPInitializationError"; + case Error::Code::kCertificateCreationError: + return os << "CertificateCreationError"; + case Error::Code::kCertificateValidationError: + return os << "CertificateValidationError"; + case Error::Code::kSha256HashFailure: + return os << "Sha256HashFailure"; + case Error::Code::kFileLoadFailure: + return os << "FileLoadFailure"; + case Error::Code::kErrCertsMissing: + return os << "ErrCertsMissing"; + case Error::Code::kErrCertsParse: + return os << "ErrCertsParse"; + case Error::Code::kErrCertsRestrictions: + return os << "ErrCertsRestrictions"; + case Error::Code::kErrCertsDateInvalid: + return os << "ErrCertsDateInvalid"; + case Error::Code::kErrCertsVerifyGeneric: + return os << "ErrCertsVerifyGeneric"; + case Error::Code::kErrCertsVerifyUntrustedCert: + return os << "kErrCertsVerifyUntrustedCert"; + case Error::Code::kErrCrlInvalid: + return os << "ErrCrlInvalid"; + case Error::Code::kErrCertsRevoked: + return os << "ErrCertsRevoked"; + case Error::Code::kErrCertsPathlen: + return os << "ErrCertsPathlen"; + case Error::Code::kErrCertSerialize: + return os << "ErrCertSerialize"; + case Error::Code::kCastV2PeerCertEmpty: + return os << "kCastV2PeerCertEmpty"; + case Error::Code::kCastV2WrongPayloadType: + return os << "kCastV2WrongPayloadType"; + case Error::Code::kCastV2NoPayload: + return os << "kCastV2NoPayload"; + case Error::Code::kCastV2PayloadParsingFailed: + return os << "kCastV2PayloadParsingFailed"; + case Error::Code::kCastV2MessageError: + return os << "CastV2kMessageError"; + case Error::Code::kCastV2NoResponse: + return os << "kCastV2NoResponse"; + case Error::Code::kCastV2FingerprintNotFound: + return os << "kCastV2FingerprintNotFound"; + case Error::Code::kCastV2CertNotSignedByTrustedCa: + return os << "kCastV2CertNotSignedByTrustedCa"; + case Error::Code::kCastV2CannotExtractPublicKey: + return os << "kCastV2CannotExtractPublicKey"; + case Error::Code::kCastV2SignedBlobsMismatch: + return os << "kCastV2SignedBlobsMismatch"; + case Error::Code::kCastV2TlsCertValidityPeriodTooLong: + return os << "kCastV2TlsCertValidityPeriodTooLong"; + case Error::Code::kCastV2TlsCertValidStartDateInFuture: + return os << "kCastV2TlsCertValidStartDateInFuture"; + case Error::Code::kCastV2TlsCertExpired: + return os << "kCastV2TlsCertExpired"; + case Error::Code::kCastV2SenderNonceMismatch: + return os << "kCastV2SenderNonceMismatch"; + case Error::Code::kCastV2DigestUnsupported: + return os << "kCastV2DigestUnsupported"; + case Error::Code::kCastV2SignatureEmpty: + return os << "kCastV2SignatureEmpty"; + case Error::Code::kCastV2ChannelNotOpen: + return os << "kCastV2ChannelNotOpen"; + case Error::Code::kCastV2AuthenticationError: + return os << "kCastV2AuthenticationError"; + case Error::Code::kCastV2ConnectError: + return os << "kCastV2ConnectError"; + case Error::Code::kCastV2CastSocketError: + return os << "kCastV2CastSocketError"; + case Error::Code::kCastV2TransportError: + return os << "kCastV2TransportError"; + case Error::Code::kCastV2InvalidMessage: + return os << "kCastV2InvalidMessage"; + case Error::Code::kCastV2InvalidChannelId: + return os << "kCastV2InvalidChannelId"; + case Error::Code::kCastV2ConnectTimeout: + return os << "kCastV2ConnectTimeout"; + case Error::Code::kCastV2PingTimeout: + return os << "kCastV2PingTimeout"; + case Error::Code::kCastV2ChannelPolicyMismatch: + return os << "kCastV2ChannelPolicyMismatch"; + case Error::Code::kCreateSignatureFailed: + return os << "kCreateSignatureFailed"; + case Error::Code::kUpdateReceivedRecordFailure: + return os << "kUpdateReceivedRecordFailure"; + case Error::Code::kRecordPublicationError: + return os << "kRecordPublicationError"; + case Error::Code::kProcessReceivedRecordFailure: + return os << "ProcessReceivedRecordFailure"; + case Error::Code::kUnknownError: + return os << "UnknownError"; + case Error::Code::kNotImplemented: + return os << "NotImplemented"; + case Error::Code::kInsufficientBuffer: + return os << "InsufficientBuffer"; + case Error::Code::kParameterInvalid: + return os << "ParameterInvalid"; + case Error::Code::kParameterOutOfRange: + return os << "ParameterOutOfRange"; + case Error::Code::kParameterNullPointer: + return os << "ParameterNullPointer"; + case Error::Code::kIndexOutOfBounds: + return os << "IndexOutOfBounds"; + case Error::Code::kItemAlreadyExists: + return os << "ItemAlreadyExists"; + case Error::Code::kItemNotFound: + return os << "ItemNotFound"; + case Error::Code::kOperationInvalid: + return os << "OperationInvalid"; + case Error::Code::kOperationInProgress: + return os << "OperationInProgress"; + case Error::Code::kOperationCancelled: + return os << "OperationCancelled"; + case Error::Code::kInterrupted: + return os << "Interrupted"; + case Error::Code::kUnknownCodec: + return os << "UnknownCodec"; + case Error::Code::kInvalidCodecParameter: + return os << "InvalidCodecParameter"; + case Error::Code::kSocketFailure: + return os << "SocketFailure"; + case Error::Code::kUnencryptedOffer: + return os << "UnencryptedOffer"; + case Error::Code::kRemotingNotSupported: + return os << "RemotingNotSupported"; + case Error::Code::kNoStreamSelected: + return os << "NoStreamSelected"; + case Error::Code::kAnswerTimeout: + return os << "AnswerTimeout"; + case Error::Code::kInvalidAnswer: + return os << "InvalidAnswer"; + case Error::Code::kMessageTimeout: + return os << "MessageTimeout"; + case Error::Code::kNone: + break; + } + + // Unused 'return' to get around failure on GCC. + return os; +} + +std::string Error::ToString() const { + std::stringstream ss; + ss << *this; + return ss.str(); +} + +std::string ToString(openscreen::Error::Code code) { + std::ostringstream ss; + ss << code; + return ss.str(); +} + +std::ostream& operator<<(std::ostream& out, const Error& error) { + out << error.code() << " = \"" << error.message() << "\""; + return out; +} + +// static +const Error& Error::None() { + static Error& error = *new Error(Code::kNone); + return error; +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/error.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/error.h new file mode 100644 index 0000000..a417e1b --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/error.h @@ -0,0 +1,432 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_ERROR_H_ +#define PLATFORM_BASE_ERROR_H_ + +#include +#include +#include +#include + + +namespace openscreen { + +// Represents an error returned by an OSP library operation. An error has a +// code and an optional message. +class Error { + public: + // TODO(crbug.com/openscreen/65): Group/rename OSP-specific errors + // NOTE: new values should be added to the end of the of enum and existing + // values should not be changed. + enum class Code : int8_t { + // No error occurred. + kNone = 0, + + // A transient condition prevented the operation from proceeding (e.g., + // cannot send on a non-blocking socket without blocking). This indicates + // the caller should try again later. + kAgain = -1, + + // CBOR errors. + kCborParsing = 1, + kCborEncoding = 2, + kCborIncompleteMessage = 3, + kCborInvalidResponseId = 4, + kCborInvalidMessage = 5, + + // Presentation start errors. + kNoAvailableReceivers = 6, + kRequestCancelled = 7, + kNoPresentationFound = 8, + kPreviousStartInProgress = 9, + kUnknownStartError = 10, + kUnknownRequestId = 11, + + kAddressInUse = 12, + kDomainNameTooLong = 13, + kDomainNameLabelTooLong = 14, + + kIOFailure = 15, + kInitializationFailure = 16, + kInvalidIPV4Address = 17, + kInvalidIPV6Address = 18, + kConnectionFailed = 19, + + kSocketOptionSettingFailure = 20, + kSocketAcceptFailure = 21, + kSocketBindFailure = 22, + kSocketClosedFailure = 23, + kSocketConnectFailure = 24, + kSocketInvalidState = 25, + kSocketListenFailure = 26, + kSocketReadFailure = 27, + kSocketSendFailure = 28, + + // MDNS errors. + kMdnsRegisterFailure = 29, + kMdnsReadFailure = 30, + kMdnsNonConformingFailure = 31, + + kParseError = 32, + kUnknownMessageType = 33, + + kNoActiveConnection = 34, + kAlreadyClosed = 35, + kInvalidConnectionState = 36, + kNoStartedPresentation = 37, + kPresentationAlreadyStarted = 38, + + kJsonParseError = 39, + kJsonWriteError = 40, + + // OpenSSL errors. + + // Was unable to generate an RSA key. + kRSAKeyGenerationFailure = 41, + kRSAKeyParseError = 42, + + // Was unable to initialize an EVP_PKEY type. + kEVPInitializationError = 43, + + // Was unable to generate a certificate. + kCertificateCreationError = 44, + + // Certificate failed validation. + kCertificateValidationError = 45, + + // Failed to produce a hashing digest. + kSha256HashFailure = 46, + + // A non-recoverable SSL library error has occurred. + kFatalSSLError = 47, + kFileLoadFailure = 48, + + // Cast certificate errors. + + // Certificates were not provided for verification. + kErrCertsMissing = 49, + + // The certificates provided could not be parsed. + kErrCertsParse = 50, + + // Key usage is missing or is not set to Digital Signature. + // This error could also be thrown if the CN is missing. + kErrCertsRestrictions = 51, + + // The current date is before the notBefore date or after the notAfter date. + kErrCertsDateInvalid = 52, + + // The certificate failed to chain to a trusted root. + kErrCertsVerifyGeneric = 53, + + // The certificate was not found in the trust store. + kErrCertsVerifyUntrustedCert = 54, + + // The CRL is missing or failed to verify. + kErrCrlInvalid = 55, + + // One of the certificates in the chain is revoked. + kErrCertsRevoked = 56, + + // The pathlen constraint of the root certificate was exceeded. + kErrCertsPathlen = 57, + + // The certificate provided could not be serialized. + kErrCertSerialize = 58, + + // Cast authentication errors. + kCastV2PeerCertEmpty = 59, + kCastV2WrongPayloadType = 60, + kCastV2NoPayload = 61, + kCastV2PayloadParsingFailed = 62, + kCastV2MessageError = 63, + kCastV2NoResponse = 64, + kCastV2FingerprintNotFound = 65, + kCastV2CertNotSignedByTrustedCa = 66, + kCastV2CannotExtractPublicKey = 67, + kCastV2SignedBlobsMismatch = 68, + kCastV2TlsCertValidityPeriodTooLong = 69, + kCastV2TlsCertValidStartDateInFuture = 70, + kCastV2TlsCertExpired = 71, + kCastV2SenderNonceMismatch = 72, + kCastV2DigestUnsupported = 73, + kCastV2SignatureEmpty = 74, + + // Cast channel errors. + kCastV2ChannelNotOpen = 75, + kCastV2AuthenticationError = 76, + kCastV2ConnectError = 77, + kCastV2CastSocketError = 78, + kCastV2TransportError = 79, + kCastV2InvalidMessage = 80, + kCastV2InvalidChannelId = 81, + kCastV2ConnectTimeout = 82, + kCastV2PingTimeout = 83, + kCastV2ChannelPolicyMismatch = 84, + + kCreateSignatureFailed = 85, + + // Discovery errors. + kUpdateReceivedRecordFailure = 86, + kRecordPublicationError = 87, + kProcessReceivedRecordFailure = 88, + + // Generic errors. + kUnknownError = 89, + kNotImplemented = 90, + kInsufficientBuffer = 91, + kParameterInvalid = 92, + kParameterOutOfRange = 93, + kParameterNullPointer = 94, + kIndexOutOfBounds = 95, + kItemAlreadyExists = 96, + kItemNotFound = 97, + kOperationInvalid = 98, + kOperationInProgress = 99, + kOperationCancelled = 100, + kInterrupted = 101, + + // Cast streaming errors. + kUnknownCodec = 102, + kInvalidCodecParameter = 103, + kSocketFailure = 104, + kUnencryptedOffer = 105, + kRemotingNotSupported = 106, + kNoStreamSelected = 107, + + // An Answer timeout means that the receiver failed to reply to our Offer + // within a reasonable amount of time. + kAnswerTimeout = 108, + + // Received an ANSWER, but it was invalid. + kInvalidAnswer = 109, + + // A generic message timeout occured. + kMessageTimeout = 110, + }; + + Error(); + Error(const Error& error); + Error(Error&& error) noexcept; + + Error(Code code); // NOLINT + Error(Code code, const std::string& message); + Error(Code code, std::string&& message); + ~Error(); + + Error& operator=(const Error& other); + Error& operator=(Error&& other); + bool operator==(const Error& other) const; + bool operator!=(const Error& other) const; + + // Special case comparison with codes. Without this case, comparisons will + // not work as expected, e.g. + // const Error foo(Error::Code::kItemNotFound, "Didn't find an item"); + // foo == Error::Code::kItemNotFound is actually false. + bool operator==(Code code) const; + bool operator!=(Code code) const; + bool ok() const { return code_ == Code::kNone; } + + Code code() const { return code_; } + const std::string& message() const { return message_; } + std::string& message() { return message_; } + + static const Error& None(); + + std::string ToString() const; + + private: + Code code_ = Code::kNone; + std::string message_; +}; + +std::string ToString(openscreen::Error::Code code); +std::ostream& operator<<(std::ostream& os, const Error::Code& code); +std::ostream& operator<<(std::ostream& out, const Error& error); + +// A convenience function to return a single value from a function that can +// return a value or an error. For normal results, construct with a ValueType* +// (ErrorOr takes ownership) and the Error will be kNone with an empty message. +// For Error results, construct with an error code and value. +// +// Example: +// +// ErrorOr Foo::DoSomething() { +// if (success) { +// return Bar(); +// } else { +// return Error(kBadThingHappened, "No can do"); +// } +// } +// +// TODO(mfoltz): Add support for type conversions. +template +class ErrorOr { + public: + static ErrorOr None() { + static ErrorOr error(Error::Code::kNone); + return error; + } + + ErrorOr(const ValueType& value) : value_(value), is_value_(true) {} // NOLINT + ErrorOr(ValueType&& value) noexcept // NOLINT + : value_(std::move(value)), is_value_(true) {} + + ErrorOr(const Error& error) : error_(error), is_value_(false) { // NOLINT + assert(error_.code() != Error::Code::kNone); + } + ErrorOr(Error&& error) noexcept // NOLINT + : error_(std::move(error)), is_value_(false) { + assert(error_.code() != Error::Code::kNone); + } + ErrorOr(Error::Code code) : error_(code), is_value_(false) { // NOLINT + assert(error_.code() != Error::Code::kNone); + } + ErrorOr(Error::Code code, std::string message) + : error_(code, std::move(message)), is_value_(false) { + assert(error_.code() != Error::Code::kNone); + } + + ErrorOr(const ErrorOr& other) = delete; + ErrorOr(ErrorOr&& other) noexcept : is_value_(other.is_value_) { + // NB: Both `value_` and `error_` are uninitialized memory at this point! + // Unlike the other constructors, the compiler will not auto-generate + // constructor calls for either union member because neither appeared in + // this constructor's initializer list. + if (other.is_value_) { + new (&value_) ValueType(std::move(other.value_)); + } else { + new (&error_) Error(std::move(other.error_)); + } + } + + ErrorOr& operator=(const ErrorOr& other) = delete; + ErrorOr& operator=(ErrorOr&& other) noexcept { + this->~ErrorOr(); + new (this) ErrorOr(std::move(other)); + return *this; + } + + ~ErrorOr() { + // NB: `value_` or `error_` must be explicitly destroyed since the compiler + // will not auto-generate the destructor calls for union members. + if (is_value_) { + value_.~ValueType(); + } else { + error_.~Error(); + } + } + + bool is_error() const { return !is_value_; } + bool is_value() const { return is_value_; } + + // Unlike Error, we CAN provide an operator bool here, since it is + // more obvious to callers that ErrorOr will be true if it's Foo. + operator bool() const { return is_value_; } + + const Error& error() const { + assert(!is_value_); + return error_; + } + Error& error() { + assert(!is_value_); + return error_; + } + + const ValueType& value() const { + assert(is_value_); + return value_; + } + ValueType& value() { + assert(is_value_); + return value_; + } + + // Move only value or fallback + ValueType&& value(ValueType&& fallback) { + if (is_value()) { + return std::move(value()); + } + return std::forward(fallback); + } + + // Copy only value or fallback + ValueType value(ValueType fallback) const { + if (is_value()) { + return value(); + } + return std::move(fallback); + } + + private: + // Only one of these is an active member, determined by `is_value_`. Since + // they are union'ed, they must be explicitly constructed and destroyed. + union { + ValueType value_; + Error error_; + }; + + // If true, `value_` is initialized and active. Otherwise, `error_` is + // initialized and active. + const bool is_value_; +}; + +// Define comparison operators using SFINAE. +template +bool operator<(const ErrorOr& lhs, const ErrorOr& rhs) { + // Handle the cases where one side is an error. + if (lhs.is_error() != rhs.is_error()) { + return lhs.is_error(); + } + + // Handle the case where both sides are errors. + if (lhs.is_error()) { + return static_cast(lhs.error().code()) < + static_cast(rhs.error().code()); + } + + // Handle the case where both are values. + return lhs.value() < rhs.value(); +} + +template +bool operator>(const ErrorOr& lhs, const ErrorOr& rhs) { + return rhs < lhs; +} + +template +bool operator<=(const ErrorOr& lhs, const ErrorOr& rhs) { + return !(lhs > rhs); +} + +template +bool operator>=(const ErrorOr& lhs, const ErrorOr& rhs) { + return !(rhs < lhs); +} + +template +bool operator==(const ErrorOr& lhs, const ErrorOr& rhs) { + // Handle the cases where one side is an error. + if (lhs.is_error() != rhs.is_error()) { + return false; + } + + // Handle the case where both sides are errors. + if (lhs.is_error()) { + return lhs.error() == rhs.error(); + } + + // Handle the case where both are values. + return lhs.value() == rhs.value(); +} + +template +bool operator!=(const ErrorOr& lhs, const ErrorOr& rhs) { + return !(lhs == rhs); +} + +} // namespace openscreen + +#endif // PLATFORM_BASE_ERROR_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/interface_info.cc b/breadcast-caststream-sys/vendor/openscreen/platform/base/interface_info.cc new file mode 100644 index 0000000..77d387a --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/interface_info.cc @@ -0,0 +1,98 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/base/interface_info.h" + +#include +#include + +namespace openscreen { + +InterfaceInfo::InterfaceInfo() = default; +InterfaceInfo::InterfaceInfo(NetworkInterfaceIndex index, + const uint8_t hardware_address[6], + std::string name, + Type type, + std::vector addresses) + : index(index), + hardware_address{hardware_address[0], hardware_address[1], + hardware_address[2], hardware_address[3], + hardware_address[4], hardware_address[5]}, + name(std::move(name)), + type(type), + addresses(std::move(addresses)) {} +InterfaceInfo::~InterfaceInfo() = default; + +IPSubnet::IPSubnet() = default; +IPSubnet::IPSubnet(IPAddress address, uint8_t prefix_length) + : address(std::move(address)), prefix_length(prefix_length) {} +IPSubnet::~IPSubnet() = default; + +IPAddress InterfaceInfo::GetIpAddressV4() const { + for (const auto& address : addresses) { + if (address.address.IsV4()) { + return address.address; + } + } + return IPAddress{}; +} + +IPAddress InterfaceInfo::GetIpAddressV6() const { + for (const auto& address : addresses) { + if (address.address.IsV6()) { + return address.address; + } + } + return IPAddress{}; +} + +bool InterfaceInfo::HasHardwareAddress() const { + return std::any_of(hardware_address.begin(), hardware_address.end(), + [](uint8_t e) { return e != 0; }); +} + +std::ostream& operator<<(std::ostream& out, const IPSubnet& subnet) { + if (subnet.address.IsV6()) { + out << '['; + } + out << subnet.address; + if (subnet.address.IsV6()) { + out << ']'; + } + return out << '/' << std::dec << static_cast(subnet.prefix_length); +} + +std::ostream& operator<<(std::ostream& out, InterfaceInfo::Type type) { + switch (type) { + case InterfaceInfo::Type::kEthernet: + out << "Ethernet"; + break; + case InterfaceInfo::Type::kWifi: + out << "Wifi"; + break; + case InterfaceInfo::Type::kLoopback: + out << "Loopback"; + break; + case InterfaceInfo::Type::kOther: + out << "Other"; + break; + } + + return out; +} + +std::ostream& operator<<(std::ostream& out, const InterfaceInfo& info) { + out << '{' << info.index << " (a.k.a. " << info.name + << "); media_type=" << info.type << "; MAC=" << std::hex + << static_cast(info.hardware_address[0]); + for (size_t i = 1; i < info.hardware_address.size(); ++i) { + out << ':' << static_cast(info.hardware_address[i]); + } + for (const IPSubnet& ip : info.addresses) { + out << "; " << ip; + } + return out << '}'; +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/interface_info.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/interface_info.h new file mode 100644 index 0000000..b5eaa6e --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/interface_info.h @@ -0,0 +1,86 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_INTERFACE_INFO_H_ +#define PLATFORM_BASE_INTERFACE_INFO_H_ + +#include + +#include +#include + +#include "platform/base/ip_address.h" + +namespace openscreen { + +// Unique identifier, usually provided by the operating system, for identifying +// a specific network interface. This value is used with UdpSocket to join +// multicast groups, or to make multicast broadcasts. An implementation may +// choose to make these values anything its UdpSocket implementation will +// recognize. +using NetworkInterfaceIndex = int64_t; +enum : NetworkInterfaceIndex { kInvalidNetworkInterfaceIndex = -1 }; + +struct IPSubnet { + IPAddress address; + + // Prefix length of `address`, which is another way of specifying a subnet + // mask. For example, 192.168.0.10/24 is a common representation of the + // address 192.168.0.10 with a 24-bit prefix (this describes a range of IPv4 + // addresses from 192.168.0.0 through 192.168.0.255). Likewise, for IPv6 + // addresses such as 2001:db8::/96, the concept is the same (this specifies + // the range of addresses having the same leading 96 bits). + uint8_t prefix_length = 0; + + IPSubnet(); + IPSubnet(IPAddress address, uint8_t prefix); + ~IPSubnet(); +}; + +struct InterfaceInfo { + enum class Type : uint32_t { kEthernet = 0, kWifi, kLoopback, kOther }; + + // Interface index, typically as specified by the operating system, + // identifying this interface on the host machine. + NetworkInterfaceIndex index = kInvalidNetworkInterfaceIndex; + + // MAC address of the interface. Typically 6 or 16 bytes. Empty if + // unavailable. + std::vector hardware_address; + + // Interface name (e.g. eth0) if available. + std::string name; + + // Hardware type of the interface. + Type type = Type::kOther; + + // All IP addresses associated with the interface. + std::vector addresses; + + // Returns an IPAddress of the given type associated with this network + // interface, or the false IPAddress if the associated address family is not + // supported on this interface. + IPAddress GetIpAddressV4() const; + IPAddress GetIpAddressV6() const; + + // Returns true if `hardware_address` is non-zero. + bool HasHardwareAddress() const; + + InterfaceInfo(); + InterfaceInfo(NetworkInterfaceIndex index, + const uint8_t hardware_address[6], + std::string name, + Type type, + std::vector addresses); + ~InterfaceInfo(); +}; + +// Human-readable output (e.g., for logging). +std::ostream& operator<<(std::ostream& out, InterfaceInfo::Type type); +std::ostream& operator<<(std::ostream& out, const IPSubnet& subnet); +std::ostream& operator<<(std::ostream& out, const InterfaceInfo& info); + +} // namespace openscreen + +#endif // PLATFORM_BASE_INTERFACE_INFO_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/ip_address.cc b/breadcast-caststream-sys/vendor/openscreen/platform/base/ip_address.cc new file mode 100644 index 0000000..ad39744 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/ip_address.cc @@ -0,0 +1,343 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/base/ip_address.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "build/build_config.h" + +#if BUILDFLAG(IS_POSIX) +#include +#endif + +namespace openscreen { + +IPAddress::IPAddress(Version version, std::span bytes) + : version_(version) { + assert(bytes.size() >= size()); + std::copy_n(bytes.begin(), size(), bytes_.begin()); +} + +bool IPAddress::operator==(const IPAddress& o) const { + return version_ == o.version_ && + std::equal(bytes_.begin(), bytes_.begin() + size(), + o.bytes_.begin()) && + scope_id_ == o.scope_id_; +} + +bool IPAddress::operator!=(const IPAddress& o) const { + return !(*this == o); +} + +IPAddress::operator bool() const { + return std::any_of(bytes_.begin(), bytes_.begin() + size(), + [](uint8_t byte) { return byte; }); +} + +void IPAddress::CopyTo(std::span bytes) const { + assert(bytes.size() >= size()); + std::copy_n(bytes_.begin(), size(), bytes.begin()); +} + +bool IPAddress::IsLinkLocal() const { + if (!IsV6()) { + return false; + } + // Link-local addresses start with fe80::/10 + return (bytes_[0] == 0xfe) && ((bytes_[1] & 0xc0) == 0x80); +} + +namespace { + +ErrorOr ParseV4(std::string_view s) { + uint8_t octets[4]; + for (int i = 0; i < 4; ++i) { + if (i > 0) { + if (s.empty() || s.front() != '.') { + return Error::Code::kInvalidIPV4Address; + } + s.remove_prefix(1); + } + const auto result = + std::from_chars(s.data(), s.data() + s.size(), octets[i]); + if (result.ec != std::errc()) { + return Error::Code::kInvalidIPV4Address; + } + s.remove_prefix(result.ptr - s.data()); + } + + if (!s.empty()) { + return Error::Code::kInvalidIPV4Address; + } + + return IPAddress(octets[0], octets[1], octets[2], octets[3]); +} + +// Returns the zero-expansion of a double-colon in `s` if `s` is a +// well-formatted IPv6 address. If `s` is ill-formatted, returns *any* string +// that is ill-formatted. +std::string ExpandIPv6DoubleColon(std::string_view s) { + constexpr std::string_view kDoubleColon = "::"; + const size_t double_colon_position = s.find(kDoubleColon); + if (double_colon_position == std::string::npos) { + return std::string(s); // Nothing to expand. + } + if (double_colon_position != s.rfind(kDoubleColon)) { + return {}; // More than one occurrence of double colons is illegal. + } + + std::ostringstream expanded; + const int num_single_colons = std::count(s.begin(), s.end(), ':') - 2; + int num_zero_groups_to_insert = 8 - num_single_colons; + if (double_colon_position != 0) { + // abcd:0123:4567::f000:1 + // ^^^^^^^^^^^^^^^ + expanded << s.substr(0, double_colon_position + 1); + --num_zero_groups_to_insert; + } + if (double_colon_position != (s.size() - 2)) { + --num_zero_groups_to_insert; + } + while (--num_zero_groups_to_insert > 0) { + expanded << "0:"; + } + expanded << '0'; + if (double_colon_position != (s.size() - 2)) { + // abcd:0123:4567::f000:1 + // ^^^^^^^ + expanded << s.substr(double_colon_position + 1); + } + return expanded.str(); +} + +} // namespace + +ErrorOr ParseV6(std::string_view s) { + std::string_view address_part = s; + uint32_t scope_id = 0; + + // Handle link-local addresses with scope ID, e.g., fe80::1%eth0 + const size_t scope_pos = s.find('%'); + if (scope_pos != std::string::npos) { + address_part = s.substr(0, scope_pos); + std::string_view scope_name = s.substr(scope_pos + 1); +#if BUILDFLAG(IS_POSIX) + scope_id = if_nametoindex(std::string(scope_name).c_str()); +#endif + if (scope_id == 0) { + // If if_nametoindex failed or is not available, try parsing as a number. + unsigned int parsed_id = 0; + const auto result = std::from_chars( + scope_name.data(), scope_name.data() + scope_name.size(), parsed_id); + + if (result.ec == std::errc() && + result.ptr == scope_name.data() + scope_name.size() && + parsed_id > 0) { + scope_id = parsed_id; + } + } + + if (scope_id == 0) { + return Error::Code::kInvalidIPV6Address; + } + } + + const std::string scan_input = ExpandIPv6DoubleColon(address_part); + std::string_view scan_view(scan_input); + uint16_t hextets[8]; + + for (int i = 0; i < 8; ++i) { + if (i > 0) { + if (scan_view.empty() || scan_view.front() != ':') { + return Error::Code::kInvalidIPV6Address; + } + scan_view.remove_prefix(1); + } + const auto result = std::from_chars( + scan_view.data(), scan_view.data() + scan_view.size(), hextets[i], 16); + if (result.ec != std::errc()) { + return Error::Code::kInvalidIPV6Address; + } + scan_view.remove_prefix(result.ptr - scan_view.data()); + } + + if (!scan_view.empty()) { + return Error::Code::kInvalidIPV6Address; + } + + IPAddress address(hextets); + if (scope_id != 0) { + if (!address.IsLinkLocal()) { + return Error::Code::kInvalidIPV6Address; + } + address.scope_id_ = scope_id; + } + return address; +} + +// static +ErrorOr IPAddress::Parse(std::string_view s) { + ErrorOr v4 = ParseV4(s); + + return v4 ? std::move(v4) : ParseV6(s); +} + +// static +const IPEndpoint IPEndpoint::kAnyV4() { + return IPEndpoint{}; +} + +// static +const IPEndpoint IPEndpoint::kAnyV6() { + return IPEndpoint{IPAddress::kAnyV6(), 0}; +} + +IPEndpoint::operator bool() const { + return address || port; +} + +// static +ErrorOr IPEndpoint::Parse(std::string_view s) { + // Look for the colon that separates the IP address from the port number. Note + // that this check also guards against the case where `s` is the empty string. + const auto colon_pos = s.rfind(':'); + if (colon_pos == std::string::npos) { + return Error(Error::Code::kParseError, "missing colon separator"); + } + // The colon cannot be the first nor the last character in `s` because that + // would mean there is no address part or port part. + if (colon_pos == 0) { + return Error(Error::Code::kParseError, "missing address before colon"); + } + if (colon_pos == (s.size() - 1)) { + return Error(Error::Code::kParseError, "missing port after colon"); + } + + ErrorOr address(Error::Code::kParseError); + if (s[0] == '[' && s[colon_pos - 1] == ']') { + // [abcd:beef:1:1::2600]:8080 + // ^^^^^^^^^^^^^^^^^^^^^ + address = ParseV6(s.substr(1, colon_pos - 2)); + } else { + // 127.0.0.1:22 + // ^^^^^^^^^ + address = ParseV4(s.substr(0, colon_pos)); + } + if (address.is_error()) { + return Error(Error::Code::kParseError, "invalid address part"); + } + + const std::string_view port_part = s.substr(colon_pos + 1); + int port; + const auto result = std::from_chars( + port_part.data(), port_part.data() + port_part.size(), port); + if (result.ec != std::errc() || + result.ptr != port_part.data() + port_part.size() || port < 0 || + port > std::numeric_limits::max()) { + return Error(Error::Code::kParseError, "invalid port part"); + } + + return IPEndpoint{address.value(), static_cast(port)}; +} + +bool operator==(const IPEndpoint& a, const IPEndpoint& b) { + return (a.address == b.address) && (a.port == b.port); +} + +bool operator!=(const IPEndpoint& a, const IPEndpoint& b) { + return !(a == b); +} + +bool IPAddress::operator<(const IPAddress& other) const { + if (version() != other.version()) { + return version() < other.version(); + } + + if (IsV4()) { + return memcmp(bytes_.data(), other.bytes_.data(), 4) < 0; + } else { + const int cmp = memcmp(bytes_.data(), other.bytes_.data(), 16); + if (cmp != 0) { + return cmp < 0; + } + return scope_id_ < other.scope_id_; + } +} + +bool operator<(const IPEndpoint& a, const IPEndpoint& b) { + if (a.address != b.address) { + return a.address < b.address; + } + + return a.port < b.port; +} + +std::ostream& operator<<(std::ostream& out, const IPAddress& address) { + char separator; + size_t values_per_separator; + int value_width; + if (address.IsV4()) { + out << std::dec; + separator = '.'; + values_per_separator = 1; + value_width = 0; + } else if (address.IsV6()) { + out << std::hex << std::setfill('0') << std::right; + separator = ':'; + values_per_separator = 2; + value_width = 2; + } + std::span bytes = address.bytes(); + for (size_t i = 0; i < bytes.size(); ++i) { + if (i > 0 && (i % values_per_separator == 0)) { + out << separator; + } + out << std::setw(value_width) << static_cast(bytes[i]); + } + if (address.IsLinkLocal() && address.GetScopeId() != 0) { +#if BUILDFLAG(IS_POSIX) + char ifname[IF_NAMESIZE]; + if (if_indextoname(address.GetScopeId(), ifname)) { + out << '%' << ifname; + } else { + out << '%' << address.GetScopeId(); + } +#else + out << '%' << address.GetScopeId(); +#endif + } + return out; +} + +std::ostream& operator<<(std::ostream& out, const IPEndpoint& endpoint) { + if (endpoint.address.IsV6()) { + out << '['; + } + out << endpoint.address; + if (endpoint.address.IsV6()) { + out << ']'; + } + return out << ':' << std::dec << static_cast(endpoint.port); +} + +std::string IPEndpoint::ToString() const { + std::ostringstream name; + name << *this; + return name.str(); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/ip_address.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/ip_address.h new file mode 100644 index 0000000..1dd67d6 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/ip_address.h @@ -0,0 +1,209 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_IP_ADDRESS_H_ +#define PLATFORM_BASE_IP_ADDRESS_H_ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "platform/base/error.h" + +namespace openscreen { + +class IPAddress { + public: + enum class Version { + kV4, + kV6, + }; + + static constexpr IPAddress kAnyV4() { return IPAddress{0, 0, 0, 0}; } + static constexpr IPAddress kAnyV6() { + return IPAddress{0, 0, 0, 0, 0, 0, 0, 0}; + } + static constexpr IPAddress kV4LoopbackAddress() { + return IPAddress{127, 0, 0, 1}; + } + static constexpr IPAddress kV6LoopbackAddress() { + return IPAddress{0, 0, 0, 0, 0, 0, 0, 1}; + } + static constexpr size_t kV4Size = 4; + static constexpr size_t kV6Size = 16; + + constexpr IPAddress() : version_(Version::kV4), bytes_({}) {} + + // `bytes` contains 4 octets for IPv4, or 8 hextets (16 bytes of big-endian + // shorts) for IPv6. + // TODO(jophba): delete once usage is removed in Chromium's network_util.cc. + inline IPAddress(Version version, const uint8_t* bytes) : version_(version) { + std::copy_n(bytes, size(), bytes_.begin()); + } + + IPAddress(Version version, std::span bytes); + + // IPv4 constructors (IPAddress from 4 octets). + explicit constexpr IPAddress(std::span bytes) + : version_(Version::kV4), + bytes_{{bytes[0], bytes[1], bytes[2], bytes[3]}} {} + + constexpr IPAddress(uint8_t b1, uint8_t b2, uint8_t b3, uint8_t b4) + : version_(Version::kV4), bytes_{{b1, b2, b3, b4}} {} + + // IPv6 constructors (IPAddress from 8 hextets). + explicit constexpr IPAddress(std::span hextets) + : IPAddress(hextets[0], + hextets[1], + hextets[2], + hextets[3], + hextets[4], + hextets[5], + hextets[6], + hextets[7]) {} + + constexpr IPAddress(uint16_t h0, + uint16_t h1, + uint16_t h2, + uint16_t h3, + uint16_t h4, + uint16_t h5, + uint16_t h6, + uint16_t h7) + : version_(Version::kV6), + bytes_{{ + static_cast(h0 >> 8), + static_cast(h0), + static_cast(h1 >> 8), + static_cast(h1), + static_cast(h2 >> 8), + static_cast(h2), + static_cast(h3 >> 8), + static_cast(h3), + static_cast(h4 >> 8), + static_cast(h4), + static_cast(h5 >> 8), + static_cast(h5), + static_cast(h6 >> 8), + static_cast(h6), + static_cast(h7 >> 8), + static_cast(h7), + }} {} + + // IPv6 constructor with scope ID. + explicit constexpr IPAddress(std::span bytes, + uint32_t scope_id) + : version_(Version::kV6), scope_id_(scope_id) { + for (size_t i = 0; i < 16; ++i) { + bytes_[i] = bytes[i]; + } + } + + constexpr IPAddress(const IPAddress& o) noexcept = default; + constexpr IPAddress(IPAddress&& o) noexcept = default; + ~IPAddress() = default; + + constexpr IPAddress& operator=(const IPAddress& o) noexcept = default; + constexpr IPAddress& operator=(IPAddress&& o) noexcept = default; + + bool operator==(const IPAddress& o) const; + bool operator!=(const IPAddress& o) const; + + // IP address comparison rules are based on the following two principles: + // 1. newer versions are greater, e.g. IPv6 > IPv4 + // 2. higher numerical values are greater, e.g. 192.168.0.1 > 10.0.0.1 + bool operator<(const IPAddress& other) const; + bool operator>(const IPAddress& other) const { return other < *this; } + bool operator<=(const IPAddress& other) const { return !(other < *this); } + bool operator>=(const IPAddress& other) const { return !(*this < other); } + explicit operator bool() const; + + Version version() const { return version_; } + size_t size() const { return (version_ == Version::kV4) ? kV4Size : kV6Size; } + bool IsV4() const { return version_ == Version::kV4; } + bool IsV6() const { return version_ == Version::kV6; } + + // Returns true if the address is an IPv6 link-local address. + bool IsLinkLocal() const; + + // Returns the scope ID for link-local IPv6 addresses. Returns 0 for + // non-link-local addresses. + uint32_t GetScopeId() const { return scope_id_; } + + // These methods assume `x` is the appropriate size, but due to various + // callers' casting needs we can't check them like the constructors above. + // Callers should instead make any necessary checks themselves. + void CopyTo(std::span bytes) const; + + // TODO(jophba): delete once usage is removed in Chromium's network_util.cc. + inline void CopyToV4(uint8_t* x) const { CopyTo(std::span(x, kV4Size)); } + inline void CopyToV6(uint8_t* x) const { CopyTo(std::span(x, kV6Size)); } + + // In some instances, we want direct access to the underlying byte storage, + // in order to avoid making multiple copies. + std::span bytes() const { + return {bytes_.data(), (version_ == Version::kV4) ? kV4Size : kV6Size}; + } + + // Parses a text representation of an IPv4 address (e.g. "192.168.0.1") or an + // IPv6 address (e.g. "abcd::1234"). + static ErrorOr Parse(std::string_view s); + + private: + friend ErrorOr ParseV6(std::string_view s); + + Version version_; + std::array bytes_; + uint32_t scope_id_ = 0; +}; + +struct IPEndpoint { + public: + IPAddress address; + uint16_t port = 0; + + // Used with various socket types to indicate "any" address. + static const IPEndpoint kAnyV4(); + static const IPEndpoint kAnyV6(); + explicit operator bool() const; + + // Parses a text representation of an IPv4/IPv6 address and port (e.g. + // "192.168.0.1:8080" or "[abcd::1234]:8080"). + static ErrorOr Parse(std::string_view s); + + std::string ToString() const; +}; + +bool operator==(const IPEndpoint& a, const IPEndpoint& b); +bool operator!=(const IPEndpoint& a, const IPEndpoint& b); + +bool operator<(const IPEndpoint& a, const IPEndpoint& b); +inline bool operator>(const IPEndpoint& a, const IPEndpoint& b) { + return b < a; +} +inline bool operator<=(const IPEndpoint& a, const IPEndpoint& b) { + return !(a > b); +} +inline bool operator>=(const IPEndpoint& a, const IPEndpoint& b) { + return !(a < b); +} + +// Outputs a string of the form: +// 123.234.34.56 +// or fe80:0000:0000:0000:1234:5678:9abc:def0 +std::ostream& operator<<(std::ostream& out, const IPAddress& address); + +// Outputs a string of the form: +// 123.234.34.56:443 +// or [fe80:0000:0000:0000:1234:5678:9abc:def0]:8080 +std::ostream& operator<<(std::ostream& out, const IPEndpoint& endpoint); + +} // namespace openscreen + +#endif // PLATFORM_BASE_IP_ADDRESS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/location.cc b/breadcast-caststream-sys/vendor/openscreen/platform/base/location.cc new file mode 100644 index 0000000..e7a83df --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/location.cc @@ -0,0 +1,54 @@ +// Copyright (c) 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/base/location.h" + +#include + +#include "platform/base/compiler_specific.h" + +namespace openscreen { + +Location::Location() = default; +Location::Location(const Location&) = default; +Location::Location(Location&&) noexcept = default; + +Location::Location(const void* program_counter) + : program_counter_(program_counter) {} + +Location& Location::operator=(const Location& other) = default; +Location& Location::operator=(Location&& other) = default; + +std::string Location::ToString() const { + if (program_counter_ == nullptr) { + return "pc:nullptr"; + } + + std::ostringstream oss; + oss << "pc:" << program_counter_; + return oss.str(); +} + +#if defined(__GNUC__) +#define RETURN_ADDRESS() \ + __builtin_extract_return_addr(__builtin_return_address(0)) +#else +#define RETURN_ADDRESS() nullptr +#endif + +// static +OSP_NOINLINE Location Location::CreateFromHere() { + return Location(RETURN_ADDRESS()); +} + +// static +OSP_NOINLINE const void* GetProgramCounter() { + return RETURN_ADDRESS(); +} + +std::ostream& operator<<(std::ostream& out, const Location& location) { + return out << location.ToString(); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/location.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/location.h new file mode 100644 index 0000000..09b93a9 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/location.h @@ -0,0 +1,65 @@ +// Copyright (c) 2012 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_LOCATION_H_ +#define PLATFORM_BASE_LOCATION_H_ + +#include + +#include +#include +#include + +namespace openscreen { + +// NOTE: lifted from Chromium's base Location implementation, forked to work +// with our base library. + +// Instances of the location class include basic information about a position +// in program source, for example the place where an object was constructed. +class Location { + public: + Location(); + Location(const Location&); + Location(Location&&) noexcept; + + // Initializes the program counter + explicit Location(const void* program_counter); + + Location& operator=(const Location& other); + Location& operator=(Location&& other); + + // Comparator for hash map insertion. The program counter should uniquely + // identify a location. + bool operator==(const Location& other) const { + return program_counter_ == other.program_counter_; + } + + // The address of the code generating this Location object. Should always be + // valid except for default initialized Location objects, which will be + // nullptr. + const void* program_counter() const { return program_counter_; } + + // Converts to the most user-readable form possible. This will return + // "pc:". + std::string ToString() const; + + static Location CreateFromHere(); + + private: +#if defined(__clang__) + [[clang::annotate("raw_ptr_exclusion")]] +#endif + const void* program_counter_ = nullptr; +}; + +std::ostream& operator<<(std::ostream& out, const Location& location); + +const void* GetProgramCounter(); + +#define CURRENT_LOCATION ::openscreen::Location::CreateFromHere() + +} // namespace openscreen + +#endif // PLATFORM_BASE_LOCATION_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/span.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/span.h new file mode 100644 index 0000000..b2c1506 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/span.h @@ -0,0 +1,39 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_SPAN_H_ +#define PLATFORM_BASE_SPAN_H_ + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "platform/base/type_util.h" + +namespace openscreen { + +// In Open Screen code, use these aliases for the most common types of Spans. +// TODO(crbug.com/364687926): rename to byte_view.h and remove Span alias. +using ByteView = std::span; +using ByteBuffer = std::span; +template +using Span = std::span; + +inline ByteView ByteViewFromString(std::string_view str) { + return ByteView(reinterpret_cast(str.data()), str.size()); +} + +inline std::string ByteViewToString(ByteView bytes) { + return std::string(reinterpret_cast(bytes.data()), bytes.size()); +} + +} // namespace openscreen + +#endif // PLATFORM_BASE_SPAN_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/tls_connect_options.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/tls_connect_options.h new file mode 100644 index 0000000..2a11e71 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/tls_connect_options.h @@ -0,0 +1,20 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_TLS_CONNECT_OPTIONS_H_ +#define PLATFORM_BASE_TLS_CONNECT_OPTIONS_H_ + + +namespace openscreen { + +struct TlsConnectOptions { + // This option allows TLS connections to devices without + // a known hostname, and will typically be “true” for cast code. + // For example, the cast_socket always sets true. + bool unsafely_skip_certificate_validation; +}; + +} // namespace openscreen + +#endif // PLATFORM_BASE_TLS_CONNECT_OPTIONS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/tls_credentials.cc b/breadcast-caststream-sys/vendor/openscreen/platform/base/tls_credentials.cc new file mode 100644 index 0000000..412a0fd --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/tls_credentials.cc @@ -0,0 +1,22 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/base/tls_credentials.h" + +#include + +namespace openscreen { + +TlsCredentials::TlsCredentials() = default; + +TlsCredentials::TlsCredentials(std::vector der_rsa_private_key, + std::vector der_rsa_public_key, + std::vector der_x509_cert) + : der_rsa_private_key(std::move(der_rsa_private_key)), + der_rsa_public_key(std::move(der_rsa_public_key)), + der_x509_cert(std::move(der_x509_cert)) {} + +TlsCredentials::~TlsCredentials() = default; + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/tls_credentials.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/tls_credentials.h new file mode 100644 index 0000000..24d19db --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/tls_credentials.h @@ -0,0 +1,33 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_TLS_CREDENTIALS_H_ +#define PLATFORM_BASE_TLS_CREDENTIALS_H_ + +#include + +#include + +namespace openscreen { + +struct TlsCredentials { + TlsCredentials(); + TlsCredentials(std::vector der_rsa_private_key, + std::vector der_rsa_public_key, + std::vector der_x509_cert); + ~TlsCredentials(); + + // DER-encoded RSA private key. + std::vector der_rsa_private_key; + + // DER-encoded RSA public key. + std::vector der_rsa_public_key; + + // DER-encoded X509 Certificate that is based on the above keys. + std::vector der_x509_cert; +}; + +} // namespace openscreen + +#endif // PLATFORM_BASE_TLS_CREDENTIALS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/tls_listen_options.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/tls_listen_options.h new file mode 100644 index 0000000..6085ba6 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/tls_listen_options.h @@ -0,0 +1,19 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_TLS_LISTEN_OPTIONS_H_ +#define PLATFORM_BASE_TLS_LISTEN_OPTIONS_H_ + +#include + + +namespace openscreen { + +struct TlsListenOptions { + uint32_t backlog_size; +}; + +} // namespace openscreen + +#endif // PLATFORM_BASE_TLS_LISTEN_OPTIONS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_activation.cc b/breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_activation.cc new file mode 100644 index 0000000..9483b79 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_activation.cc @@ -0,0 +1,72 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/base/trace_logging_activation.h" + +#include +#include +#include + +namespace openscreen { + +namespace { + +// If tracing is active, this is a valid pointer to an object that implements +// the TraceLoggingPlatform interface. If tracing is not active, this is +// nullptr. +std::atomic g_current_destination{}; + +// The count of threads currently calling into the current TraceLoggingPlatform. +std::atomic g_use_count{}; + +inline TraceLoggingPlatform* PinCurrentDestination() { + // NOTE: It's important to increment the global use count *before* loading the + // pointer, to ensure the referent is pinned-down (i.e., any thread executing + // StopTracing() stays blocked) until CurrentTracingDestination's destructor + // calls UnpinCurrentDestination(). + g_use_count.fetch_add(1); + return g_current_destination.load(std::memory_order_relaxed); +} + +inline void UnpinCurrentDestination() { + g_use_count.fetch_sub(1); +} + +} // namespace + +void StartTracing(TraceLoggingPlatform* destination) { + assert(destination); + auto* const old_destination = g_current_destination.exchange(destination); + (void)old_destination; // Prevent "unused variable" compiler warnings. + assert(old_destination == nullptr || old_destination == destination); +} + +void StopTracing() { + auto* const old_destination = g_current_destination.exchange(nullptr); + if (!old_destination) { + return; // Already stopped. + } + + // Block the current thread until the global use count goes to zero. At that + // point, there can no longer be any dangling references. Theoretically, this + // loop may never terminate; but in practice, that should never happen. If it + // did happen, that would mean one or more CPU cores are continuously spending + // most of their time executing the TraceLoggingPlatform methods, yet those + // methods are supposed to be super-cheap and take near-zero time to execute! + [[maybe_unused]] int iters = 0; + while (g_use_count.load(std::memory_order_relaxed) != 0) { + assert(iters < 1024); + std::this_thread::yield(); + ++iters; + } +} + +CurrentTracingDestination::CurrentTracingDestination() + : destination_(PinCurrentDestination()) {} + +CurrentTracingDestination::~CurrentTracingDestination() { + UnpinCurrentDestination(); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_activation.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_activation.h new file mode 100644 index 0000000..59fb495 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_activation.h @@ -0,0 +1,58 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_TRACE_LOGGING_ACTIVATION_H_ +#define PLATFORM_BASE_TRACE_LOGGING_ACTIVATION_H_ + +namespace openscreen { + +class TraceLoggingPlatform; + +// Start or Stop trace logging. It is illegal to call StartTracing() a second +// time without having called StopTracing() to stop the prior tracing session. +// +// Note that StopTracing() may block until all threads have returned from any +// in-progress calls into the TraceLoggingPlatform's methods. +void StartTracing(TraceLoggingPlatform* destination); +void StopTracing(); + +// An immutable, non-copyable and non-movable smart pointer that references the +// current trace logging destination. If tracing was active when this class was +// intantiated, the pointer is valid for the life of the instance, and can be +// used to directly invoke the methods of the TraceLoggingPlatform API. If +// tracing was not active when this class was intantiated, the pointer is null +// for the life of the instance and must not be dereferenced. +// +// An instance should be short-lived, as a platform's call to StopTracing() will +// be blocked until there are no instances remaining. +// +// NOTE: This is generally not used directly, but instead via the +// util/trace_logging macros. +class CurrentTracingDestination { + public: + CurrentTracingDestination(); + ~CurrentTracingDestination(); + + explicit operator bool() const noexcept { return !!destination_; } + TraceLoggingPlatform* operator->() const noexcept { return destination_; } + + private: + CurrentTracingDestination(const CurrentTracingDestination&) = delete; + CurrentTracingDestination(CurrentTracingDestination&&) noexcept = delete; + CurrentTracingDestination& operator=(const CurrentTracingDestination&) = + delete; + CurrentTracingDestination& operator=(CurrentTracingDestination&&) noexcept = + delete; + + // The destination at the time this class was constructed, and is valid for + // the lifetime of this class. This is nullptr if tracing was inactive. +#if defined(__clang__) + [[clang::annotate("raw_ptr_exclusion")]] +#endif + TraceLoggingPlatform* const destination_; +}; + +} // namespace openscreen + +#endif // PLATFORM_BASE_TRACE_LOGGING_ACTIVATION_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_types.cc b/breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_types.cc new file mode 100644 index 0000000..1b600b8 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_types.cc @@ -0,0 +1,62 @@ +// Copyright 2022 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/base/trace_logging_types.h" + +#include +#include + +namespace openscreen { + +std::string TraceIdHierarchy::ToString() const { + std::stringstream ss; + ss << "[" << std::hex << (HasRoot() ? root : 0) << ":" + << (HasParent() ? parent : 0) << ":" << (HasCurrent() ? current : 0) + << std::dec << "]"; + + return ss.str(); +} + +std::ostream& operator<<(std::ostream& out, const TraceIdHierarchy& ids) { + return out << ids.ToString(); +} + +bool operator==(const TraceIdHierarchy& lhs, const TraceIdHierarchy& rhs) { + return lhs.current == rhs.current && lhs.parent == rhs.parent && + lhs.root == rhs.root; +} + +bool operator!=(const TraceIdHierarchy& lhs, const TraceIdHierarchy& rhs) { + return !(lhs == rhs); +} + +const char* ToString(TraceCategory category) { + switch (category) { + case TraceCategory::kAny: + return "any"; + case TraceCategory::kMdns: + return "mdns"; + case TraceCategory::kQuic: + return "quic"; + case TraceCategory::kSsl: + return "ssl"; + case TraceCategory::kPresentation: + return "presentation"; + case TraceCategory::kStandaloneReceiver: + return "standalone_receiver"; + case TraceCategory::kDiscovery: + return "discovery"; + case TraceCategory::kStandaloneSender: + return "standalone_sender"; + case TraceCategory::kReceiver: + return "receiver"; + case TraceCategory::kSender: + return "sender"; + } + + // OSP_NOTREACHED is not available in platform/base. + std::abort(); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_types.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_types.h new file mode 100644 index 0000000..a58be35 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/trace_logging_types.h @@ -0,0 +1,71 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_TRACE_LOGGING_TYPES_H_ +#define PLATFORM_BASE_TRACE_LOGGING_TYPES_H_ + +#include + +#include +#include +#include + +namespace openscreen { + +// Define TraceId type here since other TraceLogging files import it. +using TraceId = uint64_t; + +// kEmptyTraceId is the Trace ID when tracing at a global level, not inside any +// tracing block - ie this will be the parent ID for a top level tracing block. +inline constexpr TraceId kEmptyTraceId = 0x0; + +// kUnsetTraceId is the Trace ID passed in to the tracing library when no user- +// specified value is desired. +inline constexpr TraceId kUnsetTraceId = std::numeric_limits::max(); + +// A class to represent the current TraceId Hierarchy and for the user to +// pass around as needed. +struct TraceIdHierarchy { + TraceId current = kUnsetTraceId; + TraceId parent = kUnsetTraceId; + TraceId root = kUnsetTraceId; + + static constexpr TraceIdHierarchy Empty() { + return {kEmptyTraceId, kEmptyTraceId, kEmptyTraceId}; + } + + bool HasCurrent() const { return current != kUnsetTraceId; } + bool HasParent() const { return parent != kUnsetTraceId; } + bool HasRoot() const { return root != kUnsetTraceId; } + + std::string ToString() const; +}; + +std::ostream& operator<<(std::ostream& out, const TraceIdHierarchy& ids); + +bool operator==(const TraceIdHierarchy& lhs, const TraceIdHierarchy& rhs); + +bool operator!=(const TraceIdHierarchy& lhs, const TraceIdHierarchy& rhs); + +// Supported trace category +enum class TraceCategory : int { + kAny, + kMdns, + kQuic, + kSsl, + kPresentation, + kStandaloneReceiver, + kDiscovery, + kStandaloneSender, + kReceiver, + kSender +}; + +const char* ToString(TraceCategory category); + +enum class FlowType { kFlowBegin, kFlowStep, kFlowEnd }; + +} // namespace openscreen + +#endif // PLATFORM_BASE_TRACE_LOGGING_TYPES_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/trivial_clock_traits.cc b/breadcast-caststream-sys/vendor/openscreen/platform/base/trivial_clock_traits.cc new file mode 100644 index 0000000..149be79 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/trivial_clock_traits.cc @@ -0,0 +1,55 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/base/trivial_clock_traits.h" + +namespace openscreen { +namespace { + +constexpr char kMicrosecondsUnits[] = " µs"; +constexpr char kMicrosecondsTicksUnits[] = " µs-ticks"; + +} // namespace + +std::string ToString(const TrivialClockTraits::duration& d) { + return std::to_string(d.count()) + kMicrosecondsUnits; +} + +std::string ToString(const TrivialClockTraits::time_point& tp) { + return std::to_string(tp.time_since_epoch().count()) + + kMicrosecondsTicksUnits; +} + +namespace clock_operators { + +std::ostream& operator<<(std::ostream& os, + const TrivialClockTraits::duration& d) { + return os << d.count() << kMicrosecondsUnits; +} + +std::ostream& operator<<(std::ostream& os, + const TrivialClockTraits::time_point& tp) { + return os << tp.time_since_epoch().count() << kMicrosecondsTicksUnits; +} + +std::ostream& operator<<(std::ostream& os, const std::chrono::hours& hrs) { + return (os << hrs.count() << " hours"); +} + +std::ostream& operator<<(std::ostream& os, const std::chrono::minutes& mins) { + return (os << mins.count() << " minutes"); +} + +std::ostream& operator<<(std::ostream& os, const std::chrono::seconds& secs) { + return (os << secs.count() << " seconds"); +} + +std::ostream& operator<<(std::ostream& os, + const std::chrono::milliseconds& millis) { + return (os << millis.count() << " ms"); +} + +} // namespace clock_operators + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/trivial_clock_traits.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/trivial_clock_traits.h new file mode 100644 index 0000000..aa8747f --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/trivial_clock_traits.h @@ -0,0 +1,98 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_TRIVIAL_CLOCK_TRAITS_H_ +#define PLATFORM_BASE_TRIVIAL_CLOCK_TRAITS_H_ + +#include +#include +#include +#include +#include + +namespace openscreen { + +// The Open Screen monotonic clock traits description, providing all the C++14 +// requirements of a TrivialClock, for use with STL . +class TrivialClockTraits { + public: + // TrivialClock named requirements: std::chrono templates can/may use these. + // NOTE: unless you are specifically integrating with the clock, you probably + // don't want to use these types, and instead should reference the std::chrono + // types directly. + using duration = std::chrono::microseconds; + using rep = duration::rep; + using period = duration::period; + using time_point = std::chrono::time_point; + static constexpr bool is_steady = true; + + // Helper method for named requirements. + template + static constexpr duration to_duration(D d) { + return std::chrono::duration_cast(d); + } + + // Time point values from the clock use microsecond precision, as a reasonably + // high-resolution clock is required. The time source must tick forward at + // least 10000 times per second. + using kRequiredResolution = std::ratio<1, 10000>; + + // In , a clock type is just some type properties plus a static now() + // function. So, there's nothing to instantiate here. + TrivialClockTraits() = delete; + ~TrivialClockTraits() = delete; + + // "Trivially copyable" is necessary for using the time types in + // std::atomic<>. + static_assert(std::is_trivially_copyable(), + "duration is not trivially copyable"); + static_assert(std::is_trivially_copyable(), + "time_point is not trivially copyable"); +}; + +// Convenience type definition, for injecting time sources into classes (e.g., +// &Clock::now versus something else for testing). +using ClockNowFunctionPtr = TrivialClockTraits::time_point (*)(); + +// Convenience for serializing to string, e.g. for tracing. Outputs a string of +// the form "123µs". +std::string ToString(const TrivialClockTraits::duration& d); + +// Convenience for serializing to string, e.g. for tracing. Outputs a string of +// the form "123µs-ticks". +std::string ToString(const TrivialClockTraits::time_point& tp); + +// Explicit namespace for inclusion of custom time-related operator<< +// implementations. These operators may be included in a file for use by adding: +// using clock_operators::operator<<; +// +// NOTE: in some cases, resolution of these operators may still fail, most +// notably in Google Test/Mock when attempting to serialize to an EXPECT_* +// or ASSERT_* call. In this case, the manual "ToString" functions above must +// be called instead. +namespace clock_operators { + +// Logging convenience for durations. Outputs a string of the form "123µs". +std::ostream& operator<<(std::ostream& os, + const TrivialClockTraits::duration& d); + +// Logging convenience for time points. Outputs a string of the form +// "123µs-ticks". +std::ostream& operator<<(std::ostream& os, + const TrivialClockTraits::time_point& tp); + +// Logging (and gtest pretty-printing) for several commonly-used chrono types. +std::ostream& operator<<(std::ostream& os, const std::chrono::hours&); +std::ostream& operator<<(std::ostream& os, const std::chrono::minutes&); +std::ostream& operator<<(std::ostream& os, const std::chrono::seconds&); +std::ostream& operator<<(std::ostream& os, const std::chrono::milliseconds&); +std::ostream& operator<<(std::ostream& os, const std::chrono::microseconds& d); +// Note: The ostream output operator for std::chrono::microseconds is handled by +// the one for TrivialClockTraits::duration above since they are the same type. + +} // namespace clock_operators + +} // namespace openscreen + +#endif // PLATFORM_BASE_TRIVIAL_CLOCK_TRAITS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/type_util.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/type_util.h new file mode 100644 index 0000000..fab0f6a --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/type_util.h @@ -0,0 +1,24 @@ +// Copyright 2024 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_TYPE_UTIL_H_ +#define PLATFORM_BASE_TYPE_UTIL_H_ + +#include + +// File for defining generally useful type predicates for templatized classes +// and functions. +namespace openscreen::internal { + +template +using EnableIfArithmetic = + std::enable_if_t::value>; // NOLINT + +template +using EnableIfConvertible = std::enable_if_t< + std::is_convertible::value>; // NOLINT + +} // namespace openscreen::internal + +#endif // PLATFORM_BASE_TYPE_UTIL_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/udp_packet.cc b/breadcast-caststream-sys/vendor/openscreen/platform/base/udp_packet.cc new file mode 100644 index 0000000..36207c1 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/udp_packet.cc @@ -0,0 +1,30 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/base/udp_packet.h" + +#include +#include + +namespace openscreen { + +UdpPacket::UdpPacket() : std::vector() {} + +UdpPacket::UdpPacket(size_type size, uint8_t fill_value) + : std::vector(size, fill_value) { + assert(size <= kUdpMaxPacketSize); +} + +UdpPacket::UdpPacket(UdpPacket&& other) noexcept = default; + +UdpPacket::UdpPacket(std::initializer_list init) + : std::vector(init) { + assert(size() <= kUdpMaxPacketSize); +} + +UdpPacket::~UdpPacket() = default; + +UdpPacket& UdpPacket::operator=(UdpPacket&& other) = default; + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/base/udp_packet.h b/breadcast-caststream-sys/vendor/openscreen/platform/base/udp_packet.h new file mode 100644 index 0000000..b71c618 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/base/udp_packet.h @@ -0,0 +1,54 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_BASE_UDP_PACKET_H_ +#define PLATFORM_BASE_UDP_PACKET_H_ + +#include + +#include +#include +#include + +#include "platform/base/ip_address.h" + +namespace openscreen { + +// A move-only std::vector of bytes that may not exceed the maximum possible +// size of a UDP packet. Implicit copy construction/assignment is disabled to +// prevent hidden copies (i.e., those not explicitly coded). +class UdpPacket : public std::vector { + public: + // C++14 vector constructors, sans Allocator foo, and no copy ctor. + UdpPacket(); + explicit UdpPacket(size_type size, uint8_t fill_value = {}); + template + UdpPacket(InputIt first, InputIt last) : std::vector(first, last) {} + UdpPacket(std::initializer_list init); + UdpPacket(const UdpPacket&) = delete; + UdpPacket(UdpPacket&& other) noexcept; + + ~UdpPacket(); + + UdpPacket& operator=(UdpPacket&& other); + UdpPacket& operator=(const UdpPacket&) = delete; + + const IPEndpoint& source() const { return source_; } + void set_source(IPEndpoint endpoint) { source_ = std::move(endpoint); } + + const IPEndpoint& destination() const { return destination_; } + void set_destination(IPEndpoint endpoint) { + destination_ = std::move(endpoint); + } + + static constexpr size_type kUdpMaxPacketSize = 1 << 16; + + private: + IPEndpoint source_ = {}; + IPEndpoint destination_ = {}; +}; + +} // namespace openscreen + +#endif // PLATFORM_BASE_UDP_PACKET_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/logging.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/logging.h new file mode 100644 index 0000000..a99ef55 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/logging.h @@ -0,0 +1,32 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_LOGGING_H_ +#define PLATFORM_IMPL_LOGGING_H_ + +#include + +#include "util/osp_logging.h" + +namespace openscreen { + +// Append logging output to a named FIFO having the given `filename`. If the +// file does not exist, an attempt is made to auto-create it. If unsuccessful, +// abort the program. If this is never called, logging continues to output to +// the default destination (stderr). +void SetLogFifoOrDie(const char* filename); + +// Set the global logging level. If this is never called, kWarning is the +// default. +void SetLogLevel(LogLevel level); + +// Returns the current global logging level. +LogLevel GetLogLevel(); + +// Log a trace message. Used by the text trace logging platform. +void LogTraceMessage(const std::string& message); + +} // namespace openscreen + +#endif // PLATFORM_IMPL_LOGGING_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/logging_posix.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/logging_posix.cc new file mode 100644 index 0000000..20f6844 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/logging_posix.cc @@ -0,0 +1,161 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "build/build_config.h" +#include "platform/impl/logging.h" +#include "platform/impl/logging_test.h" +#include "util/trace_logging.h" + +#if OSP_DCHECK_IS_ON() +#include + +#include +#endif + +namespace openscreen { +namespace { + +int g_log_fd = STDERR_FILENO; +LogLevel g_log_level = LogLevel::kWarning; +std::vector* g_log_messages_for_test = nullptr; + +std::ostream& operator<<(std::ostream& os, const LogLevel& level) { + const char* level_string = ""; + switch (level) { + case LogLevel::kVerbose: + level_string = "VERBOSE"; + break; + case LogLevel::kInfo: + level_string = "INFO"; + break; + case LogLevel::kWarning: + level_string = "WARNING"; + break; + case LogLevel::kError: + level_string = "ERROR"; + break; + case LogLevel::kFatal: + level_string = "FATAL"; + break; + } + os << level_string; + return os; +} + +} // namespace + +void SetLogFifoOrDie(const char* filename) { + if (g_log_fd != STDERR_FILENO) { + close(g_log_fd); + g_log_fd = STDERR_FILENO; + } + + // Note: The use of OSP_CHECK/OSP_LOG_* here will log to stderr. + struct stat st {}; + int open_result = -1; + if (stat(filename, &st) == -1 && errno == ENOENT) { + if (mkfifo(filename, 0644) == 0) { + open_result = open(filename, O_WRONLY); + OSP_CHECK_NE(open_result, -1) + << "open(" << filename << ") failed: " << strerror(errno); + } else { + OSP_LOG_FATAL << "mkfifo(" << filename << ") failed: " << strerror(errno); + } + } else if (S_ISFIFO(st.st_mode)) { + open_result = open(filename, O_WRONLY); + OSP_CHECK_NE(open_result, -1) + << "open(" << filename << ") failed: " << strerror(errno); + } else { + OSP_LOG_FATAL << "not a FIFO special file: " << filename; + } + + // Direct all logging to the opened FIFO file. + g_log_fd = open_result; +} + +void SetLogLevel(LogLevel level) { + g_log_level = level; +} + +LogLevel GetLogLevel() { + return g_log_level; +} + +bool IsLoggingOn(LogLevel level, const std::string_view file) { + // Possible future enhancement: Use glob patterns passed on the command-line + // to use a different logging level for certain files, like in Chromium. + return level >= g_log_level; +} + +void LogWithLevel(LogLevel level, + const char* file, + int line, + std::stringstream message) { + if (level < g_log_level) + return; + + std::stringstream ss; + ss << "[" << level << ":" << file << "(" << line << "):T" << std::hex + << TRACE_CURRENT_ID << "] " << message.rdbuf() << std::endl; + +// NOTE: backtrace() is only supported in modern versions of Android (33+), so +// it is just disabled here. +#if OSP_DCHECK_IS_ON() && !BUILDFLAG(IS_ANDROID) + if (level == LogLevel::kFatal) { + constexpr size_t kMaxCallstackSize = 128; + std::array callstack = {}; + + // Get the return addresses and attempt to symbolize them. + const int num_frames = backtrace(callstack.data(), callstack.size()); + char** strs = backtrace_symbols(callstack.data(), num_frames); + + if (num_frames > 0) { + ss << "Debug stack trace for fatal error:" << std::endl; + for (int i = 0; i < num_frames; ++i) { + ss << strs[i] << std::endl; + } + } + free(strs); + } +#endif + const auto ss_str = ss.str(); + const auto bytes_written = write(g_log_fd, ss_str.c_str(), ss_str.size()); + OSP_CHECK(bytes_written); + if (g_log_messages_for_test) { + g_log_messages_for_test->push_back(ss_str); + } +} + +void LogTraceMessage(const std::string& message) { + const std::string to_write = message + '\n'; + const auto bytes_written = write(g_log_fd, to_write.c_str(), to_write.size()); + OSP_CHECK(bytes_written); +} + +[[noreturn]] void Break() { +// Generally this will just resolve to an abort anyways, but gives the +// compiler a chance to perform a more appropriate, target specific trap +// as appropriate. +#if defined(_DEBUG) + __builtin_trap(); +#else + std::abort(); +#endif +} + +void SetLogBufferForTest(std::vector* messages) { + g_log_messages_for_test = messages; +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/logging_test.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/logging_test.h new file mode 100644 index 0000000..13171fd --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/logging_test.h @@ -0,0 +1,24 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_LOGGING_TEST_H_ +#define PLATFORM_IMPL_LOGGING_TEST_H_ + +#include +#include + +// These functions should only be called from logging unittests. + +namespace openscreen { + +// Append logging output to `messages`. Each log entry will be written as a new +// element including a newline. Pass nullptr to stop appending output. Calling +// this does not affect the behavior of SetLogFifoOrDie(). Normally this should +// only be called for tests as it creates an append-only buffer of log messages +// in memory. +void SetLogBufferForTest(std::vector* messages); + +} // namespace openscreen + +#endif // PLATFORM_IMPL_LOGGING_TEST_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/network_interface.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/network_interface.cc new file mode 100644 index 0000000..727cc30 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/network_interface.cc @@ -0,0 +1,31 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/impl/network_interface.h" + +#include "platform/base/ip_address.h" +#include "util/std_util.h" + +namespace openscreen { + +// Returns an InterfaceInfo associated with the system's loopback interface. +std::optional GetLoopbackInterfaceForTesting() { + const std::vector interfaces = GetNetworkInterfaces(); + auto it = std::find_if( + interfaces.begin(), interfaces.end(), [](const InterfaceInfo& info) { + return info.type == InterfaceInfo::Type::kLoopback && + ContainsIf(info.addresses, [](const IPSubnet& subnet) { + return subnet.address == IPAddress::kV4LoopbackAddress() || + subnet.address == IPAddress::kV6LoopbackAddress(); + }); + }); + + if (it == interfaces.end()) { + return std::nullopt; + } else { + return *it; + } +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/network_interface.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/network_interface.h new file mode 100644 index 0000000..d7ae1c6 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/network_interface.h @@ -0,0 +1,23 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_NETWORK_INTERFACE_H_ +#define PLATFORM_IMPL_NETWORK_INTERFACE_H_ + +#include +#include + +#include "platform/base/interface_info.h" + +namespace openscreen { + +// Implements the platform API. +std::vector GetNetworkInterfaces(); + +// Returns the system's loopback interface. Used for unit tests. +std::optional GetLoopbackInterfaceForTesting(); + +} // namespace openscreen + +#endif // PLATFORM_IMPL_NETWORK_INTERFACE_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/network_interface_linux.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/network_interface_linux.cc new file mode 100644 index 0000000..f6091a0 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/network_interface_linux.cc @@ -0,0 +1,384 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// clang-format: off +#include +#include +// clang-format: on + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "platform/api/network_interface.h" +#include "platform/base/ip_address.h" +#include "platform/base/span.h" +#include "platform/impl/network_interface.h" +#include "platform/impl/scoped_pipe.h" +#include "util/osp_logging.h" + +namespace openscreen { +namespace { + +constexpr int kNetlinkRecvmsgBufSize = 8192; + +// Safely reads the system name for the interface from the (probably) +// null-terminated string `kernel_name` and returns a std::string. +std::string GetInterfaceName(std::string_view kernel_name) { + OSP_CHECK_LT(kernel_name.length(), IFNAMSIZ); + return std::string(kernel_name); +} + +// Returns the type of the interface identified by the name `ifname`, if it can +// be determined, otherwise returns InterfaceInfo::Type::kOther. +InterfaceInfo::Type GetInterfaceType(const std::string& ifname) { + // Determine type after name has been set. + ScopedFd s(socket(AF_INET6, SOCK_DGRAM, 0)); + if (!s) { + s = ScopedFd(socket(AF_INET, SOCK_DGRAM, 0)); + if (!s) + return InterfaceInfo::Type::kOther; + } + + // Note: This uses Wireless Extensions to test the interface, which is + // deprecated. However, it's much easier than using the new nl80211 + // interface for this purpose. If Wireless Extensions are ever actually + // removed though, this will need to use nl80211. + struct iwreq wr {}; + static_assert(sizeof(wr.ifr_name) == IFNAMSIZ, + "expected size of interface name fields"); + OSP_CHECK_LT(ifname.size(), IFNAMSIZ); + wr.ifr_name[IFNAMSIZ - 1] = 0; + strncpy(wr.ifr_name, ifname.c_str(), IFNAMSIZ - 1); + if (ioctl(s.get(), SIOCGIWNAME, &wr) != -1) + return InterfaceInfo::Type::kWifi; + + struct ethtool_cmd ecmd {}; + ecmd.cmd = ETHTOOL_GSET; + struct ifreq ifr {}; + static_assert(sizeof(ifr.ifr_name) == IFNAMSIZ, + "expected size of interface name fields"); + OSP_CHECK_LT(ifname.size(), IFNAMSIZ); + wr.ifr_name[IFNAMSIZ - 1] = 0; + strncpy(ifr.ifr_name, ifname.c_str(), IFNAMSIZ - 1); + ifr.ifr_data = reinterpret_cast(&ecmd); + if (ioctl(s.get(), SIOCETHTOOL, &ifr) != -1) { + return InterfaceInfo::Type::kEthernet; + } + + return InterfaceInfo::Type::kOther; +} + +// Reads an interface's name, hardware address, and type from `rta` and places +// the results in `info`. `rta` is the first attribute structure returned as +// part of an RTM_NEWLINK message. `attrlen` is the total length of the buffer +// pointed to by `rta`. +void GetInterfaceAttributes(struct rtattr* rta, + unsigned int attrlen, + bool is_loopback, + InterfaceInfo* info) { + for (; RTA_OK(rta, attrlen); rta = RTA_NEXT(rta, attrlen)) { + if (rta->rta_type == IFLA_IFNAME) { + info->name = + GetInterfaceName(reinterpret_cast(RTA_DATA(rta))); + } else if (rta->rta_type == IFLA_ADDRESS) { + ByteView address_bytes(reinterpret_cast(RTA_DATA(rta)), + RTA_PAYLOAD(rta)); + info->hardware_address.assign(address_bytes.begin(), address_bytes.end()); + } + } + + if (is_loopback) { + info->type = InterfaceInfo::Type::kLoopback; + } else { + info->type = GetInterfaceType(info->name); + } +} + +// Reads the IPv4 or IPv6 address that comes from an RTM_NEWADDR message and +// places the result in `address`. `rta` is the first attribute structure +// returned by the message and `attrlen` is the total length of the buffer +// pointed to by `rta`. `ifname` is the name of the interface to which we +// believe the address belongs based on interface index matching. It is only +// used for sanity checking. +std::optional GetIPAddressOrNull(struct rtattr* rta, + unsigned int attrlen, + IPAddress::Version version, + const std::string& ifname) { + const size_t expected_address_size = version == IPAddress::Version::kV4 + ? IPAddress::kV4Size + : IPAddress::kV6Size; + + bool have_local = false; + IPAddress address; + IPAddress local; + for (; RTA_OK(rta, attrlen); rta = RTA_NEXT(rta, attrlen)) { + if (rta->rta_type == IFA_LABEL) { + const char* const label = reinterpret_cast(RTA_DATA(rta)); + if (ifname != label) { + OSP_LOG_ERROR << "Interface label mismatch! Expected: " << ifname + << ", Have: " << label; + return std::nullopt; + } + } else if (rta->rta_type == IFA_ADDRESS) { + OSP_CHECK_EQ(expected_address_size, RTA_PAYLOAD(rta)); + address = + IPAddress(version, std::span(static_cast(RTA_DATA(rta)), + expected_address_size)); + } else if (rta->rta_type == IFA_LOCAL) { + OSP_CHECK_EQ(expected_address_size, RTA_PAYLOAD(rta)); + have_local = true; + local = IPAddress(version, std::span(static_cast(RTA_DATA(rta)), + expected_address_size)); + } + } + return have_local ? local : address; +} + +std::vector GetLinkInfo() { + ScopedFd fd(socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)); + if (!fd) { + OSP_LOG_WARN << "netlink socket() failed: " << errno << " - " + << strerror(errno); + return {}; + } + + { + // nl_pid = 0 for the kernel. + struct sockaddr_nl peer {}; + peer.nl_family = AF_NETLINK; + struct { + struct nlmsghdr header {}; + struct ifinfomsg msg {}; + } request; + + request.header.nlmsg_len = sizeof(request); + request.header.nlmsg_type = RTM_GETLINK; + request.header.nlmsg_flags = NLM_F_REQUEST | NLM_F_ROOT; + request.header.nlmsg_seq = 0; + request.header.nlmsg_pid = 0; + request.msg.ifi_family = AF_UNSPEC; + struct iovec iov { + &request, request.header.nlmsg_len + }; + struct msghdr msg {}; + msg.msg_name = &peer; + msg.msg_namelen = sizeof(peer); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = nullptr; + msg.msg_controllen = 0; + msg.msg_flags = 0; + if (sendmsg(fd.get(), &msg, 0) < 0) { + OSP_LOG_ERROR << "netlink sendmsg() failed: " << errno << " - " + << strerror(errno); + return {}; + } + } + + std::vector info_list; + { + char buf[kNetlinkRecvmsgBufSize]{}; + struct iovec iov { + buf, sizeof(buf) + }; + struct sockaddr_nl source_address {}; + struct msghdr msg {}; + struct nlmsghdr* netlink_header = nullptr; + + msg.msg_name = &source_address; + msg.msg_namelen = sizeof(source_address); + msg.msg_iov = &iov; + msg.msg_iovlen = 1, msg.msg_control = nullptr, msg.msg_controllen = 0, + msg.msg_flags = 0; + + bool done = false; + while (!done) { + size_t len = recvmsg(fd.get(), &msg, 0); + + for (netlink_header = reinterpret_cast(buf); + NLMSG_OK(netlink_header, len); + netlink_header = NLMSG_NEXT(netlink_header, len)) { + // The end of multipart message. + if (netlink_header->nlmsg_type == NLMSG_DONE) { + done = true; + break; + } else if (netlink_header->nlmsg_type == NLMSG_ERROR) { + done = true; + OSP_LOG_ERROR << "netlink error msg: " + << reinterpret_cast( + NLMSG_DATA(netlink_header)) + ->error; + continue; + } else if ((netlink_header->nlmsg_flags & NLM_F_MULTI) == 0) { + // If this is not a multi-part message, we don't need to wait for an + // NLMSG_DONE message; this is the only message. + done = true; + } + + // RTM_NEWLINK messages describe existing network links on the host. + if (netlink_header->nlmsg_type != RTM_NEWLINK) + continue; + + struct ifinfomsg* interface_info = + static_cast(NLMSG_DATA(netlink_header)); + // Only process interfaces which are active (up). + if (!(interface_info->ifi_flags & IFF_UP)) { + continue; + } + + info_list.emplace_back(); + InterfaceInfo& info = info_list.back(); + info.index = interface_info->ifi_index; + GetInterfaceAttributes(IFLA_RTA(interface_info), + IFLA_PAYLOAD(netlink_header), + interface_info->ifi_flags & IFF_LOOPBACK, &info); + } + } + } + + return info_list; +} + +void PopulateSubnetsOrClearList(std::vector& info_list) { + ScopedFd fd(socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE)); + if (!fd) { + OSP_LOG_ERROR << "netlink socket() failed: " << errno << " - " + << strerror(errno); + info_list.clear(); + return; + } + + { + // nl_pid = 0 for the kernel. + struct sockaddr_nl peer {}; + peer.nl_family = AF_NETLINK; + struct { + struct nlmsghdr header {}; + struct ifaddrmsg msg {}; + } request; + + request.header.nlmsg_len = sizeof(request); + request.header.nlmsg_type = RTM_GETADDR; + request.header.nlmsg_flags = NLM_F_REQUEST | NLM_F_ROOT; + request.header.nlmsg_seq = 1; + request.header.nlmsg_pid = 0; + request.msg.ifa_family = AF_UNSPEC; + struct iovec iov { + &request, request.header.nlmsg_len + }; + struct msghdr msg {}; + msg.msg_name = &peer; + msg.msg_namelen = sizeof(peer); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = nullptr; + msg.msg_controllen = 0; + msg.msg_flags = 0; + if (sendmsg(fd.get(), &msg, 0) < 0) { + OSP_LOG_ERROR << "sendmsg failed: " << errno << " - " << strerror(errno); + info_list.clear(); + return; + } + } + + { + char buf[kNetlinkRecvmsgBufSize]{}; + struct iovec iov { + buf, sizeof(buf) + }; + struct sockaddr_nl source_address {}; + struct msghdr msg {}; + struct nlmsghdr* netlink_header = nullptr; + + msg.msg_name = &source_address; + msg.msg_namelen = sizeof(source_address); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = nullptr; + msg.msg_controllen = 0; + msg.msg_flags = 0; + bool done = false; + while (!done) { + size_t len = recvmsg(fd.get(), &msg, 0); + + for (netlink_header = reinterpret_cast(buf); + NLMSG_OK(netlink_header, len); + netlink_header = NLMSG_NEXT(netlink_header, len)) { + if (netlink_header->nlmsg_type == NLMSG_DONE) { + done = true; + break; + } else if (netlink_header->nlmsg_type == NLMSG_ERROR) { + done = true; + OSP_LOG_ERROR << "netlink error msg: " + << reinterpret_cast( + NLMSG_DATA(netlink_header)) + ->error; + continue; + } else if ((netlink_header->nlmsg_flags & NLM_F_MULTI) == 0) { + // If this is not a multi-part message, we don't need to wait for an + // NLMSG_DONE message; this is the only message. + done = true; + } + + if (netlink_header->nlmsg_type != RTM_NEWADDR) + continue; + + struct ifaddrmsg* interface_address = + static_cast(NLMSG_DATA(netlink_header)); + + const auto it = std::find_if( + info_list.begin(), info_list.end(), + [index = interface_address->ifa_index](const InterfaceInfo& info) { + return info.index == index; + }); + if (it == info_list.end()) { + OSP_DVLOG << "skipping address for interface " + << interface_address->ifa_index; + continue; + } + + if (interface_address->ifa_family == AF_INET || + interface_address->ifa_family == AF_INET6) { + const auto address_or_null = GetIPAddressOrNull( + IFA_RTA(interface_address), IFA_PAYLOAD(netlink_header), + interface_address->ifa_family == AF_INET + ? IPAddress::Version::kV4 + : IPAddress::Version::kV6, + it->name); + if (address_or_null) { + it->addresses.emplace_back(*address_or_null, + interface_address->ifa_prefixlen); + } + } else { + OSP_LOG_ERROR << "Unknown address family: " + << interface_address->ifa_family; + } + } + } + } +} + +} // namespace + +std::vector GetNetworkInterfaces() { + std::vector interfaces = GetLinkInfo(); + PopulateSubnetsOrClearList(interfaces); + return interfaces; +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/platform_client_posix.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/platform_client_posix.cc new file mode 100644 index 0000000..9c349ff --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/platform_client_posix.cc @@ -0,0 +1,137 @@ +// Copyright (c) 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/impl/platform_client_posix.h" + +#include +#include +#include +#include + +#include "platform/base/trivial_clock_traits.h" +#include "platform/impl/udp_socket_reader_posix.h" + +namespace openscreen { + +using clock_operators::operator<<; + +// static +PlatformClientPosix* PlatformClientPosix::instance_ = nullptr; + +// static +void PlatformClientPosix::Create(Clock::duration networking_operation_timeout, + std::unique_ptr task_runner) { + SetInstance(new PlatformClientPosix(networking_operation_timeout, + std::move(task_runner))); +} + +// static +void PlatformClientPosix::Create(Clock::duration networking_operation_timeout) { + SetInstance(new PlatformClientPosix(networking_operation_timeout)); +} + +// static +void PlatformClientPosix::ShutDown() { + OSP_CHECK(instance_); + delete instance_; + instance_ = nullptr; +} + +UdpSocketReaderPosix* PlatformClientPosix::udp_socket_reader() { + std::call_once(udp_socket_reader_initialization_, [this]() { + udp_socket_reader_ = + std::make_unique(*socket_handle_waiter()); + }); + return udp_socket_reader_.get(); +} + +TaskRunner& PlatformClientPosix::GetTaskRunner() { + return *task_runner_; +} + +PlatformClientPosix::~PlatformClientPosix() { + OSP_DVLOG << "Shutting down the Task Runner..."; + task_runner_->RequestStopSoon(); + if (task_runner_thread_ && task_runner_thread_->joinable()) { + task_runner_thread_->join(); + OSP_DVLOG << "\tTask Runner shutdown complete!"; + } + + OSP_DVLOG << "Shutting down network operations..."; + networking_loop_running_.store(false); + networking_loop_thread_.join(); + OSP_DVLOG << "\tNetwork operation shutdown complete!"; +} + +// static +void PlatformClientPosix::SetInstance(PlatformClientPosix* instance) { + OSP_CHECK(!instance_); + instance_ = instance; +} + +PlatformClientPosix::PlatformClientPosix( + Clock::duration networking_operation_timeout) + : task_runner_(new TaskRunnerImpl(Clock::now)), + networking_loop_timeout_(networking_operation_timeout), + networking_loop_thread_(&PlatformClientPosix::RunNetworkLoopUntilStopped, + this), + task_runner_thread_( + std::thread(&TaskRunnerImpl::RunUntilStopped, task_runner_.get())) {} + +PlatformClientPosix::PlatformClientPosix( + Clock::duration networking_operation_timeout, + std::unique_ptr task_runner) + : task_runner_(std::move(task_runner)), + networking_loop_timeout_(networking_operation_timeout), + networking_loop_thread_(&PlatformClientPosix::RunNetworkLoopUntilStopped, + this) {} + +SocketHandleWaiterPosix* PlatformClientPosix::socket_handle_waiter() { + std::call_once(waiter_initialization_, [this]() { + waiter_ = std::make_unique(&Clock::now); + waiter_created_.store(true); + }); + return waiter_.get(); +} + +void PlatformClientPosix::RunNetworkLoopUntilStopped() { +#if OSP_DCHECK_IS_ON() + Clock::time_point last_time = Clock::now(); + int iterations = 0; +#endif + while (networking_loop_running_.load()) { +#if OSP_DCHECK_IS_ON() + ++iterations; + const Clock::time_point current_time = Clock::now(); + const Clock::duration delta = current_time - last_time; + if (delta > std::chrono::seconds(1)) { + OSP_DCHECK_GT(iterations, 0); + OSP_VLOG << "network loop execution time averaged " + << (delta / iterations) << " over the last second."; + last_time = current_time; + iterations = 0; + } +#endif + if (!waiter_created_.load()) { + std::this_thread::sleep_for(networking_loop_timeout_); + continue; + } + const Error process_error = + socket_handle_waiter()->ProcessHandles(networking_loop_timeout_); + + // We may receive an "again" error code if there were no sockets to process. + if (process_error.code() == Error::Code::kAgain) { + std::this_thread::sleep_for(networking_loop_timeout_); + continue; + + // If there is a socket error it should be handled elsewhere. Just log + // the error here. + } else if (!process_error.ok()) { + OSP_LOG_ERROR << "error occurred while processing handles. error=" + << process_error; + } + } +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/platform_client_posix.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/platform_client_posix.h new file mode 100644 index 0000000..2abe7d6 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/platform_client_posix.h @@ -0,0 +1,130 @@ +// Copyright (c) 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_PLATFORM_CLIENT_POSIX_H_ +#define PLATFORM_IMPL_PLATFORM_CLIENT_POSIX_H_ + +#include +#include +#include +#include +#include +#include + +#include "platform/api/time.h" +#include "platform/impl/socket_handle_waiter_posix.h" +#include "platform/impl/task_runner.h" + +// LOCAL PATCH (breadcast): upstream also wires up a TlsDataRouterPosix here +// for TlsConnectionFactory support. breadcast only uses this vendored subset +// of openscreen for the Cast Streaming UDP RTP/RTCP data path -- the TLS +// CASTV2 control channel is handled by the existing rust_cast-based Rust +// code -- so the TLS data router (and the BoringSSL-flavored util/crypto/* +// helpers it pulls in) has been stripped entirely rather than ported. See +// vendor/openscreen/PATCHES.md. + +namespace openscreen { + +class UdpSocketReaderPosix; + +// Creates and provides access to singletons used by the default platform +// implementation. An instance must be created before an application uses any +// public modules in the Open Screen Library. +// +// ShutDown() should be called to destroy the PlatformClientPosix's singletons +// and TaskRunner to save resources when library APIs are not in use. +// ShutDown() calls TaskRunner::RunUntilStopped() to run any pending cleanup +// tasks. +// +// Create and ShutDown must be called in the same sequence. +// +// FIXME: Remove Create and Shutdown and use the ctor/dtor directly. +class PlatformClientPosix { + public: + // Initializes the platform implementation. + // + // `networking_loop_interval` sets the minimum amount of time that should pass + // between iterations of the loop used to handle networking operations. Higher + // values will result in less time being spent on these operations, but also + // less performant networking operations. Be careful setting values larger + // than a few hundred microseconds. + // + // `networking_operation_timeout` sets how much time may be spent on a + // single networking operation type. + // + // `task_runner` is a client-provided TaskRunner implementation. + static void Create(Clock::duration networking_operation_timeout, + std::unique_ptr task_runner); + + // Initializes the platform implementation and creates a new TaskRunner (which + // starts a new thread). + static void Create(Clock::duration networking_operation_timeout); + + // Shuts down and deletes the PlatformClient instance currently stored as a + // singleton. This method is expected to be called before program exit. After + // calling this method, if the client wishes to continue using the platform + // library, Create() must be called again. + static void ShutDown(); + + static PlatformClientPosix* GetInstance() { return instance_; } + + PlatformClientPosix(const PlatformClientPosix&) = delete; + PlatformClientPosix(PlatformClientPosix&&) noexcept = delete; + PlatformClientPosix& operator=(const PlatformClientPosix&) = delete; + PlatformClientPosix& operator=(PlatformClientPosix&&) = delete; + + // This method is thread-safe. + // FIXME: Rename to GetUdpSocketReader() + UdpSocketReaderPosix* udp_socket_reader(); + + // Returns the TaskRunner associated with this PlatformClient. + // NOTE: This method is expected to be thread safe. + TaskRunner& GetTaskRunner(); + + protected: + // Called by ShutDown(). + ~PlatformClientPosix(); + + static void SetInstance(PlatformClientPosix* client); + + private: + explicit PlatformClientPosix(Clock::duration networking_operation_timeout); + + PlatformClientPosix(Clock::duration networking_operation_timeout, + std::unique_ptr task_runner); + + // This method is thread-safe. + SocketHandleWaiterPosix* socket_handle_waiter(); + + void RunNetworkLoopUntilStopped(); + + std::unique_ptr task_runner_; + + // Track whether the associated instance variable has been created yet. + std::atomic_bool waiter_created_{false}; + + // Parameters for networking loop. + std::atomic_bool networking_loop_running_{true}; + Clock::duration networking_loop_timeout_; + + // Flags used to ensure that initialization of below instance objects occurs + // only once across all threads. + std::once_flag waiter_initialization_; + std::once_flag udp_socket_reader_initialization_; + + // Instance objects are created at runtime when they are first needed. + std::unique_ptr waiter_; + std::unique_ptr udp_socket_reader_; + + // Threads for running TaskRunner and OperationLoop instances. + // NOTE: These must be declared last to avoid nondterministic failures. + std::thread networking_loop_thread_; + std::optional task_runner_thread_; + + static PlatformClientPosix* instance_; +}; + +} // namespace openscreen + +#endif // PLATFORM_IMPL_PLATFORM_CLIENT_POSIX_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/scoped_pipe.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/scoped_pipe.h new file mode 100644 index 0000000..04c7f25 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/scoped_pipe.h @@ -0,0 +1,71 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_SCOPED_PIPE_H_ +#define PLATFORM_IMPL_SCOPED_PIPE_H_ + +#include + +#include + +namespace openscreen { + +struct IntFdTraits { + using PipeType = int; + static constexpr int kInvalidValue = -1; + + static void Close(PipeType pipe) { close(pipe); } +}; + +// This class wraps file descriptor and uses RAII to ensure it is closed +// properly when control leaves its scope. It is parameterized by a traits type +// which defines the value type of the file descriptor, an invalid value, and a +// closing function. +// +// This class is move-only as it represents ownership of the wrapped file +// descriptor. It is not thread-safe. +template +class ScopedPipe { + public: + using PipeType = typename Traits::PipeType; + + ScopedPipe() : pipe_(Traits::kInvalidValue) {} + explicit ScopedPipe(PipeType pipe) : pipe_(pipe) {} + ScopedPipe(const ScopedPipe&) = delete; + ScopedPipe(ScopedPipe&& other) : pipe_(other.release()) {} + ~ScopedPipe() { + if (pipe_ != Traits::kInvalidValue) + Traits::Close(release()); + } + + ScopedPipe& operator=(ScopedPipe&& other) { + if (pipe_ != Traits::kInvalidValue) + Traits::Close(release()); + pipe_ = other.release(); + return *this; + } + + PipeType get() const { return pipe_; } + PipeType release() { + PipeType pipe = pipe_; + pipe_ = Traits::kInvalidValue; + return pipe; + } + + bool operator==(const ScopedPipe& other) const { + return pipe_ == other.pipe_; + } + bool operator!=(const ScopedPipe& other) const { return !(*this == other); } + + explicit operator bool() const { return pipe_ != Traits::kInvalidValue; } + + private: + PipeType pipe_; +}; + +using ScopedFd = ScopedPipe; + +} // namespace openscreen + +#endif // PLATFORM_IMPL_SCOPED_PIPE_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_address_posix.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_address_posix.cc new file mode 100644 index 0000000..cc6e6eb --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_address_posix.cc @@ -0,0 +1,129 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/impl/socket_address_posix.h" + +#include +#include + +#include "util/osp_logging.h" + +namespace openscreen { + +SocketAddressPosix::SocketAddressPosix(const struct sockaddr& address) { + if (address.sa_family == AF_INET) { + std::copy_n(reinterpret_cast(&address), + sizeof(struct sockaddr_in), + reinterpret_cast(&internal_address_.v4)); + RecomputeEndpoint(IPAddress::Version::kV4); + } else if (address.sa_family == AF_INET6) { + std::copy_n(reinterpret_cast(&address), + sizeof(struct sockaddr_in6), + reinterpret_cast(&internal_address_.v6)); + RecomputeEndpoint(IPAddress::Version::kV6); + } else { + // Not IPv4 or IPv6. + OSP_NOTREACHED(); + } +} + +SocketAddressPosix::SocketAddressPosix(const IPEndpoint& endpoint) + : endpoint_(endpoint) { + if (endpoint.address.IsV4()) { + internal_address_.v4 = ToSockAddrIn(endpoint); + } else { + internal_address_.v6 = ToSockAddrIn6(endpoint); + } +} + +struct sockaddr* SocketAddressPosix::address() { + switch (version()) { + case IPAddress::Version::kV4: + return reinterpret_cast(&internal_address_.v4); + case IPAddress::Version::kV6: + return reinterpret_cast(&internal_address_.v6); + default: + OSP_NOTREACHED(); + } +} + +const struct sockaddr* SocketAddressPosix::address() const { + switch (version()) { + case IPAddress::Version::kV4: + return reinterpret_cast(&internal_address_.v4); + case IPAddress::Version::kV6: + return reinterpret_cast(&internal_address_.v6); + default: + OSP_NOTREACHED(); + } +} + +socklen_t SocketAddressPosix::size() const { + switch (version()) { + case IPAddress::Version::kV4: + return sizeof(struct sockaddr_in); + case IPAddress::Version::kV6: + return sizeof(struct sockaddr_in6); + default: + OSP_NOTREACHED(); + } +} + +void SocketAddressPosix::RecomputeEndpoint() { + RecomputeEndpoint(endpoint_.address.version()); +} + +void SocketAddressPosix::RecomputeEndpoint(IPAddress::Version version) { + switch (version) { + case IPAddress::Version::kV4: + endpoint_.address = GetIPAddressFromSockAddr(internal_address_.v4); + endpoint_.port = ntohs(internal_address_.v4.sin_port); + break; + case IPAddress::Version::kV6: + endpoint_.address = GetIPAddressFromSockAddr(internal_address_.v6); + endpoint_.port = ntohs(internal_address_.v6.sin6_port); + break; + } +} + +IPAddress GetIPAddressFromSockAddr(const struct sockaddr_in& sa) { + static_assert(IPAddress::kV4Size == sizeof(sa.sin_addr.s_addr), + "IPv4 address size mismatch."); + return IPAddress( + IPAddress::Version::kV4, + std::span( + reinterpret_cast(&sa.sin_addr.s_addr), 4)); +} + +IPAddress GetIPAddressFromSockAddr(const struct sockaddr_in6& sa) { + return IPAddress(std::span(sa.sin6_addr.s6_addr, 16), + sa.sin6_scope_id); +} + +struct sockaddr_in ToSockAddrIn(const IPEndpoint& endpoint) { + OSP_CHECK(endpoint.address.IsV4()); + struct sockaddr_in out{}; + out.sin_family = AF_INET; + out.sin_port = htons(endpoint.port); + endpoint.address.CopyTo( + std::span(reinterpret_cast(&out.sin_addr.s_addr), 4)); + return out; +} + +struct sockaddr_in6 ToSockAddrIn6(const IPEndpoint& endpoint) { + OSP_CHECK(endpoint.address.IsV6()); + struct sockaddr_in6 out{}; + out.sin6_family = AF_INET6; + out.sin6_flowinfo = 0; + out.sin6_scope_id = 0; + if (endpoint.address.IsLinkLocal() && endpoint.address.GetScopeId() != 0) { + out.sin6_scope_id = endpoint.address.GetScopeId(); + } + out.sin6_port = htons(endpoint.port); + endpoint.address.CopyTo( + std::span(reinterpret_cast(&out.sin6_addr), 16)); + return out; +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_address_posix.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_address_posix.h new file mode 100644 index 0000000..2eb800d --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_address_posix.h @@ -0,0 +1,66 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_SOCKET_ADDRESS_POSIX_H_ +#define PLATFORM_IMPL_SOCKET_ADDRESS_POSIX_H_ + +#include +#include +#include +#include +#include +#include + +#include + +#include "platform/base/ip_address.h" + +namespace openscreen { + +class SocketAddressPosix { + public: + explicit SocketAddressPosix(const struct sockaddr& address); + explicit SocketAddressPosix(const IPEndpoint& endpoint); + + SocketAddressPosix(const SocketAddressPosix&) = default; + SocketAddressPosix(SocketAddressPosix&&) noexcept = default; + SocketAddressPosix& operator=(const SocketAddressPosix&) = default; + SocketAddressPosix& operator=(SocketAddressPosix&&) noexcept = default; + + struct sockaddr* address(); + const struct sockaddr* address() const; + socklen_t size() const; + IPAddress::Version version() const { return endpoint_.address.version(); } + IPEndpoint endpoint() const { return endpoint_; } + + // Recomputes `endpoint_` if `internal_address_` is written to directly, e.g. + // by a system call. + void RecomputeEndpoint(); + + private: + void RecomputeEndpoint(IPAddress::Version version); + + // The way the sockaddr_* family works in POSIX is pretty unintuitive. The + // sockaddr_in and sockaddr_in6 structs can be reinterpreted as type + // sockaddr, however they don't have a common parent--the types are unrelated. + // Our solution for this is to wrap sockaddr_in* in a union, so that our code + // can be simplified since most platform APIs just take a sockaddr. + union SocketAddressIn { + struct sockaddr_in v4; + struct sockaddr_in6 v6; + }; + + SocketAddressIn internal_address_; + IPEndpoint endpoint_; +}; + +IPAddress GetIPAddressFromSockAddr(const struct sockaddr_in& sa); +IPAddress GetIPAddressFromSockAddr(const struct sockaddr_in6& sa); + +struct sockaddr_in ToSockAddrIn(const IPEndpoint& endpoint); +struct sockaddr_in6 ToSockAddrIn6(const IPEndpoint& endpoint); + +} // namespace openscreen + +#endif // PLATFORM_IMPL_SOCKET_ADDRESS_POSIX_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle.h new file mode 100644 index 0000000..b495a2a --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle.h @@ -0,0 +1,27 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_SOCKET_HANDLE_H_ +#define PLATFORM_IMPL_SOCKET_HANDLE_H_ + +#include + +namespace openscreen { + +// A SocketHandle is the handle used to access a Socket by the underlying +// platform. +struct SocketHandle; + +struct SocketHandleHash { + size_t operator()(const SocketHandle& handle) const; +}; + +bool operator==(const SocketHandle& lhs, const SocketHandle& rhs); +inline bool operator!=(const SocketHandle& lhs, const SocketHandle& rhs) { + return !(lhs == rhs); +} + +} // namespace openscreen + +#endif // PLATFORM_IMPL_SOCKET_HANDLE_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_posix.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_posix.cc new file mode 100644 index 0000000..9526507 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_posix.cc @@ -0,0 +1,22 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/impl/socket_handle_posix.h" + +#include +#include + +namespace openscreen { + +SocketHandle::SocketHandle(int descriptor) : fd(descriptor) {} + +bool operator==(const SocketHandle& lhs, const SocketHandle& rhs) { + return lhs.fd == rhs.fd; +} + +size_t SocketHandleHash::operator()(const SocketHandle& handle) const { + return std::hash()(handle.fd); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_posix.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_posix.h new file mode 100644 index 0000000..62f3877 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_posix.h @@ -0,0 +1,19 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_SOCKET_HANDLE_POSIX_H_ +#define PLATFORM_IMPL_SOCKET_HANDLE_POSIX_H_ + +#include "platform/impl/socket_handle.h" + +namespace openscreen { + +struct SocketHandle { + explicit SocketHandle(int descriptor); + int fd; +}; + +} // namespace openscreen + +#endif // PLATFORM_IMPL_SOCKET_HANDLE_POSIX_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter.cc new file mode 100644 index 0000000..c43c7a7 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter.cc @@ -0,0 +1,170 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/impl/socket_handle_waiter.h" + +#include +#include + +#include "platform/impl/socket_handle_posix.h" +#include "util/osp_logging.h" +#include "util/std_util.h" + +namespace openscreen { + +SocketHandleWaiter::SocketHandleWaiter(ClockNowFunctionPtr now_function) + : now_function_(now_function) {} + +SocketHandleWaiter::Subscriber::~Subscriber() = default; +SocketHandleWaiter::~SocketHandleWaiter() = default; + +void SocketHandleWaiter::Subscribe(Subscriber* subscriber, + SocketHandleRef handle, + uint32_t flags) { + std::lock_guard lock(mutex_); + if (handle_mappings_.find(handle) == handle_mappings_.end()) { + handle_mappings_.emplace(handle, SocketSubscription{subscriber, flags}); + } +} + +void SocketHandleWaiter::Unsubscribe(Subscriber* subscriber, + SocketHandleRef handle) { + std::lock_guard lock(mutex_); + auto iterator = handle_mappings_.find(handle); + if (handle_mappings_.find(handle) != handle_mappings_.end()) { + handle_mappings_.erase(iterator); + } +} + +void SocketHandleWaiter::UnsubscribeAll(Subscriber* subscriber) { + std::lock_guard lock(mutex_); + for (auto it = handle_mappings_.begin(); it != handle_mappings_.end();) { + if (it->second.subscriber == subscriber) { + it = handle_mappings_.erase(it); + } else { + it++; + } + } +} + +void SocketHandleWaiter::OnHandleDeletion( + Subscriber* subscriber, + SocketHandleRef handle, + bool disable_locking_for_testing) OSP_NO_THREAD_SAFETY_ANALYSIS { + std::unique_lock lock(mutex_); + auto it = handle_mappings_.find(handle); + if (it != handle_mappings_.end()) { + handle_mappings_.erase(it); + if (!disable_locking_for_testing) { + handles_being_deleted_.push_back(handle); + + OSP_DVLOG << "Starting to block for handle deletion"; + // This code will allow us to block completion of the socket destructor + // (and subsequent invalidation of pointers to this socket) until we no + // longer are waiting on a SELECT(...) call to it, since we only signal + // this condition variable's wait(...) to proceed outside of SELECT(...). + while (Contains(handles_being_deleted_, handle)) { + handle_deletion_block_.wait(lock); + } + OSP_DVLOG << "\tDone blocking for handle deletion!"; + } + } +} + +void SocketHandleWaiter::ProcessReadyHandles( + std::vector* handles, + Clock::duration timeout) { + if (handles->empty()) { + return; + } + + Clock::time_point start_time = now_function_(); + // Process the stalest handles one by one until we hit our timeout. + do { + Clock::time_point oldest_time = Clock::time_point::max(); + HandleWithSubscription& oldest_handle = handles->at(0); + for (HandleWithSubscription& handle : *handles) { + // Skip already processed handles. + if (handle.subscription->last_updated >= start_time) { + continue; + } + + // Select the oldest handle. + if (handle.subscription->last_updated < oldest_time) { + oldest_time = handle.subscription->last_updated; + oldest_handle = handle; + } + } + + // Already processed all handles. + if (oldest_time == Clock::time_point::max()) { + return; + } + + // Process the oldest handle. + oldest_handle.subscription->last_updated = now_function_(); + oldest_handle.subscription->subscriber->ProcessReadyHandle( + oldest_handle.ready_handle.handle, oldest_handle.ready_handle.flags); + } while (now_function_() - start_time <= timeout); +} + +Error SocketHandleWaiter::ProcessHandles(Clock::duration timeout) { + Clock::time_point start_time = now_function_(); + std::vector handles; + { + std::lock_guard lock(mutex_); + handles_being_deleted_.clear(); + handle_deletion_block_.notify_all(); + handles.reserve(handle_mappings_.size()); + for (auto& pair : handle_mappings_) { + uint32_t flags = pair.second.flags; + // Remove the write flag if there is no pending write. + if (flags & kWritable) { + const bool has_pending_write = + pair.second.subscriber->HasPendingWrite(pair.first); + if (!has_pending_write) { + flags &= ~kWritable; + } + } + handles.push_back(HandleWithFlags{.handle = pair.first, .flags = flags}); + } + } + if (handles.empty()) { + return Error::Code::kAgain; + } + + Clock::time_point current_time = now_function_(); + Clock::duration remaining_timeout = timeout - (current_time - start_time); + ErrorOr> changed_handles = + AwaitSocketsReady(handles, remaining_timeout); + + std::vector ready_handles; + { + std::lock_guard lock(mutex_); + handles_being_deleted_.clear(); + handle_deletion_block_.notify_all(); + if (changed_handles) { + auto& ch = changed_handles.value(); + ready_handles.reserve(ch.size()); + for (const auto& handle : ch) { + auto mapping_it = handle_mappings_.find(handle.handle); + if (mapping_it != handle_mappings_.end()) { + ready_handles.push_back( + HandleWithSubscription{handle, &(mapping_it->second)}); + } + } + } + + if (changed_handles.is_error()) { + return changed_handles.error(); + } + + current_time = now_function_(); + remaining_timeout = timeout - (current_time - start_time); + ProcessReadyHandles(&ready_handles, remaining_timeout); + } + return Error::None(); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter.h new file mode 100644 index 0000000..9a972c1 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter.h @@ -0,0 +1,151 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_SOCKET_HANDLE_WAITER_H_ +#define PLATFORM_IMPL_SOCKET_HANDLE_WAITER_H_ + +#include +#include +#include +#include +#include +#include + +#include "platform/api/time.h" +#include "platform/base/error.h" +#include "platform/impl/socket_handle.h" +#include "util/raw_ptr.h" +#include "util/thread_annotations.h" + +namespace openscreen { + +// The class responsible for calling platform-level method to watch UDP sockets +// for available read data. Reading from these sockets is handled at a higher +// layer. +class SocketHandleWaiter { + public: + using SocketHandleRef = std::reference_wrapper; + + // Used to manage what types of events subscribers are subscribed to. + enum Flags { + kReadable = 1 << 0, + kWritable = 1 << 1, + }; + + // Common flag configurations. + static inline constexpr uint32_t kReadWriteFlags = + Flags::kReadable | Flags::kWritable; + + class Subscriber { + public: + virtual ~Subscriber(); + + // Provides a socket handle to the subscriber which has data waiting to be + // processed. + virtual void ProcessReadyHandle(SocketHandleRef handle, uint32_t flags) = 0; + + // Method used to optimize event notifications. Generally speaking, + // sockets are ready for writing very often, causing the network event + // loop to be really busy -- a select() call may complete as frequently as + // every few nanoseconds -- so we really only want to be notified that a + // socket is ready for writing when we actually have something to write. + // + // NOTE: this is only used if the subscriber is subscribed to write events. + virtual bool HasPendingWrite(SocketHandleRef handle) = 0; + }; + + explicit SocketHandleWaiter(ClockNowFunctionPtr now_function); + SocketHandleWaiter(const SocketHandleWaiter&) = delete; + SocketHandleWaiter(SocketHandleWaiter&&) noexcept = delete; + SocketHandleWaiter& operator=(const SocketHandleWaiter&) = delete; + SocketHandleWaiter& operator=(SocketHandleWaiter&&) = delete; + virtual ~SocketHandleWaiter(); + + // Start notifying `subscriber` whenever `handle` has an event. May be called + // multiple times, to be notified for multiple handles, but should not be + // called multiple times for the same handle. + void Subscribe(Subscriber* subscriber, + SocketHandleRef handle, + uint32_t flags); + + // Stop receiving notifications for one of the handles currently subscribed + // to. + void Unsubscribe(Subscriber* subscriber, SocketHandleRef handle); + + // Stop receiving notifications for all handles currently subscribed to, or + // no-op if there are no subscriptions. + void UnsubscribeAll(Subscriber* subscriber); + + // Called when a handle will be deleted to ensure that deletion can proceed + // safely. + void OnHandleDeletion(Subscriber* subscriber, + SocketHandleRef handle, + bool disable_locking_for_testing = false) + OSP_NO_THREAD_SAFETY_ANALYSIS; + + // Gets all socket handles to process, checks them for readable data, and + // handles any changes that have occurred. + Error ProcessHandles(Clock::duration timeout); + + protected: + struct HandleWithFlags { + SocketHandleRef handle; + uint32_t flags; + }; + + // Waits until data is available in one of the provided sockets or the + // provided timeout has passed - whichever is first. If any sockets have data + // available, they are returned. + // + // NOTE: The handle `flags` are checked against the subscriber's + // HasPendingWrite() method to ensure that the kWritable flag is only passed + // if there is a pending write before this method is called. The subscriber + // may be deleted while this method is being invoked, however the handle + // itself is guaranteed to not be deleted until the invocation of this method + // has been completed. + virtual ErrorOr> AwaitSocketsReady( + const std::vector& sockets, + const Clock::duration& timeout) = 0; + + private: + struct SocketSubscription { + raw_ptr subscriber = nullptr; + // Subscribers are only informed of flags that they are interested in. + uint32_t flags = 0; + Clock::time_point last_updated = Clock::time_point::min(); + }; + + struct HandleWithSubscription { + HandleWithFlags ready_handle; + // Reference to the original subscription in the unordered map, so + // we can keep track of when we updated this socket handle. + raw_ptr subscription; + }; + + // Call the subscriber associated with each changed handle. Handles are only + // processed until `timeout` is exceeded. Must be called with `mutex_` held. + void ProcessReadyHandles(std::vector* handles, + Clock::duration timeout); + + // Guards against concurrent access to all other class data members. + std::mutex mutex_; + + // Blocks deletion of handles until they are no longer being watched. + std::condition_variable handle_deletion_block_; + + // Set of handles currently being deleted, for ensuring handle_deletion_block_ + // does not exit prematurely. + std::vector handles_being_deleted_ OSP_GUARDED_BY(mutex_); + + // Set of all socket handles currently being watched, mapped to the subscriber + // that is watching them. + std::unordered_map + handle_mappings_ OSP_GUARDED_BY(mutex_); + + const ClockNowFunctionPtr now_function_; +}; + +} // namespace openscreen + +#endif // PLATFORM_IMPL_SOCKET_HANDLE_WAITER_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter_posix.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter_posix.cc new file mode 100644 index 0000000..1463e6f --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter_posix.cc @@ -0,0 +1,102 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/impl/socket_handle_waiter_posix.h" + +#include + +#include +#include + +#include "platform/base/error.h" +#include "platform/impl/socket_handle_posix.h" +#include "platform/impl/timeval_posix.h" +#include "platform/impl/udp_socket_posix.h" +#include "util/osp_logging.h" + +namespace openscreen { + +SocketHandleWaiterPosix::SocketHandleWaiterPosix( + ClockNowFunctionPtr now_function) + : SocketHandleWaiter(now_function) {} + +SocketHandleWaiterPosix::~SocketHandleWaiterPosix() = default; + +ErrorOr> +SocketHandleWaiterPosix::AwaitSocketsReady( + const std::vector& sockets, + const Clock::duration& timeout) { + int max_fd = -1; + fd_set read_handles{}; + fd_set write_handles{}; + + FD_ZERO(&read_handles); + FD_ZERO(&write_handles); + for (const HandleWithFlags& hwf : sockets) { + if (hwf.flags & Flags::kReadable) { + FD_SET(hwf.handle.get().fd, &read_handles); + } + + // Only add the socket to the write_handles list if it is configured for + // write events and also has a pending write. This keeps us from polling + // select every few nanoseconds. + if (hwf.flags & Flags::kWritable) { + FD_SET(hwf.handle.get().fd, &write_handles); + } + max_fd = std::max(max_fd, hwf.handle.get().fd); + } + if (max_fd < 0) { + return Error::Code::kIOFailure; + } + + struct timeval tv { + ToTimeval(timeout) + }; + // This value is set to 'max_fd + 1' by convention. Also, select() is + // level-triggered so incomplete reads/writes by the caller are fine and will + // be picked up again on the next select() call. For more information, see: + // http://man7.org/linux/man-pages/man2/select.2.html + const int max_fd_to_watch = max_fd + 1; + const int rv = + select(max_fd_to_watch, &read_handles, &write_handles, nullptr, &tv); + if (rv == -1) { + // This is the case when an error condition is hit within the select(...) + // command. + return Error::Code::kIOFailure; + } else if (rv == 0) { + // This occurs when no sockets have a pending read. + return Error::Code::kAgain; + } + + std::vector changed_handles; + for (const HandleWithFlags& hwf : sockets) { + uint32_t flags = 0; + if (FD_ISSET(hwf.handle.get().fd, &read_handles)) { + flags |= Flags::kReadable; + } + if (FD_ISSET(hwf.handle.get().fd, &write_handles)) { + flags |= Flags::kWritable; + } + if (flags) { + changed_handles.push_back({hwf.handle, flags}); + } + } + return changed_handles; +} + +void SocketHandleWaiterPosix::RunUntilStopped() { + const bool was_running = is_running_.exchange(true); + OSP_CHECK(!was_running); + + constexpr Clock::duration kHandleReadyTimeout = std::chrono::milliseconds(50); + while (is_running_) { + ProcessHandles(kHandleReadyTimeout); + } +} + +void SocketHandleWaiterPosix::RequestStopSoon() { + is_running_.store(false); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter_posix.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter_posix.h new file mode 100644 index 0000000..43e3cf1 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_handle_waiter_posix.h @@ -0,0 +1,45 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_SOCKET_HANDLE_WAITER_POSIX_H_ +#define PLATFORM_IMPL_SOCKET_HANDLE_WAITER_POSIX_H_ + +#include + +#include +#include +#include + +#include "platform/impl/socket_handle_waiter.h" + +namespace openscreen { + +class SocketHandleWaiterPosix : public SocketHandleWaiter { + public: + using SocketHandleRef = SocketHandleWaiter::SocketHandleRef; + using HandleWithFlags = SocketHandleWaiter::HandleWithFlags; + + explicit SocketHandleWaiterPosix(ClockNowFunctionPtr now_function); + ~SocketHandleWaiterPosix() override; + + // Runs the Wait function in a loop until the below RequestStopSoon function + // is called. + void RunUntilStopped(); + + // Signals for the RunUntilStopped loop to cease running. + void RequestStopSoon(); + + protected: + ErrorOr> AwaitSocketsReady( + const std::vector& sockets, + const Clock::duration& timeout) override; + + private: + // Atomic so that we can perform atomic exchanges. + std::atomic_bool is_running_; +}; + +} // namespace openscreen + +#endif // PLATFORM_IMPL_SOCKET_HANDLE_WAITER_POSIX_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_state.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_state.h new file mode 100644 index 0000000..b16bc44 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/socket_state.h @@ -0,0 +1,37 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_SOCKET_STATE_H_ +#define PLATFORM_IMPL_SOCKET_STATE_H_ + +#include +#include +#include + +namespace openscreen { + +// TcpSocketState should be used by TCP and TLS sockets for indicating +// current state. NOTE: socket state transitions should only happen in +// the listed order. New states should be added in appropriate order. +enum class TcpSocketState { + // Socket is not connected. + kNotConnected = 0, + + // Socket is actively listening for incoming connections. + kListening, + + // Socket is currently being connected. + kConnecting, + + // Socket is actively connected to a remote address. + kConnected, + + // The socket connection has been terminated, either by Close() or + // by the remote side. + kClosed +}; + +} // namespace openscreen + +#endif // PLATFORM_IMPL_SOCKET_STATE_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/task_runner.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/task_runner.cc new file mode 100644 index 0000000..7770d3c --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/task_runner.cc @@ -0,0 +1,197 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/impl/task_runner.h" + +#include +#include + +#include "util/osp_logging.h" + +namespace openscreen { + +namespace { + +// This is mutated by the signal handler installed by RunUntilSignaled(), and is +// checked by RunUntilStopped(). +// +// Per the C++14 spec, passing visible changes to memory between a signal +// handler and a program thread must be done through a volatile variable. +volatile enum { + kNotRunning, + kNotSignaled, + kSignaled +} g_signal_state = kNotRunning; + +void OnReceivedSignal(int signal) { + g_signal_state = kSignaled; +} + +} // namespace + +TaskRunnerImpl::TaskWaiter::~TaskWaiter() = default; + +TaskRunnerImpl::TaskRunnerImpl(ClockNowFunctionPtr now_function, + TaskWaiter* event_waiter, + Clock::duration waiter_timeout) + : now_function_(now_function), + is_running_(false), + task_waiter_(event_waiter), + waiter_timeout_(waiter_timeout) {} + +TaskRunnerImpl::~TaskRunnerImpl() { + // Ensure no thread is currently executing inside RunUntilStopped(). + OSP_CHECK_EQ(task_runner_thread_id_, std::thread::id()); +} + +void TaskRunnerImpl::PostPackagedTask(Task task) { + std::lock_guard lock(task_mutex_); + tasks_.emplace_back(std::move(task)); + if (task_waiter_) { + task_waiter_->OnTaskPosted(); + } else { + run_loop_wakeup_.notify_one(); + } +} + +void TaskRunnerImpl::PostPackagedTaskWithDelay(Task task, + Clock::duration delay) { + std::lock_guard lock(task_mutex_); + if (delay <= Clock::duration::zero()) { + tasks_.emplace_back(std::move(task)); + } else { + delayed_tasks_.emplace( + std::make_pair(now_function_() + delay, std::move(task))); + } + if (task_waiter_) { + task_waiter_->OnTaskPosted(); + } else { + run_loop_wakeup_.notify_one(); + } +} + +bool TaskRunnerImpl::IsRunningOnTaskRunner() { + return task_runner_thread_id_ == std::this_thread::get_id(); +} + +void TaskRunnerImpl::RunUntilStopped() { + OSP_CHECK(!is_running_); + task_runner_thread_id_ = std::this_thread::get_id(); + is_running_ = true; + + OSP_DVLOG << "Running tasks until stopped..."; + // Main loop: Run until the `is_running_` flag is set back to false by the + // "quit task" posted by RequestStopSoon(), or the process received a + // termination signal. + while (is_running_) { + ScheduleDelayedTasks(); + if (GrabMoreRunnableTasks()) { + RunRunnableTasks(); + } + if (g_signal_state == kSignaled) { + is_running_ = false; + } + } + + OSP_DVLOG << "Finished running, entering flushing phase..."; + // Flushing phase: Ensure all immediately-runnable tasks are run before + // returning. Since running some tasks might cause more immediately-runnable + // tasks to be posted, loop until there is no more work. + // + // If there is bad code that posts tasks indefinitely, this loop will never + // break. However, that also means there is a code path spinning a CPU core at + // 100% all the time. Rather than mitigate this problem scenario, purposely + // let it manifest here in the hopes that unit testing will reveal it (e.g., a + // unit test that never finishes running). + while (GrabMoreRunnableTasks()) { + RunRunnableTasks(); + } + OSP_DVLOG << "Finished flushing..."; + task_runner_thread_id_ = std::thread::id(); +} + +void TaskRunnerImpl::RunUntilSignaled() { + OSP_CHECK_EQ(g_signal_state, kNotRunning) + << __func__ << " may not be invoked concurrently."; + g_signal_state = kNotSignaled; + const auto old_sigint_handler = std::signal(SIGINT, &OnReceivedSignal); + const auto old_sigterm_handler = std::signal(SIGTERM, &OnReceivedSignal); +#if defined(SIGHUP) + const auto old_sighup_handler = std::signal(SIGHUP, &OnReceivedSignal); +#endif + + RunUntilStopped(); + + std::signal(SIGINT, old_sigint_handler); + std::signal(SIGTERM, old_sigterm_handler); +#if defined(SIGHUP) + std::signal(SIGHUP, old_sighup_handler); +#endif + OSP_DVLOG << "Received signal, setting state to not running..."; + g_signal_state = kNotRunning; +} + +void TaskRunnerImpl::RequestStopSoon() { + PostTask([this]() { is_running_ = false; }); +} + +void TaskRunnerImpl::RunRunnableTasks() { + for (TaskWithMetadata& running_task : running_tasks_) { + // Move the task to the stack so that its bound state is freed immediately + // after being run. + TaskWithMetadata task = std::move(running_task); + task(); + } + running_tasks_.clear(); +} + +void TaskRunnerImpl::ScheduleDelayedTasks() { + std::lock_guard lock(task_mutex_); + + // Getting the time can be expensive on some platforms, so only get it once. + const auto current_time = now_function_(); + const auto end_of_range = delayed_tasks_.upper_bound(current_time); + for (auto it = delayed_tasks_.begin(); it != end_of_range; ++it) { + tasks_.push_back(std::move(it->second)); + } + delayed_tasks_.erase(delayed_tasks_.begin(), end_of_range); +} + +bool TaskRunnerImpl::GrabMoreRunnableTasks() OSP_NO_THREAD_SAFETY_ANALYSIS { + OSP_CHECK(running_tasks_.empty()); + + std::unique_lock lock(task_mutex_); + if (!tasks_.empty()) { + running_tasks_.swap(tasks_); + return true; + } + + if (!is_running_) { + return false; // Stop was requested. Don't wait for more tasks. + } + + if (task_waiter_) { + Clock::duration timeout = waiter_timeout_; + if (!delayed_tasks_.empty()) { + Clock::duration next_task_delta = + delayed_tasks_.begin()->first - now_function_(); + if (next_task_delta < timeout) { + timeout = next_task_delta; + } + } + lock.unlock(); + task_waiter_->WaitForTaskToBePosted(timeout); + return false; + } + + if (delayed_tasks_.empty()) { + run_loop_wakeup_.wait(lock); + } else { + run_loop_wakeup_.wait_for(lock, + delayed_tasks_.begin()->first - now_function_()); + } + return false; +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/task_runner.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/task_runner.h new file mode 100644 index 0000000..e671e6c --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/task_runner.h @@ -0,0 +1,150 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_TASK_RUNNER_H_ +#define PLATFORM_IMPL_TASK_RUNNER_H_ + +#include // NOLINT +#include +#include +#include +#include +#include +#include + +#include "platform/api/task_runner.h" +#include "platform/api/time.h" +#include "platform/base/error.h" +#include "util/raw_ptr.h" +#include "util/thread_annotations.h" +#include "util/trace_logging.h" + +namespace openscreen { + +class TaskRunnerImpl : public TaskRunner { + public: + using Task = TaskRunner::Task; + + class TaskWaiter { + public: + virtual ~TaskWaiter(); + + // These calls should be thread-safe. The absolute minimum is that + // OnTaskPosted must be safe to call from another thread while this is + // inside WaitForTaskToBePosted. NOTE: There may be spurious wakeups from + // WaitForTaskToBePosted depending on whether the specific implementation + // chooses to clear queued WakeUps before entering WaitForTaskToBePosted. + + // Blocks until some event occurs, which means new tasks may have been + // posted. Wait may only block up to `timeout` where 0 means don't block at + // all (not block forever). + virtual Error WaitForTaskToBePosted(Clock::duration timeout) = 0; + + // If a WaitForTaskToBePosted call is currently blocking, unblock it + // immediately. + virtual void OnTaskPosted() = 0; + }; + + explicit TaskRunnerImpl( + ClockNowFunctionPtr now_function, + TaskWaiter* event_waiter = nullptr, + Clock::duration waiter_timeout = std::chrono::milliseconds(100)); + TaskRunnerImpl(const TaskRunnerImpl&) = delete; + TaskRunnerImpl(TaskRunnerImpl&&) noexcept = delete; + TaskRunnerImpl& operator=(const TaskRunnerImpl&) = delete; + TaskRunnerImpl& operator=(TaskRunnerImpl&&) = delete; + + // TaskRunner overrides + ~TaskRunnerImpl() override; + void PostPackagedTask(Task task) override; + void PostPackagedTaskWithDelay(Task task, Clock::duration delay) override; + bool IsRunningOnTaskRunner() override; + + // Blocks the current thread, executing tasks from the queue with the desired + // timing; and does not return until some time after RequestStopSoon() is + // called. + virtual void RunUntilStopped(); + + // Blocks the current thread, executing tasks from the queue with the desired + // timing; and does not return until some time after the current process is + // signaled with SIGINT or SIGTERM, or after RequestStopSoon() is called. + virtual void RunUntilSignaled(); + + // Thread-safe method for requesting the TaskRunner to stop running after all + // non-delayed tasks in the queue have run. This behavior allows final + // clean-up tasks to be executed before the TaskRunner stops. + // + // If any non-delayed tasks post additional non-delayed tasks, those will be + // run as well before returning. + virtual void RequestStopSoon(); + + private: +#if defined(ENABLE_TRACE_LOGGING) + // Wrapper around a Task used to store the TraceId Metadata along with the + // task itself, and to set the current TraceIdHierarchy before executing the + // task. + class TaskWithMetadata { + public: + // NOTE: 'explicit' keyword omitted so that conversion construtor can be + // used. This simplifies switching between 'Task' and 'TaskWithMetadata' + // based on the compilation flag. + TaskWithMetadata(Task task) // NOLINT + : task_(std::move(task)), trace_ids_(TRACE_HIERARCHY) {} + + void operator()() { + TRACE_SET_HIERARCHY(trace_ids_); + std::move(task_)(); + } + + private: + Task task_; + TraceIdHierarchy trace_ids_; + }; +#else // !defined(ENABLE_TRACE_LOGGING) + using TaskWithMetadata = Task; +#endif // defined(ENABLE_TRACE_LOGGING) + + // Helper that runs all tasks in `running_tasks_` and then clears it. + void RunRunnableTasks(); + + // Look at all tasks in the delayed task queue, then schedule them if the + // minimum delay time has elapsed. + void ScheduleDelayedTasks(); + + // Transfers all ready-to-run tasks from `tasks_` to `running_tasks_`. If + // there are no ready-to-run tasks, and `is_running_` is true, this method + // will block waiting for new tasks. Returns true if any tasks were + // transferred. + bool GrabMoreRunnableTasks() OSP_NO_THREAD_SAFETY_ANALYSIS; + + const ClockNowFunctionPtr now_function_; + + // Flag that indicates whether the task runner loop should continue. This is + // only meant to be read/written on the thread executing RunUntilStopped(). + bool is_running_; + + // This mutex is used for `tasks_` and `delayed_tasks_`, and also for + // notifying the run loop to wake up when it is waiting for a task to be added + // to the queue in `run_loop_wakeup_`. + std::mutex task_mutex_; + std::vector tasks_ OSP_GUARDED_BY(task_mutex_); + std::multimap delayed_tasks_ OSP_GUARDED_BY(task_mutex_); + + // When `task_waiter_` is nullptr, `run_loop_wakeup_` is used for sleeping the + // task runner. Otherwise, `run_loop_wakeup_` isn't used and `task_waiter_` + // is used instead (along with `waiter_timeout_`). + std::condition_variable run_loop_wakeup_; + const raw_ptr task_waiter_; + Clock::duration waiter_timeout_; + + // To prevent excessive re-allocation of the underlying array of the `tasks_` + // vector, use an A/B vector-swap mechanism. `running_tasks_` starts out + // empty, and is swapped with `tasks_` when it is time to run the Tasks. + std::vector running_tasks_; + + std::thread::id task_runner_thread_id_; +}; +} // namespace openscreen + +#endif // PLATFORM_IMPL_TASK_RUNNER_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/text_trace_logging_platform.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/text_trace_logging_platform.cc new file mode 100644 index 0000000..b41e44d --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/text_trace_logging_platform.cc @@ -0,0 +1,72 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/impl/text_trace_logging_platform.h" + +#include +#include + +#include "platform/impl/logging.h" +#include "util/chrono_helpers.h" +#include "util/osp_logging.h" + +namespace openscreen { + +using clock_operators::operator<<; + +bool TextTraceLoggingPlatform::IsTraceLoggingEnabled(TraceCategory category) { + return true; +} + +TextTraceLoggingPlatform::TextTraceLoggingPlatform() { + StartTracing(this); +} + +TextTraceLoggingPlatform::~TextTraceLoggingPlatform() { + StopTracing(); +} + +void TextTraceLoggingPlatform::LogTrace(TraceEvent event, + Clock::time_point end_time) { + const auto total_runtime = (end_time - event.start_time); + std::stringstream ss; + ss << "[TRACE" << " (" << std::dec << total_runtime << ")] " << event; + LogTraceMessage(ss.str()); +} + +void TextTraceLoggingPlatform::LogAsyncStart(TraceEvent event) { + std::stringstream ss; + ss << "[ASYNC TRACE START] " << event; + LogTraceMessage(ss.str()); +} + +void TextTraceLoggingPlatform::LogAsyncEnd(TraceEvent event) { + std::stringstream ss; + ss << "[ASYNC TRACE END] " << event; + LogTraceMessage(ss.str()); +} + +void TextTraceLoggingPlatform::LogFlow(TraceEvent event, FlowType type) { + std::stringstream ss; + ss << "[FLOW"; + if (!event.flow_ids.empty()) { + ss << " #" << std::hex << event.flow_ids[0] << std::dec; + } + + switch (type) { + case FlowType::kFlowBegin: + ss << " BEGIN"; + break; + case FlowType::kFlowStep: + ss << " STEP"; + break; + case FlowType::kFlowEnd: + ss << " END"; + break; + } + ss << "] " << event.ToString(); + LogTraceMessage(ss.str()); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/text_trace_logging_platform.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/text_trace_logging_platform.h new file mode 100644 index 0000000..e78c7b1 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/text_trace_logging_platform.h @@ -0,0 +1,30 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_TEXT_TRACE_LOGGING_PLATFORM_H_ +#define PLATFORM_IMPL_TEXT_TRACE_LOGGING_PLATFORM_H_ + +#include "platform/api/trace_logging_platform.h" + +namespace openscreen { + +class TextTraceLoggingPlatform : public TraceLoggingPlatform { + public: + TextTraceLoggingPlatform(); + ~TextTraceLoggingPlatform() override; + + bool IsTraceLoggingEnabled(TraceCategory category) override; + + void LogTrace(TraceEvent event, Clock::time_point end_time) override; + + void LogAsyncStart(TraceEvent event) override; + + void LogAsyncEnd(TraceEvent event) override; + + void LogFlow(TraceEvent event, FlowType type) override; +}; + +} // namespace openscreen + +#endif // PLATFORM_IMPL_TEXT_TRACE_LOGGING_PLATFORM_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/time.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/time.cc new file mode 100644 index 0000000..74f0f2c --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/time.cc @@ -0,0 +1,49 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/api/time.h" + +#include +#include +#include + +#include "util/chrono_helpers.h" +#include "util/osp_logging.h" + +using std::chrono::high_resolution_clock; +using std::chrono::steady_clock; +using std::chrono::system_clock; + +namespace openscreen { + +Clock::time_point Clock::now() noexcept { + constexpr bool kSteadyIsGoodEnough = + std::ratio_less_equal_v; + constexpr bool kHighResIsGoodEnough = + std::ratio_less_equal_v && + high_resolution_clock::is_steady; + static_assert(kSteadyIsGoodEnough || kHighResIsGoodEnough, + "No suitable default clock (steady + high enough resolution) " + "on this platform"); + + // 'if constexpr' guarantees compile-time branching. + // We prefer steady_clock if it meets the requirements (usually cheaper). + if constexpr (kSteadyIsGoodEnough) { + return Clock::time_point( + Clock::to_duration(steady_clock::now().time_since_epoch())); + } else { + return Clock::time_point( + Clock::to_duration(high_resolution_clock::now().time_since_epoch())); + } +} + +std::chrono::seconds GetWallTimeSinceUnixEpoch() noexcept { + // C++20 guarantees that system_clock uses the Unix Epoch (1970-01-01). + // Use floor to truncate sub-second precision safely. + return std::chrono::floor( + std::chrono::system_clock::now().time_since_epoch()); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/timeval_posix.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/timeval_posix.cc new file mode 100644 index 0000000..775f6fd --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/timeval_posix.cc @@ -0,0 +1,22 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/impl/timeval_posix.h" + +#include + +#include "util/chrono_helpers.h" + +namespace openscreen { + +struct timeval ToTimeval(const Clock::duration& timeout) { + struct timeval tv {}; + const auto whole_seconds = to_seconds(timeout); + tv.tv_sec = whole_seconds.count(); + tv.tv_usec = to_microseconds(timeout - whole_seconds).count(); + + return tv; +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/timeval_posix.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/timeval_posix.h new file mode 100644 index 0000000..3230792 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/timeval_posix.h @@ -0,0 +1,18 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_TIMEVAL_POSIX_H_ +#define PLATFORM_IMPL_TIMEVAL_POSIX_H_ + +#include // timeval + +#include "platform/api/time.h" + +namespace openscreen { + +struct timeval ToTimeval(const Clock::duration& timeout); + +} // namespace openscreen + +#endif // PLATFORM_IMPL_TIMEVAL_POSIX_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_posix.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_posix.cc new file mode 100644 index 0000000..b892030 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_posix.cc @@ -0,0 +1,665 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/impl/udp_socket_posix.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "build/build_config.h" +#include "platform/api/network_interface.h" +#include "platform/api/task_runner.h" +#include "platform/base/error.h" +#include "platform/impl/socket_address_posix.h" +#include "platform/impl/udp_socket_reader_posix.h" +#include "util/osp_logging.h" + +namespace openscreen { +namespace { + +// 64 KB is the maximum possible UDP datagram size. +constexpr int kMaxUdpBufferSize = 64 << 10; + +constexpr bool IsPowerOf2(uint32_t x) { + return (x > 0) && ((x & (x - 1)) == 0); +} + +static_assert(IsPowerOf2(alignof(struct cmsghdr)), + "std::align requires power-of-2 alignment"); + +using IPv4NetworkInterfaceIndex = decltype(ip_mreqn().imr_ifindex); +using IPv6NetworkInterfaceIndex = decltype(ipv6_mreq().ipv6mr_interface); + +ErrorOr CreateNonBlockingUdpSocket(int domain) { + int fd = socket(domain, SOCK_DGRAM, 0); + if (fd == -1) { + return Error(Error::Code::kInitializationFailure, strerror(errno)); + } + // On non-Linux, the SOCK_NONBLOCK option is not available, so use the + // more-portable method of calling fcntl() to set this behavior. + if (fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK) == -1) { + close(fd); + return Error(Error::Code::kInitializationFailure, strerror(errno)); + } + return fd; +} + +} // namespace + +UdpSocketPosix::UdpSocketPosix(TaskRunner& task_runner, + Client* client, + SocketHandle handle, + const IPEndpoint& local_endpoint, + PlatformClientPosix* platform_client) + : task_runner_(task_runner), + client_(client), + handle_(handle), + local_endpoint_(local_endpoint), + platform_client_(platform_client) { + if (handle_.fd >= 0) { + if (platform_client_) { + platform_client_->udp_socket_reader()->OnCreate(this); + } + } +} + +UdpSocketPosix::~UdpSocketPosix() { + Close(); +} + +const SocketHandle& UdpSocketPosix::GetHandle() const { + return handle_; +} + +// static +ErrorOr> UdpSocket::Create( + TaskRunner& task_runner, + Client* client, + const IPEndpoint& endpoint) { + static std::atomic_bool in_create{false}; + const bool in_create_local = in_create.exchange(true); + OSP_CHECK(!in_create_local) + << "Another UdpSocket::Create call is in progress. Calls to this method " + "must be seralized."; + + if (in_create_local) { + return Error::Code::kAgain; + } + + int domain; + switch (endpoint.address.version()) { + case Version::kV4: + domain = AF_INET; + break; + case Version::kV6: + domain = AF_INET6; + break; + } + const ErrorOr fd = CreateNonBlockingUdpSocket(domain); + if (!fd) { + in_create = false; + return fd.error(); + } + + std::unique_ptr socket = std::make_unique( + task_runner, client, SocketHandle(fd.value()), endpoint); + in_create = false; + return socket; +} + +bool UdpSocketPosix::IsIPv4() const { + return local_endpoint_.address.IsV4(); +} + +bool UdpSocketPosix::IsIPv6() const { + return local_endpoint_.address.IsV6(); +} + +IPEndpoint UdpSocketPosix::GetLocalEndpoint() const { + if (local_endpoint_.port == 0) { + // Note: If the getsockname() call fails, just assume that's because the + // socket isn't bound yet. In this case, leave the original value in-place. + switch (local_endpoint_.address.version()) { + case UdpSocket::Version::kV4: { + struct sockaddr_in address {}; + socklen_t address_len = sizeof(address); + if (getsockname(handle_.fd, + reinterpret_cast(&address), + &address_len) == 0) { + OSP_CHECK_EQ(address.sin_family, AF_INET); + local_endpoint_.address = GetIPAddressFromSockAddr(address); + local_endpoint_.port = ntohs(address.sin_port); + } + break; + } + + case UdpSocket::Version::kV6: { + struct sockaddr_in6 address {}; + socklen_t address_len = sizeof(address); + if (getsockname(handle_.fd, + reinterpret_cast(&address), + &address_len) == 0) { + OSP_CHECK_EQ(address.sin6_family, AF_INET6); + local_endpoint_.address = GetIPAddressFromSockAddr(address); + local_endpoint_.port = ntohs(address.sin6_port); + } + break; + } + } + } + + return local_endpoint_; +} + +void UdpSocketPosix::Bind() { + OSP_CHECK(task_runner_->IsRunningOnTaskRunner()); + if (is_closed()) { + OnError(Error::Code::kSocketClosedFailure); + return; + } + + // This is effectively a boolean passed to setsockopt() to allow a future + // bind() on the same socket to succeed, even if the address is already in + // use. This is pretty much universally the desired behavior. + constexpr int reuse_addr = 1; + if (setsockopt(handle_.fd, SOL_SOCKET, SO_REUSEADDR, &reuse_addr, + sizeof(reuse_addr)) == -1) { + OnError(Error::Code::kSocketOptionSettingFailure); + } + +#if BUILDFLAG(IS_APPLE) + // On Mac, SO_REUSEADDR is not enough to allow a bind() on a reusable + // multicast socket. We need to also set the option SO_REUSEPORT. + constexpr int reuse_port = 1; + if (setsockopt(handle_.fd, SOL_SOCKET, SO_REUSEPORT, &reuse_port, + sizeof(reuse_port)) == -1) { + OnError(Error::Code::kSocketOptionSettingFailure); + } +#endif // BUILDFLAG(IS_APPLE) + + bool is_bound = false; + switch (local_endpoint_.address.version()) { + case UdpSocket::Version::kV4: { + struct sockaddr_in address = ToSockAddrIn(local_endpoint_); + if (bind(handle_.fd, reinterpret_cast(&address), + sizeof(address)) != -1) { + is_bound = true; + } + } break; + + case UdpSocket::Version::kV6: { + struct sockaddr_in6 address = ToSockAddrIn6(local_endpoint_); + if (bind(handle_.fd, reinterpret_cast(&address), + sizeof(address)) != -1) { + is_bound = true; + } + } break; + } + + if (is_bound) { + client_->OnBound(this); + } else { + OnError(Error::Code::kSocketBindFailure); + } +} + +void UdpSocketPosix::SetMulticastOutboundInterface( + NetworkInterfaceIndex ifindex) { + OSP_CHECK(task_runner_->IsRunningOnTaskRunner()); + if (is_closed()) { + OnError(Error::Code::kSocketClosedFailure); + return; + } + + switch (local_endpoint_.address.version()) { + case UdpSocket::Version::kV4: { + struct ip_mreqn multicast_properties {}; + // Appropriate address is set based on `imr_ifindex` when set. + multicast_properties.imr_address.s_addr = INADDR_ANY; + multicast_properties.imr_multiaddr.s_addr = INADDR_ANY; + multicast_properties.imr_ifindex = + static_cast(ifindex); + if (setsockopt(handle_.fd, IPPROTO_IP, IP_MULTICAST_IF, + &multicast_properties, + sizeof(multicast_properties)) == -1) { + OnError(Error::Code::kSocketOptionSettingFailure); + } + return; + } + + case UdpSocket::Version::kV6: { + const auto index = static_cast(ifindex); + if (setsockopt(handle_.fd, IPPROTO_IPV6, IPV6_MULTICAST_IF, &index, + sizeof(index)) == -1) { + OnError(Error::Code::kSocketOptionSettingFailure); + } + return; + } + } + + OSP_NOTREACHED(); +} + +void UdpSocketPosix::JoinMulticastGroup(const IPAddress& address, + NetworkInterfaceIndex ifindex) { + OSP_CHECK(task_runner_->IsRunningOnTaskRunner()); + if (is_closed()) { + OnError(Error::Code::kSocketClosedFailure); + return; + } + + switch (local_endpoint_.address.version()) { + case UdpSocket::Version::kV4: { + // Passed as data to setsockopt(). 1 means return IP_PKTINFO control data + // in recvmsg() calls. + const int enable_pktinfo = 1; + if (setsockopt(handle_.fd, IPPROTO_IP, IP_PKTINFO, &enable_pktinfo, + sizeof(enable_pktinfo)) == -1) { + OnError(Error::Code::kSocketOptionSettingFailure); + return; + } + struct ip_mreqn multicast_properties {}; + // Appropriate address is set based on `imr_ifindex` when set. + multicast_properties.imr_address.s_addr = INADDR_ANY; + multicast_properties.imr_ifindex = + static_cast(ifindex); + +#if BUILDFLAG(IS_APPLE) + // On macOS, we must specify the interface address, not just the index, + // because it ignores imr_ifindex in ip_mreqn (interpreting it as + // ip_mreq). + const std::vector interfaces = GetNetworkInterfaces(); + const auto it = std::find_if(interfaces.begin(), interfaces.end(), + [ifindex](const InterfaceInfo& info) { + return info.index == ifindex; + }); + + if (it != interfaces.end()) { + for (const auto& ip_net : it->addresses) { + if (ip_net.address.version() == IPAddress::Version::kV4) { + ip_net.address.CopyToV4( + reinterpret_cast(&multicast_properties.imr_address)); + break; + } + } + } +#endif + + static_assert(sizeof(multicast_properties.imr_multiaddr) == 4u, + "IPv4 address requires exactly 4 bytes"); + address.CopyTo(std::span( + reinterpret_cast(&multicast_properties.imr_multiaddr), 4)); + if (setsockopt(handle_.fd, IPPROTO_IP, IP_ADD_MEMBERSHIP, + &multicast_properties, + sizeof(multicast_properties)) == -1) { + OnError(Error::Code::kSocketOptionSettingFailure); + } + return; + } + + case UdpSocket::Version::kV6: { + // Passed as data to setsockopt(). 1 means return IPV6_PKTINFO control + // data in recvmsg() calls. + const int enable_pktinfo = 1; + if (setsockopt(handle_.fd, IPPROTO_IPV6, IPV6_RECVPKTINFO, + &enable_pktinfo, sizeof(enable_pktinfo)) == -1) { + OnError(Error::Code::kSocketOptionSettingFailure); + return; + } + struct ipv6_mreq multicast_properties = { + {/* filled-in below */}, + static_cast(ifindex), + }; + static_assert(sizeof(multicast_properties.ipv6mr_multiaddr) == 16u, + "IPv6 address requires exactly 16 bytes"); + address.CopyTo(std::span( + reinterpret_cast(&multicast_properties.ipv6mr_multiaddr), + 16)); + // Portability note: All platforms support IPV6_JOIN_GROUP, which is + // synonymous with IPV6_ADD_MEMBERSHIP. + if (setsockopt(handle_.fd, IPPROTO_IPV6, IPV6_JOIN_GROUP, + &multicast_properties, + sizeof(multicast_properties)) == -1) { + OnError(Error::Code::kSocketOptionSettingFailure); + } + return; + } + } + + OSP_NOTREACHED(); +} + +namespace { + +// Examine `posix_errno` to determine whether the specific cause of a failure +// was transient or hard, and return the appropriate error response. +Error ChooseError(decltype(errno) posix_errno, Error::Code hard_error_code) { + if (posix_errno == EAGAIN || posix_errno == EWOULDBLOCK || + posix_errno == ENOBUFS) { + return Error(Error::Code::kAgain, strerror(errno)); + } + return Error(hard_error_code, strerror(errno)); +} + +IPAddress GetIPAddressFromPktInfo(const in_pktinfo& pktinfo) { + static_assert(IPAddress::kV4Size == sizeof(pktinfo.ipi_addr), + "IPv4 address size mismatch."); + return IPAddress(IPAddress::Version::kV4, + std::span( + reinterpret_cast(&pktinfo.ipi_addr), 4)); +} + +uint16_t GetPortFromFromSockAddr(const sockaddr_in& sa) { + return ntohs(sa.sin_port); +} + +IPAddress GetIPAddressFromPktInfo(const in6_pktinfo& pktinfo) { + return IPAddress(std::span(pktinfo.ipi6_addr.s6_addr, 16), + pktinfo.ipi6_ifindex); +} + +uint16_t GetPortFromFromSockAddr(const sockaddr_in6& sa) { + return ntohs(sa.sin6_port); +} + +template +bool IsPacketInfo(cmsghdr* cmh); + +template <> +bool IsPacketInfo(cmsghdr* cmh) { + return cmh->cmsg_level == IPPROTO_IP && cmh->cmsg_type == IP_PKTINFO; +} + +template <> +bool IsPacketInfo(cmsghdr* cmh) { + return cmh->cmsg_level == IPPROTO_IPV6 && cmh->cmsg_type == IPV6_PKTINFO; +} + +template +ErrorOr ReceiveMessageInternal(int fd) { + // Try to determine the size of the incoming packet. If we cannot, + // it's not a fatal error, we will just allocate kMaxUdpBufferSize + // and shrink-to-fit below. + int upper_bound_bytes = -1; +#if BUILDFLAG(IS_LINUX) + // Returns the exact size of the datagram, or -1 on error. + upper_bound_bytes = recv(fd, nullptr, 0, MSG_PEEK | MSG_TRUNC); +#elif BUILDFLAG(IS_APPLE) + // Can't use recv(MSG_TRUNC) (not supported). Can't use ioctl(FIONREAD) + // (returns size in socket queue instead next message size). Use + // getsocktopt(...NREAD...) to get the datagram size if possible. + // Ref: https://www.unix.com/man-page/mojave/2/getsockopt/ + socklen_t optlen = sizeof(upper_bound_bytes); + if (getsockopt(fd, SOL_SOCKET, SO_NREAD, &upper_bound_bytes, &optlen) == -1) { + upper_bound_bytes = -1; + } +#endif // BUILDFLAG(IS_LINUX) + if (upper_bound_bytes > 0) { + upper_bound_bytes = std::min(upper_bound_bytes, kMaxUdpBufferSize); + } else { + upper_bound_bytes = kMaxUdpBufferSize; + } + + UdpPacket packet(upper_bound_bytes); + struct msghdr msg {}; + SockAddrType sa{}; + msg.msg_name = &sa; + msg.msg_namelen = sizeof(sa); + iovec iov = {packet.data(), packet.size()}; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + + // Although we don't do anything with the control buffer, on Linux + // it is required for the message to be properly read. +#if BUILDFLAG(IS_LINUX) + alignas(alignof(cmsghdr)) uint8_t control_buffer[2048]; + msg.msg_control = control_buffer; + msg.msg_controllen = sizeof(control_buffer); +#endif // BUILDFLAG(IS_LINUX) + + const ssize_t bytes_received = recvmsg(fd, &msg, 0); + if (bytes_received == -1) { + OSP_DVLOG << "Failed to read from socket."; + return ChooseError(errno, Error::Code::kSocketReadFailure); + } + // We may not populate the entire packet. + OSP_CHECK_LE(static_cast(bytes_received), packet.size()); + packet.resize(bytes_received); + + IPEndpoint source_endpoint = {.address = GetIPAddressFromSockAddr(sa), + .port = GetPortFromFromSockAddr(sa)}; + packet.set_source(std::move(source_endpoint)); + + // For multicast sockets, the packet's original destination address may be + // the host address (since we called bind()) but it may also be a + // multicast address. This may be relevant for handling multicast data; + // specifically, mDNSResponder requires this information to work properly. + + socklen_t sa_len = sizeof(sa); + if (((msg.msg_flags & MSG_CTRUNC) != 0)) { + return Error(Error::Code::kSocketReadFailure, "Packet was truncated"); + } + + if ((getsockname(fd, reinterpret_cast(&sa), &sa_len) == -1)) { + return Error(Error::Code::kSocketReadFailure, "Failed to get socket name"); + } + for (cmsghdr* cmh = CMSG_FIRSTHDR(&msg); cmh; cmh = CMSG_NXTHDR(&msg, cmh)) { + if (IsPacketInfo(cmh)) { + PktInfoType* pktinfo = reinterpret_cast(CMSG_DATA(cmh)); + IPEndpoint destination_endpoint = { + .address = GetIPAddressFromPktInfo(*pktinfo), + .port = GetPortFromFromSockAddr(sa)}; + packet.set_destination(std::move(destination_endpoint)); + break; + } + } + return std::move(packet); +} + +} // namespace + +void UdpSocketPosix::ReceiveMessage() { + // WARNING: This method may be called on a different thread from the thread + // calling into all the other methods. + + if (is_closed()) { + task_runner_->PostTask([weak_this = weak_factory_.GetWeakPtr()] { + if (auto* self = weak_this.get()) { + if (auto* client = self->client_.get()) { + client->OnRead(self, Error::Code::kSocketClosedFailure); + } + } + }); + return; + } + + ErrorOr read_result = Error::Code::kUnknownError; + switch (local_endpoint_.address.version()) { + case UdpSocket::Version::kV4: { + read_result = ReceiveMessageInternal(handle_.fd); + break; + } + case UdpSocket::Version::kV6: { + read_result = + ReceiveMessageInternal(handle_.fd); + break; + } + default: { + OSP_NOTREACHED(); + } + } + + task_runner_->PostTask([weak_this = weak_factory_.GetWeakPtr(), + result = std::move(read_result)]() mutable { + if (auto* self = weak_this.get()) { + if (auto* client = self->client_.get()) { + client->OnRead(self, std::move(result)); + } + } + }); +} + +void UdpSocketPosix::SendMessage(ByteView data, const IPEndpoint& dest) { + OSP_CHECK(task_runner_->IsRunningOnTaskRunner()); + if (is_closed()) { + if (client_) { + client_->OnSendError(this, Error::Code::kSocketClosedFailure); + } + return; + } + + struct iovec iov = { + reinterpret_cast(const_cast(data.data())), data.size()}; + struct msghdr msg {}; + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + msg.msg_control = nullptr; + msg.msg_controllen = 0; + msg.msg_flags = 0; + + ssize_t num_bytes_sent = -2; + switch (dest.address.version()) { + case UdpSocket::Version::kV4: { + struct sockaddr_in sa {}; + sa.sin_family = AF_INET; + sa.sin_port = htons(dest.port); + dest.address.CopyTo(std::span( + reinterpret_cast(&sa.sin_addr.s_addr), 4)); + msg.msg_name = &sa; + msg.msg_namelen = sizeof(sa); + num_bytes_sent = sendmsg(handle_.fd, &msg, 0); + break; + } + + case UdpSocket::Version::kV6: { + struct sockaddr_in6 sa {}; + sa.sin6_family = AF_INET6; + sa.sin6_port = htons(dest.port); + dest.address.CopyTo(std::span( + reinterpret_cast(&sa.sin6_addr.s6_addr), 16)); + if (dest.address.IsLinkLocal() && dest.address.GetScopeId() != 0) { + sa.sin6_scope_id = dest.address.GetScopeId(); + } + msg.msg_name = &sa; + msg.msg_namelen = sizeof(sa); + num_bytes_sent = sendmsg(handle_.fd, &msg, 0); + break; + } + } + + if (num_bytes_sent == -1) { + if (client_) { + client_->OnSendError(this, + ChooseError(errno, Error::Code::kSocketSendFailure)); + } + return; + } + + // Sanity-check: UDP datagram sendmsg() is all or nothing. + OSP_CHECK_EQ(static_cast(num_bytes_sent), data.size()); +} + +void UdpSocketPosix::SetDscp(UdpSocket::DscpMode mode) { + OSP_CHECK(task_runner_->IsRunningOnTaskRunner()); + if (is_closed()) { + OnError(Error::Code::kSocketClosedFailure); + return; + } + + int level; + int option; + switch (local_endpoint_.address.version()) { + case UdpSocket::Version::kV4: + level = IPPROTO_IP; + option = IP_TOS; + break; + case UdpSocket::Version::kV6: + level = IPPROTO_IPV6; + option = IPV6_TCLASS; + break; + } + + // The DSCP value is a 6-bit field, while the IP_TOS and IPV6_TCLASS fields + // are 8-bit fields that expect the DSCP value in the six most significant + // digits. + const int value = static_cast(mode) << 2; + if (setsockopt(handle_.fd, level, option, &value, sizeof(value)) == -1) { + OnError(Error::Code::kSocketOptionSettingFailure); + return; + } + + OSP_DVLOG << __func__ << ": successfully set DSCP to " + << static_cast(mode); +} + +void UdpSocketPosix::OnError(Error::Code error_code) { + // The call to Close() may change `errno`, so save it here. + const auto original_errno = errno; + + // Close the socket unless the error code represents a transient condition. + if (error_code != Error::Code::kNone && error_code != Error::Code::kAgain) { + Close(); + } + + if (client_) { + // Call the thread-safe strerror_r() to get the human-readable form of + // `errno`. This is a real mess: 1. Since there seems to be no constant + // defined for the maximum buffer size in the standard library, 1024 is + // used, as suggested by the man page for strerror_r(). 2. There are two + // possible versions of this function: The POSIX one returns int(0) on + // success, while the legacy GNU-specific one will provide a non-null char + // pointer (that may or may not be within the `buffer`). + char buffer[1024]; + const auto result = strerror_r(original_errno, buffer, sizeof(buffer)); + const char* errno_str; + if (std::is_convertible::value && + !result) { // Case 1: POSIX strerror_r() success. + errno_str = buffer; + } else if (std::is_convertible::value && + result) { // Case 2: GNU strerror_r() success. + errno_str = reinterpret_cast(result); + } else { // Case 3: strerror_r() failed (either version). + buffer[0] = '\0'; + errno_str = buffer; + } + + std::stringstream stream; + stream << "endpoint: " << local_endpoint_ << ", error: " << errno_str; + client_->OnError(this, Error(error_code, stream.str())); + } +} + +void UdpSocketPosix::Close() { + if (handle_.fd < 0) { + return; + } + + // Notify the UdpSocketReaderPosix that the socket handle is about to be + // closed. + if (platform_client_) { + platform_client_->udp_socket_reader()->OnDestroy(this); + } + + // It's now safe to close the socket, since no other thread (e.g., from + // UdpSocketReaderPosix) should be inside ReceiveMessage() at this point. + close(handle_.fd); + handle_.fd = -1; +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_posix.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_posix.h new file mode 100644 index 0000000..ee63199 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_posix.h @@ -0,0 +1,90 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_UDP_SOCKET_POSIX_H_ +#define PLATFORM_IMPL_UDP_SOCKET_POSIX_H_ + +#include "platform/api/udp_socket.h" +#include "platform/impl/platform_client_posix.h" +#include "platform/impl/socket_handle_posix.h" +#include "util/raw_ptr.h" +#include "util/raw_ref.h" +#include "util/weak_ptr.h" + +namespace openscreen { + +class UdpSocketReaderPosix; + +// Threading: All public methods must be called on the same thread--the one +// executing the TaskRunner. All non-public methods, except ReceiveMessage(), +// are also assumed to be called on that thread. +class UdpSocketPosix : public UdpSocket { + public: + // Creates a new UdpSocketPosix. The provided client and task_runner must + // exist for the duration of this socket's lifetime. + UdpSocketPosix(TaskRunner& task_runner, + Client* client, + SocketHandle handle, + const IPEndpoint& local_endpoint, + PlatformClientPosix* platform_client = + PlatformClientPosix::GetInstance()); + UdpSocketPosix(const UdpSocketPosix&) = delete; + UdpSocketPosix(UdpSocketPosix&&) noexcept = delete; + UdpSocketPosix& operator=(const UdpSocketPosix&) = delete; + UdpSocketPosix& operator=(UdpSocketPosix&&) = delete; + ~UdpSocketPosix() override; + + // Implementations of UdpSocket methods. + bool IsIPv4() const override; + bool IsIPv6() const override; + IPEndpoint GetLocalEndpoint() const override; + void Bind() override; + void SetMulticastOutboundInterface(NetworkInterfaceIndex ifindex) override; + void JoinMulticastGroup(const IPAddress& address, + NetworkInterfaceIndex ifindex) override; + void SendMessage(ByteView data, const IPEndpoint& dest) override; + void SetDscp(DscpMode mode) override; + + const SocketHandle& GetHandle() const; + + protected: + friend class UdpSocketReaderPosix; + + // Called by UdpSocketReaderPosix to perform a non-blocking read on the socket + // and then dispatch the packet to this socket's Client. This method is the + // only one in this class possibly being called from another thread. + void ReceiveMessage(); + + private: + // Helper to close the socket if `error` is fatal, in addition to dispatching + // an Error to the `client_`. + void OnError(Error::Code error); + + bool is_closed() const { return handle_.fd < 0; } + void Close(); + + // Task runner to use for queuing `client_` callbacks. + const raw_ref task_runner_; + + // Client to use for callbacks. This can be nullptr if the user does not want + // any callbacks (for example, in the send-only case). + const raw_ptr client_; + + // Holds the POSIX file descriptor, or -1 if the socket is closed. + SocketHandle handle_; + + // Cached value of current local endpoint. This can change (e.g., when the + // operating system auto-assigns a free local port when Bind() is called). If + // the port is zero, getsockname() is called to try to resolve it. Once the + // port is non-zero, it is assumed never to change again. + mutable IPEndpoint local_endpoint_; + + WeakPtrFactory weak_factory_{this}; + + const raw_ptr platform_client_; +}; + +} // namespace openscreen + +#endif // PLATFORM_IMPL_UDP_SOCKET_POSIX_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_reader_posix.cc b/breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_reader_posix.cc new file mode 100644 index 0000000..e8b6854 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_reader_posix.cc @@ -0,0 +1,78 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "platform/impl/udp_socket_reader_posix.h" + +#include +#include + +#include "platform/impl/socket_handle_posix.h" +#include "platform/impl/udp_socket_posix.h" +#include "util/osp_logging.h" +#include "util/std_util.h" + +namespace openscreen { + +UdpSocketReaderPosix::UdpSocketReaderPosix(SocketHandleWaiter& waiter) + : waiter_(waiter) {} + +UdpSocketReaderPosix::~UdpSocketReaderPosix() { + waiter_->UnsubscribeAll(this); +} + +void UdpSocketReaderPosix::ProcessReadyHandle(SocketHandleRef handle, + uint32_t flags) { + OSP_CHECK(flags & SocketHandleWaiter::Flags::kReadable); + std::lock_guard lock(mutex_); + // NOTE: Because sockets_ is expected to remain small, the performance here + // is better than using an unordered_set. + for (UdpSocketPosix* socket : sockets_) { + if (socket->GetHandle() == handle) { + socket->ReceiveMessage(); + break; + } + } +} + +bool UdpSocketReaderPosix::HasPendingWrite(SocketHandleRef handle) { + OSP_NOTREACHED(); +} + +void UdpSocketReaderPosix::OnCreate(UdpSocket* socket) { + UdpSocketPosix* read_socket = static_cast(socket); + { + std::lock_guard lock(mutex_); + sockets_.push_back(read_socket); + } + // We only care about read events. + waiter_->Subscribe(this, std::cref(read_socket->GetHandle()), + SocketHandleWaiter::kReadable); +} + +void UdpSocketReaderPosix::OnDestroy(UdpSocket* socket) { + UdpSocketPosix* destroyed_socket = static_cast(socket); + OnDelete(destroyed_socket); +} + +void UdpSocketReaderPosix::OnDelete(UdpSocketPosix* socket, + bool disable_locking_for_testing) { + { + std::lock_guard lock(mutex_); + auto it = std::find(sockets_.begin(), sockets_.end(), socket); + if (it != sockets_.end()) { + sockets_.erase(it); + } + } + + waiter_->OnHandleDeletion(this, std::cref(socket->GetHandle()), + disable_locking_for_testing); +} + +bool UdpSocketReaderPosix::IsMappedReadForTesting( + UdpSocketPosix* socket) const { + std::lock_guard lock(mutex_); + return Contains(sockets_, socket); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_reader_posix.h b/breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_reader_posix.h new file mode 100644 index 0000000..31daf81 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/platform/impl/udp_socket_reader_posix.h @@ -0,0 +1,82 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef PLATFORM_IMPL_UDP_SOCKET_READER_POSIX_H_ +#define PLATFORM_IMPL_UDP_SOCKET_READER_POSIX_H_ + +#include +#include +#include + +#include "platform/api/task_runner.h" +#include "platform/api/time.h" +#include "platform/impl/socket_handle.h" +#include "platform/impl/socket_handle_waiter.h" +#include "platform/impl/udp_socket_posix.h" +#include "util/raw_ptr.h" +#include "util/raw_ref.h" +#include "util/thread_annotations.h" + +namespace openscreen { + +// This is the class responsible for watching sockets for readable data, then +// calling the function associated with these sockets once that data is read. +// NOTE: This class will only function as intended while its RunUntilStopped +// method is running. +class UdpSocketReaderPosix : public SocketHandleWaiter::Subscriber { + public: + using SocketHandleRef = SocketHandleWaiter::SocketHandleRef; + + // Creates a new instance of this object. + // NOTE: The provided NetworkWaiter must outlive this object. + explicit UdpSocketReaderPosix(SocketHandleWaiter& waiter); + UdpSocketReaderPosix(const UdpSocketReaderPosix&) = delete; + UdpSocketReaderPosix(UdpSocketReaderPosix&&) noexcept = delete; + UdpSocketReaderPosix& operator=(const UdpSocketReaderPosix&) = delete; + UdpSocketReaderPosix& operator=(UdpSocketReaderPosix&&) = delete; + ~UdpSocketReaderPosix() override; + + // Waits for `socket` to be readable and then calls the socket's + // RecieveMessage(...) method to process the available packet. + // NOTE: The first read on any newly watched socket may be delayed up to 50 + // ms. + void OnCreate(UdpSocket* socket); + + // Cancels any pending wait on reading `socket`. Following this call, any + // pending reads will proceed but their associated callbacks will not fire. + // NOTE: This method will block until a delete is safe. + // NOTE: If a socket callback is removed in the middle of a wait call, data + // may be read on this socket and but the callback may not be called. If a + // socket callback is added in the middle of a wait call, the new socket may + // not be watched until after this wait call ends. + virtual void OnDestroy(UdpSocket* socket); + + // SocketHandleWaiter::Subscriber overrides. + void ProcessReadyHandle(SocketHandleRef handle, uint32_t flags) override; + + // NOTE: we don't subscribe to write events from the socket handle waiter. + bool HasPendingWrite(SocketHandleRef handle) override; + protected: + bool IsMappedReadForTesting(UdpSocketPosix* socket) const; + + private: + // Helper method to allow for OnDestroy calls without blocking. + void OnDelete(UdpSocketPosix* socket, + bool disable_locking_for_testing = false); + + // The set of all sockets that are being read from + std::vector> sockets_ OSP_GUARDED_BY(mutex_); + + // Mutex to protect against concurrent modification of socket info. + mutable std::mutex mutex_; + + // NetworkWaiter watching this NetworkReader. + const raw_ref waiter_; + + friend class TestingUdpSocketReader; +}; + +} // namespace openscreen + +#endif // PLATFORM_IMPL_UDP_SOCKET_READER_POSIX_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/alarm.cc b/breadcast-caststream-sys/vendor/openscreen/util/alarm.cc new file mode 100644 index 0000000..f829ef2 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/alarm.cc @@ -0,0 +1,131 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/alarm.h" + +#include + +#include "util/osp_logging.h" + +namespace openscreen { + +class Alarm::CancelableFunctor { + public: + explicit CancelableFunctor(Alarm* alarm) : alarm_(alarm) { + OSP_CHECK(alarm_); + OSP_CHECK(!alarm_->queued_fire_); + alarm_->queued_fire_ = this; + } + + ~CancelableFunctor() { Cancel(); } + + CancelableFunctor(CancelableFunctor&& other) : alarm_(other.alarm_) { + other.alarm_ = nullptr; + if (alarm_) { + OSP_CHECK_EQ(alarm_->queued_fire_, &other); + alarm_->queued_fire_ = this; + } + } + + CancelableFunctor& operator=(CancelableFunctor&& other) { + Cancel(); + alarm_ = other.alarm_; + other.alarm_ = nullptr; + if (alarm_) { + OSP_CHECK_EQ(alarm_->queued_fire_, &other); + alarm_->queued_fire_ = this; + } + return *this; + } + + void operator()() noexcept { + if (alarm_) { + Alarm* alarm = alarm_; + OSP_CHECK_EQ(alarm->queued_fire_, this); + alarm->queued_fire_ = nullptr; + alarm_ = nullptr; + alarm->TryInvoke(); + } + } + + void Cancel() { + if (alarm_) { + OSP_CHECK_EQ(alarm_->queued_fire_, this); + alarm_->queued_fire_ = nullptr; + alarm_ = nullptr; + } + } + + private: + raw_ptr alarm_; +}; + +Alarm::Alarm(ClockNowFunctionPtr now_function, TaskRunner& task_runner) + : now_function_(now_function), task_runner_(task_runner) { + OSP_CHECK(now_function_); +} + +Alarm::~Alarm() { + if (queued_fire_) { + queued_fire_->Cancel(); + OSP_CHECK(!queued_fire_); + } +} + +void Alarm::Cancel() { + scheduled_task_ = TaskRunner::Task(); +} + +void Alarm::ScheduleWithTask(TaskRunner::Task task, + Clock::time_point desired_alarm_time) { + OSP_CHECK(task.valid()); + + scheduled_task_ = std::move(task); + + const Clock::time_point now = now_function_(); + alarm_time_ = std::max(now, desired_alarm_time); + + // Ensure that a later firing will occur, and not too late. + if (queued_fire_) { + if (next_fire_time_ <= alarm_time_) { + return; + } + queued_fire_->Cancel(); + OSP_CHECK(!queued_fire_); + } + InvokeLater(now, alarm_time_); +} + +void Alarm::InvokeLater(Clock::time_point now, Clock::time_point fire_time) { + OSP_CHECK(!queued_fire_); + next_fire_time_ = fire_time; + // Note: Instantiating the CancelableFunctor below sets |this->queued_fire_|. + task_runner_->PostTaskWithDelay(CancelableFunctor(this), fire_time - now); +} + +void Alarm::TryInvoke() { + if (!scheduled_task_.valid()) { + return; // This Alarm was canceled in the meantime. + } + + // If this is an early firing, re-schedule for later. This happens if + // Schedule() was called again before this firing had occurred. + const Clock::time_point now = now_function_(); + if (now < alarm_time_) { + InvokeLater(now, alarm_time_); + return; + } + + // Move the client Task to the stack before executing, just in case the task + // itself: a) calls any Alarm methods re-entrantly, or b) causes the + // destruction of this Alarm instance. + // WARNING: `this` is not valid after here! + TaskRunner::Task task = std::move(scheduled_task_); + task(); +} + +// static +constexpr Clock::time_point Alarm::kImmediately; + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/util/alarm.h b/breadcast-caststream-sys/vendor/openscreen/util/alarm.h new file mode 100644 index 0000000..10665ac --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/alarm.h @@ -0,0 +1,109 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_ALARM_H_ +#define UTIL_ALARM_H_ + +#include + +#include "platform/api/task_runner.h" +#include "platform/api/time.h" +#include "util/raw_ptr.h" +#include "util/raw_ref.h" + +namespace openscreen { + +// A simple mechanism for running one Task in the future, but also allow for +// canceling the Task before it runs and/or re-scheduling a replacement Task to +// run at a different time. This mechanism is also scoped to its lifetime: if an +// Alarm is destroyed while it is scheduled, the Task is automatically canceled. +// It is safe for the client's Task to make re-entrant calls into all Alarm +// methods. +// +// Example use case: When using a TaskRunner, an object can safely schedule a +// callback into one of its instance methods (without the possibility of the +// Task executing after the object is destroyed). +// +// Design: In order to support efficient, arbitrary canceling and re-scheduling +// by the client, the Alarm posts a cancelable functor to the TaskRunner which, +// when invoked, then checks to see whether the Alarm instance still exists and, +// if so, calls its TryInvoke() method. The TryInvoke() method then determines: +// a) whether the invocation time of the client's Task has changed; and b) +// whether the Alarm was canceled in the meantime. From this, it either: a) does +// nothing; b) re-posts a new cancelable functor to the TaskRunner, to try +// running the client's Task later; or c) runs the client's Task. +class Alarm { + public: + Alarm(ClockNowFunctionPtr now_function, TaskRunner& task_runner); + ~Alarm(); + + // The design requires that Alarm instances not be copied or moved. + Alarm(const Alarm&) = delete; + Alarm& operator=(const Alarm&) = delete; + Alarm(Alarm&&) noexcept = delete; + Alarm& operator=(Alarm&&) noexcept = delete; + + // Schedule the `functor` to be invoked at `alarm_time`. If this Alarm was + // already scheduled, the prior scheduling is canceled. The Functor can be any + // callable target (e.g., function, lambda-expression, std::bind result, + // etc.). If `alarm_time` is on or before "now," such as kImmediately, it is + // scheduled to run as soon as possible. + template + inline void Schedule(Functor functor, Clock::time_point alarm_time) { + ScheduleWithTask(TaskRunner::Task(std::move(functor)), alarm_time); + } + + // Same as Schedule(), but invoke the functor at the given `delay` after right + // now. + template + inline void ScheduleFromNow(Functor functor, Clock::duration delay) { + ScheduleWithTask(TaskRunner::Task(std::move(functor)), + now_function_() + delay); + } + + // Cancels an already-scheduled task from running, or no-op. + void Cancel(); + + // See comments for Schedule(). Generally, callers will want to call + // Schedule() instead of this, for more-convenient caller-side syntax, unless + // they already have a Task to pass-in. + void ScheduleWithTask(TaskRunner::Task task, Clock::time_point alarm_time); + + // A special time_point value representing "as soon as possible." + static constexpr Clock::time_point kImmediately = Clock::time_point::min(); + + private: + // A move-only functor that holds a raw pointer back to `this` and can be + // canceled before its call operator is invoked. When canceled, its call + // operator becomes a no-op. + class CancelableFunctor; + + // Posts a delayed call to TryInvoke() to the TaskRunner. + void InvokeLater(Clock::time_point now, Clock::time_point fire_time); + + // Examines whether to invoke the client's Task now; or try again later; or + // just do nothing. See class-level design comments. + void TryInvoke(); + + const ClockNowFunctionPtr now_function_; + const raw_ref task_runner_; + + // This is the task the client wants to have run at a specific point-in-time. + // This is NOT the task that Alarm provides to the TaskRunner. + TaskRunner::Task scheduled_task_; + Clock::time_point alarm_time_{}; + + // When non-null, there is a task in the TaskRunner's queue that will call + // TryInvoke() some time in the future. This member is exclusively maintained + // by the CancelableFunctor class methods. + raw_ptr queued_fire_; + + // When the CancelableFunctor is scheduled to run. It may possibly execute + // later than this, if the TaskRunner is falling behind. + Clock::time_point next_fire_time_{}; +}; + +} // namespace openscreen + +#endif // UTIL_ALARM_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/base64.cc b/breadcast-caststream-sys/vendor/openscreen/util/base64.cc new file mode 100644 index 0000000..0cc21bc --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/base64.cc @@ -0,0 +1,65 @@ +// LOCAL PATCH (breadcast): upstream implements this on top of +// third_party/modp_b64, which isn't fetched by a plain shallow clone of +// openscreen (it's pulled in separately via gclient/DEPS). This is a +// same-interface reimplementation on top of OpenSSL's EVP_Encode/DecodeBlock +// instead, since system OpenSSL is already a build dependency here. See +// vendor/openscreen/PATCHES.md. +#include "util/base64.h" + +#include + +#include +#include +#include +#include + +namespace openscreen::base64 { + +std::string Encode(ByteView input) { + return Encode(std::string_view(reinterpret_cast(input.data()), + input.size())); +} + +std::string Encode(std::string_view input) { + const auto* data = reinterpret_cast(input.data()); + // EVP_EncodeBlock's output is 4*ceil(n/3) bytes plus a NUL terminator it + // writes but doesn't count in the returned length. + std::string out((4 * ((input.size() + 2) / 3)) + 1, '\0'); + const int output_size = EVP_EncodeBlock( + reinterpret_cast(out.data()), data, + static_cast(input.size())); + out.resize(static_cast(output_size)); + return out; +} + +bool Decode(std::string_view input, std::vector* output) { + if (input.size() % 4 != 0) { + return false; + } + std::vector out((input.size() / 4) * 3); + if (!out.empty()) { + const int decoded_size = EVP_DecodeBlock( + out.data(), reinterpret_cast(input.data()), + static_cast(input.size())); + if (decoded_size < 0) { + return false; + } + // EVP_DecodeBlock doesn't strip padding from the output size -- trim the + // 1-2 bytes corresponding to trailing '=' padding characters, matching + // the caller-visible behavior of a normal base64 decoder. + size_t padding = 0; + if (input.size() >= 2) { + if (input[input.size() - 1] == '=') { + ++padding; + } + if (input[input.size() - 2] == '=') { + ++padding; + } + } + out.resize(static_cast(decoded_size) - padding); + } + *output = std::move(out); + return true; +} + +} // namespace openscreen::base64 diff --git a/breadcast-caststream-sys/vendor/openscreen/util/base64.h b/breadcast-caststream-sys/vendor/openscreen/util/base64.h new file mode 100644 index 0000000..868a78e --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/base64.h @@ -0,0 +1,32 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_BASE64_H_ +#define UTIL_BASE64_H_ + +#include + +#include +#include +#include + +#include "platform/base/error.h" +#include "platform/base/span.h" + +namespace openscreen::base64 { + +// Encodes the input binary data in base64. +std::string Encode(ByteView input); + +// Encodes the input string in base64. +std::string Encode(std::string_view input); + +// Decodes the base64 input string. Returns true if successful and false +// otherwise. The output string is only modified if successful. The decoding can +// be done in-place. +bool Decode(std::string_view input, std::vector* output); + +} // namespace openscreen::base64 + +#endif // UTIL_BASE64_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/big_endian.cc b/breadcast-caststream-sys/vendor/openscreen/util/big_endian.cc new file mode 100644 index 0000000..c6f0bd4 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/big_endian.cc @@ -0,0 +1,47 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/big_endian.h" + +namespace openscreen { + +BigEndianReader::BigEndianReader(ByteView buffer) : BigEndianBuffer(buffer) {} + +BigEndianReader::BigEndianReader(const uint8_t* buffer, size_t length) + : BigEndianBuffer(buffer, length) {} + +bool BigEndianReader::Read(size_t length, void* out) { + return Read(ByteBuffer(static_cast(out), length)); +} + +bool BigEndianReader::Read(ByteBuffer out) { + ByteView view = remaining_span(); + if (view.size() >= out.size()) { + std::copy(view.begin(), view.begin() + out.size(), out.begin()); + Skip(out.size()); + return true; + } + return false; +} + +BigEndianWriter::BigEndianWriter(ByteBuffer buffer) : BigEndianBuffer(buffer) {} + +BigEndianWriter::BigEndianWriter(uint8_t* buffer, size_t length) + : BigEndianBuffer(buffer, length) {} + +bool BigEndianWriter::Write(const void* buffer, size_t length) { + return Write(ByteView(static_cast(buffer), length)); +} + +bool BigEndianWriter::Write(ByteView buffer) { + ByteBuffer view = remaining_span(); + if (view.size() >= buffer.size()) { + std::copy(buffer.begin(), buffer.end(), view.begin()); + Skip(buffer.size()); + return true; + } + return false; +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/util/big_endian.h b/breadcast-caststream-sys/vendor/openscreen/util/big_endian.h new file mode 100644 index 0000000..f4e4bbe --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/big_endian.h @@ -0,0 +1,255 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_BIG_ENDIAN_H_ +#define UTIL_BIG_ENDIAN_H_ + +#include + +#include +#include +#include + +#include "platform/base/span.h" +#include "util/raw_ptr.h" + +namespace openscreen { + +//////////////////////////////////////////////////////////////////////////////// +// Note: All of the functions here are defined inline, as any half-decent +// compiler will optimize them to a single integer constant or single +// instruction on most architectures. +//////////////////////////////////////////////////////////////////////////////// + +// Returns true if this code is running on a big-endian architecture. +inline bool IsBigEndianArchitecture() { + const uint16_t kTestWord = 0x0100; + uint8_t bytes[sizeof(kTestWord)]; + memcpy(bytes, &kTestWord, sizeof(bytes)); + return !!bytes[0]; +} + +namespace internal { + +template +struct MakeSizedUnsignedInteger; + +template <> +struct MakeSizedUnsignedInteger<1> { + using type = uint8_t; +}; + +template <> +struct MakeSizedUnsignedInteger<2> { + using type = uint16_t; +}; + +template <> +struct MakeSizedUnsignedInteger<4> { + using type = uint32_t; +}; + +template <> +struct MakeSizedUnsignedInteger<8> { + using type = uint64_t; +}; + +template +inline typename MakeSizedUnsignedInteger::type ByteSwap( + typename MakeSizedUnsignedInteger::type x) { + static_assert(size <= 8, + "ByteSwap() specialization missing in " __FILE__ + ". " + "Are you trying to use an integer larger than 64 bits?"); +} + +template <> +inline uint8_t ByteSwap<1>(uint8_t x) { + return x; +} + +#if defined(__clang__) || defined(__GNUC__) + +template <> +inline uint64_t ByteSwap<8>(uint64_t x) { + return __builtin_bswap64(x); +} +template <> +inline uint32_t ByteSwap<4>(uint32_t x) { + return __builtin_bswap32(x); +} +template <> +inline uint16_t ByteSwap<2>(uint16_t x) { + return __builtin_bswap16(x); +} + +#elif defined(_MSC_VER) + +template <> +inline uint64_t ByteSwap<8>(uint64_t x) { + return _byteswap_uint64(x); +} +template <> +inline uint32_t ByteSwap<4>(uint32_t x) { + return _byteswap_ulong(x); +} +template <> +inline uint16_t ByteSwap<2>(uint16_t x) { + return _byteswap_ushort(x); +} + +#else + +#include + +template <> +inline uint64_t ByteSwap<8>(uint64_t x) { + return bswap_64(x); +} +template <> +inline uint32_t ByteSwap<4>(uint32_t x) { + return bswap_32(x); +} +template <> +inline uint16_t ByteSwap<2>(uint16_t x) { + return bswap_16(x); +} + +#endif + +} // namespace internal + +// Returns the bytes of `x` in reverse order. This is only defined for 16-, 32-, +// and 64-bit unsigned integers. +template +inline std::enable_if_t::value, Integer> ByteSwap( + Integer x) { + return internal::ByteSwap(x); +} + +// Read a POD integer from `src` in big-endian byte order, returning the integer +// in native byte order. +template +inline Integer ReadBigEndian(const void* src) { + Integer result; + memcpy(&result, src, sizeof(result)); + if (!IsBigEndianArchitecture()) { + result = ByteSwap::type>(result); + } + return result; +} + +// Write a POD integer `val` to `dest` in big-endian byte order. +template +inline void WriteBigEndian(Integer val, void* dest) { + if (!IsBigEndianArchitecture()) { + val = ByteSwap::type>(val); + } + memcpy(dest, &val, sizeof(val)); +} + +template +class BigEndianBuffer { + public: + class Cursor { + public: + explicit Cursor(BigEndianBuffer* buffer) + : buffer_(buffer), origin_offset_(buffer_->offset()) {} + Cursor(const Cursor& other) = delete; + Cursor(Cursor&& other) noexcept = delete; + ~Cursor() { buffer_->set_offset(origin_offset_); } + + Cursor& operator=(const Cursor& other) = delete; + Cursor& operator=(Cursor&& other) noexcept = delete; + + void Commit() { origin_offset_ = buffer_->offset(); } + + size_t origin_offset() const { return origin_offset_; } + T* origin() const { return buffer_->begin() + origin_offset_; } + size_t delta() const { return buffer_->offset() - origin_offset_; } + + private: + raw_ptr> buffer_; + size_t origin_offset_; + }; + + bool Skip(size_t length) { + if (length > remaining()) { + return false; + } + offset_ += length; + return true; + } + + Span buffer() const { return buffer_; } + Span remaining_span() const { return buffer_.subspan(offset_); } + // TODO(crbug.com/520101123): Remove unsafe raw pointer and length methods. + T* begin() const { return buffer_.data(); } + T* current() const { return buffer_.data() + offset_; } + T* end() const { return buffer_.data() + buffer_.size(); } + size_t length() const { return buffer_.size(); } + size_t remaining() const { return buffer_.size() - offset_; } + size_t offset() const { return offset_; } + + explicit BigEndianBuffer(Span buffer) : buffer_(buffer) {} + // TODO(crbug.com/520101123): Remove unsafe raw pointer and length methods. + BigEndianBuffer(T* buffer, size_t length) : buffer_(buffer, length) {} + BigEndianBuffer(const BigEndianBuffer&) = delete; + BigEndianBuffer& operator=(const BigEndianBuffer&) = delete; + + protected: + void set_offset(size_t offset) { offset_ = offset; } + + private: + Span buffer_; + size_t offset_ = 0; +}; + +class BigEndianReader : public BigEndianBuffer { + public: + explicit BigEndianReader(ByteView buffer); + // TODO(crbug.com/520101123): Remove unsafe raw pointer and length methods. + BigEndianReader(const uint8_t* buffer, size_t length); + + template + bool Read(T* out) { + ByteView view = remaining_span(); + if (view.size() >= sizeof(T)) { + *out = ReadBigEndian(view.data()); + Skip(sizeof(T)); + return true; + } + return false; + } + + // TODO(crbug.com/520101123): Remove unsafe raw pointer and length methods. + bool Read(size_t length, void* out); + bool Read(ByteBuffer out); +}; + +class BigEndianWriter : public BigEndianBuffer { + public: + explicit BigEndianWriter(ByteBuffer buffer); + // TODO(crbug.com/520101123): Remove unsafe raw pointer and length methods. + BigEndianWriter(uint8_t* buffer, size_t length); + + template + bool Write(T value) { + ByteBuffer view = remaining_span(); + if (view.size() >= sizeof(T)) { + WriteBigEndian(value, view.data()); + Skip(sizeof(T)); + return true; + } + return false; + } + + // TODO(crbug.com/520101123): Remove unsafe raw pointer and length methods. + bool Write(const void* buffer, size_t length); + bool Write(ByteView buffer); +}; + +} // namespace openscreen + +#endif // UTIL_BIG_ENDIAN_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/bit_vector.cc b/breadcast-caststream-sys/vendor/openscreen/util/bit_vector.cc new file mode 100644 index 0000000..9472633 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/bit_vector.cc @@ -0,0 +1,34 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/bit_vector.h" + +#include + +namespace openscreen { + +BitVector::BitVector(size_t size, Fill fill) { + Resize(size, fill); +} + +void BitVector::Resize(size_t size, Fill fill) { + size_ = size; + v_.assign((size + kBitsPerWord - 1) / kBitsPerWord, + fill ? ~uint64_t{0} : uint64_t{0}); + if (fill && size % kBitsPerWord != 0) { + v_.back() &= (uint64_t{1} << (size % kBitsPerWord)) - 1; + } +} + +size_t BitVector::FindFirstSet() const { + for (size_t i = 0; i < v_.size(); ++i) { + if (v_[i] != 0) { + size_t pos = i * kBitsPerWord + std::countr_zero(v_[i]); + return (pos < size_) ? pos : size_; + } + } + return size_; +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/util/bit_vector.h b/breadcast-caststream-sys/vendor/openscreen/util/bit_vector.h new file mode 100644 index 0000000..b120f6d --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/bit_vector.h @@ -0,0 +1,66 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_BIT_VECTOR_H_ +#define UTIL_BIT_VECTOR_H_ + +#include +#include + +#include +#include +#include + +#include "util/osp_logging.h" + +namespace openscreen { + +// A simple dynamic bit vector implementation using C++20 and std::vector. +// This is used for tracking packet transmission flags in the Sender. +class BitVector { + public: + enum Fill : bool { SET = true, CLEARED = false }; + + BitVector() noexcept = default; + BitVector(size_t size, Fill fill); + + ~BitVector() = default; + + BitVector(BitVector&& other) noexcept = default; + BitVector& operator=(BitVector&& other) noexcept = default; + + BitVector(const BitVector& other) = default; + BitVector& operator=(const BitVector& other) = default; + + [[nodiscard]] size_t size() const noexcept { return size_; } + + void Resize(size_t size, Fill fill); + + void Set(size_t pos) { + OSP_CHECK_LT(pos, size_); + v_[pos / kBitsPerWord] |= (uint64_t{1} << (pos % kBitsPerWord)); + } + + void Clear(size_t pos) { + OSP_CHECK_LT(pos, size_); + v_[pos / kBitsPerWord] &= ~(uint64_t{1} << (pos % kBitsPerWord)); + } + + [[nodiscard]] bool IsSet(size_t pos) const { + OSP_CHECK_LT(pos, size_); + return (v_[pos / kBitsPerWord] >> (pos % kBitsPerWord)) & 1; + } + + [[nodiscard]] size_t FindFirstSet() const; + + private: + static constexpr size_t kBitsPerWord = std::numeric_limits::digits; + + std::vector v_; + size_t size_ = 0; +}; + +} // namespace openscreen + +#endif // UTIL_BIT_VECTOR_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/chrono_helpers.h b/breadcast-caststream-sys/vendor/openscreen/util/chrono_helpers.h new file mode 100644 index 0000000..2a76ca8 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/chrono_helpers.h @@ -0,0 +1,50 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_CHRONO_HELPERS_H_ +#define UTIL_CHRONO_HELPERS_H_ + +#include + +// This file is a collection of helpful utilities and using statement for +// working with std::chrono. In practice we previously defined these frequently, +// this header allows for a single set of convenience statements. +namespace openscreen { + +using hours = std::chrono::hours; +using microseconds = std::chrono::microseconds; +using milliseconds = std::chrono::milliseconds; +using nanoseconds = std::chrono::nanoseconds; +using seconds = std::chrono::seconds; + +// Casting statements. Note that duration_cast is not a type, it's a function, +// so its behavior is different than the using statements above. +template +static constexpr hours to_hours(D d) { + return std::chrono::duration_cast(d); +} + +template +static constexpr microseconds to_microseconds(D d) { + return std::chrono::duration_cast(d); +} + +template +static constexpr milliseconds to_milliseconds(D d) { + return std::chrono::duration_cast(d); +} + +template +static constexpr nanoseconds to_nanoseconds(D d) { + return std::chrono::duration_cast(d); +} + +template +static constexpr seconds to_seconds(D d) { + return std::chrono::duration_cast(d); +} + +} // namespace openscreen + +#endif // UTIL_CHRONO_HELPERS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/crypto/openssl_util.cc b/breadcast-caststream-sys/vendor/openscreen/util/crypto/openssl_util.cc new file mode 100644 index 0000000..2651a7a --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/crypto/openssl_util.cc @@ -0,0 +1,62 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/crypto/openssl_util.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "util/osp_logging.h" + +namespace openscreen { + +namespace { + +// Callback routine for OpenSSL to print error messages. `str` is a +// nullptr-terminated string of length `len` containing diagnostic information +// such as the library, function and reason for the error, the file and line +// where the error originated, plus potentially any context-specific +// information about the error. `context` contains a pointer to user-supplied +// data, which is currently unused. +// If this callback returns a value <= 0, OpenSSL will stop processing the +// error queue and return, otherwise it will continue calling this function +// until all errors have been removed from the queue. +int OpenSSLErrorCallback(const char* str, size_t len, void* context) { + OSP_DVLOG << "\t" << std::string_view(str, len); + return 1; +} + +} // namespace + +void EnsureOpenSSLInit() { + // LOCAL PATCH (breadcast): upstream calls OPENSSL_init_ssl() here, but + // this vendored subset never touches libssl (no TLS -- see + // ../../../PATCHES.md), so this just does the general libcrypto init + // instead. Safe to call repeatedly; OpenSSL 3.x makes this optional + // anyway, but frame_crypto.cc's key setup wants it done up front. + OPENSSL_init_crypto(0, nullptr); +} + +void ClearOpenSSLERRStack(const Location& location) { + if (OSP_DCHECK_IS_ON()) { + uint32_t error_num = ERR_peek_error(); + if (error_num == 0) { + return; + } + + OSP_DVLOG << "OpenSSL ERR_get_error stack from " << location; + ERR_print_errors_cb(&OpenSSLErrorCallback, nullptr); + } else { + ERR_clear_error(); + } +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/util/crypto/openssl_util.h b/breadcast-caststream-sys/vendor/openscreen/util/crypto/openssl_util.h new file mode 100644 index 0000000..61ceb73 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/crypto/openssl_util.h @@ -0,0 +1,61 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_CRYPTO_OPENSSL_UTIL_H_ +#define UTIL_CRYPTO_OPENSSL_UTIL_H_ + +#include + +#include + +#include "platform/base/error.h" +#include "platform/base/location.h" + +// LOCAL PATCH (breadcast): dropped SSLErrorCodeToError()/GetSSLError(), +// which format an SSL_get_error() result and reference BoringSSL's +// SSL_error_description() (not part of system OpenSSL's public API). Unused +// here -- the TLS CASTV2 control channel is handled by the existing +// rust_cast-based Rust code, not this vendored openscreen subset, which only +// needs the general ERR_*/AES pieces. See vendor/openscreen/PATCHES.md. + +namespace openscreen { +// Initialize OpenSSL if it isn't already initialized. This must be called +// before any other OpenSSL functions though it is safe and cheap to call this +// multiple times. +// This function is thread-safe, and OpenSSL will only ever be initialized once. +// OpenSSL will be properly shut down on program exit. +// Multiple sequential calls to EnsureOpenSSLInit or EnsureOpenSSLCleanup are +// ignored by OpenSSL itself. +void EnsureOpenSSLInit(); + +// Drains the OpenSSL ERR_get_error stack. On a debug build the error codes +// are send to VLOG(1), on a release build they are disregarded. In most +// cases you should pass CURRENT_LOCATION as the `location`. +void ClearOpenSSLERRStack(const Location& location); + +// Place an instance of this class on the call stack to automatically clear +// the OpenSSL error stack on function exit. +class OpenSSLErrStackTracer { + public: + // Pass CURRENT_LOCATION as `location`, to help track the source of OpenSSL + // error messages. Note any diagnostic emitted will be tagged with the + // location of the constructor call as it's not possible to trace a + // destructor's callsite. + explicit OpenSSLErrStackTracer(const Location& location) + : location_(location) { + EnsureOpenSSLInit(); + } + OpenSSLErrStackTracer(const OpenSSLErrStackTracer&) = delete; + OpenSSLErrStackTracer(OpenSSLErrStackTracer&&) noexcept = delete; + OpenSSLErrStackTracer& operator=(const OpenSSLErrStackTracer&) = delete; + OpenSSLErrStackTracer& operator=(OpenSSLErrStackTracer&&) = delete; + ~OpenSSLErrStackTracer() { ClearOpenSSLERRStack(location_); } + + private: + const Location location_; +}; + +} // namespace openscreen + +#endif // UTIL_CRYPTO_OPENSSL_UTIL_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/crypto/random_bytes.cc b/breadcast-caststream-sys/vendor/openscreen/util/crypto/random_bytes.cc new file mode 100644 index 0000000..99d9b45 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/crypto/random_bytes.cc @@ -0,0 +1,23 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/crypto/random_bytes.h" + +#include "openssl/rand.h" +#include "util/osp_logging.h" + +namespace openscreen { + +std::array GenerateRandomBytes16() { + std::array result; + GenerateRandomBytes(result); + return result; +} + +void GenerateRandomBytes(ByteBuffer out) { + // Working cryptography is mandatory for our library to run. + OSP_CHECK(RAND_bytes(out.data(), out.size()) == 1); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/util/crypto/random_bytes.h b/breadcast-caststream-sys/vendor/openscreen/util/crypto/random_bytes.h new file mode 100644 index 0000000..d3d3e22 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/crypto/random_bytes.h @@ -0,0 +1,20 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_CRYPTO_RANDOM_BYTES_H_ +#define UTIL_CRYPTO_RANDOM_BYTES_H_ + +#include +#include + +#include "platform/base/span.h" + +namespace openscreen { + +std::array GenerateRandomBytes16(); +void GenerateRandomBytes(ByteBuffer out); + +} // namespace openscreen + +#endif // UTIL_CRYPTO_RANDOM_BYTES_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/enum_name_table.h b/breadcast-caststream-sys/vendor/openscreen/util/enum_name_table.h new file mode 100644 index 0000000..f2d7a29 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/enum_name_table.h @@ -0,0 +1,51 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// This file contains helpers for working with enums that require +// both enum->string and string->enum conversions. + +#ifndef UTIL_ENUM_NAME_TABLE_H_ +#define UTIL_ENUM_NAME_TABLE_H_ + +#include +#include +#include + +#include "platform/base/error.h" +#include "util/osp_logging.h" +#include "util/string_util.h" + +namespace openscreen { + +inline constexpr char kUnknownEnumError[] = "Enum value not in array"; + +template +using EnumNameTable = std::array, Size>; + +// Get the name of an enum from the enum value. +template +ErrorOr GetEnumName(const EnumNameTable& map, + Enum enum_) { + for (auto pair : map) { + if (pair.second == enum_) { + return pair.first; + } + } + return Error(Error::Code::kParameterInvalid, kUnknownEnumError); +} + +// Get the value of an enum from the enum name. +template +ErrorOr GetEnum(const EnumNameTable& map, + std::string_view name) { + for (auto pair : map) { + if (::openscreen::string_util::EqualsIgnoreCase(pair.first, name)) { + return pair.second; + } + } + return Error(Error::Code::kParameterInvalid, kUnknownEnumError); +} + +} // namespace openscreen +#endif // UTIL_ENUM_NAME_TABLE_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/flat_map.h b/breadcast-caststream-sys/vendor/openscreen/util/flat_map.h new file mode 100644 index 0000000..dadd4a2 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/flat_map.h @@ -0,0 +1,66 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_FLAT_MAP_H_ +#define UTIL_FLAT_MAP_H_ + +#include +#include +#include +#include +#include + +#include "util/osp_logging.h" + +namespace openscreen { + +// For small numbers of elements, a vector is much more efficient than a +// map or unordered_map due to not needing hashing. FlatMap allows for +// using map-like syntax but is backed by a std::vector, combining all the +// performance of a vector with the convenience of a map. +// +// NOTE: this class allows usage of const char* as Key or Value types, but +// it is generally recommended that you use std::string, or std::string_view +// for literals. string_view is similarly efficient to a raw char* pointer, +// but gives sizing and equality operators, among other features. +template +class FlatMap final : public std::vector> { + public: + FlatMap(std::initializer_list> init_list) + : std::vector>(init_list) {} + FlatMap() = default; + FlatMap(const FlatMap&) = default; + FlatMap(FlatMap&&) noexcept = default; + FlatMap& operator=(const FlatMap&) = default; + FlatMap& operator=(FlatMap&&) = default; + ~FlatMap() = default; + + // Accessors that wrap std::find_if, and return an iterator to the key value + // pair. + decltype(auto) find(const Key& key) { + return std::find_if( + this->begin(), this->end(), + [key](const std::pair& pair) { return key == pair.first; }); + } + + decltype(auto) find(const Key& key) const { + return const_cast*>(this)->find(key); + } + + // Remove an entry from the map. Returns an iterator pointing to the new + // location of the element that followed the last element erased by the + // function call. This is the container end if the operation erased the last + // element in the sequence. + decltype(auto) erase_key(const Key& key) { + auto it = find(key); + if (it == this->end()) { + return this->end(); + } + return static_cast>*>(this)->erase(it); + } +}; + +} // namespace openscreen + +#endif // UTIL_FLAT_MAP_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/hashing.h b/breadcast-caststream-sys/vendor/openscreen/util/hashing.h new file mode 100644 index 0000000..b7bd06f --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/hashing.h @@ -0,0 +1,55 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_HASHING_H_ +#define UTIL_HASHING_H_ + +#include +#include +#include + +namespace openscreen { + +// This value is taken from absl::Hash implementation. +inline constexpr uint64_t kDefaultSeed = UINT64_C(0xc3a5c85c97cb3127); + +// Computes the aggregate hash of the provided hashable objects. +// Seed must initially use a large prime between 2^63 and 2^64 as a starting +// value, or the result of a previous call to this function. +template +uint64_t ComputeAggregateHash(uint64_t original_seed, const T&... objs) { + auto hash_combiner = [](uint64_t current_seed, + uint64_t hash_value) -> uint64_t { + static const uint64_t kMultiplier = UINT64_C(0x9ddfea08eb382d69); + uint64_t a = (hash_value ^ current_seed) * kMultiplier; + a ^= (a >> 47); + uint64_t b = (current_seed ^ a) * kMultiplier; + b ^= (b >> 47); + b *= kMultiplier; + return b; + }; + + uint64_t result = original_seed; + std::vector hashes = {std::hash()(objs)...}; + for (uint64_t hash : hashes) { + result = hash_combiner(result, hash); + } + return result; +} + +template +uint64_t ComputeAggregateHash(const T&... objs) { + return ComputeAggregateHash(kDefaultSeed, objs...); +} + +struct PairHash { + template + size_t operator()(const std::pair& pair) const { + return ComputeAggregateHash(pair.first, pair.second); + } +}; + +} // namespace openscreen + +#endif // UTIL_HASHING_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/integer_division.h b/breadcast-caststream-sys/vendor/openscreen/util/integer_division.h new file mode 100644 index 0000000..d7f0fff --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/integer_division.h @@ -0,0 +1,67 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_INTEGER_DIVISION_H_ +#define UTIL_INTEGER_DIVISION_H_ + +#include + +namespace openscreen { + +// Returns CEIL(num ÷ denom). `denom` must not equal zero. This function is +// compatible with any integer-like type, including the integer-based +// std::chrono duration types. +// +// Optimization note: See DividePositivesRoundingUp(). +template +constexpr auto DivideRoundingUp(Integer num, Integer denom) { + if (denom < Integer{0}) { + num *= -1; + denom *= -1; + } + if (num < Integer{0}) { + return num / denom; + } + return (num + denom - Integer{1}) / denom; +} + +// Same as DivideRoundingUp(), except is more-efficient for hot code paths that +// know `num` is always greater or equal to zero, and `denom` is always greater +// than zero. +template +constexpr Integer DividePositivesRoundingUp(Integer num, Integer denom) { + return DivideRoundingUp::type>(num, + denom); +} + +// Divides `num` by `denom`, and rounds to the nearest integer (exactly halfway +// between integers will round to the higher integer). This function is +// compatible with any integer-like type, including the integer-based +// std::chrono duration types. +// +// Optimization note: See DividePositivesRoundingNearest(). +template +constexpr auto DivideRoundingNearest(Integer num, Integer denom) { + if (denom < Integer{0}) { + num *= -1; + denom *= -1; + } + if (num < Integer{0}) { + return (num - ((denom - Integer{1}) / 2)) / denom; + } + return (num + (denom / 2)) / denom; +} + +// Same as DivideRoundingNearest(), except is more-efficient for hot code paths +// that know `num` is always greater or equal to zero, and `denom` is always +// greater than zero. +template +constexpr Integer DividePositivesRoundingNearest(Integer num, Integer denom) { + return DivideRoundingNearest::type>( + num, denom); +} + +} // namespace openscreen + +#endif // UTIL_INTEGER_DIVISION_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/json/json_helpers.h b/breadcast-caststream-sys/vendor/openscreen/util/json/json_helpers.h new file mode 100644 index 0000000..a0b0362 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/json/json_helpers.h @@ -0,0 +1,205 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_JSON_JSON_HELPERS_H_ +#define UTIL_JSON_JSON_HELPERS_H_ + +#include +#include +#include +#include +#include +#include + +#include "json/value.h" +#include "platform/base/error.h" +#include "util/chrono_helpers.h" +#include "util/json/json_serialization.h" +#include "util/simple_fraction.h" + +// This file contains helper methods for parsing JSON, in an attempt to +// reduce boilerplate code when working with JsonCpp. +namespace openscreen::json { + +inline bool TryParseBool(const Json::Value& value, bool* out) { + if (!value.isBool()) { + return false; + } + *out = value.asBool(); + return true; +} + +// A general note about parsing primitives. "Validation" in this context +// generally means ensuring that the values are non-negative, excepting doubles +// which may be negative in some cases. +inline bool TryParseDouble(const Json::Value& value, + double* out, + bool allow_negative = false) { + if (!value.isDouble()) { + return false; + } + const double d = value.asDouble(); + if (std::isnan(d)) { + return false; + } + if (!allow_negative && d < 0) { + return false; + } + *out = d; + return true; +} + +inline bool TryParseInt(const Json::Value& value, int* out) { + if (!value.isInt()) { + return false; + } + int i = value.asInt(); + if (i < 0) { + return false; + } + *out = i; + return true; +} + +inline bool TryParseUint(const Json::Value& value, uint32_t* out) { + if (!value.isUInt()) { + return false; + } + *out = value.asUInt(); + return true; +} + +inline bool TryParseString(const Json::Value& value, std::string* out) { + if (!value.isString()) { + return false; + } + *out = value.asString(); + return true; +} + +// We want to be more robust when we parse fractions then just +// allowing strings, this will parse numeral values such as +// value: 50 as well as value: "50" and value: "100/2". +inline bool TryParseSimpleFraction(const Json::Value& value, + SimpleFraction* out) { + if (value.isInt()) { + int parsed = value.asInt(); + if (parsed < 0) { + return false; + } + *out = SimpleFraction{parsed, 1}; + return true; + } + + if (value.isString()) { + auto fraction_or_error = SimpleFraction::FromString(value.asString()); + if (!fraction_or_error) { + return false; + } + + if (!fraction_or_error.value().is_positive() || + !fraction_or_error.value().is_defined()) { + return false; + } + *out = std::move(fraction_or_error.value()); + return true; + } + return false; +} + +inline bool TryParseMilliseconds(const Json::Value& value, milliseconds* out) { + int out_ms; + if (!TryParseInt(value, &out_ms) || out_ms < 0) { + return false; + } + *out = milliseconds(out_ms); + return true; +} + +template +using Parser = std::function; + +// NOTE: array parsing methods reset the output vector to an empty vector in +// any error case. This is especially useful for optional arrays. +template +bool TryParseArray(const Json::Value& value, + Parser parser, + std::vector* out) { + out->clear(); + if (!value.isArray() || value.empty()) { + return false; + } + + out->reserve(value.size()); + for (Json::ArrayIndex i = 0; i < value.size(); ++i) { + T v; + if (!parser(value[i], &v)) { + out->clear(); + return false; + } + out->push_back(v); + } + + return true; +} + +inline bool TryParseIntArray(const Json::Value& value, std::vector* out) { + return TryParseArray(value, TryParseInt, out); +} + +inline bool TryParseUintArray(const Json::Value& value, + std::vector* out) { + return TryParseArray(value, TryParseUint, out); +} + +inline bool TryParseStringArray(const Json::Value& value, + std::vector* out) { + return TryParseArray(value, TryParseString, out); +} + +inline bool TryParseNestedStringArray( + const Json::Value& value, + std::vector>* out) { + return TryParseArray>(value, TryParseStringArray, + out); +} + +template +Json::Value PrimitiveVectorToJson(const std::vector& vec) { + Json::Value array(Json::ValueType::arrayValue); + array.resize(vec.size()); + + for (Json::Value::ArrayIndex i = 0; i < vec.size(); ++i) { + array[i] = Json::Value(vec[i]); + } + + return array; +} + +inline Json::Value NestedStringArrayToJson( + const std::vector>& vec) { + Json::Value array(Json::ValueType::arrayValue); + array.resize(vec.size()); + + for (Json::Value::ArrayIndex i = 0; i < vec.size(); ++i) { + array[i] = PrimitiveVectorToJson(vec[i]); + } + + return array; +} + +inline bool Contains(const Json::Value& array, std::string_view value) { + if (!array.isArray()) { + return false; + } + for (const Json::Value& entry : array) { + if (entry.isString() && entry.asString() == value) { + return true; + } + } + return false; +} +} // namespace openscreen::json + +#endif // UTIL_JSON_JSON_HELPERS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/json/json_serialization.cc b/breadcast-caststream-sys/vendor/openscreen/util/json/json_serialization.cc new file mode 100644 index 0000000..29d18df --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/json/json_serialization.cc @@ -0,0 +1,62 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/json/json_serialization.h" + +#include +#include +#include +#include + +#include "json/reader.h" +#include "json/writer.h" +#include "platform/base/error.h" +#include "util/osp_logging.h" + +namespace openscreen::json { + +ErrorOr Parse(std::string_view document) { + Json::CharReaderBuilder builder; + Json::CharReaderBuilder::strictMode(&builder.settings_); + if (document.empty()) { + return ErrorOr(Error::Code::kJsonParseError, "empty document"); + } + + Json::Value root_node; + std::string error_msg; + std::unique_ptr reader(builder.newCharReader()); + const bool succeeded = reader->parse(&*document.begin(), &*document.end(), + &root_node, &error_msg); + if (!succeeded) { + return ErrorOr(Error::Code::kJsonParseError, error_msg); + } + + return root_node; +} + +ErrorOr Stringify(const Json::Value& value) { + Json::StreamWriterBuilder factory; +#ifndef _DEBUG + // Default is to "pretty print" the output JSON in a human readable + // format. On non-debug builds, we can remove pretty printing by simply + // getting rid of all indentation. + factory["indentation"] = ""; +#endif + + std::unique_ptr const writer(factory.newStreamWriter()); + std::ostringstream stream; + writer->write(value, &stream); + + if (!stream) { + // Note: jsoncpp doesn't give us more information about what actually + // went wrong, just says to "check the stream". However, failures on + // the stream should be rare, as we do not throw any errors in the jsoncpp + // library. + return ErrorOr(Error::Code::kJsonWriteError, "Invalid stream"); + } + + return stream.str(); +} + +} // namespace openscreen::json diff --git a/breadcast-caststream-sys/vendor/openscreen/util/json/json_serialization.h b/breadcast-caststream-sys/vendor/openscreen/util/json/json_serialization.h new file mode 100644 index 0000000..0d9373d --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/json/json_serialization.h @@ -0,0 +1,24 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_JSON_JSON_SERIALIZATION_H_ +#define UTIL_JSON_JSON_SERIALIZATION_H_ + +#include +#include + +#include "json/value.h" +#include "platform/base/error.h" + +namespace openscreen { + +namespace json { + +ErrorOr Parse(std::string_view value); +ErrorOr Stringify(const Json::Value& value); + +} // namespace json +} // namespace openscreen + +#endif // UTIL_JSON_JSON_SERIALIZATION_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/json/json_value.cc b/breadcast-caststream-sys/vendor/openscreen/util/json/json_value.cc new file mode 100644 index 0000000..f8af14b --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/json/json_value.cc @@ -0,0 +1,43 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/json/json_value.h" + +namespace openscreen { + +std::optional MaybeGetInt(const Json::Value& message, + const char* first, + const char* last) { + const Json::Value* value = message.find(first, last); + std::optional result; + if (value && value->isInt()) { + result = value->asInt(); + } + return result; +} + +std::optional MaybeGetString(const Json::Value& message) { + if (message.isString()) { + const char* begin = nullptr; + const char* end = nullptr; + message.getString(&begin, &end); + if (begin && end >= begin) { + return std::string_view(begin, end - begin); + } + } + return std::nullopt; +} + +std::optional MaybeGetString(const Json::Value& message, + const char* first, + const char* last) { + const Json::Value* value = message.find(first, last); + std::optional result; + if (value && value->isString()) { + return MaybeGetString(*value); + } + return result; +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/util/json/json_value.h b/breadcast-caststream-sys/vendor/openscreen/util/json/json_value.h new file mode 100644 index 0000000..93e2770 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/json/json_value.h @@ -0,0 +1,29 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_JSON_JSON_VALUE_H_ +#define UTIL_JSON_JSON_VALUE_H_ + +#include +#include + +#include "json/value.h" + +#define JSON_EXPAND_FIND_CONSTANT_ARGS(s) (s), ((s) + sizeof(s) - 1) + +namespace openscreen { + +std::optional MaybeGetInt(const Json::Value& message, + const char* first, + const char* last); + +std::optional MaybeGetString(const Json::Value& message); + +std::optional MaybeGetString(const Json::Value& message, + const char* first, + const char* last); + +} // namespace openscreen + +#endif // UTIL_JSON_JSON_VALUE_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/no_destructor.h b/breadcast-caststream-sys/vendor/openscreen/util/no_destructor.h new file mode 100644 index 0000000..7fa0acd --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/no_destructor.h @@ -0,0 +1,79 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_NO_DESTRUCTOR_H_ +#define UTIL_NO_DESTRUCTOR_H_ + +#include +#include +#include + +namespace openscreen { + +// Helper type to create a function-local static variable of type `T` when `T` +// has a non-trivial destructor. Storing a `T` in a `NoDestructor` will +// prevent `~T()` from running, even when the variable goes out of scope. +// +// Useful when a variable has static storage duration but its type has a +// non-trivial destructor. Using a function-local static variable prevents +// global constructors, while using `NoDestructor` prevents global +// destructors. +// +// ## Example Usage +// +// const std::string& GetDefaultText() { +// // Required since `static const std::string` requires a global destructor. +// static const openscreen::NoDestructor s("Hello world!"); +// return *s; +// } +template +class NoDestructor { + public: + static_assert(!(std::is_trivially_constructible_v && + std::is_trivially_destructible_v), + "T is trivially constructible and destructible; please use a " + "constinit object of type T directly instead"); + + static_assert( + !std::is_trivially_destructible_v, + "T is trivially destructible; please use a function-local static " + "of type T directly instead"); + + // Not constexpr; just write static constexpr T x = ...; if the value should + // be a constexpr. + template + explicit NoDestructor(Args&&... args) { + new (storage_) T(std::forward(args)...); + } + + // Allows copy and move construction of the contained type, to allow + // construction from an initializer list, e.g. for std::vector. + explicit NoDestructor(const T& x) { new (storage_) T(x); } + explicit NoDestructor(T&& x) { new (storage_) T(std::move(x)); } + + NoDestructor(const NoDestructor&) = delete; + NoDestructor& operator=(const NoDestructor&) = delete; + + ~NoDestructor() = default; + + const T& operator*() const { return *get(); } + T& operator*() { return *get(); } + + const T* operator->() const { return get(); } + T* operator->() { return get(); } + + const T* get() const { return reinterpret_cast(storage_); } + T* get() { return reinterpret_cast(storage_); } + + private: + alignas(T) char storage_[sizeof(T)]; + +#if defined(LEAK_SANITIZER) + T* storage_ptr_ = reinterpret_cast(storage_); +#endif // defined(LEAK_SANITIZER) +}; + +} // namespace openscreen + +#endif // UTIL_NO_DESTRUCTOR_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/osp_logging.h b/breadcast-caststream-sys/vendor/openscreen/util/osp_logging.h new file mode 100644 index 0000000..ffe04f9 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/osp_logging.h @@ -0,0 +1,145 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_OSP_LOGGING_H_ +#define UTIL_OSP_LOGGING_H_ + +#include +#include +#include + +#include "platform/api/logging.h" + +namespace openscreen::internal { + +// The stream-based logging macros below are adapted from Chromium's +// base/logging.h. +class LogMessage { + public: + LogMessage(LogLevel level, const char* file, int line) + : level_(level), file_(file), line_(line) {} + + ~LogMessage() { + LogWithLevel(level_, file_, line_, std::move(stream_)); + if (level_ == LogLevel::kFatal) { + Break(); + } + } + + std::ostream& stream() { return stream_; } + + protected: + const LogLevel level_; + + // The file here comes from the __FILE__ macro, which should persist while + // we are doing the logging. Hence, keeping it unmanaged here and not + // creating a copy should be safe. + const char* const file_; + const int line_; + std::stringstream stream_; +}; + +// Used by the OSP_LAZY_STREAM macro to return void after evaluating an ostream +// chain expression. +class Voidify { + public: + void operator&(std::ostream&) {} +}; + +} // namespace openscreen::internal + +#define OSP_LAZY_STREAM(condition, stream) \ + !(condition) ? (void)0 : openscreen::internal::Voidify() & (stream) +#define OSP_LOG_IS_ON(level_enum) \ + openscreen::IsLoggingOn(openscreen::LogLevel::level_enum, \ + std::string_view(__FILE__, std::size(__FILE__))) +#define OSP_LOG_STREAM(level_enum) \ + openscreen::internal::LogMessage(openscreen::LogLevel::level_enum, __FILE__, \ + __LINE__) \ + .stream() + +#define OSP_VLOG \ + OSP_LAZY_STREAM(OSP_LOG_IS_ON(kVerbose), OSP_LOG_STREAM(kVerbose)) +#define OSP_LOG_INFO \ + OSP_LAZY_STREAM(OSP_LOG_IS_ON(kInfo), OSP_LOG_STREAM(kInfo)) +#define OSP_LOG_WARN \ + OSP_LAZY_STREAM(OSP_LOG_IS_ON(kWarning), OSP_LOG_STREAM(kWarning)) +#define OSP_LOG_ERROR \ + OSP_LAZY_STREAM(OSP_LOG_IS_ON(kError), OSP_LOG_STREAM(kError)) +#define OSP_LOG_FATAL \ + OSP_LAZY_STREAM(OSP_LOG_IS_ON(kFatal), OSP_LOG_STREAM(kFatal)) + +#define OSP_VLOG_IF(condition) !(condition) ? (void)0 : OSP_VLOG +#define OSP_LOG_IF(level, condition) !(condition) ? (void)0 : OSP_LOG_##level + +#define OSP_CHECK(condition) \ + OSP_LOG_IF(FATAL, !(condition)) << "OSP_CHECK(" << #condition << ") failed: " + +#define OSP_CHECK_EQ(a, b) \ + OSP_CHECK((a) == (b)) << (a) << " vs. " << (b) << ": " +#define OSP_CHECK_NE(a, b) \ + OSP_CHECK((a) != (b)) << (a) << " vs. " << (b) << ": " +#define OSP_CHECK_LT(a, b) OSP_CHECK((a) < (b)) << (a) << " vs. " << (b) << ": " +#define OSP_CHECK_LE(a, b) \ + OSP_CHECK((a) <= (b)) << (a) << " vs. " << (b) << ": " +#define OSP_CHECK_GT(a, b) OSP_CHECK((a) > (b)) << (a) << " vs. " << (b) << ": " +#define OSP_CHECK_GE(a, b) \ + OSP_CHECK((a) >= (b)) << (a) << " vs. " << (b) << ": " + +#if defined(_DEBUG) || defined(DCHECK_ALWAYS_ON) +#define OSP_DCHECK_IS_ON() 1 +#define OSP_DCHECK(condition) OSP_CHECK(condition) +#define OSP_DCHECK_EQ(a, b) OSP_CHECK_EQ(a, b) +#define OSP_DCHECK_NE(a, b) OSP_CHECK_NE(a, b) +#define OSP_DCHECK_LT(a, b) OSP_CHECK_LT(a, b) +#define OSP_DCHECK_LE(a, b) OSP_CHECK_LE(a, b) +#define OSP_DCHECK_GT(a, b) OSP_CHECK_GT(a, b) +#define OSP_DCHECK_GE(a, b) OSP_CHECK_GE(a, b) +#else +#define OSP_DCHECK_IS_ON() 0 +// When DCHECKs are off, nothing will be logged. Use that fact to make +// references to the `condition` expression (or `a` and `b`) so the compiler +// won't emit unused variable warnings/errors when DCHECKs are turned off. +#define OSP_EAT_STREAM OSP_LOG_IF(FATAL, false) +#define OSP_DCHECK(condition) OSP_EAT_STREAM << !(condition) +#define OSP_DCHECK_EQ(a, b) OSP_EAT_STREAM << !((a) == (b)) +#define OSP_DCHECK_NE(a, b) OSP_EAT_STREAM << !((a) != (b)) +#define OSP_DCHECK_LT(a, b) OSP_EAT_STREAM << !((a) < (b)) +#define OSP_DCHECK_LE(a, b) OSP_EAT_STREAM << !((a) <= (b)) +#define OSP_DCHECK_GT(a, b) OSP_EAT_STREAM << !((a) > (b)) +#define OSP_DCHECK_GE(a, b) OSP_EAT_STREAM << !((a) >= (b)) +#endif + +#define OSP_DVLOG OSP_VLOG_IF(OSP_DCHECK_IS_ON()) +#define OSP_DLOG_INFO OSP_LOG_IF(INFO, OSP_DCHECK_IS_ON()) +#define OSP_DLOG_WARN OSP_LOG_IF(WARN, OSP_DCHECK_IS_ON()) +#define OSP_DLOG_ERROR OSP_LOG_IF(ERROR, OSP_DCHECK_IS_ON()) +#define OSP_DLOG_FATAL OSP_LOG_IF(FATAL, OSP_DCHECK_IS_ON()) +#define OSP_DVLOG_IF(condition) OSP_VLOG_IF(OSP_DCHECK_IS_ON() && (condition)) +#define OSP_DLOG_IF(level, condition) \ + OSP_LOG_IF(level, OSP_DCHECK_IS_ON() && (condition)) + +// Log when unimplemented code points are reached: If verbose logging is turned +// on, log always. Otherwise, just attempt to log once. +#define OSP_UNIMPLEMENTED() \ + if (OSP_LOG_IS_ON(kVerbose)) { \ + OSP_LOG_STREAM(kVerbose) << __func__ << ": UNIMPLEMENTED() hit."; \ + } else { \ + static bool needs_warning = true; \ + if (needs_warning) { \ + OSP_LOG_WARN << __func__ << ": UNIMPLEMENTED() hit."; \ + needs_warning = false; \ + } \ + } + +// Since Break() is annotated as noreturn, this will properly signal to the +// compiler that this code is truly not reached (and thus doesn't need a return +// statement for non-void returning functions/methods). +#define OSP_NOTREACHED() \ + { \ + OSP_LOG_FATAL << __func__ << ": NOTREACHED() hit."; \ + Break(); \ + } + +#endif // UTIL_OSP_LOGGING_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/raw_ptr.h b/breadcast-caststream-sys/vendor/openscreen/util/raw_ptr.h new file mode 100644 index 0000000..4def1fc --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/raw_ptr.h @@ -0,0 +1,441 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_RAW_PTR_H_ +#define UTIL_RAW_PTR_H_ + +// This header implements a conditional `raw_ptr` template. +// +// In Chromium builds (when `BUILD_WITH_CHROMIUM` is defined), it aliases +// Chromium's `base::raw_ptr` (MiraclePtr / BackupRefPtr). This allows Open +// Screen code to benefit from Chromium's UAF protection when running inside +// Chrome. +// +// In standalone builds (e.g., embedded/IoT builds where dependencies must be +// minimized and overhead must be zero), it provides a zero-overhead, +// dependency-free polyfill that behaves like a standard raw pointer but +// enforces initialization to `nullptr`. +// +// Note: Traits (like DanglingUntriaged or AllowPtrArithmetic) are intentionally +// not supported in Open Screen to ensure code safety and compatibility. + +#if defined(BUILD_WITH_CHROMIUM) + +#include "partition_alloc/pointers/raw_ptr.h" // nogncheck + +namespace openscreen { + +// Alias the Chromium implementation, restricting it to not use traits. +template +using raw_ptr = ::base::raw_ptr; + +} // namespace openscreen + +#else // !defined(BUILD_WITH_CHROMIUM) + +#include +#include +#include +#include +#include +#include + +// Optimization macros to ensure the polyfill truly has zero overhead at the ABI +// level. +#if defined(__clang__) +#define OPENSCREEN_TRIVIAL_ABI [[clang::trivial_abi]] +#else +#define OPENSCREEN_TRIVIAL_ABI +#endif + +#if defined(_MSC_VER) +#define OPENSCREEN_ALWAYS_INLINE __forceinline +#elif defined(__GNUC__) || defined(__clang__) +#define OPENSCREEN_ALWAYS_INLINE __attribute__((always_inline)) inline +#else +#define OPENSCREEN_ALWAYS_INLINE inline +#endif + +namespace openscreen { + +// Standalone polyfill for `raw_ptr`. +// It has zero runtime overhead compared to a raw pointer and compiles away. +template +class OPENSCREEN_TRIVIAL_ABI raw_ptr { + public: + // Safety: auto-initialize to nullptr. + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr() noexcept : ptr_(nullptr) {} + + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr( + std::nullptr_t) noexcept // NOLINT(runtime/explicit) + : ptr_(nullptr) {} + + // Implicit conversion from raw pointer. + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr( + T* ptr) noexcept // NOLINT(runtime/explicit) + : ptr_(ptr) {} + + // Copy and Move constructors. + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr(const raw_ptr& other) noexcept = + default; + + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr(raw_ptr&& other) noexcept + : ptr_(other.ptr_) { + other.ptr_ = nullptr; + } + + // Templated copy/move constructors for upcasting (Derived -> Base). + template > > + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr( + const raw_ptr& other) noexcept // NOLINT(runtime/explicit) + : ptr_(other.get()) {} + + template > > + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr( + raw_ptr&& other) noexcept // NOLINT(runtime/explicit) + : ptr_(other.ptr_) { + other.ptr_ = nullptr; + } + + // Destructor. + OPENSCREEN_ALWAYS_INLINE constexpr ~raw_ptr() noexcept { ptr_ = nullptr; } + + // Assignment operators. + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr& operator=( + const raw_ptr& other) noexcept = default; + + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr& operator=( + raw_ptr&& other) noexcept { + if (this != &other) { + ptr_ = other.ptr_; + other.ptr_ = nullptr; + } + return *this; + } + + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr& operator=(T* ptr) noexcept { + ptr_ = ptr; + return *this; + } + + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr& operator=( + std::nullptr_t) noexcept { + ptr_ = nullptr; + return *this; + } + + // Templated assignment operators for upcasting. + template > > + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr& operator=( + const raw_ptr& other) noexcept { + ptr_ = other.get(); + return *this; + } + + template > > + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr& operator=( + raw_ptr&& other) noexcept { + ptr_ = other.ptr_; + other.ptr_ = nullptr; + return *this; + } + + // Pointer operations. + OPENSCREEN_ALWAYS_INLINE constexpr T* get() const noexcept { return ptr_; } + + // Disable operator* for void types to prevent illegal void* dereferences and + // void& signatures. + template > > + OPENSCREEN_ALWAYS_INLINE constexpr U& operator*() const noexcept { + return *ptr_; + } + + OPENSCREEN_ALWAYS_INLINE constexpr T* operator->() const noexcept { + return ptr_; + } + + OPENSCREEN_ALWAYS_INLINE constexpr operator T*() const noexcept { + return ptr_; + } + + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr& operator+=( + ptrdiff_t delta) noexcept { + ptr_ += delta; + return *this; + } + + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr& operator-=( + ptrdiff_t delta) noexcept { + ptr_ -= delta; + return *this; + } + + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr operator+( + ptrdiff_t delta) const noexcept { + return raw_ptr(ptr_ + delta); + } + + OPENSCREEN_ALWAYS_INLINE constexpr raw_ptr operator-( + ptrdiff_t delta) const noexcept { + return raw_ptr(ptr_ - delta); + } + + OPENSCREEN_ALWAYS_INLINE constexpr explicit operator bool() const noexcept { + return ptr_ != nullptr; + } + + // Swap helper. + OPENSCREEN_ALWAYS_INLINE friend constexpr void swap(raw_ptr& lhs, + raw_ptr& rhs) noexcept { + std::swap(lhs.ptr_, rhs.ptr_); + } + + // Comparison operators (raw_ptr OP raw_ptr). + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator==( + const raw_ptr& lhs, + const raw_ptr& rhs) noexcept { + return lhs.ptr_ == rhs.ptr_; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator!=( + const raw_ptr& lhs, + const raw_ptr& rhs) noexcept { + return lhs.ptr_ != rhs.ptr_; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator<( + const raw_ptr& lhs, + const raw_ptr& rhs) noexcept { + return lhs.ptr_ < rhs.ptr_; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator>( + const raw_ptr& lhs, + const raw_ptr& rhs) noexcept { + return lhs.ptr_ > rhs.ptr_; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator<=( + const raw_ptr& lhs, + const raw_ptr& rhs) noexcept { + return lhs.ptr_ <= rhs.ptr_; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator>=( + const raw_ptr& lhs, + const raw_ptr& rhs) noexcept { + return lhs.ptr_ >= rhs.ptr_; + } + + // Comparison operators (raw_ptr OP U*). + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator==(const raw_ptr& lhs, + U* rhs) noexcept { + return lhs.ptr_ == rhs; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator!=(const raw_ptr& lhs, + U* rhs) noexcept { + return lhs.ptr_ != rhs; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator<(const raw_ptr& lhs, + U* rhs) noexcept { + return lhs.ptr_ < rhs; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator>(const raw_ptr& lhs, + U* rhs) noexcept { + return lhs.ptr_ > rhs; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator<=(const raw_ptr& lhs, + U* rhs) noexcept { + return lhs.ptr_ <= rhs; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator>=(const raw_ptr& lhs, + U* rhs) noexcept { + return lhs.ptr_ >= rhs; + } + + // Comparison operators (U* OP raw_ptr). + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator==( + U* lhs, + const raw_ptr& rhs) noexcept { + return lhs == rhs.ptr_; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator!=( + U* lhs, + const raw_ptr& rhs) noexcept { + return lhs != rhs.ptr_; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator<( + U* lhs, + const raw_ptr& rhs) noexcept { + return lhs < rhs.ptr_; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator>( + U* lhs, + const raw_ptr& rhs) noexcept { + return lhs > rhs.ptr_; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator<=( + U* lhs, + const raw_ptr& rhs) noexcept { + return lhs <= rhs.ptr_; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator>=( + U* lhs, + const raw_ptr& rhs) noexcept { + return lhs >= rhs.ptr_; + } + + // Comparison operators (raw_ptr OP nullptr). + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator==( + const raw_ptr& lhs, + std::nullptr_t) noexcept { + return lhs.ptr_ == nullptr; + } + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator!=( + const raw_ptr& lhs, + std::nullptr_t) noexcept { + return lhs.ptr_ != nullptr; + } + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator<( + const raw_ptr& lhs, + std::nullptr_t) noexcept { + return lhs.ptr_ < nullptr; + } + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator>( + const raw_ptr& lhs, + std::nullptr_t) noexcept { + return lhs.ptr_ > nullptr; + } + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator<=( + const raw_ptr& lhs, + std::nullptr_t) noexcept { + return lhs.ptr_ <= nullptr; + } + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator>=( + const raw_ptr& lhs, + std::nullptr_t) noexcept { + return lhs.ptr_ >= nullptr; + } + + // Comparison operators (nullptr OP raw_ptr). + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator==( + std::nullptr_t, + const raw_ptr& rhs) noexcept { + return nullptr == rhs.ptr_; + } + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator!=( + std::nullptr_t, + const raw_ptr& rhs) noexcept { + return nullptr != rhs.ptr_; + } + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator<( + std::nullptr_t, + const raw_ptr& rhs) noexcept { + return nullptr < rhs.ptr_; + } + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator>( + std::nullptr_t, + const raw_ptr& rhs) noexcept { + return nullptr > rhs.ptr_; + } + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator<=( + std::nullptr_t, + const raw_ptr& rhs) noexcept { + return nullptr <= rhs.ptr_; + } + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator>=( + std::nullptr_t, + const raw_ptr& rhs) noexcept { + return nullptr >= rhs.ptr_; + } + + // Stream output helper. + template + OPENSCREEN_ALWAYS_INLINE friend std::basic_ostream& operator<<( + std::basic_ostream& os, + const raw_ptr& ptr) { + return os << ptr.ptr_; + } + + private: + template + friend class raw_ptr; + +#if defined(__clang__) + [[clang::annotate("raw_ptr_exclusion")]] +#endif + T* ptr_ = nullptr; +}; + +} // namespace openscreen + +namespace std { + +// Override so map/set lookups work correctly. +template +struct less > { + using is_transparent = void; + + bool operator()(const openscreen::raw_ptr& lhs, + const openscreen::raw_ptr& rhs) const { + return lhs < rhs; + } + bool operator()(T* lhs, const openscreen::raw_ptr& rhs) const { + return lhs < rhs.get(); + } + bool operator()(const openscreen::raw_ptr& lhs, T* rhs) const { + return lhs.get() < rhs; + } +}; + +// Override so unordered_map/unordered_set lookups work correctly. +template +struct hash > { + using argument_type = openscreen::raw_ptr; + using result_type = std::size_t; + result_type operator()(argument_type const& ptr) const { + return hash()(ptr.get()); + } +}; + +// Required for algorithms like std::to_address to unpack the pointer. +template +struct pointer_traits > { + using pointer = openscreen::raw_ptr; + using element_type = T; + using difference_type = ptrdiff_t; + + template + using rebind = openscreen::raw_ptr; + + static constexpr pointer pointer_to(element_type& r) noexcept { + return pointer(&r); + } + static constexpr element_type* to_address(pointer p) noexcept { + return p.get(); + } +}; + +} // namespace std + +#endif // !defined(BUILD_WITH_CHROMIUM) + +#endif // UTIL_RAW_PTR_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/raw_ref.h b/breadcast-caststream-sys/vendor/openscreen/util/raw_ref.h new file mode 100644 index 0000000..c96229f --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/raw_ref.h @@ -0,0 +1,122 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_RAW_REF_H_ +#define UTIL_RAW_REF_H_ + +#if defined(BUILD_WITH_CHROMIUM) + +#include "partition_alloc/pointers/raw_ref.h" // nogncheck + +namespace openscreen { + +template +using raw_ref = ::base::raw_ref; + +} // namespace openscreen + +#else // !defined(BUILD_WITH_CHROMIUM) + +#include +#include + +#include "util/raw_ptr.h" + +namespace openscreen { + +template +class raw_ref; + +namespace internal { +template +struct is_raw_ref : std::false_type {}; + +template +struct is_raw_ref > : std::true_type {}; +} // namespace internal + +template +class OPENSCREEN_TRIVIAL_ABI raw_ref { + public: + OPENSCREEN_ALWAYS_INLINE constexpr explicit raw_ref(T& ref) noexcept + : ptr_(&ref) {} + + template >::value && + std::is_convertible_v > > + OPENSCREEN_ALWAYS_INLINE constexpr explicit raw_ref(U& ref) noexcept + : ptr_(&ref) {} + + OPENSCREEN_ALWAYS_INLINE constexpr raw_ref(const raw_ref& other) noexcept = + default; + OPENSCREEN_ALWAYS_INLINE constexpr raw_ref(raw_ref&& other) noexcept = + default; + + template > > + OPENSCREEN_ALWAYS_INLINE constexpr raw_ref(const raw_ref& other) noexcept + : ptr_(other.ptr_) {} + + template > > + OPENSCREEN_ALWAYS_INLINE constexpr raw_ref(raw_ref&& other) noexcept + : ptr_(std::move(other.ptr_)) {} + + ~raw_ref() = default; + + OPENSCREEN_ALWAYS_INLINE constexpr raw_ref& operator=( + const raw_ref& other) noexcept = default; + OPENSCREEN_ALWAYS_INLINE constexpr raw_ref& operator=( + raw_ref&& other) noexcept = default; + + template > > + OPENSCREEN_ALWAYS_INLINE constexpr raw_ref& operator=( + const raw_ref& other) noexcept { + ptr_ = other.ptr_; + return *this; + } + + template > > + OPENSCREEN_ALWAYS_INLINE constexpr raw_ref& operator=( + raw_ref&& other) noexcept { + ptr_ = std::move(other.ptr_); + return *this; + } + + OPENSCREEN_ALWAYS_INLINE constexpr T& get() const noexcept { return *ptr_; } + OPENSCREEN_ALWAYS_INLINE constexpr T* operator->() const noexcept { + return ptr_.get(); + } + OPENSCREEN_ALWAYS_INLINE constexpr T& operator*() const noexcept { + return *ptr_; + } + + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator==( + const raw_ref& lhs, + const raw_ref& rhs) noexcept { + return lhs.ptr_ == rhs.ptr_; + } + template + OPENSCREEN_ALWAYS_INLINE friend constexpr bool operator!=( + const raw_ref& lhs, + const raw_ref& rhs) noexcept { + return lhs.ptr_ != rhs.ptr_; + } + + private: + template + friend class raw_ref; + + raw_ptr ptr_; +}; + +} // namespace openscreen + +#endif // !defined(BUILD_WITH_CHROMIUM) + +#endif // UTIL_RAW_REF_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/read_file.cc b/breadcast-caststream-sys/vendor/openscreen/util/read_file.cc new file mode 100644 index 0000000..f96d682 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/read_file.cc @@ -0,0 +1,34 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/read_file.h" + +#include + +namespace openscreen { + +std::string ReadEntireFileToString(std::string_view filename) { + FILE* file = fopen(filename.data(), "r"); + if (file == nullptr) { + return {}; + } + fseek(file, 0, SEEK_END); + long file_size = ftell(file); // NOLINT + fseek(file, 0, SEEK_SET); + std::string contents(file_size, 0); + int bytes_read = 0; + while (bytes_read < file_size) { + size_t ret = fread(&contents[bytes_read], 1, file_size - bytes_read, file); + if (ret == 0 && ferror(file)) { + return {}; + } else { + bytes_read += ret; + } + } + fclose(file); + + return contents; +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/util/read_file.h b/breadcast-caststream-sys/vendor/openscreen/util/read_file.h new file mode 100644 index 0000000..efeb477 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/read_file.h @@ -0,0 +1,17 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_READ_FILE_H_ +#define UTIL_READ_FILE_H_ + +#include +#include + +namespace openscreen { + +std::string ReadEntireFileToString(std::string_view filename); + +} // namespace openscreen + +#endif // UTIL_READ_FILE_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/saturate_cast.h b/breadcast-caststream-sys/vendor/openscreen/util/saturate_cast.h new file mode 100644 index 0000000..870b77c --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/saturate_cast.h @@ -0,0 +1,145 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_SATURATE_CAST_H_ +#define UTIL_SATURATE_CAST_H_ + +#include +#include +#include + +namespace openscreen { + +// Case 0: When To and From are the same type, saturate_cast<> is pass-through. +template +constexpr std::enable_if_t< + std::is_same, std::remove_cv>::value, + To> +saturate_cast(From from) { + return from; +} + +// Because of the way C++ signed versus unsigned comparison works (i.e., the +// type promotion strategy employed), extra care must be taken to range-check +// the input value. For example, if the current architecture is 32-bits, then +// any int32_t compared with a uint32_t will NOT promote to a int64_t↔int64_t +// comparison. Instead, it will become a uint32_t↔uint32_t comparison (!), +// which will sometimes produce invalid results. + +// Case 1: "From" and "To" are either both signed, or are both unsigned. In +// this case, the smaller of the two types will be promoted to match the +// larger's size, and a valid comparison will be made. +template +constexpr std::enable_if_t< + std::is_integral::value && std::is_integral::value && + (std::is_signed::value == std::is_signed::value), + To> +saturate_cast(From from) { + if (from <= std::numeric_limits::min()) { + return std::numeric_limits::min(); + } + if (from >= std::numeric_limits::max()) { + return std::numeric_limits::max(); + } + return static_cast(from); +} + +// Case 2: "From" is signed, but "To" is unsigned. +template +constexpr std::enable_if_t< + std::is_integral::value && std::is_integral::value && + std::is_signed::value && !std::is_signed::value, + To> +saturate_cast(From from) { + if (from <= From{0}) { + return To{0}; + } + if (static_cast>(from) >= + std::numeric_limits::max()) { + return std::numeric_limits::max(); + } + return static_cast(from); +} + +// Case 3: "From" is unsigned, but "To" is signed. +template +constexpr std::enable_if_t< + std::is_integral::value && std::is_integral::value && + !std::is_signed::value && std::is_signed::value, + To> +saturate_cast(From from) { + if (from >= static_cast>( + std::numeric_limits::max())) { + return std::numeric_limits::max(); + } + return static_cast(from); +} + +// Case 4: "From" is a floating-point type, and "To" is an integer type (signed +// or unsigned). The result is truncated, per the usual C++ float-to-int +// conversion rules. +template +constexpr std::enable_if_t::value && + std::is_integral::value, + To> +saturate_cast(From from) { + // Note: It's invalid to compare the argument against + // std::numeric_limits::max() because the latter, an integer value, will + // be type-promoted to the floating-point type. The problem is that the + // conversion is imprecise, as "max int" might not be exactly representable as + // a floating-point value (depending on the actual types of From and To). + // + // Thus, the strategy is to compare only floating-point values/constants to + // determine whether the bounds of the range of integers has been exceeded. + // Two assumptions here: 1) "To" is either unsigned, or is a 2's complement + // signed integer type. 2) "From" is a floating-point type that can exactly + // represent all powers of 2 within its value range. + static_assert((~To(1) + To(1)) == To(-1), "assumed 2's complement integers"); + constexpr From kMaxIntPlusOne = + From(To(1) << (std::numeric_limits::digits - 1)) * From(2); + constexpr From kMaxInt = kMaxIntPlusOne - 1; + // Note: In some cases, the kMaxInt constant will equal kMaxIntPlusOne because + // there isn't an exact floating-point representation for 2^N - 1. That said, + // the following upper-bound comparison is still valid because all + // floating-point values less than 2^N would also be less than 2^N - 1. + if (from >= kMaxInt) { + return std::numeric_limits::max(); + } + if (std::is_signed::value) { + constexpr From kMinInt = -kMaxIntPlusOne; + if (from <= kMinInt) { + return std::numeric_limits::min(); + } + } else /* if To is unsigned */ { + if (from <= From(0)) { + return To(0); + } + } + return static_cast(from); +} + +// Like saturate_cast<>, but rounds to the nearest integer instead of +// truncating. +template +constexpr std::enable_if_t::value && + std::is_integral::value, + To> +rounded_saturate_cast(From from) { + const To saturated = saturate_cast(from); + if (saturated == std::numeric_limits::min() || + saturated == std::numeric_limits::max()) { + return saturated; + } + + static_assert(sizeof(To) <= sizeof(decltype(llround(from))), + "No version of lround() for the required range of values."); + if (sizeof(To) <= sizeof(decltype(lround(from)))) { + return static_cast(lround(from)); + } + return static_cast(llround(from)); +} + +} // namespace openscreen + +#endif // UTIL_SATURATE_CAST_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/scoped_wake_lock.cc b/breadcast-caststream-sys/vendor/openscreen/util/scoped_wake_lock.cc new file mode 100644 index 0000000..2ac913a --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/scoped_wake_lock.cc @@ -0,0 +1,12 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/scoped_wake_lock.h" + +namespace openscreen { + +ScopedWakeLock::ScopedWakeLock() = default; +ScopedWakeLock::~ScopedWakeLock() = default; + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/util/scoped_wake_lock.h b/breadcast-caststream-sys/vendor/openscreen/util/scoped_wake_lock.h new file mode 100644 index 0000000..213999b --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/scoped_wake_lock.h @@ -0,0 +1,46 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_SCOPED_WAKE_LOCK_H_ +#define UTIL_SCOPED_WAKE_LOCK_H_ + +#include + +#include "platform/api/task_runner.h" +#include "platform/api/task_runner_deleter.h" + +namespace openscreen { + +// Ensures that the device does not got to sleep. This is used, for example, +// while Open Screen is communicating with peers over the network for things +// like media streaming. +// +// The wake lock is RAII: It is automatically engaged when the ScopedWakeLock is +// created and released when the ScopedWakeLock is destroyed. Open Screen code +// may sometimes create multiple instances. In that case, the wake lock should +// be engaged upon creating the first instance, and then held until all +// instances have been destroyed. +// +// TODO(issuetracker.google.com/288311411): Implement for Linux. + +class ScopedWakeLock; +using ScopedWakeLockPtr = std::unique_ptr; + +class ScopedWakeLock { + public: + static ScopedWakeLockPtr Create(TaskRunner& task_runner); + + // Instances are not copied nor moved. + ScopedWakeLock(const ScopedWakeLock&) = delete; + ScopedWakeLock(ScopedWakeLock&&) noexcept = delete; + ScopedWakeLock& operator=(const ScopedWakeLock&) = delete; + ScopedWakeLock& operator=(ScopedWakeLock&&) noexcept = delete; + + ScopedWakeLock(); + virtual ~ScopedWakeLock(); +}; + +} // namespace openscreen + +#endif // UTIL_SCOPED_WAKE_LOCK_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/simple_fraction.cc b/breadcast-caststream-sys/vendor/openscreen/util/simple_fraction.cc new file mode 100644 index 0000000..23a7bb3 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/simple_fraction.cc @@ -0,0 +1,52 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/simple_fraction.h" + +#include +#include +#include +#include + +#include "util/osp_logging.h" +#include "util/string_parse.h" +#include "util/string_util.h" +#include "util/stringprintf.h" + +namespace openscreen { + +// static +ErrorOr SimpleFraction::FromString(std::string_view value) { + if (value.size() > 0 && value.at(0) == '/') { + return Error::Code::kParameterInvalid; + } + + std::vector fields = string_util::Split(value, '/'); + if (fields.size() != 1 && fields.size() != 2) { + return Error::Code::kParameterInvalid; + } + + int numerator; + int denominator = 1; + if (!string_parse::ParseAsciiNumber(fields[0], numerator)) { + return Error::Code::kParameterInvalid; + } + + if (fields.size() == 2) { + if (!string_parse::ParseAsciiNumber(fields[1], denominator)) { + return Error::Code::kParameterInvalid; + } + } + + return SimpleFraction(numerator, denominator); +} + +std::string SimpleFraction::ToString() const { + if (denominator_ == 1) { + return std::to_string(numerator_); + } + return StringFormat("{}/{}", numerator_, denominator_); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/util/simple_fraction.h b/breadcast-caststream-sys/vendor/openscreen/util/simple_fraction.h new file mode 100644 index 0000000..909362a --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/simple_fraction.h @@ -0,0 +1,73 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_SIMPLE_FRACTION_H_ +#define UTIL_SIMPLE_FRACTION_H_ + +#include +#include +#include +#include + +#include "platform/base/error.h" + +namespace openscreen { + +// SimpleFraction is used to represent simple (or "common") fractions, composed +// of a rational number written a/b where a and b are both integers. +// Some helpful notes on SimpleFraction assumptions/limitations: +// 1. SimpleFraction does not perform reductions. 2/4 != 1/2, and -1/-1 != 1/1. +// 2. denominator = 0 is considered undefined. +// 3. numerator = saturates range to int min or int max +// 4. A SimpleFraction is "positive" if and only if it is defined and at least +// equal to zero. Since reductions are not performed, -1/-1 is negative. +class SimpleFraction { + public: + static ErrorOr FromString(std::string_view value); + std::string ToString() const; + + constexpr SimpleFraction() = default; + constexpr SimpleFraction(int numerator) // NOLINT + : numerator_(numerator) {} + constexpr SimpleFraction(int numerator, int denominator) + : numerator_(numerator), denominator_(denominator) {} + + constexpr SimpleFraction(const SimpleFraction&) = default; + constexpr SimpleFraction(SimpleFraction&&) noexcept = default; + constexpr SimpleFraction& operator=(const SimpleFraction&) = default; + constexpr SimpleFraction& operator=(SimpleFraction&&) = default; + ~SimpleFraction() = default; + + constexpr bool operator==(const SimpleFraction& other) const { + return numerator_ == other.numerator_ && denominator_ == other.denominator_; + } + + constexpr bool operator!=(const SimpleFraction& other) const { + return !(*this == other); + } + + constexpr bool is_defined() const { return denominator_ != 0; } + + constexpr bool is_positive() const { + return (numerator_ >= 0) && (denominator_ > 0); + } + + constexpr explicit operator double() const { + if (denominator_ == 0) { + return nan(""); + } + return static_cast(numerator_) / static_cast(denominator_); + } + + constexpr int numerator() const { return numerator_; } + constexpr int denominator() const { return denominator_; } + + private: + int numerator_ = 0; + int denominator_ = 1; +}; + +} // namespace openscreen + +#endif // UTIL_SIMPLE_FRACTION_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/std_util.cc b/breadcast-caststream-sys/vendor/openscreen/util/std_util.cc new file mode 100644 index 0000000..7f6b9c6 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/std_util.cc @@ -0,0 +1,20 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/std_util.h" + +#include +#include +#include + +#include "util/osp_logging.h" + +namespace openscreen { + +std::string& RemoveWhitespace(std::string& s) { + s.erase(std::remove_if(s.begin(), s.end(), ::isspace), s.end()); + return s; +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/util/std_util.h b/breadcast-caststream-sys/vendor/openscreen/util/std_util.h new file mode 100644 index 0000000..ba07817 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/std_util.h @@ -0,0 +1,104 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_STD_UTIL_H_ +#define UTIL_STD_UTIL_H_ + +#include + +#include +#include +#include +#include +#include +#include + +#include "util/stringprintf.h" + +namespace openscreen { + +template +constexpr size_t countof(T (&array)[N]) { + return N; +} + +// Removes ALL whitespace in place from the string, based on the present C +// locale. This includes spaces, tabs, and returns. This is useful for string +// comparisons where whitespace doesn't matter, or, in the case of JSON +// serialization, is dependent on build configuration and other settings. +std::string& RemoveWhitespace(std::string& s); + +template +void RemoveValueFromMap(std::map* map, Value* value) { + for (auto it = map->begin(); it != map->end();) { + if (it->second == value) { + it = map->erase(it); + } else { + ++it; + } + } +} + +template +bool AreElementsSortedAndUnique(const ForwardIteratingContainer& c) { + return std::is_sorted(c.begin(), c.end()) && + std::adjacent_find(c.begin(), c.end()) == c.end(); +} + +template +void SortAndDedupeElements(RandomAccessContainer* c) { + std::sort(c->begin(), c->end()); + const auto new_end = std::unique(c->begin(), c->end()); + c->erase(new_end, c->end()); +} + +// Append the provided elements together into a single vector. This can be +// useful when creating a vector of variadic templates in the ctor. +// +// This is the base case for the recursion +template +std::vector&& Append(std::vector&& so_far) { + return std::move(so_far); +} + +// This is the recursive call. Depending on the number of remaining elements, it +// either calls into itself or into the above base case. +template +std::vector&& Append(std::vector&& so_far, + TFirst&& new_element, + TOthers&&... new_elements) { + so_far.push_back(std::move(new_element)); + return Append(std::move(so_far), std::move(new_elements)...); +} + +// Creates an empty vector with `size` elements reserved. Intended to be used as +// GetEmptyVectorOfSize(sizeof...(variadic_input)) +template +std::vector GetVectorWithCapacity(size_t size) { + std::vector results; + results.reserve(size); + return results; +} + +// Returns true if an element equal to `element` is found in `container`. +// C.begin() must return an iterator to the beginning of C and C.end() must +// return an iterator to the end. +template +bool Contains(const C& container, const E& element) { + return std::find(container.begin(), container.end(), element) != + container.end(); +} + +// Returns true if any element in `container` returns true for `predicate`. +// C.begin() must return an iterator to the beginning of C and C.end() must +// return an iterator to the end. +template +bool ContainsIf(const C& container, P predicate) { + return std::find_if(container.begin(), container.end(), + std::move(predicate)) != container.end(); +} + +} // namespace openscreen + +#endif // UTIL_STD_UTIL_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/string_parse.h b/breadcast-caststream-sys/vendor/openscreen/util/string_parse.h new file mode 100644 index 0000000..1a9d34c --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/string_parse.h @@ -0,0 +1,32 @@ +// Copyright 2024 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_STRING_PARSE_H_ +#define UTIL_STRING_PARSE_H_ + +#include +#include +#include +#include + +#include "platform/base/type_util.h" + +namespace openscreen::string_parse { + +// Parses `number` into the numeric type `result` and returns true if +// successful. `number` must be an ASCII representation of an integer or +// floating point value, and `result` must be compatible with the resulting +// value. If `number` cannot be parsed, then returns false. +template > +bool ParseAsciiNumber(std::string_view number, T& result) { + if (number.empty()) + return false; + auto [unused_ptr, error_code] = + std::from_chars(number.data(), &number.back() + 1, result); + return error_code == std::errc(); +} + +} // namespace openscreen::string_parse + +#endif // UTIL_STRING_PARSE_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/string_util.cc b/breadcast-caststream-sys/vendor/openscreen/util/string_util.cc new file mode 100644 index 0000000..4ffade9 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/string_util.cc @@ -0,0 +1,160 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/string_util.h" + +#include +#include + +namespace openscreen::string_util { +namespace internal { +// clang-format off +// Array of bitfields holding character information. Note that bitfields for all +// characters above ASCII 127 are zero-initialized. +// Position Meaning +// -------- ------- +// 1 alphabetic +// 2 alphanumeric +// 3 whitespace +// 4 punctuation +// 5 tab or space +// 6 control character +// 7 hex digit +const unsigned char kPropertyBits[256] = { + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, // 0x00 + 0x40, 0x68, 0x48, 0x48, 0x48, 0x48, 0x40, 0x40, + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, // 0x10 + 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, 0x40, + 0x28, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, // 0x20 + 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, 0x84, // 0x30 + 0x84, 0x84, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x05, // 0x40 + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, // 0x50 + 0x05, 0x05, 0x05, 0x10, 0x10, 0x10, 0x10, 0x10, + 0x10, 0x85, 0x85, 0x85, 0x85, 0x85, 0x85, 0x05, // 0x60 + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, + 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, 0x05, // 0x70 + 0x05, 0x05, 0x05, 0x10, 0x10, 0x10, 0x10, 0x40, +}; + +// Array of characters for the ascii_tolower() function. +const char kToLower[256] = { + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', + '\x08', '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', + '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', + '\x18', '\x19', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f', + '\x20', '\x21', '\x22', '\x23', '\x24', '\x25', '\x26', '\x27', + '\x28', '\x29', '\x2a', '\x2b', '\x2c', '\x2d', '\x2e', '\x2f', + '\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', + '\x38', '\x39', '\x3a', '\x3b', '\x3c', '\x3d', '\x3e', '\x3f', + '\x40', 'a', 'b', 'c', 'd', 'e', 'f', 'g', + 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', + 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', + 'x', 'y', 'z', '\x5b', '\x5c', '\x5d', '\x5e', '\x5f', + '\x60', '\x61', '\x62', '\x63', '\x64', '\x65', '\x66', '\x67', + '\x68', '\x69', '\x6a', '\x6b', '\x6c', '\x6d', '\x6e', '\x6f', + '\x70', '\x71', '\x72', '\x73', '\x74', '\x75', '\x76', '\x77', + '\x78', '\x79', '\x7a', '\x7b', '\x7c', '\x7d', '\x7e', '\x7f', + '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87', + '\x88', '\x89', '\x8a', '\x8b', '\x8c', '\x8d', '\x8e', '\x8f', + '\x90', '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', + '\x98', '\x99', '\x9a', '\x9b', '\x9c', '\x9d', '\x9e', '\x9f', + '\xa0', '\xa1', '\xa2', '\xa3', '\xa4', '\xa5', '\xa6', '\xa7', + '\xa8', '\xa9', '\xaa', '\xab', '\xac', '\xad', '\xae', '\xaf', + '\xb0', '\xb1', '\xb2', '\xb3', '\xb4', '\xb5', '\xb6', '\xb7', + '\xb8', '\xb9', '\xba', '\xbb', '\xbc', '\xbd', '\xbe', '\xbf', + '\xc0', '\xc1', '\xc2', '\xc3', '\xc4', '\xc5', '\xc6', '\xc7', + '\xc8', '\xc9', '\xca', '\xcb', '\xcc', '\xcd', '\xce', '\xcf', + '\xd0', '\xd1', '\xd2', '\xd3', '\xd4', '\xd5', '\xd6', '\xd7', + '\xd8', '\xd9', '\xda', '\xdb', '\xdc', '\xdd', '\xde', '\xdf', + '\xe0', '\xe1', '\xe2', '\xe3', '\xe4', '\xe5', '\xe6', '\xe7', + '\xe8', '\xe9', '\xea', '\xeb', '\xec', '\xed', '\xee', '\xef', + '\xf0', '\xf1', '\xf2', '\xf3', '\xf4', '\xf5', '\xf6', '\xf7', + '\xf8', '\xf9', '\xfa', '\xfb', '\xfc', '\xfd', '\xfe', '\xff', +}; + +// Array of characters for the ascii_toupper() function. +const char kToUpper[256] = { + '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', + '\x08', '\x09', '\x0a', '\x0b', '\x0c', '\x0d', '\x0e', '\x0f', + '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', + '\x18', '\x19', '\x1a', '\x1b', '\x1c', '\x1d', '\x1e', '\x1f', + '\x20', '\x21', '\x22', '\x23', '\x24', '\x25', '\x26', '\x27', + '\x28', '\x29', '\x2a', '\x2b', '\x2c', '\x2d', '\x2e', '\x2f', + '\x30', '\x31', '\x32', '\x33', '\x34', '\x35', '\x36', '\x37', + '\x38', '\x39', '\x3a', '\x3b', '\x3c', '\x3d', '\x3e', '\x3f', + '\x40', '\x41', '\x42', '\x43', '\x44', '\x45', '\x46', '\x47', + '\x48', '\x49', '\x4a', '\x4b', '\x4c', '\x4d', '\x4e', '\x4f', + '\x50', '\x51', '\x52', '\x53', '\x54', '\x55', '\x56', '\x57', + '\x58', '\x59', '\x5a', '\x5b', '\x5c', '\x5d', '\x5e', '\x5f', + '\x60', 'A', 'B', 'C', 'D', 'E', 'F', 'G', + 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', + 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', + 'X', 'Y', 'Z', '\x7b', '\x7c', '\x7d', '\x7e', '\x7f', + '\x80', '\x81', '\x82', '\x83', '\x84', '\x85', '\x86', '\x87', + '\x88', '\x89', '\x8a', '\x8b', '\x8c', '\x8d', '\x8e', '\x8f', + '\x90', '\x91', '\x92', '\x93', '\x94', '\x95', '\x96', '\x97', + '\x98', '\x99', '\x9a', '\x9b', '\x9c', '\x9d', '\x9e', '\x9f', + '\xa0', '\xa1', '\xa2', '\xa3', '\xa4', '\xa5', '\xa6', '\xa7', + '\xa8', '\xa9', '\xaa', '\xab', '\xac', '\xad', '\xae', '\xaf', + '\xb0', '\xb1', '\xb2', '\xb3', '\xb4', '\xb5', '\xb6', '\xb7', + '\xb8', '\xb9', '\xba', '\xbb', '\xbc', '\xbd', '\xbe', '\xbf', + '\xc0', '\xc1', '\xc2', '\xc3', '\xc4', '\xc5', '\xc6', '\xc7', + '\xc8', '\xc9', '\xca', '\xcb', '\xcc', '\xcd', '\xce', '\xcf', + '\xd0', '\xd1', '\xd2', '\xd3', '\xd4', '\xd5', '\xd6', '\xd7', + '\xd8', '\xd9', '\xda', '\xdb', '\xdc', '\xdd', '\xde', '\xdf', + '\xe0', '\xe1', '\xe2', '\xe3', '\xe4', '\xe5', '\xe6', '\xe7', + '\xe8', '\xe9', '\xea', '\xeb', '\xec', '\xed', '\xee', '\xef', + '\xf0', '\xf1', '\xf2', '\xf3', '\xf4', '\xf5', '\xf6', '\xf7', + '\xf8', '\xf9', '\xfa', '\xfb', '\xfc', '\xfd', '\xfe', '\xff', +}; +// clang-format on +} // namespace internal + +void AsciiStrToLower(std::string& s) { + for (auto& c : s) + c = ascii_tolower(c); +} + +std::string AsciiStrToLower(std::string_view s) { + std::string result(s); + AsciiStrToLower(result); + return result; +} + +void AsciiStrToUpper(std::string& s) { + for (auto& c : s) + c = ascii_toupper(c); +} + +std::string AsciiStrToUpper(std::string_view s) { + std::string result(s); + AsciiStrToUpper(result); + return result; +} + +[[nodiscard]] bool EqualsIgnoreCase(std::string_view a, std::string_view b) { + // std::ranges::equal checks size() automatically for random-access ranges + // like std::string_view. + return std::ranges::equal( + a, b, std::equal_to<>{}, // 1. The Predicate: Compare for equality + ascii_tolower, // 2. Projection for piece1 + ascii_tolower // 3. Projection for piece2 + ); +} + +[[nodiscard]] std::vector Split(std::string_view value, + char delim) { + auto tokens = value | std::views::split(delim) | + std::views::filter([](auto&& r) { return !r.empty(); }); + std::vector result; + for (auto&& token : tokens) { + result.emplace_back(token.begin(), token.end()); + } + return result; +} + +} // namespace openscreen::string_util diff --git a/breadcast-caststream-sys/vendor/openscreen/util/string_util.h b/breadcast-caststream-sys/vendor/openscreen/util/string_util.h new file mode 100644 index 0000000..05846d4 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/string_util.h @@ -0,0 +1,142 @@ +// Copyright 2023 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_STRING_UTIL_H_ +#define UTIL_STRING_UTIL_H_ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// String query and manipulation utilities. +// TODO(jophba): remove nested string_util namespace. +namespace openscreen::string_util { + +namespace internal { + +extern const unsigned char kPropertyBits[256]; +extern const char kToLower[256]; +extern const char kToUpper[256]; + +} // namespace internal + +// Determines whether `c` is a valid ASCII alphabetic character code. +inline bool ascii_isalpha(unsigned char c) { + return (internal::kPropertyBits[c] & 0x01) != 0; +} + +// Determines whether `c` is a valid ASCII decimal digit (i.e. [0-9]). +inline bool ascii_isdigit(unsigned char c) { + return '0' <= c && c <= '9'; +} + +// Determines whether `c` is a valid ASCII lower case hexadecimal digit +// (i.e. [a-fA-F0-9]). +inline bool ascii_islowerhex(unsigned char c) { + return ascii_isdigit(c) || ('a' <= c && c <= 'f'); +} + +// Determines whether `c` is a valid ASCII hexadecimal digit (i.e. [a-fA-F0-9]). +inline bool ascii_ishex(unsigned char c) { + return ascii_islowerhex(c) || ('A' <= c && c <= 'F'); +} + +// Determines whether `c` is a valid, printable ASCII digit. +inline bool ascii_isprint(unsigned char c) { + return c >= 32 && c < 127; +} + +// Determines whether `c` is a whitespace character +// (space, tab, vertical tab, formfeed, linefeed, or carriage return). +inline bool ascii_isspace(unsigned char c) { + return (internal::kPropertyBits[c] & 0x08) != 0; +} + +// If `c` is an upper case ASCII character, returns its lower case equivalent. +// Otherwise, returns `c` unchanged. +inline char ascii_tolower(unsigned char c) { + return internal::kToLower[c]; +} + +// Converts `s` to lowercase. +void AsciiStrToLower(std::string& s); + +// Creates a lowercase string from a given string_view. +std::string AsciiStrToLower(std::string_view s); + +inline char ascii_toupper(unsigned char c) { + return internal::kToUpper[c]; +} + +// Converts `s` to uppercase. +void AsciiStrToUpper(std::string& s); + +// Creates a uppercase string from a given string_view. +std::string AsciiStrToUpper(std::string_view s); + +// Returns whether given ASCII strings `a` and `b` are equal, ignoring +// case in the comparison. +[[nodiscard]] bool EqualsIgnoreCase(std::string_view a, std::string_view b); + +// Returns std::string_view with whitespace stripped from the beginning of the +// given string_view. +inline std::string_view StripLeadingAsciiWhitespace(std::string_view str) { + auto it = std::find_if_not(str.cbegin(), str.cend(), ascii_isspace); + return str.substr(static_cast(it - str.begin())); +} + +// Concatenates arguments into a single string. +[[nodiscard]] constexpr std::string StrCat( + std::initializer_list pieces) { + // Prefer a loop over std::accumulate since it is not constexpr in C++20. + size_t length = 0; + for (const auto& piece : pieces) { + length += piece.size(); + } + + std::string result; + result.reserve(length); + for (const auto& piece : pieces) { + result.append(piece); + } + return result; +} + +// Splits `value` into tokens separated by `delim`. Leading and trailing +// delimeters are stripped, and multiple consecutive delimeters are treated as +// one. +[[nodiscard]] std::vector Split(std::string_view value, + char delim); + +template +[[nodiscard]] std::string Join(R&& range, std::string_view delimeter = ", ") { + if (std::ranges::empty(range)) { + return {}; + } + std::stringstream ss; + ss << range.front(); + for (auto element : range | std::views::drop(1)) { + ss << delimeter << element; + } + return ss.str(); +} + +// Returns a string made by concatenating the strings iterated by `[begin, +// end)`, each separated by `delim`. +template +[[nodiscard]] std::string Join(Iterator begin, + Iterator end, + std::string_view delimeter = ", ") { + return Join(std::ranges::subrange{begin, end}, delimeter); +} + +} // namespace openscreen::string_util + +#endif // UTIL_STRING_UTIL_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/stringprintf.cc b/breadcast-caststream-sys/vendor/openscreen/util/stringprintf.cc new file mode 100644 index 0000000..bdb9e45 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/stringprintf.cc @@ -0,0 +1,29 @@ +// Copyright 2020 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/stringprintf.h" + +#include +#include +#include +#include + +#include "util/osp_logging.h" + +namespace openscreen { + +std::string HexEncode(const uint8_t* bytes, size_t len) { + return HexEncode(ByteView(bytes, len)); +} + +std::string HexEncode(ByteView bytes) { + std::ostringstream hex_dump; + hex_dump << std::setfill('0') << std::hex; + for (uint8_t byte : bytes) { + hex_dump << std::setw(2) << static_cast(byte); + } + return hex_dump.str(); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/util/stringprintf.h b/breadcast-caststream-sys/vendor/openscreen/util/stringprintf.h new file mode 100644 index 0000000..4e10f96 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/stringprintf.h @@ -0,0 +1,33 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_STRINGPRINTF_H_ +#define UTIL_STRINGPRINTF_H_ + +#include + +#include +#include +#include +#include + +#include "platform/base/span.h" + +namespace openscreen { + +// TODO(crbug.com/364687926): remove and replace with direct calls to +// std::format now that we are on C++20. +template +[[nodiscard]] std::string StringFormat(std::format_string fmt, + Args&&... args) { + return std::format(fmt, std::forward(args)...); +} + +// Returns a hex string representation of the given `bytes`. +std::string HexEncode(const uint8_t* bytes, size_t len); +std::string HexEncode(ByteView bytes); + +} // namespace openscreen + +#endif // UTIL_STRINGPRINTF_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/thread_annotations.h b/breadcast-caststream-sys/vendor/openscreen/util/thread_annotations.h new file mode 100644 index 0000000..98a8cfa --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/thread_annotations.h @@ -0,0 +1,50 @@ +// Copyright 2026 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_THREAD_ANNOTATIONS_H_ +#define UTIL_THREAD_ANNOTATIONS_H_ + +#if defined(__clang__) && !defined(SWIG) +#define OSP_THREAD_ANNOTATION_ATTRIBUTE__(x) __attribute__((x)) +#else +#define OSP_THREAD_ANNOTATION_ATTRIBUTE__(x) +#endif + +#define OSP_GUARDED_BY(x) \ + OSP_THREAD_ANNOTATION_ATTRIBUTE__(guarded_by(x)) + +#define OSP_PT_GUARDED_BY(x) \ + OSP_THREAD_ANNOTATION_ATTRIBUTE__(pt_guarded_by(x)) + +#define OSP_EXCLUSIVE_LOCKS_REQUIRED(...) \ + OSP_THREAD_ANNOTATION_ATTRIBUTE__(exclusive_locks_required(__VA_ARGS__)) + +#define OSP_SHARED_LOCKS_REQUIRED(...) \ + OSP_THREAD_ANNOTATION_ATTRIBUTE__(shared_locks_required(__VA_ARGS__)) + +#define OSP_EXCLUSIVE_LOCK_FUNCTION(...) \ + OSP_THREAD_ANNOTATION_ATTRIBUTE__(exclusive_lock_function(__VA_ARGS__)) + +#define OSP_SHARED_LOCK_FUNCTION(...) \ + OSP_THREAD_ANNOTATION_ATTRIBUTE__(shared_lock_function(__VA_ARGS__)) + +#define OSP_UNLOCK_FUNCTION(...) \ + OSP_THREAD_ANNOTATION_ATTRIBUTE__(unlock_function(__VA_ARGS__)) + +#define OSP_LOCKS_EXCLUDED(...) \ + OSP_THREAD_ANNOTATION_ATTRIBUTE__(locks_excluded(__VA_ARGS__)) + +#define OSP_LOCK_RETURNED(x) \ + OSP_THREAD_ANNOTATION_ATTRIBUTE__(lock_returned(x)) + +#define OSP_LOCKABLE \ + OSP_THREAD_ANNOTATION_ATTRIBUTE__(lockable) + +#define OSP_SCOPED_LOCKABLE \ + OSP_THREAD_ANNOTATION_ATTRIBUTE__(scoped_lockable) + +#define OSP_NO_THREAD_SAFETY_ANALYSIS \ + OSP_THREAD_ANNOTATION_ATTRIBUTE__(no_thread_safety_analysis) + +#endif // UTIL_THREAD_ANNOTATIONS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/trace_logging.h b/breadcast-caststream-sys/vendor/openscreen/util/trace_logging.h new file mode 100644 index 0000000..98bbb82 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/trace_logging.h @@ -0,0 +1,282 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_TRACE_LOGGING_H_ +#define UTIL_TRACE_LOGGING_H_ + +#include +#include +#include +#include +#include + +#include "platform/base/trace_logging_types.h" + +// All compile-time macros for tracing. +// NOTE: The ternary operator is used here to ensure that the TraceLogger object +// is only constructed if tracing is enabled, but at the same time is created in +// the caller's scope. The C++ standards guide guarantees that the constructor +// should only be called when IsTraceLoggingEnabled(...) evaluates to true. +// static_cast calls are used because if the type of the result of the ternary +// operator does not match the expected type, temporary storage is used for the +// created object, which results in an extra call to the constructor and +// destructor of the tracing objects. +// +// Further details about how these macros are used can be found in +// docs/trace_logging.md. + +#if defined(ENABLE_TRACE_LOGGING) + +#define INCLUDING_FROM_UTIL_TRACE_LOGGING_H_ +#include "util/trace_logging/macro_support.h" +#undef INCLUDING_FROM_UTIL_TRACE_LOGGING_H_ + +#define TRACE_SET_RESULT(result) \ + do { \ + if (TRACE_IS_ENABLED(openscreen::TraceCategory::kAny)) { \ + openscreen::internal::ScopedTraceOperation::set_result(result); \ + } \ + } while (false) +#define TRACE_SET_HIERARCHY(ids) TRACE_SET_HIERARCHY_INTERNAL(__LINE__, ids) +#define TRACE_HIERARCHY \ + (TRACE_IS_ENABLED(openscreen::TraceCategory::kAny) \ + ? openscreen::internal::ScopedTraceOperation::hierarchy() \ + : openscreen::TraceIdHierarchy::Empty()) +#define TRACE_CURRENT_ID \ + (TRACE_IS_ENABLED(openscreen::TraceCategory::kAny) \ + ? openscreen::internal::ScopedTraceOperation::current_id() \ + : kEmptyTraceId) +#define TRACE_ROOT_ID \ + (TRACE_IS_ENABLED(openscreen::TraceCategory::kAny) \ + ? openscreen::internal::ScopedTraceOperation::root_id() \ + : kEmptyTraceId) + +namespace openscreen::internal { + +template +std::string ToString(T&& val) { + using DecayT = std::decay_t; + if constexpr (std::is_constructible_v) { + return std::string(std::forward(val)); + } else if constexpr (std::is_arithmetic_v) { + return std::to_string(val); + } else { + std::ostringstream oss; + oss << val; + return oss.str(); + } +} + +// Helper to extract a flow ID from various types (arithmetic or wrappers like +// FrameId). +template +constexpr uint64_t ToFlowId(const T& val) { + if constexpr (std::is_arithmetic_v) { + return static_cast(val); + } else { + // Assume it's a numeric wrapper like FrameId with a .value() method. + return static_cast(val.value()); + } +} + +} // namespace openscreen::internal + +template +inline std::vector ToArgumentArray( + const char* argname = nullptr, + V1&& argval = V1(), + const char* argname_two = nullptr, + V2&& argval_two = V2()) { + std::vector out; + if (argname) { + out.emplace_back(argname, + openscreen::internal::ToString(std::forward(argval))); + } + if (argname_two) { + out.emplace_back(argname_two, openscreen::internal::ToString( + std::forward(argval_two))); + } + return out; +} + +// Synchronous Trace Macros. +// +// Scoped traces with no arguments. +#define TRACE_SCOPED(category, name, ...) \ + TRACE_SCOPED_INTERNAL(__LINE__, category, name, ToArgumentArray(), \ + ##__VA_ARGS__) +#define TRACE_DEFAULT_SCOPED(category, ...) \ + TRACE_SCOPED(category, __PRETTY_FUNCTION__, ##__VA_ARGS__) + +// Scoped traces with one argument. +#define TRACE_SCOPED1(category, name, argname, argval, ...) \ + TRACE_SCOPED_INTERNAL(__LINE__, category, name, \ + ToArgumentArray(argname, argval), ##__VA_ARGS__) +#define TRACE_DEFAULT_SCOPED1(category, argname, argval, ...) \ + TRACE_SCOPED1(category, __PRETTY_FUNCTION__, argname, argval, ##__VA_ARGS__) + +// Scoped traces with two arguments. +#define TRACE_SCOPED2(category, name, argname, argval, argname_two, \ + argval_two, ...) \ + TRACE_SCOPED_INTERNAL( \ + __LINE__, category, name, \ + ToArgumentArray(argname, argval, argname_two, argval_two), \ + ##__VA_ARGS__) +#define TRACE_DEFAULT_SCOPED2(category, argname, argval, argname_two, \ + argval_two, ...) \ + TRACE_SCOPED2(category, __PRETTY_FUNCTION__, argname, argval, argname_two, \ + argval_two, ##__VA_ARGS__) + +// Asynchronous Trace Macros. +#define TRACE_ASYNC_START(category, name, ...) \ + TRACE_ASYNC_START_INTERNAL(__LINE__, category, name, ToArgumentArray(), \ + ##__VA_ARGS__) + +#define TRACE_ASYNC_START1(category, name, argname, argval, ...) \ + TRACE_ASYNC_START_INTERNAL(__LINE__, category, name, \ + ToArgumentArray(argname, argval), ##__VA_ARGS__) + +#define TRACE_ASYNC_START2(category, name, argname, argval, argname_two, \ + argval_two, ...) \ + TRACE_ASYNC_START_INTERNAL( \ + __LINE__, category, name, \ + ToArgumentArray(argname, argval, argname_two, argval_two), \ + ##__VA_ARGS__) + +#define TRACE_ASYNC_END(category, id, result) \ + TRACE_IS_ENABLED(category) \ + ? openscreen::internal::ScopedTraceOperation::TraceAsyncEnd( \ + __LINE__, __FILE__, id, result) \ + : false + +// Flow events are used to link trace events across different threads or +// processes. Flows are linked by their flow_id. +// - Flows can span across different trace categories. +// - If a TRACE_FLOW_BEGIN is missing (e.g. because the embedder didn't +// instrument it), +// the first TRACE_FLOW_STEP encountered will effectively start the flow +// visualization. +#define TRACE_FLOW_BEGIN(category, name, flow_id) \ + TRACE_IS_ENABLED(category) \ + ? openscreen::internal::ScopedTraceOperation::TraceFlow( \ + category, name, __FILE__, __LINE__, \ + openscreen::internal::ToFlowId(flow_id), \ + openscreen::FlowType::kFlowBegin) \ + : false + +#define TRACE_FLOW_STEP(category, name, flow_id) \ + TRACE_IS_ENABLED(category) \ + ? openscreen::internal::ScopedTraceOperation::TraceFlow( \ + category, name, __FILE__, __LINE__, \ + openscreen::internal::ToFlowId(flow_id), \ + openscreen::FlowType::kFlowStep) \ + : false + +#define TRACE_FLOW_END(category, name, flow_id) \ + TRACE_IS_ENABLED(category) \ + ? openscreen::internal::ScopedTraceOperation::TraceFlow( \ + category, name, __FILE__, __LINE__, \ + openscreen::internal::ToFlowId(flow_id), \ + openscreen::FlowType::kFlowEnd) \ + : false + +#define TRACE_FLOW_BEGIN_WITH_TIME(category, name, flow_id, timestamp) \ + TRACE_IS_ENABLED(category) \ + ? openscreen::internal::ScopedTraceOperation::TraceFlow( \ + category, name, __FILE__, __LINE__, \ + openscreen::internal::ToFlowId(flow_id), \ + openscreen::FlowType::kFlowBegin, timestamp) \ + : false + +#define TRACE_FLOW_STEP_WITH_TIME(category, name, flow_id, timestamp) \ + TRACE_IS_ENABLED(category) \ + ? openscreen::internal::ScopedTraceOperation::TraceFlow( \ + category, name, __FILE__, __LINE__, \ + openscreen::internal::ToFlowId(flow_id), \ + openscreen::FlowType::kFlowStep, timestamp) \ + : false + +#define TRACE_FLOW_END_WITH_TIME(category, name, flow_id, timestamp) \ + TRACE_IS_ENABLED(category) \ + ? openscreen::internal::ScopedTraceOperation::TraceFlow( \ + category, name, __FILE__, __LINE__, \ + openscreen::internal::ToFlowId(flow_id), \ + openscreen::FlowType::kFlowEnd, timestamp) \ + : false + +#define TRACE_FLOW_DEFAULT_BEGIN(category, flow_id) \ + TRACE_FLOW_BEGIN(category, __PRETTY_FUNCTION__, flow_id) + +#define TRACE_FLOW_DEFAULT_STEP(category, flow_id) \ + TRACE_FLOW_STEP(category, __PRETTY_FUNCTION__, flow_id) + +#define TRACE_FLOW_DEFAULT_END(category, flow_id) \ + TRACE_FLOW_END(category, __PRETTY_FUNCTION__, flow_id) + +#else // ENABLE_TRACE_LOGGING not defined + +namespace openscreen::internal { +// Consumes `args` (to avoid "warn unused variable" errors at compile time), and +// provides a "void" result type in the macros below. +template +inline void DoNothingForTracing(Args... args) {} +} // namespace openscreen::internal + +#define TRACE_SET_RESULT(result) \ + openscreen::internal::DoNothingForTracing(result) +#define TRACE_SET_HIERARCHY(ids) openscreen::internal::DoNothingForTracing(ids) +#define TRACE_HIERARCHY openscreen::TraceIdHierarchy::Empty() +#define TRACE_CURRENT_ID openscreen::kEmptyTraceId +#define TRACE_ROOT_ID openscreen::kEmptyTraceId +#define TRACE_SCOPED(category, name, ...) \ + openscreen::internal::DoNothingForTracing(category, name, ##__VA_ARGS__) +#define TRACE_DEFAULT_SCOPED(category, ...) \ + TRACE_SCOPED(category, __PRETTY_FUNCTION__, ##__VA_ARGS__) + +#define TRACE_SCOPED1(category, name, argname, argval, ...) \ + openscreen::internal::DoNothingForTracing(category, name, argname, argval, \ + ##__VA_ARGS__) +#define TRACE_DEFAULT_SCOPED1(category, argname, argval, ...) \ + TRACE_SCOPED1(category, __PRETTY_FUNCTION__, argname, argval, ##__VA_ARGS__) + +#define TRACE_SCOPED2(category, name, argname, argval, argname_two, \ + argval_two, ...) \ + openscreen::internal::DoNothingForTracing( \ + category, name, argname, argval, argname_two, argval_two, ##__VA_ARGS__) +#define TRACE_DEFAULT_SCOPED2(category, argname, argval, argname_two, \ + argval_two, ...) \ + TRACE_SCOPED2(category, __PRETTY_FUNCTION__, argname, argval, argname_two, \ + argval_two, ##__VA_ARGS__) + +#define TRACE_ASYNC_START(category, name, ...) \ + openscreen::internal::DoNothingForTracing(category, name, ##__VA_ARGS__) +#define TRACE_ASYNC_END(category, id, result) \ + openscreen::internal::DoNothingForTracing(category, id, result) + +#define TRACE_FLOW_BEGIN(category, name, flow_id) \ + openscreen::internal::DoNothingForTracing(category, name, flow_id) +#define TRACE_FLOW_STEP(category, name, flow_id) \ + openscreen::internal::DoNothingForTracing(category, name, flow_id) +#define TRACE_FLOW_END(category, name, flow_id) \ + openscreen::internal::DoNothingForTracing(category, name, flow_id) + +#define TRACE_FLOW_BEGIN_WITH_TIME(category, name, flow_id, timestamp) \ + openscreen::internal::DoNothingForTracing(category, name, flow_id, timestamp) + +#define TRACE_FLOW_STEP_WITH_TIME(category, name, flow_id, timestamp) \ + openscreen::internal::DoNothingForTracing(category, name, flow_id, timestamp) + +#define TRACE_FLOW_END_WITH_TIME(category, name, flow_id, timestamp) \ + openscreen::internal::DoNothingForTracing(category, name, flow_id, timestamp) + +#define TRACE_FLOW_DEFAULT_BEGIN(category, flow_id) \ + openscreen::internal::DoNothingForTracing(category, flow_id) +#define TRACE_FLOW_DEFAULT_STEP(category, flow_id) \ + openscreen::internal::DoNothingForTracing(category, flow_id) +#define TRACE_FLOW_DEFAULT_END(category, flow_id) \ + openscreen::internal::DoNothingForTracing(category, flow_id) + +#endif // defined(ENABLE_TRACE_LOGGING) + +#endif // UTIL_TRACE_LOGGING_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/trace_logging/macro_support.h b/breadcast-caststream-sys/vendor/openscreen/util/trace_logging/macro_support.h new file mode 100644 index 0000000..db59514 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/trace_logging/macro_support.h @@ -0,0 +1,84 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_TRACE_LOGGING_MACRO_SUPPORT_H_ +#define UTIL_TRACE_LOGGING_MACRO_SUPPORT_H_ + +#ifndef INCLUDING_FROM_UTIL_TRACE_LOGGING_H_ +#error "Do not include this header directly. Use util/trace_logging.h." +#endif + +#ifndef ENABLE_TRACE_LOGGING +#error "BUG: This file should not have been reached." +#endif + +#include "platform/api/trace_logging_platform.h" +#include "platform/base/trace_logging_activation.h" +#include "platform/base/trace_logging_types.h" +#include "util/trace_logging/scoped_trace_operations.h" + +// Helper macros. These are used to simplify the macros below. +// NOTE: These cannot be #undef'd or they will stop working outside this file. +// NOTE: Two of these below macros are intentionally the same. This is to work +// around optimizations in the C++ Precompiler. +#define TRACE_INTERNAL_CONCAT(a, b) a##b +#define TRACE_INTERNAL_CONCAT_CONST(a, b) TRACE_INTERNAL_CONCAT(a, b) +#define TRACE_INTERNAL_UNIQUE_VAR_NAME(a) \ + TRACE_INTERNAL_CONCAT_CONST(a, __LINE__) + +namespace openscreen::internal { + +inline bool IsTraceLoggingEnabled(TraceCategory category) { + const CurrentTracingDestination destination; + return destination && destination->IsTraceLoggingEnabled(category); +} + +} // namespace openscreen::internal + +#define TRACE_IS_ENABLED(category) \ + openscreen::internal::IsTraceLoggingEnabled(category) + +// Internal logging macros. +#define TRACE_SET_HIERARCHY_INTERNAL(line, ids) \ + alignas(32) uint8_t TRACE_INTERNAL_CONCAT_CONST( \ + tracing_storage, line)[sizeof(openscreen::internal::TraceIdSetter)]; \ + [[maybe_unused]] \ + const auto TRACE_INTERNAL_UNIQUE_VAR_NAME(trace_ref_) = \ + TRACE_IS_ENABLED(openscreen::TraceCategory::kAny) \ + ? openscreen::internal::TraceInstanceHelper< \ + openscreen::internal::TraceIdSetter>:: \ + Create(TRACE_INTERNAL_CONCAT_CONST(tracing_storage, line), \ + ids) \ + : openscreen::internal::TraceInstanceHelper< \ + openscreen::internal::TraceIdSetter>::Empty() + +#define TRACE_SCOPED_INTERNAL(line, category, name, ...) \ + alignas(32) uint8_t TRACE_INTERNAL_CONCAT_CONST( \ + tracing_storage, \ + line)[sizeof(openscreen::internal::SynchronousTraceLogger)]; \ + [[maybe_unused]] \ + const auto TRACE_INTERNAL_UNIQUE_VAR_NAME(trace_ref_) = \ + TRACE_IS_ENABLED(category) \ + ? openscreen::internal::TraceInstanceHelper< \ + openscreen::internal::SynchronousTraceLogger>:: \ + Create(TRACE_INTERNAL_CONCAT_CONST(tracing_storage, line), \ + category, name, __FILE__, __LINE__, ##__VA_ARGS__) \ + : openscreen::internal::TraceInstanceHelper< \ + openscreen::internal::SynchronousTraceLogger>::Empty() + +#define TRACE_ASYNC_START_INTERNAL(line, category, name, ...) \ + alignas(32) uint8_t TRACE_INTERNAL_CONCAT_CONST( \ + temp_storage, \ + line)[sizeof(openscreen::internal::AsynchronousTraceLogger)]; \ + [[maybe_unused]] \ + const auto TRACE_INTERNAL_UNIQUE_VAR_NAME(trace_ref_) = \ + TRACE_IS_ENABLED(category) \ + ? openscreen::internal::TraceInstanceHelper< \ + openscreen::internal::AsynchronousTraceLogger>:: \ + Create(TRACE_INTERNAL_CONCAT_CONST(temp_storage, line), \ + category, name, __FILE__, __LINE__, ##__VA_ARGS__) \ + : openscreen::internal::TraceInstanceHelper< \ + openscreen::internal::AsynchronousTraceLogger>::Empty() + +#endif // UTIL_TRACE_LOGGING_MACRO_SUPPORT_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/trace_logging/scoped_trace_operations.cc b/breadcast-caststream-sys/vendor/openscreen/util/trace_logging/scoped_trace_operations.cc new file mode 100644 index 0000000..f7e6848 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/trace_logging/scoped_trace_operations.cc @@ -0,0 +1,160 @@ +// Copyright 2018 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/trace_logging/scoped_trace_operations.h" + +#include "platform/api/trace_logging_platform.h" +#include "platform/base/trace_logging_activation.h" +#include "util/osp_logging.h" + +#if defined(ENABLE_TRACE_LOGGING) + +namespace openscreen::internal { + +// static +bool ScopedTraceOperation::TraceAsyncEnd(const uint32_t line, + const char* file, + TraceId id, + Error::Code e) { + const CurrentTracingDestination destination; + if (destination) { + TraceEvent end_event; + end_event.start_time = Clock::now(); + end_event.line_number = line; + end_event.file_name = file; + end_event.ids.current = id; + end_event.result = e; + destination->LogAsyncEnd(std::move(end_event)); + return true; + } + return false; +} + +// static +bool ScopedTraceOperation::TraceFlow( + TraceCategory category, + const char* name, + const char* file, + uint32_t line, + uint64_t flow_id, + FlowType type, + std::optional timestamp) { + const CurrentTracingDestination destination; + if (destination) { + const auto start_time = timestamp ? *timestamp : Clock::now(); + TraceEvent event(category, start_time, name, file, line); + event.flow_ids.push_back(flow_id); + destination->LogFlow(std::move(event), type); + return true; + } + return false; +} + +ScopedTraceOperation::ScopedTraceOperation(TraceId trace_id, + TraceId parent_id, + TraceId root_id) { + if (traces_ == nullptr) { + // Create the stack if it doesnt' exist. + traces_ = new TraceStack(); + + // Create a new root node. This will re-call this constructor and add the + // root node to the stack before proceeding with the original node. + root_node_ = new TraceIdSetter(TraceIdHierarchy::Empty()); + OSP_CHECK(!traces_->empty()); + } + + // Setting trace id fields. + root_id_ = root_id != kUnsetTraceId ? root_id : traces_->top()->root_id_; + parent_id_ = + parent_id != kUnsetTraceId ? parent_id : traces_->top()->trace_id_; + trace_id_ = + trace_id != kUnsetTraceId ? trace_id : trace_id_counter_.fetch_add(1); + + // Add this item to the stack. + traces_->push(this); + OSP_CHECK_LT(traces_->size(), 1024); +} + +ScopedTraceOperation::~ScopedTraceOperation() { + OSP_CHECK(traces_ != nullptr && !traces_->empty()); + OSP_CHECK_EQ(traces_->top(), this); + traces_->pop(); + + // If there's only one item left, it must be the root node. Deleting the root + // node will re-call this destructor and delete the traces_ stack. + if (traces_->size() == 1) { + OSP_CHECK_EQ(traces_->top(), root_node_); + delete root_node_; + root_node_ = nullptr; + } else if (traces_->empty()) { + delete traces_; + traces_ = nullptr; + } +} + +// static +thread_local ScopedTraceOperation::TraceStack* ScopedTraceOperation::traces_ = + nullptr; + +// static +thread_local ScopedTraceOperation* ScopedTraceOperation::root_node_ = nullptr; + +// static +std::atomic ScopedTraceOperation::trace_id_counter_{ + uint64_t{0x01} << (sizeof(TraceId) * 8 - 1)}; + +TraceLoggerBase::TraceLoggerBase(TraceCategory category, + const char* name, + const char* file, + uint32_t line, + std::vector arguments, + TraceId current, + TraceId parent, + TraceId root) + : ScopedTraceOperation(current, parent, root), + event_(category, Clock::now(), name, file, line) { + event_.arguments = std::move(arguments); + event_.TruncateStrings(); +} + +TraceLoggerBase::TraceLoggerBase(TraceCategory category, + const char* name, + const char* file, + uint32_t line, + std::vector arguments, + TraceIdHierarchy ids) + : TraceLoggerBase(category, + name, + file, + line, + std::move(arguments), + ids.current, + ids.parent, + ids.root) {} + +SynchronousTraceLogger::~SynchronousTraceLogger() { + const CurrentTracingDestination destination; + if (destination) { + const auto end_time = Clock::now(); + event_.ids = to_hierarchy(); + destination->LogTrace(event_, end_time); + } +} + +AsynchronousTraceLogger::~AsynchronousTraceLogger() { + const CurrentTracingDestination destination; + if (destination) { + event_.ids = to_hierarchy(); + destination->LogAsyncStart(event_); + } +} + +TraceIdSetter::TraceIdSetter(TraceIdHierarchy ids) + : ScopedTraceOperation(ids.current, ids.parent, ids.root) {} + +TraceIdSetter::~TraceIdSetter() = default; + +} // namespace openscreen::internal + +#endif // defined(ENABLE_TRACE_LOGGING) diff --git a/breadcast-caststream-sys/vendor/openscreen/util/trace_logging/scoped_trace_operations.h b/breadcast-caststream-sys/vendor/openscreen/util/trace_logging/scoped_trace_operations.h new file mode 100644 index 0000000..e94198d --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/trace_logging/scoped_trace_operations.h @@ -0,0 +1,224 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_TRACE_LOGGING_SCOPED_TRACE_OPERATIONS_H_ +#define UTIL_TRACE_LOGGING_SCOPED_TRACE_OPERATIONS_H_ + +#include +#include +#include +#include +#include +#include +#include + +#include "platform/api/time.h" +#include "platform/api/trace_logging_platform.h" +#include "platform/base/error.h" +#include "platform/base/trace_logging_types.h" +#include "util/osp_logging.h" + +#if defined(ENABLE_TRACE_LOGGING) + +namespace openscreen::internal { + +// A base class for all trace logging objects which will create new entries in +// the Trace Hierarchy. +// 1) The sharing of all static and thread_local variables across template +// specializations. +// 2) Including all children in the same traces vector. +class ScopedTraceOperation { + public: + // Define the destructor to remove this item from the stack when it's + // destroyed. + virtual ~ScopedTraceOperation(); + + ScopedTraceOperation(const ScopedTraceOperation&) = delete; + ScopedTraceOperation(ScopedTraceOperation&&) noexcept = delete; + ScopedTraceOperation& operator=(const ScopedTraceOperation&) = delete; + ScopedTraceOperation& operator=(ScopedTraceOperation&&) = delete; + + // Getters the current Trace Hierarchy. If the traces_ stack hasn't been + // created yet, return as if the empty root node is there. + static TraceId current_id() { + return traces_ == nullptr ? kEmptyTraceId : traces_->top()->trace_id_; + } + + static TraceId root_id() { + return traces_ == nullptr ? kEmptyTraceId : traces_->top()->root_id_; + } + + static TraceIdHierarchy hierarchy() { + if (traces_ == nullptr) { + return TraceIdHierarchy::Empty(); + } + + return traces_->top()->to_hierarchy(); + } + + // Static method to set the result of the most recent trace. + static void set_result(const Error& error) { set_result(error.code()); } + static void set_result(Error::Code error) { + if (traces_ == nullptr) { + return; + } + traces_->top()->SetTraceResult(error); + } + + // Traces the end of an asynchronous call. + // NOTE: This returns a bool rather than a void because it keeps the syntax of + // the ternary operator in the macros simpler. + static bool TraceAsyncEnd(const uint32_t line, + const char* file, + TraceId id, + Error::Code e); + + // Traces a flow event. + static bool TraceFlow( + TraceCategory category, + const char* name, + const char* file, + uint32_t line, + uint64_t flow_id, + FlowType type, + std::optional timestamp = std::nullopt); + + protected: + // Sets the result of this trace log. + // NOTE: this must be define in this class rather than TraceLogger so that it + // can be called on traces.back() without a potentially unsafe cast or type + // checking at runtime. + virtual void SetTraceResult(Error::Code error) = 0; + + // Constructor to set all trace id information. + ScopedTraceOperation(TraceId current_id = kUnsetTraceId, + TraceId parent_id = kUnsetTraceId, + TraceId root_id = kUnsetTraceId); + + // Current TraceId information. + TraceId trace_id_; + TraceId parent_id_; + TraceId root_id_; + + TraceIdHierarchy to_hierarchy() { return {trace_id_, parent_id_, root_id_}; } + + private: + // NOTE: A std::vector is used for backing the stack because it provides the + // best perf. Further perf improvement could be achieved later by swapping + // this out for a circular buffer once OSP supports that. Additional details + // can be found here: + // https://www.codeproject.com/Articles/1185449/Performance-of-a-Circular-Buffer-vs-Vector-Deque-a + using TraceStack = + std::stack>; + + // Counter to pick IDs when it is not provided. + static std::atomic trace_id_counter_; + + // The LIFO stack of TraceLoggers currently being watched by this + // thread. + static thread_local TraceStack* traces_; + static thread_local ScopedTraceOperation* root_node_; +}; + +// The class which does actual trace logging. +class TraceLoggerBase : public ScopedTraceOperation { + public: + TraceLoggerBase(TraceCategory category, + const char* name, + const char* file, + uint32_t line, + std::vector arguments = {}, + TraceId current = kUnsetTraceId, + TraceId parent = kUnsetTraceId, + TraceId root = kUnsetTraceId); + + TraceLoggerBase(TraceCategory category, + const char* name, + const char* file, + uint32_t line, + std::vector arguments, + TraceIdHierarchy ids); + + TraceLoggerBase(const TraceLoggerBase&) = delete; + TraceLoggerBase(TraceLoggerBase&&) noexcept = delete; + TraceLoggerBase& operator=(const TraceLoggerBase&) = delete; + TraceLoggerBase& operator=(TraceLoggerBase&&) = delete; + + protected: + // Set the result. + void SetTraceResult(Error::Code error) override { event_.result = error; } + + TraceEvent event_; +}; + +class SynchronousTraceLogger : public TraceLoggerBase { + public: + using TraceLoggerBase::TraceLoggerBase; + + SynchronousTraceLogger(const SynchronousTraceLogger&) = delete; + SynchronousTraceLogger(SynchronousTraceLogger&&) noexcept = delete; + SynchronousTraceLogger& operator=(const SynchronousTraceLogger&) = delete; + SynchronousTraceLogger& operator=(SynchronousTraceLogger&&) = delete; + + ~SynchronousTraceLogger() override; +}; + +class AsynchronousTraceLogger : public TraceLoggerBase { + public: + using TraceLoggerBase::TraceLoggerBase; + + AsynchronousTraceLogger(const AsynchronousTraceLogger&) = delete; + AsynchronousTraceLogger(AsynchronousTraceLogger&&) noexcept = delete; + AsynchronousTraceLogger& operator=(const AsynchronousTraceLogger&) = delete; + AsynchronousTraceLogger& operator=(AsynchronousTraceLogger&&) = delete; + + ~AsynchronousTraceLogger() override; +}; + +// Inserts a fake element into the ScopedTraceOperation stack to set +// the current TraceId Hierarchy manually. +class TraceIdSetter final : public ScopedTraceOperation { + public: + explicit TraceIdSetter(TraceIdHierarchy ids); + TraceIdSetter(const TraceIdSetter&) = delete; + TraceIdSetter(TraceIdSetter&&) noexcept = delete; + TraceIdSetter& operator=(const TraceIdSetter&) = delete; + TraceIdSetter& operator=(TraceIdSetter&&) = delete; + ~TraceIdSetter() final; + + // Creates a new TraceIdSetter to set the full TraceId Hierarchy to default + // values and does not push it to the traces stack. + static TraceIdSetter* CreateStackRootNode(); + + private: + // Implement abstract method for use in Macros. + void SetTraceResult(Error::Code error) {} +}; + +// This helper object allows us to delete objects allocated on the stack in a +// unique_ptr. +template +class TraceInstanceHelper { + private: + class TraceOperationOnStackDeleter { + public: + void operator()(T* ptr) { ptr->~T(); } + }; + + using TraceInstanceWrapper = std::unique_ptr; + + public: + template + static TraceInstanceWrapper Create(uint8_t storage[sizeof(T)], Args... args) { + return TraceInstanceWrapper(new (storage) T(std::forward(args)...)); + } + + static TraceInstanceWrapper Empty() { return TraceInstanceWrapper(); } +}; + +} // namespace openscreen::internal + +#endif // defined(ENABLE_TRACE_LOGGING) + +#endif // UTIL_TRACE_LOGGING_SCOPED_TRACE_OPERATIONS_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/uuid.cc b/breadcast-caststream-sys/vendor/openscreen/util/uuid.cc new file mode 100644 index 0000000..1234edd --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/uuid.cc @@ -0,0 +1,127 @@ +// Copyright 2025 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "util/uuid.h" + +#include +#include + +#include + +#include "util/big_endian.h" +#include "util/crypto/random_bytes.h" +#include "util/hashing.h" +#include "util/osp_logging.h" +#include "util/string_util.h" +#include "util/stringprintf.h" + +namespace openscreen { + +namespace { + +constexpr bool IsHyphenPosition(size_t i) { + return i == 8 || i == 13 || i == 18 || i == 23; +} + +// Returns a canonical Uuid string given that `input` is validly formatted +// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, such that x is a hexadecimal digit. +// If `strict`, x must be a lower-case hexadecimal digit. +std::string GetCanonicalUuidInternal(std::string_view input, bool strict) { + constexpr size_t kUuidLength = 36; + if (input.length() != kUuidLength) { + return {}; + } + + std::string lowercase; + lowercase.resize(kUuidLength); + for (size_t i = 0; i < input.length(); ++i) { + auto current = input[i]; + if (IsHyphenPosition(i)) { + if (current != '-') { + return {}; + } + lowercase[i] = '-'; + } else { + if (strict ? !string_util::ascii_islowerhex(current) + : !string_util::ascii_ishex(current)) { + return {}; + } + lowercase[i] = static_cast(string_util::ascii_tolower(current)); + } + } + + return lowercase; +} + +} // namespace + +// static +Uuid Uuid::GenerateRandomV4() { + return FormatRandomDataAsV4Impl(GenerateRandomBytes16()); +} + +// static +Uuid Uuid::FormatRandomDataAsV4Impl(ByteView input) { + OSP_CHECK_EQ(input.size(), kGuidV4InputLength); + + auto first_u64 = ReadBigEndian(input.first(8).data()); + auto second_u64 = ReadBigEndian(input.last(8).data()); + + // Set the Uuid to version 4 as described in RFC 4122, section 4.4. + // The format of Uuid version 4 must be xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, + // where y is one of [8, 9, a, b]. + + // Clear the version bits and set the version to 4: + first_u64 &= 0xffffffff'ffff0fffULL; + first_u64 |= 0x00000000'00004000ULL; + + // Clear bit 65 and set bit 64, to set the 'var' field to 0b10 per RFC 9562 + // section 5.4. + second_u64 &= 0x3fffffff'ffffffffULL; + second_u64 |= 0x80000000'00000000ULL; + + Uuid uuid; + uuid.lowercase_ = + StringFormat("{:08x}-{:04x}-{:04x}-{:04x}-{:012x}", + static_cast(first_u64 >> 32), + static_cast((first_u64 >> 16) & 0x0000'ffff), + static_cast(first_u64 & 0x0000'ffff), + static_cast(second_u64 >> 48), + second_u64 & 0x0000'ffff'ffff'ffffULL); + return uuid; +} + +// static +Uuid Uuid::ParseCaseInsensitive(std::string_view input) { + Uuid uuid; + uuid.lowercase_ = GetCanonicalUuidInternal(input, /*strict=*/false); + return uuid; +} + +// static +Uuid Uuid::ParseLowercase(std::string_view input) { + Uuid uuid; + uuid.lowercase_ = GetCanonicalUuidInternal(input, /*strict=*/true); + return uuid; +} + +Uuid::Uuid() = default; +Uuid::Uuid(const Uuid& other) = default; +Uuid::Uuid(Uuid&& other) noexcept = default; +Uuid& Uuid::operator=(const Uuid& other) = default; +Uuid& Uuid::operator=(Uuid&& other) = default; + +const std::string& Uuid::AsLowercaseString() const { + return lowercase_; +} + +std::ostream& operator<<(std::ostream& out, const Uuid& uuid) { + return out << uuid.AsLowercaseString(); +} + +size_t UuidHash::operator()(const Uuid& uuid) const { + return ComputeAggregateHash(uuid.AsLowercaseString()); +} + +} // namespace openscreen diff --git a/breadcast-caststream-sys/vendor/openscreen/util/uuid.h b/breadcast-caststream-sys/vendor/openscreen/util/uuid.h new file mode 100644 index 0000000..fe90adb --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/uuid.h @@ -0,0 +1,81 @@ +// Copyright 2025 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_UUID_H_ +#define UTIL_UUID_H_ + +#include + +#include +#include +#include +#include + +#include "platform/base/span.h" + +namespace openscreen { + +// UUID implementation strongly based off of Chromium's base::Uuid +// implementation. Provides securely generated random Uuids as well as parsing +// logic for inputted UUIDs. +class Uuid { + public: + // Length in bytes of the input required to format the input as a Uuid in the + // form of version 4. + static constexpr size_t kGuidV4InputLength = 16; + + // Generate a 128-bit random Uuid in the form of version 4. see RFC 4122, + // section 4.4. The format of Uuid version 4 must be + // xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx, where y is one of [8, 9, a, b]. The + // hexadecimal values "a" through "f" are output as lower case characters. + static Uuid GenerateRandomV4(); + + // Returns a valid Uuid if the input string conforms to the Uuid format, and + // an invalid Uuid otherwise. Accepts both lower case and upper case hex + // characters. + static Uuid ParseCaseInsensitive(std::string_view input); + + // Similar to ParseCaseInsensitive(), but all hexadecimal values "a" through + // "f" must be lower case characters. + static Uuid ParseLowercase(std::string_view input); + + // Constructs an invalid Uuid. + Uuid(); + + Uuid(const Uuid& other); + Uuid(Uuid&& other) noexcept; + Uuid& operator=(const Uuid& other); + Uuid& operator=(Uuid&& other); + + bool is_valid() const { return !lowercase_.empty(); } + + // Returns the Uuid in a lowercase string format if it is valid, and an empty + // string otherwise. The returned value is guaranteed to be parsed by + // ParseLowercase(). + const std::string& AsLowercaseString() const; + + // Invalid Uuids are equal. + friend bool operator==(const Uuid&, const Uuid&) = default; + // Uuids are 128bit chunks of data so must be indistinguishable if equivalent. + friend std::strong_ordering operator<=>(const Uuid&, const Uuid&) = default; + + private: + static Uuid FormatRandomDataAsV4Impl(ByteView input); + + // The lowercase form of the Uuid. Empty for invalid Uuids. + std::string lowercase_; +}; + +// For runtime usage only. Do not store the result of this hash, as it may +// change in the future. +struct UuidHash { + size_t operator()(const Uuid& uuid) const; +}; + +// Stream operator so Uuid objects can be used in logging statements. +std::ostream& operator<<(std::ostream& out, const Uuid& uuid); + +} // namespace openscreen + +#endif // UTIL_UUID_H_ diff --git a/breadcast-caststream-sys/vendor/openscreen/util/weak_ptr.h b/breadcast-caststream-sys/vendor/openscreen/util/weak_ptr.h new file mode 100644 index 0000000..e190ba5 --- /dev/null +++ b/breadcast-caststream-sys/vendor/openscreen/util/weak_ptr.h @@ -0,0 +1,217 @@ +// Copyright 2019 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef UTIL_WEAK_PTR_H_ +#define UTIL_WEAK_PTR_H_ + +#include +#include + +#include "util/osp_logging.h" + +namespace openscreen { + +// Weak pointers are pointers to an object that do not affect its lifetime, +// and which may be invalidated (i.e. reset to nullptr) by the object, or its +// owner, at any time; most commonly when the object is about to be deleted. +// +// Weak pointers are useful when an object needs to be accessed safely by one +// or more objects other than its owner, and those callers can cope with the +// object vanishing and e.g. tasks posted to it being silently dropped. +// Reference-counting such an object would complicate the ownership graph and +// make it harder to reason about the object's lifetime. +// +// EXAMPLE: +// +// class Controller { +// public: +// void SpawnWorker() { new Worker(weak_factory_.GetWeakPtr()); } +// void WorkComplete(const Result& result) { ... } +// private: +// // Member variables should appear before the WeakPtrFactory, to ensure +// // that any WeakPtrs to Controller are invalidated before its members +// // variable's destructors are executed, rendering them invalid. +// WeakPtrFactory weak_factory_{this}; +// }; +// +// class Worker { +// public: +// explicit Worker(WeakPtr controller) +// : controller_(std::move(controller)) {} +// private: +// void DidCompleteAsynchronousProcessing(const Result& result) { +// if (controller_) +// controller_->WorkComplete(result); +// delete this; +// } +// const WeakPtr controller_; +// }; +// +// With this implementation a caller may use SpawnWorker() to dispatch multiple +// Workers and subsequently delete the Controller, without waiting for all +// Workers to have completed. +// +// ------------------------- IMPORTANT: Thread-safety ------------------------- +// +// Generally, Open Screen code is meant to be single-threaded. For the few +// exceptional cases, the following is relevant: +// +// WeakPtrs may be created from WeakPtrFactory, and also duplicated/moved on any +// thread/sequence. However, they may only be dereferenced on the same +// thread/sequence that will ultimately execute the WeakPtrFactory destructor or +// call InvalidateWeakPtrs(). Otherwise, use-during-free or use-after-free is +// possible. +// +// openscreen::WeakPtr and WeakPtrFactory are similar, but not identical, to +// Chromium's base::WeakPtrFactory. Open Screen WeakPtrs may be safely created +// from WeakPtrFactory on any thread/sequence, since they are backed by the +// thread-safe bookkeeping of std::shared_ptr<>. + +template +class WeakPtrFactory; + +template +class WeakPtr { + public: + WeakPtr() = default; + ~WeakPtr() = default; + + // Copy/Move constructors and assignment operators. + WeakPtr(const WeakPtr& other) : impl_(other.impl_) {} + + WeakPtr(WeakPtr&& other) noexcept : impl_(std::move(other.impl_)) {} + + WeakPtr& operator=(const WeakPtr& other) { + impl_ = other.impl_; + return *this; + } + + WeakPtr& operator=(WeakPtr&& other) noexcept { + impl_ = std::move(other.impl_); + return *this; + } + + // Create/Assign from nullptr. + WeakPtr(std::nullptr_t) {} // NOLINT + + WeakPtr& operator=(std::nullptr_t) { + impl_.reset(); + return *this; + } + + // Copy/Move constructors and assignment operators with upcast conversion. + template + WeakPtr(const WeakPtr& other) : impl_(other.as_std_weak_ptr()) {} + + template + WeakPtr(WeakPtr&& other) noexcept + : impl_(std::move(other).as_std_weak_ptr()) {} + + template + WeakPtr& operator=(const WeakPtr& other) { + impl_ = other.as_std_weak_ptr(); + return *this; + } + + template + WeakPtr& operator=(WeakPtr&& other) noexcept { + impl_ = std::move(other).as_std_weak_ptr(); + return *this; + } + + // Accessors. + T* get() const { return impl_.lock().get(); } + + T& operator*() const { + T* const pointer = get(); + OSP_CHECK(pointer); + return *pointer; + } + + T* operator->() const { + T* const pointer = get(); + OSP_CHECK(pointer); + return pointer; + } + + // Allow conditionals to test validity, e.g. if (weak_ptr) {...} + explicit operator bool() const { return get() != nullptr; } + + // Conversion to std::weak_ptr. It is unsafe to convert in the other + // direction. See comments for private constructors, below. + const std::weak_ptr& as_std_weak_ptr() const& { return impl_; } + std::weak_ptr as_std_weak_ptr() && { return std::move(impl_); } + + private: + friend class WeakPtrFactory; + + // Called by WeakPtrFactory and the WeakPtr upcast conversion + // constructors and assigners. These are purposely not being exposed publicly + // because that would allow a WeakPtr to be valid/invalid by a different + // ownership/threading model than the intended one (see top-level comments). + template + explicit WeakPtr(const std::weak_ptr& other) : impl_(other) {} + + template + explicit WeakPtr(std::weak_ptr&& other) noexcept + : impl_(std::move(other)) {} + + std::weak_ptr impl_; +}; + +// Allow callers to compare WeakPtrs against nullptr to test validity. +template +bool operator!=(const WeakPtr& weak_ptr, std::nullptr_t) { + return weak_ptr.get() != nullptr; +} +template +bool operator!=(std::nullptr_t, const WeakPtr& weak_ptr) { + return weak_ptr.get() != nullptr; +} +template +bool operator==(const WeakPtr& weak_ptr, std::nullptr_t) { + return weak_ptr.get() == nullptr; +} +template +bool operator==(std::nullptr_t, const WeakPtr& weak_ptr) { + return weak_ptr == nullptr; +} + +template +class WeakPtrFactory { + public: + explicit WeakPtrFactory(T* instance) { Reset(instance); } + WeakPtrFactory(WeakPtrFactory&& other) noexcept = default; + WeakPtrFactory& operator=(WeakPtrFactory&& other) noexcept = default; + + // Thread-safe: WeakPtrs may be created on any thread/seuence. They may also + // be copied and moved on any thread/sequence. However, they MUST only be + // dereferenced on the same thread/sequence that calls the destructor or + // InvalidateWeakPtrs(). + WeakPtr GetWeakPtr() const { + return WeakPtr(std::weak_ptr(bookkeeper_)); + } + + // Destruction and Invalidation: These must be called on the same + // thread/sequence that dereferences any WeakPtrs to avoid use-after-free + // bugs. + ~WeakPtrFactory() = default; + void InvalidateWeakPtrs() { Reset(bookkeeper_.get()); } + + private: + WeakPtrFactory(const WeakPtrFactory& other) = delete; + WeakPtrFactory& operator=(const WeakPtrFactory& other) = delete; + + void Reset(T* instance) { + // T is owned externally to WeakPtrFactory. Thus, provide a no-op Deleter. + bookkeeper_ = {instance, [](T*) {}}; + } + + // Manages the std::weak_ptr's referring to T. Does not own T. + std::shared_ptr bookkeeper_; +}; + +} // namespace openscreen + +#endif // UTIL_WEAK_PTR_H_ diff --git a/breadcast-core/Cargo.toml b/breadcast-core/Cargo.toml new file mode 100644 index 0000000..b454309 --- /dev/null +++ b/breadcast-core/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "breadcast-core" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +description = "Shared, GTK-agnostic logic for breadcast: Chromecast + DLNA discovery/control, capture/encode/serve pipeline" + +[dependencies] +anyhow = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tracing = { workspace = true } +tokio = { workspace = true } +mdns-sd = "0.20" +rust_cast = { version = "0.21", features = ["thread_safe"] } +ashpd = { version = "0.13", features = ["screencast"] } +gstreamer = "0.25" +gstreamer-app = "0.25" +gstreamer-video = "0.25" +tiny_http = "0.12" +rupnp = "3.0.0" +futures-util = "0.3.33" +breadcast-caststream-sys = { path = "../breadcast-caststream-sys" } + +[dev-dependencies] +tracing-subscriber = { workspace = true } + +[[example]] +name = "discover" +path = "src/examples/discover.rs" + +[[example]] +name = "cast_test" +path = "src/examples/cast_test.rs" + +[[example]] +name = "capture_test" +path = "src/examples/capture_test.rs" + +[[example]] +name = "video_test" +path = "src/examples/video_test.rs" + +[[example]] +name = "mirror_test" +path = "src/examples/mirror_test.rs" + +[[example]] +name = "apple_hls_test" +path = "src/examples/apple_hls_test.rs" + +[[example]] +name = "dlna_discover" +path = "src/examples/dlna_discover.rs" + +[[example]] +name = "dlna_mirror_test" +path = "src/examples/dlna_mirror_test.rs" + +[[example]] +name = "cast_stream_test" +path = "src/examples/cast_stream_test.rs" diff --git a/breadcast-core/src/capture/mod.rs b/breadcast-core/src/capture/mod.rs new file mode 100644 index 0000000..a04beb3 --- /dev/null +++ b/breadcast-core/src/capture/mod.rs @@ -0,0 +1,3 @@ +pub mod portal; + +pub use portal::CaptureSession; diff --git a/breadcast-core/src/capture/portal.rs b/breadcast-core/src/capture/portal.rs new file mode 100644 index 0000000..77b99cc --- /dev/null +++ b/breadcast-core/src/capture/portal.rs @@ -0,0 +1,144 @@ +use std::os::unix::fs::{DirBuilderExt, OpenOptionsExt}; +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use ashpd::desktop::PersistMode; +use ashpd::desktop::Session; +use ashpd::desktop::screencast::{CursorMode, Screencast, SelectSourcesOptions, SourceType}; + +/// Where the portal's restore token is cached, so re-mirroring doesn't +/// require re-clicking the system picker every single time — the token +/// (opaque to us) is what lets a later `SelectSources` call skip straight +/// to "yes, the same source as last time" instead of prompting again. +/// +/// Filters out an *empty* `XDG_CACHE_HOME` in addition to an unset one — +/// some environments export it but leave it blank, which would otherwise +/// resolve to a relative path against the current working directory. +fn restore_token_path() -> PathBuf { + let base = std::env::var("XDG_CACHE_HOME") + .ok() + .filter(|s| !s.is_empty()) + .map(PathBuf::from) + .unwrap_or_else(|| { + PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string())).join(".cache") + }); + base.join("breadcast").join("portal-restore-token") +} + +/// A live `xdg-desktop-portal` ScreenCast session. Keeping this alive keeps +/// the underlying PipeWire stream(s) open; dropping it without calling +/// [`CaptureSession::close`] leaves the portal to notice the D-Bus +/// connection went away rather than an explicit teardown. +pub struct CaptureSession { + session: Session, + video_node_id: u32, +} + +impl CaptureSession { + /// Opens the portal's screen-cast picker (monitor/window selection is + /// the portal's own native UI — see `xdg-desktop-portal-hyprland`'s own + /// picker dialog — not anything breadcast draws itself) and returns a + /// session bound to whatever the user picked. + /// + /// `CursorMode::Embedded` bakes the cursor into the captured frames, + /// which is what you want for "mirror my screen" (as opposed to + /// `Metadata`, meant for apps that composite their own cursor). + pub async fn start() -> Result { + let proxy = Screencast::new() + .await + .context("failed to connect to the ScreenCast portal (is xdg-desktop-portal running?)")?; + + let session = proxy + .create_session(Default::default()) + .await + .context("failed to create a portal screencast session")?; + + // Everything past this point can fail (denied/cancelled picker, no + // streams, etc.) — ashpd's `Session` has no `Drop` impl, so on any + // of those paths the session would otherwise leak for the rest of + // this process's life (and in a long-running daemon, potentially a + // leaked PipeWire node per cancelled picker). Route every error + // through an explicit close instead of an early `?` return. + match Self::negotiate(&proxy, &session).await { + Ok(video_node_id) => Ok(Self { session, video_node_id }), + Err(e) => { + let _ = session.close().await; + Err(e) + } + } + } + + async fn negotiate(proxy: &Screencast, session: &Session) -> Result { + let token_path = restore_token_path(); + let existing_token = std::fs::read_to_string(&token_path).ok(); + + let mut select_options = SelectSourcesOptions::default() + .set_cursor_mode(CursorMode::Embedded) + .set_sources(SourceType::Monitor | SourceType::Window) + .set_multiple(false) + .set_persist_mode(PersistMode::ExplicitlyRevoked); + if let Some(token) = existing_token.as_deref() { + select_options = select_options.set_restore_token(token); + } + + proxy + .select_sources(session, select_options) + .await + .context("failed to send SelectSources to the portal")? + .response() + .context("SelectSources request was denied or cancelled")?; + + let response = proxy + .start(session, None, Default::default()) + .await + .context("failed to send Start to the portal")? + .response() + .context("screen cast was cancelled (user closed the portal picker)")?; + + // Persist whatever token came back so the *next* start() can skip + // the picker. A stale/invalid token is not a failure mode to guard + // against here — the portal falls back to prompting again on its + // own if the token no longer resolves to a valid grant. + // + // The token is a no-prompt capability to re-open a screen capture + // of this user's session, so it's written 0600 in a 0700 directory + // rather than relying on umask — any other local user being able to + // read it would let them silently re-grant themselves the same + // capture access. + if let Some(token) = response.restore_token() { + if let Some(parent) = token_path.parent() { + let _ = std::fs::DirBuilder::new().recursive(true).mode(0o700).create(parent); + } + if let Ok(mut file) = std::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .mode(0o600) + .open(&token_path) + { + use std::io::Write; + let _ = file.write_all(token.as_bytes()); + } + } + + let stream = response + .streams() + .first() + .context("portal returned zero streams")?; + Ok(stream.pipe_wire_node_id()) + } + + /// The PipeWire node id for the selected video source — this is what + /// gets passed to GStreamer's `pipewiresrc path=`. + pub fn video_node_id(&self) -> u32 { + self.video_node_id + } + + /// Explicitly closes the portal session, ending the PipeWire stream. + pub async fn close(self) -> Result<()> { + self.session + .close() + .await + .context("failed to close the portal screencast session") + } +} diff --git a/breadcast-core/src/cast_sender.rs b/breadcast-core/src/cast_sender.rs new file mode 100644 index 0000000..8f82467 --- /dev/null +++ b/breadcast-core/src/cast_sender.rs @@ -0,0 +1,335 @@ +use std::sync::mpsc; +use std::thread; + +use anyhow::{Context, Result}; +use rust_cast::{ + CastDevice as RustCastDevice, ChannelMessage, + channels::{ + heartbeat::HeartbeatResponse, + media::{Media, MediaResponse, Status, StreamType}, + receiver::CastDeviceApp, + }, + message_manager::CastMessagePayload, +}; + +use crate::device::CastDevice; + +enum Command { + Load { + content_url: String, + content_type: String, + stream_type: StreamType, + reply: mpsc::Sender>, + }, + /// Sends a message on an arbitrary namespace to the launched app's + /// transport id — used for the Cast Streaming OFFER/ANSWER exchange + /// (see [`crate::caststream`]), which the built-in `media`/`receiver` + /// channels have no support for. Routed through this session's io + /// thread rather than sent directly, for the same reason `Load`/`Stop` + /// are: `rust_cast`'s `MessageManager` has no way to demultiplex + /// responses across threads (see this struct's own doc comment). + SendRaw { + namespace: String, + message: String, + reply: mpsc::Sender>, + }, + Stop { + reply: mpsc::Sender>, + }, +} + +/// An unsolicited message received on a namespace none of `rust_cast`'s +/// built-in channels claim (`ChannelMessage::Raw`) — e.g. an ANSWER on Cast +/// Streaming's `urn:x-cast:com.google.cast.webrtc` namespace. Binary +/// payloads are dropped (Cast Streaming's control messages are always JSON +/// text; nothing in this project's protocol usage sends binary here). +pub struct RawMessage { + pub source_id: String, + pub namespace: String, + pub message: String, +} + +/// The stream of [`RawMessage`]s received on namespaces +/// [`CastSession::connect`]'s default app (the Default Media Receiver) +/// never sends, but a custom-app session started via +/// [`CastSession::connect_app`] (e.g. the Cast Streaming Mirroring receiver) +/// does. Draining this is required for such sessions to make any progress — +/// see `cast_stream_test.rs`. +pub struct RawMessages { + rx: mpsc::Receiver, +} + +impl RawMessages { + pub fn recv(&self) -> Option { + self.rx.recv().ok() + } +} + +/// A live CASTV2 session to a device: connected, Default Media Receiver +/// launched, ready to load media. +/// +/// All device I/O happens on a single dedicated thread spawned by +/// [`CastSession::connect`] — `rust_cast`'s `MessageManager` has no way to +/// demultiplex responses by request id across threads. `load()`/`stop()` +/// each do their own blocking read internally (`receive_find_map`); if a +/// *different* thread were concurrently blocked in `device.receive()` (e.g. +/// pumping heartbeats/media status, as a previous version of this struct +/// did), whichever thread's read happens to be in flight when a response +/// arrives "wins" it — the other blocks forever waiting for a response that +/// was already consumed and handled elsewhere. Routing every command and +/// every unsolicited message through one thread's loop avoids that race +/// entirely. `CastSession` is `Clone` (cheap: just clones a channel sender) +/// so multiple callers can issue commands concurrently without needing +/// `&mut` or reintroducing the race. +#[derive(Clone)] +pub struct CastSession { + command_tx: mpsc::Sender, + /// The launched app's transport id — also its CASTV2 destination id on + /// namespaces outside the built-in channels (see + /// [`Self::send_raw_message`]/[`crate::caststream`]). + transport_id: String, +} + +/// The stream of unsolicited `MediaResponse` messages (player-state +/// transitions, load failures, etc.) pushed by the device — separate from +/// [`CastSession`] so a caller can hold one "watch what's happening" handle +/// alongside any number of cheap [`CastSession`] clones used to issue +/// commands, without either needing exclusive access. +pub struct MediaEvents { + rx: mpsc::Receiver, +} + +impl MediaEvents { + /// Blocks until the next `MediaResponse`, or returns `None` once the + /// session's io thread has ended (device disconnected, session + /// stopped, or a fatal receive error). + pub fn recv(&self) -> Option { + self.rx.recv().ok() + } + + /// Like [`Self::recv`] in a loop, invoking `on_media` for each message + /// until the session ends. Useful for interactive smoke testing. + pub fn pump_with_media_callback(&self, mut on_media: impl FnMut(&MediaResponse)) { + while let Some(media) = self.recv() { + on_media(&media); + } + } +} + +impl CastSession { + /// Connects to `target`, launches the Default Media Receiver, and spawns + /// the session's io thread (which immediately starts servicing + /// heartbeats on its own — a caller no longer needs to run anything just + /// to keep the connection alive). Returns a `CastSession` for issuing + /// commands plus a `MediaEvents` for observing player state. + /// + /// Uses `connect_without_host_verification`: Cast receivers present a + /// self-signed certificate by design (every Cast sender, including + /// Google's own, connects this way) — this is not a shortcut to harden + /// later, verifying against a CA chain will simply never succeed here. + pub fn connect(target: &CastDevice) -> Result<(Self, MediaEvents)> { + let (session, media, _raw) = Self::connect_app(target, CastDeviceApp::DefaultMediaReceiver)?; + Ok((session, media)) + } + + /// Like [`Self::connect`], but launches an arbitrary app (e.g. + /// [`crate::caststream::MIRRORING_APP_ID`] via + /// `CastDeviceApp::Custom(MIRRORING_APP_ID.to_string())`) and also + /// returns a [`RawMessages`] stream for namespaces the built-in channels + /// don't claim — required for anything beyond the Default Media + /// Receiver's `media`/`receiver` namespaces, e.g. Cast Streaming's + /// OFFER/ANSWER exchange. + pub fn connect_app(target: &CastDevice, app: CastDeviceApp) -> Result<(Self, MediaEvents, RawMessages)> { + let device: RustCastDevice<'static> = + RustCastDevice::connect_without_host_verification(target.host.clone(), target.port) + .map_err(|e| anyhow::anyhow!("{e}")) + .with_context(|| format!("failed to connect to {} ({}:{})", target.name, target.host, target.port))?; + + device + .connection + .connect("receiver-0") + .map_err(|e| anyhow::anyhow!("{e}")) + .context("failed to open the receiver-0 connection")?; + + let launched = device + .receiver + .launch_app(&app) + .map_err(|e| anyhow::anyhow!("{e}")) + .with_context(|| format!("failed to launch app {app:?}"))?; + + device + .connection + .connect(launched.transport_id.as_str()) + .map_err(|e| anyhow::anyhow!("{e}")) + .context("failed to open the app transport connection")?; + + let (command_tx, command_rx) = mpsc::channel(); + let (media_tx, media_rx) = mpsc::channel(); + let (raw_tx, raw_rx) = mpsc::channel(); + + let transport_id = launched.transport_id.clone(); + thread::spawn(move || { + run_io_loop(device, launched.transport_id, launched.session_id, command_rx, media_tx, raw_tx) + }); + + Ok(( + Self { command_tx, transport_id }, + MediaEvents { rx: media_rx }, + RawMessages { rx: raw_rx }, + )) + } + + /// The launched app's transport id — the CASTV2 destination id to use + /// with [`Self::send_raw_message`] and, equivalently, as the + /// `receiver_id` a [`crate::caststream::CastStreamSender`] targets. + pub fn transport_id(&self) -> &str { + &self.transport_id + } + + /// Sends `message` on `namespace` to the launched app's transport id. + /// Blocks until the session's io thread has handed it off to + /// `rust_cast` (not until any reply — Cast Streaming's ANSWER, for + /// example, arrives later as a [`RawMessage`] on the [`RawMessages`] + /// stream, not as this call's return value). + pub fn send_raw_message(&self, namespace: &str, message: &str) -> Result<()> { + let (reply_tx, reply_rx) = mpsc::channel(); + self.command_tx + .send(Command::SendRaw { + namespace: namespace.to_string(), + message: message.to_string(), + reply: reply_tx, + }) + .map_err(|_| anyhow::anyhow!("cast session io thread has already ended"))?; + reply_rx + .recv() + .map_err(|_| anyhow::anyhow!("cast session io thread ended before replying to send_raw_message"))? + } + + /// Loads `content_url` for playback. `content_type` is the MIME type + /// (e.g. `"video/mp4"` for a one-shot file, `"application/vnd.apple.mpegurl"` + /// for an HLS stream). Blocks until the device acknowledges the load or + /// the session's io thread ends. + pub fn load(&self, content_url: &str, content_type: &str, stream_type: StreamType) -> Result { + let (reply_tx, reply_rx) = mpsc::channel(); + self.command_tx + .send(Command::Load { + content_url: content_url.to_string(), + content_type: content_type.to_string(), + stream_type, + reply: reply_tx, + }) + .map_err(|_| anyhow::anyhow!("cast session io thread has already ended"))?; + reply_rx + .recv() + .map_err(|_| anyhow::anyhow!("cast session io thread ended before replying to load"))? + } + + /// Stops the receiver app and disconnects, ending the session (and its + /// io thread) cleanly. Without this, the TV is left showing a frozen + /// last frame indefinitely after the sender process exits — there is no + /// `Drop` impl doing this automatically because it needs a round trip + /// with the device that can fail, and silently swallowing that on drop + /// would hide exactly the kind of failure this project has already lost + /// a lot of time chasing blind. + pub fn stop(&self) -> Result<()> { + let (reply_tx, reply_rx) = mpsc::channel(); + if self.command_tx.send(Command::Stop { reply: reply_tx }).is_err() { + return Ok(()); // io thread already ended — nothing left to stop + } + reply_rx + .recv() + .map_err(|_| anyhow::anyhow!("cast session io thread ended before replying to stop"))? + } +} + +/// Owns the device connection for the session's lifetime, on its own +/// thread. Interleaves servicing queued commands (each a blocking +/// request/response round trip via `rust_cast`'s internals — safe here +/// since this is the only thread ever calling into `device`) with draining +/// unsolicited messages (heartbeat ping -> pong, media status -> forwarded +/// to `media_tx`). +/// +/// Commands are only picked up between `device.receive()` calls, so +/// worst-case latency to service one is bounded by how often the device +/// pushes a message — in practice the receiver's own heartbeat ping (every +/// few seconds), not "forever": `rust_cast` gives no way to set a read +/// timeout on the underlying stream to poll more eagerly than that. +fn run_io_loop( + device: RustCastDevice<'static>, + transport_id: String, + session_id: String, + command_rx: mpsc::Receiver, + media_tx: mpsc::Sender, + raw_tx: mpsc::Sender, +) { + loop { + match command_rx.try_recv() { + Ok(Command::Load { content_url, content_type, stream_type, reply }) => { + let media = Media { + content_id: content_url, + stream_type, + content_type, + metadata: None, + duration: None, + }; + let result = device + .media + .load(transport_id.as_str(), session_id.as_str(), &media) + .map_err(|e| anyhow::anyhow!("{e}")) + .context("failed to load media"); + let _ = reply.send(result); + } + Ok(Command::SendRaw { namespace, message, reply }) => { + let result = device + .send_message(&namespace, transport_id.as_str(), &message) + .map_err(|e| anyhow::anyhow!("{e}")) + .context("failed to send raw message"); + let _ = reply.send(result); + } + Ok(Command::Stop { reply }) => { + let result = device + .receiver + .stop_app(session_id.as_str()) + .map_err(|e| anyhow::anyhow!("{e}")) + .context("failed to stop the receiver app"); + let _ = device.connection.disconnect(transport_id.as_str()); + let _ = reply.send(result); + return; // session over — end the io thread + } + Err(mpsc::TryRecvError::Empty) => {} + Err(mpsc::TryRecvError::Disconnected) => return, // every CastSession clone dropped + } + + match device.receive() { + // Only pong an actual PING — matching every `Heartbeat(_)` + // variant (as a previous version did) would also fire on PONGs, + // which is harmless only by accident today since nothing here + // sends its own PING yet. + Ok(ChannelMessage::Heartbeat(HeartbeatResponse::Ping)) => { + if device.heartbeat.pong().is_err() { + return; + } + } + Ok(ChannelMessage::Heartbeat(_)) => {} + Ok(ChannelMessage::Media(media_response)) => { + let _ = media_tx.send(media_response); // no listener is fine — nobody's watching + } + Ok(ChannelMessage::Raw(msg)) => { + if let CastMessagePayload::String(message) = msg.payload { + let _ = raw_tx.send(RawMessage { + source_id: msg.source, + namespace: msg.namespace, + message, + }); + } + // Binary payloads are silently dropped — see `RawMessage`'s + // doc comment for why that's fine for this project's usage. + } + Ok(_) => {} + Err(e) => { + tracing::debug!(error = %e, "cast session io loop ending: receive error"); + return; + } + } + } +} diff --git a/breadcast-core/src/caststream.rs b/breadcast-core/src/caststream.rs new file mode 100644 index 0000000..9d2b076 --- /dev/null +++ b/breadcast-core/src/caststream.rs @@ -0,0 +1,292 @@ +//! Safe wrapper over `breadcast-caststream-sys`'s raw FFI to the vendored +//! openscreen Cast Streaming sender — the same low-latency mirroring +//! protocol Chrome's tab/desktop casting uses (unlike [`crate::cast_sender`]'s +//! HLS approach, which targets the Default Media Receiver instead). See +//! `breadcast-caststream-sys/vendor/openscreen/PATCHES.md` for how the +//! vendored C++ this wraps was built. +//! +//! This does *not* replicate [`CastSession`](crate::cast_sender::CastSession)'s +//! own single-io-thread actor pattern internally — the underlying C++ already +//! runs its own dedicated TaskRunner/networking threads (see `facade.h`'s +//! threading contract), so every method here just marshals across FFI rather +//! than through a Rust-owned loop. What *does* need a Rust-side thread is +//! draining [`CastStreamEvents`] and forwarding [`CastStreamEvent::OutboundMessage`] +//! over the existing CASTV2 connection — see `cast_stream_test.rs` for the +//! intended pattern (pump events on one thread, call `on_message`/ +//! `enqueue_frame` from others). + +use std::ffi::c_void; +use std::os::raw::c_char; +use std::sync::mpsc; + +use anyhow::{Result, bail}; +use breadcast_caststream_sys::{ + self as sys, breadcast_caststream_sender_create, breadcast_caststream_sender_destroy, + breadcast_caststream_sender_enqueue_frame, breadcast_caststream_sender_estimated_bandwidth_bps, + breadcast_caststream_sender_needs_key_frame, breadcast_caststream_sender_negotiate, + breadcast_caststream_sender_on_message, +}; + +/// The Cast Streaming ("Mirroring") receiver app id, pre-installed on every +/// Chromecast/Google TV — distinct from [`rust_cast::channels::receiver::CastDeviceApp::DefaultMediaReceiver`]'s +/// `CC1AD845`, which is what [`crate::cast_sender::CastSession`] launches for +/// the HLS path. +pub const MIRRORING_APP_ID: &str = "0F5096E8"; + +/// The CASTV2 namespace Cast Streaming's OFFER/ANSWER exchange runs on. +pub const WEBRTC_NAMESPACE: &str = "urn:x-cast:com.google.cast.webrtc"; + +#[derive(Debug, Clone, Copy)] +pub struct VideoParams { + pub width: i32, + pub height: i32, + pub max_bitrate_bps: i32, + pub max_frame_rate_numerator: i32, + pub max_frame_rate_denominator: i32, +} + +impl Default for VideoParams { + fn default() -> Self { + Self { + width: 1920, + height: 1080, + max_bitrate_bps: 8_000_000, + max_frame_rate_numerator: 30, + max_frame_rate_denominator: 1, + } + } +} + +/// Events pushed from the underlying C++ TaskRunner thread — see this +/// module's doc comment on why a Rust-owned pump loop is still needed even +/// though the FFI layer runs its own threads. +#[derive(Debug)] +pub enum CastStreamEvent { + /// The C++ side needs this JSON `message` sent to `destination_id` on + /// [`WEBRTC_NAMESPACE`] over the existing CASTV2 connection (e.g. the + /// OFFER). The caller is expected to do that via + /// `rust_cast::CastDevice::send_message` (see the `send_message` patch + /// documented in `vendor/rust_cast-0.21.0/PATCHES.md`). + OutboundMessage { destination_id: String, message: String }, + /// OFFER/ANSWER negotiation succeeded; `enqueue_frame` will now accept + /// frames. + Negotiated, + /// A negotiation or session error occurred. + Error(String), + /// The receiver reported picture loss and wants a key frame ASAP (also + /// obtainable via the pull-style [`CastStreamSender::needs_key_frame`]). + PictureLost, +} + +struct CallbackContext { + events_tx: mpsc::Sender, +} + +/// A live Cast Streaming sender session. See the module doc comment for the +/// threading model. +pub struct CastStreamSender { + raw: *mut sys::CastStreamSender, + // Kept alive for `raw`'s lifetime -- its address is the FFI `user_data` + // every callback trampoline below casts back. Never read directly + // through this field; the callbacks access it via the raw pointer, so + // this exists purely to own the allocation and free it (after + // `destroy()`, in `Drop`) rather than leak it. + _context: Box, +} + +// Safety: the underlying C++ handle has no thread-affinity for the FFI +// entry points themselves (see facade.h's threading contract) -- every +// `breadcast_caststream_sender_*` call internally marshals onto the +// TaskRunner thread via `TaskRunner::PostTask`, which is documented +// thread-safe regardless of caller thread. All methods below take `&self` +// only (no interior mutation outside that marshaling), so concurrent calls +// from multiple threads sharing an `Arc` are as safe as +// they are from a single thread -- hence `Sync` too, not just `Send`. +unsafe impl Send for CastStreamSender {} +unsafe impl Sync for CastStreamSender {} + +impl CastStreamSender { + /// Starts a Cast Streaming session targeting `remote_ip` (the same IP + /// `rust_cast` already connected to for the CASTV2 control channel). + /// `local_source_id`/`receiver_id` are the CASTV2 source/destination IDs + /// to use on [`WEBRTC_NAMESPACE`] -- `receiver_id` should be the + /// launched Mirroring app's `transport_id` (the same id + /// `connection`/`media` channels already target), matching how every + /// other namespace conversation with a launched app is addressed. + /// + /// Returns the sender plus a receiver for [`CastStreamEvent`]s -- drain + /// it on a dedicated thread; `OutboundMessage` events in particular need + /// prompt forwarding for negotiation to make progress. + pub fn start( + remote_ip: &str, + local_source_id: &str, + receiver_id: &str, + params: VideoParams, + ) -> Result<(Self, mpsc::Receiver)> { + let (events_tx, events_rx) = mpsc::channel(); + let context = Box::into_raw(Box::new(CallbackContext { events_tx })); + + let raw = unsafe { + breadcast_caststream_sender_create( + remote_ip.as_ptr() as *const c_char, + remote_ip.len(), + local_source_id.as_ptr() as *const c_char, + local_source_id.len(), + receiver_id.as_ptr() as *const c_char, + receiver_id.len(), + params.width, + params.height, + params.max_bitrate_bps, + params.max_frame_rate_numerator, + params.max_frame_rate_denominator, + context as *mut c_void, + post_message_trampoline, + on_negotiated_trampoline, + on_error_trampoline, + on_picture_lost_trampoline, + ) + }; + + if raw.is_null() { + // SAFETY: `context` was created by the `Box::into_raw` above and + // has not been handed to any live C++ object (create() failed + // before storing it anywhere), so reclaiming and dropping it + // here is the only way to avoid leaking it. + drop(unsafe { Box::from_raw(context) }); + bail!("breadcast_caststream_sender_create failed (invalid remote_ip?)"); + } + + // SAFETY: `context` was created by `Box::into_raw` immediately + // above and its address was just handed to the C++ side as + // `user_data` -- reconstructing the `Box` here doesn't move or free + // the underlying allocation (only dropping it would), so the + // pointer C++ holds stays valid for as long as this `Box` lives, + // i.e. until `Drop` runs (after `destroy()`, see below). + let context = unsafe { Box::from_raw(context) }; + + Ok((Self { raw, _context: context }, events_rx)) + } + + /// Sends the OFFER and begins waiting for an ANSWER (delivered via + /// [`Self::on_message`]). Completion is reported as a + /// [`CastStreamEvent::Negotiated`] or [`CastStreamEvent::Error`] on the + /// event receiver returned by [`Self::start`]. + pub fn negotiate(&self) { + unsafe { breadcast_caststream_sender_negotiate(self.raw) }; + } + + /// Delivers a message received on [`WEBRTC_NAMESPACE`] (e.g. the + /// receiver's ANSWER) into the session. + pub fn on_message(&self, source_id: &str, message_namespace: &str, message: &str) { + unsafe { + breadcast_caststream_sender_on_message( + self.raw, + source_id.as_ptr() as *const c_char, + source_id.len(), + message_namespace.as_ptr() as *const c_char, + message_namespace.len(), + message.as_ptr() as *const c_char, + message.len(), + ); + } + } + + /// Enqueues one encoded video access unit (Annex-B H.264) for sending. + /// `capture_time_us` only needs to be monotonically increasing and + /// proportional to real elapsed time between frames -- it does not need + /// to be wall-clock-accurate. + /// + /// Returns an error if the session isn't negotiated yet or the frame + /// was rejected under backpressure; callers should treat the latter as + /// a dropped frame, not a fatal condition (see + /// [`Self::needs_key_frame`]/[`Self::estimated_bandwidth_bps`] for how + /// to react). + pub fn enqueue_frame(&self, data: &[u8], is_key_frame: bool, capture_time_us: i64) -> Result<()> { + let result = unsafe { + breadcast_caststream_sender_enqueue_frame( + self.raw, + data.as_ptr(), + data.len(), + is_key_frame as i32, + capture_time_us, + ) + }; + if result != 0 { + bail!("frame not enqueued (session not negotiated yet)"); + } + Ok(()) + } + + /// True if the receiver wants a key frame as soon as possible. Cheap to + /// poll frequently (e.g. once per captured frame, before encoding it). + pub fn needs_key_frame(&self) -> bool { + unsafe { breadcast_caststream_sender_needs_key_frame(self.raw) != 0 } + } + + /// Best-effort current bandwidth estimate in bits per second, meant to + /// drive the video encoder's target bitrate -- this vendored subset of + /// openscreen only does flow control, not congestion control. Cheap to + /// poll frequently. + pub fn estimated_bandwidth_bps(&self) -> i32 { + unsafe { breadcast_caststream_sender_estimated_bandwidth_bps(self.raw) } + } +} + +impl Drop for CastStreamSender { + fn drop(&mut self) { + // Blocks until the C++ side's threads stop -- after this returns, + // no more callbacks will fire, so it's safe for `_context` to be + // freed right after (implicitly, as this struct finishes dropping). + unsafe { breadcast_caststream_sender_destroy(self.raw) }; + } +} + +unsafe fn context_from_user_data<'a>(user_data: *mut c_void) -> &'a CallbackContext { + // SAFETY: every callback below is only ever invoked by the C++ facade + // with the exact `user_data` pointer passed into `sender_create`, which + // is `_context`'s address for the lifetime of the owning + // `CastStreamSender` (see its field doc comment) -- and per facade.h's + // threading contract, no callback fires after `sender_destroy` returns, + // which is also the last point `_context` could be dropped. + unsafe { &*(user_data as *const CallbackContext) } +} + +unsafe fn str_from_raw_parts<'a>(ptr: *const c_char, len: usize) -> std::borrow::Cow<'a, str> { + // SAFETY: every callback below documents (matching facade.h) that these + // buffers are borrowed and valid only for the duration of the call -- + // this is called synchronously within that window, and the result is + // copied (via `.into_owned()` at each call site) before returning. + let bytes = unsafe { std::slice::from_raw_parts(ptr as *const u8, len) }; + String::from_utf8_lossy(bytes) +} + +extern "C" fn post_message_trampoline( + user_data: *mut c_void, + destination_id: *const c_char, + destination_id_len: usize, + _message_namespace: *const c_char, + _message_namespace_len: usize, + message: *const c_char, + message_len: usize, +) { + let ctx = unsafe { context_from_user_data(user_data) }; + let destination_id = unsafe { str_from_raw_parts(destination_id, destination_id_len) }.into_owned(); + let message = unsafe { str_from_raw_parts(message, message_len) }.into_owned(); + let _ = ctx.events_tx.send(CastStreamEvent::OutboundMessage { destination_id, message }); +} + +extern "C" fn on_negotiated_trampoline(user_data: *mut c_void) { + let ctx = unsafe { context_from_user_data(user_data) }; + let _ = ctx.events_tx.send(CastStreamEvent::Negotiated); +} + +extern "C" fn on_error_trampoline(user_data: *mut c_void, message: *const c_char, message_len: usize) { + let ctx = unsafe { context_from_user_data(user_data) }; + let message = unsafe { str_from_raw_parts(message, message_len) }.into_owned(); + let _ = ctx.events_tx.send(CastStreamEvent::Error(message)); +} + +extern "C" fn on_picture_lost_trampoline(user_data: *mut c_void) { + let ctx = unsafe { context_from_user_data(user_data) }; + let _ = ctx.events_tx.send(CastStreamEvent::PictureLost); +} diff --git a/breadcast-core/src/device.rs b/breadcast-core/src/device.rs new file mode 100644 index 0000000..613f49b --- /dev/null +++ b/breadcast-core/src/device.rs @@ -0,0 +1,14 @@ +use serde::{Deserialize, Serialize}; + +/// A Chromecast / Google TV device discovered on the LAN via mDNS. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CastDevice { + /// Stable device id from the `id=` TXT record. + pub id: String, + /// Friendly name from the `fn=` TXT record (e.g. "Living Room TV"). + pub name: String, + /// Model name from the `md=` TXT record (e.g. "Chromecast"). + pub model: String, + pub host: String, + pub port: u16, +} diff --git a/breadcast-core/src/discovery.rs b/breadcast-core/src/discovery.rs new file mode 100644 index 0000000..35b8cda --- /dev/null +++ b/breadcast-core/src/discovery.rs @@ -0,0 +1,186 @@ +use std::collections::HashMap; +use std::net::IpAddr; +use std::time::{Duration, Instant}; + +use anyhow::{Context, Result}; +use mdns_sd::{ScopedIp, ServiceDaemon, ServiceEvent}; +use tokio::sync::mpsc; +use tracing::{debug, warn}; + +use crate::device::CastDevice; + +/// How long a resolved address is trusted without being re-seen in a later +/// `ServiceResolved` event. mDNS re-resolves well inside this window in +/// normal operation, so an address that goes quiet for this long is more +/// likely stale (DHCP lease change, interface removed) than merely unlucky +/// timing. +const ADDRESS_STALE_AFTER: Duration = Duration::from_secs(300); + +/// Picks the best address to connect to a resolved device on: routable IPv4 +/// first (works unambiguously with rust_cast's TLS connect and with URLs +/// embedded in Cast media-load requests), falling back to a non-link-local +/// IPv6 address. Link-local IPv6 (`fe80::...%wlan0`) is deliberately last +/// resort — its zone-id suffix doesn't round-trip through `IpAddr`'s +/// `FromStr`/`Display`, so it can silently become unconnectable downstream. +/// +/// `addresses` is sorted most-recently-seen first (with a stable tie-break) +/// before picking, rather than iterated in whatever order a `HashSet` (or a +/// `Vec` built from one) happens to produce — with a device that resolves +/// to two IPv4 addresses (e.g. wired + wireless, or a DHCP lease change), +/// an unordered choice can silently pick a stale/unreachable one, and pick +/// a *different* one across otherwise-identical runs. +fn pick_address(addresses: &[(ScopedIp, Instant)]) -> Option { + let now = Instant::now(); + let mut candidates: Vec<&(ScopedIp, Instant)> = addresses + .iter() + .filter(|(_, seen)| now.duration_since(*seen) < ADDRESS_STALE_AFTER) + .collect(); + candidates.sort_by(|a, b| { + b.1.cmp(&a.1) // most-recently-seen first + .then_with(|| a.0.to_ip_addr().to_string().cmp(&b.0.to_ip_addr().to_string())) + }); + + candidates + .iter() + .find(|(ip, _)| ip.is_ipv4()) + .or_else(|| { + candidates.iter().find(|(ip, _)| match ip.to_ip_addr() { + IpAddr::V6(v6) => !v6.is_unicast_link_local(), + IpAddr::V4(_) => false, + }) + }) + .or_else(|| candidates.first()) + .map(|(ip, _)| ip.to_ip_addr()) +} + +const SERVICE_TYPE: &str = "_googlecast._tcp.local."; + +/// A change in the set of Cast devices visible on the LAN. +/// +/// `Found` fires on every mDNS re-resolution of a device (e.g. once per +/// network interface, or periodically as records refresh), not just the +/// first sighting — consumers should treat it as an upsert keyed by +/// `CastDevice::id`, not an append-only log. +#[derive(Debug, Clone)] +pub enum DiscoveryEvent { + Found(CastDevice), + Lost { id: String }, +} + +/// Browses `_googlecast._tcp.local.` on a background thread and forwards +/// found/lost devices over an unbounded channel. Dropping the returned +/// `Discovery` stops the underlying mDNS daemon (via an explicit `Drop` +/// impl below — `mdns_sd::ServiceDaemon` has none of its own, so without +/// it every dropped `Discovery` would leak its daemon thread and 5353 +/// multicast socket for the rest of the process's life). +pub struct Discovery { + daemon: ServiceDaemon, +} + +impl Drop for Discovery { + fn drop(&mut self) { + let _ = self.daemon.shutdown(); + } +} + +impl Discovery { + /// Starts browsing and returns the handle plus a receiver of events. + /// `id=` TXT records are used to dedupe: a `ServiceResolved` for an + /// already-known id is treated as an update, not a duplicate `Found`. + pub fn start() -> Result<(Self, mpsc::UnboundedReceiver)> { + let daemon = ServiceDaemon::new().context("failed to start mDNS daemon")?; + let browse_rx = daemon + .browse(SERVICE_TYPE) + .context("failed to browse _googlecast._tcp.local.")?; + + let (tx, rx) = mpsc::unbounded_channel(); + + // mdns-sd's receiver is a blocking `flume` channel, so it needs its + // own OS thread rather than a tokio task; forwarding into an + // unbounded tokio channel is a non-blocking send from here. + std::thread::spawn(move || { + // fullname -> last-known device id, so a ServiceRemoved (which + // only carries the fullname) can still emit the right Lost{id}. + let mut fullname_to_id: HashMap = HashMap::new(); + // fullname -> every address seen for it, with a last-seen + // timestamp each. mDNS resolves progressively: the first + // ServiceResolved for a device often carries only a link-local + // IPv6 address, with the routable IPv4/global-IPv6 address + // arriving in a later event for the same fullname. Accumulating + // (not replacing) means `pick_address` always chooses from + // everything seen so far, not just whatever happened to be in + // the latest packet — the timestamp lets it also prefer the + // freshest address and ignore ones that have gone stale. + let mut fullname_to_addrs: HashMap> = HashMap::new(); + + while let Ok(event) = browse_rx.recv() { + match event { + ServiceEvent::ServiceResolved(info) => { + let Some(id) = info.get_property_val_str("id").map(str::to_string) else { + warn!(fullname = %info.get_fullname(), "cast device missing id= TXT record, skipping"); + continue; + }; + let name = info + .get_property_val_str("fn") + .unwrap_or_else(|| info.get_hostname()) + .to_string(); + let model = info + .get_property_val_str("md") + .unwrap_or("Chromecast") + .to_string(); + + let addrs = fullname_to_addrs + .entry(info.get_fullname().to_string()) + .or_default(); + let now = std::time::Instant::now(); + for addr in info.get_addresses().iter().cloned() { + match addrs.iter_mut().find(|(seen, _)| *seen == addr) { + Some(entry) => entry.1 = now, + None => addrs.push((addr, now)), + } + } + + let Some(host) = pick_address(addrs).map(|ip| ip.to_string()) else { + warn!(%id, "cast device resolved with no usable addresses, skipping"); + continue; + }; + + fullname_to_id.insert(info.get_fullname().to_string(), id.clone()); + + let device = CastDevice { + id, + name, + model, + host, + port: info.get_port(), + }; + debug!(?device, "cast device found"); + if tx.send(DiscoveryEvent::Found(device)).is_err() { + break; // receiver dropped, stop the thread + } + } + ServiceEvent::ServiceRemoved(_ty, fullname) => { + fullname_to_addrs.remove(&fullname); + if let Some(id) = fullname_to_id.remove(&fullname) { + debug!(%id, "cast device lost"); + if tx.send(DiscoveryEvent::Lost { id }).is_err() { + break; + } + } + } + _ => {} + } + } + }); + + Ok((Self { daemon }, rx)) + } + + /// Stops the mDNS daemon and its browse thread. + pub fn stop(self) -> Result<()> { + self.daemon + .shutdown() + .context("failed to shut down mDNS daemon")?; + Ok(()) + } +} diff --git a/breadcast-core/src/dlna/device.rs b/breadcast-core/src/dlna/device.rs new file mode 100644 index 0000000..a7021fa --- /dev/null +++ b/breadcast-core/src/dlna/device.rs @@ -0,0 +1,16 @@ +/// A DLNA/UPnP media renderer discovered on the LAN via SSDP — the class of +/// device Windows' own "Cast to Device" (Win+K) targets, and what most +/// non-Chromecast smart TVs (Samsung, LG/Tizen, Sony) expose alongside or +/// instead of Google Cast. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DlnaDevice { + /// Human-readable name from the device's `friendlyName` element. + pub friendly_name: String, + /// The device description XML URL (e.g. `http://192.168.1.50:1400/desc.xml`). + /// This, not a separately-tracked id, is what uniquely identifies a UPnP + /// device here — UPnP itself has a UDN concept, but reading it requires + /// enabling `rupnp`'s `full_device_spec` feature for that one field, + /// while the description URL is already available for free and is + /// exactly what `rupnp::Device` itself keys its own identity on. + pub url: String, +} diff --git a/breadcast-core/src/dlna/discovery.rs b/breadcast-core/src/dlna/discovery.rs new file mode 100644 index 0000000..9e44a07 --- /dev/null +++ b/breadcast-core/src/dlna/discovery.rs @@ -0,0 +1,131 @@ +use std::collections::{HashMap, HashSet}; +use std::time::Duration; + +use rupnp::ssdp::SearchTarget; +use tokio::sync::mpsc; +use tracing::{debug, warn}; + +use super::device::DlnaDevice; +use super::AV_TRANSPORT; + +/// How often to re-issue an SSDP search burst. Unlike mDNS (continuous +/// multicast browsing via `mdns-sd` in [`crate::discovery`]), SSDP has no +/// "subscribe and get pushed updates" primitive exposed by `rupnp` — +/// discovery here is "ask, wait [`SEARCH_TIMEOUT`], collect whoever +/// answered," repeated on this interval, rather than event-driven. +const POLL_INTERVAL: Duration = Duration::from_secs(30); + +/// How long to wait for M-SEARCH responses on each poll. SSDP devices are +/// expected to jitter their response within a device-chosen window, so this +/// needs to be more than an instant, but this is still per-poll latency +/// before newly-found devices are reported. +const SEARCH_TIMEOUT: Duration = Duration::from_secs(3); + +/// A device not reconfirmed within this many consecutive polls is reported +/// lost. More than one (rather than declaring it gone after a single missed +/// poll) tolerates an occasional dropped UDP response — SSDP runs over +/// unreliable multicast, so one missed reply is routine, not a signal the +/// device actually left the network. +const MISSED_POLLS_BEFORE_LOST: u32 = 2; + +/// A change in the set of DLNA renderers visible on the LAN. Mirrors +/// [`crate::discovery::DiscoveryEvent`]'s shape for consistency between the +/// two casting protocols, though the underlying mechanism differs (see +/// [`DlnaDiscovery`]'s docs). +#[derive(Debug, Clone)] +pub enum DlnaDiscoveryEvent { + Found(DlnaDevice), + Lost { url: String }, +} + +/// Periodically searches for UPnP `AVTransport` services (i.e. media +/// renderers — smart TVs, DLNA-capable receivers, and what Windows' own +/// "Cast to Device" targets) on the LAN via SSDP, and forwards found/lost +/// devices over a channel. +/// +/// Runs as a background `tokio` task rather than the dedicated OS thread +/// [`crate::discovery::Discovery`] needs — `mdns-sd`'s browse channel is a +/// blocking `flume` receiver with no async-friendly interface, but `rupnp` +/// and `ssdp-client` are natively `tokio`-async, so a plain spawned task is +/// both sufficient and more idiomatic here. Dropping the returned +/// `DlnaDiscovery` aborts that task. +pub struct DlnaDiscovery { + task: tokio::task::JoinHandle<()>, +} + +impl Drop for DlnaDiscovery { + fn drop(&mut self) { + self.task.abort(); + } +} + +impl DlnaDiscovery { + /// Starts polling and returns the handle plus a receiver of events. + pub fn start() -> (Self, mpsc::UnboundedReceiver) { + let (tx, rx) = mpsc::unbounded_channel(); + + let task = tokio::spawn(async move { + // description URL -> (device, consecutive polls since last confirmed) + let mut known: HashMap = HashMap::new(); + + loop { + let search_target = SearchTarget::URN(AV_TRANSPORT); + match rupnp::discover(&search_target, SEARCH_TIMEOUT, None).await { + Ok(stream) => { + use futures_util::StreamExt; + let mut stream = std::pin::pin!(stream); + let mut confirmed: HashSet = HashSet::new(); + + while let Some(result) = stream.next().await { + let device = match result { + Ok(device) => device, + Err(e) => { + warn!(error = %e, "failed to resolve a discovered UPnP device, skipping"); + continue; + } + }; + + let url = device.url().to_string(); + confirmed.insert(url.clone()); + + if let Some(entry) = known.get_mut(&url) { + entry.1 = 0; + continue; + } + + let dlna_device = DlnaDevice { + friendly_name: device.friendly_name().to_string(), + url: url.clone(), + }; + known.insert(url, (dlna_device.clone(), 0)); + debug!(?dlna_device, "DLNA renderer found"); + if tx.send(DlnaDiscoveryEvent::Found(dlna_device)).is_err() { + return; // receiver dropped, stop polling + } + } + + known.retain(|url, (_, missed)| { + if confirmed.contains(url) { + true + } else { + *missed += 1; + if *missed >= MISSED_POLLS_BEFORE_LOST { + debug!(%url, "DLNA renderer lost"); + let _ = tx.send(DlnaDiscoveryEvent::Lost { url: url.clone() }); + false + } else { + true + } + } + }); + } + Err(e) => warn!(error = %e, "SSDP search for AVTransport devices failed"), + } + + tokio::time::sleep(POLL_INTERVAL).await; + } + }); + + (Self { task }, rx) + } +} diff --git a/breadcast-core/src/dlna/mod.rs b/breadcast-core/src/dlna/mod.rs new file mode 100644 index 0000000..26b2199 --- /dev/null +++ b/breadcast-core/src/dlna/mod.rs @@ -0,0 +1,23 @@ +//! DLNA/UPnP media-renderer casting: discovery via SSDP, control via the +//! `AVTransport` service's SOAP actions. This is the protocol behind most +//! non-Chromecast smart TVs (Samsung, LG/Tizen, Sony) and behind Windows' +//! own "Cast to Device" (Win+K) flyout — a different device population +//! than [`crate::cast_sender`]'s Cast V2/CASTV2, not an alternate path to +//! the same devices. +//! +//! Reuses the rest of breadcast-core as-is: the same [`crate::capture`], +//! [`crate::pipeline`], and [`crate::http_server`] produce the HLS stream a +//! [`DlnaSession`] is handed a URL to — only discovery and the +//! device-control protocol differ from the Cast path. + +mod device; +mod discovery; +mod session; + +use rupnp::ssdp::URN; + +pub use device::DlnaDevice; +pub use discovery::{DlnaDiscovery, DlnaDiscoveryEvent}; +pub use session::DlnaSession; + +const AV_TRANSPORT: URN = URN::service("schemas-upnp-org", "AVTransport", 1); diff --git a/breadcast-core/src/dlna/session.rs b/breadcast-core/src/dlna/session.rs new file mode 100644 index 0000000..2447ce3 --- /dev/null +++ b/breadcast-core/src/dlna/session.rs @@ -0,0 +1,122 @@ +use anyhow::{Context, Result}; +use rupnp::Service; + +use super::device::DlnaDevice; +use super::AV_TRANSPORT; + +/// A connected UPnP `AVTransport` control point for a single DLNA media +/// renderer. +/// +/// Unlike [`CastSession`](crate::CastSession), there is no persistent +/// connection or background thread to manage here: every UPnP action is an +/// independent SOAP-over-HTTP request, so `DlnaSession` is just a cheap, +/// `Clone`-able handle to the renderer's resolved control endpoint. +/// Concurrent `load()`/`stop()` calls are naturally safe — each is its own +/// HTTP request — with none of the single-socket message-demultiplexing +/// hazard `CastSession` has to run an actor thread to avoid for CASTV2. +#[derive(Clone)] +pub struct DlnaSession { + device_url: rupnp::http::Uri, + service: Service, +} + +impl DlnaSession { + /// Fetches `device`'s full description and resolves its `AVTransport` + /// service. Fails if the device no longer answers, or turns out not to + /// expose `AVTransport` after all — shouldn't happen given discovery + /// already searched for exactly that service, but a device's + /// description can in principle change between being found and being + /// connected to. + pub async fn connect(device: &DlnaDevice) -> Result { + let device_url: rupnp::http::Uri = device + .url + .parse() + .with_context(|| format!("invalid device description URL: {}", device.url))?; + + let full_device = rupnp::Device::from_url(device_url.clone()) + .await + .with_context(|| format!("failed to fetch device description from {}", device.url))?; + + let service = full_device + .find_service(&AV_TRANSPORT) + .with_context(|| format!("{} has no AVTransport service", device.friendly_name))? + .clone(); + + Ok(Self { device_url, service }) + } + + /// Sets `content_url` as the renderer's current transport URI and + /// starts playback. + /// + /// `content_url` is XML-escaped before being embedded in the SOAP + /// request body — in breadcast's own usage it's always a URL this + /// process generated itself (safe by construction), but nothing about + /// this function's signature guarantees that stays true for every + /// caller, and an unescaped `&` alone would produce malformed XML the + /// renderer would reject with no useful diagnostic. + pub async fn load(&self, content_url: &str) -> Result<()> { + let escaped = xml_escape(content_url); + let set_uri_payload = format!( + "0{escaped}" + ); + self.service + .action(&self.device_url, "SetAVTransportURI", &set_uri_payload) + .await + .context("SetAVTransportURI failed")?; + + self.service + .action(&self.device_url, "Play", "01") + .await + .context("Play failed")?; + + Ok(()) + } + + /// Stops playback. Unlike `CastSession::stop`, there's no persistent + /// session or launched app to tear down — this is just the `Stop` + /// action. + pub async fn stop(&self) -> Result<()> { + self.service + .action(&self.device_url, "Stop", "0") + .await + .context("Stop failed")?; + Ok(()) + } + + /// Polls `GetTransportInfo` and returns the renderer's own reported + /// `CurrentTransportState` (e.g. `"PLAYING"`, `"TRANSITIONING"`, + /// `"STOPPED"`, `"NO_MEDIA_PRESENT"`). + /// + /// This is a request/response poll, not a push subscription — + /// `AVTransport` does support UPnP eventing (`Service::subscribe` in + /// `rupnp`) for state pushed as it changes, but that needs a locally + /// bound HTTP callback listener, which is more machinery than a status + /// check is worth for now. A caller that wants live updates polls this + /// on an interval instead. + pub async fn transport_state(&self) -> Result { + let response = self + .service + .action(&self.device_url, "GetTransportInfo", "0") + .await + .context("GetTransportInfo failed")?; + response + .get("CurrentTransportState") + .cloned() + .context("GetTransportInfo response had no CurrentTransportState") + } +} + +fn xml_escape(input: &str) -> String { + let mut escaped = String::with_capacity(input.len()); + for c in input.chars() { + match c { + '&' => escaped.push_str("&"), + '<' => escaped.push_str("<"), + '>' => escaped.push_str(">"), + '"' => escaped.push_str("""), + '\'' => escaped.push_str("'"), + other => escaped.push(other), + } + } + escaped +} diff --git a/breadcast-core/src/examples/apple_hls_test.rs b/breadcast-core/src/examples/apple_hls_test.rs new file mode 100644 index 0000000..45545d5 --- /dev/null +++ b/breadcast-core/src/examples/apple_hls_test.rs @@ -0,0 +1,36 @@ +use breadcast_core::{CastSession, Discovery, DiscoveryEvent}; +use rust_cast::channels::media::{MediaResponse, StreamType}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + let (_d, mut events) = Discovery::start()?; + let device = loop { + let ev = tokio::time::timeout(std::time::Duration::from_secs(45), events.recv()) + .await + .map_err(|_| anyhow::anyhow!("timed out waiting for a matching device"))? + .ok_or_else(|| anyhow::anyhow!("discovery channel closed before a matching device was found"))?; + if let DiscoveryEvent::Found(d) = ev { + if d.name.to_lowercase().contains("master bedroom") && d.model.to_lowercase().contains("chromecast") && d.host.parse::().is_ok() { + break d; + } + } + }; + println!("Found {}", device.name); + let (session, media_events) = CastSession::connect(&device)?; + let stream_type = std::env::args().nth(1).unwrap_or_default(); + let stream_type = if stream_type == "live" { StreamType::Live } else { StreamType::Buffered }; + println!("Using stream_type={stream_type:?}"); + let status = session.load("http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8", "application/vnd.apple.mpegurl", stream_type)?; + println!("Load status: {status:#?}"); + std::thread::spawn(move || { + media_events.pump_with_media_callback(|m| match m { + MediaResponse::Status(s) => for e in &s.entries { println!(" player_state={:?} idle_reason={:?}", e.player_state, e.idle_reason); }, + MediaResponse::LoadFailed(f) => println!(" LOAD FAILED: {f:?}"), + other => println!(" other: {other:?}"), + }) + }); + tokio::time::sleep(std::time::Duration::from_secs(30)).await; + let _ = session.stop(); + Ok(()) +} diff --git a/breadcast-core/src/examples/capture_test.rs b/breadcast-core/src/examples/capture_test.rs new file mode 100644 index 0000000..69ca8a9 --- /dev/null +++ b/breadcast-core/src/examples/capture_test.rs @@ -0,0 +1,22 @@ +//! Phase 2 step 1: opens the portal's screen-cast picker and prints the +//! PipeWire node id it hands back. Run with: +//! cargo run -p breadcast-core --example capture_test +//! A system picker dialog should appear — pick a monitor or window. + +use breadcast_core::CaptureSession; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + + println!("Opening the portal screen-cast picker (look for a system dialog)..."); + let session = CaptureSession::start().await?; + println!("Got PipeWire video node id: {}", session.video_node_id()); + + println!("Holding the session open for 5s, then closing..."); + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + session.close().await?; + println!("Closed."); + + Ok(()) +} diff --git a/breadcast-core/src/examples/cast_stream_test.rs b/breadcast-core/src/examples/cast_stream_test.rs new file mode 100644 index 0000000..6ceec30 --- /dev/null +++ b/breadcast-core/src/examples/cast_stream_test.rs @@ -0,0 +1,202 @@ +//! Low-latency mirroring via Cast Streaming (the same protocol Chrome's +//! tab/desktop casting uses) instead of `mirror_test`'s HLS approach: no +//! HTTP server, no multi-second segment-buffering floor, and it targets the +//! Chromecast's built-in Mirroring receiver (app id `0F5096E8`) instead of +//! the Default Media Receiver. Run with: +//! cargo run -p breadcast-core --example cast_stream_test [name substring] +//! Defaults to "master bedroom" if no argument is given. + +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use breadcast_core::caststream::{CastStreamEvent, VideoParams, WEBRTC_NAMESPACE}; +use breadcast_core::net::local_lan_ip; +use breadcast_core::pipeline::{ + build_video_pipeline_for_streaming, pull_encoded_frame, request_key_frame, set_video_bitrate_kbps, +}; +use breadcast_core::{CastSession, CastStreamSender, Discovery, DiscoveryEvent}; +use gstreamer as gst; +use gstreamer::prelude::*; +use rust_cast::channels::receiver::CastDeviceApp; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + gst::init()?; + + let name_filter = std::env::args() + .nth(1) + .unwrap_or_else(|| "master bedroom".to_string()) + .to_lowercase(); + + let lan_ip = local_lan_ip()?; + println!("This machine's LAN IP: {lan_ip}"); + + println!("Looking for a Cast device matching \"{name_filter}\"..."); + let (_discovery, mut events) = Discovery::start()?; + let device = loop { + let event = tokio::time::timeout(Duration::from_secs(25), events.recv()) + .await + .map_err(|_| anyhow::anyhow!("timed out waiting for a device matching \"{name_filter}\""))? + .ok_or_else(|| anyhow::anyhow!("discovery channel closed"))?; + + if let DiscoveryEvent::Found(device) = event { + let name = device.name.to_lowercase(); + let model = device.model.to_lowercase(); + if name.contains(&name_filter) + && model.contains("chromecast") + && device.host.parse::().is_ok() + { + break device; + } + } + }; + println!("Found {} ({}) at {}:{}", device.name, device.model, device.host, device.port); + + println!("Opening the portal screen-cast picker (look for a system dialog)..."); + let capture = breadcast_core::CaptureSession::start().await?; + println!("Got PipeWire video node id: {}", capture.video_node_id()); + + let (pipeline, appsink, encoder) = build_video_pipeline_for_streaming(capture.video_node_id())?; + + // Watch the encode pipeline's own bus in the background -- see + // mirror_test.rs's identical block for why this matters. + { + let pipeline_watch = pipeline.clone(); + std::thread::spawn(move || { + match breadcast_core::pipeline::run_until_error_or_timeout(&pipeline_watch, gst::ClockTime::from_seconds(120)) { + Ok(outcome) => tracing::debug!(?outcome, "encode pipeline bus watcher ended"), + Err(e) => eprintln!("ENCODE PIPELINE ERROR: {e:?}"), + } + }); + } + + println!("Connecting and launching the Mirroring receiver ({} )...", breadcast_core::caststream::MIRRORING_APP_ID); + let (session, _media_events, raw_messages) = + CastSession::connect_app(&device, CastDeviceApp::Custom(breadcast_core::caststream::MIRRORING_APP_ID.to_string()))?; + println!("Mirroring receiver launched, transport_id={}", session.transport_id()); + + let (sender, stream_events) = CastStreamSender::start( + &device.host, + "sender-0", + session.transport_id(), + VideoParams::default(), + )?; + let sender = Arc::new(sender); + + // Forwards inbound webrtc-namespace messages (the ANSWER) from the + // existing CASTV2 connection into the Cast Streaming session. + let inbound_pump = { + let sender = sender.clone(); + std::thread::spawn(move || { + while let Some(msg) = raw_messages.recv() { + if msg.namespace == WEBRTC_NAMESPACE { + sender.on_message(&msg.source_id, &msg.namespace, &msg.message); + } + } + }) + }; + + let negotiated = Arc::new(AtomicBool::new(false)); + + // Forwards outbound webrtc-namespace messages (the OFFER) out over the + // existing CASTV2 connection, and tracks negotiation completion. + let outbound_pump = { + let session = session.clone(); + let negotiated = negotiated.clone(); + std::thread::spawn(move || { + while let Ok(event) = stream_events.recv() { + match event { + CastStreamEvent::OutboundMessage { message, .. } => { + if let Err(e) = session.send_raw_message(WEBRTC_NAMESPACE, &message) { + eprintln!("failed to send Cast Streaming message: {e:?}"); + } + } + CastStreamEvent::Negotiated => { + println!("Cast Streaming negotiated -- receiver is ready for frames."); + negotiated.store(true, Ordering::Release); + } + CastStreamEvent::Error(message) => { + eprintln!("Cast Streaming error: {message}"); + } + CastStreamEvent::PictureLost => { + println!("receiver reported picture loss"); + } + } + } + }) + }; + + println!("Sending OFFER..."); + sender.negotiate(); + + let deadline = std::time::Instant::now() + Duration::from_secs(10); + while !negotiated.load(Ordering::Acquire) && std::time::Instant::now() < deadline { + tokio::time::sleep(Duration::from_millis(50)).await; + } + if !negotiated.load(Ordering::Acquire) { + anyhow::bail!("never received an ANSWER (negotiation timed out after 10s)"); + } + + pipeline.set_state(gst::State::Playing)?; + println!("Pipeline playing. Mirroring for up to 60s -- check the TV."); + + let sender_for_frames = sender.clone(); + let frame_pump = std::thread::spawn(move || -> anyhow::Result<()> { + let mut last_bitrate_update = std::time::Instant::now(); + loop { + let Some((data, is_key_frame, capture_time_us)) = pull_encoded_frame(&appsink)? else { + return Ok(()); // EOS + }; + + if sender_for_frames.needs_key_frame() && !is_key_frame { + request_key_frame(&appsink); + } + + if let Err(e) = sender_for_frames.enqueue_frame(&data, is_key_frame, capture_time_us) { + tracing::debug!(error = ?e, "dropped a frame (not negotiated yet or backpressure)"); + } + + if last_bitrate_update.elapsed() >= Duration::from_secs(1) { + let bps = sender_for_frames.estimated_bandwidth_bps(); + // Leave headroom below the raw estimate for RTP/RTCP + // overhead and estimation noise, and never go below a + // usable floor. + let target_kbps = ((bps as f64 * 0.85) / 1000.0).max(500.0) as u32; + set_video_bitrate_kbps(&encoder, target_kbps); + last_bitrate_update = std::time::Instant::now(); + } + } + }); + + let mirror_duration = Duration::from_secs(60); + let mut elapsed = Duration::ZERO; + while elapsed < mirror_duration && !frame_pump.is_finished() && !outbound_pump.is_finished() { + tokio::time::sleep(Duration::from_secs(1)).await; + elapsed += Duration::from_secs(1); + } + + pipeline.set_state(gst::State::Null)?; + capture.close().await?; + if let Err(e) = session.stop() { + eprintln!("failed to cleanly stop the cast session: {e:?}"); + } + match frame_pump.join() { + Ok(Ok(())) => {} + Ok(Err(e)) => eprintln!("frame pump ended with an error: {e:?}"), + Err(panic) => eprintln!("frame pump thread panicked: {panic:?}"), + } + // `session.stop()` above ends the CastSession's io thread, which in turn + // closes the raw_messages/stream_events channels these two pump threads + // are blocked reading from -- so both should already be finishing. + if let Err(panic) = inbound_pump.join() { + eprintln!("inbound message pump thread panicked: {panic:?}"); + } + if let Err(panic) = outbound_pump.join() { + eprintln!("outbound message pump thread panicked: {panic:?}"); + } + println!("Stopped."); + + Ok(()) +} diff --git a/breadcast-core/src/examples/cast_test.rs b/breadcast-core/src/examples/cast_test.rs new file mode 100644 index 0000000..2e4135c --- /dev/null +++ b/breadcast-core/src/examples/cast_test.rs @@ -0,0 +1,95 @@ +//! Phase 3 smoke test: discovers a Cast device by name substring and casts a +//! sample video to it, to prove real CASTV2 device control ahead of building +//! the actual capture pipeline. +//! Run with: cargo run -p breadcast-core --example cast_test [name substring] +//! Defaults to "master bedroom" if no argument is given. + +use breadcast_core::{CastSession, Discovery, DiscoveryEvent}; +use rust_cast::channels::media::{MediaResponse, StreamType}; + +// Google's old gtv-videos-bucket sample assets (from the classic Cast SDK +// docs) now 403 — confirmed dead via `curl -I`, not a Cast-side issue. The +// 21MB samplelib.com/mp4/sample-30s.mp4 loaded but then the receiver killed +// the sender connection ("failed to fill whole buffer" / EOF) on both real +// devices tested — consistent with a non-"fast-start" MP4 (moov atom at the +// end) stalling the receiver's simple HTML5 video player. This one is small +// (788KB/10s) and known-good. +const SAMPLE_VIDEO: &str = "https://www.w3schools.com/html/mov_bbb.mp4"; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + + let name_filter = std::env::args() + .nth(1) + .unwrap_or_else(|| "master bedroom".to_string()) + .to_lowercase(); + + let (_discovery, mut events) = Discovery::start()?; + println!("Looking for a Cast device matching \"{name_filter}\"..."); + + let device = loop { + let event = tokio::time::timeout(std::time::Duration::from_secs(15), events.recv()) + .await + .map_err(|_| anyhow::anyhow!("timed out waiting for a device matching \"{name_filter}\""))? + .ok_or_else(|| anyhow::anyhow!("discovery channel closed"))?; + + if let DiscoveryEvent::Found(device) = event { + let name = device.name.to_lowercase(); + let model = device.model.to_lowercase(); + // Restrict to actual Chromecast-family receivers (excludes a + // TV's own "Smart TV" Cast platform, which shares the "TV" name + // prefix with its paired dongle in this household). + // + // Hold out for an IPv4 address specifically, not just + // non-link-local: mDNS resolution races between interfaces, and + // a device's *global* IPv6 address can arrive in a Found event + // before its IPv4 address does, or before its IPv4 address is + // even reachable (seen in practice: "No route to host" on a + // global IPv6 the LAN doesn't actually route). IPv4 is the safe + // default on this network; a later phase can add real + // reachability probing instead of an address-family heuristic. + if name.contains(&name_filter) + && model.contains("chromecast") + && device.host.parse::().is_ok() + { + break device; + } + } + }; + + println!( + "Found {} ({}) at {}:{} — connecting...", + device.name, device.model, device.host, device.port + ); + + let (session, media_events) = CastSession::connect(&device)?; + println!("Connected, Default Media Receiver launched. Loading sample video..."); + + let status = session.load(SAMPLE_VIDEO, "video/mp4", StreamType::Buffered)?; + println!("Receiver acknowledged the load: {status:#?}"); + println!("Watching player state for 90s (video is ~30s, but buffering can be slow)..."); + + let pump = std::thread::spawn(move || { + media_events.pump_with_media_callback(|media| { + if let MediaResponse::Status(status) = media { + for entry in &status.entries { + println!( + " player_state={:?} idle_reason={:?} current_time={:?} extended_status={:?}", + entry.player_state, entry.idle_reason, entry.current_time, + entry.extended_status.as_ref().map(|e| e.player_state) + ); + } + } + }); + }); + + tokio::time::sleep(std::time::Duration::from_secs(90)).await; + if let Err(e) = session.stop() { + eprintln!("failed to cleanly stop the cast session: {e:?}"); + } + let _ = pump.join(); // media_events channel is now closed (io thread ended), so this returns immediately + println!("Done."); + + Ok(()) +} diff --git a/breadcast-core/src/examples/discover.rs b/breadcast-core/src/examples/discover.rs new file mode 100644 index 0000000..db1359d --- /dev/null +++ b/breadcast-core/src/examples/discover.rs @@ -0,0 +1,25 @@ +//! Prints Chromecast/Google TV devices as they appear/disappear on the LAN. +//! Run with: cargo run -p breadcast-core --example discover + +use breadcast_core::{Discovery, DiscoveryEvent}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + + let (_discovery, mut events) = Discovery::start()?; + println!("Browsing for _googlecast._tcp.local. devices... (Ctrl+C to stop)"); + + while let Some(event) = events.recv().await { + match event { + DiscoveryEvent::Found(device) => { + println!("+ {} ({}) at {}:{} [{}]", device.name, device.model, device.host, device.port, device.id); + } + DiscoveryEvent::Lost { id } => { + println!("- {id}"); + } + } + } + + Ok(()) +} diff --git a/breadcast-core/src/examples/dlna_discover.rs b/breadcast-core/src/examples/dlna_discover.rs new file mode 100644 index 0000000..5fbbc56 --- /dev/null +++ b/breadcast-core/src/examples/dlna_discover.rs @@ -0,0 +1,27 @@ +//! Prints DLNA/UPnP media renderers as they appear/disappear on the LAN +//! (via periodic SSDP search for `AVTransport` services — the class of +//! device Windows' own "Cast to Device", Win+K, targets). Run with: +//! cargo run -p breadcast-core --example dlna_discover + +use breadcast_core::{DlnaDiscovery, DlnaDiscoveryEvent}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + + let (_discovery, mut events) = DlnaDiscovery::start(); + println!("Searching for DLNA/UPnP AVTransport devices every 30s... (Ctrl+C to stop)"); + + while let Some(event) = events.recv().await { + match event { + DlnaDiscoveryEvent::Found(device) => { + println!("+ {} [{}]", device.friendly_name, device.url); + } + DlnaDiscoveryEvent::Lost { url } => { + println!("- {url}"); + } + } + } + + Ok(()) +} diff --git a/breadcast-core/src/examples/dlna_mirror_test.rs b/breadcast-core/src/examples/dlna_mirror_test.rs new file mode 100644 index 0000000..3d7b400 --- /dev/null +++ b/breadcast-core/src/examples/dlna_mirror_test.rs @@ -0,0 +1,107 @@ +//! DLNA equivalent of `mirror_test`: captures this machine's screen, +//! encodes+serves it as HLS, discovers a DLNA/UPnP media renderer by name +//! substring, and casts the live stream to it via `AVTransport`. Run with: +//! cargo run -p breadcast-core --example dlna_mirror_test [name substring] +//! Matches any renderer found if no argument is given. +//! +//! This proves the same milestone `mirror_test` proved for Cast — a real +//! receiver actually playing this pipeline's live HLS output — but for a +//! different, unproven protocol path. DLNA renderer support for a +//! *live-growing* (never-ending) HLS playlist is much less consistent +//! across devices than Chromecast's: DLNA's classic use case is "play this +//! one finite file," and plenty of renderers' UPnP stacks predate HLS +//! entirely. Whether a given real renderer handles this is exactly what +//! this example is for finding out, not something to assume from the Cast +//! path working. + +use std::time::Duration; + +use breadcast_core::net::local_lan_ip; +use breadcast_core::pipeline::{hls_output_dir, wait_for_playlist_segments}; +use breadcast_core::{CaptureSession, DlnaDiscovery, DlnaDiscoveryEvent, DlnaSession}; +use gstreamer as gst; +use gstreamer::prelude::*; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + gst::init()?; + + let name_filter = std::env::args().nth(1).unwrap_or_default().to_lowercase(); + + let lan_ip = local_lan_ip()?; + println!("This machine's LAN IP: {lan_ip}"); + + println!("Opening the portal screen-cast picker (look for a system dialog)..."); + let capture = CaptureSession::start().await?; + println!("Got PipeWire video node id: {}", capture.video_node_id()); + + let output_dir = hls_output_dir("dlna-mirror")?; + let pipeline = breadcast_core::pipeline::build_video_pipeline(capture.video_node_id(), &output_dir)?; + pipeline.set_state(gst::State::Playing)?; + println!("Pipeline playing, writing HLS to {}", output_dir.display()); + + // Watch the encode pipeline's own bus in the background — see + // mirror_test.rs's identical block for why this matters. + { + let pipeline_watch = pipeline.clone(); + std::thread::spawn(move || { + match breadcast_core::pipeline::run_until_error_or_timeout(&pipeline_watch, gst::ClockTime::from_seconds(120)) { + Ok(outcome) => tracing::debug!(?outcome, "encode pipeline bus watcher ended"), + Err(e) => eprintln!("ENCODE PIPELINE ERROR: {e:?}"), + } + }); + } + + // A different fixed port than mirror_test's 8825, so both examples can + // run at once (e.g. testing Cast and DLNA against the same live + // desktop) without a bind conflict. + let http = breadcast_core::http_server::HttpServer::start("0.0.0.0:8826", output_dir.clone())?; + let stream_url = http.url(lan_ip, "playlist.m3u8"); + println!("Serving at {stream_url}"); + + wait_for_playlist_segments(&output_dir.join("playlist.m3u8"), 3, Duration::from_secs(20)).await?; + println!("Playlist has segments, proceeding to cast."); + + println!("Searching for a DLNA renderer matching \"{name_filter}\"..."); + let (_discovery, mut events) = DlnaDiscovery::start(); + let device = loop { + let event = tokio::time::timeout(Duration::from_secs(35), events.recv()) + .await + .map_err(|_| anyhow::anyhow!("timed out waiting for a renderer matching \"{name_filter}\""))? + .ok_or_else(|| anyhow::anyhow!("discovery channel closed"))?; + + if let DlnaDiscoveryEvent::Found(device) = event { + if device.friendly_name.to_lowercase().contains(&name_filter) { + break device; + } + } + }; + println!("Found {} [{}]", device.friendly_name, device.url); + + let session = DlnaSession::connect(&device).await?; + println!("Connected to AVTransport. Loading the live stream..."); + session.load(&stream_url).await?; + println!("Renderer accepted the load."); + + println!("Mirroring for up to 60s — check the display. Polling transport state:"); + let mirror_duration = Duration::from_secs(60); + let mut elapsed = Duration::ZERO; + while elapsed < mirror_duration { + match session.transport_state().await { + Ok(state) => println!(" transport_state={state}"), + Err(e) => println!(" failed to poll transport state: {e:?}"), + } + tokio::time::sleep(Duration::from_secs(3)).await; + elapsed += Duration::from_secs(3); + } + + if let Err(e) = session.stop().await { + eprintln!("failed to cleanly stop the DLNA session: {e:?}"); + } + pipeline.set_state(gst::State::Null)?; + capture.close().await?; + println!("Stopped."); + + Ok(()) +} diff --git a/breadcast-core/src/examples/mirror_test.rs b/breadcast-core/src/examples/mirror_test.rs new file mode 100644 index 0000000..eff8ee8 --- /dev/null +++ b/breadcast-core/src/examples/mirror_test.rs @@ -0,0 +1,134 @@ +//! Phase 2 finale: captures this machine's screen, encodes+serves it as +//! HLS, discovers a Cast device by name substring, and casts the live +//! stream to it. Run with: +//! cargo run -p breadcast-core --example mirror_test [name substring] +//! Defaults to "master bedroom" if no argument is given. + +use std::time::Duration; + +use breadcast_core::pipeline::{hls_output_dir, wait_for_playlist_segments}; +use breadcast_core::{CaptureSession, CastSession, Discovery, DiscoveryEvent}; +use breadcast_core::net::local_lan_ip; +use gstreamer as gst; +use gstreamer::prelude::*; +use rust_cast::channels::media::{MediaResponse, StreamType}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + gst::init()?; + + let name_filter = std::env::args() + .nth(1) + .unwrap_or_else(|| "master bedroom".to_string()) + .to_lowercase(); + let stream_type = match std::env::args().nth(2).as_deref() { + Some("live") => StreamType::Live, + _ => StreamType::Buffered, + }; + println!("Using stream_type={stream_type:?}"); + + let lan_ip = local_lan_ip()?; + println!("This machine's LAN IP: {lan_ip}"); + + println!("Opening the portal screen-cast picker (look for a system dialog)..."); + let capture = CaptureSession::start().await?; + println!("Got PipeWire video node id: {}", capture.video_node_id()); + + let output_dir = hls_output_dir("cast-mirror")?; + let pipeline = breadcast_core::pipeline::build_video_pipeline(capture.video_node_id(), &output_dir)?; + pipeline.set_state(gst::State::Playing)?; + println!("Pipeline playing, writing HLS to {}", output_dir.display()); + + // Watch the encode pipeline's own bus in the background — without + // this, a vah264enc/hlssink3 error partway through mirroring is + // invisible (nothing else polls this pipeline's bus), and the failure + // would only ever show up indirectly as the Cast session stalling. + { + let pipeline_watch = pipeline.clone(); + std::thread::spawn(move || { + match breadcast_core::pipeline::run_until_error_or_timeout(&pipeline_watch, gst::ClockTime::from_seconds(120)) { + Ok(outcome) => tracing::debug!(?outcome, "encode pipeline bus watcher ended"), + Err(e) => eprintln!("ENCODE PIPELINE ERROR: {e:?}"), + } + }); + } + + // Fixed port (not 0/ephemeral) so a firewall rule can be added once and + // stay valid across runs instead of chasing a random port every time. + let http = breadcast_core::http_server::HttpServer::start("0.0.0.0:8825", output_dir.clone())?; + let stream_url = http.url(lan_ip, "playlist.m3u8"); + println!("Serving at {stream_url}"); + + wait_for_playlist_segments(&output_dir.join("playlist.m3u8"), 3, Duration::from_secs(20)).await?; + println!("Playlist has segments, proceeding to cast."); + + println!("Looking for a Cast device matching \"{name_filter}\"..."); + let (_discovery, mut events) = Discovery::start()?; + let device = loop { + let event = tokio::time::timeout(std::time::Duration::from_secs(25), events.recv()) + .await + .map_err(|_| anyhow::anyhow!("timed out waiting for a device matching \"{name_filter}\""))? + .ok_or_else(|| anyhow::anyhow!("discovery channel closed"))?; + + if let DiscoveryEvent::Found(device) = event { + let name = device.name.to_lowercase(); + let model = device.model.to_lowercase(); + if name.contains(&name_filter) + && model.contains("chromecast") + && device.host.parse::().is_ok() + { + break device; + } + } + }; + println!("Found {} ({}) at {}:{}", device.name, device.model, device.host, device.port); + + let (session, media_events) = CastSession::connect(&device)?; + println!("Connected, Default Media Receiver launched. Loading the live stream..."); + let status = session.load(&stream_url, "application/vnd.apple.mpegurl", stream_type)?; + println!("Receiver acknowledged the load: {status:#?}"); + + println!("Mirroring for up to 60s — check the TV. Watching player state live:"); + let pump = std::thread::spawn(move || { + media_events.pump_with_media_callback(|media| match media { + MediaResponse::Status(status) => { + for entry in &status.entries { + println!( + " player_state={:?} idle_reason={:?} extended_status={:?}", + entry.player_state, + entry.idle_reason, + entry.extended_status.as_ref().map(|e| e.player_state) + ); + } + } + MediaResponse::LoadFailed(f) => println!(" LOAD FAILED: {f:?}"), + MediaResponse::LoadCancelled(c) => println!(" LOAD CANCELLED: {c:?}"), + MediaResponse::Error(e) => println!(" MEDIA ERROR: {e:?}"), + MediaResponse::InvalidRequest(r) => println!(" INVALID REQUEST: {r:?}"), + other => println!(" other media message: {other:?}"), + }) + }); + + let mirror_duration = Duration::from_secs(60); + let mut elapsed = Duration::ZERO; + while elapsed < mirror_duration && !pump.is_finished() { + tokio::time::sleep(Duration::from_secs(1)).await; + elapsed += Duration::from_secs(1); + } + if pump.is_finished() { + println!("Cast session ended early (after {elapsed:?}) — connection likely dropped or the receiver stopped playback."); + } + + if let Err(e) = session.stop() { + eprintln!("failed to cleanly stop the cast session: {e:?}"); + } + pipeline.set_state(gst::State::Null)?; + capture.close().await?; + if let Err(panic) = pump.join() { + eprintln!("cast session pump thread panicked: {panic:?}"); + } + println!("Stopped."); + + Ok(()) +} diff --git a/breadcast-core/src/examples/video_test.rs b/breadcast-core/src/examples/video_test.rs new file mode 100644 index 0000000..8fd702a --- /dev/null +++ b/breadcast-core/src/examples/video_test.rs @@ -0,0 +1,44 @@ +//! Phase 2 step 2: opens the portal picker, captures video only, encodes +//! via VA-API, and writes an HLS playlist+segments to a local dir. Run +//! with: cargo run -p breadcast-core --example video_test [output_dir] +//! Then, in another terminal: ffplay /playlist.m3u8 + +use breadcast_core::CaptureSession; +use gstreamer as gst; +use gstreamer::prelude::*; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + gst::init()?; + + let output_dir = std::env::args() + .nth(1) + .unwrap_or_else(|| "/tmp/breadcast-hls-test".to_string()); + let output_dir = std::path::PathBuf::from(output_dir); + + println!("Opening the portal screen-cast picker (look for a system dialog)..."); + let capture = CaptureSession::start().await?; + println!("Got PipeWire video node id: {}", capture.video_node_id()); + + let pipeline = breadcast_core::pipeline::build_video_pipeline(capture.video_node_id(), &output_dir)?; + pipeline.set_state(gst::State::Playing)?; + println!( + "Pipeline playing. Writing HLS to {}. Point ffplay/vlc at {}/playlist.m3u8 now.", + output_dir.display(), + output_dir.display() + ); + println!("Running for 30s..."); + + let outcome = + breadcast_core::pipeline::run_until_error_or_timeout(&pipeline, gst::ClockTime::from_seconds(30)); + + pipeline.set_state(gst::State::Null)?; + capture.close().await?; + + match outcome? { + breadcast_core::pipeline::RunOutcome::Eos => println!("Done: pipeline reached end-of-stream (source stopped sharing)."), + breadcast_core::pipeline::RunOutcome::Timeout => println!("Done: 30s elapsed with no pipeline errors."), + } + Ok(()) +} diff --git a/breadcast-core/src/http_server.rs b/breadcast-core/src/http_server.rs new file mode 100644 index 0000000..4565514 --- /dev/null +++ b/breadcast-core/src/http_server.rs @@ -0,0 +1,266 @@ +use std::net::SocketAddr; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +use anyhow::{Context, Result}; + +/// Number of worker threads pulling requests off the server's shared queue. +/// tiny_http's `Server::recv()` takes `&self` specifically so it can be +/// called from multiple threads concurrently (its own docs' recommended +/// pattern) — bounding this at a small fixed number, instead of spawning a +/// fresh OS thread per request, keeps the request volume any single LAN +/// host can inflict on this process capped, since this server ends up +/// reachable by every device on the LAN, not just the Cast receiver it's +/// meant for. +const WORKER_THREADS: usize = 8; + +/// Serves `root` (the `hlssink3` output directory: `playlist.m3u8` + +/// `segment*.ts`) over plain HTTP. Runs a small fixed pool of worker +/// threads; dropping the handle does not stop the server (there is no clean +/// shutdown yet — matches the smoke-testing scope of the rest of Phase 2). +/// +/// Every servable path is namespaced under a random token +/// (`//playlist.m3u8`, etc. — see [`HttpServer::token`]) rather than +/// served at the root. There's no way to add an `Authorization` header a +/// Cast receiver will send back, so this is the standard mitigation for a +/// server that must stay unauthenticated but shouldn't let every other +/// device on the LAN casually load `/playlist.m3u8` and watch this +/// machine's screen. +/// +/// Sets `Content-Type` per Google's Cast media docs (the receiver's HTTP +/// client needs a correct type to load segments reliably), `Cache-Control` +/// (critical for `playlist.m3u8`, which is rewritten every segment — a +/// cached stale copy stalls playback with no error anywhere), Range/206 +/// support, and a permissive CORS header — get this right from the start +/// since a Chromecast has no devtools console to debug a silent load +/// failure against. +pub struct HttpServer { + addr: SocketAddr, + token: String, +} + +impl HttpServer { + /// Binds on `bind_addr` (use `0.0.0.0:0` to let the OS pick a free + /// port — read the actual port back via [`HttpServer::addr`]) and + /// starts serving `root` in the background. Build URLs to hand to a + /// Cast device with [`HttpServer::url`], not by hand — it includes the + /// required path token. + pub fn start(bind_addr: &str, root: PathBuf) -> Result { + let root = root + .canonicalize() + .with_context(|| format!("failed to canonicalize HLS root {}", root.display()))?; + + let server = tiny_http::Server::http(bind_addr) + .map_err(|e| anyhow::anyhow!("{e}")) + .with_context(|| format!("failed to bind HTTP server on {bind_addr}"))?; + let addr = match server.server_addr() { + tiny_http::ListenAddr::IP(addr) => addr, + other => anyhow::bail!("HTTP server bound to a non-IP address: {other:?}"), + }; + + let server = Arc::new(server); + let token = random_token(); + + for _ in 0..WORKER_THREADS { + let server = Arc::clone(&server); + let root = root.clone(); + let token = token.clone(); + std::thread::spawn(move || { + while let Ok(request) = server.recv() { + if let Err(e) = handle_request(request, &root, &token) { + tracing::warn!(error = %e, "HLS HTTP request failed"); + } + } + }); + } + + Ok(Self { addr, token }) + } + + /// The bound address, e.g. `0.0.0.0:41823`. Combine with this + /// machine's LAN IP (not `0.0.0.0` itself) to build the URL handed to + /// the Cast device — `0.0.0.0` only means anything to sockets on this + /// host. + pub fn addr(&self) -> SocketAddr { + self.addr + } + + /// Builds a full URL for `relative` (e.g. `"playlist.m3u8"`), rooted at + /// `host` (this machine's LAN-reachable IP — see [`HttpServer::addr`]'s + /// doc for why that can't just be `self.addr()`), including the + /// unguessable path token every request must carry. + pub fn url(&self, host: std::net::IpAddr, relative: &str) -> String { + format!("http://{host}:{}/{}/{relative}", self.addr.port(), self.token) + } +} + +/// Generates a 32-hex-character unguessable token from `/dev/urandom`. This +/// is a Linux-only project already (PipeWire, Hyprland's portal, VA-API) so +/// reaching for the platform's random device directly is fine — no `rand` +/// crate dependency for one call site. +fn random_token() -> String { + let mut bytes = [0u8; 16]; + let read_ok = std::fs::File::open("/dev/urandom") + .and_then(|mut f| { + use std::io::Read; + f.read_exact(&mut bytes) + }) + .is_ok(); + if !read_ok { + // Unreachable in practice on Linux, but better than a zero-entropy + // token if it ever happened. + use std::hash::{Hash, Hasher}; + let mut hasher = std::collections::hash_map::DefaultHasher::new(); + std::time::SystemTime::now().hash(&mut hasher); + std::process::id().hash(&mut hasher); + bytes[..8].copy_from_slice(&hasher.finish().to_le_bytes()); + } + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +/// Parses a single-range `Range: bytes=start-end` header value against a +/// body of `len` bytes. Returns `None` for anything absent, malformed, or +/// multi-range (multipart ranges aren't needed for HLS segment fetches, and +/// falling back to a full 200 response for those is always a valid +/// response under the HTTP spec). +fn parse_range(value: &str, len: usize) -> Option<(usize, usize)> { + let spec = value.strip_prefix("bytes=")?; + if spec.contains(',') || len == 0 { + return None; + } + let (start, end) = spec.split_once('-')?; + let last = len - 1; + match (start.trim(), end.trim()) { + ("", "") => None, + ("", suffix_len) => { + let n: usize = suffix_len.parse().ok()?; + Some((len.saturating_sub(n), last)) + } + (start, "") => { + let start: usize = start.parse().ok()?; + Some((start, last)) + } + (start, end) => { + let start: usize = start.parse().ok()?; + let end: usize = end.parse().ok()?; + Some((start, end)) + } + } +} + +fn handle_request(request: tiny_http::Request, root: &Path, token: &str) -> Result<()> { + // `Split::next()` on a non-empty pattern always yields at least one + // item, so this never actually hits a `None` case. + let url_path = request.url().split('?').next().expect("split always yields at least one item").to_string(); + let remote = request + .remote_addr() + .map(|a| a.to_string()) + .unwrap_or_else(|| "?".to_string()); + + // Reject anything not under the unguessable token prefix before even + // touching the filesystem — see the struct docs for why this exists. + let Some(relative) = url_path + .trim_start_matches('/') + .strip_prefix(token) + .and_then(|rest| rest.strip_prefix('/')) + else { + tracing::info!(%remote, path = %url_path, status = 404, "HLS request (bad or missing path token)"); + return respond_status(request, 404); + }; + + // Reject any path that could escape `root` (e.g. `../../etc/passwd`) — + // the URL is attacker-controlled input the moment this server is + // reachable from the LAN, which it is by design (the Cast device is a + // different host). `root` itself is canonicalized once in `start()`, so + // this comparison is meaningful even if `root` was originally relative + // or contained a symlinked component — comparing a canonical path + // against a non-canonical one would make `starts_with` spuriously fail + // and 404 every request. + let requested = root.join(relative); + let Ok(canonical) = requested.canonicalize() else { + tracing::info!(%remote, path = %url_path, status = 404, "HLS request (not found)"); + return respond_status(request, 404); + }; + if !canonical.starts_with(root) || !canonical.is_file() { + tracing::info!(%remote, path = %url_path, status = 404, "HLS request (outside root or not a file)"); + return respond_status(request, 404); + } + + let data = match std::fs::read(&canonical) { + Ok(data) => data, + Err(_) => { + // hlssink3 rotates out old segments (`max-files`) concurrently + // with requests for them — a file that existed at + // canonicalize() above but is gone by the time it's read is + // routine for a live stream, not a server fault. Answer with a + // normal 404 (a receiver skips it and asks for the next + // segment) instead of letting the request drop unanswered, + // which tiny_http turns into an unexplained bare 500. + tracing::info!(%remote, path = %url_path, status = 404, "HLS request (file removed before read, likely segment rotation)"); + return respond_status(request, 404); + } + }; + + let extension = canonical.extension().and_then(|e| e.to_str()); + let content_type = match extension { + Some("m3u8") => "application/vnd.apple.mpegurl", + Some("ts") => "video/mp2t", + _ => "application/octet-stream", + }; + // The playlist is rewritten in place on every segment — must never be + // cached, or a receiver/intermediary replaying a stale copy stalls + // playback with no error anywhere. Segments are written once under a + // unique numbered filename and never modified after that, so they're + // safe to cache aggressively. + let cache_control = match extension { + Some("m3u8") => "no-cache, no-store, must-revalidate", + _ => "public, max-age=3600, immutable", + }; + + let range = request + .headers() + .iter() + .find(|h| h.field.equiv("Range")) + .and_then(|h| parse_range(h.value.as_str(), data.len())); + + let mut headers = vec![ + ("Content-Type".to_string(), content_type.to_string()), + ("Cache-Control".to_string(), cache_control.to_string()), + ("Access-Control-Allow-Origin".to_string(), "*".to_string()), + ("Access-Control-Allow-Headers".to_string(), "Range, Accept-Encoding".to_string()), + ("Access-Control-Expose-Headers".to_string(), "Content-Length, Content-Range".to_string()), + ("Accept-Ranges".to_string(), "bytes".to_string()), + ]; + + let (status, body) = match range { + Some((start, end)) if start <= end && end < data.len() => { + headers.push(("Content-Range".to_string(), format!("bytes {start}-{end}/{}", data.len()))); + (206u16, data[start..=end].to_vec()) + } + Some(_) => { + headers.push(("Content-Range".to_string(), format!("bytes */{}", data.len()))); + tracing::info!(%remote, path = %url_path, status = 416, "HLS request (unsatisfiable range)"); + return respond(request, 416, Vec::new(), &headers); + } + None => (200u16, data), + }; + + tracing::info!(%remote, path = %url_path, status, "HLS request"); + respond(request, status, body, &headers) +} + +fn respond(request: tiny_http::Request, status: u16, body: Vec, headers: &[(String, String)]) -> Result<()> { + let mut response = tiny_http::Response::from_data(body).with_status_code(status); + for (name, value) in headers { + if let Ok(header) = tiny_http::Header::from_bytes(name.as_bytes(), value.as_bytes()) { + response.add_header(header); + } + } + request.respond(response).context("failed to write HTTP response") +} + +fn respond_status(request: tiny_http::Request, status: u16) -> Result<()> { + request + .respond(tiny_http::Response::empty(status)) + .context("failed to write HTTP error response") +} diff --git a/breadcast-core/src/ipc.rs b/breadcast-core/src/ipc.rs new file mode 100644 index 0000000..c8f8459 --- /dev/null +++ b/breadcast-core/src/ipc.rs @@ -0,0 +1,86 @@ +//! Message shapes for breadcastd's private control socket +//! (`$XDG_RUNTIME_DIR/breadcast/breadcastd.sock`, newline-delimited JSON) — +//! shared between `breadcastd` (which serves them) and `breadcast` (the +//! GTK4 popup, which is the only client) so the two never drift out of +//! sync with each other. See `breadcastd/src/ipc.rs` for the actual +//! socket-handling code; this crate only holds the wire types, since it's +//! the one both binaries already depend on. + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ClientRequest { + pub id: u64, + pub method: String, + #[serde(default)] + pub params: Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type")] +pub enum ServerMessage { + #[serde(rename = "response")] + Response { + id: u64, + #[serde(skip_serializing_if = "Option::is_none")] + result: Option, + #[serde(skip_serializing_if = "Option::is_none")] + error: Option, + }, + #[serde(rename = "event")] + Event { event: String, data: Value }, +} + +impl ServerMessage { + pub fn ok(id: u64, result: Value) -> Self { + Self::Response { id, result: Some(result), error: None } + } + + pub fn err(id: u64, error: impl std::fmt::Display) -> Self { + Self::Response { id, result: None, error: Some(error.to_string()) } + } +} + +/// Which casting protocol a [`DeviceInfo`]/[`StateInfo::Casting`] refers to +/// — Cast V2/Cast Streaming ([`crate::cast_sender`]/[`crate::caststream`]) +/// or DLNA/UPnP AVTransport ([`crate::dlna`]). The two protocols discover +/// disjoint device populations (see `dlna/mod.rs`'s doc comment), so a +/// unified device list needs this to tell them apart — a Cast device id and +/// a DLNA device url share no namespace, but both are opaque strings to the +/// GTK client, which otherwise has no way to know which `start_cast` +/// dispatch path it's picking. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Protocol { + Cast, + Dlna, +} + +/// A discovered device as reported by `"list_devices"`/`"device_list_changed"` +/// — a protocol-agnostic projection of either a [`crate::CastDevice`] (`id` +/// is its mDNS id) or a [`crate::dlna::DlnaDevice`] (`id` is its description URL, +/// the closest thing DLNA has to a stable identifier — see +/// `dlna/device.rs`'s doc comment on [`crate::dlna::DlnaDevice::url`]). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeviceInfo { + pub id: String, + pub name: String, + pub model: String, + pub protocol: Protocol, +} + +/// The daemon's current activity — pushed as a `"state_changed"` event and +/// returned by the `"get_state"` request. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "state", rename_all = "snake_case")] +pub enum StateInfo { + Idle, + Casting { device_id: String, device_name: String, protocol: Protocol }, +} + +/// Returns `$XDG_RUNTIME_DIR/breadcast/breadcastd.sock`. +pub fn socket_path() -> anyhow::Result { + let runtime_dir = std::env::var("XDG_RUNTIME_DIR").map_err(|_| anyhow::anyhow!("XDG_RUNTIME_DIR is not set"))?; + Ok(std::path::Path::new(&runtime_dir).join("breadcast").join("breadcastd.sock")) +} diff --git a/breadcast-core/src/lib.rs b/breadcast-core/src/lib.rs new file mode 100644 index 0000000..3905593 --- /dev/null +++ b/breadcast-core/src/lib.rs @@ -0,0 +1,17 @@ +pub mod capture; +pub mod cast_sender; +pub mod caststream; +pub mod device; +pub mod discovery; +pub mod dlna; +pub mod http_server; +pub mod ipc; +pub mod net; +pub mod pipeline; + +pub use capture::CaptureSession; +pub use cast_sender::CastSession; +pub use caststream::{CastStreamEvent, CastStreamSender, VideoParams as CastStreamVideoParams}; +pub use device::CastDevice; +pub use discovery::{Discovery, DiscoveryEvent}; +pub use dlna::{DlnaDevice, DlnaDiscovery, DlnaDiscoveryEvent, DlnaSession}; diff --git a/breadcast-core/src/net.rs b/breadcast-core/src/net.rs new file mode 100644 index 0000000..fe20e75 --- /dev/null +++ b/breadcast-core/src/net.rs @@ -0,0 +1,61 @@ +use std::net::{IpAddr, Ipv4Addr}; + +use anyhow::{Context, Result}; + +/// Finds this machine's LAN-reachable IPv4 address by enumerating network +/// interfaces directly, rather than the more common "UDP-connect to a +/// public address and read back the local endpoint" trick — that trick +/// asks the kernel's *default route*, which Tailscale can silently take +/// over via policy routing (confirmed on a real machine: with Tailscale +/// active, `ip route get 8.8.8.8` resolves via `tailscale0`, not the real +/// LAN interface). Using it handed a Cast device a 100.64.0.0/10 Tailscale +/// CGNAT address it could never reach, which surfaced only as an +/// unexplained `LOAD FAILED` — exactly the class of silent failure this +/// project has already spent a lot of time chasing. Explicitly excluding +/// VPN/virtual interface name prefixes sidesteps the whole problem +/// regardless of what the default route happens to be. +/// +/// Shared by every casting protocol (Cast, DLNA, ...) — they all need to +/// embed this machine's own address in a URL handed to a receiver device. +pub fn local_lan_ip() -> Result { + const EXCLUDED_PREFIXES: &[&str] = &["tailscale", "wg", "docker", "veth", "br-", "virbr", "lo"]; + + let output = std::process::Command::new("ip") + .args(["-4", "-o", "addr", "show", "scope", "global", "up"]) + .output() + .context("failed to run `ip addr show` to find this machine's LAN IP")?; + if !output.status.success() { + anyhow::bail!("`ip addr show` exited with {}", output.status); + } + let text = String::from_utf8_lossy(&output.stdout); + + let mut candidates: Vec<(String, Ipv4Addr)> = Vec::new(); + for line in text.lines() { + // Format: "3: wlan0 inet 10.179.161.89/23 brd ... scope global dynamic wlan0" + let mut fields = line.split_whitespace(); + let Some(_index) = fields.next() else { continue }; + let Some(iface) = fields.next() else { continue }; + if EXCLUDED_PREFIXES.iter().any(|p| iface.starts_with(p)) { + continue; + } + if fields.next() != Some("inet") { + continue; + } + let Some(cidr) = fields.next() else { continue }; + let Some(addr) = cidr.split('/').next().and_then(|a| a.parse::().ok()) else { continue }; + if !addr.is_private() { + continue; + } + candidates.push((iface.to_string(), addr)); + } + + // Prefer a conventionally-named physical/Wi-Fi interface when there's a + // choice, but any private, non-excluded address is acceptable. + candidates.sort_by_key(|(iface, _)| !(iface.starts_with("wl") || iface.starts_with("en") || iface.starts_with("eth"))); + + candidates + .into_iter() + .map(|(_, addr)| IpAddr::V4(addr)) + .next() + .context("no LAN-reachable IPv4 address found (excluding loopback/VPN/virtual interfaces) — is this machine connected to a network?") +} diff --git a/breadcast-core/src/pipeline/mod.rs b/breadcast-core/src/pipeline/mod.rs new file mode 100644 index 0000000..abf8b89 --- /dev/null +++ b/breadcast-core/src/pipeline/mod.rs @@ -0,0 +1,312 @@ +use std::os::unix::fs::DirBuilderExt; +use std::path::{Path, PathBuf}; +use std::time::Duration; + +use anyhow::{Context, Result, bail}; +use gstreamer as gst; +use gstreamer::prelude::*; +use gstreamer_app as gst_app; +use gstreamer_video as gst_video; + +/// Builds (but doesn't start) the capture → encode → mux → HLS pipeline for +/// a single video source. `output_dir` is created if it doesn't exist; +/// `hlssink3` writes `segment%05d.ts` files and `playlist.m3u8` there. +/// +/// Idempotently calls `gst::init()` itself rather than requiring every +/// caller to remember to — `gst::parse::launch` panics +/// (`assert_initialized_main_thread!()`) if GStreamer was never +/// initialized, which every current caller happens to do first, but that's +/// a footgun for a `pub` function once something other than a smoke-test +/// example calls it (e.g. `breadcastd`). +/// +/// Uses a `gst::parse::launch` string rather than the typed element-builder +/// API — this is the prototyping-first approach: get the pipeline shape +/// right and provable against real hardware before hardening it into typed +/// Rust with per-element error handling. Filesystem paths are deliberately +/// *not* interpolated into that string, though: `output_dir` is caller +/// (eventually user-facing) input, and a path containing `"`, `\`, or `!` +/// would either break `gst::parse::launch`'s own string syntax or inject +/// extra elements into the parsed graph. `hlssink3`'s `location`/ +/// `playlist-location` are set as plain element properties after parsing +/// instead, which need no escaping at all. +/// +/// `vah264enc` (not the deprecated `vaapih264enc`) needs `gst-plugin-va` +/// installed (`pacman -S gst-plugin-va`) — it's a separate Arch package +/// from `gst-plugins-bad` itself, not bundled in. `hlssink3` similarly +/// needs `gst-plugin-hlssink3`. `hlssink3` (not `hlscmafsink`) is +/// deliberate: the Chromecast Default Media Receiver only plays classic +/// MPEG-TS-segmented HLS, not fMP4/CMAF — and `hlssink3` does its own +/// internal MPEG-TS muxing per segment via its `video`/`audio` *request* +/// pads, so no separate `mpegtsmux` element goes in front of it (confirmed +/// via `gst-inspect-1.0 hlssink3`: its only pad templates are `video` and +/// `audio`, not a generic always-available `sink`). +pub fn build_video_pipeline(video_node_id: u32, output_dir: &Path) -> Result { + gst::init().context("failed to initialize GStreamer")?; + + std::fs::create_dir_all(output_dir) + .with_context(|| format!("failed to create HLS output dir {}", output_dir.display()))?; + + let segment_pattern = output_dir.join("segment%05d.ts"); + let playlist_path = output_dir.join("playlist.m3u8"); + + // Capped to 1280x720@30 and H.264 Main profile: this machine's native + // 1920x1200 at an uncapped framerate (observed via ffprobe as a + // nonsensical 120fps/240tbr — pipewiresrc doesn't cap the rate on its + // own) was confirmed via a real Chromecast to fetch fine over HTTP + // (200s on the playlist and first segment) but then fail to actually + // play — consistent with exceeding what an older Chromecast's H.264 + // decoder profile/level supports, not a network/CORS/HLS-structure + // problem. 720p30 Main is a conservative, broadly-compatible baseline; + // revisit upward (1080p, High profile) once a specific device's real + // ceiling is known. Note this ignores the source's 16:10 aspect ratio + // (stretches to 16:9) — correctness/compatibility first, an + // aspect-preserving scale (letterbox via `videoscale + // add-borders=true`) is a follow-up, not a blocker. + let pipeline_str = format!( + "pipewiresrc path={video_node_id} do-timestamp=true ! \ + videoconvert ! videoscale ! videorate ! \ + video/x-raw,format=NV12,width=1280,height=720,framerate=30/1 ! \ + vah264enc bitrate=4000 key-int-max=60 rate-control=cbr ! \ + video/x-h264,profile=main ! \ + h264parse config-interval=1 ! \ + hlssink.video \ + hlssink3 name=hlssink target-duration=2 playlist-length=6 max-files=10" + ); + + let element = gst::parse::launch(&pipeline_str).context("failed to parse GStreamer pipeline")?; + let Ok(pipeline) = element.downcast::() else { + bail!("parsed GStreamer graph was not a top-level Pipeline"); + }; + + let hlssink = pipeline + .by_name("hlssink") + .context("parsed pipeline has no element named 'hlssink'")?; + hlssink.set_property( + "location", + segment_pattern.to_str().context("HLS segment path is not valid UTF-8")?, + ); + hlssink.set_property( + "playlist-location", + playlist_path.to_str().context("HLS playlist path is not valid UTF-8")?, + ); + + Ok(pipeline) +} + +/// Builds (but doesn't start) the capture → encode → `appsink` pipeline used +/// for low-latency Cast Streaming mirroring (see [`crate::caststream`]) — +/// the counterpart to [`build_video_pipeline`]'s HLS path, which the +/// Chromecast Mirroring receiver can't play (it speaks RTP, not HLS). +/// +/// Differs from the HLS pipeline in exactly the ways that matter for +/// feeding openscreen's `Sender::EnqueueFrame`, which wants standalone, +/// receiver-decodable Annex-B access units, not a muxed container: +/// - `h264parse config-interval=-1` re-inserts SPS/PPS before every key +/// frame (not just once) — required since there's no container-level +/// "here's the codec config" the receiver can fall back on, unlike HLS's +/// `.ts` segments. +/// - An explicit `video/x-h264,stream-format=byte-stream,alignment=au` caps +/// filter after `h264parse` — `vah264enc`'s default output is `avc` +/// (4-byte length-prefixed NAL units, the ISO/MP4 convention), but +/// RTP/Cast Streaming payloads need Annex-B (0x00 0x00 0x00 0x01 start +/// codes), the same format `h264parse` can produce but won't unless asked. +/// - `appsink` instead of `hlssink3`: each pulled `gst::Sample` is one +/// complete access unit (`alignment=au`), ready to hand to +/// `CastStreamSender::enqueue_frame` — see `cast_stream_test.rs` for the +/// pull loop. `sync=false` since these are being forwarded over the +/// network as fast as produced, not paced against a clock for local +/// playback; `drop=true`/`max-buffers=4` bounds memory if the pull loop +/// ever falls behind rather than growing an unbounded backlog. +/// +/// Returns the pipeline plus its `appsink` and the `vah264enc` element (the +/// latter so a caller can drive its `bitrate` property from +/// `CastStreamSender::estimated_bandwidth_bps()` — see +/// [`request_key_frame`]/`set_video_bitrate_kbps` for the two knobs a +/// congestion-control loop needs). +pub fn build_video_pipeline_for_streaming( + video_node_id: u32, +) -> Result<(gst::Pipeline, gst_app::AppSink, gst::Element)> { + gst::init().context("failed to initialize GStreamer")?; + + // Same 1280x720@30 Main-profile baseline as build_video_pipeline, for + // the same reason (see its doc comment) -- broad decoder compatibility + // first, revisit upward once a specific device's real ceiling is known. + let pipeline_str = "pipewiresrc path=%VIDEO_NODE_ID% do-timestamp=true ! \ + videoconvert ! videoscale ! videorate ! \ + video/x-raw,format=NV12,width=1280,height=720,framerate=30/1 ! \ + vah264enc name=venc bitrate=4000 key-int-max=60 rate-control=cbr ! \ + video/x-h264,profile=main ! \ + h264parse name=h264parse config-interval=-1 ! \ + video/x-h264,stream-format=byte-stream,alignment=au ! \ + appsink name=appsink emit-signals=false sync=false max-buffers=4 drop=true" + .replace("%VIDEO_NODE_ID%", &video_node_id.to_string()); + + let element = gst::parse::launch(&pipeline_str).context("failed to parse GStreamer pipeline")?; + let Ok(pipeline) = element.downcast::() else { + bail!("parsed GStreamer graph was not a top-level Pipeline"); + }; + + let appsink = pipeline + .by_name("appsink") + .context("parsed pipeline has no element named 'appsink'")? + .downcast::() + .map_err(|_| anyhow::anyhow!("'appsink' element was not a GstAppSink"))?; + + let encoder = pipeline.by_name("venc").context("parsed pipeline has no element named 'venc'")?; + + Ok((pipeline, appsink, encoder)) +} + +/// Pulls one complete Annex-B H.264 access unit from `appsink`, blocking +/// until one is available. Returns `None` once the pipeline reaches EOS or +/// the sink otherwise stops (e.g. pipeline torn down from another thread). +pub fn pull_encoded_frame(appsink: &gst_app::AppSink) -> Result, bool, i64)>> { + let sample = match appsink.pull_sample() { + Ok(sample) => sample, + Err(_) if appsink.is_eos() => return Ok(None), + Err(e) => bail!("appsink pull_sample failed: {e}"), + }; + let buffer = sample.buffer().context("pulled sample had no buffer")?; + let map = buffer.map_readable().context("failed to map sample buffer readable")?; + let is_key_frame = !buffer.flags().contains(gst::BufferFlags::DELTA_UNIT); + // `.unwrap_or(0)` rather than propagating a missing PTS as an error: + // CastStreamSender::enqueue_frame only needs monotonically-increasing, + // real-elapsed-time-proportional values (see its doc comment) -- an + // occasional buffer with no PTS shouldn't abort an otherwise-live + // stream over it. + let capture_time_us = buffer.pts().map(|t| t.useconds() as i64).unwrap_or(0); + Ok(Some((map.as_slice().to_vec(), is_key_frame, capture_time_us))) +} + +/// Sends an upstream "force key unit" event from `appsink`, propagating to +/// `vah264enc` and causing it to emit an IDR frame on its next output -- +/// the mechanism `cast_stream_test.rs`'s pull loop uses when +/// `CastStreamSender::needs_key_frame()` reports true. +pub fn request_key_frame(appsink: &gst_app::AppSink) { + let event = gst_video::UpstreamForceKeyUnitEvent::builder().all_headers(true).build(); + let _ = appsink.send_event(event); +} + +/// Updates `encoder`'s (a `vah264enc` element, as returned by +/// [`build_video_pipeline_for_streaming`]) target bitrate in kbps. Meant to +/// be driven periodically from `CastStreamSender::estimated_bandwidth_bps()` +/// -- this vendored subset of openscreen only does flow control, not +/// congestion control (see `Sender`'s class comment in +/// `breadcast-caststream-sys/vendor/openscreen/cast/streaming/public/sender.h`), +/// so actually throttling the encoder in response is this project's own +/// responsibility. +pub fn set_video_bitrate_kbps(encoder: &gst::Element, kbps: u32) { + encoder.set_property("bitrate", kbps); +} + +/// Why [`run_until_error_or_timeout`] returned successfully — distinct from +/// each other because a caller (e.g. a UI reporting "mirroring stopped") +/// needs to tell "the user hit Stop-sharing in the portal picker, EOS is +/// expected" apart from "nothing happened for N seconds, which for a smoke +/// test just means the run duration elapsed normally." Collapsing both into +/// a bare `Ok(())`, as a previous version of this function did, is exactly +/// the kind of silent-success-that-wasn't this project has already lost a +/// lot of time chasing elsewhere (the Cast `LOAD FAILED` debugging). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RunOutcome { + /// The pipeline reached end-of-stream (e.g. the portal source ended + /// because the user stopped sharing). + Eos, + /// `timeout` elapsed with no error or EOS. + Timeout, +} + +/// Blocks the calling thread until the pipeline reports an error or EOS, or +/// `timeout` elapses (whichever first). Returns which of those happened, or +/// `Err` on a real pipeline error. Meant for smoke-testing from a +/// synchronous `main`/example; the real daemon will want an async/watch-based +/// version instead of blocking a thread. +pub fn run_until_error_or_timeout(pipeline: &gst::Pipeline, timeout: gst::ClockTime) -> Result { + let bus = pipeline.bus().context("pipeline has no bus")?; + let deadline = std::time::Instant::now() + std::time::Duration::from(timeout); + + loop { + let remaining = deadline.saturating_duration_since(std::time::Instant::now()); + if remaining.is_zero() { + return Ok(RunOutcome::Timeout); + } + let Some(msg) = bus.timed_pop_filtered( + gst::ClockTime::from_mseconds(remaining.as_millis().min(500) as u64), + &[gst::MessageType::Error, gst::MessageType::Eos, gst::MessageType::Warning], + ) else { + continue; + }; + + use gst::MessageView; + match msg.view() { + MessageView::Error(e) => { + bail!( + "GStreamer pipeline error from {:?}: {} ({:?})", + e.src().map(|s| s.path_string()), + e.error(), + e.debug() + ); + } + MessageView::Warning(w) => { + tracing::warn!( + src = ?w.src().map(|s| s.path_string()), + error = %w.error(), + "GStreamer pipeline warning" + ); + } + MessageView::Eos(_) => return Ok(RunOutcome::Eos), + _ => {} + } + } +} + +/// A private, per-run HLS output directory under `$XDG_RUNTIME_DIR` (0700, +/// tmpfs, cleared on logout) rather than a fixed path under `/tmp`. A fixed +/// `/tmp` path is predictable and `/tmp` is world-writable: another local +/// user could pre-create or symlink it before this runs, to either read the +/// screen-recording segments this then serves on the LAN, or plant files +/// for the HTTP server to hand out. `XDG_RUNTIME_DIR` is exclusively +/// readable/writable by this user, so predictability of the subdirectory +/// name under it doesn't matter. +/// +/// `label` distinguishes concurrent sessions of different kinds (e.g. +/// `"cast-mirror"` vs `"dlna-mirror"`) from colliding on the same path if +/// ever run at once on the same machine; the process id further +/// distinguishes concurrent runs of the *same* kind. +pub fn hls_output_dir(label: &str) -> Result { + let runtime_dir = std::env::var("XDG_RUNTIME_DIR").context("XDG_RUNTIME_DIR is not set")?; + let dir = PathBuf::from(runtime_dir) + .join("breadcast") + .join(format!("{label}-{}", std::process::id())); + std::fs::DirBuilder::new() + .recursive(true) + .mode(0o700) + .create(&dir) + .with_context(|| format!("failed to create HLS output dir {}", dir.display()))?; + Ok(dir) +} + +/// Polls `playlist_path` until it contains at least `min_segments` `#EXTINF` +/// entries or `timeout` elapses. Casting/loading a URL before the encode +/// pipeline has actually produced any segments — which an earlier version +/// of this project's examples did unconditionally, via a fixed sleep +/// regardless of whether encoding had actually started — hands the +/// receiver a 404 playlist and produces an unexplained load failure. +pub async fn wait_for_playlist_segments(playlist_path: &Path, min_segments: usize, timeout: Duration) -> Result<()> { + let deadline = tokio::time::Instant::now() + timeout; + loop { + if let Ok(contents) = std::fs::read_to_string(playlist_path) { + if contents.lines().filter(|l| l.starts_with("#EXTINF")).count() >= min_segments { + return Ok(()); + } + } + if tokio::time::Instant::now() >= deadline { + anyhow::bail!( + "HLS playlist at {} never accumulated {min_segments} segments within {timeout:?} — \ + the encode pipeline may not be producing output (check for a GStreamer error above)", + playlist_path.display() + ); + } + tokio::time::sleep(Duration::from_millis(250)).await; + } +} diff --git a/breadcast/Cargo.toml b/breadcast/Cargo.toml new file mode 100644 index 0000000..edb6b8a --- /dev/null +++ b/breadcast/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "breadcast" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +description = "breadcast GTK4 popup: device picker and cast controls (thin IPC client of breadcastd)" + +[[bin]] +name = "breadcast" +path = "src/main.rs" + +[dependencies] +anyhow = { workspace = true } +serde_json = { workspace = true } +breadcast-core = { path = "../breadcast-core" } +bread-theme = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["gtk"] } +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["gtk"] } +gtk4 = { version = "0.11", features = ["v4_12"] } +gtk4-layer-shell = "0.8" diff --git a/breadcast/src/css.rs b/breadcast/src/css.rs new file mode 100644 index 0000000..0f94064 --- /dev/null +++ b/breadcast/src/css.rs @@ -0,0 +1,62 @@ +use bread_theme::Palette; + +/// Plain GTK4 CSS (not libadwaita) — matches `bos-settings`'/`breadclip`'s +/// precedent: libadwaita rows can't be width-constrained from outside a +/// fixed-width popup panel the way a plain `ListBoxRow` can. +pub fn build_css(palette: &Palette) -> String { + format!( + r#" + .cast-panel {{ + background-color: {bg}; + border-radius: 12px; + border: 1px solid alpha({fg}, 0.08); + padding: 12px; + }} + .cast-title {{ + font-weight: 700; + font-size: 1.1em; + color: {fg}; + }} + .cast-status-pill {{ + border-radius: 999px; + padding: 3px 10px; + font-size: 0.85em; + background-color: {surface}; + color: {overlay}; + }} + .cast-status-pill.casting {{ + background-color: alpha({accent}, 0.25); + color: {accent}; + }} + .cast-device-row {{ + border-radius: 8px; + padding: 8px 10px; + color: {fg}; + }} + .cast-device-row:hover {{ + background-color: alpha({fg}, 0.06); + }} + .cast-device-row:selected {{ + background-color: alpha({accent}, 0.18); + }} + .cast-device-model {{ + font-size: 0.85em; + color: {overlay}; + }} + .cast-empty-label {{ + color: {overlay}; + padding: 24px 8px; + }} + .cast-stop-button {{ + background-color: alpha(#e35b5b, 0.15); + color: #e35b5b; + border-radius: 8px; + }} + "#, + bg = palette.background, + fg = palette.foreground, + surface = palette.color0, + overlay = palette.color7, + accent = palette.color4, + ) +} diff --git a/breadcast/src/ipc_client.rs b/breadcast/src/ipc_client.rs new file mode 100644 index 0000000..3b1ccf3 --- /dev/null +++ b/breadcast/src/ipc_client.rs @@ -0,0 +1,68 @@ +//! A thin, synchronous client for breadcastd's IPC socket (see +//! `breadcast_core::ipc` for the wire types and +//! `breadcastd/src/ipc.rs` for the server side). No async runtime here — +//! GTK4 already has its own main loop (glib), so this uses a plain +//! blocking reader thread feeding an `mpsc::Receiver` the GTK side polls +//! via `glib::timeout_add_local` (see `main.rs`), rather than pulling in +//! tokio just for one socket. + +use std::io::{BufRead, BufReader, Write}; +use std::os::unix::net::UnixStream; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::mpsc; + +use anyhow::{Context, Result}; +use breadcast_core::ipc::{ClientRequest, ServerMessage, socket_path}; + +pub struct IpcClient { + write_half: UnixStream, + next_id: AtomicU64, +} + +impl IpcClient { + /// Connects to breadcastd's socket and starts a background thread + /// forwarding every parsed `ServerMessage` (both responses and pushed + /// events — the caller distinguishes them, see `ServerMessage`'s + /// variants) to the returned receiver, until the connection closes + /// (breadcastd not running, or it exited). + pub fn connect() -> Result<(Self, mpsc::Receiver)> { + let path = socket_path()?; + let write_half = UnixStream::connect(&path) + .with_context(|| format!("failed to connect to breadcastd at {} — is it running?", path.display()))?; + let read_half = write_half.try_clone().context("failed to duplicate the IPC socket handle")?; + + let (tx, rx) = mpsc::channel(); + std::thread::spawn(move || { + let reader = BufReader::new(read_half); + for line in reader.lines() { + let Ok(line) = line else { break }; + if line.trim().is_empty() { + continue; + } + match serde_json::from_str::(&line) { + Ok(message) => { + if tx.send(message).is_err() { + break; + } + } + Err(e) => eprintln!("breadcast: malformed IPC message from breadcastd, ignoring: {e} ({line})"), + } + } + }); + + Ok((Self { write_half, next_id: AtomicU64::new(1) }, rx)) + } + + /// Sends a request and returns its id (so the caller can match it + /// against the `ServerMessage::Response` that arrives later on the + /// receiver from [`Self::connect`] — this method doesn't itself wait + /// for a reply, matching the GTK main loop's non-blocking event style). + pub fn send(&mut self, method: &str, params: serde_json::Value) -> Result { + let id = self.next_id.fetch_add(1, Ordering::Relaxed); + let request = ClientRequest { id, method: method.to_string(), params }; + let mut line = serde_json::to_string(&request).context("failed to serialize IPC request")?; + line.push('\n'); + self.write_half.write_all(line.as_bytes()).context("failed to write to breadcastd's IPC socket")?; + Ok(id) + } +} diff --git a/breadcast/src/main.rs b/breadcast/src/main.rs new file mode 100644 index 0000000..e86ac43 --- /dev/null +++ b/breadcast/src/main.rs @@ -0,0 +1,202 @@ +//! breadcast — GTK4 layer-shell popup: a thin IPC client of `breadcastd` +//! (see `breadcast_core::ipc` for the wire protocol). Shows discovered Cast +//! devices and start/stop controls; holds no pipeline or protocol code of +//! its own — that all lives in `breadcastd`, so closing this popup never +//! interrupts an active cast. + +mod css; +mod ipc_client; + +use std::cell::RefCell; +use std::rc::Rc; + +use bread_theme::load_palette; +use breadcast_core::ipc::{DeviceInfo, Protocol, ServerMessage, StateInfo}; +use gtk4::prelude::*; +use gtk4::{Align, Application, Box as GBox, Button, Label, ListBox, Orientation, SelectionMode, glib}; +use ipc_client::IpcClient; + +const PANEL_WIDTH: i32 = 360; + +fn main() { + let _singleton_guard = match bread_utils::singleton::toggle_or_kill("breadcast") { + Ok(bread_utils::singleton::Toggle::Started(guard)) => Some(guard), + Ok(bread_utils::singleton::Toggle::KilledExisting) => return, + Err(e) => { + eprintln!("breadcast: single-instance lock unavailable ({e}); continuing without it"); + None + } + }; + + let app = Application::builder().application_id("com.breadway.breadcast").build(); + app.connect_activate(build_ui); + app.run(); +} + +fn build_ui(app: &Application) { + bread_theme::gtk::apply_shared(); + bread_theme::gtk::apply_app_css(|| css::build_css(&load_palette())); + + let window = bread_utils::gtk_popup::new_overlay_window(app, "breadcast"); + + let panel = GBox::new(Orientation::Vertical, 8); + panel.add_css_class("cast-panel"); + panel.set_size_request(PANEL_WIDTH, -1); + panel.set_halign(Align::Center); + panel.set_valign(Align::Center); + + let header = GBox::new(Orientation::Horizontal, 8); + let title = Label::new(Some("Cast")); + title.add_css_class("cast-title"); + title.set_hexpand(true); + title.set_halign(Align::Start); + let status_pill = Label::new(Some("Idle")); + status_pill.add_css_class("cast-status-pill"); + header.append(&title); + header.append(&status_pill); + panel.append(&header); + + let stop_button = Button::with_label("Stop mirroring"); + stop_button.add_css_class("cast-stop-button"); + stop_button.set_visible(false); + panel.append(&stop_button); + + let list = ListBox::new(); + list.set_selection_mode(SelectionMode::Browse); + panel.append(&list); + + let empty_label = Label::new(Some("Searching for devices...")); + empty_label.add_css_class("cast-empty-label"); + panel.append(&empty_label); + + window.set_child(Some(&panel)); + bread_utils::gtk_popup::close_on_outside_click(&window, &panel, { + let window = window.clone(); + move || window.close() + }); + + match IpcClient::connect() { + Ok((client, rx)) => { + let client = Rc::new(RefCell::new(client)); + let _ = client.borrow_mut().send("list_devices", serde_json::Value::Null); + let _ = client.borrow_mut().send("get_state", serde_json::Value::Null); + + list.connect_row_activated({ + let client = client.clone(); + move |_, row| { + let Some(device_id) = (unsafe { row.data::("device_id") }) else { return }; + let device_id = unsafe { device_id.as_ref() }.clone(); + let _ = client.borrow_mut().send("start_cast", serde_json::json!({ "device_id": device_id })); + } + }); + + stop_button.connect_clicked({ + let client = client.clone(); + move |_| { + let _ = client.borrow_mut().send("stop_cast", serde_json::Value::Null); + } + }); + + let list = list.clone(); + let empty_label = empty_label.clone(); + let status_pill = status_pill.clone(); + let stop_button = stop_button.clone(); + glib::timeout_add_local(std::time::Duration::from_millis(100), move || { + while let Ok(message) = rx.try_recv() { + handle_server_message(message, &list, &empty_label, &status_pill, &stop_button); + } + glib::ControlFlow::Continue + }); + } + Err(e) => { + empty_label.set_label(&format!("breadcastd isn't running ({e})")); + } + } + + window.present(); +} + +/// Both a successful `"list_devices"`/`"get_state"` response and the +/// corresponding pushed event carry the same JSON shape in `result`/`data` +/// respectively — this normalizes both into one dispatch so there's only +/// one device-list/state rendering path to keep in sync. +fn handle_server_message( + message: ServerMessage, + list: &ListBox, + empty_label: &Label, + status_pill: &Label, + stop_button: &Button, +) { + let payload = match message { + ServerMessage::Event { event, data } => Some((event, data)), + ServerMessage::Response { result: Some(result), .. } if result.is_array() => { + Some(("device_list_changed".to_string(), result)) + } + ServerMessage::Response { result: Some(result), .. } if result.get("state").is_some() => { + Some(("state_changed".to_string(), result)) + } + ServerMessage::Response { error: Some(error), .. } => { + eprintln!("breadcast: request failed: {error}"); + None + } + _ => None, + }; + let Some((event, data)) = payload else { return }; + match event.as_str() { + "device_list_changed" => update_device_list(list, empty_label, data), + "state_changed" => update_state(status_pill, stop_button, data), + _ => {} + } +} + +fn update_device_list(list: &ListBox, empty_label: &Label, data: serde_json::Value) { + let Ok(devices) = serde_json::from_value::>(data) else { return }; + + while let Some(row) = list.row_at_index(0) { + list.remove(&row); + } + + empty_label.set_visible(devices.is_empty()); + list.set_visible(!devices.is_empty()); + + for device in &devices { + let row = gtk4::ListBoxRow::new(); + unsafe { row.set_data("device_id", device.id.clone()) }; + + let row_box = GBox::new(Orientation::Vertical, 2); + row_box.add_css_class("cast-device-row"); + let name = Label::new(Some(&device.name)); + name.set_halign(Align::Start); + let model = Label::new(Some(&format!("{} · {}", device.model, protocol_label(device.protocol)))); + model.add_css_class("cast-device-model"); + model.set_halign(Align::Start); + row_box.append(&name); + row_box.append(&model); + + row.set_child(Some(&row_box)); + list.append(&row); + } +} + +fn protocol_label(protocol: Protocol) -> &'static str { + match protocol { + Protocol::Cast => "Cast", + Protocol::Dlna => "DLNA", + } +} + +fn update_state(status_pill: &Label, stop_button: &Button, data: serde_json::Value) { + let Ok(state) = serde_json::from_value::(data) else { return }; + match state { + StateInfo::Idle => { + status_pill.set_label("Idle"); + status_pill.remove_css_class("casting"); + stop_button.set_visible(false); + } + StateInfo::Casting { device_name, .. } => { + status_pill.set_label(&format!("Casting to {device_name}")); + status_pill.add_css_class("casting"); + stop_button.set_visible(true); + } + } +} diff --git a/breadcastd/Cargo.toml b/breadcastd/Cargo.toml new file mode 100644 index 0000000..0ddd21c --- /dev/null +++ b/breadcastd/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "breadcastd" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +description = "breadcast background daemon: discovery, capture/encode/serve pipeline, Cast V2 session" + +[[bin]] +name = "breadcastd" +path = "src/main.rs" + +[dependencies] +breadcast-core = { path = "../breadcast-core" } +anyhow = { workspace = true } +serde = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +tokio = { workspace = true } +serde_json = { workspace = true } +rust_cast = { version = "0.21", features = ["thread_safe"] } +gstreamer = "0.25" +gstreamer-app = "0.25" +bread-utils = { git = "https://git.breadway.dev/Breadway/bread-ecosystem", tag = "v0.7.1", features = ["bread-client"] } diff --git a/breadcastd/src/bread_events.rs b/breadcastd/src/bread_events.rs new file mode 100644 index 0000000..f67e778 --- /dev/null +++ b/breadcastd/src/bread_events.rs @@ -0,0 +1,93 @@ +//! `bread.cast.*` event integration — optional, non-blocking. See +//! `EVENTS.md` at the repo root for the full contract. breadcastd works +//! identically with or without breadd running; every call here is +//! fire-and-forget (`BreadClient::emit` never blocks or errors this +//! process) so a missing or restarting breadd never affects discovery or +//! mirroring itself. + +use bread_utils::bread_client::{BreadClient, BreadEvent}; +use tokio::sync::{mpsc, oneshot}; + +use crate::daemon::DaemonCommand; + +/// This app's id in bread's sibling-app namespace registry +/// (`bread_shared::apps::KNOWN_APPS`) — events publish as `bread.cast.*`, +/// commands arrive on `bread.command.cast.*`. +pub const APP_ID: &str = "cast"; + +pub fn emit_device_found(client: &BreadClient, id: &str, name: &str, model: &str, protocol: &str) { + client.emit( + "bread.cast.device_found", + serde_json::json!({ + "id": id, + "name": name, + "model": model, + "protocol": protocol, + }), + ); +} + +pub fn emit_mirroring_started(client: &BreadClient, device_id: &str, device_name: &str, protocol: &str) { + client.emit( + "bread.cast.mirroring_started", + serde_json::json!({ + "device_id": device_id, + "device_name": device_name, + "protocol": protocol, + }), + ); +} + +pub fn emit_mirroring_stopped(client: &BreadClient) { + client.emit("bread.cast.mirroring_stopped", serde_json::json!({})); +} + +pub fn emit_mirroring_failed(client: &BreadClient, device_id: &str, error: &str) { + client.emit( + "bread.cast.mirroring_failed", + serde_json::json!({ + "device_id": device_id, + "error": error, + }), + ); +} + +/// Reacts to `bread.command.cast.*` verbs, e.g. from a Hyprland keybind. +/// Runs on `BreadClient::subscribe`'s dedicated background thread (a plain +/// `std::thread`, not a tokio worker) — `mpsc::Sender::blocking_send` is +/// safe to call from here for exactly that reason, but would panic if +/// called from inside the tokio runtime. +/// +/// Both verbs reply is intentionally not awaited: the reply channel exists +/// because `DaemonCommand::StartCast`/`StopCast` need one for the IPC +/// socket's request/response use (see `ipc.rs`), but a fire-and-forget bus +/// command has nowhere to deliver a reply to anyway — the outcome shows up +/// as a `bread.cast.mirroring_started`/`.failed`/`.stopped` event instead +/// (see `daemon.rs`'s `start_cast`/`StopCast` handling). +pub fn handle_command(event: &BreadEvent, daemon_tx: &mpsc::Sender) { + let Some(verb) = event.event.strip_prefix("bread.command.cast.") else { + return; + }; + match verb { + "start" => { + let Some(device_id) = event.data.get("device_id").and_then(|v| v.as_str()) else { + tracing::warn!("bread.command.cast.start missing a string \"device_id\", ignoring"); + return; + }; + let (reply, _reply_rx) = oneshot::channel(); + if daemon_tx + .blocking_send(DaemonCommand::StartCast { device_id: device_id.to_string(), reply }) + .is_err() + { + tracing::warn!("daemon actor unavailable, dropping bread.command.cast.start"); + } + } + "stop" => { + let (reply, _reply_rx) = oneshot::channel(); + if daemon_tx.blocking_send(DaemonCommand::StopCast { reply }).is_err() { + tracing::warn!("daemon actor unavailable, dropping bread.command.cast.stop"); + } + } + other => tracing::info!(verb = other, "ignoring unknown bread.command.cast verb"), + } +} diff --git a/breadcastd/src/cast_mirror.rs b/breadcastd/src/cast_mirror.rs new file mode 100644 index 0000000..4326d33 --- /dev/null +++ b/breadcastd/src/cast_mirror.rs @@ -0,0 +1,202 @@ +//! Owns one active Cast Streaming mirroring session end-to-end: portal +//! capture, the GStreamer encode pipeline, the CASTV2 connection to the +//! Mirroring receiver, and the three pump threads that shuttle +//! OFFER/ANSWER messages and encoded frames between them. This is +//! `cast_stream_test.rs`'s orchestration, restructured into something the +//! daemon can start and stop on demand instead of running for a fixed +//! duration from a CLI `main`. +//! +//! The Cast Streaming (low-latency, RTP-based) path — see `dlna_mirror.rs` +//! for the DLNA/UPnP counterpart (HLS-over-HTTP, polled instead of pushed). + +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; + +use anyhow::{Context, Result}; +use breadcast_core::caststream::{CastStreamEvent, VideoParams, WEBRTC_NAMESPACE}; +use breadcast_core::pipeline::{ + build_video_pipeline_for_streaming, pull_encoded_frame, request_key_frame, set_video_bitrate_kbps, +}; +use breadcast_core::{CastDevice, CaptureSession, CastSession, CastStreamSender}; +use gstreamer as gst; +use gstreamer::prelude::*; +use rust_cast::channels::receiver::CastDeviceApp; + +use crate::daemon::DaemonCommand; + +pub struct CastMirrorSession { + pipeline: gst::Pipeline, + session: CastSession, + capture: Option, + threads: Vec>, +} + +impl CastMirrorSession { + /// Starts mirroring to `device`. Blocks (briefly) on the portal picker, + /// the CASTV2 handshake, and OFFER/ANSWER negotiation before returning + /// -- by the time this resolves, frames are already flowing. + /// + /// `daemon_tx` is used to report unprompted session death (a GStreamer + /// error, the user clicking "stop sharing" in the portal picker, the + /// receiver dropping the connection) back to the daemon actor, so it + /// can transition back to `Idle` and notify GUI clients even if nobody + /// called `stop()`. + pub async fn start(device: CastDevice, daemon_tx: tokio::sync::mpsc::Sender) -> Result { + let capture = CaptureSession::start().await.context("failed to start portal screen capture")?; + let video_node_id = capture.video_node_id(); + + let (pipeline, appsink, encoder) = + build_video_pipeline_for_streaming(video_node_id).context("failed to build the encode pipeline")?; + + { + let pipeline_watch = pipeline.clone(); + std::thread::spawn(move || { + match breadcast_core::pipeline::run_until_error_or_timeout(&pipeline_watch, gst::ClockTime::from_seconds(3600)) + { + Ok(outcome) => tracing::debug!(?outcome, "encode pipeline bus watcher ended"), + Err(e) => tracing::error!(error = ?e, "encode pipeline error"), + } + }); + } + + // The blocking CASTV2 TCP+TLS handshake + app launch is quick + // (milliseconds on a LAN) but still blocking I/O -- run it off the + // async worker thread pool rather than stalling it, even briefly. + let device_for_connect = device.clone(); + let (session, _media_events, raw_messages) = tokio::task::spawn_blocking(move || { + CastSession::connect_app( + &device_for_connect, + CastDeviceApp::Custom(breadcast_core::caststream::MIRRORING_APP_ID.to_string()), + ) + }) + .await + .context("connect_app task panicked")? + .context("failed to connect and launch the Mirroring receiver")?; + + let (sender, stream_events) = + CastStreamSender::start(&device.host, "sender-0", session.transport_id(), VideoParams::default()) + .context("failed to start the Cast Streaming session")?; + let sender = Arc::new(sender); + + let mut threads = Vec::new(); + + threads.push({ + let sender = sender.clone(); + std::thread::spawn(move || { + while let Some(msg) = raw_messages.recv() { + if msg.namespace == WEBRTC_NAMESPACE { + sender.on_message(&msg.source_id, &msg.namespace, &msg.message); + } + } + }) + }); + + let negotiated = Arc::new(AtomicBool::new(false)); + threads.push({ + let session = session.clone(); + let negotiated = negotiated.clone(); + std::thread::spawn(move || { + while let Ok(event) = stream_events.recv() { + match event { + CastStreamEvent::OutboundMessage { message, .. } => { + if let Err(e) = session.send_raw_message(WEBRTC_NAMESPACE, &message) { + tracing::warn!(error = ?e, "failed to send Cast Streaming message"); + } + } + CastStreamEvent::Negotiated => negotiated.store(true, Ordering::Release), + CastStreamEvent::Error(message) => tracing::warn!(%message, "Cast Streaming error"), + CastStreamEvent::PictureLost => tracing::debug!("receiver reported picture loss"), + } + } + }) + }); + + tracing::info!(device = %device.name, "sending Cast Streaming OFFER"); + sender.negotiate(); + + let deadline = tokio::time::Instant::now() + tokio::time::Duration::from_secs(10); + while !negotiated.load(Ordering::Acquire) && tokio::time::Instant::now() < deadline { + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; + } + if !negotiated.load(Ordering::Acquire) { + let _ = session.stop(); + capture.close().await.ok(); + anyhow::bail!("never received an ANSWER from {} (negotiation timed out)", device.name); + } + + pipeline.set_state(gst::State::Playing).context("failed to start the encode pipeline")?; + tracing::info!(device = %device.name, "mirroring started"); + + threads.push({ + let device_name = device.name.clone(); + std::thread::spawn(move || { + let result = frame_pump_loop(&appsink, &encoder, &sender); + if let Err(e) = result { + tracing::warn!(device = %device_name, error = ?e, "frame pump ended with an error"); + } + // Best-effort: if this is running, the daemon actor is (or + // was, very recently) still alive. If the channel is full or + // closed, there's nothing more useful to do from this + // thread than drop the notification. + let _ = daemon_tx.blocking_send(DaemonCommand::SessionEnded); + }) + }); + + Ok(Self { pipeline, session, capture: Some(capture), threads }) + } + + /// Tears down the session: stops the pipeline (which unblocks the frame + /// pump thread's blocking `appsink.pull_sample()` call), stops the + /// CASTV2 session (which ends its io thread, closing the channels the + /// other two pump threads block on), then joins every thread. + pub async fn stop(mut self) { + if let Err(e) = self.pipeline.set_state(gst::State::Null) { + tracing::warn!(error = ?e, "failed to stop the encode pipeline cleanly"); + } + if let Err(e) = self.session.stop() { + tracing::warn!(error = ?e, "failed to cleanly stop the cast session"); + } + if let Some(capture) = self.capture.take() { + if let Err(e) = capture.close().await { + tracing::warn!(error = ?e, "failed to cleanly close the portal capture session"); + } + } + for thread in self.threads.drain(..) { + // These threads all end once the pipeline/session teardown + // above propagates to them (see this method's own doc comment) + // -- `spawn_blocking` just keeps `.join()`'s wait off the async + // runtime's worker threads. + if let Err(panic) = tokio::task::spawn_blocking(move || thread.join()).await { + tracing::warn!(error = ?panic, "mirror session pump thread join task panicked"); + } + } + } +} + +fn frame_pump_loop( + appsink: &gstreamer_app::AppSink, + encoder: &gst::Element, + sender: &CastStreamSender, +) -> Result<()> { + let mut last_bitrate_update = std::time::Instant::now(); + loop { + let Some((data, is_key_frame, capture_time_us)) = pull_encoded_frame(appsink)? else { + return Ok(()); // EOS -- pipeline was set to Null, or the portal source ended + }; + + if sender.needs_key_frame() && !is_key_frame { + request_key_frame(appsink); + } + + if let Err(e) = sender.enqueue_frame(&data, is_key_frame, capture_time_us) { + tracing::debug!(error = ?e, "dropped a frame (not negotiated yet or backpressure)"); + } + + if last_bitrate_update.elapsed() >= std::time::Duration::from_secs(1) { + let bps = sender.estimated_bandwidth_bps(); + let target_kbps = ((bps as f64 * 0.85) / 1000.0).max(500.0) as u32; + set_video_bitrate_kbps(encoder, target_kbps); + last_bitrate_update = std::time::Instant::now(); + } + } +} diff --git a/breadcastd/src/daemon.rs b/breadcastd/src/daemon.rs new file mode 100644 index 0000000..dbb3f21 --- /dev/null +++ b/breadcastd/src/daemon.rs @@ -0,0 +1,221 @@ +//! The daemon's single state-owning actor: current `StateInfo`, the known +//! Cast and DLNA device lists, and the active mirroring session (if any) +//! all live here, touched only by this task -- the same "single owner + +//! channel" pattern `breadcast-core::cast_sender::CastSession` uses for its +//! io thread, for the same reason (avoids retrofitting locks around state +//! that multiple IPC connections and two independent discovery loops all +//! need to touch). + +use std::collections::HashMap; + +use bread_utils::bread_client::BreadClient; +use breadcast_core::ipc::{DeviceInfo, Protocol, ServerMessage, StateInfo}; +use breadcast_core::{CastDevice, DlnaDevice}; +use tokio::sync::{broadcast, mpsc, oneshot}; + +use crate::bread_events; +use crate::cast_mirror::CastMirrorSession; +use crate::dlna_mirror::DlnaMirrorSession; + +pub enum DaemonCommand { + ListDevices { reply: oneshot::Sender> }, + GetState { reply: oneshot::Sender }, + StartCast { device_id: String, reply: oneshot::Sender> }, + StopCast { reply: oneshot::Sender> }, + CastDeviceFound(CastDevice), + CastDeviceLost(String), + DlnaDeviceFound(DlnaDevice), + /// DLNA devices have no separate stable id -- their description URL + /// (`DlnaDevice::url`) doubles as one, see `DlnaDevice`'s doc comment. + DlnaDeviceLost(String), + /// Sent by an active session's own background thread/task when it ends + /// on its own (portal "stop sharing", a GStreamer error, the receiver + /// dropping the connection or stopping playback) -- as opposed to + /// `StopCast` being called. Either way the daemon needs to forget the + /// (now-dead) session and go back to `Idle`. + SessionEnded, +} + +pub fn spawn(events_tx: broadcast::Sender, bread_client: BreadClient) -> mpsc::Sender { + let (command_tx, mut command_rx) = mpsc::channel(32); + let self_tx = command_tx.clone(); + tokio::spawn(async move { + let mut daemon = Daemon { + cast_devices: HashMap::new(), + dlna_devices: HashMap::new(), + state: StateInfo::Idle, + active_session: None, + events_tx, + bread_client, + self_tx, + }; + while let Some(command) = command_rx.recv().await { + daemon.handle(command).await; + } + }); + command_tx +} + +/// The currently active mirroring session, if any -- exactly one of the two +/// protocol-specific session types, chosen by which device map `StartCast` +/// found the requested device id in. +enum ActiveSession { + Cast(CastMirrorSession), + Dlna(Box), +} + +impl ActiveSession { + async fn stop(self) { + match self { + ActiveSession::Cast(session) => session.stop().await, + ActiveSession::Dlna(session) => session.stop().await, + } + } +} + +struct Daemon { + cast_devices: HashMap, + /// Keyed by `DlnaDevice::url`, the closest thing DLNA has to a stable + /// device id -- see `breadcast_core::ipc::DeviceInfo`'s doc comment. + dlna_devices: HashMap, + state: StateInfo, + active_session: Option, + events_tx: broadcast::Sender, + /// Used to publish `bread.cast.mirroring_started`/`.stopped`/`.failed` + /// on state transitions -- see `bread_events.rs`. A no-op if breadd + /// isn't running (see that module's doc comment). + bread_client: BreadClient, + /// A clone of this actor's own command sender, handed to each mirror + /// session so its background pump/poll can report unprompted death + /// (see `DaemonCommand::SessionEnded`) without this module needing to + /// expose anything beyond the command channel itself. + self_tx: mpsc::Sender, +} + +impl Daemon { + async fn handle(&mut self, command: DaemonCommand) { + match command { + DaemonCommand::ListDevices { reply } => { + let _ = reply.send(self.device_list()); + } + DaemonCommand::GetState { reply } => { + let _ = reply.send(self.state.clone()); + } + DaemonCommand::StartCast { device_id, reply } => { + self.start_cast(device_id, reply).await; + } + DaemonCommand::StopCast { reply } => { + if let Some(session) = self.active_session.take() { + session.stop().await; + bread_events::emit_mirroring_stopped(&self.bread_client); + } + self.state = StateInfo::Idle; + self.broadcast_state(); + let _ = reply.send(Ok(())); + } + DaemonCommand::SessionEnded => { + // The session already tore itself down (that's what + // triggered this) -- just drop our handle to it and update + // state. Ignored if this arrives after an explicit + // `StopCast` already cleared `active_session` (the + // background pump/poll it came from may briefly outlive + // that call). + if self.active_session.take().is_some() { + tracing::info!("mirror session ended on its own, returning to idle"); + self.state = StateInfo::Idle; + self.broadcast_state(); + bread_events::emit_mirroring_stopped(&self.bread_client); + } + } + DaemonCommand::CastDeviceFound(device) => { + self.cast_devices.insert(device.id.clone(), device); + self.broadcast_devices(); + } + DaemonCommand::CastDeviceLost(id) => { + self.cast_devices.remove(&id); + self.broadcast_devices(); + } + DaemonCommand::DlnaDeviceFound(device) => { + self.dlna_devices.insert(device.url.clone(), device); + self.broadcast_devices(); + } + DaemonCommand::DlnaDeviceLost(url) => { + self.dlna_devices.remove(&url); + self.broadcast_devices(); + } + } + } + + async fn start_cast(&mut self, device_id: String, reply: oneshot::Sender>) { + if self.active_session.is_some() { + let _ = reply.send(Err("already casting -- stop the current session first".to_string())); + return; + } + + if let Some(device) = self.cast_devices.get(&device_id).cloned() { + match CastMirrorSession::start(device.clone(), self.self_tx.clone()).await { + Ok(session) => { + self.active_session = Some(ActiveSession::Cast(session)); + self.state = + StateInfo::Casting { device_id: device.id.clone(), device_name: device.name.clone(), protocol: Protocol::Cast }; + self.broadcast_state(); + bread_events::emit_mirroring_started(&self.bread_client, &device.id, &device.name, "cast"); + let _ = reply.send(Ok(())); + } + Err(e) => { + bread_events::emit_mirroring_failed(&self.bread_client, &device.id, &e.to_string()); + let _ = reply.send(Err(e.to_string())); + } + } + return; + } + + if let Some(device) = self.dlna_devices.get(&device_id).cloned() { + match DlnaMirrorSession::start(device.clone(), self.self_tx.clone()).await { + Ok(session) => { + self.active_session = Some(ActiveSession::Dlna(Box::new(session))); + self.state = StateInfo::Casting { + device_id: device.url.clone(), + device_name: device.friendly_name.clone(), + protocol: Protocol::Dlna, + }; + self.broadcast_state(); + bread_events::emit_mirroring_started(&self.bread_client, &device.url, &device.friendly_name, "dlna"); + let _ = reply.send(Ok(())); + } + Err(e) => { + bread_events::emit_mirroring_failed(&self.bread_client, &device.url, &e.to_string()); + let _ = reply.send(Err(e.to_string())); + } + } + return; + } + + let _ = reply.send(Err(format!("unknown device id \"{device_id}\""))); + } + + fn device_list(&self) -> Vec { + let mut devices: Vec = self + .cast_devices + .values() + .map(|d| DeviceInfo { id: d.id.clone(), name: d.name.clone(), model: d.model.clone(), protocol: Protocol::Cast }) + .collect(); + devices.extend(self.dlna_devices.values().map(|d| DeviceInfo { + id: d.url.clone(), + name: d.friendly_name.clone(), + model: "DLNA renderer".to_string(), + protocol: Protocol::Dlna, + })); + devices + } + + fn broadcast_state(&self) { + let data = serde_json::to_value(&self.state).expect("StateInfo always serializes"); + let _ = self.events_tx.send(ServerMessage::Event { event: "state_changed".to_string(), data }); + } + + fn broadcast_devices(&self) { + let data = serde_json::to_value(self.device_list()).expect("Vec always serializes"); + let _ = self.events_tx.send(ServerMessage::Event { event: "device_list_changed".to_string(), data }); + } +} diff --git a/breadcastd/src/dlna_mirror.rs b/breadcastd/src/dlna_mirror.rs new file mode 100644 index 0000000..73262de --- /dev/null +++ b/breadcastd/src/dlna_mirror.rs @@ -0,0 +1,139 @@ +//! Owns one active DLNA/UPnP mirroring session: portal capture, the +//! GStreamer HLS encode pipeline, the local HTTP server, and the +//! `AVTransport` control session. This is `dlna_mirror_test.rs`'s +//! orchestration, restructured into something the daemon can start and +//! stop on demand — the DLNA counterpart to `cast_mirror.rs`'s +//! `CastMirrorSession`. +//! +//! Unlike the Cast Streaming path, there's no persistent bidirectional +//! connection to a DLNA renderer to read events from — `AVTransport` is a +//! plain SOAP request/response protocol (see `DlnaSession`'s doc comment), +//! so "did the renderer stop on its own" can only be *polled*, not pushed. + +use std::time::Duration; + +use anyhow::{Context, Result}; +use breadcast_core::net::local_lan_ip; +use breadcast_core::pipeline::{build_video_pipeline, hls_output_dir, wait_for_playlist_segments}; +use breadcast_core::{CaptureSession, DlnaDevice, DlnaSession}; +use gstreamer as gst; +use gstreamer::prelude::*; + +use crate::daemon::DaemonCommand; + +/// How often to poll `GetTransportInfo` for an unprompted stop (the user +/// stopped playback from the TV's own remote, or the renderer just dropped +/// the stream) — see [`DlnaSession::transport_state`]'s doc comment for why +/// this is a poll, not a push. +const POLL_INTERVAL: Duration = Duration::from_secs(3); + +pub struct DlnaMirrorSession { + pipeline: gst::Pipeline, + session: DlnaSession, + capture: Option, + poll_task: tokio::task::JoinHandle<()>, +} + +impl DlnaMirrorSession { + /// Starts mirroring to `device`. Blocks (briefly) on the portal picker, + /// pipeline startup, and the renderer accepting the `SetAVTransportURI` + /// + `Play` actions before returning. + /// + /// `daemon_tx` is used to report an unprompted session end (the + /// renderer stopping playback on its own, a GStreamer error, or the + /// renderer becoming unreachable) back to the daemon actor — mirrors + /// `CastMirrorSession::start`'s same use of it. + pub async fn start(device: DlnaDevice, daemon_tx: tokio::sync::mpsc::Sender) -> Result { + let capture = CaptureSession::start().await.context("failed to start portal screen capture")?; + let video_node_id = capture.video_node_id(); + + let output_dir = hls_output_dir("dlna-mirror")?; + let pipeline = + build_video_pipeline(video_node_id, &output_dir).context("failed to build the encode pipeline")?; + + // Fire-and-forget, same as `CastMirrorSession::start`'s identical + // block: nothing joins this thread, it just self-terminates on + // pipeline error, EOS, or its own 1-hour timeout, whichever is + // first — see that function's doc comment for why that's fine. + { + let pipeline_watch = pipeline.clone(); + std::thread::spawn(move || { + match breadcast_core::pipeline::run_until_error_or_timeout(&pipeline_watch, gst::ClockTime::from_seconds(3600)) + { + Ok(outcome) => tracing::debug!(?outcome, "DLNA encode pipeline bus watcher ended"), + Err(e) => tracing::error!(error = ?e, "DLNA encode pipeline error"), + } + }); + } + + pipeline.set_state(gst::State::Playing).context("failed to start the encode pipeline")?; + + let lan_ip = local_lan_ip().context("failed to determine this machine's LAN-reachable IP")?; + // Bind an ephemeral port (`:0`) rather than a fixed one like the + // `dlna_mirror_test` example uses -- the daemon may need to run + // alongside that example, or a future concurrent-session mode, + // without a bind conflict. `HttpServer::start`'s worker threads + // outlive this session once it stops (documented pre-existing + // limitation, see `http_server.rs` -- not something introduced + // here); one leaked idle listener per DLNA cast is an accepted + // cost until that gets a real shutdown path. + let http = breadcast_core::http_server::HttpServer::start("0.0.0.0:0", output_dir.clone()) + .context("failed to start the HLS HTTP server")?; + let stream_url = http.url(lan_ip, "playlist.m3u8"); + + wait_for_playlist_segments(&output_dir.join("playlist.m3u8"), 3, Duration::from_secs(20)) + .await + .context("encode pipeline never produced playable HLS segments")?; + + let session = DlnaSession::connect(&device).await.context("failed to connect to the DLNA renderer")?; + session.load(&stream_url).await.context("renderer rejected the stream load")?; + tracing::info!(device = %device.friendly_name, %stream_url, "DLNA mirroring started"); + + let poll_task = { + let session = session.clone(); + let device_name = device.friendly_name.clone(); + tokio::spawn(async move { + loop { + tokio::time::sleep(POLL_INTERVAL).await; + match session.transport_state().await { + Ok(state) if state == "STOPPED" || state == "NO_MEDIA_PRESENT" => { + tracing::info!(device = %device_name, %state, "DLNA renderer ended playback on its own"); + let _ = daemon_tx.send(DaemonCommand::SessionEnded).await; + return; + } + Ok(_) => {} + Err(e) => { + tracing::warn!(device = %device_name, error = ?e, "DLNA transport state poll failed, treating renderer as gone"); + let _ = daemon_tx.send(DaemonCommand::SessionEnded).await; + return; + } + } + } + }) + }; + + Ok(Self { pipeline, session, capture: Some(capture), poll_task }) + } + + /// Tears down the session: stops polling, tells the renderer to stop, + /// stops the encode pipeline, and closes the portal capture session. + pub async fn stop(mut self) { + // A request, not a wait -- if the poll task is mid-poll and sends + // one more `SessionEnded` right as this races it, that's harmless: + // `daemon.rs`'s handler already no-ops when `active_session` was + // already cleared by this explicit stop. + self.poll_task.abort(); + + if let Err(e) = self.session.stop().await { + tracing::warn!(error = ?e, "failed to cleanly stop the DLNA session"); + } + if let Err(e) = self.pipeline.set_state(gst::State::Null) { + tracing::warn!(error = ?e, "failed to stop the encode pipeline cleanly"); + } + if let Some(capture) = self.capture.take() { + if let Err(e) = capture.close().await { + tracing::warn!(error = ?e, "failed to cleanly close the portal capture session"); + } + } + } +} diff --git a/breadcastd/src/ipc.rs b/breadcastd/src/ipc.rs new file mode 100644 index 0000000..d8cbd85 --- /dev/null +++ b/breadcastd/src/ipc.rs @@ -0,0 +1,175 @@ +//! breadcastd's private control socket: newline-delimited JSON over a +//! `UnixListener` at `$XDG_RUNTIME_DIR/breadcast/breadcastd.sock`, separate +//! from breadd's own pub/sub bus (`bread_events.rs`) — that's for external +//! automation (a Hyprland keybind emitting `bread.command.cast.*`), this is +//! for the `breadcast` GTK4 popup's actual live state, which needs pushed +//! updates a best-effort pub/sub bus doesn't fit as naturally. +//! +//! The wire message types (`ClientRequest`/`ServerMessage`/`StateInfo`) live +//! in `breadcast_core::ipc`, shared with the `breadcast` GUI client so the +//! two never drift out of sync. +//! +//! Multiple clients (in practice: zero or one `breadcast` popup instance at +//! a time, but nothing here assumes that) can connect concurrently; each +//! gets its own copy of every broadcast event. + +use anyhow::{Context, Result}; +use breadcast_core::ipc::{ClientRequest, ServerMessage, socket_path}; +use serde_json::Value; +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::{UnixListener, UnixStream}; +use tokio::sync::{broadcast, mpsc}; + +use crate::daemon::DaemonCommand; + +/// Removes any stale socket left behind by an unclean shutdown, then binds +/// and serves forever. `daemon_tx` is how request handling reaches the +/// daemon's single state-owning actor (see `daemon.rs`); `events_tx`'s +/// receiver half is (re-)subscribed per connection, so every connection +/// sees the same `device_list_changed`/`state_changed` events from the +/// point it connects onward. +pub async fn serve(daemon_tx: mpsc::Sender, events_tx: broadcast::Sender) -> Result<()> { + let socket_path = socket_path()?; + if let Some(parent) = socket_path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("failed to create socket dir {}", parent.display()))?; + } + // A stale socket file from an unclean previous exit makes bind() fail + // with AddrInUse even though nothing is actually listening -- the + // daemon's own singleton lock (see main.rs) already guarantees at most + // one live breadcastd, so it's always safe to clear this before binding. + let _ = std::fs::remove_file(&socket_path); + + let listener = UnixListener::bind(&socket_path) + .with_context(|| format!("failed to bind {}", socket_path.display()))?; + tracing::info!(path = %socket_path.display(), "IPC socket listening"); + + loop { + let (stream, _addr) = listener.accept().await.context("failed to accept IPC connection")?; + let daemon_tx = daemon_tx.clone(); + let events_rx = events_tx.subscribe(); + tokio::spawn(async move { + if let Err(e) = handle_connection(stream, daemon_tx, events_rx).await { + tracing::debug!(error = %e, "IPC connection ended"); + } + }); + } +} + +async fn handle_connection( + stream: UnixStream, + daemon_tx: mpsc::Sender, + mut events_rx: broadcast::Receiver, +) -> Result<()> { + let (read_half, write_half) = stream.into_split(); + let mut reader = BufReader::new(read_half).lines(); + + // A single task owns the write half and serializes everything written + // to it (responses and pushed events both funnel through `writer_tx`) + // -- two independent tasks (one per direction) writing directly to the + // same stream could interleave partial JSON lines. + let (writer_tx, mut writer_rx) = mpsc::unbounded_channel::(); + let writer_task = tokio::spawn(async move { + let mut write_half = write_half; + while let Some(msg) = writer_rx.recv().await { + let Ok(mut line) = serde_json::to_string(&msg) else { continue }; + line.push('\n'); + if write_half.write_all(line.as_bytes()).await.is_err() { + return; + } + } + }); + + let forward_tx = writer_tx.clone(); + let forward_task = tokio::spawn(async move { + loop { + match events_rx.recv().await { + Ok(event) => { + if forward_tx.send(event).is_err() { + return; + } + } + Err(broadcast::error::RecvError::Lagged(_)) => continue, + Err(broadcast::error::RecvError::Closed) => return, + } + } + }); + + while let Some(line) = reader.next_line().await? { + if line.trim().is_empty() { + continue; + } + let request: ClientRequest = match serde_json::from_str(&line) { + Ok(req) => req, + Err(e) => { + tracing::debug!(error = %e, %line, "malformed IPC request, ignoring"); + continue; + } + }; + let response = handle_request(request, &daemon_tx).await; + if writer_tx.send(response).is_err() { + break; + } + } + + forward_task.abort(); + drop(writer_tx); + let _ = writer_task.await; + Ok(()) +} + +async fn handle_request(request: ClientRequest, daemon_tx: &mpsc::Sender) -> ServerMessage { + let id = request.id; + match request.method.as_str() { + "list_devices" => { + let (reply_tx, reply_rx) = tokio::sync::oneshot::channel(); + if daemon_tx.send(DaemonCommand::ListDevices { reply: reply_tx }).await.is_err() { + return ServerMessage::err(id, "daemon actor has stopped"); + } + match reply_rx.await { + Ok(devices) => ServerMessage::ok(id, serde_json::json!(devices)), + Err(_) => ServerMessage::err(id, "daemon actor dropped the reply"), + } + } + "get_state" => { + let (reply_tx, reply_rx) = tokio::sync::oneshot::channel(); + if daemon_tx.send(DaemonCommand::GetState { reply: reply_tx }).await.is_err() { + return ServerMessage::err(id, "daemon actor has stopped"); + } + match reply_rx.await { + Ok(state) => ServerMessage::ok(id, serde_json::json!(state)), + Err(_) => ServerMessage::err(id, "daemon actor dropped the reply"), + } + } + "start_cast" => { + let Some(device_id) = request.params.get("device_id").and_then(Value::as_str) else { + return ServerMessage::err(id, "start_cast requires a string \"device_id\" param"); + }; + let (reply_tx, reply_rx) = tokio::sync::oneshot::channel(); + if daemon_tx + .send(DaemonCommand::StartCast { device_id: device_id.to_string(), reply: reply_tx }) + .await + .is_err() + { + return ServerMessage::err(id, "daemon actor has stopped"); + } + match reply_rx.await { + Ok(Ok(())) => ServerMessage::ok(id, Value::Null), + Ok(Err(e)) => ServerMessage::err(id, e), + Err(_) => ServerMessage::err(id, "daemon actor dropped the reply"), + } + } + "stop_cast" => { + let (reply_tx, reply_rx) = tokio::sync::oneshot::channel(); + if daemon_tx.send(DaemonCommand::StopCast { reply: reply_tx }).await.is_err() { + return ServerMessage::err(id, "daemon actor has stopped"); + } + match reply_rx.await { + Ok(Ok(())) => ServerMessage::ok(id, Value::Null), + Ok(Err(e)) => ServerMessage::err(id, e), + Err(_) => ServerMessage::err(id, "daemon actor dropped the reply"), + } + } + other => ServerMessage::err(id, format!("unknown method \"{other}\"")), + } +} diff --git a/breadcastd/src/main.rs b/breadcastd/src/main.rs new file mode 100644 index 0000000..47cfde1 --- /dev/null +++ b/breadcastd/src/main.rs @@ -0,0 +1,122 @@ +//! breadcastd — background daemon: mDNS (Cast) and SSDP (DLNA) device +//! discovery, both mirroring session lifecycles (Cast Streaming and +//! DLNA/AVTransport), and a private IPC socket the `breadcast` GTK4 popup +//! talks to. Also does optional breadd event integration for external +//! automation (a Hyprland keybind, etc.) — see `bread_events.rs`; that's +//! separate from (and does not replace) the IPC socket, since a popup UI +//! needs live pushed state that a best-effort pub/sub bus doesn't fit as +//! naturally. See `ipc.rs`'s doc comment. + +mod bread_events; +mod cast_mirror; +mod daemon; +mod dlna_mirror; +mod ipc; + +use std::collections::HashMap; + +use bread_utils::singleton::{Acquire, try_acquire}; +use breadcast_core::{CastDevice, DlnaDevice, DlnaDiscovery, DlnaDiscoveryEvent, Discovery, DiscoveryEvent}; +use daemon::DaemonCommand; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + tracing_subscriber::fmt::init(); + + let _guard = match try_acquire(bread_events::APP_ID)? { + Acquire::Acquired(guard) => guard, + Acquire::HeldByOther(pid) => { + tracing::error!(?pid, "breadcastd already running, exiting"); + std::process::exit(1); + } + }; + + let bread_client = bread_utils::bread_client::BreadClient::connect(bread_events::APP_ID); + + let (events_tx, _events_rx) = tokio::sync::broadcast::channel(64); + let daemon_tx = daemon::spawn(events_tx.clone(), bread_client.clone()); + + let commands_daemon_tx = daemon_tx.clone(); + let _commands = bread_client.subscribe("bread.command.cast.**", move |event| { + bread_events::handle_command(&event, &commands_daemon_tx); + }); + + let ipc_daemon_tx = daemon_tx.clone(); + tokio::spawn(async move { + if let Err(e) = ipc::serve(ipc_daemon_tx, events_tx).await { + tracing::error!(error = ?e, "IPC server ended unexpectedly"); + } + }); + + let (_discovery, mut cast_events) = Discovery::start()?; + let (_dlna_discovery, mut dlna_events) = DlnaDiscovery::start(); + tracing::info!("breadcastd started, browsing for Cast and DLNA devices"); + + // `DiscoveryEvent::Found`/`DlnaDiscoveryEvent::Found` fire on every + // re-resolution/re-poll (see `discovery.rs`'s and `dlna/discovery.rs`'s + // doc comments), not just the first sighting. Every event is forwarded + // to the daemon actor unconditionally (its `HashMap` re-insert is a + // harmless no-op for an unchanged device, aside from a redundant but + // cheap `device_list_changed` broadcast) — but `bread.cast.device_found` + // on the breadd bus is gated on an actual *change*, so anything + // subscribed there expecting "on device_found, do X" doesn't fire + // repeatedly for the same device. + let mut known_cast_devices: HashMap = HashMap::new(); + let mut known_dlna_devices: HashMap = HashMap::new(); + + loop { + tokio::select! { + event = cast_events.recv() => { + match event { + Some(DiscoveryEvent::Found(device)) => { + if known_cast_devices.get(&device.id) != Some(&device) { + tracing::info!(?device, "Cast device found"); + bread_events::emit_device_found(&bread_client, &device.id, &device.name, &device.model, "cast"); + known_cast_devices.insert(device.id.clone(), device.clone()); + } + let _ = daemon_tx.send(DaemonCommand::CastDeviceFound(device)).await; + } + Some(DiscoveryEvent::Lost { id }) => { + tracing::info!(%id, "Cast device lost"); + known_cast_devices.remove(&id); + let _ = daemon_tx.send(DaemonCommand::CastDeviceLost(id)).await; + } + // The mDNS browse thread itself ended (daemon shutdown, + // an unrecoverable browse error) — that's Cast + // discovery's one job gone, not a normal exit. + // Returning `Ok(())` from `main` here would exit 0, and + // systemd's `Restart=on-failure` would never trigger. + None => { + tracing::error!("Cast discovery channel closed unexpectedly, exiting"); + std::process::exit(1); + } + } + } + event = dlna_events.recv() => { + match event { + Some(DlnaDiscoveryEvent::Found(device)) => { + if known_dlna_devices.get(&device.url) != Some(&device) { + tracing::info!(?device, "DLNA device found"); + bread_events::emit_device_found(&bread_client, &device.url, &device.friendly_name, "DLNA renderer", "dlna"); + known_dlna_devices.insert(device.url.clone(), device.clone()); + } + let _ = daemon_tx.send(DaemonCommand::DlnaDeviceFound(device)).await; + } + Some(DlnaDiscoveryEvent::Lost { url }) => { + tracing::info!(%url, "DLNA device lost"); + known_dlna_devices.remove(&url); + let _ = daemon_tx.send(DaemonCommand::DlnaDeviceLost(url)).await; + } + // Same reasoning as the Cast arm above -- `DlnaDiscovery`'s + // background task only ends via a panic or this process + // dropping every sender, neither of which should happen + // while this loop is still the one holding the receiver. + None => { + tracing::error!("DLNA discovery channel closed unexpectedly, exiting"); + std::process::exit(1); + } + } + } + } + } +} diff --git a/contrib/binds.json b/contrib/binds.json new file mode 100644 index 0000000..593e61f --- /dev/null +++ b/contrib/binds.json @@ -0,0 +1,6 @@ +{ + "_comment": "BOS-native keybind snippet (hyprland.lua + JSON config, not classic hyprland.conf — see CONTRIBUTING.md/README for context). There is no per-app self-registration mechanism today: merge this entry's contents into the 'bindings' array of BOS's own skel binds.json by hand, the same way breadclip's Super+V entry was added. Not auto-installed by bakery.", + "bindings": [ + { "action": "exec", "command": "breadcast", "key": "C", "label": "Cast your screen (breadcast)", "category": "apps", "demo_cmd": "breadcast" } + ] +} diff --git a/contrib/breadcastd.service b/contrib/breadcastd.service new file mode 100644 index 0000000..c7778b5 --- /dev/null +++ b/contrib/breadcastd.service @@ -0,0 +1,19 @@ +[Unit] +Description=breadcast screen-mirroring daemon +Documentation=https://git.breadway.dev/breadway/breadcast +# Start after the graphical session is ready so WAYLAND_DISPLAY is set +After=graphical-session.target +PartOf=graphical-session.target + +[Service] +Type=simple +ExecStart=%h/.cargo/bin/breadcastd +Restart=on-failure +RestartSec=2 + +# Forward stdout/stderr to the journal so `journalctl --user -u breadcastd` works +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=graphical-session.target diff --git a/contrib/hyprland.conf b/contrib/hyprland.conf new file mode 100644 index 0000000..ba463b6 --- /dev/null +++ b/contrib/hyprland.conf @@ -0,0 +1,18 @@ +# breadcast — add these to your hyprland.conf +# +# This is classic hyprlang (.conf) syntax, for stock Hyprland installs. On +# BOS specifically, hyprland.conf is deprecated in favor of a Lua config +# (hyprland.lua) driven by JSON files (binds.json, etc.) — see +# contrib/binds.json for the equivalent BOS-native keybind snippet instead +# of this file. There is currently no BOS-native equivalent for the +# layerrule lines below (no layer-rule JSON schema exists yet); on BOS the +# popup will work without the frosted-glass blur until that's added. +# +# Blur: blurs what's behind the transparent window, giving the frosted-glass look. +# ignorezero: skips blurring fully-transparent pixels (outside the panel) for +# a cleaner result. +layerrule = blur, breadcast +layerrule = ignorezero, breadcast + +# Keybind: Super+C opens the device picker popup. +bind = $mainMod, C, exec, breadcast diff --git a/vendor/rust_cast-0.21.0/.gitignore b/vendor/rust_cast-0.21.0/.gitignore new file mode 100644 index 0000000..7ed8900 --- /dev/null +++ b/vendor/rust_cast-0.21.0/.gitignore @@ -0,0 +1,8 @@ +.vscode +.idea +**/*.rs.bk +target +out +*.iml +node_modules +Cargo.lock diff --git a/vendor/rust_cast-0.21.0/.husky/commit-msg b/vendor/rust_cast-0.21.0/.husky/commit-msg new file mode 100755 index 0000000..0398b7a --- /dev/null +++ b/vendor/rust_cast-0.21.0/.husky/commit-msg @@ -0,0 +1 @@ +npx --no -- commitlint --edit ${1} diff --git a/vendor/rust_cast-0.21.0/.husky/pre-push b/vendor/rust_cast-0.21.0/.husky/pre-push new file mode 100755 index 0000000..b398f1e --- /dev/null +++ b/vendor/rust_cast-0.21.0/.husky/pre-push @@ -0,0 +1,30 @@ +#!/bin/sh + +set -eu + +if ! cargo +nightly fmt --all -- --check +then + echo "There are some code style issues." + echo "Run `cargo fmt` first." + exit 1 +fi + +if ! cargo clippy --all-targets -- -D warnings +then + echo "There are some clippy issues." + exit 1 +fi + +if ! cargo test +then + echo "There are some test issues." + exit 1 +fi + +if ! cargo test --features thread_safe +then + echo "There are some test issues (with `thread_safe` feature)." + exit 1 +fi + +exit 0 diff --git a/vendor/rust_cast-0.21.0/Cargo.toml b/vendor/rust_cast-0.21.0/Cargo.toml new file mode 100644 index 0000000..95b1d0d --- /dev/null +++ b/vendor/rust_cast-0.21.0/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "rust_cast" +description = "Library that allows you to communicate with Google Cast enabled devices (e.g. Chromecast)." +documentation = "https://docs.rs/crate/rust_cast/" +homepage = "https://github.com/azasypkin/rust-cast" +repository = "https://github.com/azasypkin/rust-cast" +readme = "README.md" +license = "MIT" +keywords = ["cast", "chromecast", "google"] +version = "0.21.0" +authors = ["Aleh Zasypkin "] +categories = ["api-bindings", "hardware-support", "multimedia"] +edition = "2024" +exclude = [ + ".github/*", + "examples/*", + "protobuf/*", +] + +[dependencies] +byteorder = "1.5" +log = "0.4" +protobuf = "=3.7.2" +rustls = "0.23" +rustls-native-certs = "0.8" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +thiserror = "2" + +[dev-dependencies] +ansi_term = "0.12" +docopt = "1" +env_logger = "0.11" +mdns-sd = "0.17" + +[build-dependencies] +protobuf-codegen = "=3.7.2" + +[features] +thread_safe = [] +cast = [] diff --git a/vendor/rust_cast-0.21.0/LICENSE b/vendor/rust_cast-0.21.0/LICENSE new file mode 100644 index 0000000..3ba08dc --- /dev/null +++ b/vendor/rust_cast-0.21.0/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Aleh Zasypkin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/vendor/rust_cast-0.21.0/PATCHES.md b/vendor/rust_cast-0.21.0/PATCHES.md new file mode 100644 index 0000000..50f4b8f --- /dev/null +++ b/vendor/rust_cast-0.21.0/PATCHES.md @@ -0,0 +1,37 @@ +# Vendoring notes + +This is [rust_cast 0.21.0](https://github.com/azasypkin/rust-cast) (MIT +licensed), vendored via `[patch.crates-io]` in the workspace `Cargo.toml` +purely to add one method upstream doesn't have: a generic point-to-point +send on an arbitrary CASTV2 namespace. + +## Why + +breadcast's Cast Streaming integration (`breadcast-caststream-sys`, wrapping +a vendored `chromium/openscreen`) needs to exchange OFFER/ANSWER JSON with a +launched receiver app on the `urn:x-cast:com.google.cast.webrtc` namespace -- +a point-to-point conversation targeting that app's `transport_id`, the same +target `ConnectionChannel`/`MediaChannel` already use. None of rust_cast's +built-in channels expose that: `ReceiverChannel::broadcast_message()` is the +closest, but it hardcodes destination `"*"`, which is a different +conversation than a namespace-specific exchange with one particular app. + +Receiving such messages needs no patch -- `CastDevice::receive()` already +returns them as `ChannelMessage::Raw(CastMessage)` whenever no built-in +channel claims the namespace. + +## The patch + +`src/lib.rs`: added `CastDevice::send_message(&self, namespace, +destination, message)`, built the same way `ReceiverChannel::broadcast_message()` +is internally, but with a caller-supplied `destination` instead of a +hardcoded `"*"`. See the doc comment on that method for the exact rationale +(marked "LOCAL PATCH (breadcast, not upstream)"). + +## Rolling the pin + +To move to a newer rust_cast release: copy the new version from +`~/.cargo/registry/src/*/rust_cast-/`, re-apply the same method +addition (small enough to redo by hand), swap the version in this +directory's own `Cargo.toml`, and update the `path` if the directory name +changes. diff --git a/vendor/rust_cast-0.21.0/README.md b/vendor/rust_cast-0.21.0/README.md new file mode 100644 index 0000000..c5fc50c --- /dev/null +++ b/vendor/rust_cast-0.21.0/README.md @@ -0,0 +1,102 @@ +[![Docs](https://docs.rs/rust_cast/badge.svg)](https://docs.rs/crate/rust_cast/) +![Build Status](https://github.com/azasypkin/rust-cast/actions/workflows/ci.yml/badge.svg) + +# Usage +* [Documentation](https://docs.rs/crate/rust_cast/) +* Try out [Rust Caster](./examples/rust_caster.rs) example to see this crate in action! + +# Build + +Proto files are taken from [Chromium Open Screen GitHub mirror](https://chromium.googlesource.com/openscreen/+/37a17677e5ded963fc41a3d8dee7a59484e5ec13/cast/common/channel/proto). + +By default `cargo build` won't try to generate Rust code from the files located at `protobuf/*`, if you want to do that +use `GENERATE_PROTO` environment variable during build and make sure you have `protoc` binary in `$PATH`: + +```bash +$ GENERATE_PROTO=true cargo build +``` + +# Run example + +## Generic features + +First, you need to figure out the address of the device to connect to. For example, you can use `avahi` with the following command: +```bash +$ avahi-browse -a --resolve +``` + +```bash +// Get some info about the Google Cast enabled device (e.g. Chromecast). +$ cargo run --example rust_caster -- -a 192.168.0.100 -i + +Number of apps run: 1 +App#0: Default Media Receiver (CC1AD845) +Volume level: 1 +Muted: false + +// Run specific app on the Chromecast. +$ cargo run --example rust_caster -- -a 192.168.0.100 -r youtube + +// Stop specific active app. +$ cargo run --example rust_caster -- -a 192.168.0.100 -s youtube + +// Stop currently active app. +$ cargo run --example rust_caster -- -a 192.168.0.100 --stop-current + +The following app has been stopped: Default Media Receiver (CC1AD845) +``` + +## Media features +```bash +// Stream a video. +$ cargo run --example rust_caster -- -a 192.168.0.100 -m http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 + +// Stream a video of specific format with buffering. +$ cargo run --example rust_caster -- -a 192.168.0.100 -m http://xxx.webm --media-type video/webm --media-stream-type buffered + +// Stream video from YouTube (doesn't work with the latest YouTube app, fix is welcome). +$ cargo run --example rust_caster -- -a 192.168.0.100 -m 7LcUOEP7Brc --media-app youtube + +// Display an image. +$ cargo run --example rust_caster -- -a 192.168.0.100 -m https://azasypkin.github.io/style-my-image/images/mozilla.jpg + +// Change volume level. +$ cargo run --example rust_caster -- -a 192.168.0.100 --media-volume 0.5 + +// Mute/unmute media. +$ cargo run --example rust_caster -- -a 192.168.0.100 --media-mute [--media-unmute] + +// Pause media. +$ cargo run --example rust_caster -- -a 192.168.0.100 --media-app youtube --media-pause + +// Resume/play media. +$ cargo run --example rust_caster -- -a 192.168.0.100 --media-app youtube --media-play + +// Seek media. +$ cargo run --example rust_caster -- -a 192.168.0.100 --media-app youtube --media-seek 100 +``` + +For all possible values of `--media-type` see [Supported Media for Google Cast](https://developers.google.com/cast/docs/media). + +# DNS TXT Record description + +* `md` - Model Name (e.g. "Chromecast"); +* `id` - UUID without hyphens of the particular device (e.g. xx12x3x456xx789xx01xx234x56789x0); +* `fn` - Friendly Name of the device (e.g. "Living Room"); +* `rs` - Unknown (recent share???) (e.g. "Youtube TV"); +* `bs` - Uknonwn (e.g. "XX1XXX2X3456"); +* `st` - Unknown (e.g. "1"); +* `ca` - Unknown (e.g. "1234"); +* `ic` - Icon path (e.g. "/setup/icon.png"); +* `ve` - Version (e.g. "04"). + +# Model names + +* `Chromecast` - Regular chromecast, supports video/audio; +* `Chromecast Audio` - Chromecast Audio device, supports only audio. + +# Useful links and sources of inspiration + +* [DIAL Protocol](http://www.dial-multiscreen.org/); +* [An implementation of the Chromecast CASTV2 protocol in JS](https://github.com/thibauts/node-castv2); +* [Chromecast - steps closer to a python native api](http://www.clift.org/fred/chromecast-steps-closer-to-a-python-native-api.html); diff --git a/vendor/rust_cast-0.21.0/build.rs b/vendor/rust_cast-0.21.0/build.rs new file mode 100644 index 0000000..eff94c8 --- /dev/null +++ b/vendor/rust_cast-0.21.0/build.rs @@ -0,0 +1,22 @@ +use protobuf_codegen::{Codegen, Customize}; +use std::env; + +fn main() { + let generate_proto = env::var("GENERATE_PROTO").unwrap_or_else(|_| "false".to_string()); + if generate_proto == "true" { + Codegen::new() + .out_dir("src/cast") + .inputs([ + "protobuf/authority_keys.proto", + "protobuf/cast_channel.proto", + ]) + .includes(["protobuf"]) + .customize(Customize::default().gen_mod_rs(false)) + .run() + .expect("protoc"); + } + + println!("rerun-if-env-changed=GENERATE_PROTO"); + println!("rerun-if-changed=protobuf/authority_keys.proto"); + println!("rerun-if-changed=protobuf/cast_channel.proto"); +} diff --git a/vendor/rust_cast-0.21.0/rustfmt.toml b/vendor/rust_cast-0.21.0/rustfmt.toml new file mode 100644 index 0000000..5293560 --- /dev/null +++ b/vendor/rust_cast-0.21.0/rustfmt.toml @@ -0,0 +1,2 @@ +unstable_features = true +imports_granularity = "Crate" diff --git a/vendor/rust_cast-0.21.0/src/cast/authority_keys.rs b/vendor/rust_cast-0.21.0/src/cast/authority_keys.rs new file mode 100644 index 0000000..639da16 --- /dev/null +++ b/vendor/rust_cast-0.21.0/src/cast/authority_keys.rs @@ -0,0 +1,306 @@ +// This file is generated by rust-protobuf 3.7.2. Do not edit +// .proto file is parsed by protoc 33.2 +// @generated + +// https://github.com/rust-lang/rust-clippy/issues/702 +#![allow(unknown_lints)] +#![allow(clippy::all)] + +#![allow(unused_attributes)] +#![cfg_attr(rustfmt, rustfmt::skip)] + +#![allow(dead_code)] +#![allow(missing_docs)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(trivial_casts)] +#![allow(unused_results)] +#![allow(unused_mut)] + +//! Generated file from `authority_keys.proto` +// Generated for lite runtime + +/// Generated files are compatible only with the same version +/// of protobuf runtime. +const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_3_7_2; + +// @@protoc_insertion_point(message:openscreen.cast.proto.AuthorityKeys) +#[derive(PartialEq,Clone,Default,Debug)] +pub struct AuthorityKeys { + // message fields + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthorityKeys.keys) + pub keys: ::std::vec::Vec, + // special fields + // @@protoc_insertion_point(special_field:openscreen.cast.proto.AuthorityKeys.special_fields) + pub special_fields: ::protobuf::SpecialFields, +} + +impl<'a> ::std::default::Default for &'a AuthorityKeys { + fn default() -> &'a AuthorityKeys { + ::default_instance() + } +} + +impl AuthorityKeys { + pub fn new() -> AuthorityKeys { + ::std::default::Default::default() + } +} + +impl ::protobuf::Message for AuthorityKeys { + const NAME: &'static str = "AuthorityKeys"; + + fn is_initialized(&self) -> bool { + for v in &self.keys { + if !v.is_initialized() { + return false; + } + }; + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { + while let Some(tag) = is.read_raw_tag_or_eof()? { + match tag { + 10 => { + self.keys.push(is.read_message()?); + }, + tag => { + ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u64 { + let mut my_size = 0; + for value in &self.keys { + let len = value.compute_size(); + my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; + }; + my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); + self.special_fields.cached_size().set(my_size as u32); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { + for v in &self.keys { + ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; + }; + os.write_unknown_fields(self.special_fields.unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn special_fields(&self) -> &::protobuf::SpecialFields { + &self.special_fields + } + + fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { + &mut self.special_fields + } + + fn new() -> AuthorityKeys { + AuthorityKeys::new() + } + + fn clear(&mut self) { + self.keys.clear(); + self.special_fields.clear(); + } + + fn default_instance() -> &'static AuthorityKeys { + static instance: AuthorityKeys = AuthorityKeys { + keys: ::std::vec::Vec::new(), + special_fields: ::protobuf::SpecialFields::new(), + }; + &instance + } +} + +/// Nested message and enums of message `AuthorityKeys` +pub mod authority_keys { + // @@protoc_insertion_point(message:openscreen.cast.proto.AuthorityKeys.Key) + #[derive(PartialEq,Clone,Default,Debug)] + pub struct Key { + // message fields + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthorityKeys.Key.fingerprint) + pub fingerprint: ::std::option::Option<::std::vec::Vec>, + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthorityKeys.Key.public_key) + pub public_key: ::std::option::Option<::std::vec::Vec>, + // special fields + // @@protoc_insertion_point(special_field:openscreen.cast.proto.AuthorityKeys.Key.special_fields) + pub special_fields: ::protobuf::SpecialFields, + } + + impl<'a> ::std::default::Default for &'a Key { + fn default() -> &'a Key { + ::default_instance() + } + } + + impl Key { + pub fn new() -> Key { + ::std::default::Default::default() + } + + // required bytes fingerprint = 1; + + pub fn fingerprint(&self) -> &[u8] { + match self.fingerprint.as_ref() { + Some(v) => v, + None => &[], + } + } + + pub fn clear_fingerprint(&mut self) { + self.fingerprint = ::std::option::Option::None; + } + + pub fn has_fingerprint(&self) -> bool { + self.fingerprint.is_some() + } + + // Param is passed by value, moved + pub fn set_fingerprint(&mut self, v: ::std::vec::Vec) { + self.fingerprint = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_fingerprint(&mut self) -> &mut ::std::vec::Vec { + if self.fingerprint.is_none() { + self.fingerprint = ::std::option::Option::Some(::std::vec::Vec::new()); + } + self.fingerprint.as_mut().unwrap() + } + + // Take field + pub fn take_fingerprint(&mut self) -> ::std::vec::Vec { + self.fingerprint.take().unwrap_or_else(|| ::std::vec::Vec::new()) + } + + // required bytes public_key = 2; + + pub fn public_key(&self) -> &[u8] { + match self.public_key.as_ref() { + Some(v) => v, + None => &[], + } + } + + pub fn clear_public_key(&mut self) { + self.public_key = ::std::option::Option::None; + } + + pub fn has_public_key(&self) -> bool { + self.public_key.is_some() + } + + // Param is passed by value, moved + pub fn set_public_key(&mut self, v: ::std::vec::Vec) { + self.public_key = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_public_key(&mut self) -> &mut ::std::vec::Vec { + if self.public_key.is_none() { + self.public_key = ::std::option::Option::Some(::std::vec::Vec::new()); + } + self.public_key.as_mut().unwrap() + } + + // Take field + pub fn take_public_key(&mut self) -> ::std::vec::Vec { + self.public_key.take().unwrap_or_else(|| ::std::vec::Vec::new()) + } + } + + impl ::protobuf::Message for Key { + const NAME: &'static str = "Key"; + + fn is_initialized(&self) -> bool { + if self.fingerprint.is_none() { + return false; + } + if self.public_key.is_none() { + return false; + } + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { + while let Some(tag) = is.read_raw_tag_or_eof()? { + match tag { + 10 => { + self.fingerprint = ::std::option::Option::Some(is.read_bytes()?); + }, + 18 => { + self.public_key = ::std::option::Option::Some(is.read_bytes()?); + }, + tag => { + ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u64 { + let mut my_size = 0; + if let Some(v) = self.fingerprint.as_ref() { + my_size += ::protobuf::rt::bytes_size(1, &v); + } + if let Some(v) = self.public_key.as_ref() { + my_size += ::protobuf::rt::bytes_size(2, &v); + } + my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); + self.special_fields.cached_size().set(my_size as u32); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { + if let Some(v) = self.fingerprint.as_ref() { + os.write_bytes(1, v)?; + } + if let Some(v) = self.public_key.as_ref() { + os.write_bytes(2, v)?; + } + os.write_unknown_fields(self.special_fields.unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn special_fields(&self) -> &::protobuf::SpecialFields { + &self.special_fields + } + + fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { + &mut self.special_fields + } + + fn new() -> Key { + Key::new() + } + + fn clear(&mut self) { + self.fingerprint = ::std::option::Option::None; + self.public_key = ::std::option::Option::None; + self.special_fields.clear(); + } + + fn default_instance() -> &'static Key { + static instance: Key = Key { + fingerprint: ::std::option::Option::None, + public_key: ::std::option::Option::None, + special_fields: ::protobuf::SpecialFields::new(), + }; + &instance + } + } +} diff --git a/vendor/rust_cast-0.21.0/src/cast/cast_channel.rs b/vendor/rust_cast-0.21.0/src/cast/cast_channel.rs new file mode 100644 index 0000000..9cd7c7c --- /dev/null +++ b/vendor/rust_cast-0.21.0/src/cast/cast_channel.rs @@ -0,0 +1,1555 @@ +// This file is generated by rust-protobuf 3.7.2. Do not edit +// .proto file is parsed by protoc 33.2 +// @generated + +// https://github.com/rust-lang/rust-clippy/issues/702 +#![allow(unknown_lints)] +#![allow(clippy::all)] + +#![allow(unused_attributes)] +#![cfg_attr(rustfmt, rustfmt::skip)] + +#![allow(dead_code)] +#![allow(missing_docs)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] +#![allow(trivial_casts)] +#![allow(unused_results)] +#![allow(unused_mut)] + +//! Generated file from `cast_channel.proto` +// Generated for lite runtime + +/// Generated files are compatible only with the same version +/// of protobuf runtime. +const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_3_7_2; + +// @@protoc_insertion_point(message:openscreen.cast.proto.CastMessage) +#[derive(PartialEq,Clone,Default,Debug)] +pub struct CastMessage { + // message fields + // @@protoc_insertion_point(field:openscreen.cast.proto.CastMessage.protocol_version) + pub protocol_version: ::std::option::Option<::protobuf::EnumOrUnknown>, + // @@protoc_insertion_point(field:openscreen.cast.proto.CastMessage.source_id) + pub source_id: ::std::option::Option<::std::string::String>, + // @@protoc_insertion_point(field:openscreen.cast.proto.CastMessage.destination_id) + pub destination_id: ::std::option::Option<::std::string::String>, + // @@protoc_insertion_point(field:openscreen.cast.proto.CastMessage.namespace) + pub namespace: ::std::option::Option<::std::string::String>, + // @@protoc_insertion_point(field:openscreen.cast.proto.CastMessage.payload_type) + pub payload_type: ::std::option::Option<::protobuf::EnumOrUnknown>, + // @@protoc_insertion_point(field:openscreen.cast.proto.CastMessage.payload_utf8) + pub payload_utf8: ::std::option::Option<::std::string::String>, + // @@protoc_insertion_point(field:openscreen.cast.proto.CastMessage.payload_binary) + pub payload_binary: ::std::option::Option<::std::vec::Vec>, + // @@protoc_insertion_point(field:openscreen.cast.proto.CastMessage.continued) + pub continued: ::std::option::Option, + // @@protoc_insertion_point(field:openscreen.cast.proto.CastMessage.remaining_length) + pub remaining_length: ::std::option::Option, + // special fields + // @@protoc_insertion_point(special_field:openscreen.cast.proto.CastMessage.special_fields) + pub special_fields: ::protobuf::SpecialFields, +} + +impl<'a> ::std::default::Default for &'a CastMessage { + fn default() -> &'a CastMessage { + ::default_instance() + } +} + +impl CastMessage { + pub fn new() -> CastMessage { + ::std::default::Default::default() + } + + // required .openscreen.cast.proto.CastMessage.ProtocolVersion protocol_version = 1; + + pub fn protocol_version(&self) -> cast_message::ProtocolVersion { + match self.protocol_version { + Some(e) => e.enum_value_or(cast_message::ProtocolVersion::CASTV2_1_0), + None => cast_message::ProtocolVersion::CASTV2_1_0, + } + } + + pub fn clear_protocol_version(&mut self) { + self.protocol_version = ::std::option::Option::None; + } + + pub fn has_protocol_version(&self) -> bool { + self.protocol_version.is_some() + } + + // Param is passed by value, moved + pub fn set_protocol_version(&mut self, v: cast_message::ProtocolVersion) { + self.protocol_version = ::std::option::Option::Some(::protobuf::EnumOrUnknown::new(v)); + } + + // required string source_id = 2; + + pub fn source_id(&self) -> &str { + match self.source_id.as_ref() { + Some(v) => v, + None => "", + } + } + + pub fn clear_source_id(&mut self) { + self.source_id = ::std::option::Option::None; + } + + pub fn has_source_id(&self) -> bool { + self.source_id.is_some() + } + + // Param is passed by value, moved + pub fn set_source_id(&mut self, v: ::std::string::String) { + self.source_id = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_source_id(&mut self) -> &mut ::std::string::String { + if self.source_id.is_none() { + self.source_id = ::std::option::Option::Some(::std::string::String::new()); + } + self.source_id.as_mut().unwrap() + } + + // Take field + pub fn take_source_id(&mut self) -> ::std::string::String { + self.source_id.take().unwrap_or_else(|| ::std::string::String::new()) + } + + // required string destination_id = 3; + + pub fn destination_id(&self) -> &str { + match self.destination_id.as_ref() { + Some(v) => v, + None => "", + } + } + + pub fn clear_destination_id(&mut self) { + self.destination_id = ::std::option::Option::None; + } + + pub fn has_destination_id(&self) -> bool { + self.destination_id.is_some() + } + + // Param is passed by value, moved + pub fn set_destination_id(&mut self, v: ::std::string::String) { + self.destination_id = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_destination_id(&mut self) -> &mut ::std::string::String { + if self.destination_id.is_none() { + self.destination_id = ::std::option::Option::Some(::std::string::String::new()); + } + self.destination_id.as_mut().unwrap() + } + + // Take field + pub fn take_destination_id(&mut self) -> ::std::string::String { + self.destination_id.take().unwrap_or_else(|| ::std::string::String::new()) + } + + // required string namespace = 4; + + pub fn namespace(&self) -> &str { + match self.namespace.as_ref() { + Some(v) => v, + None => "", + } + } + + pub fn clear_namespace(&mut self) { + self.namespace = ::std::option::Option::None; + } + + pub fn has_namespace(&self) -> bool { + self.namespace.is_some() + } + + // Param is passed by value, moved + pub fn set_namespace(&mut self, v: ::std::string::String) { + self.namespace = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_namespace(&mut self) -> &mut ::std::string::String { + if self.namespace.is_none() { + self.namespace = ::std::option::Option::Some(::std::string::String::new()); + } + self.namespace.as_mut().unwrap() + } + + // Take field + pub fn take_namespace(&mut self) -> ::std::string::String { + self.namespace.take().unwrap_or_else(|| ::std::string::String::new()) + } + + // required .openscreen.cast.proto.CastMessage.PayloadType payload_type = 5; + + pub fn payload_type(&self) -> cast_message::PayloadType { + match self.payload_type { + Some(e) => e.enum_value_or(cast_message::PayloadType::STRING), + None => cast_message::PayloadType::STRING, + } + } + + pub fn clear_payload_type(&mut self) { + self.payload_type = ::std::option::Option::None; + } + + pub fn has_payload_type(&self) -> bool { + self.payload_type.is_some() + } + + // Param is passed by value, moved + pub fn set_payload_type(&mut self, v: cast_message::PayloadType) { + self.payload_type = ::std::option::Option::Some(::protobuf::EnumOrUnknown::new(v)); + } + + // optional string payload_utf8 = 6; + + pub fn payload_utf8(&self) -> &str { + match self.payload_utf8.as_ref() { + Some(v) => v, + None => "", + } + } + + pub fn clear_payload_utf8(&mut self) { + self.payload_utf8 = ::std::option::Option::None; + } + + pub fn has_payload_utf8(&self) -> bool { + self.payload_utf8.is_some() + } + + // Param is passed by value, moved + pub fn set_payload_utf8(&mut self, v: ::std::string::String) { + self.payload_utf8 = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_payload_utf8(&mut self) -> &mut ::std::string::String { + if self.payload_utf8.is_none() { + self.payload_utf8 = ::std::option::Option::Some(::std::string::String::new()); + } + self.payload_utf8.as_mut().unwrap() + } + + // Take field + pub fn take_payload_utf8(&mut self) -> ::std::string::String { + self.payload_utf8.take().unwrap_or_else(|| ::std::string::String::new()) + } + + // optional bytes payload_binary = 7; + + pub fn payload_binary(&self) -> &[u8] { + match self.payload_binary.as_ref() { + Some(v) => v, + None => &[], + } + } + + pub fn clear_payload_binary(&mut self) { + self.payload_binary = ::std::option::Option::None; + } + + pub fn has_payload_binary(&self) -> bool { + self.payload_binary.is_some() + } + + // Param is passed by value, moved + pub fn set_payload_binary(&mut self, v: ::std::vec::Vec) { + self.payload_binary = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_payload_binary(&mut self) -> &mut ::std::vec::Vec { + if self.payload_binary.is_none() { + self.payload_binary = ::std::option::Option::Some(::std::vec::Vec::new()); + } + self.payload_binary.as_mut().unwrap() + } + + // Take field + pub fn take_payload_binary(&mut self) -> ::std::vec::Vec { + self.payload_binary.take().unwrap_or_else(|| ::std::vec::Vec::new()) + } + + // optional bool continued = 8; + + pub fn continued(&self) -> bool { + self.continued.unwrap_or(false) + } + + pub fn clear_continued(&mut self) { + self.continued = ::std::option::Option::None; + } + + pub fn has_continued(&self) -> bool { + self.continued.is_some() + } + + // Param is passed by value, moved + pub fn set_continued(&mut self, v: bool) { + self.continued = ::std::option::Option::Some(v); + } + + // optional uint32 remaining_length = 9; + + pub fn remaining_length(&self) -> u32 { + self.remaining_length.unwrap_or(0) + } + + pub fn clear_remaining_length(&mut self) { + self.remaining_length = ::std::option::Option::None; + } + + pub fn has_remaining_length(&self) -> bool { + self.remaining_length.is_some() + } + + // Param is passed by value, moved + pub fn set_remaining_length(&mut self, v: u32) { + self.remaining_length = ::std::option::Option::Some(v); + } +} + +impl ::protobuf::Message for CastMessage { + const NAME: &'static str = "CastMessage"; + + fn is_initialized(&self) -> bool { + if self.protocol_version.is_none() { + return false; + } + if self.source_id.is_none() { + return false; + } + if self.destination_id.is_none() { + return false; + } + if self.namespace.is_none() { + return false; + } + if self.payload_type.is_none() { + return false; + } + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { + while let Some(tag) = is.read_raw_tag_or_eof()? { + match tag { + 8 => { + self.protocol_version = ::std::option::Option::Some(is.read_enum_or_unknown()?); + }, + 18 => { + self.source_id = ::std::option::Option::Some(is.read_string()?); + }, + 26 => { + self.destination_id = ::std::option::Option::Some(is.read_string()?); + }, + 34 => { + self.namespace = ::std::option::Option::Some(is.read_string()?); + }, + 40 => { + self.payload_type = ::std::option::Option::Some(is.read_enum_or_unknown()?); + }, + 50 => { + self.payload_utf8 = ::std::option::Option::Some(is.read_string()?); + }, + 58 => { + self.payload_binary = ::std::option::Option::Some(is.read_bytes()?); + }, + 64 => { + self.continued = ::std::option::Option::Some(is.read_bool()?); + }, + 72 => { + self.remaining_length = ::std::option::Option::Some(is.read_uint32()?); + }, + tag => { + ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u64 { + let mut my_size = 0; + if let Some(v) = self.protocol_version { + my_size += ::protobuf::rt::int32_size(1, v.value()); + } + if let Some(v) = self.source_id.as_ref() { + my_size += ::protobuf::rt::string_size(2, &v); + } + if let Some(v) = self.destination_id.as_ref() { + my_size += ::protobuf::rt::string_size(3, &v); + } + if let Some(v) = self.namespace.as_ref() { + my_size += ::protobuf::rt::string_size(4, &v); + } + if let Some(v) = self.payload_type { + my_size += ::protobuf::rt::int32_size(5, v.value()); + } + if let Some(v) = self.payload_utf8.as_ref() { + my_size += ::protobuf::rt::string_size(6, &v); + } + if let Some(v) = self.payload_binary.as_ref() { + my_size += ::protobuf::rt::bytes_size(7, &v); + } + if let Some(v) = self.continued { + my_size += 1 + 1; + } + if let Some(v) = self.remaining_length { + my_size += ::protobuf::rt::uint32_size(9, v); + } + my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); + self.special_fields.cached_size().set(my_size as u32); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { + if let Some(v) = self.protocol_version { + os.write_enum(1, ::protobuf::EnumOrUnknown::value(&v))?; + } + if let Some(v) = self.source_id.as_ref() { + os.write_string(2, v)?; + } + if let Some(v) = self.destination_id.as_ref() { + os.write_string(3, v)?; + } + if let Some(v) = self.namespace.as_ref() { + os.write_string(4, v)?; + } + if let Some(v) = self.payload_type { + os.write_enum(5, ::protobuf::EnumOrUnknown::value(&v))?; + } + if let Some(v) = self.payload_utf8.as_ref() { + os.write_string(6, v)?; + } + if let Some(v) = self.payload_binary.as_ref() { + os.write_bytes(7, v)?; + } + if let Some(v) = self.continued { + os.write_bool(8, v)?; + } + if let Some(v) = self.remaining_length { + os.write_uint32(9, v)?; + } + os.write_unknown_fields(self.special_fields.unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn special_fields(&self) -> &::protobuf::SpecialFields { + &self.special_fields + } + + fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { + &mut self.special_fields + } + + fn new() -> CastMessage { + CastMessage::new() + } + + fn clear(&mut self) { + self.protocol_version = ::std::option::Option::None; + self.source_id = ::std::option::Option::None; + self.destination_id = ::std::option::Option::None; + self.namespace = ::std::option::Option::None; + self.payload_type = ::std::option::Option::None; + self.payload_utf8 = ::std::option::Option::None; + self.payload_binary = ::std::option::Option::None; + self.continued = ::std::option::Option::None; + self.remaining_length = ::std::option::Option::None; + self.special_fields.clear(); + } + + fn default_instance() -> &'static CastMessage { + static instance: CastMessage = CastMessage { + protocol_version: ::std::option::Option::None, + source_id: ::std::option::Option::None, + destination_id: ::std::option::Option::None, + namespace: ::std::option::Option::None, + payload_type: ::std::option::Option::None, + payload_utf8: ::std::option::Option::None, + payload_binary: ::std::option::Option::None, + continued: ::std::option::Option::None, + remaining_length: ::std::option::Option::None, + special_fields: ::protobuf::SpecialFields::new(), + }; + &instance + } +} + +/// Nested message and enums of message `CastMessage` +pub mod cast_message { + #[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] + // @@protoc_insertion_point(enum:openscreen.cast.proto.CastMessage.ProtocolVersion) + pub enum ProtocolVersion { + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.CastMessage.ProtocolVersion.CASTV2_1_0) + CASTV2_1_0 = 0, + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.CastMessage.ProtocolVersion.CASTV2_1_1) + CASTV2_1_1 = 1, + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.CastMessage.ProtocolVersion.CASTV2_1_2) + CASTV2_1_2 = 2, + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.CastMessage.ProtocolVersion.CASTV2_1_3) + CASTV2_1_3 = 3, + } + + impl ::protobuf::Enum for ProtocolVersion { + const NAME: &'static str = "ProtocolVersion"; + + fn value(&self) -> i32 { + *self as i32 + } + + fn from_i32(value: i32) -> ::std::option::Option { + match value { + 0 => ::std::option::Option::Some(ProtocolVersion::CASTV2_1_0), + 1 => ::std::option::Option::Some(ProtocolVersion::CASTV2_1_1), + 2 => ::std::option::Option::Some(ProtocolVersion::CASTV2_1_2), + 3 => ::std::option::Option::Some(ProtocolVersion::CASTV2_1_3), + _ => ::std::option::Option::None + } + } + + fn from_str(str: &str) -> ::std::option::Option { + match str { + "CASTV2_1_0" => ::std::option::Option::Some(ProtocolVersion::CASTV2_1_0), + "CASTV2_1_1" => ::std::option::Option::Some(ProtocolVersion::CASTV2_1_1), + "CASTV2_1_2" => ::std::option::Option::Some(ProtocolVersion::CASTV2_1_2), + "CASTV2_1_3" => ::std::option::Option::Some(ProtocolVersion::CASTV2_1_3), + _ => ::std::option::Option::None + } + } + + const VALUES: &'static [ProtocolVersion] = &[ + ProtocolVersion::CASTV2_1_0, + ProtocolVersion::CASTV2_1_1, + ProtocolVersion::CASTV2_1_2, + ProtocolVersion::CASTV2_1_3, + ]; + } + + impl ::std::default::Default for ProtocolVersion { + fn default() -> Self { + ProtocolVersion::CASTV2_1_0 + } + } + + + #[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] + // @@protoc_insertion_point(enum:openscreen.cast.proto.CastMessage.PayloadType) + pub enum PayloadType { + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.CastMessage.PayloadType.STRING) + STRING = 0, + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.CastMessage.PayloadType.BINARY) + BINARY = 1, + } + + impl ::protobuf::Enum for PayloadType { + const NAME: &'static str = "PayloadType"; + + fn value(&self) -> i32 { + *self as i32 + } + + fn from_i32(value: i32) -> ::std::option::Option { + match value { + 0 => ::std::option::Option::Some(PayloadType::STRING), + 1 => ::std::option::Option::Some(PayloadType::BINARY), + _ => ::std::option::Option::None + } + } + + fn from_str(str: &str) -> ::std::option::Option { + match str { + "STRING" => ::std::option::Option::Some(PayloadType::STRING), + "BINARY" => ::std::option::Option::Some(PayloadType::BINARY), + _ => ::std::option::Option::None + } + } + + const VALUES: &'static [PayloadType] = &[ + PayloadType::STRING, + PayloadType::BINARY, + ]; + } + + impl ::std::default::Default for PayloadType { + fn default() -> Self { + PayloadType::STRING + } + } + +} + +// @@protoc_insertion_point(message:openscreen.cast.proto.AuthChallenge) +#[derive(PartialEq,Clone,Default,Debug)] +pub struct AuthChallenge { + // message fields + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthChallenge.signature_algorithm) + pub signature_algorithm: ::std::option::Option<::protobuf::EnumOrUnknown>, + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthChallenge.sender_nonce) + pub sender_nonce: ::std::option::Option<::std::vec::Vec>, + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthChallenge.hash_algorithm) + pub hash_algorithm: ::std::option::Option<::protobuf::EnumOrUnknown>, + // special fields + // @@protoc_insertion_point(special_field:openscreen.cast.proto.AuthChallenge.special_fields) + pub special_fields: ::protobuf::SpecialFields, +} + +impl<'a> ::std::default::Default for &'a AuthChallenge { + fn default() -> &'a AuthChallenge { + ::default_instance() + } +} + +impl AuthChallenge { + pub fn new() -> AuthChallenge { + ::std::default::Default::default() + } + + // optional .openscreen.cast.proto.SignatureAlgorithm signature_algorithm = 1; + + pub fn signature_algorithm(&self) -> SignatureAlgorithm { + match self.signature_algorithm { + Some(e) => e.enum_value_or(SignatureAlgorithm::RSASSA_PKCS1v15), + None => SignatureAlgorithm::RSASSA_PKCS1v15, + } + } + + pub fn clear_signature_algorithm(&mut self) { + self.signature_algorithm = ::std::option::Option::None; + } + + pub fn has_signature_algorithm(&self) -> bool { + self.signature_algorithm.is_some() + } + + // Param is passed by value, moved + pub fn set_signature_algorithm(&mut self, v: SignatureAlgorithm) { + self.signature_algorithm = ::std::option::Option::Some(::protobuf::EnumOrUnknown::new(v)); + } + + // optional bytes sender_nonce = 2; + + pub fn sender_nonce(&self) -> &[u8] { + match self.sender_nonce.as_ref() { + Some(v) => v, + None => &[], + } + } + + pub fn clear_sender_nonce(&mut self) { + self.sender_nonce = ::std::option::Option::None; + } + + pub fn has_sender_nonce(&self) -> bool { + self.sender_nonce.is_some() + } + + // Param is passed by value, moved + pub fn set_sender_nonce(&mut self, v: ::std::vec::Vec) { + self.sender_nonce = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_sender_nonce(&mut self) -> &mut ::std::vec::Vec { + if self.sender_nonce.is_none() { + self.sender_nonce = ::std::option::Option::Some(::std::vec::Vec::new()); + } + self.sender_nonce.as_mut().unwrap() + } + + // Take field + pub fn take_sender_nonce(&mut self) -> ::std::vec::Vec { + self.sender_nonce.take().unwrap_or_else(|| ::std::vec::Vec::new()) + } + + // optional .openscreen.cast.proto.HashAlgorithm hash_algorithm = 3; + + pub fn hash_algorithm(&self) -> HashAlgorithm { + match self.hash_algorithm { + Some(e) => e.enum_value_or(HashAlgorithm::SHA1), + None => HashAlgorithm::SHA1, + } + } + + pub fn clear_hash_algorithm(&mut self) { + self.hash_algorithm = ::std::option::Option::None; + } + + pub fn has_hash_algorithm(&self) -> bool { + self.hash_algorithm.is_some() + } + + // Param is passed by value, moved + pub fn set_hash_algorithm(&mut self, v: HashAlgorithm) { + self.hash_algorithm = ::std::option::Option::Some(::protobuf::EnumOrUnknown::new(v)); + } +} + +impl ::protobuf::Message for AuthChallenge { + const NAME: &'static str = "AuthChallenge"; + + fn is_initialized(&self) -> bool { + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { + while let Some(tag) = is.read_raw_tag_or_eof()? { + match tag { + 8 => { + self.signature_algorithm = ::std::option::Option::Some(is.read_enum_or_unknown()?); + }, + 18 => { + self.sender_nonce = ::std::option::Option::Some(is.read_bytes()?); + }, + 24 => { + self.hash_algorithm = ::std::option::Option::Some(is.read_enum_or_unknown()?); + }, + tag => { + ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u64 { + let mut my_size = 0; + if let Some(v) = self.signature_algorithm { + my_size += ::protobuf::rt::int32_size(1, v.value()); + } + if let Some(v) = self.sender_nonce.as_ref() { + my_size += ::protobuf::rt::bytes_size(2, &v); + } + if let Some(v) = self.hash_algorithm { + my_size += ::protobuf::rt::int32_size(3, v.value()); + } + my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); + self.special_fields.cached_size().set(my_size as u32); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { + if let Some(v) = self.signature_algorithm { + os.write_enum(1, ::protobuf::EnumOrUnknown::value(&v))?; + } + if let Some(v) = self.sender_nonce.as_ref() { + os.write_bytes(2, v)?; + } + if let Some(v) = self.hash_algorithm { + os.write_enum(3, ::protobuf::EnumOrUnknown::value(&v))?; + } + os.write_unknown_fields(self.special_fields.unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn special_fields(&self) -> &::protobuf::SpecialFields { + &self.special_fields + } + + fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { + &mut self.special_fields + } + + fn new() -> AuthChallenge { + AuthChallenge::new() + } + + fn clear(&mut self) { + self.signature_algorithm = ::std::option::Option::None; + self.sender_nonce = ::std::option::Option::None; + self.hash_algorithm = ::std::option::Option::None; + self.special_fields.clear(); + } + + fn default_instance() -> &'static AuthChallenge { + static instance: AuthChallenge = AuthChallenge { + signature_algorithm: ::std::option::Option::None, + sender_nonce: ::std::option::Option::None, + hash_algorithm: ::std::option::Option::None, + special_fields: ::protobuf::SpecialFields::new(), + }; + &instance + } +} + +// @@protoc_insertion_point(message:openscreen.cast.proto.AuthResponse) +#[derive(PartialEq,Clone,Default,Debug)] +pub struct AuthResponse { + // message fields + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthResponse.signature) + pub signature: ::std::option::Option<::std::vec::Vec>, + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthResponse.client_auth_certificate) + pub client_auth_certificate: ::std::option::Option<::std::vec::Vec>, + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthResponse.intermediate_certificate) + pub intermediate_certificate: ::std::vec::Vec<::std::vec::Vec>, + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthResponse.signature_algorithm) + pub signature_algorithm: ::std::option::Option<::protobuf::EnumOrUnknown>, + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthResponse.sender_nonce) + pub sender_nonce: ::std::option::Option<::std::vec::Vec>, + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthResponse.hash_algorithm) + pub hash_algorithm: ::std::option::Option<::protobuf::EnumOrUnknown>, + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthResponse.crl) + pub crl: ::std::option::Option<::std::vec::Vec>, + // special fields + // @@protoc_insertion_point(special_field:openscreen.cast.proto.AuthResponse.special_fields) + pub special_fields: ::protobuf::SpecialFields, +} + +impl<'a> ::std::default::Default for &'a AuthResponse { + fn default() -> &'a AuthResponse { + ::default_instance() + } +} + +impl AuthResponse { + pub fn new() -> AuthResponse { + ::std::default::Default::default() + } + + // required bytes signature = 1; + + pub fn signature(&self) -> &[u8] { + match self.signature.as_ref() { + Some(v) => v, + None => &[], + } + } + + pub fn clear_signature(&mut self) { + self.signature = ::std::option::Option::None; + } + + pub fn has_signature(&self) -> bool { + self.signature.is_some() + } + + // Param is passed by value, moved + pub fn set_signature(&mut self, v: ::std::vec::Vec) { + self.signature = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_signature(&mut self) -> &mut ::std::vec::Vec { + if self.signature.is_none() { + self.signature = ::std::option::Option::Some(::std::vec::Vec::new()); + } + self.signature.as_mut().unwrap() + } + + // Take field + pub fn take_signature(&mut self) -> ::std::vec::Vec { + self.signature.take().unwrap_or_else(|| ::std::vec::Vec::new()) + } + + // required bytes client_auth_certificate = 2; + + pub fn client_auth_certificate(&self) -> &[u8] { + match self.client_auth_certificate.as_ref() { + Some(v) => v, + None => &[], + } + } + + pub fn clear_client_auth_certificate(&mut self) { + self.client_auth_certificate = ::std::option::Option::None; + } + + pub fn has_client_auth_certificate(&self) -> bool { + self.client_auth_certificate.is_some() + } + + // Param is passed by value, moved + pub fn set_client_auth_certificate(&mut self, v: ::std::vec::Vec) { + self.client_auth_certificate = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_client_auth_certificate(&mut self) -> &mut ::std::vec::Vec { + if self.client_auth_certificate.is_none() { + self.client_auth_certificate = ::std::option::Option::Some(::std::vec::Vec::new()); + } + self.client_auth_certificate.as_mut().unwrap() + } + + // Take field + pub fn take_client_auth_certificate(&mut self) -> ::std::vec::Vec { + self.client_auth_certificate.take().unwrap_or_else(|| ::std::vec::Vec::new()) + } + + // optional .openscreen.cast.proto.SignatureAlgorithm signature_algorithm = 4; + + pub fn signature_algorithm(&self) -> SignatureAlgorithm { + match self.signature_algorithm { + Some(e) => e.enum_value_or(SignatureAlgorithm::RSASSA_PKCS1v15), + None => SignatureAlgorithm::RSASSA_PKCS1v15, + } + } + + pub fn clear_signature_algorithm(&mut self) { + self.signature_algorithm = ::std::option::Option::None; + } + + pub fn has_signature_algorithm(&self) -> bool { + self.signature_algorithm.is_some() + } + + // Param is passed by value, moved + pub fn set_signature_algorithm(&mut self, v: SignatureAlgorithm) { + self.signature_algorithm = ::std::option::Option::Some(::protobuf::EnumOrUnknown::new(v)); + } + + // optional bytes sender_nonce = 5; + + pub fn sender_nonce(&self) -> &[u8] { + match self.sender_nonce.as_ref() { + Some(v) => v, + None => &[], + } + } + + pub fn clear_sender_nonce(&mut self) { + self.sender_nonce = ::std::option::Option::None; + } + + pub fn has_sender_nonce(&self) -> bool { + self.sender_nonce.is_some() + } + + // Param is passed by value, moved + pub fn set_sender_nonce(&mut self, v: ::std::vec::Vec) { + self.sender_nonce = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_sender_nonce(&mut self) -> &mut ::std::vec::Vec { + if self.sender_nonce.is_none() { + self.sender_nonce = ::std::option::Option::Some(::std::vec::Vec::new()); + } + self.sender_nonce.as_mut().unwrap() + } + + // Take field + pub fn take_sender_nonce(&mut self) -> ::std::vec::Vec { + self.sender_nonce.take().unwrap_or_else(|| ::std::vec::Vec::new()) + } + + // optional .openscreen.cast.proto.HashAlgorithm hash_algorithm = 6; + + pub fn hash_algorithm(&self) -> HashAlgorithm { + match self.hash_algorithm { + Some(e) => e.enum_value_or(HashAlgorithm::SHA1), + None => HashAlgorithm::SHA1, + } + } + + pub fn clear_hash_algorithm(&mut self) { + self.hash_algorithm = ::std::option::Option::None; + } + + pub fn has_hash_algorithm(&self) -> bool { + self.hash_algorithm.is_some() + } + + // Param is passed by value, moved + pub fn set_hash_algorithm(&mut self, v: HashAlgorithm) { + self.hash_algorithm = ::std::option::Option::Some(::protobuf::EnumOrUnknown::new(v)); + } + + // optional bytes crl = 7; + + pub fn crl(&self) -> &[u8] { + match self.crl.as_ref() { + Some(v) => v, + None => &[], + } + } + + pub fn clear_crl(&mut self) { + self.crl = ::std::option::Option::None; + } + + pub fn has_crl(&self) -> bool { + self.crl.is_some() + } + + // Param is passed by value, moved + pub fn set_crl(&mut self, v: ::std::vec::Vec) { + self.crl = ::std::option::Option::Some(v); + } + + // Mutable pointer to the field. + // If field is not initialized, it is initialized with default value first. + pub fn mut_crl(&mut self) -> &mut ::std::vec::Vec { + if self.crl.is_none() { + self.crl = ::std::option::Option::Some(::std::vec::Vec::new()); + } + self.crl.as_mut().unwrap() + } + + // Take field + pub fn take_crl(&mut self) -> ::std::vec::Vec { + self.crl.take().unwrap_or_else(|| ::std::vec::Vec::new()) + } +} + +impl ::protobuf::Message for AuthResponse { + const NAME: &'static str = "AuthResponse"; + + fn is_initialized(&self) -> bool { + if self.signature.is_none() { + return false; + } + if self.client_auth_certificate.is_none() { + return false; + } + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { + while let Some(tag) = is.read_raw_tag_or_eof()? { + match tag { + 10 => { + self.signature = ::std::option::Option::Some(is.read_bytes()?); + }, + 18 => { + self.client_auth_certificate = ::std::option::Option::Some(is.read_bytes()?); + }, + 26 => { + self.intermediate_certificate.push(is.read_bytes()?); + }, + 32 => { + self.signature_algorithm = ::std::option::Option::Some(is.read_enum_or_unknown()?); + }, + 42 => { + self.sender_nonce = ::std::option::Option::Some(is.read_bytes()?); + }, + 48 => { + self.hash_algorithm = ::std::option::Option::Some(is.read_enum_or_unknown()?); + }, + 58 => { + self.crl = ::std::option::Option::Some(is.read_bytes()?); + }, + tag => { + ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u64 { + let mut my_size = 0; + if let Some(v) = self.signature.as_ref() { + my_size += ::protobuf::rt::bytes_size(1, &v); + } + if let Some(v) = self.client_auth_certificate.as_ref() { + my_size += ::protobuf::rt::bytes_size(2, &v); + } + for value in &self.intermediate_certificate { + my_size += ::protobuf::rt::bytes_size(3, &value); + }; + if let Some(v) = self.signature_algorithm { + my_size += ::protobuf::rt::int32_size(4, v.value()); + } + if let Some(v) = self.sender_nonce.as_ref() { + my_size += ::protobuf::rt::bytes_size(5, &v); + } + if let Some(v) = self.hash_algorithm { + my_size += ::protobuf::rt::int32_size(6, v.value()); + } + if let Some(v) = self.crl.as_ref() { + my_size += ::protobuf::rt::bytes_size(7, &v); + } + my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); + self.special_fields.cached_size().set(my_size as u32); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { + if let Some(v) = self.signature.as_ref() { + os.write_bytes(1, v)?; + } + if let Some(v) = self.client_auth_certificate.as_ref() { + os.write_bytes(2, v)?; + } + for v in &self.intermediate_certificate { + os.write_bytes(3, &v)?; + }; + if let Some(v) = self.signature_algorithm { + os.write_enum(4, ::protobuf::EnumOrUnknown::value(&v))?; + } + if let Some(v) = self.sender_nonce.as_ref() { + os.write_bytes(5, v)?; + } + if let Some(v) = self.hash_algorithm { + os.write_enum(6, ::protobuf::EnumOrUnknown::value(&v))?; + } + if let Some(v) = self.crl.as_ref() { + os.write_bytes(7, v)?; + } + os.write_unknown_fields(self.special_fields.unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn special_fields(&self) -> &::protobuf::SpecialFields { + &self.special_fields + } + + fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { + &mut self.special_fields + } + + fn new() -> AuthResponse { + AuthResponse::new() + } + + fn clear(&mut self) { + self.signature = ::std::option::Option::None; + self.client_auth_certificate = ::std::option::Option::None; + self.intermediate_certificate.clear(); + self.signature_algorithm = ::std::option::Option::None; + self.sender_nonce = ::std::option::Option::None; + self.hash_algorithm = ::std::option::Option::None; + self.crl = ::std::option::Option::None; + self.special_fields.clear(); + } + + fn default_instance() -> &'static AuthResponse { + static instance: AuthResponse = AuthResponse { + signature: ::std::option::Option::None, + client_auth_certificate: ::std::option::Option::None, + intermediate_certificate: ::std::vec::Vec::new(), + signature_algorithm: ::std::option::Option::None, + sender_nonce: ::std::option::Option::None, + hash_algorithm: ::std::option::Option::None, + crl: ::std::option::Option::None, + special_fields: ::protobuf::SpecialFields::new(), + }; + &instance + } +} + +// @@protoc_insertion_point(message:openscreen.cast.proto.AuthError) +#[derive(PartialEq,Clone,Default,Debug)] +pub struct AuthError { + // message fields + // @@protoc_insertion_point(field:openscreen.cast.proto.AuthError.error_type) + pub error_type: ::std::option::Option<::protobuf::EnumOrUnknown>, + // special fields + // @@protoc_insertion_point(special_field:openscreen.cast.proto.AuthError.special_fields) + pub special_fields: ::protobuf::SpecialFields, +} + +impl<'a> ::std::default::Default for &'a AuthError { + fn default() -> &'a AuthError { + ::default_instance() + } +} + +impl AuthError { + pub fn new() -> AuthError { + ::std::default::Default::default() + } + + // required .openscreen.cast.proto.AuthError.ErrorType error_type = 1; + + pub fn error_type(&self) -> auth_error::ErrorType { + match self.error_type { + Some(e) => e.enum_value_or(auth_error::ErrorType::INTERNAL_ERROR), + None => auth_error::ErrorType::INTERNAL_ERROR, + } + } + + pub fn clear_error_type(&mut self) { + self.error_type = ::std::option::Option::None; + } + + pub fn has_error_type(&self) -> bool { + self.error_type.is_some() + } + + // Param is passed by value, moved + pub fn set_error_type(&mut self, v: auth_error::ErrorType) { + self.error_type = ::std::option::Option::Some(::protobuf::EnumOrUnknown::new(v)); + } +} + +impl ::protobuf::Message for AuthError { + const NAME: &'static str = "AuthError"; + + fn is_initialized(&self) -> bool { + if self.error_type.is_none() { + return false; + } + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { + while let Some(tag) = is.read_raw_tag_or_eof()? { + match tag { + 8 => { + self.error_type = ::std::option::Option::Some(is.read_enum_or_unknown()?); + }, + tag => { + ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u64 { + let mut my_size = 0; + if let Some(v) = self.error_type { + my_size += ::protobuf::rt::int32_size(1, v.value()); + } + my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); + self.special_fields.cached_size().set(my_size as u32); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { + if let Some(v) = self.error_type { + os.write_enum(1, ::protobuf::EnumOrUnknown::value(&v))?; + } + os.write_unknown_fields(self.special_fields.unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn special_fields(&self) -> &::protobuf::SpecialFields { + &self.special_fields + } + + fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { + &mut self.special_fields + } + + fn new() -> AuthError { + AuthError::new() + } + + fn clear(&mut self) { + self.error_type = ::std::option::Option::None; + self.special_fields.clear(); + } + + fn default_instance() -> &'static AuthError { + static instance: AuthError = AuthError { + error_type: ::std::option::Option::None, + special_fields: ::protobuf::SpecialFields::new(), + }; + &instance + } +} + +/// Nested message and enums of message `AuthError` +pub mod auth_error { + #[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] + // @@protoc_insertion_point(enum:openscreen.cast.proto.AuthError.ErrorType) + pub enum ErrorType { + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.AuthError.ErrorType.INTERNAL_ERROR) + INTERNAL_ERROR = 0, + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.AuthError.ErrorType.NO_TLS) + NO_TLS = 1, + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.AuthError.ErrorType.SIGNATURE_ALGORITHM_UNAVAILABLE) + SIGNATURE_ALGORITHM_UNAVAILABLE = 2, + } + + impl ::protobuf::Enum for ErrorType { + const NAME: &'static str = "ErrorType"; + + fn value(&self) -> i32 { + *self as i32 + } + + fn from_i32(value: i32) -> ::std::option::Option { + match value { + 0 => ::std::option::Option::Some(ErrorType::INTERNAL_ERROR), + 1 => ::std::option::Option::Some(ErrorType::NO_TLS), + 2 => ::std::option::Option::Some(ErrorType::SIGNATURE_ALGORITHM_UNAVAILABLE), + _ => ::std::option::Option::None + } + } + + fn from_str(str: &str) -> ::std::option::Option { + match str { + "INTERNAL_ERROR" => ::std::option::Option::Some(ErrorType::INTERNAL_ERROR), + "NO_TLS" => ::std::option::Option::Some(ErrorType::NO_TLS), + "SIGNATURE_ALGORITHM_UNAVAILABLE" => ::std::option::Option::Some(ErrorType::SIGNATURE_ALGORITHM_UNAVAILABLE), + _ => ::std::option::Option::None + } + } + + const VALUES: &'static [ErrorType] = &[ + ErrorType::INTERNAL_ERROR, + ErrorType::NO_TLS, + ErrorType::SIGNATURE_ALGORITHM_UNAVAILABLE, + ]; + } + + impl ::std::default::Default for ErrorType { + fn default() -> Self { + ErrorType::INTERNAL_ERROR + } + } + +} + +// @@protoc_insertion_point(message:openscreen.cast.proto.DeviceAuthMessage) +#[derive(PartialEq,Clone,Default,Debug)] +pub struct DeviceAuthMessage { + // message fields + // @@protoc_insertion_point(field:openscreen.cast.proto.DeviceAuthMessage.challenge) + pub challenge: ::protobuf::MessageField, + // @@protoc_insertion_point(field:openscreen.cast.proto.DeviceAuthMessage.response) + pub response: ::protobuf::MessageField, + // @@protoc_insertion_point(field:openscreen.cast.proto.DeviceAuthMessage.error) + pub error: ::protobuf::MessageField, + // special fields + // @@protoc_insertion_point(special_field:openscreen.cast.proto.DeviceAuthMessage.special_fields) + pub special_fields: ::protobuf::SpecialFields, +} + +impl<'a> ::std::default::Default for &'a DeviceAuthMessage { + fn default() -> &'a DeviceAuthMessage { + ::default_instance() + } +} + +impl DeviceAuthMessage { + pub fn new() -> DeviceAuthMessage { + ::std::default::Default::default() + } +} + +impl ::protobuf::Message for DeviceAuthMessage { + const NAME: &'static str = "DeviceAuthMessage"; + + fn is_initialized(&self) -> bool { + for v in &self.challenge { + if !v.is_initialized() { + return false; + } + }; + for v in &self.response { + if !v.is_initialized() { + return false; + } + }; + for v in &self.error { + if !v.is_initialized() { + return false; + } + }; + true + } + + fn merge_from(&mut self, is: &mut ::protobuf::CodedInputStream<'_>) -> ::protobuf::Result<()> { + while let Some(tag) = is.read_raw_tag_or_eof()? { + match tag { + 10 => { + ::protobuf::rt::read_singular_message_into_field(is, &mut self.challenge)?; + }, + 18 => { + ::protobuf::rt::read_singular_message_into_field(is, &mut self.response)?; + }, + 26 => { + ::protobuf::rt::read_singular_message_into_field(is, &mut self.error)?; + }, + tag => { + ::protobuf::rt::read_unknown_or_skip_group(tag, is, self.special_fields.mut_unknown_fields())?; + }, + }; + } + ::std::result::Result::Ok(()) + } + + // Compute sizes of nested messages + #[allow(unused_variables)] + fn compute_size(&self) -> u64 { + let mut my_size = 0; + if let Some(v) = self.challenge.as_ref() { + let len = v.compute_size(); + my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; + } + if let Some(v) = self.response.as_ref() { + let len = v.compute_size(); + my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; + } + if let Some(v) = self.error.as_ref() { + let len = v.compute_size(); + my_size += 1 + ::protobuf::rt::compute_raw_varint64_size(len) + len; + } + my_size += ::protobuf::rt::unknown_fields_size(self.special_fields.unknown_fields()); + self.special_fields.cached_size().set(my_size as u32); + my_size + } + + fn write_to_with_cached_sizes(&self, os: &mut ::protobuf::CodedOutputStream<'_>) -> ::protobuf::Result<()> { + if let Some(v) = self.challenge.as_ref() { + ::protobuf::rt::write_message_field_with_cached_size(1, v, os)?; + } + if let Some(v) = self.response.as_ref() { + ::protobuf::rt::write_message_field_with_cached_size(2, v, os)?; + } + if let Some(v) = self.error.as_ref() { + ::protobuf::rt::write_message_field_with_cached_size(3, v, os)?; + } + os.write_unknown_fields(self.special_fields.unknown_fields())?; + ::std::result::Result::Ok(()) + } + + fn special_fields(&self) -> &::protobuf::SpecialFields { + &self.special_fields + } + + fn mut_special_fields(&mut self) -> &mut ::protobuf::SpecialFields { + &mut self.special_fields + } + + fn new() -> DeviceAuthMessage { + DeviceAuthMessage::new() + } + + fn clear(&mut self) { + self.challenge.clear(); + self.response.clear(); + self.error.clear(); + self.special_fields.clear(); + } + + fn default_instance() -> &'static DeviceAuthMessage { + static instance: DeviceAuthMessage = DeviceAuthMessage { + challenge: ::protobuf::MessageField::none(), + response: ::protobuf::MessageField::none(), + error: ::protobuf::MessageField::none(), + special_fields: ::protobuf::SpecialFields::new(), + }; + &instance + } +} + +#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] +// @@protoc_insertion_point(enum:openscreen.cast.proto.SignatureAlgorithm) +pub enum SignatureAlgorithm { + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.SignatureAlgorithm.UNSPECIFIED) + UNSPECIFIED = 0, + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.SignatureAlgorithm.RSASSA_PKCS1v15) + RSASSA_PKCS1v15 = 1, + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.SignatureAlgorithm.RSASSA_PSS) + RSASSA_PSS = 2, +} + +impl ::protobuf::Enum for SignatureAlgorithm { + const NAME: &'static str = "SignatureAlgorithm"; + + fn value(&self) -> i32 { + *self as i32 + } + + fn from_i32(value: i32) -> ::std::option::Option { + match value { + 0 => ::std::option::Option::Some(SignatureAlgorithm::UNSPECIFIED), + 1 => ::std::option::Option::Some(SignatureAlgorithm::RSASSA_PKCS1v15), + 2 => ::std::option::Option::Some(SignatureAlgorithm::RSASSA_PSS), + _ => ::std::option::Option::None + } + } + + fn from_str(str: &str) -> ::std::option::Option { + match str { + "UNSPECIFIED" => ::std::option::Option::Some(SignatureAlgorithm::UNSPECIFIED), + "RSASSA_PKCS1v15" => ::std::option::Option::Some(SignatureAlgorithm::RSASSA_PKCS1v15), + "RSASSA_PSS" => ::std::option::Option::Some(SignatureAlgorithm::RSASSA_PSS), + _ => ::std::option::Option::None + } + } + + const VALUES: &'static [SignatureAlgorithm] = &[ + SignatureAlgorithm::UNSPECIFIED, + SignatureAlgorithm::RSASSA_PKCS1v15, + SignatureAlgorithm::RSASSA_PSS, + ]; +} + +impl ::std::default::Default for SignatureAlgorithm { + fn default() -> Self { + SignatureAlgorithm::UNSPECIFIED + } +} + + +#[derive(Clone,Copy,PartialEq,Eq,Debug,Hash)] +// @@protoc_insertion_point(enum:openscreen.cast.proto.HashAlgorithm) +pub enum HashAlgorithm { + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.HashAlgorithm.SHA1) + SHA1 = 0, + // @@protoc_insertion_point(enum_value:openscreen.cast.proto.HashAlgorithm.SHA256) + SHA256 = 1, +} + +impl ::protobuf::Enum for HashAlgorithm { + const NAME: &'static str = "HashAlgorithm"; + + fn value(&self) -> i32 { + *self as i32 + } + + fn from_i32(value: i32) -> ::std::option::Option { + match value { + 0 => ::std::option::Option::Some(HashAlgorithm::SHA1), + 1 => ::std::option::Option::Some(HashAlgorithm::SHA256), + _ => ::std::option::Option::None + } + } + + fn from_str(str: &str) -> ::std::option::Option { + match str { + "SHA1" => ::std::option::Option::Some(HashAlgorithm::SHA1), + "SHA256" => ::std::option::Option::Some(HashAlgorithm::SHA256), + _ => ::std::option::Option::None + } + } + + const VALUES: &'static [HashAlgorithm] = &[ + HashAlgorithm::SHA1, + HashAlgorithm::SHA256, + ]; +} + +impl ::std::default::Default for HashAlgorithm { + fn default() -> Self { + HashAlgorithm::SHA1 + } +} + diff --git a/vendor/rust_cast-0.21.0/src/cast/mod.rs b/vendor/rust_cast-0.21.0/src/cast/mod.rs new file mode 100644 index 0000000..2996ee3 --- /dev/null +++ b/vendor/rust_cast-0.21.0/src/cast/mod.rs @@ -0,0 +1,3 @@ +pub mod authority_keys; +pub mod cast_channel; +pub mod proxies; diff --git a/vendor/rust_cast-0.21.0/src/cast/proxies.rs b/vendor/rust_cast-0.21.0/src/cast/proxies.rs new file mode 100644 index 0000000..5844f71 --- /dev/null +++ b/vendor/rust_cast-0.21.0/src/cast/proxies.rs @@ -0,0 +1,529 @@ +/// Proxy classes for the `connection` channel. +pub mod connection { + use serde::Serialize; + + #[derive(Serialize, Debug)] + pub struct ConnectionRequest { + #[serde(rename = "type")] + pub typ: String, + #[serde(rename = "userAgent")] + pub user_agent: String, + } +} + +/// Proxy classes for the `heartbeat` channel. +pub mod heartbeat { + use serde::Serialize; + + #[derive(Serialize, Debug)] + pub struct HeartBeatRequest { + #[serde(rename = "type")] + pub typ: String, + } +} + +/// Proxy classes for the `media` channel. +pub mod media { + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Debug)] + pub struct GetStatusRequest { + #[serde(rename = "requestId")] + pub request_id: u32, + + #[serde(rename = "type")] + pub typ: String, + + #[serde(rename = "mediaSessionId", skip_serializing_if = "Option::is_none")] + pub media_session_id: Option, + } + + // Really LoadRequest + /// https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.LoadRequest + #[derive(Serialize, Debug)] + pub struct MediaRequest { + #[serde(rename = "requestId")] + pub request_id: u32, + + #[serde(rename = "sessionId")] + pub session_id: String, + + #[serde(rename = "type")] + pub typ: String, + + pub media: Media, + + #[serde(rename = "currentTime")] + pub current_time: f64, + + #[serde(rename = "customData")] + pub custom_data: CustomData, + + pub autoplay: bool, + + #[serde(rename = "queueData", skip_serializing_if = "Option::is_none")] + pub queue_data: Option, + } + + /// https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueItem + #[derive(Serialize, Debug)] + pub struct QueueItem { + #[serde(rename = "activeTrackIds")] + #[serde(skip_serializing_if = "Option::is_none")] + pub active_track_ids: Option>, + + pub autoplay: bool, + + #[serde(rename = "customData")] + #[serde(skip_serializing_if = "Option::is_none")] + pub custom_data: Option, + + #[serde(rename = "itemId")] + #[serde(skip_serializing_if = "Option::is_none")] + pub item_id: Option, + + pub media: Media, + + #[serde(rename = "playbackDuration")] + pub playback_duration: Option, + + #[serde(rename = "preloadTime")] + pub preload_time: f64, + + #[serde(rename = "startTime")] + pub start_time: f64, + } + + /// https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueLoadRequest + #[derive(Serialize, Debug)] + pub struct QueueLoadRequest { + #[serde(rename = "type")] + pub typ: String, + + #[serde(rename = "requestId")] + pub request_id: u32, + + #[serde(rename = "customData")] + #[serde(skip_serializing_if = "Option::is_none")] + pub custom_data: Option, + + pub items: Vec, + + // This is from https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueData + #[serde(rename = "queueType")] + #[serde(skip_serializing_if = "Option::is_none")] + pub queue_type: Option, + + #[serde(rename = "repeatMode")] + pub repeat_mode: String, + + #[serde(rename = "startIndex")] + pub start_index: u16, + } + + /// https://developers.google.com/cast/docs/reference/web_sender/chrome.cast.media.QueueData + #[derive(Serialize, Debug)] + pub struct QueueData { + pub items: Vec, + + #[serde(rename = "queueType")] + #[serde(skip_serializing_if = "Option::is_none")] + pub queue_type: Option, + + #[serde(rename = "repeatMode")] + pub repeat_mode: String, + + #[serde(rename = "startIndex")] + pub start_index: u16, + } + + #[derive(Serialize, Debug)] + pub struct PlaybackGenericRequest { + #[serde(rename = "requestId")] + pub request_id: u32, + + #[serde(rename = "mediaSessionId")] + pub media_session_id: i32, + + #[serde(rename = "type")] + pub typ: String, + + #[serde(rename = "customData")] + pub custom_data: CustomData, + } + + #[derive(Serialize, Debug)] + pub struct PlaybackSeekRequest { + #[serde(rename = "requestId")] + pub request_id: u32, + + #[serde(rename = "mediaSessionId")] + pub media_session_id: i32, + + #[serde(rename = "type")] + pub typ: String, + + #[serde(rename = "resumeState")] + pub resume_state: Option, + + #[serde(rename = "currentTime")] + pub current_time: Option, + + #[serde(rename = "customData")] + pub custom_data: CustomData, + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct Media { + #[serde(rename = "contentId")] + pub content_id: String, + #[serde(rename = "streamType", default)] + pub stream_type: String, + #[serde(rename = "contentType")] + pub content_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub metadata: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub duration: Option, + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct Metadata { + #[serde(rename = "metadataType")] + pub metadata_type: u32, + + #[serde(skip_serializing_if = "Option::is_none")] + pub title: Option, + + #[serde(skip_serializing_if = "Option::is_none", rename = "seriesTitle")] + pub series_title: Option, + + #[serde(skip_serializing_if = "Option::is_none", rename = "albumName")] + pub album_name: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub subtitle: Option, + + #[serde(skip_serializing_if = "Option::is_none", rename = "albumArtist")] + pub album_artist: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub artist: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub composer: Option, + + pub images: Vec, + + #[serde(skip_serializing_if = "Option::is_none", rename = "releaseDate")] + pub release_date: Option, + + #[serde(skip_serializing_if = "Option::is_none", rename = "originalAirDate")] + pub original_air_date: Option, + + #[serde(skip_serializing_if = "Option::is_none", rename = "creationDateTime")] + pub creation_date_time: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub studio: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub location: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub latitude: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub longitude: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub season: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub episode: Option, + + #[serde(skip_serializing_if = "Option::is_none", rename = "trackNumber")] + pub track_number: Option, + + #[serde(skip_serializing_if = "Option::is_none", rename = "discNumber")] + pub disc_number: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + } + + impl Metadata { + pub fn new(metadata_type: u32) -> Metadata { + Metadata { + metadata_type, + title: None, + series_title: None, + album_name: None, + subtitle: None, + album_artist: None, + artist: None, + composer: None, + images: Vec::new(), + release_date: None, + original_air_date: None, + creation_date_time: None, + studio: None, + location: None, + latitude: None, + longitude: None, + season: None, + episode: None, + track_number: None, + disc_number: None, + width: None, + height: None, + } + } + } + + #[derive(Serialize, Deserialize, Debug)] + pub struct Image { + pub url: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub width: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub height: Option, + } + + #[derive(Serialize, Debug)] + pub struct CustomData {} + + impl CustomData { + pub fn new() -> CustomData { + CustomData {} + } + } + + #[derive(Deserialize, Debug)] + pub struct ExtendedStatus { + #[serde(rename = "playerState")] + pub player_state: String, + #[serde(rename = "mediaSessionId")] + pub media_session_id: Option, + pub media: Option, + } + + #[derive(Deserialize, Debug)] + pub struct Status { + #[serde(rename = "mediaSessionId")] + pub media_session_id: i32, + #[serde(default)] + pub media: Option, + #[serde(rename = "playbackRate")] + pub playback_rate: f32, + #[serde(rename = "playerState")] + pub player_state: String, + #[serde(rename = "currentItemId")] + pub current_item_id: Option, + #[serde(rename = "loadingItemId")] + pub loading_item_id: Option, + #[serde(rename = "preloadedItemId")] + pub preloaded_item_id: Option, + #[serde(rename = "idleReason")] + pub idle_reason: Option, + #[serde(rename = "extendedStatus")] + pub extended_status: Option, + #[serde(rename = "currentTime")] + pub current_time: Option, + #[serde(rename = "supportedMediaCommands")] + pub supported_media_commands: u32, + } + + #[derive(Deserialize, Debug)] + #[allow(dead_code)] + pub struct StatusReply { + #[serde(rename = "requestId", default)] + pub request_id: u32, + + #[serde(rename = "type")] + pub typ: String, + + pub status: Vec, + } + + #[derive(Deserialize, Debug)] + pub struct LoadCancelledReply { + #[serde(rename = "requestId")] + pub request_id: u32, + } + + #[derive(Deserialize, Debug)] + pub struct LoadFailedReply { + #[serde(rename = "requestId")] + pub request_id: u32, + } + + #[derive(Deserialize, Debug)] + pub struct InvalidPlayerStateReply { + #[serde(rename = "requestId")] + pub request_id: u32, + } + + #[derive(Deserialize, Debug)] + #[allow(dead_code)] + pub struct InvalidRequestReply { + #[serde(rename = "requestId")] + pub request_id: u32, + + #[serde(rename = "type")] + pub typ: String, + + pub reason: Option, + } + + /// The media error encountered during media operations. + #[derive(Deserialize, Debug, PartialEq)] + #[serde(rename_all = "camelCase")] + pub struct MediaErrorReply { + /// The detailed error code associated with the media error. + pub detailed_error_code: i32, + /// The type of the error message. + #[serde(rename = "type")] + pub message_type: String, + } +} + +/// Proxy classes for the `receiver` channel. +pub mod receiver { + use std::borrow::Cow; + + use serde::{Deserialize, Serialize}; + + #[derive(Serialize, Debug)] + pub struct AppLaunchRequest { + #[serde(rename = "requestId")] + pub request_id: u32, + + #[serde(rename = "type")] + pub typ: String, + + #[serde(rename = "appId")] + pub app_id: String, + } + + #[derive(Serialize, Debug)] + pub struct AppStopRequest<'a> { + #[serde(rename = "requestId")] + pub request_id: u32, + + #[serde(rename = "type")] + pub typ: String, + + #[serde(rename = "sessionId")] + pub session_id: Cow<'a, str>, + } + + #[derive(Serialize, Debug)] + pub struct GetStatusRequest { + #[serde(rename = "requestId")] + pub request_id: u32, + + #[serde(rename = "type")] + pub typ: String, + } + + #[derive(Serialize, Debug)] + pub struct SetVolumeRequest { + #[serde(rename = "requestId")] + pub request_id: u32, + + #[serde(rename = "type")] + pub typ: String, + + pub volume: Volume, + } + + #[derive(Deserialize, Debug)] + #[allow(dead_code)] + pub struct StatusReply { + #[serde(rename = "requestId")] + pub request_id: u32, + + #[serde(rename = "type")] + pub typ: String, + + pub status: Status, + } + + #[derive(Deserialize, Debug)] + pub struct Status { + #[serde(default)] + pub applications: Vec, + + #[serde(rename = "isActiveInput", default)] + pub is_active_input: bool, + + #[serde(rename = "isStandBy", default)] + pub is_stand_by: bool, + + /// Volume parameters of the currently active cast device. + pub volume: Volume, + } + + #[derive(Deserialize, Debug)] + pub struct Application { + #[serde(rename = "appId")] + pub app_id: String, + + #[serde(rename = "sessionId")] + pub session_id: String, + + #[serde(rename = "transportId", default)] + pub transport_id: String, + + #[serde(default)] + pub namespaces: Vec, + + #[serde(rename = "displayName")] + pub display_name: String, + + #[serde(rename = "statusText")] + pub status_text: String, + } + + #[derive(Deserialize, Debug)] + pub struct AppNamespace { + pub name: String, + } + + /// Structure that describes possible cast device volume options. + #[derive(Deserialize, Serialize, Debug)] + pub struct Volume { + /// Volume level. + pub level: Option, + /// Mute/unmute state. + pub muted: Option, + } + + #[derive(Deserialize, Debug)] + #[allow(dead_code)] + pub struct LaunchErrorReply { + #[serde(rename = "requestId")] + pub request_id: u32, + + #[serde(rename = "type")] + pub typ: String, + + pub reason: Option, + } + + #[derive(Deserialize, Debug)] + #[allow(dead_code)] + pub struct InvalidRequestReply { + #[serde(rename = "requestId")] + pub request_id: u32, + + #[serde(rename = "type")] + pub typ: String, + + pub reason: Option, + } +} diff --git a/vendor/rust_cast-0.21.0/src/channels/connection.rs b/vendor/rust_cast-0.21.0/src/channels/connection.rs new file mode 100644 index 0000000..9773024 --- /dev/null +++ b/vendor/rust_cast-0.21.0/src/channels/connection.rs @@ -0,0 +1,113 @@ +use std::{ + borrow::Cow, + io::{Read, Write}, +}; + +use crate::{ + Lrc, + cast::proxies, + errors::Error, + message_manager::{CastMessage, CastMessagePayload, MessageManager}, +}; + +pub(crate) const CHANNEL_NAMESPACE: &str = "urn:x-cast:com.google.cast.tp.connection"; +const CHANNEL_USER_AGENT: &str = "RustCast"; + +const MESSAGE_TYPE_CONNECT: &str = "CONNECT"; +const MESSAGE_TYPE_CLOSE: &str = "CLOSE"; + +#[derive(Clone, Debug)] +pub enum ConnectionResponse { + Connect, + Close, + NotImplemented(String, serde_json::Value), +} + +pub struct ConnectionChannel<'a, W> +where + W: Read + Write, +{ + sender: Cow<'a, str>, + message_manager: Lrc>, +} + +impl<'a, W> ConnectionChannel<'a, W> +where + W: Read + Write, +{ + pub fn new(sender: S, message_manager: Lrc>) -> ConnectionChannel<'a, W> + where + S: Into>, + { + ConnectionChannel { + sender: sender.into(), + message_manager, + } + } + + pub fn connect(&self, destination: S) -> Result<(), Error> + where + S: Into>, + { + let payload = serde_json::to_string(&proxies::connection::ConnectionRequest { + typ: MESSAGE_TYPE_CONNECT.to_string(), + user_agent: CHANNEL_USER_AGENT.to_string(), + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: destination.into().to_string(), + payload: CastMessagePayload::String(payload), + }) + } + + pub fn disconnect(&self, destination: S) -> Result<(), Error> + where + S: Into>, + { + let payload = serde_json::to_string(&proxies::connection::ConnectionRequest { + typ: MESSAGE_TYPE_CLOSE.to_string(), + user_agent: CHANNEL_USER_AGENT.to_string(), + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: destination.into().to_string(), + payload: CastMessagePayload::String(payload), + }) + } + + pub fn can_handle(&self, message: &CastMessage) -> bool { + message.namespace == CHANNEL_NAMESPACE + } + + pub fn parse(&self, message: &CastMessage) -> Result { + let reply = match message.payload { + CastMessagePayload::String(ref payload) => { + serde_json::from_str::(payload)? + } + _ => { + return Err(Error::Internal( + "Binary payload is not supported!".to_string(), + )); + } + }; + + let message_type = reply + .as_object() + .and_then(|object| object.get("type")) + .and_then(|property| property.as_str()) + .unwrap_or("") + .to_string(); + + let response = match message_type.as_ref() { + MESSAGE_TYPE_CONNECT => ConnectionResponse::Connect, + MESSAGE_TYPE_CLOSE => ConnectionResponse::Close, + _ => ConnectionResponse::NotImplemented(message_type.to_string(), reply), + }; + + Ok(response) + } +} diff --git a/vendor/rust_cast-0.21.0/src/channels/heartbeat.rs b/vendor/rust_cast-0.21.0/src/channels/heartbeat.rs new file mode 100644 index 0000000..6c23436 --- /dev/null +++ b/vendor/rust_cast-0.21.0/src/channels/heartbeat.rs @@ -0,0 +1,110 @@ +use std::{ + borrow::Cow, + io::{Read, Write}, +}; + +use crate::{ + Lrc, + cast::proxies, + errors::Error, + message_manager::{CastMessage, CastMessagePayload, MessageManager}, +}; + +pub(crate) const CHANNEL_NAMESPACE: &str = "urn:x-cast:com.google.cast.tp.heartbeat"; + +const MESSAGE_TYPE_PING: &str = "PING"; +const MESSAGE_TYPE_PONG: &str = "PONG"; + +#[derive(Clone, Debug)] +pub enum HeartbeatResponse { + Ping, + Pong, + NotImplemented(String, serde_json::Value), +} + +pub struct HeartbeatChannel<'a, W> +where + W: Read + Write, +{ + sender: Cow<'a, str>, + receiver: Cow<'a, str>, + message_manager: Lrc>, +} + +impl<'a, W> HeartbeatChannel<'a, W> +where + W: Read + Write, +{ + pub fn new( + sender: S, + receiver: S, + message_manager: Lrc>, + ) -> HeartbeatChannel<'a, W> + where + S: Into>, + { + HeartbeatChannel { + sender: sender.into(), + receiver: receiver.into(), + message_manager, + } + } + + pub fn ping(&self) -> Result<(), Error> { + let payload = serde_json::to_string(&proxies::heartbeat::HeartBeatRequest { + typ: MESSAGE_TYPE_PING.to_string(), + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: self.receiver.to_string(), + payload: CastMessagePayload::String(payload), + }) + } + + pub fn pong(&self) -> Result<(), Error> { + let payload = serde_json::to_string(&proxies::heartbeat::HeartBeatRequest { + typ: MESSAGE_TYPE_PONG.to_string(), + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: self.receiver.to_string(), + payload: CastMessagePayload::String(payload), + }) + } + + pub fn can_handle(&self, message: &CastMessage) -> bool { + message.namespace == CHANNEL_NAMESPACE + } + + pub fn parse(&self, message: &CastMessage) -> Result { + let reply = match message.payload { + CastMessagePayload::String(ref payload) => { + serde_json::from_str::(payload)? + } + _ => { + return Err(Error::Internal( + "Binary payload is not supported!".to_string(), + )); + } + }; + + let message_type = reply + .as_object() + .and_then(|object| object.get("type")) + .and_then(|property| property.as_str()) + .unwrap_or("") + .to_string(); + + let response = match message_type.as_ref() { + MESSAGE_TYPE_PING => HeartbeatResponse::Ping, + MESSAGE_TYPE_PONG => HeartbeatResponse::Pong, + _ => HeartbeatResponse::NotImplemented(message_type.to_string(), reply), + }; + + Ok(response) + } +} diff --git a/vendor/rust_cast-0.21.0/src/channels/media.rs b/vendor/rust_cast-0.21.0/src/channels/media.rs new file mode 100644 index 0000000..b42f756 --- /dev/null +++ b/vendor/rust_cast-0.21.0/src/channels/media.rs @@ -0,0 +1,1643 @@ +use std::{ + borrow::Cow, + fmt, + io::{Read, Write}, + str::FromStr, + string::ToString, +}; + +use crate::{ + Lrc, + cast::proxies, + errors::Error, + message_manager::{CastMessage, CastMessagePayload, MessageManager}, +}; + +pub(crate) const CHANNEL_NAMESPACE: &str = "urn:x-cast:com.google.cast.media"; + +const MESSAGE_TYPE_GET_STATUS: &str = "GET_STATUS"; +const MESSAGE_TYPE_LOAD: &str = "LOAD"; +const MESSAGE_TYPE_QUEUE_LOAD: &str = "QUEUE_LOAD"; +const MESSAGE_TYPE_PLAY: &str = "PLAY"; +const MESSAGE_TYPE_PAUSE: &str = "PAUSE"; +const MESSAGE_TYPE_STOP: &str = "STOP"; +const MESSAGE_TYPE_SEEK: &str = "SEEK"; +const MESSAGE_TYPE_MEDIA_STATUS: &str = "MEDIA_STATUS"; +const MESSAGE_TYPE_LOAD_CANCELLED: &str = "LOAD_CANCELLED"; +const MESSAGE_TYPE_LOAD_FAILED: &str = "LOAD_FAILED"; +const MESSAGE_TYPE_INVALID_PLAYER_STATE: &str = "INVALID_PLAYER_STATE"; +const MESSAGE_TYPE_INVALID_REQUEST: &str = "INVALID_REQUEST"; +const MESSAGE_TYPE_ERROR: &str = "ERROR"; + +/// Describes the way cast device should stream content. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum StreamType { + /// This variant allows cast device to automatically choose whatever way it's most comfortable + /// with. + None, + /// Cast device should buffer some portion of the content and only then start streaming. + Buffered, + /// Cast device should display content as soon as it gets any portion of it. + Live, +} + +impl FromStr for StreamType { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "BUFFERED" | "buffered" => Ok(StreamType::Buffered), + "LIVE" | "live" => Ok(StreamType::Live), + _ => Ok(StreamType::None), + } + } +} + +impl fmt::Display for StreamType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let stream_type = match *self { + StreamType::None => "NONE", + StreamType::Buffered => "BUFFERED", + StreamType::Live => "LIVE", + }; + + write!(f, "{}", stream_type) + } +} + +/// Generic, movie, TV show, music track, or photo metadata. +#[derive(Clone, Debug, PartialEq)] +pub enum Metadata { + Generic(GenericMediaMetadata), + Movie(MovieMediaMetadata), + TvShow(TvShowMediaMetadata), + MusicTrack(MusicTrackMediaMetadata), + Photo(PhotoMediaMetadata), +} + +impl Metadata { + fn encode(&self) -> proxies::media::Metadata { + match self { + Metadata::Generic(x) => proxies::media::Metadata { + title: x.title.clone(), + subtitle: x.subtitle.clone(), + images: x.images.iter().map(|i| i.encode()).collect(), + release_date: x.release_date.clone(), + ..proxies::media::Metadata::new(0) + }, + Metadata::Movie(x) => proxies::media::Metadata { + title: x.title.clone(), + subtitle: x.subtitle.clone(), + studio: x.studio.clone(), + images: x.images.iter().map(|i| i.encode()).collect(), + release_date: x.release_date.clone(), + ..proxies::media::Metadata::new(1) + }, + Metadata::TvShow(x) => proxies::media::Metadata { + series_title: x.series_title.clone(), + subtitle: x.episode_title.clone(), + season: x.season, + episode: x.episode, + images: x.images.iter().map(|i| i.encode()).collect(), + original_air_date: x.original_air_date.clone(), + ..proxies::media::Metadata::new(2) + }, + Metadata::MusicTrack(x) => proxies::media::Metadata { + album_name: x.album_name.clone(), + title: x.title.clone(), + album_artist: x.album_artist.clone(), + artist: x.artist.clone(), + composer: x.composer.clone(), + track_number: x.track_number, + disc_number: x.disc_number, + images: x.images.iter().map(|i| i.encode()).collect(), + release_date: x.release_date.clone(), + ..proxies::media::Metadata::new(3) + }, + Metadata::Photo(x) => proxies::media::Metadata { + title: x.title.clone(), + artist: x.artist.clone(), + location: x.location.clone(), + latitude: x.latitude_longitude.map(|coord| coord.0), + longitude: x.latitude_longitude.map(|coord| coord.1), + width: x.dimensions.map(|dims| dims.0), + height: x.dimensions.map(|dims| dims.1), + creation_date_time: x.creation_date_time.clone(), + ..proxies::media::Metadata::new(4) + }, + } + } +} + +impl TryFrom<&proxies::media::Metadata> for Metadata { + type Error = Error; + + fn try_from(m: &proxies::media::Metadata) -> Result { + Ok(match m.metadata_type { + 0 => Self::Generic(GenericMediaMetadata { + title: m.title.clone(), + subtitle: m.subtitle.clone(), + images: m.images.iter().map(Image::from).collect(), + release_date: m.release_date.clone(), + }), + 1 => Self::Movie(MovieMediaMetadata { + title: m.title.clone(), + subtitle: m.subtitle.clone(), + studio: m.studio.clone(), + images: m.images.iter().map(Image::from).collect(), + release_date: m.release_date.clone(), + }), + 2 => Self::TvShow(TvShowMediaMetadata { + series_title: m.series_title.clone(), + episode_title: m.subtitle.clone(), + season: m.season, + episode: m.episode, + images: m.images.iter().map(Image::from).collect(), + original_air_date: m.original_air_date.clone(), + }), + 3 => Self::MusicTrack(MusicTrackMediaMetadata { + album_name: m.album_name.clone(), + title: m.title.clone(), + album_artist: m.album_artist.clone(), + artist: m.artist.clone(), + composer: m.composer.clone(), + track_number: m.track_number, + disc_number: m.disc_number, + images: m.images.iter().map(Image::from).collect(), + release_date: m.release_date.clone(), + }), + 4 => { + let mut dimensions = None; + let mut latitude_longitude = None; + if let Some(width) = m.width + && let Some(height) = m.height + { + dimensions = Some((width, height)) + } + if let Some(lat) = m.latitude + && let Some(long) = m.longitude + { + latitude_longitude = Some((lat, long)) + } + Self::Photo(PhotoMediaMetadata { + title: m.title.clone(), + artist: m.artist.clone(), + location: m.location.clone(), + latitude_longitude, + dimensions, + creation_date_time: m.creation_date_time.clone(), + }) + } + _ => { + return Err(Error::Parsing(format!( + "Bad metadataType {}", + m.metadata_type + ))); + } + }) + } +} + +/// Generic media metadata. +/// +/// See also the [`GenericMediaMetadata` Cast reference](https://developers.google.com/cast/docs/reference/messages#GenericMediaMetadata). +#[derive(Clone, Debug, Default, PartialEq)] +pub struct GenericMediaMetadata { + /// Descriptive title of the content. + pub title: Option, + /// Descriptive subtitle of the content. + pub subtitle: Option, + /// Zero or more URLs to an image associated with the content. + pub images: Vec, + /// Date and time the content was released, formatted as ISO 8601. + pub release_date: Option, +} + +/// Movie media metadata. +/// +/// See also the [`MovieMediaMetadata` Cast reference](https://developers.google.com/cast/docs/reference/messages#MovieMediaMetadata). +#[derive(Clone, Debug, Default, PartialEq)] +pub struct MovieMediaMetadata { + /// Title of the movie. + pub title: Option, + /// Subtitle of the movie. + pub subtitle: Option, + /// Studio which released the movie. + pub studio: Option, + /// Zero or more URLs to an image associated with the content. + pub images: Vec, + /// Date and time the movie was released, formatted as ISO 8601. + pub release_date: Option, +} + +/// TV show media metadata. +/// +/// See also the [`TvShowMediaMetadata` Cast reference](https://developers.google.com/cast/docs/reference/messages#TvShowMediaMetadata). +#[derive(Clone, Debug, Default, PartialEq)] +pub struct TvShowMediaMetadata { + /// Title of the TV series. + pub series_title: Option, + /// Title of the episode. + pub episode_title: Option, + /// Season number of the TV show. + pub season: Option, + /// Episode number (in the season) of the episode. + pub episode: Option, + /// Zero or more URLs to an image associated with the content. + pub images: Vec, + /// Date and time this episode was released, formatted as ISO 8601. + pub original_air_date: Option, +} + +/// Music track media metadata. +/// +/// See also the [`MusicTrackMediaMetadata` Cast reference](https://developers.google.com/cast/docs/reference/messages#MusicTrackMediaMetadata). +#[derive(Clone, Debug, Default, PartialEq)] +pub struct MusicTrackMediaMetadata { + /// Album or collection from which the track is taken. + pub album_name: Option, + /// Name of the track (for example, song title). + pub title: Option, + /// Name of the artist associated with the album featuring this track. + pub album_artist: Option, + /// Name of the artist associated with the track. + pub artist: Option, + /// Name of the composer associated with the track. + pub composer: Option, + /// Number of the track on the album. + pub track_number: Option, + /// Number of the volume (for example, a disc) of the album. + pub disc_number: Option, + /// Zero or more URLs to an image associated with the content. + pub images: Vec, + /// Date and time the content was released, formatted as ISO 8601. + pub release_date: Option, +} + +/// Photo media metadata. +/// +/// See also the [`PhotoMediaMetadata` Cast reference](https://developers.google.com/cast/docs/reference/messages#PhotoMediaMetadata). +#[derive(Clone, Debug, Default, PartialEq)] +pub struct PhotoMediaMetadata { + /// Title of the photograph. + pub title: Option, + /// Name of the photographer. + pub artist: Option, + /// Verbal location where the photograph was taken, for example “Madrid, Spain”. + pub location: Option, + /// Latitude and longitude of the location where the photograph was taken. + pub latitude_longitude: Option<(f64, f64)>, + /// Width and height of the photograph in pixels. + pub dimensions: Option<(u32, u32)>, + /// Date and time the photograph was taken, formatted as ISO 8601. + pub creation_date_time: Option, +} + +/// Image URL and optionally size metadata. +/// +/// This is the description of an image, including a small amount of metadata to +/// allow the sender application a choice of images, depending on how it will +/// render them. The height and width are optional on only one item in an array +/// of images. +/// +/// See also the [`Image` Cast reference](https://developers.google.com/cast/docs/reference/messages#Image). +#[derive(Clone, Debug, PartialEq)] +pub struct Image { + /// URL of the image. + pub url: String, + /// Width and height of the image. + pub dimensions: Option<(u32, u32)>, +} + +impl Image { + pub fn new(url: String) -> Image { + Image { + url, + dimensions: None, + } + } + + fn encode(&self) -> proxies::media::Image { + proxies::media::Image { + url: self.url.clone(), + width: self.dimensions.map(|d| d.0), + height: self.dimensions.map(|d| d.1), + } + } +} + +impl From<&proxies::media::Image> for Image { + fn from(i: &proxies::media::Image) -> Self { + let mut dimensions = None; + if let Some(width) = i.width + && let Some(height) = i.height + { + dimensions = Some((width, height)); + }; + Self { + url: i.url.clone(), + dimensions, + } + } +} + +/// Describes possible player states. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum PlayerState { + /// Player has not been loaded yet. + Idle, + /// Player is actively playing content. + Playing, + /// Player is in PLAY mode but not actively playing content (currentTime is not changing). + Buffering, + /// Player is paused. + Paused, +} + +impl FromStr for PlayerState { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "IDLE" => Ok(PlayerState::Idle), + "PLAYING" => Ok(PlayerState::Playing), + "BUFFERING" => Ok(PlayerState::Buffering), + "PAUSED" => Ok(PlayerState::Paused), + _ => Err(Error::Internal(format!("Unknown player state {}", s))), + } + } +} + +impl fmt::Display for PlayerState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let player_state = match *self { + PlayerState::Idle => "IDLE", + PlayerState::Playing => "PLAYING", + PlayerState::Buffering => "BUFFERING", + PlayerState::Paused => "PAUSED", + }; + + write!(f, "{}", player_state) + } +} + +/// Describes possible player states. +/// Can appear when the base state is PlayerState::Idle +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ExtendedPlayerState { + /// Player is loading the next media + Loading, +} + +impl FromStr for ExtendedPlayerState { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "LOADING" => Ok(Self::Loading), + _ => Err(Error::Internal(format!( + "Unknown extended player state {}", + s + ))), + } + } +} + +impl fmt::Display for ExtendedPlayerState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let player_state = match *self { + Self::Loading => "LOADING", + }; + + write!(f, "{}", player_state) + } +} + +/// Describes possible player idle reasons. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum IdleReason { + /// A sender requested to stop playback using the STOP command. + Cancelled, + /// A sender requested playing a different media using the LOAD command. + Interrupted, + /// The media playback completed. + Finished, + /// The media was interrupted due to an error; For example, if the player could not download the + /// media due to network issues. + Error, +} + +impl FromStr for IdleReason { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "CANCELLED" => Ok(IdleReason::Cancelled), + "INTERRUPTED" => Ok(IdleReason::Interrupted), + "FINISHED" => Ok(IdleReason::Finished), + "ERROR" => Ok(IdleReason::Error), + _ => Err(Error::Internal(format!("Unknown idle reason {}", s))), + } + } +} + +/// +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum QueueType { + Album, + Playlist, + Audiobook, + RadioStation, + PodcastSeries, + TvSeries, + VideoPlaylist, + LiveTv, + Movie, +} + +impl FromStr for QueueType { + type Err = Error; + fn from_str(s: &str) -> Result { + match s { + "ALBUM" => Ok(Self::Album), + "PLAYLIST" => Ok(Self::Playlist), + "AUDIOBOOK" => Ok(Self::Audiobook), + "RADIO_STATION" => Ok(Self::RadioStation), + "PODCAST_SERIES" => Ok(Self::PodcastSeries), + "TV_SERIES" => Ok(Self::TvSeries), + "VIDEO_PLAYLIST" => Ok(Self::VideoPlaylist), + "LIVE_TV" => Ok(Self::LiveTv), + "MOVIE" => Ok(Self::Movie), + _ => Err(Error::Internal(format!("Unknown queue type {}", s))), + } + } +} + +impl fmt::Display for QueueType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let str = match self { + QueueType::Album => "ALBUM", + QueueType::Playlist => "PLAYLIST", + QueueType::Audiobook => "AUDIOBOOK", + QueueType::RadioStation => "RADIO_STATION", + QueueType::PodcastSeries => "PODCAST_SERIES", + QueueType::TvSeries => "TV_SERIES", + QueueType::VideoPlaylist => "VIDEO_PLAYLIST", + QueueType::LiveTv => "LIVE_TV", + QueueType::Movie => "MOVIE", + } + .to_string(); + write!(f, "{}", str) + } +} + +/// Describes the operation to perform with playback while seeking. +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ResumeState { + /// Forces media to start. + PlaybackStart, + /// Forces media to pause. + PlaybackPause, +} + +impl FromStr for ResumeState { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s { + "PLAYBACK_START" | "start" => Ok(ResumeState::PlaybackStart), + "PLAYBACK_PAUSE" | "pause" => Ok(ResumeState::PlaybackPause), + _ => Err(Error::Internal(format!("Unknown resume state {}", s))), + } + } +} + +impl fmt::Display for ResumeState { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let resume_state = match *self { + ResumeState::PlaybackStart => "PLAYBACK_START", + ResumeState::PlaybackPause => "PLAYBACK_PAUSE", + }; + + write!(f, "{}", resume_state) + } +} + +/// This data structure describes a media stream. +#[derive(Clone, Debug, PartialEq)] +pub struct Media { + /// Service-specific identifier of the content currently loaded by the media player. This is a + /// free form string and is specific to the application. In most cases, this will be the URL to + /// the media, but the sender can choose to pass a string that the receiver can interpret + /// properly. Max length: 1k. + pub content_id: String, + /// Describes the type of media artifact. + pub stream_type: StreamType, + /// MIME content type of the media being played. + pub content_type: String, + /// Generic, movie, TV show, music track, or photo metadata. + pub metadata: Option, + /// Duration of the currently playing stream in seconds. + pub duration: Option, +} + +impl Media { + fn encode(&self) -> proxies::media::Media { + let metadata = self.metadata.as_ref().map(|m| m.encode()); + + proxies::media::Media { + content_id: self.content_id.clone(), + stream_type: self.stream_type.to_string(), + content_type: self.content_type.clone(), + metadata, + duration: self.duration, + } + } +} + +impl TryFrom<&proxies::media::Media> for Media { + type Error = Error; + + fn try_from(m: &proxies::media::Media) -> Result { + Ok(Self { + content_id: m.content_id.to_string(), + stream_type: StreamType::from_str(m.stream_type.as_ref())?, + content_type: m.content_type.to_string(), + metadata: m.metadata.as_ref().map(TryInto::try_into).transpose()?, + duration: m.duration, + }) + } +} + +/// One item in a queue +#[derive(Clone, Debug)] +pub struct QueueItem { + /// The item as media + pub media: Media, +} + +impl QueueItem { + fn encode(&self) -> proxies::media::QueueItem { + proxies::media::QueueItem { + active_track_ids: None, + autoplay: true, + custom_data: None, + item_id: None, + media: self.media.encode(), + playback_duration: None, + preload_time: 20., + start_time: 0., + } + } +} + +/// A queue of items to play in sequence +#[derive(Clone, Debug)] +pub struct MediaQueue { + /// Every item in the queue, in order + pub items: Vec, + /// Array index of the first item to be played + /// Starts at zero. + pub start_index: u16, + /// What the queue represents + pub queue_type: QueueType, +} + +impl MediaQueue { + fn encode(&self) -> proxies::media::QueueData { + proxies::media::QueueData { + items: self.items.iter().map(|qi| qi.encode()).collect(), + queue_type: Some(self.queue_type.to_string()), + repeat_mode: "REPEAT_OFF".to_owned(), + start_index: self.start_index, + } + } +} + +/// Describes the current status of the media artifact with respect to the session. +#[derive(Clone, Debug, PartialEq)] +pub struct Status { + /// Unique id of the request that requested the status. + pub request_id: u32, + /// Detailed status of every media status entry. + pub entries: Vec, +} + +/// Status of loading the next media +#[derive(Clone, Debug, PartialEq)] +pub struct ExtendedStatus { + /// Describes the state of the player. + pub player_state: ExtendedPlayerState, + /// Unique ID for the playback of this specific session. This ID is set by the receiver at LOAD + /// and can be used to identify a specific instance of a playback. For example, two playbacks of + /// "Wish you were here" within the same session would each have a unique mediaSessionId. + pub media_session_id: Option, + /// Full description of the content that is being played back. Only be returned in a status + /// messages if the Media has changed. + pub media: Option, +} + +impl TryFrom<&proxies::media::ExtendedStatus> for ExtendedStatus { + type Error = Error; + + fn try_from(es: &proxies::media::ExtendedStatus) -> Result { + Ok(Self { + player_state: ExtendedPlayerState::from_str(&es.player_state)?, + media_session_id: es.media_session_id, + media: es.media.as_ref().map(Media::try_from).transpose()?, + }) + } +} + +/// Detailed status of the media artifact with respect to the session. +#[derive(Clone, Debug, PartialEq)] +pub struct StatusEntry { + /// Unique ID for the playback of this specific session. This ID is set by the receiver at LOAD + /// and can be used to identify a specific instance of a playback. For example, two playbacks of + /// "Wish you were here" within the same session would each have a unique mediaSessionId. + pub media_session_id: i32, + /// Full description of the content that is being played back. Only be returned in a status + /// messages if the Media has changed. + pub media: Option, + /// Indicates whether the media time is progressing, and at what rate. This is independent of + /// the player state since the media time can stop in any state. 1.0 is regular time, 0.5 is + /// slow motion. + pub playback_rate: f32, + /// Describes the state of the player. + pub player_state: PlayerState, + /// Id of the current queue item + pub current_item_id: Option, + /// Id of the item currently loading + pub loading_item_id: Option, + /// Id of the item currently preloaded + pub preloaded_item_id: Option, + /// If the player_state is IDLE and the reason it became IDLE is known, this property is + /// provided. If the player is IDLE because it just started, this property will not be provided. + /// If the player is in any other state this property should not be provided. + pub idle_reason: Option, + /// An extended status can be used when the player is idle (no playback) but also loading + /// another media. + pub extended_status: Option, + /// The current position of the media player since the beginning of the content, in seconds. + /// If this a live stream content, then this field represents the time in seconds from the + /// beginning of the event that should be known to the player. + pub current_time: Option, + /// Flags describing which media commands the media player supports: + /// * `1` `Pause`; + /// * `2` `Seek`; + /// * `4` `Stream volume`; + /// * `8` `Stream mute`; + /// * `16` `Skip forward`; + /// * `32` `Skip backward`; + /// * `1 << 12` `Unknown`; + /// * `1 << 13` `Unknown`; + /// * `1 << 18` `Unknown`. + /// + /// Combinations are described as summations; for example, Pause+Seek+StreamVolume+Mute == 15. + pub supported_media_commands: u32, +} + +impl TryFrom<&proxies::media::Status> for StatusEntry { + type Error = Error; + + fn try_from(x: &proxies::media::Status) -> Result { + Ok(Self { + media_session_id: x.media_session_id, + media: x.media.as_ref().map(TryInto::try_into).transpose()?, + playback_rate: x.playback_rate, + player_state: PlayerState::from_str(x.player_state.as_ref())?, + current_item_id: x.current_item_id, + loading_item_id: x.loading_item_id, + preloaded_item_id: x.preloaded_item_id, + idle_reason: x + .idle_reason + .as_ref() + .map(|reason| IdleReason::from_str(reason)) + .transpose()?, + extended_status: x + .extended_status + .as_ref() + .map(ExtendedStatus::try_from) + .transpose()?, + current_time: x.current_time, + supported_media_commands: x.supported_media_commands, + }) + } +} + +/// Describes the load cancelled error. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct LoadCancelled { + /// Unique id of the request that caused this error. + pub request_id: u32, +} + +/// Describes the load failed error. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct LoadFailed { + /// Unique id of the request that caused this error. + pub request_id: u32, +} + +/// The additional options for a load command request. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct LoadOptions { + /// The current time of the content to start the playback at. + pub current_time: f64, + /// Whether to start playback automatically after the media has been loaded. + pub autoplay: bool, +} + +impl Default for LoadOptions { + fn default() -> Self { + LoadOptions { + current_time: 0f64, + autoplay: true, + } + } +} + +/// Describes the invalid player state error. +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct InvalidPlayerState { + /// Unique id of the request that caused this error. + pub request_id: u32, +} + +/// Describes the invalid request error. +#[derive(Clone, Debug, PartialEq)] +pub struct InvalidRequest { + /// Unique id of the invalid request. + pub request_id: u32, + /// Description of the invalid request reason if available. + pub reason: Option, +} + +/// The media error encountered during media operations. +#[derive(Clone, Debug, PartialEq)] +pub struct MediaError { + /// The detailed error code associated with the media error. + pub detailed_error_code: MediaDetailedErrorCode, + /// The type of the error message. + pub message_type: String, +} + +/// The detailed media error code. +/// https://developers.google.com/android/reference/com/google/android/gms/cast/MediaError.DetailedErrorCode#constants +#[derive(Clone, Debug, PartialEq)] +pub enum MediaDetailedErrorCode { + /// An error occurs outside of the framework (e.g., if an event handler throws an error). + App = 900, + /// Break clip load interceptor fails. + BreakClipLoadingError = 901, + /// Break seek interceptor fails. + BreakSeekInterceptorError = 902, + /// A DASH manifest contains invalid segment info. + DashInvalidSegmentInfo = 423, + /// A DASH manifest is missing a MimeType. + DashManifestNoMimeType = 422, + /// A DASH manifest is missing periods. + DashManifestNoPeriods = 421, + /// An unknown error occurs while parsing a DASH manifest. + DashManifestUnknown = 420, + /// An unknown network error occurs while handling a DASH stream. + DashNetwork = 321, + /// A DASH stream is missing an init. + DashNoInit = 322, + /// Returned when an unknown error occurs. + Generic = 999, + /// An error occurs while parsing an HLS master manifest. + HlsManifestMaster = 411, + /// An error occurs while parsing an HLS playlist. + HlsManifestPlaylist = 412, + /// An HLS segment is invalid. + HlsNetworkInvalidSegment = 315, + /// A request for an HLS key fails before it is sent. + HlsNetworkKeyLoad = 314, + /// An HLS master playlist fails to download. + HlsNetworkMasterPlaylist = 311, + /// An HLS key fails to download. + HlsNetworkNoKeyResponse = 313, + /// An HLS playlist fails to download. + HlsNetworkPlaylist = 312, + /// An HLS segment fails to parse. + HlsSegmentParsing = 316, + /// When an image fails to load. + ImageError = 903, + /// A load command failed. + LoadFailed = 905, + /// A load was interrupted by an unload, or by another load. + LoadInterrupted = 904, + /// An unknown error occurs while parsing a manifest. + ManifestUnknown = 400, + /// There is a media keys failure due to a network issue. + MediakeysNetwork = 201, + /// There is an unknown error with media keys. + MediakeysUnknown = 200, + /// A MediaKeySession object cannot be created. + MediakeysUnsupported = 202, + /// Crypto failed. + MediakeysWebcrypto = 203, + /// The fetching process for the media resource was aborted by the user agent at the user's request. + MediaAborted = 101, + /// An error occurred while decoding the media resource, after the resource was established to be usable. + MediaDecode = 102, + /// An error message was sent to the sender. + MediaErrorMessage = 906, + /// A network error caused the user agent to stop fetching the media resource, after the resource was established to be usable. + MediaNetwork = 103, + /// The media resource indicated by the src attribute was not suitable. + MediaSrcNotSupported = 104, + /// The HTMLMediaElement throws an error, but CAF does not recognize the specific error. + MediaUnknown = 100, + /// There was an unknown network issue. + NetworkUnknown = 300, + /// A segment fails to download. + SegmentNetwork = 301, + /// An unknown segment error occurs. + SegmentUnknown = 500, + /// An error occurs while parsing a Smooth manifest. + SmoothManifest = 431, + /// An unknown network error occurs while handling a Smooth stream. + SmoothNetwork = 331, + /// A Smooth stream is missing media data. + SmoothNoMediaData = 332, + /// A source buffer cannot be added to the MediaSource. + SourceBufferFailure = 110, + /// An unknown error occurred with a text stream. + TextUnknown = 600, +} + +impl TryFrom for MediaDetailedErrorCode { + type Error = Error; + + fn try_from(value: i32) -> Result { + match value { + 900 => Ok(MediaDetailedErrorCode::App), + 901 => Ok(MediaDetailedErrorCode::BreakClipLoadingError), + 902 => Ok(MediaDetailedErrorCode::BreakSeekInterceptorError), + 423 => Ok(MediaDetailedErrorCode::DashInvalidSegmentInfo), + 422 => Ok(MediaDetailedErrorCode::DashManifestNoMimeType), + 421 => Ok(MediaDetailedErrorCode::DashManifestNoPeriods), + 420 => Ok(MediaDetailedErrorCode::DashManifestUnknown), + 321 => Ok(MediaDetailedErrorCode::DashNetwork), + 322 => Ok(MediaDetailedErrorCode::DashNoInit), + 999 => Ok(MediaDetailedErrorCode::Generic), + 411 => Ok(MediaDetailedErrorCode::HlsManifestMaster), + 412 => Ok(MediaDetailedErrorCode::HlsManifestPlaylist), + 315 => Ok(MediaDetailedErrorCode::HlsNetworkInvalidSegment), + 314 => Ok(MediaDetailedErrorCode::HlsNetworkKeyLoad), + 311 => Ok(MediaDetailedErrorCode::HlsNetworkMasterPlaylist), + 313 => Ok(MediaDetailedErrorCode::HlsNetworkNoKeyResponse), + 312 => Ok(MediaDetailedErrorCode::HlsNetworkPlaylist), + 316 => Ok(MediaDetailedErrorCode::HlsSegmentParsing), + 903 => Ok(MediaDetailedErrorCode::ImageError), + 905 => Ok(MediaDetailedErrorCode::LoadFailed), + 904 => Ok(MediaDetailedErrorCode::LoadInterrupted), + 400 => Ok(MediaDetailedErrorCode::ManifestUnknown), + 201 => Ok(MediaDetailedErrorCode::MediakeysNetwork), + 200 => Ok(MediaDetailedErrorCode::MediakeysUnknown), + 202 => Ok(MediaDetailedErrorCode::MediakeysUnsupported), + 203 => Ok(MediaDetailedErrorCode::MediakeysWebcrypto), + 101 => Ok(MediaDetailedErrorCode::MediaAborted), + 102 => Ok(MediaDetailedErrorCode::MediaDecode), + 906 => Ok(MediaDetailedErrorCode::MediaErrorMessage), + 103 => Ok(MediaDetailedErrorCode::MediaNetwork), + 104 => Ok(MediaDetailedErrorCode::MediaSrcNotSupported), + 100 => Ok(MediaDetailedErrorCode::MediaUnknown), + 300 => Ok(MediaDetailedErrorCode::NetworkUnknown), + 301 => Ok(MediaDetailedErrorCode::SegmentNetwork), + 500 => Ok(MediaDetailedErrorCode::SegmentUnknown), + 431 => Ok(MediaDetailedErrorCode::SmoothManifest), + 331 => Ok(MediaDetailedErrorCode::SmoothNetwork), + 332 => Ok(MediaDetailedErrorCode::SmoothNoMediaData), + 110 => Ok(MediaDetailedErrorCode::SourceBufferFailure), + 600 => Ok(MediaDetailedErrorCode::TextUnknown), + _ => Err(Error::Parsing(format!( + "media error code {} is not supported", + value + ))), + } + } +} + +/// Represents all currently supported incoming messages that media channel can handle. +#[derive(Clone, Debug, PartialEq)] +pub enum MediaResponse { + /// Statuses of the currently active media. + Status(Status), + /// Sent when the load request was cancelled (a second load request was received). + LoadCancelled(LoadCancelled), + /// Sent when the load request failed. The player state will be IDLE. + LoadFailed(LoadFailed), + /// Sent when the request by the sender can not be fulfilled because the player is not in a + /// valid state. For example, if the application has not created a media element yet. + InvalidPlayerState(InvalidPlayerState), + /// Error indicating that request is not valid. + InvalidRequest(InvalidRequest), + /// The media error that occurred while executing a media operation on the media channel. + Error(MediaError), + /// Used every time when channel can't parse the message. Associated data contains `type` string + /// field and raw JSON data returned from cast device. + NotImplemented(String, serde_json::Value), +} + +pub struct MediaChannel<'a, W> +where + W: Read + Write, +{ + sender: Cow<'a, str>, + message_manager: Lrc>, +} + +impl<'a, W> MediaChannel<'a, W> +where + W: Read + Write, +{ + pub fn new(sender: S, message_manager: Lrc>) -> MediaChannel<'a, W> + where + S: Into>, + { + MediaChannel { + sender: sender.into(), + message_manager, + } + } + + /// Retrieves status of the cast device media session. + /// + /// # Arguments + /// + /// * `destination` - `protocol` identifier of specific app media session; + /// * `media_session_id` - Media session ID of the media for which the media status should be + /// returned. If none is provided, then the status for all media session IDs will be provided. + /// + /// # Return value + /// + /// Returned `Result` should consist of either `Status` instance or an `Error`. + pub fn get_status( + &self, + destination: S, + media_session_id: Option, + ) -> Result + where + S: Into>, + { + let request_id = self.message_manager.generate_request_id().get(); + + let payload = serde_json::to_string(&proxies::media::GetStatusRequest { + typ: MESSAGE_TYPE_GET_STATUS.to_string(), + request_id, + media_session_id, + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: destination.into().to_string(), + payload: CastMessagePayload::String(payload), + })?; + + self.message_manager.receive_find_map(|message| { + if !self.can_handle(message) { + return Ok(None); + } + + match self.parse(message)? { + MediaResponse::Status(status) => { + if status.request_id == request_id { + return Ok(Some(status)); + } + } + MediaResponse::InvalidRequest(error) => { + if error.request_id == request_id { + return Err(Error::Internal(format!( + "Invalid request ({}).", + error.reason.unwrap_or_else(|| "Unknown".to_string()) + ))); + } + } + _ => {} + } + + Ok(None) + }) + } + + /// Loads provided media to the application. + /// + /// # Arguments + /// * `destination` - `protocol` of the application to load media with (e.g. `web-1`); + /// * `session_id` - Current session identifier of the player application; + /// * `media` - `Media` instance that describes the media we'd like to load. + /// + /// # Return value + /// + /// Returned `Result` should consist of either `Status` instance or an `Error`. + pub fn load(&self, destination: S, session_id: S, media: &Media) -> Result + where + S: Into>, + { + self.load_with_opts(destination, session_id, media, LoadOptions::default()) + } + + /// Loads provided media to the application with the additional provided options. + /// + /// # Arguments + /// * `destination` - `protocol` of the application to load media with (e.g. `web-1`); + /// * `session_id` - Current session identifier of the player application; + /// * `media` - `Media` instance that describes the media we'd like to load. + /// * `options` - Additional options for the load request. + /// + /// # Return value + /// + /// Returned `Result` should consist of either `Status` instance or an `Error`. + pub fn load_with_opts( + &self, + destination: S, + session_id: S, + media: &Media, + options: LoadOptions, + ) -> Result + where + S: Into>, + { + self.load_with_queue(destination, session_id, media, None, options) + } + + /// Loads provided media to the application. + /// + /// # Arguments + /// * `destination` - `protocol` of the application to load media with (e.g. `web-1`); + /// * `session_id` - Current session identifier of the player application; + /// * `media` - `Media` instance that describes the media we'd like to load. + /// + /// # Return value + /// + /// Returned `Result` should consist of either `Status` instance or an `Error`. + pub fn load_with_queue( + &self, + destination: S, + session_id: S, + media: &Media, + queue: Option<&MediaQueue>, + options: LoadOptions, + ) -> Result + where + S: Into>, + { + let request_id = self.message_manager.generate_request_id().get(); + + let payload = serde_json::to_string(&proxies::media::MediaRequest { + request_id, + session_id: session_id.into().to_string(), + typ: MESSAGE_TYPE_LOAD.to_string(), + + media: media.encode(), + + current_time: options.current_time, + autoplay: options.autoplay, + custom_data: proxies::media::CustomData::new(), + queue_data: queue.map(|qd| qd.encode()), + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: destination.into().to_string(), + payload: CastMessagePayload::String(payload), + })?; + + // Once media is loaded cast receiver device should emit status update event, or load failed + // event if something went wrong. + self.message_manager.receive_find_map(|message| { + if !self.can_handle(message) { + return Ok(None); + } + + match self.parse(message)? { + MediaResponse::Status(status) => { + if status.request_id == request_id { + return Ok(Some(status)); + } + + // [WORKAROUND] In some cases we don't receive response (e.g. from YouTube app), + // so let's just wait for the response with the media we're interested in and + // return it. + let has_media = { + status.entries.iter().any(|entry| { + if let Some(ref loaded_media) = entry.media { + return loaded_media.content_id == media.content_id; + } + + false + }) + }; + + if has_media { + return Ok(Some(status)); + } + } + MediaResponse::LoadFailed(error) => { + if error.request_id == request_id { + return Err(Error::Internal("Failed to load media.".to_string())); + } + } + MediaResponse::LoadCancelled(error) => { + if error.request_id == request_id { + return Err(Error::Internal( + "Load cancelled by another request.".to_string(), + )); + } + } + MediaResponse::InvalidPlayerState(error) => { + if error.request_id == request_id { + return Err(Error::Internal( + "Load failed because of invalid player state.".to_string(), + )); + } + } + MediaResponse::InvalidRequest(error) => { + if error.request_id == request_id { + return Err(Error::Internal(format!( + "Load failed because of invalid media request (reason: {}).", + error.reason.unwrap_or_else(|| "UNKNOWN".to_string()) + ))); + } + } + _ => {} + } + + Ok(None) + }) + } + + pub fn load_queue( + &self, + destination: S, + _session_id: S, + queue: &MediaQueue, + ) -> Result + where + S: Into>, + { + let request_id = self.message_manager.generate_request_id().get(); + + let payload = serde_json::to_string(&proxies::media::QueueLoadRequest { + typ: MESSAGE_TYPE_QUEUE_LOAD.to_string(), + request_id, + custom_data: None, + items: queue.items.iter().map(|qi| qi.encode()).collect(), + queue_type: Some(queue.queue_type.to_string()), + repeat_mode: "REPEAT_OFF".to_owned(), + start_index: queue.start_index, + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: destination.into().to_string(), + payload: CastMessagePayload::String(payload), + })?; + + // Once media is loaded cast receiver device should emit status update event, or load failed + // event if something went wrong. + self.message_manager.receive_find_map(|message| { + if !self.can_handle(message) { + return Ok(None); + } + + match self.parse(message)? { + MediaResponse::Status(status) => { + if status.request_id == request_id { + return Ok(Some(status)); + } + } + MediaResponse::LoadFailed(error) => { + if error.request_id == request_id { + return Err(Error::Internal("Failed to load media.".to_string())); + } + } + MediaResponse::LoadCancelled(error) => { + if error.request_id == request_id { + return Err(Error::Internal( + "Load cancelled by another request.".to_string(), + )); + } + } + MediaResponse::InvalidPlayerState(error) => { + if error.request_id == request_id { + return Err(Error::Internal( + "Load failed because of invalid player state.".to_string(), + )); + } + } + MediaResponse::InvalidRequest(error) => { + if error.request_id == request_id { + return Err(Error::Internal(format!( + "Load failed because of invalid media request (reason: {}).", + error.reason.unwrap_or_else(|| "UNKNOWN".to_string()) + ))); + } + } + _ => {} + } + + Ok(None) + }) + } + + /// Pauses playback of the current content. Triggers a STATUS event notification to all sender + /// applications. + /// + /// # Arguments + /// + /// * `destination` - `protocol` of the media application (e.g. `web-1`); + /// * `media_session_id` - ID of the media session to be paused. + /// + /// # Return value + /// + /// Returned `Result` should consist of either `Status` instance or an `Error`. + pub fn pause(&self, destination: S, media_session_id: i32) -> Result + where + S: Into>, + { + let request_id = self.message_manager.generate_request_id().get(); + + let payload = serde_json::to_string(&proxies::media::PlaybackGenericRequest { + request_id, + media_session_id, + typ: MESSAGE_TYPE_PAUSE.to_string(), + custom_data: proxies::media::CustomData::new(), + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: destination.into().to_string(), + payload: CastMessagePayload::String(payload), + })?; + + self.receive_status_entry(request_id, media_session_id) + } + + /// Begins playback of the content that was loaded with the load call, playback is continued + /// from the current time position. + /// + /// # Arguments + /// + /// * `destination` - `protocol` of the media application (e.g. `web-1`); + /// * `media_session_id` - ID of the media session to be played. + /// + /// # Return value + /// + /// Returned `Result` should consist of either `Status` instance or an `Error`. + pub fn play(&self, destination: S, media_session_id: i32) -> Result + where + S: Into>, + { + let request_id = self.message_manager.generate_request_id().get(); + + let payload = serde_json::to_string(&proxies::media::PlaybackGenericRequest { + request_id, + media_session_id, + typ: MESSAGE_TYPE_PLAY.to_string(), + custom_data: proxies::media::CustomData::new(), + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: destination.into().to_string(), + payload: CastMessagePayload::String(payload), + })?; + + self.receive_status_entry(request_id, media_session_id) + } + + /// Stops playback of the current content. Triggers a STATUS event notification to all sender + /// applications. After this command the content will no longer be loaded and the + /// media_session_id is invalidated. + /// + /// # Arguments + /// + /// * `destination` - `protocol` of the media application (e.g. `web-1`); + /// * `media_session_id` - ID of the media session to be stopped. + /// + /// # Return value + /// + /// Returned `Result` should consist of either `Status` instance or an `Error`. + pub fn stop(&self, destination: S, media_session_id: i32) -> Result + where + S: Into>, + { + let request_id = self.message_manager.generate_request_id().get(); + + let payload = serde_json::to_string(&proxies::media::PlaybackGenericRequest { + request_id, + media_session_id, + typ: MESSAGE_TYPE_STOP.to_string(), + custom_data: proxies::media::CustomData::new(), + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: destination.into().to_string(), + payload: CastMessagePayload::String(payload), + })?; + + self.receive_status_entry(request_id, media_session_id) + } + + /// Sets the current position in the stream. Triggers a STATUS event notification to all sender + /// applications. If the position provided is outside the range of valid positions for the + /// current content, then the player should pick a valid position as close to the requested + /// position as possible. + /// + /// # Arguments + /// + /// * `destination` - `protocol` of the media application (e.g. `web-1`); + /// * `media_session_id` - ID of the media session to seek in; + /// * `current_time` - Time in seconds to seek to. + /// + /// # Return value + /// + /// Returned `Result` should consist of either `Status` instance or an `Error`. + pub fn seek( + &self, + destination: S, + media_session_id: i32, + current_time: Option, + resume_state: Option, + ) -> Result + where + S: Into>, + { + let request_id = self.message_manager.generate_request_id().get(); + + let payload = serde_json::to_string(&proxies::media::PlaybackSeekRequest { + request_id, + media_session_id, + typ: MESSAGE_TYPE_SEEK.to_string(), + current_time, + resume_state: resume_state.map(|s| s.to_string()), + custom_data: proxies::media::CustomData::new(), + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: destination.into().to_string(), + payload: CastMessagePayload::String(payload), + })?; + + self.receive_status_entry(request_id, media_session_id) + } + + pub fn can_handle(&self, message: &CastMessage) -> bool { + message.namespace == CHANNEL_NAMESPACE + } + + pub fn parse(&self, message: &CastMessage) -> Result { + let reply = match message.payload { + CastMessagePayload::String(ref payload) => { + serde_json::from_str::(payload)? + } + _ => { + return Err(Error::Internal( + "Binary payload is not supported!".to_string(), + )); + } + }; + + let message_type = reply + .as_object() + .and_then(|object| object.get("type")) + .and_then(|property| property.as_str()) + .unwrap_or("") + .to_string(); + + let response = match message_type.as_ref() { + MESSAGE_TYPE_MEDIA_STATUS => { + let reply: proxies::media::StatusReply = serde_json::value::from_value(reply)?; + + let entries = reply + .status + .iter() + .map(StatusEntry::try_from) + .collect::>()?; + + MediaResponse::Status(Status { + request_id: reply.request_id, + entries, + }) + } + MESSAGE_TYPE_LOAD_CANCELLED => { + let reply: proxies::media::LoadCancelledReply = + serde_json::value::from_value(reply)?; + + MediaResponse::LoadCancelled(LoadCancelled { + request_id: reply.request_id, + }) + } + MESSAGE_TYPE_LOAD_FAILED => { + let reply: proxies::media::LoadFailedReply = serde_json::value::from_value(reply)?; + + MediaResponse::LoadFailed(LoadFailed { + request_id: reply.request_id, + }) + } + MESSAGE_TYPE_INVALID_PLAYER_STATE => { + let reply: proxies::media::InvalidPlayerStateReply = + serde_json::value::from_value(reply)?; + + MediaResponse::InvalidPlayerState(InvalidPlayerState { + request_id: reply.request_id, + }) + } + MESSAGE_TYPE_INVALID_REQUEST => { + let reply: proxies::media::InvalidRequestReply = + serde_json::value::from_value(reply)?; + + MediaResponse::InvalidRequest(InvalidRequest { + request_id: reply.request_id, + reason: reply.reason, + }) + } + MESSAGE_TYPE_ERROR => { + let reply: proxies::media::MediaErrorReply = serde_json::value::from_value(reply)?; + let detailed_error_code = + MediaDetailedErrorCode::try_from(reply.detailed_error_code)?; + + MediaResponse::Error(MediaError { + detailed_error_code, + message_type: reply.message_type, + }) + } + _ => MediaResponse::NotImplemented(message_type.to_string(), reply), + }; + + Ok(response) + } + + /// Waits for the status entry with specified `request_id` and `media_session_id`. This method + /// is very handy for the media playback methods where particular `StatusEntry` is required. + /// + /// # Arguments + /// + /// * `request_id` - ID of the request that caused status entry to be broadcasted. + /// * `media_session_id` - ID of the media session to receive. + /// + /// # Return value + /// + /// Returned `Result` should consist of either `Status` instance or an `Error`. + fn receive_status_entry( + &self, + request_id: u32, + media_session_id: i32, + ) -> Result { + self.message_manager.receive_find_map(|message| { + if !self.can_handle(message) { + return Ok(None); + } + + match self.parse(message)? { + MediaResponse::Status(mut status) => { + if status.request_id == request_id { + let position = status + .entries + .iter() + .position(|e| e.media_session_id == media_session_id); + + return Ok(position.map(|position| status.entries.remove(position))); + } + } + MediaResponse::InvalidPlayerState(error) => { + if error.request_id == request_id { + return Err(Error::Internal( + "Request failed because of invalid player state.".to_string(), + )); + } + } + MediaResponse::InvalidRequest(error) => { + if error.request_id == request_id { + return Err(Error::Internal(format!( + "Invalid request ({}).", + error.reason.unwrap_or_else(|| "Unknown".to_string()) + ))); + } + } + _ => {} + } + + Ok(None) + }) + } +} + +#[cfg(test)] +mod tests { + use crate::{ + DEFAULT_RECEIVER_ID, DEFAULT_SENDER_ID, + cast::cast_channel::cast_message::{PayloadType, ProtocolVersion}, + tests::MockTcpStream, + }; + use protobuf::EnumOrUnknown; + + use super::*; + + #[test] + fn test_get_status() { + let mut stream = MockTcpStream::new(); + let payload = format!( + r#"{{ + "requestId":1, + "type":"{}", + "status":[ + {{ + "mediaSessionId":1, + "playerState":"PLAYING", + "playbackRate":1.0, + "supportedMediaCommands":2300 + }} + ] + }}"#, + MESSAGE_TYPE_MEDIA_STATUS + ); + stream.add_message(crate::cast::cast_channel::CastMessage { + protocol_version: Some(EnumOrUnknown::new(ProtocolVersion::CASTV2_1_2)), + source_id: Some(DEFAULT_RECEIVER_ID.to_string()), + destination_id: Some(DEFAULT_SENDER_ID.to_string()), + namespace: Some(CHANNEL_NAMESPACE.to_string()), + payload_type: Some(EnumOrUnknown::new(PayloadType::STRING)), + payload_utf8: Some(payload), + payload_binary: None, + continued: None, + remaining_length: None, + special_fields: Default::default(), + }); + let channel = MediaChannel { + sender: Cow::from(DEFAULT_SENDER_ID), + message_manager: Lrc::new(MessageManager::new(stream)), + }; + + let result = channel.get_status("MyAppTransportId", None).unwrap(); + + assert_eq!(1, result.request_id); + if let Some(entry) = result.entries.first() { + assert_eq!(1, entry.media_session_id); + assert_eq!(PlayerState::Playing, entry.player_state); + assert_eq!(1.0, entry.playback_rate); + assert_eq!(2300, entry.supported_media_commands); + } + } + + #[test] + fn test_parse_media_error() { + let message = CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: DEFAULT_RECEIVER_ID.to_string(), + destination: DEFAULT_SENDER_ID.to_string(), + payload: CastMessagePayload::String( + "{\"type\":\"ERROR\",\"detailedErrorCode\":104,\"itemId\":1}".to_string(), + ), + }; + let channel = MediaChannel { + sender: Cow::from(DEFAULT_SENDER_ID), + message_manager: Lrc::new(MessageManager::new(MockTcpStream::new())), + }; + let expected_result = MediaError { + detailed_error_code: MediaDetailedErrorCode::MediaSrcNotSupported, + message_type: MESSAGE_TYPE_ERROR.to_string(), + }; + + let response = channel.parse(&message).unwrap(); + + assert_eq!(MediaResponse::Error(expected_result), response); + } + + #[test] + fn test_parse_unknown_message_type() { + let message_type = "FOO_BAR"; + let payload = format!("{{\"type\":\"{}\",\"itemId\":666}}", message_type); + let expected_payload = serde_json::from_str::(payload.as_str()).unwrap(); + let message = CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: DEFAULT_RECEIVER_ID.to_string(), + destination: DEFAULT_SENDER_ID.to_string(), + payload: CastMessagePayload::String(payload), + }; + let stream = MockTcpStream::new(); + let channel = MediaChannel { + sender: Cow::from(DEFAULT_SENDER_ID), + message_manager: Lrc::new(MessageManager::new(stream)), + }; + let expected_result = + MediaResponse::NotImplemented(message_type.to_string(), expected_payload); + + let result = channel.parse(&message).unwrap(); + + assert_eq!(expected_result, result); + } +} diff --git a/vendor/rust_cast-0.21.0/src/channels/mod.rs b/vendor/rust_cast-0.21.0/src/channels/mod.rs new file mode 100644 index 0000000..626547f --- /dev/null +++ b/vendor/rust_cast-0.21.0/src/channels/mod.rs @@ -0,0 +1,4 @@ +pub mod connection; +pub mod heartbeat; +pub mod media; +pub mod receiver; diff --git a/vendor/rust_cast-0.21.0/src/channels/receiver.rs b/vendor/rust_cast-0.21.0/src/channels/receiver.rs new file mode 100644 index 0000000..e585466 --- /dev/null +++ b/vendor/rust_cast-0.21.0/src/channels/receiver.rs @@ -0,0 +1,518 @@ +use std::{ + borrow::Cow, + convert::Into, + fmt, + io::{Read, Write}, + str::FromStr, + string::ToString, +}; + +use serde::Serialize; + +use crate::{ + Lrc, + cast::proxies, + errors::Error, + message_manager::{CastMessage, CastMessagePayload, MessageManager}, +}; + +pub(crate) const CHANNEL_NAMESPACE: &str = "urn:x-cast:com.google.cast.receiver"; + +const MESSAGE_TYPE_LAUNCH: &str = "LAUNCH"; +const MESSAGE_TYPE_STOP: &str = "STOP"; +const MESSAGE_TYPE_GET_STATUS: &str = "GET_STATUS"; +const MESSAGE_TYPE_SET_VOLUME: &str = "SET_VOLUME"; + +const MESSAGE_TYPE_RECEIVER_STATUS: &str = "RECEIVER_STATUS"; +const MESSAGE_TYPE_LAUNCH_ERROR: &str = "LAUNCH_ERROR"; +const MESSAGE_TYPE_INVALID_REQUEST: &str = "INVALID_REQUEST"; + +const APP_DEFAULT_MEDIA_RECEIVER_ID: &str = "CC1AD845"; +const APP_BACKDROP_ID: &str = "E8C28D3C"; +const APP_YOUTUBE_ID: &str = "233637DE"; + +/// Structure that describes possible cast device volume options. +#[derive(Copy, Clone, Debug)] +pub struct Volume { + /// Volume level. + pub level: Option, + /// Mute/unmute state. + pub muted: Option, +} + +/// This `From` implementation is useful when only volume level is needed. +impl From for Volume { + fn from(level: f32) -> Self { + Self { + level: Some(level), + muted: None, + } + } +} + +/// This `From` implementation is useful when only mute/unmute state is needed. +impl From for Volume { + fn from(muted: bool) -> Self { + Self { + level: None, + muted: Some(muted), + } + } +} + +/// This `From<(f32, bool)>` implementation is useful when both volume level and mute/unmute state are +/// needed. +impl From<(f32, bool)> for Volume { + fn from((level, muted): (f32, bool)) -> Self { + Self { + level: Some(level), + muted: Some(muted), + } + } +} + +/// Structure that describes currently run Cast Device application. +#[derive(Clone, Debug)] +pub struct Application { + /// The identifier of the Cast application. Not for display. + pub app_id: String, + /// Session id of the currently active application. + pub session_id: String, + /// Name of the `pipe` to talk to the application. + pub transport_id: String, + /// A list of the namespaces supported by the receiver application. + pub namespaces: Vec, + /// The human-readable name of the Cast application, for example, "YouTube". + pub display_name: String, + /// Descriptive text for the current application content, for example “My vacations”. + pub status_text: String, +} + +/// Describes the current status of the receiver cast device. +#[derive(Clone, Debug)] +pub struct Status { + /// Unique id of the request that requested the status. + pub request_id: u32, + /// Contains the list of applications that are currently run. + pub applications: Vec, + /// Determines whether the Cast device is the active input or not. + pub is_active_input: bool, + /// Determines whether the Cast device is in stand by mode. + pub is_stand_by: bool, + /// Volume parameters of the currently active cast device. + pub volume: Volume, +} + +/// Describes the application launch error. +#[derive(Clone, Debug)] +pub struct LaunchError { + /// Unique id of the request that tried to launch application. + pub request_id: u32, + /// Description of the launch error reason if available. + pub reason: Option, +} + +/// Describes the invalid request error. +#[derive(Clone, Debug)] +pub struct InvalidRequest { + /// Unique id of the invalid request. + pub request_id: u32, + /// Description of the invalid request reason if available. + pub reason: Option, +} + +/// Represents all currently supported incoming messages that receiver channel can handle. +#[derive(Clone, Debug)] +pub enum ReceiverResponse { + /// Status of the currently active receiver. + Status(Status), + /// Error indicating that receiver failed to launch application. + LaunchError(LaunchError), + /// Error indicating that request is not valid. + InvalidRequest(InvalidRequest), + /// Used every time when channel can't parse the message. Associated data contains `type` string + /// field and raw JSON data returned from cast device. + NotImplemented(String, serde_json::Value), +} + +#[derive(Clone, Debug, PartialEq)] +pub enum CastDeviceApp { + DefaultMediaReceiver, + Backdrop, + YouTube, + Custom(String), +} + +impl FromStr for CastDeviceApp { + type Err = (); + + fn from_str(s: &str) -> Result { + let app = match s { + APP_DEFAULT_MEDIA_RECEIVER_ID | "default" => CastDeviceApp::DefaultMediaReceiver, + APP_BACKDROP_ID | "backdrop" => CastDeviceApp::Backdrop, + APP_YOUTUBE_ID | "youtube" => CastDeviceApp::YouTube, + custom => CastDeviceApp::Custom(custom.to_string()), + }; + + Ok(app) + } +} + +impl fmt::Display for CastDeviceApp { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let str = match *self { + CastDeviceApp::DefaultMediaReceiver => APP_DEFAULT_MEDIA_RECEIVER_ID.to_string(), + CastDeviceApp::Backdrop => APP_BACKDROP_ID.to_string(), + CastDeviceApp::YouTube => APP_YOUTUBE_ID.to_string(), + CastDeviceApp::Custom(ref app_id) => app_id.to_string(), + }; + write!(f, "{}", str) + } +} + +pub struct ReceiverChannel<'a, W> +where + W: Write + Read, +{ + sender: Cow<'a, str>, + receiver: Cow<'a, str>, + message_manager: Lrc>, +} + +impl<'a, W> ReceiverChannel<'a, W> +where + W: Write + Read, +{ + pub fn new( + sender: S, + receiver: S, + message_manager: Lrc>, + ) -> ReceiverChannel<'a, W> + where + S: Into>, + { + ReceiverChannel { + sender: sender.into(), + receiver: receiver.into(), + message_manager, + } + } + + /// Launches the specified receiver's application. + /// + /// # Examples + /// + /// ```no_run + /// use std::str::FromStr; + /// use rust_cast::{CastDevice, channels::receiver::CastDeviceApp}; + /// + /// # let cast_device = CastDevice::connect_without_host_verification("host", 1234).unwrap(); + /// cast_device.receiver.launch_app(&CastDeviceApp::from_str("youtube").unwrap()); + /// ``` + /// + /// # Arguments + /// + /// * `app` - `CastDeviceApp` instance reference to run. + pub fn launch_app(&self, app: &CastDeviceApp) -> Result { + let request_id = self.message_manager.generate_request_id().get(); + + let payload = serde_json::to_string(&proxies::receiver::AppLaunchRequest { + typ: MESSAGE_TYPE_LAUNCH.to_string(), + request_id, + app_id: app.to_string(), + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: self.receiver.to_string(), + payload: CastMessagePayload::String(payload), + })?; + + // Once application is run cast receiver device should emit status update event, or launch + // error event if something went wrong. + self.message_manager.receive_find_map(|message| { + if !self.can_handle(message) { + return Ok(None); + } + + match self.parse(message)? { + ReceiverResponse::Status(mut status) => { + if status.request_id == request_id { + return Ok(Some(status.applications.remove(0))); + } + } + ReceiverResponse::LaunchError(error) => { + if error.request_id == request_id { + return Err(Error::Internal(format!( + "Could not run application ({}).", + error.reason.unwrap_or_else(|| "Unknown".to_string()) + ))); + } + } + _ => {} + } + + Ok(None) + }) + } + + /// Broadcasts a message over a cast device's message bus. + /// + /// Receiver can observe messages using `context.addCustomMessageListener` with custom namespace. + /// + /// ```javascript, no_run + /// context.addCustomMessageListener('urn:x-cast:com.example.castdata', function(customEvent) { + /// // do something with message + /// }); + /// ``` + /// + /// Namespace should start with `urn:x-cast:` + /// + /// # Arguments + /// + /// * `namespace` - Message namespace that should start with `urn:x-cast:`. + /// * `message` - Message instance to send. + pub fn broadcast_message( + &self, + namespace: &str, + message: &M, + ) -> Result<(), Error> { + if !namespace.starts_with("urn:x-cast:") { + return Err(Error::Namespace(format!( + "'{}' should start with 'urn:x-cast:' prefix", + namespace + ))); + } + let payload = serde_json::to_string(message)?; + self.message_manager.send(CastMessage { + namespace: namespace.to_string(), + source: self.sender.to_string(), + destination: "*".into(), + payload: CastMessagePayload::String(payload), + })?; + + Ok(()) + } + + /// Stops currently active app using corresponding `session_id`. + /// + /// # Arguments + /// * `session_id` - identifier of the active application session from `Application` instance. + pub fn stop_app(&self, session_id: S) -> Result<(), Error> + where + S: Into>, + { + let request_id = self.message_manager.generate_request_id().get(); + + let payload = serde_json::to_string(&proxies::receiver::AppStopRequest { + typ: MESSAGE_TYPE_STOP.to_string(), + request_id, + session_id: session_id.into(), + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: self.receiver.to_string(), + payload: CastMessagePayload::String(payload), + })?; + + // Once application is stopped cast receiver device should emit status update event, or + // invalid request event if provided session id is not valid. + self.message_manager.receive_find_map(|message| { + if !self.can_handle(message) { + return Ok(None); + } + + match self.parse(message)? { + ReceiverResponse::Status(status) => { + if status.request_id == request_id { + return Ok(Some(())); + } + } + ReceiverResponse::InvalidRequest(error) => { + if error.request_id == request_id { + return Err(Error::Internal(format!( + "Invalid request ({}).", + error.reason.unwrap_or_else(|| "Unknown".to_string()) + ))); + } + } + _ => {} + } + + Ok(None) + }) + } + + /// Retrieves status of the cast device receiver. + /// + /// # Return value + /// + /// Returned `Result` should consist of either `Status` instance or an `Error`. + pub fn get_status(&self) -> Result { + let request_id = self.message_manager.generate_request_id().get(); + + let payload = serde_json::to_string(&proxies::receiver::GetStatusRequest { + typ: MESSAGE_TYPE_GET_STATUS.to_string(), + request_id, + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: self.receiver.to_string(), + payload: CastMessagePayload::String(payload), + })?; + + self.message_manager.receive_find_map(|message| { + if !self.can_handle(message) { + return Ok(None); + } + + let message = self.parse(message)?; + if let ReceiverResponse::Status(status) = message + && status.request_id == request_id + { + return Ok(Some(status)); + } + + Ok(None) + }) + } + + /// Sets volume for the active cast device. + /// + /// # Arguments + /// + /// * `volume` - anything that can be converted to a valid `Volume` structure. It's possible to + /// set volume level, mute/unmute state or both altogether. + /// + /// # Return value + /// + /// Actual `Volume` instance returned by receiver. + /// + /// # Errors + /// + /// Usually method can fail only if network connection with cast device is lost for some reason. + pub fn set_volume(&self, volume: T) -> Result + where + T: Into, + { + let request_id = self.message_manager.generate_request_id().get(); + let volume = volume.into(); + + let payload = serde_json::to_string(&proxies::receiver::SetVolumeRequest { + typ: MESSAGE_TYPE_SET_VOLUME.to_string(), + request_id, + volume: proxies::receiver::Volume { + level: volume.level, + muted: volume.muted, + }, + })?; + + self.message_manager.send(CastMessage { + namespace: CHANNEL_NAMESPACE.to_string(), + source: self.sender.to_string(), + destination: self.receiver.to_string(), + payload: CastMessagePayload::String(payload), + })?; + + self.message_manager.receive_find_map(|message| { + if !self.can_handle(message) { + return Ok(None); + } + + let message = self.parse(message)?; + if let ReceiverResponse::Status(status) = message + && status.request_id == request_id + { + return Ok(Some(status.volume)); + } + + Ok(None) + }) + } + + pub fn can_handle(&self, message: &CastMessage) -> bool { + message.namespace == CHANNEL_NAMESPACE + } + + pub fn parse(&self, message: &CastMessage) -> Result { + let reply = match message.payload { + CastMessagePayload::String(ref payload) => { + serde_json::from_str::(payload)? + } + _ => { + return Err(Error::Internal( + "Binary payload is not supported!".to_string(), + )); + } + }; + + let message_type = reply + .as_object() + .and_then(|object| object.get("type")) + .and_then(|property| property.as_str()) + .unwrap_or("") + .to_string(); + + let response = match message_type.as_ref() { + MESSAGE_TYPE_RECEIVER_STATUS => { + let status_reply: proxies::receiver::StatusReply = + serde_json::value::from_value(reply)?; + + let status = Status { + request_id: status_reply.request_id, + applications: status_reply + .status + .applications + .iter() + .map(|app| Application { + app_id: app.app_id.clone(), + session_id: app.session_id.clone(), + transport_id: app.transport_id.clone(), + namespaces: app + .namespaces + .iter() + .map(|ns| ns.name.clone()) + .collect::>(), + display_name: app.display_name.clone(), + status_text: app.status_text.clone(), + }) + .collect::>(), + is_active_input: status_reply.status.is_active_input, + is_stand_by: status_reply.status.is_stand_by, + volume: Volume { + level: status_reply.status.volume.level, + muted: status_reply.status.volume.muted, + }, + }; + + ReceiverResponse::Status(status) + } + MESSAGE_TYPE_LAUNCH_ERROR => { + let reply: proxies::receiver::LaunchErrorReply = + serde_json::value::from_value(reply)?; + + ReceiverResponse::LaunchError(LaunchError { + request_id: reply.request_id, + reason: reply.reason, + }) + } + MESSAGE_TYPE_INVALID_REQUEST => { + let reply: proxies::receiver::InvalidRequestReply = + serde_json::value::from_value(reply)?; + + ReceiverResponse::InvalidRequest(InvalidRequest { + request_id: reply.request_id, + reason: reply.reason, + }) + } + _ => ReceiverResponse::NotImplemented(message_type.to_string(), reply), + }; + + Ok(response) + } +} diff --git a/vendor/rust_cast-0.21.0/src/errors.rs b/vendor/rust_cast-0.21.0/src/errors.rs new file mode 100644 index 0000000..a85d722 --- /dev/null +++ b/vendor/rust_cast-0.21.0/src/errors.rs @@ -0,0 +1,69 @@ +use std::io::Error as IoError; + +use protobuf::Error as ProtobufError; +use rustls::pki_types::InvalidDnsNameError; +use serde_json::error::Error as SerializationError; +use thiserror::Error; + +/// Consolidates possible error types that can occur in the lib. +#[derive(Debug, Error)] +pub enum Error { + /// This variant is used when error occurs in the lib logic. + #[error("an internal error occurred, {0}")] + Internal(String), + /// This variant includes everything related to the network connection. + #[error("{0}")] + Io(IoError), + /// This variant includes all possible errors that come from Protobuf layer. + #[error("{0}")] + Protobuf(ProtobufError), + /// Errors with JSON (de)serialization of incoming and outgoing + /// messages. + #[error("{0}")] + Serialization(SerializationError), + /// Errors parsing messages (valid JSON but bad semantics) + #[error("{0}")] + Parsing(String), + /// This variant is used to indicate invalid DNS name used to connect to Cast device. + #[error("{0}")] + Dns(InvalidDnsNameError), + /// This variant includes any error that comes from rustls. + #[error("{0}")] + Tls(rustls::Error), + /// Problems with given namespace + #[error("{0}")] + Namespace(String), + /// This variant is used when message retrieval takes too long. + #[error("{0}")] + Timeout(String), +} + +impl From for Error { + fn from(err: IoError) -> Error { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: ProtobufError) -> Error { + Error::Protobuf(err) + } +} + +impl From for Error { + fn from(err: SerializationError) -> Error { + Error::Serialization(err) + } +} + +impl From for Error { + fn from(err: rustls::Error) -> Error { + Error::Tls(err) + } +} + +impl From for Error { + fn from(err: InvalidDnsNameError) -> Error { + Error::Dns(err) + } +} diff --git a/vendor/rust_cast-0.21.0/src/lib.rs b/vendor/rust_cast-0.21.0/src/lib.rs new file mode 100644 index 0000000..adc54e9 --- /dev/null +++ b/vendor/rust_cast-0.21.0/src/lib.rs @@ -0,0 +1,571 @@ +#![deny(warnings)] + +use std::{borrow::Cow, net::TcpStream, sync::Arc}; + +use rustls::{ + ClientConfig, ClientConnection, DigitallySignedStruct, RootCertStore, StreamOwned, + client::danger::{HandshakeSignatureValid, ServerCertVerified, ServerCertVerifier}, + crypto::{aws_lc_rs::default_provider, verify_tls12_signature, verify_tls13_signature}, + pki_types::{CertificateDer, ServerName, UnixTime}, +}; + +use channels::{ + connection::{ConnectionChannel, ConnectionResponse}, + heartbeat::{HeartbeatChannel, HeartbeatResponse}, + media::{MediaChannel, MediaResponse}, + receiver::{ReceiverChannel, ReceiverResponse}, +}; +use errors::Error; +use message_manager::{CastMessage, CastMessagePayload, MessageManager}; + +#[cfg(not(feature = "cast"))] +mod cast; +#[cfg(feature = "cast")] +pub mod cast; +pub mod channels; +pub mod errors; +pub mod message_manager; +mod utils; + +const DEFAULT_SENDER_ID: &str = "sender-0"; +const DEFAULT_RECEIVER_ID: &str = "receiver-0"; + +#[cfg(feature = "thread_safe")] +type Lrc = std::sync::Arc; +#[cfg(not(feature = "thread_safe"))] +type Lrc = std::rc::Rc; + +/// Supported channel message types. +#[derive(Clone, Debug)] +pub enum ChannelMessage { + /// Message to be processed by `ConnectionChannel`. + Connection(ConnectionResponse), + /// Message to be processed by `HeartbeatChannel`. + Heartbeat(HeartbeatResponse), + /// Message to be processed by `MediaChannel`. + Media(MediaResponse), + /// Message to be processed by `ReceiverChannel`. + Receiver(ReceiverResponse), + /// Raw message is returned when built-in channels can't process it (e.g. because of unknown + /// `namespace`). + Raw(CastMessage), +} + +/// Structure that manages connection to a cast device. +pub struct CastDevice<'a> { + message_manager: Lrc>>, + + /// Channel that manages connection responses/requests. + pub connection: ConnectionChannel<'a, StreamOwned>, + + /// Channel that allows connection to stay alive (via ping-pong requests/responses). + pub heartbeat: HeartbeatChannel<'a, StreamOwned>, + + /// Channel that manages various media stuff. + pub media: MediaChannel<'a, StreamOwned>, + + /// Channel that manages receiving platform (e.g. Chromecast). + pub receiver: ReceiverChannel<'a, StreamOwned>, +} + +impl<'a> CastDevice<'a> { + /// Connects to the cast device using host name and port. + /// + /// # Examples + /// + /// ```no_run + /// use rust_cast::CastDevice; + /// + /// let device = CastDevice::connect("192.168.1.2", 8009)?; + /// # Ok::<(), rust_cast::errors::Error>(()) + /// ``` + /// + /// # Arguments + /// + /// * `host` - Cast device host name. + /// * `port` - Cast device port number. + /// + /// # Errors + /// + /// This method may fail if connection to Cast device can't be established for some reason + /// (e.g. wrong host name or port). + /// + /// # Return value + /// + /// Instance of `CastDevice` that allows you to manage connection. + pub fn connect(host: S, port: u16) -> Result, Error> + where + S: Into>, + { + let host = host.into(); + log::debug!("Establishing connection with cast device at {host}:{port}…"); + + let mut root_store = RootCertStore::empty(); + let (valid, invalid) = root_store.add_parsable_certificates( + rustls_native_certs::load_native_certs().expect("Could not load platform certs."), + ); + if invalid > 0 { + log::warn!( + "Failed to parse {invalid} out of {} root certificates.", + valid + invalid + ); + } else { + log::debug!("Successfully parsed {valid} root certificates."); + } + + let mut config = ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + config.key_log = Arc::new(rustls::KeyLogFile::new()); + + let conn = ClientConnection::new( + config.into(), + ServerName::try_from(host.as_ref())?.to_owned(), + )?; + let stream = StreamOwned::new(conn, TcpStream::connect((host.as_ref(), port))?); + + log::debug!("Connection with {host}:{port} successfully established."); + + CastDevice::connect_to_device(stream) + } + + /// Connects to the cast device using host name and port _without_ host verification. Use on + /// your own risk! + /// + /// # Examples + /// + /// ```no_run + /// use rust_cast::CastDevice; + /// + /// let device = CastDevice::connect_without_host_verification("192.168.1.2", 8009)?; + /// # Ok::<(), rust_cast::errors::Error>(()) + /// ``` + /// + /// # Arguments + /// + /// * `host` - Cast device host name. + /// * `port` - Cast device port number. + /// + /// # Errors + /// + /// This method may fail if connection to Cast device can't be established for some reason + /// (e.g. wrong host name or port). + /// + /// # Return value + /// + /// Instance of `CastDevice` that allows you to manage connection. + pub fn connect_without_host_verification(host: S, port: u16) -> Result, Error> + where + S: Into>, + { + let host = host.into(); + + log::debug!("Establishing non-verified connection with cast device at {host}:{port}…"); + + let mut config = ClientConfig::builder() + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoCertificateVerification {})) + .with_no_client_auth(); + config.key_log = Arc::new(rustls::KeyLogFile::new()); + let stream = StreamOwned::new( + ClientConnection::new( + Arc::new(config), + ServerName::try_from(host.as_ref())?.to_owned(), + )?, + TcpStream::connect((host.as_ref(), port))?, + ); + + log::debug!("Connection with {host}:{port} successfully established."); + + CastDevice::connect_to_device(stream) + } + + /// Waits for any message returned by cast device (e.g. Chromecast) and returns its parsed + /// version. + /// + /// # Examples + /// + /// ```no_run + /// use rust_cast::ChannelMessage; + /// + /// # use rust_cast::CastDevice; + /// # let cast_device = CastDevice::connect_without_host_verification("192.168.1.2", 8009)?; + /// + /// match cast_device.receive() { + /// Ok(ChannelMessage::Connection(res)) => log::debug!("Connection message: {:?}", res), + /// Ok(ChannelMessage::Heartbeat(_)) => cast_device.heartbeat.pong()?, + /// Ok(_) => {}, + /// Err(err) => log::error!("Error occurred while receiving message {}", err) + /// } + /// # Ok::<(), rust_cast::errors::Error>(()) + /// ``` + /// + /// # Errors + /// + /// Usually fails if message returned by device can't be parsed. + /// + /// # Returned values + /// + /// Parsed channel message. + pub fn receive(&self) -> Result { + let cast_message = self.message_manager.receive()?; + + if self.connection.can_handle(&cast_message) { + return Ok(ChannelMessage::Connection( + self.connection.parse(&cast_message)?, + )); + } + + if self.heartbeat.can_handle(&cast_message) { + return Ok(ChannelMessage::Heartbeat( + self.heartbeat.parse(&cast_message)?, + )); + } + + if self.media.can_handle(&cast_message) { + return Ok(ChannelMessage::Media(self.media.parse(&cast_message)?)); + } + + if self.receiver.can_handle(&cast_message) { + return Ok(ChannelMessage::Receiver( + self.receiver.parse(&cast_message)?, + )); + } + + Ok(ChannelMessage::Raw(cast_message)) + } + + /// Connects to the cast device using provided ssl stream. + /// + /// # Arguments + /// + /// * `ssl_stream` - SSL Stream for the TCP connection established with the device. + /// + /// # Return value + /// + /// Instance of `CastDevice` that allows you to manage connection. + fn connect_to_device( + ssl_stream: StreamOwned, + ) -> Result, Error> { + let message_manager_rc = Lrc::new(MessageManager::new(ssl_stream)); + + let heartbeat = HeartbeatChannel::new( + DEFAULT_SENDER_ID, + DEFAULT_RECEIVER_ID, + Lrc::clone(&message_manager_rc), + ); + let connection = ConnectionChannel::new(DEFAULT_SENDER_ID, Lrc::clone(&message_manager_rc)); + let receiver = ReceiverChannel::new( + DEFAULT_SENDER_ID, + DEFAULT_RECEIVER_ID, + Lrc::clone(&message_manager_rc), + ); + let media = MediaChannel::new(DEFAULT_SENDER_ID, Lrc::clone(&message_manager_rc)); + + Ok(CastDevice { + message_manager: message_manager_rc, + heartbeat, + connection, + receiver, + media, + }) + } + + /// Sends `message` (a raw, already-serialized string payload -- e.g. + /// JSON text) on an arbitrary `namespace` to a specific `destination` + /// (typically a launched app's `transport_id`, the same id `connection`/`media` + /// target). + /// + /// LOCAL PATCH (breadcast, not upstream): none of the built-in channels expose a + /// generic point-to-point send -- `ReceiverChannel::broadcast_message()` is the + /// closest, but it hardcodes destination `"*"`, which isn't the same conversation + /// as a namespace-specific exchange with one particular launched app (e.g. Cast + /// Streaming's OFFER/ANSWER negotiation on `urn:x-cast:com.google.cast.webrtc`, + /// which breadcast's `breadcast-caststream-sys` crate needs -- there, the payload + /// is already-serialized JSON produced by the vendored openscreen C++, so this + /// takes a raw string rather than a `Serialize` value like + /// `broadcast_message()` does, to avoid double-encoding it). Receiving such + /// messages needs no patch: `CastDevice::receive()` already returns them as + /// `ChannelMessage::Raw(CastMessage)` whenever no built-in channel claims the + /// namespace. + pub fn send_message(&self, namespace: &str, destination: &str, message: &str) -> Result<(), Error> { + self.message_manager.send(CastMessage { + namespace: namespace.to_string(), + source: DEFAULT_SENDER_ID.to_string(), + destination: destination.to_string(), + payload: CastMessagePayload::String(message.to_string()), + }) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use byteorder::{BigEndian, WriteBytesExt}; + use log::warn; + use protobuf::Message; + use std::{ + fmt::Display, + io::{Read, Write}, + sync::{Arc, RwLock}, + }; + + use crate::{cast::cast_channel, utils::read_u32_from_buffer}; + + #[test] + #[cfg(feature = "thread_safe")] + fn test_thread_safe() { + use crate::CastDevice; + + fn is_sync() {} + fn is_send() {} + + is_sync::(); + is_send::(); + } + + /// A mock implementation of a TCP stream for testing purposes. + /// + /// # Example + /// + /// ```rust + /// use rust_cast::channels::media::MediaChannel; + /// use rust_cast::message_manager::MessageManager; + /// use rust_cast::Lrc; + /// + /// let stream = MockTcpStream::new(); + /// let message_manager = Lrc::new(MessageManager::new(stream)); + /// let channel = MediaChannel::new( + /// "sender-0", + /// message_manager + /// ); + /// ``` + #[derive(Debug, Default, Clone)] + pub struct MockTcpStream { + /// Inner stream of the TCP stream which allows cloning and referencing the same stream source. + inner: Arc>, + } + + impl MockTcpStream { + /// Creates a new empty `MockTcpStream` instance. + pub fn new() -> Self { + MockTcpStream { + inner: Arc::new(RwLock::new(InnerStream::default())), + } + } + + /// Add a response message to be returned by read operations on the stream. + pub fn add_message(&mut self, message: M) { + let message = message.write_to_bytes().unwrap(); + let mut mutex = self.inner.write().unwrap(); + mutex.response_messages.push(message); + } + + /// Returns the received message at the given index if present, else [None]. + pub fn received_message(&self, index: usize) -> Option { + self.inner + .read() + .expect("expected to acquire read lock") + .received_messages + .get(index) + .cloned() + } + + fn inner_read(&self, buf: &mut [u8]) -> std::io::Result { + self.inner.write().unwrap().read(buf) + } + + fn inner_write(&self, buf: &[u8]) -> std::io::Result { + self.inner.write().unwrap().write(buf) + } + + fn inner_flush(&self) -> std::io::Result<()> { + self.inner.write().unwrap().flush() + } + } + + impl Read for MockTcpStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + self.inner_read(buf) + } + } + + impl Write for MockTcpStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.inner_write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.inner_flush() + } + } + + /// Represents a TCP message containing a received payload from the sender. + #[derive(Debug, Clone)] + #[allow(dead_code)] + pub struct TcpMessage { + /// The known length of the message. + pub message_length: u32, + /// The payload of the message. + pub payload: Vec, + } + + impl TcpMessage { + /// Parses and returns the CastMessage contained in the payload. + pub fn message(&self) -> cast_channel::CastMessage { + ::parse_from_bytes(self.payload.as_slice()) + .unwrap() + } + } + + impl Display for TcpMessage { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", String::from_utf8_lossy(self.payload.as_slice())) + } + } + + #[derive(Debug, Clone, PartialEq)] + enum CursorLocation { + Length, + Payload, + } + + #[derive(Debug, Clone)] + struct ReadCursor { + pub location: CursorLocation, + pub index: usize, + } + + impl ReadCursor { + pub fn next(&self) -> Self { + match self.location { + CursorLocation::Length => Self { + location: CursorLocation::Payload, + index: self.index, + }, + CursorLocation::Payload => Self { + location: CursorLocation::Length, + index: self.index + 1, + }, + } + } + } + + impl Default for ReadCursor { + fn default() -> Self { + Self { + location: CursorLocation::Length, + index: 0, + } + } + } + + /// Inner representation of a stream used by `MockTcpStream` for testing purposes. + #[derive(Debug, Default)] + struct InnerStream { + /// The current position of the read cursor. + cursor: ReadCursor, + /// Buffer containing the messages which should be returned by the read operation. + response_messages: Vec>, + /// Buffer for storing the payload of the current message being written. + payload_buffer: Option, + /// Vector containing the received messages from the sender. + received_messages: Vec, + } + + impl Read for InnerStream { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + if let Some(message) = self.response_messages.get(self.cursor.index) { + let result: std::io::Result = match &self.cursor.location { + CursorLocation::Length => { + let mut len = Vec::::new(); + len.write_u32::(message.len() as u32).unwrap(); + buf[..4].copy_from_slice(len.as_slice()); + Ok(4) + } + CursorLocation::Payload => { + let len = message.len(); + buf[..len].copy_from_slice(message.as_slice()); + Ok(len) + } + }; + + self.cursor = self.cursor.next(); + result + } else { + warn!("No more messages to read"); + Ok(0) + } + } + } + + impl Write for InnerStream { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + if let Some(mut payload_buffer) = self.payload_buffer.take() { + payload_buffer.payload = buf.to_vec(); + self.received_messages.push(payload_buffer); + } else { + let length = read_u32_from_buffer(buf).unwrap(); + self.payload_buffer = Some(TcpMessage { + message_length: length, + payload: vec![], + }); + } + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + // flush is never called when sending messages + // so we don't execute any logic here + Ok(()) + } + } +} + +#[derive(Debug)] +pub struct NoCertificateVerification; +impl ServerCertVerifier for NoCertificateVerification { + fn verify_server_cert( + &self, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, + _ocsp: &[u8], + _now: UnixTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls12_signature( + message, + cert, + dss, + &default_provider().signature_verification_algorithms, + ) + } + + fn verify_tls13_signature( + &self, + message: &[u8], + cert: &CertificateDer<'_>, + dss: &DigitallySignedStruct, + ) -> Result { + verify_tls13_signature( + message, + cert, + dss, + &default_provider().signature_verification_algorithms, + ) + } + + fn supported_verify_schemes(&self) -> Vec { + default_provider() + .signature_verification_algorithms + .supported_schemes() + } +} diff --git a/vendor/rust_cast-0.21.0/src/message_manager.rs b/vendor/rust_cast-0.21.0/src/message_manager.rs new file mode 100644 index 0000000..20e394a --- /dev/null +++ b/vendor/rust_cast-0.21.0/src/message_manager.rs @@ -0,0 +1,347 @@ +use std::{ + io::{Read, Write}, + num::NonZeroU32, + ops::{Deref, DerefMut}, +}; + +use crate::{ + cast::{ + cast_channel, + cast_channel::cast_message::{PayloadType, ProtocolVersion}, + }, + errors::Error, + utils, +}; + +struct Lock( + #[cfg(feature = "thread_safe")] std::sync::Mutex, + #[cfg(not(feature = "thread_safe"))] std::cell::RefCell, +); + +struct LockGuardMut<'a, T>( + #[cfg(feature = "thread_safe")] std::sync::MutexGuard<'a, T>, + #[cfg(not(feature = "thread_safe"))] std::cell::RefMut<'a, T>, +); + +impl<'a, T> Deref for LockGuardMut<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} + +impl<'a, T> DerefMut for LockGuardMut<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.deref_mut() + } +} + +impl Lock { + fn new(data: T) -> Self { + Lock({ + #[cfg(feature = "thread_safe")] + let lock = std::sync::Mutex::new(data); + #[cfg(not(feature = "thread_safe"))] + let lock = std::cell::RefCell::new(data); + lock + }) + } + + fn borrow_mut(&self) -> LockGuardMut<'_, T> { + LockGuardMut({ + #[cfg(feature = "thread_safe")] + let guard = self.0.lock().unwrap(); + #[cfg(not(feature = "thread_safe"))] + let guard = self.0.borrow_mut(); + guard + }) + } +} + +/// Type of the payload that `CastMessage` can have. +#[derive(Debug, Clone, PartialEq)] +pub enum CastMessagePayload { + /// Payload represented by UTF-8 string (usually it's just a JSON string). + String(String), + /// Payload represented by binary data. + Binary(Vec), +} + +/// Base structure that represents messages that are exchanged between Receiver and Sender. +#[derive(Debug, Clone, PartialEq)] +pub struct CastMessage { + /// A namespace is a labeled protocol. That is, messages that are exchanged throughout the + /// Cast ecosystem utilize namespaces to identify the protocol of the message being sent. + pub namespace: String, + /// Unique identifier of the `sender` application. + pub source: String, + /// Unique identifier of the `receiver` application. + pub destination: String, + /// Payload data attached to the message (either string or binary). + pub payload: CastMessagePayload, +} + +/// Static structure that is responsible for (de)serializing and sending/receiving Cast protocol +/// messages. +pub struct MessageManager +where + S: Write + Read, +{ + message_buffer: Lock>, + stream: Lock, + request_counter: Lock, +} + +impl MessageManager +where + S: Write + Read, +{ + pub fn new(stream: S) -> Self { + MessageManager { + stream: Lock::new(stream), + message_buffer: Lock::new(vec![]), + request_counter: Lock::new(NonZeroU32::MIN), + } + } + + /// Sends `message` to the Cast Device. + /// + /// # Arguments + /// + /// * `message` - `CastMessage` instance to be sent to the Cast Device. + pub fn send(&self, message: CastMessage) -> Result<(), Error> { + let mut raw_message = cast_channel::CastMessage::new(); + + raw_message.set_protocol_version(ProtocolVersion::CASTV2_1_0); + + raw_message.set_namespace(message.namespace); + raw_message.set_source_id(message.source); + raw_message.set_destination_id(message.destination); + + match message.payload { + CastMessagePayload::String(payload) => { + raw_message.set_payload_type(PayloadType::STRING); + raw_message.set_payload_utf8(payload); + } + + CastMessagePayload::Binary(payload) => { + raw_message.set_payload_type(PayloadType::BINARY); + raw_message.set_payload_binary(payload); + } + }; + + let message_content_buffer = utils::to_vec(&raw_message)?; + let message_length_buffer = + utils::write_u32_to_buffer(message_content_buffer.len() as u32)?; + + let writer = &mut *self.stream.borrow_mut(); + + writer.write_all(&message_length_buffer)?; + writer.write_all(&message_content_buffer)?; + + log::debug!("Message sent: {:?}", raw_message); + + Ok(()) + } + + /// Waits for the next `CastMessage` available. Can also return existing message from the + /// internal message buffer containing messages that have been received previously, but haven't + /// been consumed for some reason (e.g. during `receive_find_map` call). + /// + /// # Return value + /// + /// `Result` containing parsed `CastMessage` or `Error`. + pub fn receive(&self) -> Result { + let mut message_buffer = self.message_buffer.borrow_mut(); + + // If we have messages in the buffer, let's return them from it. + if message_buffer.is_empty() { + self.read() + } else { + Ok(message_buffer.remove(0)) + } + } + + /// Waits for the next `CastMessage` for which `f` returns valid mapped value. Messages in which + /// `f` is not interested are placed into internal message buffer and can be later retrieved + /// with `receive`. This method always reads from the stream. + /// + /// # Example + /// + /// ```no_run + /// # use std::net::TcpStream; + /// # use rust_cast::message_manager::{CastMessage, MessageManager}; + /// # use rustls::{ClientConfig, ClientConnection, RootCertStore, StreamOwned}; + /// # use rustls::pki_types::ServerName; + /// # let config = ClientConfig::builder() + /// # .with_root_certificates(RootCertStore::empty()) + /// # .with_no_client_auth(); + /// # let server_name = ServerName::try_from("0")?.to_owned(); + /// # let conn = ClientConnection::new(config.into(), server_name)?; + /// # let tcp_stream = TcpStream::connect(("0", 8009)).unwrap(); + /// # let ssl_stream = StreamOwned::new(conn, tcp_stream); + /// # let message_manager = MessageManager::new(ssl_stream); + /// # fn can_handle(message: &CastMessage) -> bool { unimplemented!() } + /// # fn parse(message: &CastMessage) { unimplemented!() } + /// message_manager.receive_find_map(|message| { + /// if !can_handle(message) { + /// return Ok(None); + /// } + /// + /// parse(message); + /// + /// Ok(Some(())) + /// })?; + /// # Ok::<(), rust_cast::errors::Error>(()) + /// ``` + /// + /// # Arguments + /// + /// * `f` - Function that analyzes and maps `CastMessage` to any other type. If message doesn't + /// look like something `f` is looking for, then `Ok(None)` should be returned so that message + /// is not lost and placed into internal message buffer for later retrieval. + /// + /// # Return value + /// + /// `Result` containing parsed `CastMessage` or `Error`. + pub fn receive_find_map(&self, f: F) -> Result + where + F: Fn(&CastMessage) -> Result, Error>, + { + loop { + let message = self.read()?; + + // If message is found, just return mapped result, otherwise keep unprocessed message + // in the buffer, it can be later retrieved with `receive`. + match f(&message)? { + Some(r) => return Ok(r), + None => self.message_buffer.borrow_mut().push(message), + } + } + } + + /// Generates unique integer number that is used in some requests to map them with the response. + /// + /// # Return value + /// + /// Unique (in the scope of this particular `MessageManager` instance) integer number. + pub fn generate_request_id(&self) -> NonZeroU32 { + let mut counter = self.request_counter.borrow_mut(); + let request_id = *counter; + *counter = counter.checked_add(1).unwrap(); + request_id + } + + /// Reads next `CastMessage` from the stream. + /// + /// # Return value + /// + /// `Result` containing parsed `CastMessage` or `Error`. + fn read(&self) -> Result { + let mut buffer: [u8; 4] = [0; 4]; + + let reader = &mut *self.stream.borrow_mut(); + + reader.read_exact(&mut buffer)?; + + let length = utils::read_u32_from_buffer(&buffer)?; + + let mut buffer: Vec = Vec::with_capacity(length as usize); + let mut limited_reader = reader.take(u64::from(length)); + + limited_reader.read_to_end(&mut buffer)?; + + let raw_message = utils::from_vec::(buffer.to_vec())?; + + log::debug!("Message received: {:?}", raw_message); + + Ok(CastMessage { + namespace: raw_message.namespace().to_string(), + source: raw_message.source_id().to_string(), + destination: raw_message.destination_id().to_string(), + payload: match raw_message.payload_type() { + PayloadType::STRING => { + CastMessagePayload::String(raw_message.payload_utf8().to_string()) + } + PayloadType::BINARY => { + CastMessagePayload::Binary(raw_message.payload_binary().to_owned()) + } + }, + }) + } +} + +#[cfg(test)] +mod tests { + use protobuf::EnumOrUnknown; + + use crate::{DEFAULT_RECEIVER_ID, DEFAULT_SENDER_ID, tests::MockTcpStream}; + + use super::*; + + #[test] + fn test_receive() { + let mut stream = MockTcpStream::new(); + let payload = r#"{"type":"PING"}"#; + stream.add_message(cast_channel::CastMessage { + protocol_version: Some(EnumOrUnknown::new(ProtocolVersion::CASTV2_1_2)), + source_id: Some(DEFAULT_RECEIVER_ID.to_string()), + destination_id: Some(DEFAULT_SENDER_ID.to_string()), + namespace: Some(crate::channels::heartbeat::CHANNEL_NAMESPACE.to_string()), + payload_type: Some(EnumOrUnknown::new(PayloadType::STRING)), + payload_utf8: Some(payload.to_string()), + payload_binary: None, + continued: None, + remaining_length: None, + special_fields: Default::default(), + }); + let message_manager = MessageManager::new(stream); + let expected_result = CastMessage { + namespace: crate::channels::heartbeat::CHANNEL_NAMESPACE.to_string(), + source: DEFAULT_RECEIVER_ID.to_string(), + destination: DEFAULT_SENDER_ID.to_string(), + payload: CastMessagePayload::String(payload.to_string()), + }; + + let result = message_manager + .receive() + .expect("expected to receive a message"); + + assert_eq!(expected_result, result); + } + + #[test] + fn test_send() { + let payload = r#"{"type":"PONG"}"#; + let namespace = crate::channels::heartbeat::CHANNEL_NAMESPACE; + let stream = MockTcpStream::new(); + let message_manager = MessageManager::new(stream.clone()); + let expected_message = cast_channel::CastMessage { + protocol_version: Some(EnumOrUnknown::new(ProtocolVersion::CASTV2_1_0)), + source_id: Some(DEFAULT_SENDER_ID.to_string()), + destination_id: Some(DEFAULT_RECEIVER_ID.to_string()), + namespace: Some(namespace.to_string()), + payload_type: Some(EnumOrUnknown::new(PayloadType::STRING)), + payload_utf8: Some(payload.to_string()), + payload_binary: None, + continued: None, + remaining_length: None, + special_fields: Default::default(), + }; + + message_manager + .send(CastMessage { + namespace: namespace.to_string(), + source: DEFAULT_SENDER_ID.to_string(), + destination: DEFAULT_RECEIVER_ID.to_string(), + payload: CastMessagePayload::String(payload.to_string()), + }) + .unwrap(); + + let tcp_message = stream + .received_message(0) + .expect("expected a message to have been received"); + assert_eq!(expected_message, tcp_message.message()); + } +} diff --git a/vendor/rust_cast-0.21.0/src/utils.rs b/vendor/rust_cast-0.21.0/src/utils.rs new file mode 100644 index 0000000..852b860 --- /dev/null +++ b/vendor/rust_cast-0.21.0/src/utils.rs @@ -0,0 +1,29 @@ +use crate::errors::Error; +use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt}; +use std::io::Cursor; + +pub fn read_u32_from_buffer(buffer: &[u8]) -> Result { + Ok(Cursor::new(buffer).read_u32::()?) +} + +pub fn write_u32_to_buffer(number: u32) -> Result, Error> { + let mut buffer = vec![]; + + buffer.write_u32::(number)?; + + Ok(buffer) +} + +pub fn to_vec(message: &M) -> Result, Error> { + let mut buffer = vec![]; + + message.write_to_writer(&mut buffer)?; + + Ok(buffer) +} + +pub fn from_vec(buffer: Vec) -> Result { + let mut read_buffer = Cursor::new(buffer); + + Ok(M::parse_from_reader(&mut read_buffer)?) +}