Implement Cast Streaming mirroring, DLNA casting, daemon+GUI, and breadd integration
Some checks failed
dev release / build (push) Failing after 12s

Builds out the full v1 scope: a vendored+patched openscreen subset for
low-latency Cast Streaming (Mirroring receiver 0F5096E8) alongside the
existing Cast V2/HLS and new DLNA/AVTransport casting paths, breadcastd's
Idle/Casting state machine with a private IPC socket, the breadcast GTK4
popup as a thin IPC client, and bread.cast.*/bread.command.cast.* breadd
integration (device discovery, start/stop, mirroring lifecycle events).
Also adds bakery/systemd/Forgejo CI packaging.

Validated end-to-end against a real Chromecast/Google TV: negotiated
Cast Streaming session, live pipeline playback, and daemon+GUI click-to-cast/
stop through the actual popup.
This commit is contained in:
Breadway 2026-08-03 09:07:21 +08:00
parent 887c29002f
commit 8c745d18e0
283 changed files with 36788 additions and 0 deletions

View file

@ -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/<issue> 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}"

View file

@ -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}"

View file

@ -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

34
.gitignore vendored Normal file
View file

@ -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

91
CONTRIBUTING.md Normal file
View file

@ -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/<short-name>
fix/<issue-number-or-short-name>
```
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/<issue>` 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.<timestamp>+<sha>` /
`-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.

2589
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

28
Cargo.toml Normal file
View file

@ -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 <plasticbread849@gmail.com>"]
[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" }

44
EVENTS.md Normal file
View file

@ -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": "<mdns id or DLNA description URL>", "name": "<friendly name>", "model": "<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": "<id>", "device_name": "<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": "<id>", "error": "<message>" }` | 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": "<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.

21
LICENSE Normal file
View file

@ -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.

97
README.md Normal file
View file

@ -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.

34
bakery.toml Normal file
View file

@ -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",
]

View file

@ -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 }

View file

@ -0,0 +1,60 @@
use std::path::{Path, PathBuf};
fn collect_cc_files(dir: &Path, out: &mut Vec<PathBuf>) {
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");
}

View file

