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)?) +}