@ -0,0 +1,297 @@
#include "facade.h"
#include <atomic>
#include <future>
#include <memory>
#include <string>
#include <vector>
#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<openscreen::cast::Environment> environment;
std::unique_ptr<breadcast_caststream::MessagePortBridge> message_port;
std::unique_ptr<breadcast_caststream::MirroringSenderSession> 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<bool> negotiated{false};
std::atomic<bool> needs_key_frame{true};
std::atomic<int32_t> 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<CastStreamSender*>(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<CastStreamSender*>(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<CastStreamSender*>(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<CastStreamSender>();
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<void> constructed;
std::future<void> constructed_future = constructed.get_future();
task_runner.PostTask([handle_ptr, &task_runner, &params, &callbacks, remote_address,
local_source_id_str, receiver_id_str, post_message, user_data,
&constructed] {
handle_ptr->environment =
std::make_unique<openscreen::cast::Environment>(&openscreen::Clock::now, task_runner);
handle_ptr->message_port =
std::make_unique<breadcast_caststream::MessagePortBridge>(user_data, post_message);
handle_ptr->session = std::make_unique<breadcast_caststream::MirroringSenderSession>(
*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<std::string>(source_id, source_id_len);
auto ns = std::make_shared<std::string>(message_namespace, message_namespace_len);
auto body = std::make_shared<std::string>(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<std::vector<uint8_t>>(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<void> done;
std::future<void> 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"

View file

@ -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 <cstddef>
#include <cstdint>
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_

View file

@ -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);
}

View file

@ -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

View file

@ -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 <cstddef>
#include <cstdint>
#include <string>
#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_

View file

@ -0,0 +1,181 @@
#include "session.h"
#include <utility>
#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<ReceiverMessage> message) { OnAnswer(std::move(message)); });
if (!result.ok()) {
ReportError(result.message());
}
}
void MirroringSenderSession::OnAnswer(ErrorOr<ReceiverMessage> 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<openscreen::cast::Answer>(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<uint16_t>(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<openscreen::cast::SenderImpl>(
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

View file

@ -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 <cstddef>
#include <memory>
#include <string>
#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<openscreen::cast::ReceiverMessage> 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<openscreen::cast::SenderImpl> video_sender_;
};
} // namespace breadcast_caststream
#endif // BREADCAST_CASTSTREAM_SESSION_H_

View file

@ -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<Vec<u8>> = 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) };
}

View file

@ -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.

View file

@ -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.

View file

@ -0,0 +1 @@
920129874c16365415a8a52fdbbc492d7623dc11

View file

@ -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_

View file

@ -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_<FLAG>()` 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_<FLAG>() 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_

View file

@ -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 <string>
#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_

View file

@ -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 <string>
#include <vector>
#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<Resolution> 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_

View file

@ -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 <algorithm>
#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<int>(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<int64_t>(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<int32_t>(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<int32_t>(result);
}
} // namespace openscreen::cast

View file

@ -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 <stdint.h>
#include <limits>
#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<int>(std::numeric_limits<index_mod_256_t>::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_

View file

@ -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 <cmath>
#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<Clock::duration> ClockDriftSmoother::Current() const {
if (last_update_time_ == kNullTime) {
return std::nullopt;
}
return Clock::duration(
rounded_saturate_cast<Clock::duration::rep>(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<double>(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<double>((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<double>(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

View file

@ -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 <chrono>
#include <optional>
#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<Clock::duration> 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<double, Clock::duration::period> 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_

View file

@ -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 <memory>
#include <optional>
#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<ClockOffsetEstimator> 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<Clock::duration> 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<Clock::duration> GetEstimatedLatency() const = 0;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_CLOCK_OFFSET_ESTIMATOR_H_

View file

@ -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 <algorithm>
#include <chrono>
#include <memory>
#include <utility>
#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<uint64_t>(packet_id) << 48) |
(static_cast<uint64_t>(rtp.lower_32_bits()) << 1) |
static_cast<uint64_t>(audio ? 1 : 0);
}
} // namespace
std::unique_ptr<ClockOffsetEstimator> ClockOffsetEstimator::Create() {
return std::make_unique<ClockOffsetEstimatorImpl>();
}
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<Clock::duration> 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<Clock::duration> 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<double>(std::chrono::nanoseconds(process_noise).count()) *
std::chrono::nanoseconds(process_noise).count()),
r_nanos_squared_(
static_cast<double>(
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<double>(std::chrono::nanoseconds(measurement).count());
const double estimate_nanos =
static_cast<double>(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<Clock::duration>(std::chrono::nanoseconds(
static_cast<Clock::duration::rep>(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

View file

@ -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 <stddef.h>
#include <stdint.h>
#include <map>
#include <optional>
#include <utility>
#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<Clock::duration> GetEstimatedOffset() const final;
std::optional<Clock::duration> 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<Clock::time_point>,
std::optional<Clock::time_point>>
TimeTickPair;
typedef std::map<uint64_t, TimeTickPair> 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_

View file

@ -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 <algorithm>
#include <utility>
#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<PacketNack>* 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<FramePacketId>(kAllPacketsLost + 1) <
FramePacketId{0x0000 + 1},
"comparison requires integer wrap-around");
return PacketNack{a.frame_id,
static_cast<FramePacketId>(a.packet_id + 1)} <
PacketNack{b.frame_id,
static_cast<FramePacketId>(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<RtcpReportBlock> receiver_report;
std::vector<RtcpReceiverFrameLogMessage> log_messages;
FrameId checkpoint_frame_id;
milliseconds target_playout_delay{};
std::vector<FrameId> received_frames;
std::vector<PacketNack> 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<int>(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<RtcpReportBlock>& receiver_report) {
if (in.size() < kRtcpReceiverReportSize) {
return false;
}
if (ConsumeField<uint32_t>(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<RtcpReceiverFrameLogMessage>& messages) {
if (in.size() < 2 * sizeof(uint32_t)) {
return false;
}
const uint32_t sender_ssrc = ConsumeField<uint32_t>(in);
const uint32_t name = ConsumeField<uint32_t>(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<RtcpReceiverFrameLogMessage>& messages) {
while (!in.empty()) {
if (in.size() < kRtcpReceiverFrameLogMessageHeaderSize) {
messages.clear();
return false;
}
const uint32_t truncated_rtp_timestamp = ConsumeField<uint32_t>(in);
const uint32_t data = ConsumeField<uint32_t>(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<uint8_t>(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<uint16_t>(in);
const uint16_t event_type_and_timestamp_delta =
ConsumeField<uint16_t>(in);
// Skip unknown event types, they are not useful.
const auto event_type =
StatisticsEvent::FromWireType(static_cast<StatisticsEvent::WireType>(
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<int16_t>(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<FrameId>* received_frames,
std::vector<PacketNack>& packet_nacks) {
OSP_CHECK(!max_feedback_frame_id.is_null());
if (static_cast<int>(in.size()) < kRtcpFeedbackHeaderSize) {
return false;
}
if (ConsumeField<uint32_t>(in) != session_->receiver_ssrc() ||
ConsumeField<uint32_t>(in) != session_->sender_ssrc()) {
return true; // Ignore report from mismatched SSRC(s).
}
if (ConsumeField<uint32_t>(in) != kRtcpCastIdentifierWord) {
return false;
}
const FrameId feedback_frame_id =
max_feedback_frame_id.ExpandLessThanOrEqual(ConsumeField<uint8_t>(in));
const int loss_field_count = ConsumeField<uint8_t>(in);
const auto playout_delay = milliseconds(ConsumeField<uint16_t>(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<int>(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<uint8_t>(in));
FramePacketId packet_id = ConsumeField<uint16_t>(in);
uint8_t bits = ConsumeField<uint8_t>(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<int>(in.size()) < kRtcpFeedbackAckHeaderSize ||
ConsumeField<uint32_t>(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<uint8_t>(in);
if (static_cast<int>(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<uint8_t>(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<int>(in.size()) < kRtcpExtendedReportHeaderSize) {
return false;
}
if (ConsumeField<uint32_t>(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<int>(in.size()) < kRtcpExtendedReportBlockHeaderSize) {
return false;
}
const uint8_t block_type = ConsumeField<uint8_t>(in);
in = in.subspan(sizeof(uint8_t)); // Skip the "reserved" byte.
const int block_data_size =
static_cast<int>(ConsumeField<uint16_t>(in)) * 4;
if (static_cast<int>(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<uint64_t>(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<int>(in.size()) < kRtcpPictureLossIndicatorHeaderSize) {
return false;
}
// Only set the flag if the PLI is from the Receiver and to this Sender.
if (ConsumeField<uint32_t>(in) == session_->receiver_ssrc() &&
ConsumeField<uint32_t>(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<RtcpReceiverFrameLogMessage> messages) {}
void CompoundRtcpParser::Client::OnReceiverIndicatesPictureLoss() {}
void CompoundRtcpParser::Client::OnReceiverCheckpoint(
FrameId frame_id,
milliseconds playout_delay) {}
void CompoundRtcpParser::Client::OnReceiverHasFrames(
std::vector<FrameId> acks) {}
void CompoundRtcpParser::Client::OnReceiverIsMissingPackets(
std::vector<PacketNack> nacks) {}
} // namespace openscreen::cast

View file

@ -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 <chrono>
#include <optional>
#include <vector>
#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<RtcpReceiverFrameLogMessage> 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<FrameId> 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<PacketNack> 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<RtcpReportBlock>& receiver_report);
bool ParseApplicationDefined(
RtcpSubtype subtype,
ByteView in,
std::vector<RtcpReceiverFrameLogMessage>& messages);
bool ParseFrameLogMessages(
ByteView in,
std::vector<RtcpReceiverFrameLogMessage>& messages);
bool ParseFeedback(ByteView in,
FrameId max_feedback_frame_id,
FrameId* checkpoint_frame_id,
std::chrono::milliseconds* target_playout_delay,
std::vector<FrameId>* received_frames,
std::vector<PacketNack>& packet_nacks);
bool ParseExtendedReports(ByteView in,
Clock::time_point& receiver_reference_time);
bool ParsePictureLossIndicator(ByteView in, bool& picture_loss_indicator);
const raw_ref<RtcpSession> session_;
const raw_ref<Client> 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_

View file

@ -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 <stdint.h>
#include <limits>
#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 <typename FullWidthInteger, class Subclass>
class ExpandedValueBase {
static_assert(std::numeric_limits<FullWidthInteger>::is_signed,
"FullWidthInteger must be a signed integer.");
static_assert(std::numeric_limits<FullWidthInteger>::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<uint8_t>(value_);
}
constexpr uint16_t lower_16_bits() const {
return static_cast<uint16_t>(value_);
}
constexpr uint32_t lower_32_bits() const {
return static_cast<uint32_t>(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 <typename ShortUnsigned>
Subclass ExpandLessThanOrEqual(ShortUnsigned x) const {
static_assert(!std::numeric_limits<ShortUnsigned>::is_signed,
"`x` must be an unsigned integer.");
static_assert(std::numeric_limits<ShortUnsigned>::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<ShortUnsigned>::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<ShortUnsigned>(0),
x & (static_cast<ShortUnsigned>(1) << ((sizeof(x) * 8) - 1)));
return Subclass(x);
}
}
// Compute the smallest value greater than `this` value whose lower bits are
// those of `x`.
template <typename ShortUnsigned>
Subclass ExpandGreaterThan(ShortUnsigned x) const {
const Subclass maximum_possible_result(
value_ + std::numeric_limits<ShortUnsigned>::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 <typename ShortUnsigned>
Subclass Expand(ShortUnsigned x) const {
const Subclass maximum_possible_result(
value_ + max_distance_for_expansion<ShortUnsigned>());
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<uint64_t>(value_);
}
static Subclass DeserializeForIPC(uint64_t serialized) {
return Subclass(static_cast<FullWidthInteger>(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 <typename ShortUnsigned>
static constexpr FullWidthInteger max_distance_for_expansion() {
return std::numeric_limits<ShortUnsigned>::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_

View file

@ -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 <random>
#include <utility>
#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<EncodedFrame&&>(other)),
owned_data_(std::move(other.owned_data_)) {
data = owned_data_;
other.data = ByteView();
}
EncryptedFrame& EncryptedFrame::operator=(EncryptedFrame&& other) {
this->EncodedFrame::operator=(static_cast<EncodedFrame&&>(other));
owned_data_ = std::move(other.owned_data_);
data = owned_data_;
other.data = ByteView();
return *this;
}
FrameCrypto::FrameCrypto(const std::array<uint8_t, 16>& aes_key,
const std::array<uint8_t, 16>& 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*>(&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<uint8_t, 16> aes_nonce{};
static_assert(AES_BLOCK_SIZE == sizeof(aes_nonce),
"AES_BLOCK_SIZE is not 16 bytes.");
WriteBigEndian<uint32_t>(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<uint8_t, 16> 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

View file

@ -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 <stddef.h>
#include <stdint.h>
#include <array>
#include <vector>
#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<uint8_t> owned_data_;
};
// Encrypts EncodedFrames before sending, or decrypts EncryptedFrames that have
// been received.
class FrameCrypto {
public:
using ChunkList = std::span<const ByteView>;
// 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<uint8_t, 16>& aes_key,
const std::array<uint8_t, 16>& 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<uint8_t, 16> 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_

View file

@ -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_

View file

@ -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<NtpSeconds>(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<NtpSeconds>(time_since_start);
const auto remainder =
std::chrono::duration_cast<NtpFraction>(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

View file

@ -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 <stdint.h>
#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<int64_t, std::chrono::seconds::period>;
using NtpFraction =
std::chrono::duration<int64_t, std::ratio<1, INT64_C(0x100000000)>>;
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<uint64_t>(seconds.count()) << 32) |
static_cast<uint32_t>(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_

View file

@ -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<ApparentPacketType, Ssrc> 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<uint32_t>(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<uint32_t>(packet.data() + kRtcpCommonHeaderSize)});
}
return std::make_pair(ApparentPacketType::UNKNOWN, Ssrc{0});
}
} // namespace openscreen::cast

View file

@ -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 <utility>
#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 <typename Integer>
inline Integer ConsumeField(ByteView& in) {
OSP_CHECK_GE(in.size(), sizeof(Integer));
const Integer result = ReadBigEndian<Integer>(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 <typename Integer>
inline void AppendField(Integer value, ByteBuffer& out) {
WriteBigEndian<Integer>(value, out.data());
out = out.subspan(sizeof(Integer));
}
// Returns a bitmask for a field having the given number of bits. For example,
// FieldBitmask<uint8_t>(5) returns 0b00011111.
template <typename Integer>
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<ApparentPacketType, Ssrc> InspectPacketForRouting(ByteView packet);
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_PACKET_UTIL_H_

View file

@ -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 <algorithm>
#include <limits>
#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<int>(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<uint8_t>(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<uint8_t>(byte0, buffer);
AppendField<uint8_t>(static_cast<uint8_t>(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<uint16_t>(payload_size / sizeof(uint32_t), buffer);
}
// static
std::optional<RtcpCommonHeader> RtcpCommonHeader::Parse(ByteView buffer) {
if (buffer.size() < kRtcpCommonHeaderSize) {
return std::nullopt;
}
const uint8_t byte0 = ConsumeField<uint8_t>(buffer);
if ((byte0 >> kRtcpReportCountFieldNumBits) !=
kRtcpRequiredVersionAndPaddingBits) {
return std::nullopt;
}
const uint8_t report_count_or_subtype =
byte0 & FieldBitmask<uint8_t>(kRtcpReportCountFieldNumBits);
const uint8_t byte1 = ConsumeField<uint8_t>(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<RtcpPacketType>(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<RtcpSubtype>(report_count_or_subtype)) {
case RtcpSubtype::kPictureLossIndicator:
case RtcpSubtype::kReceiverLog:
case RtcpSubtype::kFeedback:
header.with.subtype =
static_cast<RtcpSubtype>(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<int>(ConsumeField<uint16_t>(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<uint32_t>(ssrc, buffer);
OSP_CHECK_GE(packet_fraction_lost_numerator,
std::numeric_limits<uint8_t>::min());
OSP_CHECK_LE(packet_fraction_lost_numerator,
std::numeric_limits<uint8_t>::max());
OSP_CHECK_GE(cumulative_packets_lost, 0);
OSP_CHECK_LE(cumulative_packets_lost,
FieldBitmask<int>(kRtcpCumulativePacketsFieldNumBits));
AppendField<uint32_t>(
(static_cast<int>(packet_fraction_lost_numerator)
<< kRtcpCumulativePacketsFieldNumBits) |
(static_cast<int>(cumulative_packets_lost) &
FieldBitmask<uint32_t>(kRtcpCumulativePacketsFieldNumBits)),
buffer);
AppendField<uint32_t>(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<uint32_t>::max()});
AppendField<uint32_t>(jitter_ticks, buffer);
AppendField<uint32_t>(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<uint32_t>::max()});
AppendField<uint32_t>(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<uint8_t>(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<int>(
std::min(std::max<int64_t>(num_lost, 0),
FieldBitmask<int64_t>(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<uint32_t>::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<Delay>(local_clock_delay);
}
// static
std::optional<RtcpReportBlock> RtcpReportBlock::ParseOne(ByteView buffer,
int report_count,
Ssrc ssrc) {
if (static_cast<int>(buffer.size()) < (kRtcpReportBlockSize * report_count)) {
return std::nullopt;
}
std::optional<RtcpReportBlock> result;
for (int block = 0; block < report_count; ++block) {
if (ConsumeField<uint32_t>(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<uint32_t>(buffer);
report_block.packet_fraction_lost_numerator =
second_word >> kRtcpCumulativePacketsFieldNumBits;
report_block.cumulative_packets_lost =
second_word &
FieldBitmask<uint32_t>(kRtcpCumulativePacketsFieldNumBits);
report_block.extended_high_sequence_number = ConsumeField<uint32_t>(buffer);
report_block.jitter =
RtpTimeDelta::FromTicks(ConsumeField<uint32_t>(buffer));
report_block.last_status_report_id = ConsumeField<uint32_t>(buffer);
report_block.delay_since_last_report =
RtcpReportBlock::Delay(ConsumeField<uint32_t>(buffer));
}
return result;
}
RtcpSenderReport::RtcpSenderReport() = default;
RtcpSenderReport::~RtcpSenderReport() = default;
} // namespace openscreen::cast

View file

@ -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 <stdint.h>
#include <optional>
#include <tuple>
#include <vector>
#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<RtcpCommonHeader> 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<uint32_t>(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<int64_t, std::ratio<1, 65536>>;
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<RtcpReportBlock> 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<RtcpReportBlock> 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<RtcpReceiverEventLogMessage> messages;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_RTCP_COMMON_H_

View file

@ -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

View file

@ -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_

View file

@ -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<RtpPayloadType>(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<RtcpPacketType>(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

View file

@ -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 <stdint.h>
#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_

View file

@ -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 <algorithm>
#include <limits>
#include <random>
#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<std::minstd_rand::result_type>(
Clock::now().time_since_epoch().count()));
return std::uniform_int_distribution<uint16_t>()(generator);
}
} // namespace
RtpPacketizer::RtpPacketizer(RtpPayloadType payload_type,
Ssrc sender_ssrc,
int max_packet_size)
: payload_type_7bits_(static_cast<uint8_t>(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<int>(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<uint16_t>::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<int>(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<uint8_t>(kRtpRequiredFirstByte, buffer);
AppendField<uint8_t>(
(is_last_packet ? kRtpMarkerBitMask : 0) | payload_type_7bits_, buffer);
AppendField<uint16_t>(sequence_number_++, buffer);
AppendField<uint32_t>(frame.rtp_timestamp.lower_32_bits(), buffer);
AppendField<uint32_t>(sender_ssrc_, buffer);
// Cast Header.
AppendField<uint8_t>(
((frame.dependency == EncodedFrame::Dependency::kKeyFrame)
? kRtpKeyFrameBitMask
: 0) |
kRtpHasReferenceFrameIdBitMask |
(include_adaptive_latency_change ? 1 : 0),
buffer);
AppendField<uint8_t>(frame.frame_id.lower_8_bits(), buffer);
AppendField<uint16_t>(packet_id, buffer);
AppendField<uint16_t>(num_packets - 1, buffer);
AppendField<uint8_t>(frame.referenced_frame_id.lower_8_bits(), buffer);
// Extension of Cast Header for Adaptive Latency change.
if (include_adaptive_latency_change) {
AppendField<uint16_t>(
(kAdaptiveLatencyRtpExtensionType << kNumExtensionDataSizeFieldBits) |
sizeof(uint16_t),
buffer);
AppendField<uint16_t>(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<int>(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

View file

@ -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 <stdint.h>
#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_

View file

@ -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 <algorithm>
#include <chrono>
#include <ratio>
#include <utility>
#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<Clock::duration>(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<size_t>(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<int>(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<RtcpReceiverFrameLogMessage> 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<FrameId> 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<PacketNack> 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<int>(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

View file

@ -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 <stdint.h>
#include <array>
#include <chrono>
#include <optional>
#include <vector>
#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<EncryptedFrame> 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<Clock::time_point> 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<PendingFrameSlot> 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<RtcpReceiverFrameLogMessage> messages) final;
void OnReceiverIndicatesPictureLoss() final;
void OnReceiverCheckpoint(FrameId frame_id,
std::chrono::milliseconds playout_delay) final;
void OnReceiverHasFrames(std::vector<FrameId> acks) final;
void OnReceiverIsMissingPackets(std::vector<PacketNack> 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<SenderPacketRouter> 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<PendingFrameSlot, kMaxUnackedFrames> 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> 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<FrameId> pending_cancellations_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_SENDER_IMPL_H_

View file

@ -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<ByteBuffer, StatusReportId> 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<uint32_t>(session_->sender_ssrc(), buffer);
const NtpTimestamp ntp_timestamp =
session_->ntp_converter().ToNtpTimestamp(sender_report.reference_time);
AppendField<uint64_t>(ntp_timestamp, buffer);
AppendField<uint32_t>(sender_report.rtp_timestamp.lower_32_bits(), buffer);
AppendField<uint32_t>(sender_report.send_packet_count, buffer);
AppendField<uint32_t>(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<uint64_t>(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

View file

@ -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 <stdint.h>
#include <utility>
#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<ByteBuffer, StatusReportId> 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<RtcpSession> session_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_SENDER_REPORT_BUILDER_H_

View file

@ -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 <algorithm>
#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<double>(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<ClockOffsetEstimator> 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<StatisticsCollector>(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<FrameEvent>& 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<PacketEvent>& 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<Clock::duration> 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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(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<int>(
StatisticType::kTimeSinceLastReceiverResponseMs)] =
InMilliseconds(end_time - session_stats.last_response_received_time);
}
stats_list[static_cast<int>(StatisticType::kNumLateFrames)] =
session_stats.late_frame_counter;
}
std::optional<Clock::time_point> StatisticsAnalyzer::ToSenderTimestamp(
Clock::time_point receiver_timestamp,
StatisticsEvent::MediaType media_type) const {
const std::optional<Clock::duration> receiver_offset =
offset_estimator_->GetEstimatedOffset();
if (!receiver_offset) {
return {};
}
return receiver_timestamp - *receiver_offset;
}
} // namespace openscreen::cast

View file

@ -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 <map>
#include <memory>
#include <optional>
#include <utility>
#include <vector>
#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<ClockOffsetEstimator> 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 <typename T>
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<T&>(const_cast<const AVPair*>(this)->Get(media_type));
}
};
using FrameStatsMap = std::map<StatisticsEvent::Type, FrameStatsAggregate>;
using PacketStatsMap = std::map<StatisticsEvent::Type, PacketStatsAggregate>;
using LatencyStatsMap = std::map<StatisticType, LatencyStatsAggregate>;
using FrameInfoMap = std::map<RtpTimeTicks, FrameInfo>;
using PacketKey = std::pair<RtpTimeTicks, uint16_t>;
using PacketInfoMap = std::map<PacketKey, PacketInfo>;
// 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<FrameEvent>& frame_events);
void ProcessPacketEvents(const std::vector<PacketEvent>& 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<Clock::time_point> ToSenderTimestamp(
Clock::time_point receiver_timestamp,
StatisticsEvent::MediaType media_type) const;
// The statistics client to which we report analyzed statistics.
const raw_ptr<SenderStatsClient> stats_client_;
// The statistics collector from which we take the un-analyzed stats packets.
std::unique_ptr<StatisticsCollector> statistics_collector_;
// Keeps track of the best-guess clock offset between the sender and receiver.
std::unique_ptr<ClockOffsetEstimator> 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<FrameInfoMap> recent_frame_infos_;
AVPair<PacketInfoMap> recent_packet_infos_;
// Aggregate statistics.
AVPair<FrameStatsMap> frame_stats_;
AVPair<PacketStatsMap> packet_stats_;
AVPair<LatencyStatsMap> latency_stats_;
// Stats that relate to the entirety of the session. For example, total late
// frames, or time of last event.
AVPair<SessionStats> session_stats_;
// Histograms.
AVPair<SenderStats::HistogramsList> histograms_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_STATISTICS_ANALYZER_H_

View file

@ -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 <stdint.h>
#include <limits>
#include <utility>
#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<uint32_t>(&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<uint16_t>(&event.packet_id);
success &= reader.Read<uint16_t>(&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<uint64_t>(std::numeric_limits<uint32_t>::max()) <=
static_cast<uint64_t>(std::numeric_limits<size_t>::max()),
"invalid type cast assumption");
OSP_CHECK_LE(packet.size(),
static_cast<size_t>(std::numeric_limits<uint32_t>::max()));
event.size = static_cast<uint32_t>(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<PacketEvent> StatisticsCollector::TakeRecentPacketEvents() {
std::vector<PacketEvent> out;
recent_packet_events_.swap(out);
return out;
}
std::vector<FrameEvent> StatisticsCollector::TakeRecentFrameEvents() {
std::vector<FrameEvent> out;
recent_frame_events_.swap(out);
return out;
}
} // namespace openscreen::cast

View file

@ -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 <vector>
#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<PacketEvent> 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<FrameEvent> TakeRecentFrameEvents();
private:
ClockNowFunctionPtr now_;
std::vector<PacketEvent> recent_packet_events_;
std::vector<FrameEvent> recent_frame_events_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_STATISTICS_COLLECTOR_H_

View file

@ -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<int>(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<int>(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

View file

@ -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 <stddef.h>
#include <stdint.h>
#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_

View file

@ -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 <utility>
#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<uint32_t>(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<RtcpReceiverFrameLogMessage>& 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<int>(event_message.type);
break;
}
}
}
}
} // namespace openscreen::cast

View file

@ -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 <vector>
#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<RtcpReceiverFrameLogMessage>& 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> environment_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_IMPL_STATISTICS_DISPATCHER_H_

View file

@ -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 <array>
#include <utility>
#include "util/enum_name_table.h"
#include "util/osp_logging.h"
namespace openscreen::cast {
namespace {
constexpr EnumNameTable<AudioCodec, 3> kAudioCodecNames{
{{"aac", AudioCodec::kAac},
{"opus", AudioCodec::kOpus},
{"REMOTE_AUDIO", AudioCodec::kNotSpecified}}};
constexpr EnumNameTable<VideoCodec, 6> 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<AudioCodec> StringToAudioCodec(std::string_view name) {
return GetEnum(kAudioCodecNames, name);
}
const char* CodecToString(VideoCodec codec) {
return GetEnumName(kVideoCodecNames, codec).value();
}
ErrorOr<VideoCodec> StringToVideoCodec(std::string_view name) {
return GetEnum(kVideoCodecNames, name);
}
} // namespace openscreen::cast

View file

@ -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 <string>
#include <string_view>
#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<AudioCodec> StringToAudioCodec(std::string_view name);
const char* CodecToString(VideoCodec codec);
ErrorOr<VideoCodec> StringToVideoCodec(std::string_view name);
} // namespace openscreen::cast
#endif // CAST_STREAMING_MESSAGE_FIELDS_H_

View file

@ -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 <string_view>
#include <utility>
#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<AspectRatioConstraint, 2> kAspectRatioConstraintNames{
{{kScalingReceiver, AspectRatioConstraint::kVariable},
{kScalingSender, AspectRatioConstraint::kFixed}}};
Json::Value AspectRatioConstraintToJson(AspectRatioConstraint aspect_ratio) {
return Json::Value(GetEnumName(kAspectRatioConstraintNames, aspect_ratio)
.value(kScalingSender));
}
std::optional<AspectRatioConstraint> TryParseAspectRatioConstraint(
const Json::Value& value) {
std::string aspect_ratio;
if (!json::TryParseString(value, &aspect_ratio)) {
return std::nullopt;
}
ErrorOr<AspectRatioConstraint> constraint =
GetEnum(kAspectRatioConstraintNames, aspect_ratio);
if (constraint.is_error()) {
return std::nullopt;
}
return constraint.value();
}
template <typename T>
ErrorOr<std::optional<T>> ParseOptional(const Json::Value& value) {
if (!value) {
return std::optional<T>{};
}
auto out = T::TryParse(value);
if (out.is_error()) {
return out.error();
}
return std::optional<T>{std::move(out.value())};
}
} // namespace
// static
ErrorOr<AspectRatio> 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<std::string_view> 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> 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> 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<Dimensions>(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> 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> 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<Dimensions>(root[kDimensions]);
if (dimensions.is_error()) {
return dimensions.error();
}
out.dimensions = std::move(dimensions.value());
auto aspect_ratio = ParseOptional<AspectRatio>(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> 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<Constraints>(root[kConstraints]);
if (constraints.is_error()) {
return constraints.error();
}
out.constraints = std::move(constraints.value());
auto display = ParseOptional<DisplayDescription>(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

View file

@ -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 <array>
#include <chrono>
#include <cstdint>
#include <initializer_list>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#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<AudioConstraints> 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<std::chrono::milliseconds> max_delay = {};
};
struct VideoConstraints {
static ErrorOr<VideoConstraints> TryParse(const Json::Value& value);
Json::Value ToJson() const;
bool IsValid() const;
std::optional<double> max_pixels_per_second = {};
std::optional<Dimensions> min_resolution = {};
Dimensions max_dimensions = {};
int min_bit_rate = 0; // optional
int max_bit_rate = 0;
std::optional<std::chrono::milliseconds> max_delay = {};
};
struct Constraints {
static ErrorOr<Constraints> 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<AspectRatio> 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<DisplayDescription> 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> dimensions;
std::optional<AspectRatio> aspect_ratio = {};
std::optional<AspectRatioConstraint> aspect_ratio_constraint = {};
};
struct Answer {
static ErrorOr<Answer> TryParse(const Json::Value& value);
Json::Value ToJson() const;
bool IsValid() const;
int udp_port = 0;
std::vector<int> send_indexes;
std::vector<Ssrc> ssrcs;
// Constraints and display descriptions are optional fields, and maybe null in
// the valid case.
std::optional<Constraints> constraints;
std::optional<DisplayDescription> display;
std::vector<int> receiver_rtcp_event_log;
std::vector<int> receiver_rtcp_dscp;
// RTP extensions should be empty, but not null.
std::vector<std::vector<std::string>> rtp_extensions = {};
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_ANSWER_MESSAGES_H_

View file

@ -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 <algorithm>
#include <utility>
#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<double>(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<double>(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

View file

@ -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 <chrono>
#include <cmath>
#include <memory>
#include <tuple>
#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_

View file

@ -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 <ostream>
#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

View file

@ -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 <chrono>
#include <ostream>
#include <ratio>
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_

View file

@ -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

View file

@ -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 <stdint.h>
#include <chrono>
#include <vector>
#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_

View file

@ -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 <algorithm>
#include <utility>
#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<std::unique_ptr<UdpSocket>> 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<std::unique_ptr<UdpSocket>&>(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<UdpPacket> 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<std::vector<uint8_t>&>(packet)));
}
} // namespace openscreen::cast

View file

@ -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 <stdint.h>
#include <functional>
#include <memory>
#include <vector>
#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<uint8_t> 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<UdpPacket> packet_or_error) final;
ClockNowFunctionPtr now_function_;
const raw_ref<TaskRunner> 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<UdpSocket> socket_;
// These are externally set/cleared. Behaviors are described in getter/setter
// method comments above.
IPEndpoint local_endpoint_{};
IPEndpoint remote_endpoint_{};
raw_ptr<PacketConsumer> packet_consumer_ = nullptr;
SocketState state_ = SocketState::kStarting;
raw_ptr<SocketSubscriber> socket_subscriber_ = nullptr;
raw_ptr<StatisticsCollector> statistics_collector_ = nullptr;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_ENVIRONMENT_H_

View file

@ -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<null>";
return "F" + std::to_string(value());
}
} // namespace openscreen::cast

View file

@ -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 <stdint.h>
#include <limits>
#include <sstream>
#include <string>
#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<int64_t, FrameId> {
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<int64_t>::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<int64_t, FrameId>;
friend std::ostream& operator<<(std::ostream& out, const FrameId rhs);
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_FRAME_ID_H_

View file

@ -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 <inttypes.h>
#include <algorithm>
#include <limits>
#include <ranges>
#include <string>
#include <string_view>
#include <utility>
#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<CastMode, 2> 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<RtpPayloadType>(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<uint8_t, kAesBytesSize>* 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<Resolution>* 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> 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<int>(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<Json::UInt>(ssrc);
root["targetDelay"] = static_cast<int>(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<int>::max() &&
rtp_timebase >= 1;
}
ErrorOr<AudioStream> 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<AudioCodec> 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> 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<VideoCodec> 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> Offer::TryParse(const Json::Value& root) {
if (!root.isObject()) {
return Error(Error::Code::kJsonParseError, "null offer");
}
const ErrorOr<CastMode> 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<AudioStream> audio_streams;
std::vector<VideoStream> video_streams;
using Dscp = std::optional<int>;
std::optional<Dscp> 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

View file

@ -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 <chrono>
#include <string>
#include <vector>
#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<Stream> 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<uint8_t, 16> aes_key = {};
std::array<uint8_t, 16> 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<int> 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<std::string> rtp_extensions;
};
struct AudioStream {
static ErrorOr<AudioStream> 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<VideoStream> 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<Resolution> resolutions;
std::string error_recovery_mode;
};
struct Offer {
static ErrorOr<Offer> TryParse(const Json::Value& root);
Json::Value ToJson() const;
bool IsValid() const;
CastMode cast_mode = CastMode::kMirroring;
std::vector<AudioStream> audio_streams;
std::vector<VideoStream> video_streams;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_OFFER_MESSAGES_H_

View file

@ -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 <utility>
#include <variant>
#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<ReceiverMessage::Type, 4> kMessageTypeNames{
{{kMessageTypeAnswer, ReceiverMessage::Type::kAnswer},
{"CAPABILITIES_RESPONSE", ReceiverMessage::Type::kCapabilitiesResponse},
{"RPC", ReceiverMessage::Type::kRpc},
{"INPUT", ReceiverMessage::Type::kInput}}};
EnumNameTable<MediaCapability, 10> 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<ReceiverMessage::Type> 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<MediaCapability> 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<Error::Code>(code - kOpenscreenErrorOffset);
}
}
ReceiverError::ReceiverError(Error::Code code, std::string_view description)
: code(static_cast<int>(code) + kOpenscreenErrorOffset),
openscreen_code(code),
description(description) {}
ReceiverError::ReceiverError(const Error& error)
: code(static_cast<int>(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> 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<int>(*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> 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<MediaCapability> capabilities;
if (!json::TryParseArray<MediaCapability>(
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> 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<ReceiverError> 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<ReceiverCapability> 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<uint8_t> 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<uint8_t> 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<Json::Value> 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<Answer>(body).ToJson();
} else {
root[kResult] = kResultError;
root[kErrorMessageBody] = std::get<ReceiverError>(body).ToJson();
}
break;
case ReceiverMessage::Type::kCapabilitiesResponse:
if (valid) {
root[kResult] = kResultOk;
root[kCapabilitiesMessageBody] =
std::get<ReceiverCapability>(body).ToJson();
} else {
root[kResult] = kResultError;
root[kErrorMessageBody] = std::get<ReceiverError>(body).ToJson();
}
break;
// NOTE: RPC messages do NOT have a result field.
case ReceiverMessage::Type::kRpc:
root[kRpcMessageBody] =
base64::Encode(std::get<std::vector<uint8_t>>(body));
break;
case ReceiverMessage::Type::kInput:
root[kInputMessageBody] =
base64::Encode(std::get<std::vector<uint8_t>>(body));
break;
default:
OSP_NOTREACHED();
}
return root;
}
} // namespace openscreen::cast

View file

@ -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 <memory>
#include <optional>
#include <string>
#include <string_view>
#include <variant>
#include <vector>
#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<ReceiverCapability> 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<MediaCapability> 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<ReceiverError> 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<Error::Code> 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<ReceiverMessage> Parse(const Json::Value& value);
ErrorOr<Json::Value> ToJson() const;
Type type = Type::kUnknown;
int32_t sequence_number = -1;
bool valid = false;
std::variant<std::monostate,
Answer,
std::vector<uint8_t>, // Binary-encoded protobuf message.
ReceiverCapability,
ReceiverError>
body;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_RECEIVER_MESSAGE_H_

View file

@ -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

View file

@ -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 <stdint.h>
#include <chrono>
#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_

View file

@ -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 <algorithm>
#include <utility>
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<uint8_t, 16> aes_secret_key,
std::array<uint8_t, 16> 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

View file

@ -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 <array>
#include <chrono>
#include <cstdint>
#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<uint8_t, 16> aes_secret_key,
std::array<uint8_t, 16> 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<uint8_t, 16> aes_secret_key{};
std::array<uint8_t, 16> 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_

View file

@ -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 <algorithm>
#include <chrono>
#include <string>
#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<std::pair<int, SenderSessionMessenger::ReplyCallback>>*
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<Json::Value> 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<uint8_t>(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<uint8_t>(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<Json::Value> 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<ReceiverMessage> 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<uint8_t>(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<uint8_t>(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<Json::Value> 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<Json::Value> 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<SenderMessage> 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

View file

@ -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 <functional>
#include <string>
#include <utility>
#include <vector>
#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<void(Error)>;
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<MessagePort> 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<void(ErrorOr<ReceiverMessage>)>;
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<TaskRunner> 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<int, ReplyCallback> 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<SenderSessionMessenger> weak_factory_{this};
};
// Message port interface designed for messaging to and from a sender.
class ReceiverSessionMessenger final : public SessionMessenger {
public:
using RequestCallback =
std::function<void(const std::string&, SenderMessage)>;
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(const std::string& /* source_id */,
const std::string& /* message_namespace */,
const std::string& /* message */)>;
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<SenderMessage::Type, RequestCallback> callbacks_;
std::vector<std::pair<std::string, CustomMessageCallback>>
custom_message_handlers_;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_PUBLIC_SESSION_MESSENGER_H_

View file

@ -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 <algorithm>
#include <iomanip>
#include <iostream>
#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 <typename Type>
Json::Value ToJson(const Type& t) {
return t.ToJson();
}
template <>
Json::Value ToJson(const double& t) {
return t;
}
template <typename T, typename Type>
Json::Value ArrayToJson(
const std::array<T, static_cast<size_t>(Type::kNumTypes)>& list,
const EnumNameTable<Type, static_cast<size_t>(Type::kNumTypes)>& names) {
Json::Value out;
for (size_t i = 0; i < list.size(); ++i) {
ErrorOr<const char*> name = GetEnumName(names, static_cast<Type>(i));
OSP_CHECK(name);
out[name.value()] = ToJson(list[i]);
}
return out;
}
} // namespace
// External linkage for unit test
extern const EnumNameTable<StatisticType,
static_cast<size_t>(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,
static_cast<size_t>(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<int> 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

View file

@ -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 <stddef.h>
#include <stdint.h>
#include <string>
#include <utility>
#include <vector>
#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<int> buckets;
private:
SimpleHistogram(int64_t min,
int64_t max,
int64_t width,
std::vector<int> buckets);
std::string GetBucketName(size_t index) const;
};
std::ostream& operator<<(std::ostream& out, const SimpleHistogram& histogram);
struct SenderStats {
using StatisticsList =
std::array<double, static_cast<size_t>(StatisticType::kNumTypes)>;
using HistogramsList =
std::array<SimpleHistogram,
static_cast<size_t>(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_

View file

@ -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 <utility>
#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> 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> 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<double>(frame_rate),
static_cast<double>(other.frame_rate)));
}
bool Dimensions::operator!=(const Dimensions& other) const {
return !(*this == other);
}
bool Dimensions::IsSupersetOf(const Dimensions& other) const {
if (static_cast<double>(frame_rate) !=
static_cast<double>(other.frame_rate)) {
return static_cast<double>(frame_rate) >=
static_cast<double>(other.frame_rate);
}
return ToResolution().IsSupersetOf(other.ToResolution());
}
} // namespace openscreen::cast

View file

@ -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<Resolution> 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<Dimensions> 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<double>(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_

View file

@ -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 <sstream>
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

View file

@ -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 <stdint.h>
#include <chrono>
#include <cmath>
#include <limits>
#include <sstream>
#include <type_traits>
#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<microseconds>(kAudioSamplingRate);
// RtpTimeDelta two_seconds_in_rtp =
// RtpTimeDelta::FromDuration(std::chrono::seconds(2),
// kAudioSamplingRate);
class RtpTimeDelta : public ExpandedValueBase<int64_t, RtpTimeDelta> {
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 <typename IntType>
constexpr RtpTimeDelta operator*(IntType rhs) const {
static_assert(std::numeric_limits<IntType>::is_integer,
"|rhs| must be a POD integer type");
return RtpTimeDelta(value_ * rhs);
}
template <typename IntType>
constexpr RtpTimeDelta operator/(IntType rhs) const {
static_assert(std::numeric_limits<IntType>::is_integer,
"|rhs| must be a POD integer type");
return RtpTimeDelta(value_ / rhs);
}
template <typename IntType>
constexpr RtpTimeDelta& operator*=(IntType rhs) {
return (*this = (*this * rhs));
}
template <typename IntType>
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 <typename Duration>
Duration ToDuration(int rtp_timebase) const {
OSP_CHECK_GT(rtp_timebase, 0);
constexpr Duration kOneSecond =
std::chrono::duration_cast<Duration>(std::chrono::seconds(1));
return Duration(ToNearestRepresentativeValue<typename Duration::rep>(
static_cast<double>(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 <typename Duration>
static constexpr RtpTimeDelta FromDuration(Duration duration,
int rtp_timebase) {
constexpr Duration kOneSecond =
std::chrono::duration_cast<Duration>(std::chrono::seconds(1));
static_assert(kOneSecond > Duration::zero(),
"Duration is too coarse-grained to represent one second.");
return RtpTimeDelta(ToNearestRepresentativeValue<int64_t>(
static_cast<double>(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<int64_t, RtpTimeDelta>;
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 <typename Rep>
static std::enable_if_t<std::is_floating_point<Rep>::value, Rep>
ToNearestRepresentativeValue(double ticks) {
return Rep(ticks);
}
template <typename Rep>
static std::enable_if_t<std::is_integral<Rep>::value, Rep>
ToNearestRepresentativeValue(double ticks) {
return rounded_saturate_cast<Rep>(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<microseconds>(kAudioSamplingRate);
// RtpTimeTicks at_three_seconds = RtpTimeDelta::FromTimeSinceOrigin(
// std::chrono::seconds(3), kAudioSamplingRate);
class RtpTimeTicks : public ExpandedValueBase<int64_t, RtpTimeTicks> {
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 <typename Duration>
Duration ToTimeSinceOrigin(int rtp_timebase) const {
return (*this - RtpTimeTicks()).ToDuration<Duration>(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 <typename Duration>
static constexpr RtpTimeTicks FromTimeSinceOrigin(Duration time_since_origin,
int rtp_timebase) {
return RtpTimeTicks() +
RtpTimeDelta::FromDuration(time_since_origin, rtp_timebase);
}
private:
friend class ExpandedValueBase<int64_t, RtpTimeTicks>;
friend std::ostream& operator<<(std::ostream& out, const RtpTimeTicks rhs);
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_RTP_TIME_H_

View file

@ -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 <utility>
#include <variant>
#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<SenderMessage::Type, 4> 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<SenderMessage::Type> parsed = GetEnum(kMessageTypeNames, type);
return parsed.value(SenderMessage::Type::kUnknown);
}
} // namespace
// static
ErrorOr<SenderMessage> 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<uint8_t> 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<uint8_t> 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<Json::Value> SenderMessage::ToJson() const {
OSP_CHECK(type != SenderMessage::Type::kUnknown)
<< "Trying to send an unknown message is a developer error";
Json::Value root;
ErrorOr<const char*> 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<Offer>(body).ToJson();
break;
case SenderMessage::Type::kRpc:
root[kRpcMessageBody] =
base64::Encode(std::get<std::vector<uint8_t>>(body));
break;
case SenderMessage::Type::kInput:
root[kInputMessageBody] =
base64::Encode(std::get<std::vector<uint8_t>>(body));
break;
case SenderMessage::Type::kGetCapabilities:
break;
default:
OSP_NOTREACHED();
}
return root;
}
} // namespace openscreen::cast

View file

@ -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 <memory>
#include <string>
#include <variant>
#include <vector>
#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<SenderMessage> Parse(const Json::Value& value);
ErrorOr<Json::Value> ToJson() const;
Type type = Type::kUnknown;
int32_t sequence_number = -1;
bool valid = false;
std::variant<std::monostate,
std::vector<uint8_t>, // Binary-encoded protobuf message.
Offer,
std::string>
body;
};
} // namespace openscreen::cast
#endif // CAST_STREAMING_SENDER_MESSAGE_H_

View file

@ -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 <algorithm>
#include <utility>
#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<uint8_t> 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<ApparentPacketType, Ssrc> 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<size_t>(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<int>(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

View file

@ -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 <stdint.h>
#include <chrono>
#include <memory>
#include <vector>
#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> 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<SenderEntry>;
// Environment::PacketConsumer implementation.
void OnReceivedPacket(const IPEndpoint& source,
Clock::time_point arrival_time,
std::vector<uint8_t> 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> environment_;
const int packet_buffer_size_;
const std::unique_ptr<uint8_t[]> 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_

View file

@ -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 <random>
#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<std::minstd_rand::result_type>(
Clock::now().time_since_epoch().count()));
std::uniform_int_distribution<int> distribution(
higher_priority ? kHigherPriorityMin : kNormalPriorityMin,
higher_priority ? kHigherPriorityMax : kNormalPriorityMax);
return static_cast<Ssrc>(distribution(generator));
}
int ComparePriority(Ssrc ssrc_a, Ssrc ssrc_b) {
return static_cast<int>(ssrc_a) - static_cast<int>(ssrc_b);
}
} // namespace openscreen::cast

Some files were not shown because too many files have changed in this diff Show more