├── .dockerignore ├── .editorconfig ├── .github ├── logo.svg └── workflows │ ├── pr.yml │ └── release.yml ├── .gitignore ├── .rustfmt.toml ├── .vscode └── extensions.json ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile ├── README.md ├── default.nix ├── deploy ├── fly-relay.sh ├── fly.toml └── publish ├── dev ├── .gitignore ├── README.md ├── cert ├── clock ├── go.mod ├── go.sum ├── pub ├── pub_multi_track ├── relay └── sub ├── docker-compose.yml ├── flake.lock ├── flake.nix ├── moq-api ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── client.rs │ ├── error.rs │ ├── lib.rs │ ├── main.rs │ ├── model.rs │ └── server.rs ├── moq-catalog ├── CHANGELOG.md ├── Cargo.toml └── src │ └── lib.rs ├── moq-clock-ietf ├── CHANGELOG.md ├── Cargo.toml └── src │ ├── cli.rs │ ├── clock.rs │ └── main.rs ├── moq-dir ├── CHANGELOG.md ├── Cargo.toml └── src │ ├── lib.rs │ ├── listing.rs │ ├── listings.rs │ ├── main.rs │ └── session.rs ├── moq-native-ietf ├── CHANGELOG.md ├── Cargo.toml └── src │ ├── lib.rs │ ├── quic.rs │ └── tls.rs ├── moq-pub ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── cli.rs │ ├── lib.rs │ ├── main.rs │ └── media.rs ├── moq-relay-ietf ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── api.rs │ ├── consumer.rs │ ├── local.rs │ ├── main.rs │ ├── producer.rs │ ├── relay.rs │ ├── remote.rs │ ├── session.rs │ ├── tls.rs │ └── web.rs ├── moq-sub ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── main.rs │ └── media.rs ├── moq-transport ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── README.md └── src │ ├── coding │ ├── decode.rs │ ├── encode.rs │ ├── mod.rs │ ├── params.rs │ ├── string.rs │ ├── tuple.rs │ └── varint.rs │ ├── data │ ├── datagram.rs │ ├── header.rs │ ├── mod.rs │ ├── object.rs │ ├── subgroup.rs │ └── track.rs │ ├── error.rs │ ├── lib.rs │ ├── message │ ├── announce.rs │ ├── announce_cancel.rs │ ├── announce_error.rs │ ├── announce_ok.rs │ ├── fetch.rs │ ├── fetch_cancel.rs │ ├── fetch_error.rs │ ├── fetch_ok.rs │ ├── filter_type.rs │ ├── go_away.rs │ ├── group_order.rs │ ├── max_subscribe_id.rs │ ├── mod.rs │ ├── publisher.rs │ ├── subscribe.rs │ ├── subscribe_done.rs │ ├── subscribe_error.rs │ ├── subscribe_namespace.rs │ ├── subscribe_namespace_error.rs │ ├── subscribe_namespace_ok.rs │ ├── subscribe_ok.rs │ ├── subscribe_update.rs │ ├── subscriber.rs │ ├── track_status.rs │ ├── track_status_request.rs │ ├── unannounce.rs │ ├── unsubscribe.rs │ └── unsubscribe_namespace.rs │ ├── serve │ ├── broadcast.rs │ ├── datagram.rs │ ├── error.rs │ ├── mod.rs │ ├── object.rs │ ├── stream.rs │ ├── subgroup.rs │ ├── track.rs │ └── tracks.rs │ ├── session │ ├── announce.rs │ ├── announced.rs │ ├── error.rs │ ├── mod.rs │ ├── publisher.rs │ ├── reader.rs │ ├── subscribe.rs │ ├── subscribed.rs │ ├── subscriber.rs │ ├── track_status_requested.rs │ └── writer.rs │ ├── setup │ ├── client.rs │ ├── mod.rs │ ├── role.rs │ ├── server.rs │ └── version.rs │ ├── util │ ├── mod.rs │ ├── queue.rs │ ├── state.rs │ └── watch.rs │ └── watch │ ├── mod.rs │ ├── queue.rs │ └── state.rs └── package.nix /.dockerignore: -------------------------------------------------------------------------------- 1 | target 2 | dev 3 | *.mp4 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_style = space 9 | indent_size = 4 10 | max_line_length = 120 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [*.yml] 16 | indent_style = space 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pr 2 | 3 | on: 4 | pull_request: 5 | branches: ["main"] 6 | 7 | env: 8 | CARGO_TERM_COLOR: always 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | # Install Rust with clippy/rustfmt 18 | - uses: actions-rust-lang/setup-rust-toolchain@v1 19 | with: 20 | components: clippy, rustfmt 21 | 22 | # Make sure u guys don't write bad code 23 | - run: cargo test --verbose 24 | - run: cargo clippy --no-deps 25 | - run: cargo fmt --check 26 | 27 | # Check for unused dependencies 28 | - uses: bnjbvr/cargo-machete@main 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | permissions: 4 | pull-requests: write 5 | contents: write 6 | 7 | on: 8 | push: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | release: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - name: Install Rust toolchain 21 | uses: dtolnay/rust-toolchain@stable 22 | - name: Run release-plz 23 | uses: MarcoIeni/release-plz-action@v0.5 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | target/ 3 | logs/ 4 | *.mp4 5 | result 6 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | hard_tabs = false 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "EditorConfig.EditorConfig", 4 | "rust-lang.rust-analyzer", 5 | ] 6 | } -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "moq-transport", 4 | "moq-relay-ietf", 5 | "moq-pub", 6 | "moq-sub", 7 | "moq-api", 8 | "moq-clock-ietf", 9 | "moq-dir", 10 | "moq-native-ietf", 11 | "moq-catalog", 12 | ] 13 | resolver = "2" 14 | 15 | [workspace.dependencies] 16 | web-transport = "0.3" 17 | env_logger = "0.11" 18 | log = { version = "0.4", features = ["std"] } 19 | 20 | # Use debug symbols in production until things are more stable 21 | [profile.release] 22 | debug = true 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:bookworm as builder 2 | 3 | # Create a build directory and copy over all of the files 4 | WORKDIR /build 5 | COPY . ./ 6 | 7 | # Reuse a cache between builds. 8 | # I tried to `cargo install`, but it doesn't seem to work with workspaces. 9 | # There's also issues with the cache mount since it builds into /usr/local/cargo/bin 10 | # We can't mount that without clobbering cargo itself. 11 | # We instead we build the binaries and copy them to the cargo bin directory. 12 | RUN --mount=type=cache,target=/usr/local/cargo/registry \ 13 | --mount=type=cache,target=/build/target \ 14 | cargo build --release && cp /build/target/release/moq-* /usr/local/cargo/bin 15 | 16 | # Create a pub image that also contains ffmpeg and a helper script 17 | FROM debian:bookworm-slim as moq-pub 18 | 19 | # Install required utilities and ffmpeg 20 | RUN apt-get update && \ 21 | apt-get install -y ffmpeg wget 22 | 23 | # Copy the publish script into the image 24 | COPY ./deploy/publish /usr/local/bin/publish 25 | 26 | # Copy over the built binaries. 27 | COPY --from=builder /usr/local/cargo/bin/moq-* /usr/local/bin 28 | 29 | # Use our publish script 30 | CMD [ "publish" ] 31 | 32 | # Create an image with just the binaries 33 | FROM debian:bookworm-slim 34 | 35 | RUN apt-get update && \ 36 | apt-get install -y --no-install-recommends ca-certificates curl libssl3 && \ 37 | rm -rf /var/lib/apt/lists/* 38 | 39 | LABEL org.opencontainers.image.source=https://github.com/kixelated/moq-rs 40 | LABEL org.opencontainers.image.licenses="MIT OR Apache-2.0" 41 | 42 | COPY --from=builder /usr/local/cargo/bin/moq-* /usr/local/bin 43 | 44 | # Entrypoint to load relay TLS config in Fly 45 | # TODO remove this; it should be specific to the fly deployment. 46 | COPY deploy/fly-relay.sh . 47 | 48 | # Default to moq-relay 49 | CMD ["moq-relay"] 50 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Luke Curley 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export CAROOT ?= $(shell cd dev ; go run filippo.io/mkcert -CAROOT) 2 | 3 | .PHONY: run 4 | run: dev/localhost.crt 5 | @docker compose up --build --remove-orphans 6 | 7 | dev/localhost.crt: 8 | @dev/cert 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Media over QUIC 3 |

4 | 5 | Media over QUIC (MoQ) is a live media delivery protocol utilizing QUIC streams. 6 | See [quic.video](https://quic.video) for more information. 7 | 8 | This repository contains a few crates: 9 | 10 | - **moq-relay**: Accepting content from publishers and serves it to any subscribers. 11 | - **moq-pub**: Publishes fMP4 broadcasts. 12 | - **moq-transport**: An implementation of the underlying MoQ protocol. 13 | - **moq-api**: A HTTP API server that stores the origin for each broadcast, backed by redis. 14 | - **moq-dir**: Aggregates announcements, used to discover broadcasts. 15 | - **moq-clock**: A dumb clock client/server just to prove MoQ is more than media. 16 | 17 | There's currently no way to view media with this repo; you'll need to use [moq-js](https://github.com/kixelated/moq-js) for that. 18 | A hosted version is available at [quic.video](https://quic.video) and accepts the `?host=localhost:4443` query parameter. 19 | 20 | # Development 21 | 22 | Launch a basic cluster, including provisioning certs and deploying root certificates: 23 | 24 | ``` 25 | make run 26 | ``` 27 | 28 | Then, visit https://quic.video/publish/?server=localhost:4443. 29 | 30 | For more control, use the [dev helper scripts](dev/README.md). 31 | 32 | # Usage 33 | 34 | ## moq-relay 35 | 36 | [moq-relay](moq-relay) is a server that forwards subscriptions from publishers to subscribers, caching and deduplicating along the way. 37 | It's designed to be run in a datacenter, relaying media across multiple hops to deduplicate and improve QoS. 38 | The relays optionally register themselves via the [moq-api](moq-api) endpoints, which is used to discover other relays and share broadcasts. 39 | 40 | Notable arguments: 41 | 42 | - `--bind ` Listen on this address, default: `[::]:4443` 43 | - `--tls-cert ` Use the certificate file at this path 44 | - `--tls-key ` Use the private key at this path 45 | - `--announce ` Forward all announcements to this instance, typically [moq-dir](moq-dir). 46 | 47 | This listens for WebTransport connections on `UDP https://localhost:4443` by default. 48 | You need a client to connect to that address, to both publish and consume media. 49 | 50 | ## moq-pub 51 | 52 | A client that publishes a fMP4 stream over MoQ, with a few restrictions. 53 | 54 | - `separate_moof`: Each fragment must contain a single track. 55 | - `frag_keyframe`: A keyframe must be at the start of each keyframe. 56 | - `fragment_per_frame`: (optional) Each frame should be a separate fragment to minimize latency. 57 | 58 | This client can currently be used in conjuction with either ffmpeg or gstreamer. 59 | 60 | ### ffmpeg 61 | 62 | moq-pub can be run as a binary, accepting a stream (from ffmpeg via stdin) and publishing it to the given relay. 63 | See [dev/pub](dev/pub) for the required ffmpeg flags. 64 | 65 | ### gstreamer 66 | 67 | moq-pub can also be run as a library, currently used for a [gstreamer plugin](https://github.com/kixelated/moq-gst). 68 | This is in a separate repository to avoid gstreamer being a hard requirement. 69 | See [run](https://github.com/kixelated/moq-gst/blob/main/run) for an example pipeline. 70 | 71 | ## moq-transport 72 | 73 | A media-agnostic library used by [moq-relay](moq-relay) and [moq-pub](moq-pub) to serve the underlying subscriptions. 74 | It has caching/deduplication built-in, so your application is oblivious to the number of connections under the hood. 75 | 76 | See the published [crate](https://crates.io/crates/moq-transport) and [documentation](https://docs.rs/moq-transport/latest/moq_transport/). 77 | 78 | ## moq-clock 79 | 80 | [moq-clock](moq-clock) is a simple client that can publish or subscribe to the current time. 81 | It's meant to demonstate that [moq-transport](moq-transport) can be used for more than just media. 82 | 83 | ## moq-dir 84 | 85 | [moq-dir](moq-dir) is a server that aggregates announcements. 86 | It produces tracks based on the prefix, which are subscribable and can be used to discover broadcasts. 87 | 88 | For example, if a client announces the broadcast `.public.room.12345.alice`, then `moq-dir` will produce the following track: 89 | 90 | ``` 91 | TRACK namespace=. track=public.room.12345. 92 | OBJECT +alice 93 | ``` 94 | 95 | Use the `--announce ` flag when running the relay to forward all announcements to the instance. 96 | 97 | ## moq-api 98 | 99 | This is a API server that exposes a REST API. 100 | It's used by relays to inserts themselves as origins when publishing, and to find the origin when subscribing. 101 | It's basically just a thin wrapper around redis that is only needed to run multiple relays in a (simple) cluster. 102 | 103 | # License 104 | 105 | Licensed under either: 106 | 107 | - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 108 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 109 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs ? import { }, 3 | }: 4 | rec { 5 | moq-rs = pkgs.callPackage ./package.nix { }; 6 | default = moq-rs; 7 | publish = pkgs.dockerTools.buildLayeredImage { 8 | name = "moq-pub"; 9 | tag = "latest"; 10 | contents = pkgs.buildEnv { 11 | name = "image-root"; 12 | paths = with pkgs; [ 13 | bashInteractive 14 | coreutils 15 | ffmpeg 16 | wget 17 | moq-rs 18 | ]; 19 | pathsToLink = [ "/bin" ]; 20 | }; 21 | config = { 22 | Entrypoint = [ "bash" ]; 23 | Cmd = [ deploy/publish ]; 24 | }; 25 | }; 26 | relay = pkgs.dockerTools.buildLayeredImage { 27 | name = "moq-relay"; 28 | tag = "latest"; 29 | contents = pkgs.buildEnv { 30 | name = "image-root"; 31 | paths = with pkgs; [ 32 | bashInteractive 33 | coreutils 34 | curl 35 | dockerTools.caCertificates 36 | moq-rs 37 | ]; 38 | pathsToLink = [ 39 | "/bin" 40 | "/etc" 41 | ]; 42 | }; 43 | config = { 44 | Entrypoint = [ "bash" ]; 45 | Cmd = [ "moq-relay" ]; 46 | }; 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /deploy/fly-relay.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | mkdir cert 4 | # Nothing to see here... 5 | echo "$MOQ_CRT" | base64 -d > dev/moq-demo.crt 6 | echo "$MOQ_KEY" | base64 -d > dev/moq-demo.key 7 | 8 | RUST_LOG=info moq-relay --tls-cert dev/moq-demo.crt --tls-key dev/moq-demo.key 9 | -------------------------------------------------------------------------------- /deploy/fly.toml: -------------------------------------------------------------------------------- 1 | app = "englishm-moq-relay" 2 | kill_signal = "SIGINT" 3 | kill_timeout = 5 4 | 5 | [env] 6 | PORT = "4443" 7 | 8 | [experimental] 9 | cmd = "./fly-relay.sh" 10 | 11 | [[services]] 12 | internal_port = 4443 13 | protocol = "udp" 14 | 15 | [services.concurrency] 16 | hard_limit = 25 17 | soft_limit = 20 18 | 19 | [[services.ports]] 20 | port = "4443" 21 | -------------------------------------------------------------------------------- /deploy/publish: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | ADDR=${ADDR:-"https://relay.quic.video"} 5 | NAME=${NAME:-"bbb"} 6 | URL=${URL:-"http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"} 7 | REGION=${REGION:-"server"} 8 | 9 | # Download the funny bunny 10 | wget -nv "${URL}" -O "tmp.mp4" 11 | 12 | # Properly fragment the file 13 | ffmpeg -i tmp.mp4 \ 14 | -c copy \ 15 | -f mp4 -movflags cmaf+separate_moof+delay_moov+skip_trailer+frag_every_frame \ 16 | "${NAME}.mp4" 17 | 18 | rm tmp.mp4 19 | 20 | # ffmpeg 21 | # -hide_banner: Hide the banner 22 | # -v quiet: and any other output 23 | # -stats: But we still want some stats on stderr 24 | # -stream_loop -1: Loop the broadcast an infinite number of times 25 | # -re: Output in real-time 26 | # -i "${INPUT}": Read from a file on disk 27 | # -vf "drawtext": Render the current time in the corner of the video 28 | # -an: Disable audio for now 29 | # -b:v 3M: Output video at 3Mbps 30 | # -preset ultrafast: Don't use much CPU at the cost of quality 31 | # -tune zerolatency: Optimize for latency at the cost of quality 32 | # -f mp4: Output to mp4 format 33 | # -movflags: Build a fMP4 file with a frame per fragment 34 | # - | moq-pub: Output to stdout and moq-pub to publish 35 | 36 | # Run ffmpeg 37 | ffmpeg \ 38 | -stream_loop -1 \ 39 | -hide_banner \ 40 | -v quiet \ 41 | -re \ 42 | -i "${NAME}.mp4" \ 43 | -vf "drawtext=fontfile=/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf:text='${REGION}\: %{gmtime\: %H\\\\\:%M\\\\\:%S.%3N}':x=(W-tw)-24:y=24:fontsize=48:fontcolor=white:box=1:boxcolor=black@0.5" \ 44 | -an \ 45 | -b:v 3M \ 46 | -preset ultrafast \ 47 | -tune zerolatency \ 48 | -f mp4 \ 49 | -movflags cmaf+separate_moof+delay_moov+skip_trailer+frag_every_frame \ 50 | - | moq-pub "${ADDR}" --name "${NAME}" 51 | -------------------------------------------------------------------------------- /dev/.gitignore: -------------------------------------------------------------------------------- 1 | *.crt 2 | *.key 3 | *.hex 4 | *.mp4 5 | *.fmp4 6 | -------------------------------------------------------------------------------- /dev/README.md: -------------------------------------------------------------------------------- 1 | # Local Development 2 | 3 | This is a collection of helpful scripts for local development. 4 | Check the source for each script for a list of useful arguments. 5 | 6 | ## moq-relay 7 | 8 | Hosts a [relay server](../moq-relay) on `localhost:4443` using self-signed certificates. 9 | 10 | Requires: 11 | 12 | - Rust 13 | - Go (mkcert) 14 | 15 | ```bash 16 | ./dev/relay 17 | ``` 18 | 19 | All clients listed can connect to this relay instance to publish and/or subscribe. 20 | You can do this via [moq-js](https://github.com/kixelated/moq-js) for a UI, either self-hosted or accessed via https://quic.video/publish/?server=localhost:4443. 21 | 22 | The purpose of a relay is to support clustering and fanout. 23 | Like mentioned in the root README, the easiest way to do this is via docker compose: 24 | 25 | ```bash 26 | make run 27 | ``` 28 | 29 | This hosts a Redis instance and [moq-api](../moq-api) instance to store the list of origins. 30 | It also hosts a [moq-dir](../moq-dir) instance to serve the current announcements. 31 | 32 | ## moq-pub 33 | 34 | Publish some test footage from disk to the localhost relay using [moq-pub](../moq-pub). 35 | This downloads Big Buck Bunny and publishes a broadcast named `bbb`. 36 | 37 | Requires: 38 | 39 | - Rust 40 | - ffmpeg 41 | 42 | ```bash 43 | ./dev/pub 44 | ``` 45 | 46 | Alternatively, you can use Gstreamer via [moq-gst](https://github.com/kixelated/moq-gst). 47 | The [run](https://github.com/kixelated/moq-gst/blob/main/run) script does the exact same thing. 48 | 49 | ## moq-sub 50 | 51 | You can use `moq-sub` to subscribe to media streams from a MoQ relay and pipe them to the standard output. 52 | By piping the command to a video player, e.g. `ffplay` or `mpv`, you can play a MoQ broadcast natively. 53 | 54 | Currently, `moq-sub` simply dumps all received segments of the first video and the first audio track 55 | directly to `stdout`. 56 | 57 | The following command subscribes to a stream from a MoQ relay and plays it with `ffplay`. 58 | By default, the URL is `https://localhost:4433/dev`, so it will play the stream published with `dev/pub` 59 | to the relay started with `dev/relay`. You can change the broadcast name by setting the `NAME` env var. 60 | 61 | ```bash 62 | ./dev/sub 63 | ``` 64 | 65 | ## moq-clock 66 | 67 | To show that MoQ can do more than just media, we made a simple clock. 68 | 69 | ```bash 70 | ./dev/clock --publish 71 | ``` 72 | 73 | And run the subscriber in a separate terminal: 74 | 75 | ```bash 76 | ./dev/clock 77 | ``` 78 | -------------------------------------------------------------------------------- /dev/cert: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | cd "$(dirname "${BASH_SOURCE[0]}")" 5 | 6 | # Generate a new RSA key/cert for local development 7 | HOST="localhost" 8 | CRT="$HOST.crt" 9 | KEY="$HOST.key" 10 | 11 | # Install the system certificate if it's not already 12 | # NOTE: The ecdsa flag does nothing but I wish it did 13 | go run filippo.io/mkcert -ecdsa -install 14 | 15 | # Generate a new certificate for localhost 16 | # This fork of mkcert supports the -days flag. 17 | # TODO remove the -days flag when Chrome accepts self-signed certs. 18 | go run filippo.io/mkcert -ecdsa -days 10 -cert-file "$CRT" -key-file "$KEY" localhost 127.0.0.1 ::1 19 | -------------------------------------------------------------------------------- /dev/clock: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Change directory to the root of the project 5 | cd "$(dirname "$0")/.." 6 | 7 | # Use debug logging by default 8 | export RUST_LOG="${RUST_LOG:-debug}" 9 | 10 | # Connect to localhost by default. 11 | HOST="${HOST:-localhost}" 12 | PORT="${PORT:-4443}" 13 | ADDR="${ADDR:-$HOST:$PORT}" 14 | SCHEME="${SCHEME:-https}" 15 | 16 | # Combine the host and name into a URL. 17 | URL="${URL:-"$SCHEME://$ADDR"}" 18 | 19 | cargo run --bin moq-clock-ietf -- "$URL" "$@" 20 | -------------------------------------------------------------------------------- /dev/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kixelated/warp/cert 2 | 3 | go 1.18 4 | 5 | require ( 6 | filippo.io/mkcert v1.4.4 // indirect 7 | golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 // indirect 8 | golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 // indirect 9 | golang.org/x/text v0.3.7 // indirect 10 | howett.net/plist v1.0.0 // indirect 11 | software.sslmate.com/src/go-pkcs12 v0.2.0 // indirect 12 | ) 13 | 14 | replace filippo.io/mkcert => github.com/kixelated/mkcert v1.4.4-days 15 | -------------------------------------------------------------------------------- /dev/go.sum: -------------------------------------------------------------------------------- 1 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 2 | github.com/kixelated/mkcert v1.4.4-days h1:T2P9W4ruEfgLHOl5UljPwh0d79FbFWkSe2IONcUBxG8= 3 | github.com/kixelated/mkcert v1.4.4-days/go.mod h1:VyvOchVuAye3BoUsPUOOofKygVwLV2KQMVFJNRq+1dA= 4 | golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= 5 | golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 6 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 7 | golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 h1:yssD99+7tqHWO5Gwh81phT+67hg+KttniBr6UnEXOY8= 8 | golang.org/x/net v0.0.0-20220421235706-1d1ef9303861/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 9 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 10 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 11 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 12 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 13 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 14 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 15 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 16 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg= 19 | howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= 20 | howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= 21 | software.sslmate.com/src/go-pkcs12 v0.2.0 h1:nlFkj7bTysH6VkC4fGphtjXRbezREPgrHuJG20hBGPE= 22 | software.sslmate.com/src/go-pkcs12 v0.2.0/go.mod h1:23rNcYsMabIc1otwLpTkCCPwUq6kQsTyowttG/as0kQ= 23 | -------------------------------------------------------------------------------- /dev/pub: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Change directory to the root of the project 5 | cd "$(dirname "$0")/.." 6 | 7 | # Download the Big Buck Bunny video if it doesn't exist 8 | if [ ! -f dev/bbb.fmp4 ]; then 9 | if [ ! -f dev/bbb.mp4 ]; then 10 | echo "Downloading ya boye Big Buck Bunny..." 11 | wget http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4 -O dev/bbb.mp4 12 | fi 13 | 14 | echo "Converting to a (properly) fragmented MP4..." 15 | ffmpeg -i dev/bbb.mp4 \ 16 | -c copy \ 17 | -f mp4 -movflags cmaf+separate_moof+delay_moov+skip_trailer+frag_every_frame \ 18 | dev/bbb.fmp4 19 | fi 20 | 21 | # Use debug logging by default 22 | export RUST_LOG="${RUST_LOG:-debug}" 23 | 24 | # Connect to localhost by default. 25 | HOST="${HOST:-localhost}" 26 | PORT="${PORT:-4443}" 27 | ADDR="${ADDR:-$HOST:$PORT}" 28 | SCHEME="${SCHEME:-https}" 29 | 30 | # Use the name "bbb" for the broadcast. 31 | NAME="${NAME:-bbb}" 32 | 33 | # Combine the host into a URL. 34 | URL="${URL:-"$SCHEME://$ADDR"}" 35 | 36 | # Default to a source video 37 | INPUT="${INPUT:-dev/bbb.fmp4}" 38 | 39 | # Print out the watch URL 40 | echo "Watch URL: https://quic.video/watch/$NAME?server=$ADDR" 41 | 42 | # Run ffmpeg and pipe the output to moq-pub 43 | ffmpeg -hide_banner -v quiet \ 44 | -stream_loop -1 -re \ 45 | -i "$INPUT" \ 46 | -c copy \ 47 | -f mp4 -movflags cmaf+separate_moof+delay_moov+skip_trailer+frag_every_frame \ 48 | - | cargo run --bin moq-pub -- --name "$NAME" "$URL" "$@" 49 | -------------------------------------------------------------------------------- /dev/relay: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Change directory to the root of the project 5 | cd "$(dirname "$0")/.." 6 | 7 | # Generate the self-signed certificate if needed 8 | ./dev/cert 9 | 10 | # Use debug logging by default 11 | export RUST_LOG="${RUST_LOG:-debug}" 12 | 13 | # Default to a self-signed certificate 14 | # TODO automatically generate if it doesn't exist. 15 | CERT="${CERT:-dev/localhost.crt}" 16 | KEY="${KEY:-dev/localhost.key}" 17 | 18 | # Default to listening on localhost:4443 19 | PORT="${PORT:-4443}" 20 | BIND="${BIND:-[::]:$PORT}" 21 | 22 | # A list of optional args 23 | ARGS="" 24 | 25 | # Connect to the given URL to get announcements 26 | # TODO default to a public instance? 27 | if [ -n "${ANNOUNCE-}" ]; then 28 | ARGS="$ARGS --announce $ANNOUNCE" 29 | fi 30 | 31 | # Provide our node URL when registering origins. 32 | if [ -n "${HOST-}" ]; then 33 | ARGS="$ARGS --host $HOST" 34 | fi 35 | 36 | echo "Publish URL: https://quic.video/publish/?server=localhost:$PORT" 37 | 38 | # Run the relay and forward any arguments 39 | cargo run --bin moq-relay-ietf -- --bind "$BIND" --tls-cert "$CERT" --tls-key "$KEY" --dev $ARGS -- "$@" 40 | -------------------------------------------------------------------------------- /dev/sub: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | 4 | # Change directory to the root of the project 5 | cd "$(dirname "$0")/.." 6 | 7 | # Use debug logging by default 8 | export RUST_LOG="${RUST_LOG:-debug}" 9 | 10 | # Connect to localhost by default. 11 | HOST="${HOST:-localhost}" 12 | PORT="${PORT:-4443}" 13 | ADDR="${ADDR:-$HOST:$PORT}" 14 | 15 | # Use the broadcast name "bbb" by default 16 | NAME="${NAME:-bbb}" 17 | 18 | # Combine the host and name into a URL. 19 | URL="${URL:-"https://$ADDR/$NAME"}" 20 | 21 | cargo run --bin moq-sub -- --name "$NAME" "$URL" "$@" | ffplay - 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | x-moq: &x-moq 2 | build: . 3 | environment: 4 | RUST_LOG: ${RUST_LOG:-debug} 5 | volumes: 6 | - ./dev/localhost.crt:/etc/tls/cert:ro 7 | - ./dev/localhost.key:/etc/tls/key:ro 8 | - certs:/etc/ssl/certs 9 | depends_on: 10 | install-certs: 11 | condition: service_completed_successfully 12 | 13 | services: 14 | redis: 15 | image: redis:7 16 | ports: 17 | - "6379" 18 | 19 | api: 20 | <<: *x-moq 21 | entrypoint: moq-api 22 | command: --redis redis://redis:6379 23 | ports: 24 | - "80" 25 | 26 | relay1: 27 | <<: *x-moq 28 | entrypoint: moq-relay-ietf 29 | command: --tls-cert /etc/tls/cert --tls-key /etc/tls/key --tls-disable-verify --api http://api --node https://relay1 --dev --announce https://dir 30 | depends_on: 31 | - api 32 | - dir 33 | ports: 34 | - "4443:443" 35 | - "4443:443/udp" 36 | 37 | relay2: 38 | <<: *x-moq 39 | entrypoint: moq-relay-ietf 40 | command: --tls-cert /etc/tls/cert --tls-key /etc/tls/key --tls-disable-verify --api http://api --node https://relay2 --dev --announce https://dir 41 | depends_on: 42 | - api 43 | - dir 44 | ports: 45 | - "4444:443" 46 | - "4444:443/udp" 47 | 48 | dir: 49 | <<: *x-moq 50 | entrypoint: moq-dir 51 | command: --tls-cert /etc/tls/cert --tls-key /etc/tls/key 52 | ports: 53 | - "443/udp" 54 | 55 | install-certs: 56 | image: golang:latest 57 | working_dir: /work 58 | command: go run filippo.io/mkcert -install 59 | environment: 60 | CAROOT: /work/caroot 61 | volumes: 62 | - ${CAROOT:-.}:/work/caroot 63 | - certs:/etc/ssl/certs 64 | - ./dev/go.mod:/work/go.mod:ro 65 | - ./dev/go.sum:/work/go.sum:ro 66 | 67 | volumes: 68 | certs: 69 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "nixpkgs": { 4 | "locked": { 5 | "lastModified": 1729850857, 6 | "narHash": "sha256-WvLXzNNnnw+qpFOmgaM3JUlNEH+T4s22b5i2oyyCpXE=", 7 | "owner": "NixOS", 8 | "repo": "nixpkgs", 9 | "rev": "41dea55321e5a999b17033296ac05fe8a8b5a257", 10 | "type": "github" 11 | }, 12 | "original": { 13 | "owner": "NixOS", 14 | "ref": "nixpkgs-unstable", 15 | "repo": "nixpkgs", 16 | "type": "github" 17 | } 18 | }, 19 | "root": { 20 | "inputs": { 21 | "nixpkgs": "nixpkgs" 22 | } 23 | } 24 | }, 25 | "root": "root", 26 | "version": 7 27 | } 28 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; 3 | outputs = 4 | { nixpkgs, ... }: 5 | let 6 | supportedSystems = [ 7 | "x86_64-linux" 8 | "x86_64-darwin" 9 | "aarch64-linux" 10 | "aarch64-darwin" 11 | ]; 12 | forAllSystems = nixpkgs.lib.genAttrs supportedSystems; 13 | in 14 | { 15 | packages = forAllSystems ( 16 | system: 17 | import ./default.nix { 18 | pkgs = nixpkgs.legacyPackages.${system}; 19 | } 20 | ); 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /moq-api/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.2.3](https://github.com/englishm/moq-rs/compare/moq-api-v0.2.2...moq-api-v0.2.3) - 2025-01-16 10 | 11 | ### Other 12 | 13 | - cargo fmt 14 | 15 | ## [0.2.2](https://github.com/englishm/moq-rs/compare/moq-api-v0.2.1...moq-api-v0.2.2) - 2024-10-23 16 | 17 | ### Other 18 | 19 | - Update repository URLs for all crates 20 | 21 | ## [0.2.1](https://github.com/kixelated/moq-rs/compare/moq-api-v0.2.0...moq-api-v0.2.1) - 2024-10-01 22 | 23 | ### Other 24 | 25 | - really small correction with url.join ([#181](https://github.com/kixelated/moq-rs/pull/181)) 26 | 27 | ## [0.1.2](https://github.com/kixelated/moq-rs/compare/moq-api-v0.1.1...moq-api-v0.1.2) - 2024-07-24 28 | 29 | ### Other 30 | - update Cargo.lock dependencies 31 | 32 | ## [0.1.1](https://github.com/kixelated/moq-rs/compare/moq-api-v0.1.0...moq-api-v0.1.1) - 2024-06-03 33 | 34 | ### Other 35 | - Add an index server (moq-dir) that lists all announcements ([#160](https://github.com/kixelated/moq-rs/pull/160)) 36 | -------------------------------------------------------------------------------- /moq-api/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moq-api" 3 | description = "Media over QUIC" 4 | authors = ["Luke Curley"] 5 | repository = "https://github.com/englishm/moq-rs" 6 | license = "MIT OR Apache-2.0" 7 | 8 | version = "0.2.3" 9 | edition = "2021" 10 | 11 | keywords = ["quic", "http3", "webtransport", "media", "live"] 12 | categories = ["multimedia", "network-programming", "web-programming"] 13 | 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | # HTTP server 19 | axum = "0.7" 20 | hyper = { version = "1.4", features = ["full"] } 21 | tokio = { version = "1", features = ["full"] } 22 | 23 | # HTTP client 24 | reqwest = { version = "0.12", features = ["json", "rustls-tls"] } 25 | 26 | # JSON encoding 27 | serde = "1" 28 | serde_json = "1" 29 | 30 | # CLI 31 | clap = { version = "4", features = ["derive"] } 32 | 33 | # Database 34 | redis = { version = "0.25", features = [ 35 | "tokio-rustls-comp", 36 | "connection-manager", 37 | ] } 38 | url = { version = "2", features = ["serde"] } 39 | 40 | # Error handling 41 | log = { workspace = true } 42 | env_logger = { workspace = true } 43 | thiserror = "1" 44 | -------------------------------------------------------------------------------- /moq-api/README.md: -------------------------------------------------------------------------------- 1 | # moq-api 2 | 3 | A thin HTTP API that wraps Redis. 4 | Basically I didn't want the relays connecting to Redis directly. 5 | -------------------------------------------------------------------------------- /moq-api/src/client.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | 3 | use crate::{ApiError, Origin}; 4 | 5 | #[derive(Clone)] 6 | pub struct Client { 7 | // The address of the moq-api server 8 | url: Url, 9 | 10 | client: reqwest::Client, 11 | } 12 | 13 | impl Client { 14 | pub fn new(url: Url) -> Self { 15 | let client = reqwest::Client::new(); 16 | Self { url, client } 17 | } 18 | 19 | pub async fn get_origin(&self, namespace: &str) -> Result, ApiError> { 20 | let url = self.url.join(&format!("origin/{namespace}"))?; 21 | let resp = self.client.get(url).send().await?; 22 | if resp.status() == reqwest::StatusCode::NOT_FOUND { 23 | return Ok(None); 24 | } 25 | 26 | let origin: Origin = resp.json().await?; 27 | Ok(Some(origin)) 28 | } 29 | 30 | pub async fn set_origin(&self, namespace: &str, origin: Origin) -> Result<(), ApiError> { 31 | let url = self.url.join(&format!("origin/{namespace}"))?; 32 | 33 | let resp = self.client.post(url).json(&origin).send().await?; 34 | resp.error_for_status()?; 35 | 36 | Ok(()) 37 | } 38 | 39 | pub async fn delete_origin(&self, namespace: &str) -> Result<(), ApiError> { 40 | let url = self.url.join(&format!("origin/{namespace}"))?; 41 | 42 | let resp = self.client.delete(url).send().await?; 43 | resp.error_for_status()?; 44 | 45 | Ok(()) 46 | } 47 | 48 | pub async fn patch_origin(&self, namespace: &str, origin: Origin) -> Result<(), ApiError> { 49 | let url = self.url.join(&format!("origin/{namespace}"))?; 50 | 51 | let resp = self.client.patch(url).json(&origin).send().await?; 52 | resp.error_for_status()?; 53 | 54 | Ok(()) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /moq-api/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum ApiError { 5 | #[error("redis error: {0}")] 6 | Redis(#[from] redis::RedisError), 7 | 8 | #[error("reqwest error: {0}")] 9 | Request(#[from] reqwest::Error), 10 | 11 | #[error("hyper error: {0}")] 12 | Hyper(#[from] hyper::Error), 13 | 14 | #[error("url error: {0}")] 15 | Url(#[from] url::ParseError), 16 | 17 | #[error("io error: {0}")] 18 | Io(#[from] std::io::Error), 19 | } 20 | -------------------------------------------------------------------------------- /moq-api/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod client; 2 | mod error; 3 | mod model; 4 | 5 | pub use client::*; 6 | pub use error::*; 7 | pub use model::*; 8 | -------------------------------------------------------------------------------- /moq-api/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | mod server; 4 | use moq_api::ApiError; 5 | use server::{Server, ServerConfig}; 6 | 7 | #[tokio::main] 8 | async fn main() -> Result<(), ApiError> { 9 | env_logger::init(); 10 | 11 | let config = ServerConfig::parse(); 12 | let server = Server::new(config); 13 | server.run().await 14 | } 15 | -------------------------------------------------------------------------------- /moq-api/src/model.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | use url::Url; 4 | 5 | #[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] 6 | pub struct Origin { 7 | pub url: Url, 8 | } 9 | -------------------------------------------------------------------------------- /moq-catalog/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.2.2](https://github.com/englishm/moq-rs/compare/moq-catalog-v0.2.1...moq-catalog-v0.2.2) - 2025-01-16 11 | 12 | ### Other 13 | 14 | - cargo fmt 15 | 16 | ## [0.2.1](https://github.com/englishm/moq-rs/compare/moq-catalog-v0.2.0...moq-catalog-v0.2.1) - 2024-10-23 17 | 18 | ### Other 19 | 20 | - Update repository URLs for all crates 21 | -------------------------------------------------------------------------------- /moq-catalog/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moq-catalog" 3 | description = "Media over QUIC" 4 | authors = ["Luke Curley"] 5 | repository = "https://github.com/englishm/moq-rs" 6 | license = "MIT OR Apache-2.0" 7 | 8 | version = "0.2.2" 9 | edition = "2021" 10 | 11 | keywords = ["quic", "http3", "webtransport", "media", "live"] 12 | categories = ["multimedia", "network-programming", "web-programming"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | serde = { version = "1", features = ["derive"] } 18 | -------------------------------------------------------------------------------- /moq-clock-ietf/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.6.4](https://github.com/englishm/moq-rs/compare/moq-clock-ietf-v0.6.3...moq-clock-ietf-v0.6.4) - 2025-02-24 10 | 11 | ### Other 12 | 13 | - updated the following local packages: moq-transport 14 | 15 | ## [0.6.3](https://github.com/englishm/moq-rs/compare/moq-clock-ietf-v0.6.2...moq-clock-ietf-v0.6.3) - 2025-01-16 16 | 17 | ### Other 18 | 19 | - cargo fmt 20 | - Change type of namespace to tuple 21 | - Remove object/stream (gone in -06) 22 | - cargo fmt 23 | - s/group/subgroup/g 24 | 25 | ## [0.6.2](https://github.com/englishm/moq-rs/compare/moq-clock-ietf-v0.6.1...moq-clock-ietf-v0.6.2) - 2024-10-31 26 | 27 | ### Other 28 | 29 | - updated the following local packages: moq-transport 30 | 31 | ## [0.6.1](https://github.com/englishm/moq-rs/compare/moq-clock-ietf-v0.6.0...moq-clock-ietf-v0.6.1) - 2024-10-31 32 | 33 | ### Other 34 | 35 | - release 36 | 37 | ## [0.6.0](https://github.com/englishm/moq-rs/releases/tag/moq-clock-ietf-v0.6.0) - 2024-10-23 38 | 39 | ### Other 40 | 41 | - Update repository URLs for all crates 42 | - Rename crate 43 | 44 | ## [0.5.1](https://github.com/kixelated/moq-rs/compare/moq-clock-v0.5.0...moq-clock-v0.5.1) - 2024-10-01 45 | 46 | ### Other 47 | 48 | - update Cargo.lock dependencies 49 | 50 | ## [0.4.2](https://github.com/kixelated/moq-rs/compare/moq-clock-v0.4.1...moq-clock-v0.4.2) - 2024-07-24 51 | 52 | ### Other 53 | - update Cargo.lock dependencies 54 | 55 | ## [0.4.1](https://github.com/kixelated/moq-rs/compare/moq-clock-v0.4.0...moq-clock-v0.4.1) - 2024-06-03 56 | 57 | ### Other 58 | - Initial gstreamer support ([#163](https://github.com/kixelated/moq-rs/pull/163)) 59 | - Add an index server (moq-dir) that lists all announcements ([#160](https://github.com/kixelated/moq-rs/pull/160)) 60 | -------------------------------------------------------------------------------- /moq-clock-ietf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moq-clock-ietf" 3 | description = "CLOCK over QUIC" 4 | authors = ["Luke Curley"] 5 | repository = "https://github.com/englishm/moq-rs" 6 | license = "MIT OR Apache-2.0" 7 | 8 | version = "0.6.4" 9 | edition = "2021" 10 | 11 | keywords = ["quic", "http3", "webtransport", "media", "live"] 12 | categories = ["multimedia", "network-programming", "web-programming"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | moq-native-ietf = { path = "../moq-native-ietf", version = "0.5" } 18 | moq-transport = { path = "../moq-transport", version = "0.10" } 19 | 20 | # QUIC 21 | url = "2" 22 | 23 | # Async stuff 24 | tokio = { version = "1", features = ["full"] } 25 | 26 | # CLI, logging, error handling 27 | clap = { version = "4", features = ["derive"] } 28 | log = { workspace = true } 29 | env_logger = { workspace = true } 30 | anyhow = { version = "1", features = ["backtrace"] } 31 | tracing = "0.1" 32 | tracing-subscriber = "0.3" 33 | 34 | # CLOCK STUFF 35 | chrono = "0.4" 36 | -------------------------------------------------------------------------------- /moq-clock-ietf/src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::{net, path}; 3 | use url::Url; 4 | 5 | #[derive(Parser, Clone, Debug)] 6 | pub struct Config { 7 | /// Listen for UDP packets on the given address. 8 | #[arg(long, default_value = "[::]:0")] 9 | pub bind: net::SocketAddr, 10 | 11 | /// Connect to the given URL starting with https:// 12 | #[arg(value_parser = moq_url)] 13 | pub url: Url, 14 | 15 | /// Use the TLS root CA at this path, encoded as PEM. 16 | /// 17 | /// This value can be provided multiple times for multiple roots. 18 | /// If this is empty, system roots will be used instead 19 | #[arg(long)] 20 | pub tls_root: Vec, 21 | 22 | /// Danger: Disable TLS certificate verification. 23 | /// 24 | /// Fine for local development, but should be used in caution in production. 25 | #[arg(long)] 26 | pub tls_disable_verify: bool, 27 | 28 | /// Publish the current time to the relay, otherwise only subscribe. 29 | #[arg(long)] 30 | pub publish: bool, 31 | 32 | /// The name of the clock track. 33 | #[arg(long, default_value = "clock")] 34 | pub namespace: String, 35 | 36 | /// The name of the clock track. 37 | #[arg(long, default_value = "now")] 38 | pub track: String, 39 | } 40 | 41 | fn moq_url(s: &str) -> Result { 42 | Url::try_from(s).map_err(|e| e.to_string()) 43 | } 44 | -------------------------------------------------------------------------------- /moq-clock-ietf/src/clock.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use moq_transport::serve::{ 3 | DatagramsReader, StreamReader, Subgroup, SubgroupWriter, SubgroupsReader, SubgroupsWriter, 4 | TrackReader, TrackReaderMode, 5 | }; 6 | 7 | use chrono::prelude::*; 8 | 9 | pub struct Publisher { 10 | track: SubgroupsWriter, 11 | } 12 | 13 | impl Publisher { 14 | pub fn new(track: SubgroupsWriter) -> Self { 15 | Self { track } 16 | } 17 | 18 | pub async fn run(mut self) -> anyhow::Result<()> { 19 | let start = Utc::now(); 20 | let mut now = start; 21 | 22 | // Just for fun, don't start at zero. 23 | let mut sequence = start.minute(); 24 | 25 | loop { 26 | let segment = self 27 | .track 28 | .create(Subgroup { 29 | group_id: sequence as u64, 30 | subgroup_id: 0, 31 | priority: 0, 32 | }) 33 | .context("failed to create minute segment")?; 34 | 35 | sequence += 1; 36 | 37 | tokio::spawn(async move { 38 | if let Err(err) = Self::send_segment(segment, now).await { 39 | log::warn!("failed to send minute: {:?}", err); 40 | } 41 | }); 42 | 43 | let next = now + chrono::Duration::try_minutes(1).unwrap(); 44 | let next = next.with_second(0).unwrap().with_nanosecond(0).unwrap(); 45 | 46 | let delay = (next - now).to_std().unwrap(); 47 | tokio::time::sleep(delay).await; 48 | 49 | now = next; // just assume we didn't undersleep 50 | } 51 | } 52 | 53 | async fn send_segment( 54 | mut segment: SubgroupWriter, 55 | mut now: DateTime, 56 | ) -> anyhow::Result<()> { 57 | // Everything but the second. 58 | let base = now.format("%Y-%m-%d %H:%M:").to_string(); 59 | 60 | segment 61 | .write(base.clone().into()) 62 | .context("failed to write base")?; 63 | 64 | loop { 65 | let delta = now.format("%S").to_string(); 66 | segment 67 | .write(delta.clone().into()) 68 | .context("failed to write delta")?; 69 | 70 | println!("{}{}", base, delta); 71 | 72 | let next = now + chrono::Duration::try_seconds(1).unwrap(); 73 | let next = next.with_nanosecond(0).unwrap(); 74 | 75 | let delay = (next - now).to_std().unwrap(); 76 | tokio::time::sleep(delay).await; 77 | 78 | // Get the current time again to check if we overslept 79 | let next = Utc::now(); 80 | if next.minute() != now.minute() { 81 | return Ok(()); 82 | } 83 | 84 | now = next; 85 | } 86 | } 87 | } 88 | pub struct Subscriber { 89 | track: TrackReader, 90 | } 91 | 92 | impl Subscriber { 93 | pub fn new(track: TrackReader) -> Self { 94 | Self { track } 95 | } 96 | 97 | pub async fn run(self) -> anyhow::Result<()> { 98 | match self.track.mode().await.context("failed to get mode")? { 99 | TrackReaderMode::Stream(stream) => Self::recv_stream(stream).await, 100 | TrackReaderMode::Subgroups(subgroups) => Self::recv_subgroups(subgroups).await, 101 | TrackReaderMode::Datagrams(datagrams) => Self::recv_datagrams(datagrams).await, 102 | } 103 | } 104 | 105 | async fn recv_stream(mut track: StreamReader) -> anyhow::Result<()> { 106 | while let Some(mut subgroup) = track.next().await? { 107 | while let Some(object) = subgroup.read_next().await? { 108 | let str = String::from_utf8_lossy(&object); 109 | println!("{}", str); 110 | } 111 | } 112 | 113 | Ok(()) 114 | } 115 | 116 | async fn recv_subgroups(mut subgroups: SubgroupsReader) -> anyhow::Result<()> { 117 | while let Some(mut subgroup) = subgroups.next().await? { 118 | let base = subgroup 119 | .read_next() 120 | .await 121 | .context("failed to get first object")? 122 | .context("empty subgroup")?; 123 | 124 | let base = String::from_utf8_lossy(&base); 125 | 126 | while let Some(object) = subgroup.read_next().await? { 127 | let str = String::from_utf8_lossy(&object); 128 | println!("{}{}", base, str); 129 | } 130 | } 131 | 132 | Ok(()) 133 | } 134 | 135 | async fn recv_datagrams(mut datagrams: DatagramsReader) -> anyhow::Result<()> { 136 | while let Some(datagram) = datagrams.read().await? { 137 | let str = String::from_utf8_lossy(&datagram.payload); 138 | println!("{}", str); 139 | } 140 | 141 | Ok(()) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /moq-clock-ietf/src/main.rs: -------------------------------------------------------------------------------- 1 | use moq_native_ietf::quic; 2 | use std::net; 3 | use url::Url; 4 | 5 | use anyhow::Context; 6 | use clap::Parser; 7 | 8 | mod clock; 9 | 10 | use moq_transport::{ 11 | coding::Tuple, 12 | serve, 13 | session::{Publisher, Subscriber}, 14 | }; 15 | 16 | #[derive(Parser, Clone)] 17 | pub struct Cli { 18 | /// Listen for UDP packets on the given address. 19 | #[arg(long, default_value = "[::]:0")] 20 | pub bind: net::SocketAddr, 21 | 22 | /// Connect to the given URL starting with https:// 23 | #[arg()] 24 | pub url: Url, 25 | 26 | /// The TLS configuration. 27 | #[command(flatten)] 28 | pub tls: moq_native_ietf::tls::Args, 29 | 30 | /// Publish the current time to the relay, otherwise only subscribe. 31 | #[arg(long)] 32 | pub publish: bool, 33 | 34 | /// The name of the clock track. 35 | #[arg(long, default_value = "clock")] 36 | pub namespace: String, 37 | 38 | /// The name of the clock track. 39 | #[arg(long, default_value = "now")] 40 | pub track: String, 41 | } 42 | 43 | #[tokio::main] 44 | async fn main() -> anyhow::Result<()> { 45 | env_logger::init(); 46 | 47 | // Disable tracing so we don't get a bunch of Quinn spam. 48 | let tracer = tracing_subscriber::FmtSubscriber::builder() 49 | .with_max_level(tracing::Level::WARN) 50 | .finish(); 51 | tracing::subscriber::set_global_default(tracer).unwrap(); 52 | 53 | let config = Cli::parse(); 54 | let tls = config.tls.load()?; 55 | 56 | let quic = quic::Endpoint::new(quic::Config { 57 | bind: config.bind, 58 | tls, 59 | })?; 60 | 61 | log::info!("connecting to server: url={}", config.url); 62 | 63 | let session = quic.client.connect(&config.url).await?; 64 | 65 | if config.publish { 66 | let (session, mut publisher) = Publisher::connect(session) 67 | .await 68 | .context("failed to create MoQ Transport session")?; 69 | 70 | let (mut writer, _, reader) = serve::Tracks { 71 | namespace: Tuple::from_utf8_path(&config.namespace), 72 | } 73 | .produce(); 74 | 75 | let track = writer.create(&config.track).unwrap(); 76 | let clock = clock::Publisher::new(track.groups()?); 77 | 78 | tokio::select! { 79 | res = session.run() => res.context("session error")?, 80 | res = clock.run() => res.context("clock error")?, 81 | res = publisher.announce(reader) => res.context("failed to serve tracks")?, 82 | } 83 | } else { 84 | let (session, mut subscriber) = Subscriber::connect(session) 85 | .await 86 | .context("failed to create MoQ Transport session")?; 87 | 88 | let (prod, sub) = 89 | serve::Track::new(Tuple::from_utf8_path(&config.namespace), config.track).produce(); 90 | 91 | let clock = clock::Subscriber::new(sub); 92 | 93 | tokio::select! { 94 | res = session.run() => res.context("session error")?, 95 | res = clock.run() => res.context("clock error")?, 96 | res = subscriber.subscribe(prod) => res.context("failed to subscribe to track")?, 97 | } 98 | } 99 | 100 | Ok(()) 101 | } 102 | -------------------------------------------------------------------------------- /moq-dir/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.3.3](https://github.com/englishm/moq-rs/compare/moq-dir-v0.3.2...moq-dir-v0.3.3) - 2025-02-24 10 | 11 | ### Other 12 | 13 | - updated the following local packages: moq-transport 14 | 15 | ## [0.3.2](https://github.com/englishm/moq-rs/compare/moq-dir-v0.3.1...moq-dir-v0.3.2) - 2025-01-16 16 | 17 | ### Other 18 | 19 | - cargo fmt 20 | - Change type of namespace to tuple 21 | - Fix one place that still uses groups? 22 | - cargo fmt 23 | - s/group/subgroup/g 24 | 25 | ## [0.3.1](https://github.com/englishm/moq-rs/compare/moq-dir-v0.3.0...moq-dir-v0.3.1) - 2024-10-31 26 | 27 | ### Other 28 | 29 | - update Cargo.lock dependencies 30 | 31 | ## [0.2.1](https://github.com/kixelated/moq-rs/compare/moq-dir-v0.2.0...moq-dir-v0.2.1) - 2024-10-01 32 | 33 | ### Other 34 | 35 | - update Cargo.lock dependencies 36 | 37 | ## [0.1.2](https://github.com/kixelated/moq-rs/compare/moq-dir-v0.1.1...moq-dir-v0.1.2) - 2024-07-24 38 | 39 | ### Other 40 | - update Cargo.lock dependencies 41 | 42 | ## [0.1.1](https://github.com/kixelated/moq-rs/compare/moq-dir-v0.1.0...moq-dir-v0.1.1) - 2024-06-03 43 | 44 | ### Other 45 | - make moq-dir session accessible ([#168](https://github.com/kixelated/moq-rs/pull/168)) 46 | - Make listings accessible ([#167](https://github.com/kixelated/moq-rs/pull/167)) 47 | - Initial gstreamer support ([#163](https://github.com/kixelated/moq-rs/pull/163)) 48 | - Bump webtransport. ([#162](https://github.com/kixelated/moq-rs/pull/162)) 49 | - Return the proper announce error code. 50 | -------------------------------------------------------------------------------- /moq-dir/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moq-dir" 3 | description = "Media over QUIC" 4 | authors = ["Luke Curley"] 5 | repository = "https://github.com/englishm/moq-rs" 6 | license = "MIT OR Apache-2.0" 7 | 8 | version = "0.3.3" 9 | edition = "2021" 10 | 11 | keywords = ["quic", "http3", "webtransport", "media", "live"] 12 | categories = ["multimedia", "network-programming", "web-programming"] 13 | 14 | [dependencies] 15 | moq-native-ietf = { path = "../moq-native-ietf", version = "0.5" } 16 | moq-transport = { path = "../moq-transport", version = "0.10" } 17 | 18 | # QUIC 19 | web-transport = { workspace = true } 20 | bytes = "1" 21 | 22 | # Async stuff 23 | tokio = { version = "1", features = ["full"] } 24 | futures = "0.3" 25 | 26 | # Error handling 27 | anyhow = { version = "1", features = ["backtrace"] } 28 | 29 | # CLI 30 | clap = { version = "4", features = ["derive"] } 31 | 32 | # Logging 33 | log = { workspace = true } 34 | env_logger = { workspace = true } 35 | tracing = "0.1" 36 | tracing-subscriber = "0.3" 37 | -------------------------------------------------------------------------------- /moq-dir/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod listing; 2 | pub use listing::*; 3 | 4 | mod listings; 5 | pub use listings::*; 6 | 7 | mod session; 8 | pub use session::*; 9 | -------------------------------------------------------------------------------- /moq-dir/src/listings.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::HashMap, 3 | sync::{Arc, Mutex}, 4 | }; 5 | 6 | use moq_transport::coding::Tuple; 7 | use moq_transport::serve::{ServeError, Tracks, TracksReader, TracksWriter}; 8 | 9 | use crate::{ListingReader, ListingWriter}; 10 | 11 | struct State { 12 | writer: TracksWriter, 13 | active: HashMap, 14 | } 15 | 16 | #[derive(Clone)] 17 | pub struct Listings { 18 | state: Arc>, 19 | reader: TracksReader, 20 | } 21 | 22 | impl Listings { 23 | pub fn new(namespace: String) -> Self { 24 | let (writer, _, reader) = Tracks::new(Tuple::from_utf8_path(&namespace)).produce(); 25 | 26 | let state = State { 27 | writer, 28 | active: HashMap::new(), 29 | }; 30 | 31 | Self { 32 | state: Arc::new(Mutex::new(state)), 33 | reader, 34 | } 35 | } 36 | 37 | // Returns a Registration that removes on drop. 38 | pub fn register(&mut self, path: &str) -> Result, ServeError> { 39 | let (prefix, base) = Self::prefix(path); 40 | 41 | let namespace = self.reader.namespace.to_utf8_path(); 42 | 43 | if !prefix.starts_with(&namespace) { 44 | // Ignore anything that isn't in our namespace. 45 | return Ok(None); 46 | } 47 | 48 | // Remove the namespace prefix from the path. 49 | let prefix = &prefix[namespace.len()..]; 50 | 51 | let mut state = self.state.lock().unwrap(); 52 | if let Some(listing) = state.active.get_mut(prefix) { 53 | listing.insert(base.to_string())?; 54 | } else { 55 | log::info!("creating prefix: {}", prefix); 56 | let track = state.writer.create(prefix).unwrap(); 57 | 58 | let mut listing = ListingWriter::new(track); 59 | listing.insert(base.to_string())?; 60 | state.active.insert(prefix.to_string(), listing); 61 | } 62 | 63 | log::info!("added listing: {} {}", prefix, base); 64 | 65 | Ok(Some(Registration { 66 | listing: self.clone(), 67 | prefix: prefix.to_string(), 68 | base: base.to_string(), 69 | })) 70 | } 71 | 72 | fn remove(&mut self, prefix: &str, base: &str) -> Result<(), ServeError> { 73 | let mut state = self.state.lock().unwrap(); 74 | 75 | let listing = state.active.get_mut(prefix).ok_or(ServeError::NotFound)?; 76 | listing.remove(base)?; 77 | 78 | log::info!("removed listing: {} {}", prefix, base); 79 | 80 | if listing.is_empty() { 81 | log::info!("removed prefix: {}", prefix); 82 | state.active.remove(prefix); 83 | state.writer.remove(prefix); 84 | } 85 | 86 | Ok(()) 87 | } 88 | 89 | pub fn subscribe(&mut self, name: &str) -> Option { 90 | self.reader.subscribe(name).map(ListingReader::new) 91 | } 92 | 93 | pub fn tracks(&self) -> TracksReader { 94 | self.reader.clone() 95 | } 96 | 97 | // Returns the prefix for the string. 98 | // This is just the content before the last '/', like a directory name. 99 | // ex. "/foo/bar/baz" -> ("/foo/bar", "baz") 100 | pub fn prefix(path: &str) -> (&str, &str) { 101 | // Find the last '/' and return the parts. 102 | match path.rfind('.') { 103 | Some(index) => (&path[..index + 1], &path[index + 1..]), 104 | None => ("", path), 105 | } 106 | } 107 | } 108 | 109 | // Used to remove the registration on drop. 110 | pub struct Registration { 111 | listing: Listings, 112 | prefix: String, 113 | base: String, 114 | } 115 | 116 | impl Drop for Registration { 117 | fn drop(&mut self) { 118 | self.listing.remove(&self.prefix, &self.base).ok(); 119 | } 120 | } 121 | 122 | #[cfg(test)] 123 | mod tests { 124 | use super::*; 125 | 126 | #[test] 127 | fn test_bucket() { 128 | assert!(Listings::prefix(".") == (".", "")); 129 | assert!(Listings::prefix(".foo") == (".", "foo")); 130 | assert!(Listings::prefix(".foo.") == (".foo.", "")); 131 | assert!(Listings::prefix(".foo.bar") == (".foo.", "bar")); 132 | assert!(Listings::prefix(".foo.bar.") == (".foo.bar.", "")); 133 | assert!(Listings::prefix(".foo.bar.baz") == (".foo.bar.", "baz")); 134 | assert!(Listings::prefix(".foo.bar.baz.") == (".foo.bar.baz.", "")); 135 | 136 | assert!(Listings::prefix("") == ("", "")); 137 | assert!(Listings::prefix("foo") == ("", "foo")); 138 | assert!(Listings::prefix("foo.") == ("foo.", "")); 139 | assert!(Listings::prefix("foo.bar") == ("foo.", "bar")); 140 | assert!(Listings::prefix("foo.bar.") == ("foo.bar.", "")); 141 | assert!(Listings::prefix("foo.bar.baz") == ("foo.bar.", "baz")); 142 | assert!(Listings::prefix("foo.bar.baz.") == ("foo.bar.baz.", "")); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /moq-dir/src/main.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use clap::Parser; 3 | use futures::{stream::FuturesUnordered, StreamExt}; 4 | 5 | use std::net; 6 | 7 | use moq_native_ietf::{quic, tls}; 8 | 9 | mod listing; 10 | mod listings; 11 | mod session; 12 | 13 | pub use listing::*; 14 | pub use listings::*; 15 | pub use session::*; 16 | 17 | #[derive(Clone, clap::Parser)] 18 | pub struct Cli { 19 | /// Listen for UDP packets on the given address. 20 | #[arg(long, default_value = "[::]:443")] 21 | pub bind: net::SocketAddr, 22 | 23 | /// The TLS configuration. 24 | #[command(flatten)] 25 | pub tls: tls::Args, 26 | 27 | /// Aggregate all announcements received with this namespace prefix. 28 | /// The list of announcements that match are available as tracks, ending with /. 29 | /// 30 | /// ex. ANNOUNCE namespace=public/meeting/12342/alice 31 | /// ex. TRACK namespace=public/ name=meeting/12342/ payload=alice 32 | /// 33 | /// Any announcements that don't match are ignored. 34 | #[arg(long, default_value = ".")] 35 | pub namespace: String, 36 | } 37 | 38 | #[tokio::main] 39 | async fn main() -> anyhow::Result<()> { 40 | env_logger::init(); 41 | 42 | // Disable tracing so we don't get a bunch of Quinn spam. 43 | let tracer = tracing_subscriber::FmtSubscriber::builder() 44 | .with_max_level(tracing::Level::WARN) 45 | .finish(); 46 | tracing::subscriber::set_global_default(tracer).unwrap(); 47 | 48 | let cli = Cli::parse(); 49 | let tls = cli.tls.load()?; 50 | 51 | let quic = quic::Endpoint::new(quic::Config { 52 | bind: cli.bind, 53 | tls, 54 | })?; 55 | let mut quic = quic.server.context("missing server certificate")?; 56 | 57 | let listings = Listings::new(cli.namespace); 58 | 59 | let mut tasks = FuturesUnordered::new(); 60 | 61 | log::info!("listening on {}", quic.local_addr()?); 62 | 63 | loop { 64 | tokio::select! { 65 | res = quic.accept() => { 66 | let session = res.context("failed to accept QUIC connection")?; 67 | let session = Session::new(session, listings.clone()); 68 | 69 | tasks.push(async move { 70 | if let Err(err) = session.run().await { 71 | log::warn!("session terminated: {}", err); 72 | } 73 | }); 74 | }, 75 | res = tasks.next(), if !tasks.is_empty() => res.unwrap(), 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /moq-dir/src/session.rs: -------------------------------------------------------------------------------- 1 | use futures::{stream::FuturesUnordered, FutureExt, StreamExt}; 2 | use moq_transport::session::{Announced, Publisher, Subscriber}; 3 | 4 | use crate::Listings; 5 | 6 | #[derive(Clone)] 7 | pub struct Session { 8 | session: web_transport::Session, 9 | listings: Listings, 10 | } 11 | 12 | impl Session { 13 | pub fn new(session: web_transport::Session, listings: Listings) -> Self { 14 | Self { session, listings } 15 | } 16 | 17 | pub async fn run(self) -> anyhow::Result<()> { 18 | let session = self.session.clone(); 19 | let (session, publisher, subscriber) = 20 | moq_transport::session::Session::accept(session).await?; 21 | 22 | let mut tasks = FuturesUnordered::new(); 23 | tasks.push(async move { session.run().await.map_err(Into::into) }.boxed()); 24 | 25 | if let Some(remote) = publisher { 26 | tasks.push(Self::serve_subscriber(self.clone(), remote).boxed()); 27 | } 28 | 29 | if let Some(remote) = subscriber { 30 | tasks.push(Self::serve_publisher(self.clone(), remote).boxed()); 31 | } 32 | 33 | // Return the first error 34 | tasks.select_next_some().await?; 35 | 36 | Ok(()) 37 | } 38 | 39 | async fn serve_subscriber(self, mut remote: Publisher) -> anyhow::Result<()> { 40 | // Announce our namespace and serve any matching subscriptions AUTOMATICALLY 41 | remote.announce(self.listings.tracks()).await?; 42 | 43 | Ok(()) 44 | } 45 | 46 | async fn serve_publisher(self, mut remote: Subscriber) -> anyhow::Result<()> { 47 | let mut tasks = FuturesUnordered::new(); 48 | 49 | loop { 50 | tokio::select! { 51 | Some(announce) = remote.announced() => { 52 | let this = self.clone(); 53 | 54 | tasks.push(async move { 55 | let info = announce.clone(); 56 | log::info!("serving announce: {:?}", info); 57 | 58 | if let Err(err) = this.serve_announce(announce).await { 59 | log::warn!("failed serving announce: {:?}, error: {}", info, err) 60 | } 61 | }); 62 | }, 63 | _ = tasks.next(), if !tasks.is_empty() => {}, 64 | else => return Ok(()), 65 | }; 66 | } 67 | } 68 | 69 | async fn serve_announce(mut self, mut announce: Announced) -> anyhow::Result<()> { 70 | announce.ok()?; 71 | 72 | match self.listings.register(&announce.namespace.to_utf8_path()) { 73 | Ok(_) => announce.closed().await?, 74 | Err(err) => { 75 | announce.close(err.clone())?; 76 | return Err(err.into()); 77 | } 78 | } 79 | 80 | Ok(()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /moq-native-ietf/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.5.4](https://github.com/englishm/moq-rs/compare/moq-native-ietf-v0.5.3...moq-native-ietf-v0.5.4) - 2025-02-24 10 | 11 | ### Other 12 | 13 | - updated the following local packages: moq-transport 14 | 15 | ## [0.5.3](https://github.com/englishm/moq-rs/compare/moq-native-ietf-v0.5.2...moq-native-ietf-v0.5.3) - 2025-01-16 16 | 17 | ### Other 18 | 19 | - cargo fmt 20 | 21 | ## [0.5.2](https://github.com/englishm/moq-rs/compare/moq-native-ietf-v0.5.1...moq-native-ietf-v0.5.2) - 2024-10-31 22 | 23 | ### Other 24 | 25 | - updated the following local packages: moq-transport 26 | 27 | ## [0.5.1](https://github.com/englishm/moq-rs/compare/moq-native-ietf-v0.5.0...moq-native-ietf-v0.5.1) - 2024-10-31 28 | 29 | ### Other 30 | 31 | - release 32 | 33 | ## [0.5.0](https://github.com/englishm/moq-rs/releases/tag/moq-native-ietf-v0.5.0) - 2024-10-23 34 | 35 | ### Other 36 | 37 | - Update repository URLs for all crates 38 | - Rename crate 39 | 40 | ## [0.2.2](https://github.com/kixelated/moq-rs/compare/moq-native-v0.2.1...moq-native-v0.2.2) - 2024-07-24 41 | 42 | ### Other 43 | - Add sslkeylogfile envvar for debugging ([#173](https://github.com/kixelated/moq-rs/pull/173)) 44 | 45 | ## [0.2.1](https://github.com/kixelated/moq-rs/compare/moq-native-v0.2.0...moq-native-v0.2.1) - 2024-06-03 46 | 47 | ### Other 48 | - Revert "filter DNS query results to only include addresses that our quic endpoint can use ([#166](https://github.com/kixelated/moq-rs/pull/166))" 49 | - filter DNS query results to only include addresses that our quic endpoint can use ([#166](https://github.com/kixelated/moq-rs/pull/166)) 50 | - Remove Cargo.lock from moq-transport 51 | -------------------------------------------------------------------------------- /moq-native-ietf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moq-native-ietf" 3 | description = "Media over QUIC - Helper library for native applications" 4 | authors = ["Luke Curley"] 5 | repository = "https://github.com/englishm/moq-rs" 6 | license = "MIT OR Apache-2.0" 7 | 8 | version = "0.5.4" 9 | edition = "2021" 10 | 11 | keywords = ["quic", "http3", "webtransport", "media", "live"] 12 | categories = ["multimedia", "network-programming", "web-programming"] 13 | 14 | [dependencies] 15 | moq-transport = { path = "../moq-transport", version = "0.10" } 16 | web-transport = { workspace = true } 17 | web-transport-quinn = "0.3" 18 | 19 | rustls = { version = "0.23", features = ["ring"] } 20 | rustls-pemfile = "2" 21 | rustls-native-certs = "0.7" 22 | quinn = { version = "0.11", features = ["ring"] } 23 | ring = "0.17" 24 | webpki = "0.22" 25 | 26 | hex = "0.4" 27 | url = "2" 28 | 29 | tokio = { version = "1", features = ["full"] } 30 | futures = "0.3" 31 | 32 | anyhow = { version = "1", features = ["backtrace"] } 33 | clap = { version = "4", features = ["derive"] } 34 | log = { version = "0.4", features = ["std"] } 35 | -------------------------------------------------------------------------------- /moq-native-ietf/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod quic; 2 | pub mod tls; 3 | -------------------------------------------------------------------------------- /moq-pub/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.8.4](https://github.com/englishm/moq-rs/compare/moq-pub-v0.8.3...moq-pub-v0.8.4) - 2025-02-24 10 | 11 | ### Other 12 | 13 | - updated the following local packages: moq-transport 14 | 15 | ## [0.8.3](https://github.com/englishm/moq-rs/compare/moq-pub-v0.8.2...moq-pub-v0.8.3) - 2025-01-16 16 | 17 | ### Other 18 | 19 | - cargo fmt 20 | - Change type of namespace to tuple 21 | - more fixes 22 | - moq-pub uses subgroups 23 | 24 | ## [0.8.2](https://github.com/englishm/moq-rs/compare/moq-pub-v0.8.1...moq-pub-v0.8.2) - 2024-10-31 25 | 26 | ### Other 27 | 28 | - updated the following local packages: moq-transport 29 | 30 | ## [0.8.1](https://github.com/englishm/moq-rs/compare/moq-pub-v0.8.0...moq-pub-v0.8.1) - 2024-10-31 31 | 32 | ### Other 33 | 34 | - update Cargo.lock dependencies 35 | 36 | ## [0.7.1](https://github.com/kixelated/moq-rs/compare/moq-pub-v0.7.0...moq-pub-v0.7.1) - 2024-10-01 37 | 38 | ### Fixed 39 | 40 | - add a way to reset Media object ([#189](https://github.com/kixelated/moq-rs/pull/189)) 41 | 42 | ## [0.6.1](https://github.com/kixelated/moq-rs/compare/moq-pub-v0.6.0...moq-pub-v0.6.1) - 2024-07-24 43 | 44 | ### Other 45 | - Fix some catalog bugs involving the name. ([#178](https://github.com/kixelated/moq-rs/pull/178)) 46 | 47 | ## [0.5.1](https://github.com/kixelated/moq-rs/compare/moq-pub-v0.5.0...moq-pub-v0.5.1) - 2024-06-03 48 | 49 | ### Other 50 | - Fix audio in the BBB demo. ([#164](https://github.com/kixelated/moq-rs/pull/164)) 51 | -------------------------------------------------------------------------------- /moq-pub/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moq-pub" 3 | description = "Media over QUIC" 4 | authors = ["Mike English", "Luke Curley"] 5 | repository = "https://github.com/englishm/moq-rs" 6 | license = "MIT OR Apache-2.0" 7 | 8 | version = "0.8.4" 9 | edition = "2021" 10 | 11 | keywords = ["quic", "http3", "webtransport", "media", "live"] 12 | categories = ["multimedia", "network-programming", "web-programming"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | moq-native-ietf = { path = "../moq-native-ietf", version = "0.5" } 18 | moq-transport = { path = "../moq-transport", version = "0.10" } 19 | moq-catalog = { path = "../moq-catalog", version = "0.2" } 20 | 21 | url = "2" 22 | bytes = "1" 23 | 24 | # Async stuff 25 | tokio = { version = "1", features = ["full"] } 26 | 27 | # CLI, logging, error handling 28 | clap = { version = "4", features = ["derive"] } 29 | log = { workspace = true } 30 | env_logger = { workspace = true } 31 | mp4 = "0.14" 32 | anyhow = { version = "1", features = ["backtrace"] } 33 | serde_json = "1" 34 | rfc6381-codec = "0.2" 35 | tracing = "0.1" 36 | tracing-subscriber = "0.3" 37 | -------------------------------------------------------------------------------- /moq-pub/README.md: -------------------------------------------------------------------------------- 1 | # moq-pub 2 | 3 | A command line tool for publishing media via Media over QUIC (MoQ). 4 | 5 | Expects to receive fragmented MP4 via standard input and connect to a MOQT relay. 6 | 7 | ``` 8 | ffmpeg ... - | moq-pub https://localhost:4443 9 | ``` 10 | 11 | ### Invoking `moq-pub`: 12 | 13 | Here's how I'm currently testing things, with a local copy of Big Buck Bunny named `bbb_source.mp4`: 14 | 15 | ``` 16 | $ ffmpeg -hide_banner -v quiet -stream_loop -1 -re -i bbb_source.mp4 -an -f mp4 -movflags empty_moov+frag_every_frame+separate_moof+omit_tfhd_offset - | RUST_LOG=moq_pub=info moq-pub https://localhost:4443 17 | ``` 18 | 19 | This relies on having `moq-relay` (the relay server) already running locally in another shell. 20 | 21 | Note also that we're dropping the audio track (`-an`) above until audio playback is stabilized on the `moq-js` side. 22 | 23 | ### Known issues 24 | 25 | - Expects only one H.264/AVC1-encoded video track (catalog generation doesn't support audio tracks yet) 26 | - Doesn't yet gracefully handle EOF - workaround: never stop sending it media (`-stream_loop -1`) 27 | - Probably still full of lots of bugs 28 | - Various other TODOs you can find in the code 29 | -------------------------------------------------------------------------------- /moq-pub/src/cli.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::{net, path}; 3 | use url::Url; 4 | 5 | #[derive(Parser, Clone, Debug)] 6 | pub struct Config { 7 | /// Listen for UDP packets on the given address. 8 | #[arg(long, default_value = "[::]:0")] 9 | pub bind: net::SocketAddr, 10 | 11 | /// Advertise this frame rate in the catalog (informational) 12 | // TODO auto-detect this from the input when not provided 13 | #[arg(long, default_value = "24")] 14 | pub fps: u8, 15 | 16 | /// Advertise this bit rate in the catalog (informational) 17 | // TODO auto-detect this from the input when not provided 18 | #[arg(long, default_value = "1500000")] 19 | pub bitrate: u32, 20 | 21 | /// Connect to the given URL starting with https:// 22 | #[arg(value_parser = moq_url)] 23 | pub url: Url, 24 | 25 | /// The name of the broadcast 26 | #[arg(long)] 27 | pub name: String, 28 | 29 | /// Use the TLS root CA at this path, encoded as PEM. 30 | /// 31 | /// This value can be provided multiple times for multiple roots. 32 | /// If this is empty, system roots will be used instead 33 | #[arg(long)] 34 | pub tls_root: Vec, 35 | 36 | /// Danger: Disable TLS certificate verification. 37 | /// 38 | /// Fine for local development, but should be used in caution in production. 39 | #[arg(long)] 40 | pub tls_disable_verify: bool, 41 | } 42 | 43 | fn moq_url(s: &str) -> Result { 44 | Url::try_from(s).map_err(|e| e.to_string()) 45 | } 46 | -------------------------------------------------------------------------------- /moq-pub/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod media; 2 | pub use media::*; 3 | -------------------------------------------------------------------------------- /moq-pub/src/main.rs: -------------------------------------------------------------------------------- 1 | use bytes::BytesMut; 2 | use std::net; 3 | use url::Url; 4 | 5 | use anyhow::Context; 6 | use clap::Parser; 7 | use tokio::io::AsyncReadExt; 8 | 9 | use moq_native_ietf::quic; 10 | use moq_pub::Media; 11 | use moq_transport::{coding::Tuple, serve, session::Publisher}; 12 | 13 | #[derive(Parser, Clone)] 14 | pub struct Cli { 15 | /// Listen for UDP packets on the given address. 16 | #[arg(long, default_value = "[::]:0")] 17 | pub bind: net::SocketAddr, 18 | 19 | /// Advertise this frame rate in the catalog (informational) 20 | // TODO auto-detect this from the input when not provided 21 | #[arg(long, default_value = "24")] 22 | pub fps: u8, 23 | 24 | /// Advertise this bit rate in the catalog (informational) 25 | // TODO auto-detect this from the input when not provided 26 | #[arg(long, default_value = "1500000")] 27 | pub bitrate: u32, 28 | 29 | /// Connect to the given URL starting with https:// 30 | #[arg()] 31 | pub url: Url, 32 | 33 | /// The name of the broadcast 34 | #[arg(long)] 35 | pub name: String, 36 | 37 | /// The TLS configuration. 38 | #[command(flatten)] 39 | pub tls: moq_native_ietf::tls::Args, 40 | } 41 | 42 | #[tokio::main] 43 | async fn main() -> anyhow::Result<()> { 44 | env_logger::init(); 45 | 46 | // Disable tracing so we don't get a bunch of Quinn spam. 47 | let tracer = tracing_subscriber::FmtSubscriber::builder() 48 | .with_max_level(tracing::Level::WARN) 49 | .finish(); 50 | tracing::subscriber::set_global_default(tracer).unwrap(); 51 | 52 | let cli = Cli::parse(); 53 | 54 | let (writer, _, reader) = serve::Tracks::new(Tuple::from_utf8_path(&cli.name)).produce(); 55 | let media = Media::new(writer)?; 56 | 57 | let tls = cli.tls.load()?; 58 | 59 | let quic = quic::Endpoint::new(moq_native_ietf::quic::Config { 60 | bind: cli.bind, 61 | tls: tls.clone(), 62 | })?; 63 | 64 | log::info!("connecting to relay: url={}", cli.url); 65 | let session = quic.client.connect(&cli.url).await?; 66 | 67 | let (session, mut publisher) = Publisher::connect(session) 68 | .await 69 | .context("failed to create MoQ Transport publisher")?; 70 | 71 | tokio::select! { 72 | res = session.run() => res.context("session error")?, 73 | res = run_media(media) => { 74 | res.context("media error")? 75 | }, 76 | res = publisher.announce(reader) => res.context("publisher error")?, 77 | } 78 | 79 | Ok(()) 80 | } 81 | 82 | async fn run_media(mut media: Media) -> anyhow::Result<()> { 83 | let mut input = tokio::io::stdin(); 84 | let mut buf = BytesMut::new(); 85 | loop { 86 | input 87 | .read_buf(&mut buf) 88 | .await 89 | .context("failed to read from stdin")?; 90 | media.parse(&mut buf).context("failed to parse media")?; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /moq-relay-ietf/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.7.4](https://github.com/englishm/moq-rs/compare/moq-relay-ietf-v0.7.3...moq-relay-ietf-v0.7.4) - 2025-02-24 10 | 11 | ### Other 12 | 13 | - updated the following local packages: moq-transport 14 | 15 | ## [0.7.3](https://github.com/englishm/moq-rs/compare/moq-relay-ietf-v0.7.2...moq-relay-ietf-v0.7.3) - 2025-01-16 16 | 17 | ### Other 18 | 19 | - cargo fmt 20 | - Change type of namespace to tuple 21 | 22 | ## [0.7.2](https://github.com/englishm/moq-rs/compare/moq-relay-ietf-v0.7.1...moq-relay-ietf-v0.7.2) - 2024-10-31 23 | 24 | ### Other 25 | 26 | - updated the following local packages: moq-transport 27 | 28 | ## [0.7.1](https://github.com/englishm/moq-rs/compare/moq-relay-ietf-v0.7.0...moq-relay-ietf-v0.7.1) - 2024-10-31 29 | 30 | ### Other 31 | 32 | - release 33 | 34 | ## [0.7.0](https://github.com/englishm/moq-rs/releases/tag/moq-relay-ietf-v0.7.0) - 2024-10-23 35 | 36 | ### Other 37 | 38 | - Update repository URLs for all crates 39 | - Rename crate 40 | 41 | ## [0.6.1](https://github.com/kixelated/moq-rs/compare/moq-relay-v0.6.0...moq-relay-v0.6.1) - 2024-10-01 42 | 43 | ### Other 44 | 45 | - update Cargo.lock dependencies 46 | 47 | ## [0.5.1](https://github.com/kixelated/moq-rs/compare/moq-relay-v0.5.0...moq-relay-v0.5.1) - 2024-07-24 48 | 49 | ### Other 50 | - update Cargo.lock dependencies 51 | -------------------------------------------------------------------------------- /moq-relay-ietf/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moq-relay-ietf" 3 | description = "Media over QUIC" 4 | authors = ["Luke Curley"] 5 | repository = "https://github.com/englishm/moq-rs" 6 | license = "MIT OR Apache-2.0" 7 | 8 | version = "0.7.4" 9 | edition = "2021" 10 | 11 | keywords = ["quic", "http3", "webtransport", "media", "live"] 12 | categories = ["multimedia", "network-programming", "web-programming"] 13 | 14 | [dependencies] 15 | moq-transport = { path = "../moq-transport", version = "0.10" } 16 | moq-native-ietf = { path = "../moq-native-ietf", version = "0.5" } 17 | moq-api = { path = "../moq-api", version = "0.2" } 18 | 19 | # QUIC 20 | url = "2" 21 | 22 | # Async stuff 23 | tokio = { version = "1", features = ["full"] } 24 | futures = "0.3" 25 | 26 | # Web server to serve the fingerprint 27 | axum = { version = "0.7", features = ["tokio"] } 28 | hyper-serve = { version = "0.6", features = [ 29 | "tls-rustls", 30 | ] } # fork of axum-server 31 | tower-http = { version = "0.5", features = ["cors"] } 32 | hex = "0.4" 33 | 34 | # Error handling 35 | anyhow = { version = "1", features = ["backtrace"] } 36 | 37 | # CLI 38 | clap = { version = "4", features = ["derive"] } 39 | 40 | # Logging 41 | log = { workspace = true } 42 | env_logger = { workspace = true } 43 | tracing = "0.1" 44 | tracing-subscriber = "0.3" 45 | -------------------------------------------------------------------------------- /moq-relay-ietf/README.md: -------------------------------------------------------------------------------- 1 | # moq-relay 2 | 3 | A server that connects publishing clients to subscribing clients. 4 | All subscriptions are deduplicated and cached, so that a single publisher can serve many subscribers. 5 | 6 | ## Usage 7 | 8 | The publisher must choose a unique name for their broadcast, sent as the WebTransport path when connecting to the server. 9 | We currently do a dumb string comparison, so capatilization matters as do slashes. 10 | 11 | For example: `CONNECT https://relay.quic.video/BigBuckBunny` 12 | 13 | The MoqTransport handshake includes a `role` parameter, which must be `publisher` or `subscriber`. 14 | The specification allows a `both` role but you'll get an error. 15 | 16 | You can have one publisher and any number of subscribers connected to the same path. 17 | If the publisher disconnects, then all subscribers receive an error and will not get updates, even if a new publisher reuses the path. 18 | -------------------------------------------------------------------------------- /moq-relay-ietf/src/api.rs: -------------------------------------------------------------------------------- 1 | use url::Url; 2 | 3 | #[derive(Clone)] 4 | pub struct Api { 5 | client: moq_api::Client, 6 | origin: moq_api::Origin, 7 | } 8 | 9 | impl Api { 10 | pub fn new(url: Url, node: Url) -> Self { 11 | let origin = moq_api::Origin { url: node }; 12 | let client = moq_api::Client::new(url); 13 | 14 | Self { client, origin } 15 | } 16 | 17 | pub async fn set_origin(&self, namespace: String) -> Result { 18 | let refresh = Refresh::new(self.client.clone(), self.origin.clone(), namespace); 19 | refresh.update().await?; 20 | Ok(refresh) 21 | } 22 | 23 | pub async fn get_origin( 24 | &self, 25 | namespace: &str, 26 | ) -> Result, moq_api::ApiError> { 27 | self.client.get_origin(namespace).await 28 | } 29 | } 30 | 31 | pub struct Refresh { 32 | client: moq_api::Client, 33 | origin: moq_api::Origin, 34 | namespace: String, 35 | refresh: tokio::time::Interval, 36 | } 37 | 38 | impl Refresh { 39 | fn new(client: moq_api::Client, origin: moq_api::Origin, namespace: String) -> Self { 40 | let duration = tokio::time::Duration::from_secs(300); 41 | let mut refresh = tokio::time::interval(tokio::time::Duration::from_secs(300)); 42 | refresh.reset_after(duration); // skip the first tick 43 | 44 | Self { 45 | client, 46 | origin, 47 | namespace, 48 | refresh, 49 | } 50 | } 51 | 52 | async fn update(&self) -> Result<(), moq_api::ApiError> { 53 | // Register the origin in moq-api. 54 | log::debug!( 55 | "registering origin: namespace={} url={}", 56 | self.namespace, 57 | self.origin.url 58 | ); 59 | self.client 60 | .set_origin(&self.namespace, self.origin.clone()) 61 | .await 62 | } 63 | 64 | pub async fn run(&mut self) -> anyhow::Result<()> { 65 | loop { 66 | self.refresh.tick().await; 67 | self.update().await?; 68 | } 69 | } 70 | } 71 | 72 | impl Drop for Refresh { 73 | fn drop(&mut self) { 74 | // TODO this is really lazy 75 | let namespace = self.namespace.clone(); 76 | let client = self.client.clone(); 77 | log::debug!("removing origin: namespace={}", namespace,); 78 | tokio::spawn(async move { client.delete_origin(&namespace).await }); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /moq-relay-ietf/src/consumer.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Context; 2 | use futures::{stream::FuturesUnordered, FutureExt, StreamExt}; 3 | use moq_transport::{ 4 | serve::Tracks, 5 | session::{Announced, SessionError, Subscriber}, 6 | }; 7 | 8 | use crate::{Api, Locals, Producer}; 9 | 10 | #[derive(Clone)] 11 | pub struct Consumer { 12 | remote: Subscriber, 13 | locals: Locals, 14 | api: Option, 15 | forward: Option, // Forward all announcements to this subscriber 16 | } 17 | 18 | impl Consumer { 19 | pub fn new( 20 | remote: Subscriber, 21 | locals: Locals, 22 | api: Option, 23 | forward: Option, 24 | ) -> Self { 25 | Self { 26 | remote, 27 | locals, 28 | api, 29 | forward, 30 | } 31 | } 32 | 33 | pub async fn run(mut self) -> Result<(), SessionError> { 34 | let mut tasks = FuturesUnordered::new(); 35 | 36 | loop { 37 | tokio::select! { 38 | Some(announce) = self.remote.announced() => { 39 | let this = self.clone(); 40 | 41 | tasks.push(async move { 42 | let info = announce.clone(); 43 | log::info!("serving announce: {:?}", info); 44 | 45 | if let Err(err) = this.serve(announce).await { 46 | log::warn!("failed serving announce: {:?}, error: {}", info, err) 47 | } 48 | }); 49 | }, 50 | _ = tasks.next(), if !tasks.is_empty() => {}, 51 | else => return Ok(()), 52 | }; 53 | } 54 | } 55 | 56 | async fn serve(mut self, mut announce: Announced) -> Result<(), anyhow::Error> { 57 | let mut tasks = FuturesUnordered::new(); 58 | 59 | let (_, mut request, reader) = Tracks::new(announce.namespace.clone()).produce(); 60 | 61 | if let Some(api) = self.api.as_ref() { 62 | let mut refresh = api.set_origin(reader.namespace.to_utf8_path()).await?; 63 | tasks.push( 64 | async move { refresh.run().await.context("failed refreshing origin") }.boxed(), 65 | ); 66 | } 67 | 68 | // Register the local tracks, unregister on drop 69 | let _register = self.locals.register(reader.clone()).await?; 70 | 71 | announce.ok()?; 72 | 73 | if let Some(mut forward) = self.forward { 74 | tasks.push( 75 | async move { 76 | log::info!("forwarding announce: {:?}", reader.info); 77 | forward 78 | .announce(reader) 79 | .await 80 | .context("failed forwarding announce") 81 | } 82 | .boxed(), 83 | ); 84 | } 85 | 86 | loop { 87 | tokio::select! { 88 | // If the announce is closed, return the error 89 | Err(err) = announce.closed() => return Err(err.into()), 90 | 91 | // Wait for the next subscriber and serve the track. 92 | Some(track) = request.next() => { 93 | let mut remote = self.remote.clone(); 94 | 95 | tasks.push(async move { 96 | let info = track.clone(); 97 | log::info!("forwarding subscribe: {:?}", info); 98 | 99 | if let Err(err) = remote.subscribe(track).await { 100 | log::warn!("failed forwarding subscribe: {:?}, error: {}", info, err) 101 | } 102 | 103 | Ok(()) 104 | }.boxed()); 105 | }, 106 | res = tasks.next(), if !tasks.is_empty() => res.unwrap()?, 107 | else => return Ok(()), 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /moq-relay-ietf/src/local.rs: -------------------------------------------------------------------------------- 1 | use std::collections::hash_map; 2 | use std::collections::HashMap; 3 | 4 | use std::sync::{Arc, Mutex}; 5 | 6 | use moq_transport::{ 7 | coding::Tuple, 8 | serve::{ServeError, TracksReader}, 9 | }; 10 | 11 | #[derive(Clone)] 12 | pub struct Locals { 13 | lookup: Arc>>, 14 | } 15 | 16 | impl Default for Locals { 17 | fn default() -> Self { 18 | Self::new() 19 | } 20 | } 21 | 22 | impl Locals { 23 | pub fn new() -> Self { 24 | Self { 25 | lookup: Default::default(), 26 | } 27 | } 28 | 29 | pub async fn register(&mut self, tracks: TracksReader) -> anyhow::Result { 30 | let namespace = tracks.namespace.clone(); 31 | match self.lookup.lock().unwrap().entry(namespace.clone()) { 32 | hash_map::Entry::Vacant(entry) => entry.insert(tracks), 33 | hash_map::Entry::Occupied(_) => return Err(ServeError::Duplicate.into()), 34 | }; 35 | 36 | let registration = Registration { 37 | locals: self.clone(), 38 | namespace, 39 | }; 40 | 41 | Ok(registration) 42 | } 43 | 44 | pub fn route(&self, namespace: &Tuple) -> Option { 45 | self.lookup.lock().unwrap().get(namespace).cloned() 46 | } 47 | } 48 | 49 | pub struct Registration { 50 | locals: Locals, 51 | namespace: Tuple, 52 | } 53 | 54 | impl Drop for Registration { 55 | fn drop(&mut self) { 56 | self.locals.lookup.lock().unwrap().remove(&self.namespace); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /moq-relay-ietf/src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | mod api; 4 | mod consumer; 5 | mod local; 6 | mod producer; 7 | mod relay; 8 | mod remote; 9 | mod session; 10 | mod web; 11 | 12 | pub use api::*; 13 | pub use consumer::*; 14 | pub use local::*; 15 | pub use producer::*; 16 | pub use relay::*; 17 | pub use remote::*; 18 | pub use session::*; 19 | pub use web::*; 20 | 21 | use std::net; 22 | use url::Url; 23 | 24 | #[derive(Parser, Clone)] 25 | pub struct Cli { 26 | /// Listen on this address 27 | #[arg(long, default_value = "[::]:443")] 28 | pub bind: net::SocketAddr, 29 | 30 | /// The TLS configuration. 31 | #[command(flatten)] 32 | pub tls: moq_native_ietf::tls::Args, 33 | 34 | /// Forward all announces to the provided server for authentication/routing. 35 | /// If not provided, the relay accepts every unique announce. 36 | #[arg(long)] 37 | pub announce: Option, 38 | 39 | /// The URL of the moq-api server in order to run a cluster. 40 | /// Must be used in conjunction with --node to advertise the origin 41 | #[arg(long)] 42 | pub api: Option, 43 | 44 | /// The hostname that we advertise to other origins. 45 | /// The provided certificate must be valid for this address. 46 | #[arg(long)] 47 | pub node: Option, 48 | 49 | /// Enable development mode. 50 | /// This hosts a HTTPS web server via TCP to serve the fingerprint of the certificate. 51 | #[arg(long)] 52 | pub dev: bool, 53 | } 54 | 55 | #[tokio::main] 56 | async fn main() -> anyhow::Result<()> { 57 | env_logger::init(); 58 | 59 | // Disable tracing so we don't get a bunch of Quinn spam. 60 | let tracer = tracing_subscriber::FmtSubscriber::builder() 61 | .with_max_level(tracing::Level::WARN) 62 | .finish(); 63 | tracing::subscriber::set_global_default(tracer).unwrap(); 64 | 65 | let cli = Cli::parse(); 66 | let tls = cli.tls.load()?; 67 | 68 | if tls.server.is_none() { 69 | anyhow::bail!("missing TLS certificates"); 70 | } 71 | 72 | // Create a QUIC server for media. 73 | let relay = Relay::new(RelayConfig { 74 | tls: tls.clone(), 75 | bind: cli.bind, 76 | node: cli.node, 77 | api: cli.api, 78 | announce: cli.announce, 79 | })?; 80 | 81 | if cli.dev { 82 | // Create a web server too. 83 | // Currently this only contains the certificate fingerprint (for development only). 84 | let web = Web::new(WebConfig { 85 | bind: cli.bind, 86 | tls, 87 | }); 88 | 89 | tokio::spawn(async move { 90 | web.run().await.expect("failed to run web server"); 91 | }); 92 | } 93 | 94 | relay.run().await 95 | } 96 | -------------------------------------------------------------------------------- /moq-relay-ietf/src/producer.rs: -------------------------------------------------------------------------------- 1 | use futures::{stream::FuturesUnordered, StreamExt}; 2 | use moq_transport::{ 3 | serve::{ServeError, TracksReader}, 4 | session::{Publisher, SessionError, Subscribed}, 5 | }; 6 | 7 | use crate::{Locals, RemotesConsumer}; 8 | 9 | #[derive(Clone)] 10 | pub struct Producer { 11 | remote: Publisher, 12 | locals: Locals, 13 | remotes: Option, 14 | } 15 | 16 | impl Producer { 17 | pub fn new(remote: Publisher, locals: Locals, remotes: Option) -> Self { 18 | Self { 19 | remote, 20 | locals, 21 | remotes, 22 | } 23 | } 24 | 25 | pub async fn announce(&mut self, tracks: TracksReader) -> Result<(), SessionError> { 26 | self.remote.announce(tracks).await 27 | } 28 | 29 | pub async fn run(mut self) -> Result<(), SessionError> { 30 | let mut tasks = FuturesUnordered::new(); 31 | 32 | loop { 33 | tokio::select! { 34 | Some(subscribe) = self.remote.subscribed() => { 35 | let this = self.clone(); 36 | 37 | tasks.push(async move { 38 | let info = subscribe.clone(); 39 | log::info!("serving subscribe: {:?}", info); 40 | 41 | if let Err(err) = this.serve(subscribe).await { 42 | log::warn!("failed serving subscribe: {:?}, error: {}", info, err) 43 | } 44 | }) 45 | }, 46 | _= tasks.next(), if !tasks.is_empty() => {}, 47 | else => return Ok(()), 48 | }; 49 | } 50 | } 51 | 52 | async fn serve(self, subscribe: Subscribed) -> Result<(), anyhow::Error> { 53 | if let Some(mut local) = self.locals.route(&subscribe.namespace) { 54 | if let Some(track) = local.subscribe(&subscribe.name) { 55 | log::info!("serving from local: {:?}", track.info); 56 | return Ok(subscribe.serve(track).await?); 57 | } 58 | } 59 | 60 | if let Some(remotes) = &self.remotes { 61 | if let Some(remote) = remotes.route(&subscribe.namespace).await? { 62 | if let Some(track) = 63 | remote.subscribe(subscribe.namespace.clone(), subscribe.name.clone())? 64 | { 65 | log::info!("serving from remote: {:?} {:?}", remote.info, track.info); 66 | 67 | // NOTE: Depends on drop(track) being called afterwards 68 | return Ok(subscribe.serve(track.reader).await?); 69 | } 70 | } 71 | } 72 | 73 | Err(ServeError::NotFound.into()) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /moq-relay-ietf/src/relay.rs: -------------------------------------------------------------------------------- 1 | use std::net; 2 | 3 | use anyhow::Context; 4 | 5 | use futures::{stream::FuturesUnordered, FutureExt, StreamExt}; 6 | use moq_native_ietf::quic; 7 | use url::Url; 8 | 9 | use crate::{Api, Consumer, Locals, Producer, Remotes, RemotesConsumer, RemotesProducer, Session}; 10 | 11 | pub struct RelayConfig { 12 | /// Listen on this address 13 | pub bind: net::SocketAddr, 14 | 15 | /// The TLS configuration. 16 | pub tls: moq_native_ietf::tls::Config, 17 | 18 | /// Forward all announcements to the (optional) URL. 19 | pub announce: Option, 20 | 21 | /// Connect to the HTTP moq-api at this URL. 22 | pub api: Option, 23 | 24 | /// Our hostname which we advertise to other origins. 25 | /// We use QUIC, so the certificate must be valid for this address. 26 | pub node: Option, 27 | } 28 | 29 | pub struct Relay { 30 | quic: quic::Endpoint, 31 | announce: Option, 32 | locals: Locals, 33 | api: Option, 34 | remotes: Option<(RemotesProducer, RemotesConsumer)>, 35 | } 36 | 37 | impl Relay { 38 | // Create a QUIC endpoint that can be used for both clients and servers. 39 | pub fn new(config: RelayConfig) -> anyhow::Result { 40 | let quic = quic::Endpoint::new(quic::Config { 41 | bind: config.bind, 42 | tls: config.tls, 43 | })?; 44 | 45 | let api = if let (Some(url), Some(node)) = (config.api, config.node) { 46 | log::info!("using moq-api: url={} node={}", url, node); 47 | Some(Api::new(url, node)) 48 | } else { 49 | None 50 | }; 51 | 52 | let locals = Locals::new(); 53 | 54 | let remotes = api.clone().map(|api| { 55 | Remotes { 56 | api, 57 | quic: quic.client.clone(), 58 | } 59 | .produce() 60 | }); 61 | 62 | Ok(Self { 63 | quic, 64 | announce: config.announce, 65 | api, 66 | locals, 67 | remotes, 68 | }) 69 | } 70 | 71 | pub async fn run(self) -> anyhow::Result<()> { 72 | let mut tasks = FuturesUnordered::new(); 73 | 74 | let remotes = self.remotes.map(|(producer, consumer)| { 75 | tasks.push(producer.run().boxed()); 76 | consumer 77 | }); 78 | 79 | let forward = if let Some(url) = &self.announce { 80 | log::info!("forwarding announces to {}", url); 81 | let session = self 82 | .quic 83 | .client 84 | .connect(url) 85 | .await 86 | .context("failed to establish forward connection")?; 87 | let (session, publisher, subscriber) = 88 | moq_transport::session::Session::connect(session) 89 | .await 90 | .context("failed to establish forward session")?; 91 | 92 | // Create a normal looking session, except we never forward or register announces. 93 | let session = Session { 94 | session, 95 | producer: Some(Producer::new( 96 | publisher, 97 | self.locals.clone(), 98 | remotes.clone(), 99 | )), 100 | consumer: Some(Consumer::new(subscriber, self.locals.clone(), None, None)), 101 | }; 102 | 103 | let forward = session.producer.clone(); 104 | 105 | tasks.push(async move { session.run().await.context("forwarding failed") }.boxed()); 106 | 107 | forward 108 | } else { 109 | None 110 | }; 111 | 112 | let mut server = self.quic.server.context("missing TLS certificate")?; 113 | log::info!("listening on {}", server.local_addr()?); 114 | 115 | loop { 116 | tokio::select! { 117 | res = server.accept() => { 118 | let conn = res.context("failed to accept QUIC connection")?; 119 | 120 | let locals = self.locals.clone(); 121 | let remotes = remotes.clone(); 122 | let forward = forward.clone(); 123 | let api = self.api.clone(); 124 | 125 | tasks.push(async move { 126 | let (session, publisher, subscriber) = match moq_transport::session::Session::accept(conn).await { 127 | Ok(session) => session, 128 | Err(err) => { 129 | log::warn!("failed to accept MoQ session: {}", err); 130 | return Ok(()); 131 | } 132 | }; 133 | 134 | let session = Session { 135 | session, 136 | producer: publisher.map(|publisher| Producer::new(publisher, locals.clone(), remotes)), 137 | consumer: subscriber.map(|subscriber| Consumer::new(subscriber, locals, api, forward)), 138 | }; 139 | 140 | if let Err(err) = session.run().await { 141 | log::warn!("failed to run MoQ session: {}", err); 142 | } 143 | 144 | Ok(()) 145 | }.boxed()); 146 | }, 147 | res = tasks.next(), if !tasks.is_empty() => res.unwrap()?, 148 | } 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /moq-relay-ietf/src/session.rs: -------------------------------------------------------------------------------- 1 | use futures::{stream::FuturesUnordered, FutureExt, StreamExt}; 2 | use moq_transport::session::SessionError; 3 | 4 | use crate::{Consumer, Producer}; 5 | 6 | pub struct Session { 7 | pub session: moq_transport::session::Session, 8 | pub producer: Option, 9 | pub consumer: Option, 10 | } 11 | 12 | impl Session { 13 | pub async fn run(self) -> Result<(), SessionError> { 14 | let mut tasks = FuturesUnordered::new(); 15 | tasks.push(self.session.run().boxed()); 16 | 17 | if let Some(producer) = self.producer { 18 | tasks.push(producer.run().boxed()); 19 | } 20 | 21 | if let Some(consumer) = self.consumer { 22 | tasks.push(consumer.run().boxed()); 23 | } 24 | 25 | tasks.select_next_some().await 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /moq-relay-ietf/src/web.rs: -------------------------------------------------------------------------------- 1 | use std::{net, sync::Arc}; 2 | 3 | use axum::{extract::State, http::Method, response::IntoResponse, routing::get, Router}; 4 | use hyper_serve::tls_rustls::RustlsAcceptor; 5 | use tower_http::cors::{Any, CorsLayer}; 6 | 7 | pub struct WebConfig { 8 | pub bind: net::SocketAddr, 9 | pub tls: moq_native_ietf::tls::Config, 10 | } 11 | 12 | // Run a HTTP server using Axum 13 | // TODO remove this when Chrome adds support for self-signed certificates using WebTransport 14 | pub struct Web { 15 | app: Router, 16 | server: hyper_serve::Server, 17 | } 18 | 19 | impl Web { 20 | pub fn new(config: WebConfig) -> Self { 21 | // Get the first certificate's fingerprint. 22 | // TODO serve all of them so we can support multiple signature algorithms. 23 | let fingerprint = config 24 | .tls 25 | .fingerprints 26 | .first() 27 | .expect("missing certificate") 28 | .clone(); 29 | 30 | let mut tls = config.tls.server.expect("missing server configuration"); 31 | tls.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; 32 | let tls = hyper_serve::tls_rustls::RustlsConfig::from_config(Arc::new(tls)); 33 | 34 | let app = Router::new() 35 | .route("/fingerprint", get(serve_fingerprint)) 36 | .layer( 37 | CorsLayer::new() 38 | .allow_origin(Any) 39 | .allow_methods([Method::GET]), 40 | ) 41 | .with_state(fingerprint); 42 | 43 | let server = hyper_serve::bind_rustls(config.bind, tls); 44 | 45 | Self { app, server } 46 | } 47 | 48 | pub async fn run(self) -> anyhow::Result<()> { 49 | self.server.serve(self.app.into_make_service()).await?; 50 | Ok(()) 51 | } 52 | } 53 | 54 | async fn serve_fingerprint(State(fingerprint): State) -> impl IntoResponse { 55 | fingerprint 56 | } 57 | -------------------------------------------------------------------------------- /moq-sub/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.3.3](https://github.com/englishm/moq-rs/compare/moq-sub-v0.3.2...moq-sub-v0.3.3) - 2025-02-24 10 | 11 | ### Other 12 | 13 | - updated the following local packages: moq-transport 14 | 15 | ## [0.3.2](https://github.com/englishm/moq-rs/compare/moq-sub-v0.3.1...moq-sub-v0.3.2) - 2025-01-16 16 | 17 | ### Other 18 | 19 | - cargo fmt 20 | - Change type of namespace to tuple 21 | - s/group/subgroup/g 22 | 23 | ## [0.3.1](https://github.com/englishm/moq-rs/compare/moq-sub-v0.3.0...moq-sub-v0.3.1) - 2024-10-31 24 | 25 | ### Other 26 | 27 | - update Cargo.lock dependencies 28 | 29 | ## [0.2.1](https://github.com/kixelated/moq-rs/compare/moq-sub-v0.2.0...moq-sub-v0.2.1) - 2024-10-01 30 | 31 | ### Fixed 32 | 33 | - don't interleave groups ([#188](https://github.com/kixelated/moq-rs/pull/188)) 34 | 35 | ### Other 36 | 37 | - Allow moqt URL scheme for QUIC ([#187](https://github.com/kixelated/moq-rs/pull/187)) 38 | 39 | ## [0.1.1](https://github.com/kixelated/moq-rs/compare/moq-sub-v0.1.0...moq-sub-v0.1.1) - 2024-07-24 40 | 41 | ### Other 42 | - update Cargo.lock dependencies 43 | -------------------------------------------------------------------------------- /moq-sub/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moq-sub" 3 | description = "Media over QUIC" 4 | authors = [] 5 | repository = "https://github.com/englishm/moq-rs" 6 | license = "MIT OR Apache-2.0" 7 | 8 | version = "0.3.3" 9 | edition = "2021" 10 | 11 | keywords = ["quic", "http3", "webtransport", "media", "live"] 12 | categories = ["multimedia", "network-programming", "web-programming"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | 16 | [dependencies] 17 | moq-transport = { path = "../moq-transport", version = "0.10" } 18 | moq-native-ietf = { path = "../moq-native-ietf", version = "0.5" } 19 | url = "2" 20 | 21 | # Async stuff 22 | tokio = { version = "1", features = ["full"] } 23 | 24 | # CLI, logging, error handling 25 | clap = { version = "4", features = ["derive"] } 26 | log = { version = "0.4", features = ["std"] } 27 | env_logger = "0.11" 28 | mp4 = "0.14" 29 | anyhow = { version = "1", features = ["backtrace"] } 30 | tracing = "0.1" 31 | tracing-subscriber = "0.3" 32 | -------------------------------------------------------------------------------- /moq-sub/README.md: -------------------------------------------------------------------------------- 1 | # moq-sub 2 | 3 | A command line tool for subscribing to media via Media over QUIC (MoQ). 4 | 5 | Takes an URL to MoQ relay with a broadcast name in the path part of the URL. It will connect to the relay, subscribe to 6 | the broadcast, and dump the media segments of the first video and first audio track to STDOUT. 7 | 8 | ``` 9 | moq-sub https://localhost:4443/dev | ffplay - 10 | ``` 11 | -------------------------------------------------------------------------------- /moq-sub/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod media; 2 | -------------------------------------------------------------------------------- /moq-sub/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::net; 2 | 3 | use anyhow::Context; 4 | use clap::Parser; 5 | use url::Url; 6 | 7 | use moq_native_ietf::quic; 8 | use moq_sub::media::Media; 9 | use moq_transport::{coding::Tuple, serve::Tracks}; 10 | 11 | #[tokio::main] 12 | async fn main() -> anyhow::Result<()> { 13 | env_logger::init(); 14 | 15 | // Disable tracing so we don't get a bunch of Quinn spam. 16 | let tracer = tracing_subscriber::FmtSubscriber::builder() 17 | .with_max_level(tracing::Level::WARN) 18 | .finish(); 19 | tracing::subscriber::set_global_default(tracer).unwrap(); 20 | 21 | let out = tokio::io::stdout(); 22 | 23 | let config = Config::parse(); 24 | let tls = config.tls.load()?; 25 | let quic = quic::Endpoint::new(quic::Config { 26 | bind: config.bind, 27 | tls, 28 | })?; 29 | 30 | let session = quic.client.connect(&config.url).await?; 31 | 32 | let (session, subscriber) = moq_transport::session::Subscriber::connect(session) 33 | .await 34 | .context("failed to create MoQ Transport session")?; 35 | 36 | // Associate empty set of Tracks with provided namespace 37 | let tracks = Tracks::new(Tuple::from_utf8_path(&config.name)); 38 | 39 | let mut media = Media::new(subscriber, tracks, out).await?; 40 | 41 | tokio::select! { 42 | res = session.run() => res.context("session error")?, 43 | res = media.run() => res.context("media error")?, 44 | } 45 | 46 | Ok(()) 47 | } 48 | 49 | #[derive(Parser, Clone)] 50 | pub struct Config { 51 | /// Listen for UDP packets on the given address. 52 | #[arg(long, default_value = "[::]:0")] 53 | pub bind: net::SocketAddr, 54 | 55 | /// Connect to the given URL starting with https:// 56 | #[arg(value_parser = moq_url)] 57 | pub url: Url, 58 | 59 | /// The name of the broadcast 60 | #[arg(long)] 61 | pub name: String, 62 | 63 | /// The TLS configuration. 64 | #[command(flatten)] 65 | pub tls: moq_native_ietf::tls::Args, 66 | } 67 | 68 | fn moq_url(s: &str) -> Result { 69 | let url = Url::try_from(s).map_err(|e| e.to_string())?; 70 | 71 | // Make sure the scheme is moq 72 | if url.scheme() != "https" && url.scheme() != "moqt" { 73 | return Err("url scheme must be https:// for WebTransport & moqt:// for QUIC".to_string()); 74 | } 75 | 76 | Ok(url) 77 | } 78 | -------------------------------------------------------------------------------- /moq-transport/.gitignore: -------------------------------------------------------------------------------- 1 | Cargo.lock 2 | -------------------------------------------------------------------------------- /moq-transport/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [0.10.0](https://github.com/englishm/moq-rs/compare/moq-transport-v0.9.0...moq-transport-v0.10.0) - 2025-02-24 10 | 11 | ### Fixed 12 | 13 | - fixed clippy warning 14 | 15 | ### Other 16 | 17 | - Merge pull request [#32](https://github.com/englishm/moq-rs/pull/32) from englishm/me/draft-07 18 | - added required fields for announce_cancel 19 | - cargo fmt 20 | - Fix linter nit: unused variable 21 | - Cleaned up and uncommmented error.rs, started adding max_subscribe_id 22 | 23 | ## [0.9.0](https://github.com/englishm/moq-rs/compare/moq-transport-v0.8.1...moq-transport-v0.9.0) - 2025-01-16 24 | 25 | ### Fixed 26 | 27 | - fixes to moq-transport, relay compiles 28 | 29 | ### Other 30 | 31 | - cargo fmt 32 | - Fix some clippy warnings 33 | - MaxSubscribeId message coding 34 | - Update SETUP message tests 35 | - Add length of params to SubscribeOk 36 | - Add length field to client and server setup 37 | - Add lengths to control messages 38 | - Renumber stream type ids 39 | - Add payload length to datagrams 40 | - Change type of namespace to tuple 41 | - Add Tuple type 42 | - first stab at subscribe namespace messages 43 | - Add new error type 44 | - Remove object/stream (gone in -06) 45 | - remove comment 46 | - more fixes 47 | - rename groups to subgroups 48 | - Bump target draft version to 06 49 | 50 | ## [0.8.1](https://github.com/englishm/moq-rs/compare/moq-transport-v0.8.0...moq-transport-v0.8.1) - 2024-11-14 51 | 52 | ### Other 53 | 54 | - Defend crash due to probable buffer issues while attempting to decode u8 ([#4](https://github.com/englishm/moq-rs/pull/4)) 55 | 56 | ## [0.8.0](https://github.com/englishm/moq-rs/compare/moq-transport-v0.7.1...moq-transport-v0.8.0) - 2024-10-31 57 | 58 | ### Other 59 | 60 | - Add GroupOrder to SubscribeOk 61 | 62 | ## [0.7.1](https://github.com/englishm/moq-rs/compare/moq-transport-v0.7.0...moq-transport-v0.7.1) - 2024-10-31 63 | 64 | ### Other 65 | 66 | - Fix u8 encoding 67 | 68 | ## [0.5.3](https://github.com/kixelated/moq-rs/compare/moq-transport-v0.5.2...moq-transport-v0.5.3) - 2024-07-24 69 | 70 | ### Other 71 | - Fixed typo in definitions of STREAM_HEADER_TRACK and STREAM_HEADER_GROUP ([#175](https://github.com/kixelated/moq-rs/pull/175)) 72 | 73 | ## [0.5.2](https://github.com/kixelated/moq-rs/compare/moq-transport-v0.5.1...moq-transport-v0.5.2) - 2024-06-03 74 | 75 | ### Other 76 | - Make listings accessible ([#167](https://github.com/kixelated/moq-rs/pull/167)) 77 | -------------------------------------------------------------------------------- /moq-transport/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "moq-transport" 3 | description = "Media over QUIC" 4 | authors = ["Luke Curley"] 5 | repository = "https://github.com/englishm/moq-rs" 6 | license = "MIT OR Apache-2.0" 7 | 8 | version = "0.10.0" 9 | edition = "2021" 10 | 11 | keywords = ["quic", "http3", "webtransport", "media", "live"] 12 | categories = ["multimedia", "network-programming", "web-programming"] 13 | 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [dependencies] 18 | bytes = "1" 19 | thiserror = "1" 20 | tokio = { version = "1", features = ["macros", "io-util", "sync"] } 21 | log = "0.4" 22 | 23 | web-transport = { workspace = true } 24 | 25 | paste = "1" 26 | futures = "0.3" 27 | -------------------------------------------------------------------------------- /moq-transport/README.md: -------------------------------------------------------------------------------- 1 | [![Documentation](https://docs.rs/moq-transport/badge.svg)](https://docs.rs/moq-transport/) 2 | [![Crates.io](https://img.shields.io/crates/v/moq-transport.svg)](https://crates.io/crates/moq-transport) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE-MIT) 4 | 5 | # moq-transport 6 | 7 | A Rust implementation of the proposed IETF standard. 8 | 9 | [Specification](https://datatracker.ietf.org/doc/draft-ietf-moq-transport/) 10 | [Github](https://github.com/moq-wg/moq-transport) 11 | -------------------------------------------------------------------------------- /moq-transport/src/coding/decode.rs: -------------------------------------------------------------------------------- 1 | use super::BoundsExceeded; 2 | use std::{io, string::FromUtf8Error, sync}; 3 | use thiserror::Error; 4 | 5 | pub trait Decode: Sized { 6 | fn decode(buf: &mut B) -> Result; 7 | 8 | // Helper function to make sure we have enough bytes to decode 9 | fn decode_remaining(buf: &mut B, required: usize) -> Result<(), DecodeError> { 10 | let needed = required.saturating_sub(buf.remaining()); 11 | if needed > 0 { 12 | Err(DecodeError::More(needed)) 13 | } else { 14 | Ok(()) 15 | } 16 | } 17 | } 18 | 19 | /// A decode error. 20 | #[derive(Error, Debug, Clone)] 21 | pub enum DecodeError { 22 | #[error("fill buffer")] 23 | More(usize), 24 | 25 | #[error("invalid payload length {0} got {1}")] 26 | InvalidLength(usize, usize), 27 | 28 | #[error("invalid string")] 29 | InvalidString(#[from] FromUtf8Error), 30 | 31 | #[error("invalid message: {0:?}")] 32 | InvalidMessage(u64), 33 | 34 | #[error("invalid role: {0:?}")] 35 | InvalidRole(u64), 36 | 37 | #[error("invalid subscribe location")] 38 | InvalidSubscribeLocation, 39 | 40 | #[error("invalid filter type")] 41 | InvalidFilterType, 42 | 43 | #[error("invalid group order")] 44 | InvalidGroupOrder, 45 | 46 | #[error("invalid object status")] 47 | InvalidObjectStatus, 48 | 49 | #[error("invalid track status code")] 50 | InvalidTrackStatusCode, 51 | 52 | #[error("missing field")] 53 | MissingField, 54 | 55 | #[error("invalid value")] 56 | InvalidValue, 57 | 58 | #[error("varint bounds exceeded")] 59 | BoundsExceeded(#[from] BoundsExceeded), 60 | 61 | // TODO move these to ParamError 62 | #[error("duplicate parameter")] 63 | DupliateParameter, 64 | 65 | #[error("missing parameter")] 66 | MissingParameter, 67 | 68 | #[error("invalid parameter")] 69 | InvalidParameter, 70 | 71 | #[error("io error: {0}")] 72 | Io(sync::Arc), 73 | } 74 | 75 | impl From for DecodeError { 76 | fn from(err: io::Error) -> Self { 77 | Self::Io(sync::Arc::new(err)) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /moq-transport/src/coding/encode.rs: -------------------------------------------------------------------------------- 1 | use std::{io, sync}; 2 | 3 | use super::BoundsExceeded; 4 | 5 | pub trait Encode: Sized { 6 | fn encode(&self, w: &mut W) -> Result<(), EncodeError>; 7 | 8 | // Helper function to make sure we have enough bytes to encode 9 | fn encode_remaining(buf: &mut W, required: usize) -> Result<(), EncodeError> { 10 | let needed = required.saturating_sub(buf.remaining_mut()); 11 | if needed > 0 { 12 | Err(EncodeError::More(needed)) 13 | } else { 14 | Ok(()) 15 | } 16 | } 17 | } 18 | 19 | /// An encode error. 20 | #[derive(thiserror::Error, Debug, Clone)] 21 | pub enum EncodeError { 22 | #[error("short buffer")] 23 | More(usize), 24 | 25 | #[error("varint too large")] 26 | BoundsExceeded(#[from] BoundsExceeded), 27 | 28 | #[error("invalid value")] 29 | InvalidValue, 30 | 31 | #[error("missing field")] 32 | MissingField, 33 | 34 | #[error("i/o error: {0}")] 35 | Io(sync::Arc), 36 | } 37 | 38 | impl From for EncodeError { 39 | fn from(err: io::Error) -> Self { 40 | Self::Io(sync::Arc::new(err)) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /moq-transport/src/coding/mod.rs: -------------------------------------------------------------------------------- 1 | mod decode; 2 | mod encode; 3 | mod params; 4 | mod string; 5 | mod tuple; 6 | mod varint; 7 | 8 | pub use decode::*; 9 | pub use encode::*; 10 | pub use params::*; 11 | pub use tuple::*; 12 | pub use varint::*; 13 | -------------------------------------------------------------------------------- /moq-transport/src/coding/params.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::io::Cursor; 3 | 4 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 5 | 6 | #[derive(Default, Debug, Clone)] 7 | pub struct Params(pub HashMap>); 8 | 9 | impl Decode for Params { 10 | fn decode(mut r: &mut R) -> Result { 11 | let mut params = HashMap::new(); 12 | 13 | // I hate this encoding so much; let me encode my role and get on with my life. 14 | let count = u64::decode(r)?; 15 | for _ in 0..count { 16 | let kind = u64::decode(r)?; 17 | if params.contains_key(&kind) { 18 | return Err(DecodeError::DupliateParameter); 19 | } 20 | 21 | let size = usize::decode(&mut r)?; 22 | Self::decode_remaining(r, size)?; 23 | 24 | // Don't allocate the entire requested size to avoid a possible attack 25 | // Instead, we allocate up to 1024 and keep appending as we read further. 26 | let mut buf = vec![0; size]; 27 | r.copy_to_slice(&mut buf); 28 | 29 | params.insert(kind, buf); 30 | } 31 | 32 | Ok(Params(params)) 33 | } 34 | } 35 | 36 | impl Encode for Params { 37 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 38 | self.0.len().encode(w)?; 39 | 40 | for (kind, value) in self.0.iter() { 41 | kind.encode(w)?; 42 | value.len().encode(w)?; 43 | Self::encode_remaining(w, value.len())?; 44 | w.put_slice(value); 45 | } 46 | 47 | Ok(()) 48 | } 49 | } 50 | 51 | impl Params { 52 | pub fn new() -> Self { 53 | Self::default() 54 | } 55 | 56 | pub fn set(&mut self, kind: u64, p: P) -> Result<(), EncodeError> { 57 | let mut value = Vec::new(); 58 | p.encode(&mut value)?; 59 | self.0.insert(kind, value); 60 | 61 | Ok(()) 62 | } 63 | 64 | pub fn has(&self, kind: u64) -> bool { 65 | self.0.contains_key(&kind) 66 | } 67 | 68 | pub fn get(&mut self, kind: u64) -> Result, DecodeError> { 69 | if let Some(value) = self.0.remove(&kind) { 70 | let mut cursor = Cursor::new(value); 71 | Ok(Some(P::decode(&mut cursor)?)) 72 | } else { 73 | Ok(None) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /moq-transport/src/coding/string.rs: -------------------------------------------------------------------------------- 1 | use super::{Decode, DecodeError, Encode, EncodeError}; 2 | 3 | impl Encode for String { 4 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 5 | self.len().encode(w)?; 6 | Self::encode_remaining(w, self.len())?; 7 | w.put(self.as_ref()); 8 | Ok(()) 9 | } 10 | } 11 | 12 | impl Decode for String { 13 | /// Decode a string with a varint length prefix. 14 | fn decode(r: &mut R) -> Result { 15 | let size = usize::decode(r)?; 16 | Self::decode_remaining(r, size)?; 17 | 18 | let mut buf = vec![0; size]; 19 | r.copy_to_slice(&mut buf); 20 | let str = String::from_utf8(buf)?; 21 | 22 | Ok(str) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /moq-transport/src/coding/tuple.rs: -------------------------------------------------------------------------------- 1 | // 2 | use super::{Decode, DecodeError, Encode, EncodeError}; 3 | use core::hash::{Hash, Hasher}; 4 | /// Tuple Field 5 | #[derive(Clone, Debug, Default)] 6 | pub struct TupleField { 7 | pub value: Vec, 8 | } 9 | 10 | impl Eq for TupleField {} 11 | 12 | impl PartialEq for TupleField { 13 | fn eq(&self, other: &Self) -> bool { 14 | self.value.eq(&other.value) 15 | } 16 | } 17 | 18 | impl Hash for TupleField { 19 | fn hash(&self, state: &mut H) { 20 | self.value.hash(state); 21 | } 22 | } 23 | 24 | impl Decode for TupleField { 25 | fn decode(r: &mut R) -> Result { 26 | let size = usize::decode(r)?; 27 | Self::decode_remaining(r, size)?; 28 | let mut buf = vec![0; size]; 29 | r.copy_to_slice(&mut buf); 30 | Ok(Self { value: buf }) 31 | } 32 | } 33 | 34 | impl Encode for TupleField { 35 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 36 | self.value.len().encode(w)?; 37 | Self::encode_remaining(w, self.value.len())?; 38 | w.put_slice(&self.value); 39 | Ok(()) 40 | } 41 | } 42 | 43 | impl TupleField { 44 | pub fn new() -> Self { 45 | Self::default() 46 | } 47 | 48 | pub fn from_utf8(path: &str) -> Self { 49 | let mut field = TupleField::new(); 50 | field.value = path.as_bytes().to_vec(); 51 | field 52 | } 53 | 54 | pub fn set(&mut self, p: P) -> Result<(), EncodeError> { 55 | let mut value = Vec::new(); 56 | p.encode(&mut value)?; 57 | self.value = value; 58 | Ok(()) 59 | } 60 | 61 | pub fn get(&self) -> Result { 62 | P::decode(&mut bytes::Bytes::from(self.value.clone())) 63 | } 64 | } 65 | 66 | /// Tuple 67 | #[derive(Clone, Debug, Default)] 68 | pub struct Tuple { 69 | pub fields: Vec, 70 | } 71 | 72 | impl Hash for Tuple { 73 | fn hash(&self, state: &mut H) { 74 | self.fields.hash(state); 75 | } 76 | } 77 | 78 | impl Eq for Tuple {} 79 | 80 | impl PartialEq for Tuple { 81 | fn eq(&self, other: &Self) -> bool { 82 | self.fields.eq(&other.fields) 83 | } 84 | } 85 | 86 | impl Decode for Tuple { 87 | fn decode(r: &mut R) -> Result { 88 | let count = u64::decode(r)? as usize; 89 | let mut fields = Vec::new(); 90 | for _ in 0..count { 91 | fields.push(TupleField::decode(r)?); 92 | } 93 | Ok(Self { fields }) 94 | } 95 | } 96 | 97 | impl Encode for Tuple { 98 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 99 | self.fields.len().encode(w)?; 100 | for field in &self.fields { 101 | field.encode(w)?; 102 | } 103 | Ok(()) 104 | } 105 | } 106 | 107 | impl Tuple { 108 | pub fn new() -> Self { 109 | Self::default() 110 | } 111 | 112 | pub fn add(&mut self, field: TupleField) { 113 | self.fields.push(field); 114 | } 115 | 116 | pub fn get(&self, index: usize) -> Result { 117 | self.fields[index].get() 118 | } 119 | 120 | pub fn set(&mut self, index: usize, f: TupleField) -> Result<(), EncodeError> { 121 | self.fields[index].set(f) 122 | } 123 | 124 | pub fn clear(&mut self) { 125 | self.fields.clear(); 126 | } 127 | 128 | pub fn from_utf8_path(path: &str) -> Self { 129 | let mut tuple = Tuple::new(); 130 | for part in path.split('/') { 131 | tuple.add(TupleField::from_utf8(part)); 132 | } 133 | tuple 134 | } 135 | 136 | pub fn to_utf8_path(&self) -> String { 137 | let mut path = String::new(); 138 | for field in &self.fields { 139 | path.push('/'); 140 | path.push_str(&String::from_utf8_lossy(&field.value)); 141 | } 142 | path 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /moq-transport/src/data/datagram.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | use crate::data::ObjectStatus; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct Datagram { 6 | // The subscribe ID. 7 | pub subscribe_id: u64, 8 | 9 | // The track alias. 10 | pub track_alias: u64, 11 | 12 | // The sequence number within the track. 13 | pub group_id: u64, 14 | 15 | // The object ID within the group. 16 | pub object_id: u64, 17 | 18 | // Publisher priority, where **smaller** values are sent first. 19 | pub publisher_priority: u8, 20 | 21 | // Object status 22 | pub object_status: ObjectStatus, 23 | 24 | // The payload length. 25 | pub payload_len: u64, 26 | 27 | // The payload. 28 | pub payload: bytes::Bytes, 29 | } 30 | 31 | impl Decode for Datagram { 32 | fn decode(r: &mut R) -> Result { 33 | let subscribe_id = u64::decode(r)?; 34 | let track_alias = u64::decode(r)?; 35 | let group_id = u64::decode(r)?; 36 | let object_id = u64::decode(r)?; 37 | let publisher_priority = u8::decode(r)?; 38 | let object_status = ObjectStatus::decode(r)?; 39 | let payload_len = u64::decode(r)?; 40 | if payload_len != r.remaining() as u64 { 41 | return Err(DecodeError::InvalidLength( 42 | payload_len as usize, 43 | r.remaining(), 44 | )); 45 | } 46 | let payload = r.copy_to_bytes(r.remaining()); 47 | 48 | Ok(Self { 49 | subscribe_id, 50 | track_alias, 51 | group_id, 52 | object_id, 53 | publisher_priority, 54 | object_status, 55 | payload_len, 56 | payload, 57 | }) 58 | } 59 | } 60 | 61 | impl Encode for Datagram { 62 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 63 | self.subscribe_id.encode(w)?; 64 | self.track_alias.encode(w)?; 65 | self.group_id.encode(w)?; 66 | self.object_id.encode(w)?; 67 | self.publisher_priority.encode(w)?; 68 | self.object_status.encode(w)?; 69 | self.payload_len.encode(w)?; 70 | Self::encode_remaining(w, self.payload.len())?; 71 | w.put_slice(&self.payload); 72 | 73 | Ok(()) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /moq-transport/src/data/header.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | use paste::paste; 3 | use std::fmt; 4 | 5 | use super::{SubgroupHeader, TrackHeader}; 6 | 7 | // Use a macro to generate the message types rather than copy-paste. 8 | // This implements a decode/encode method that uses the specified type. 9 | macro_rules! header_types { 10 | {$($name:ident = $val:expr,)*} => { 11 | /// All supported message types. 12 | #[derive(Clone)] 13 | pub enum Header { 14 | $($name(paste! { [<$name Header>] })),* 15 | } 16 | 17 | impl Decode for Header { 18 | fn decode(r: &mut R) -> Result { 19 | let t = u64::decode(r)?; 20 | 21 | match t { 22 | $($val => { 23 | let msg = ] }>::decode(r)?; 24 | Ok(Self::$name(msg)) 25 | })* 26 | _ => Err(DecodeError::InvalidMessage(t)), 27 | } 28 | } 29 | } 30 | 31 | impl Encode for Header { 32 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 33 | match self { 34 | $(Self::$name(ref m) => { 35 | self.id().encode(w)?; 36 | m.encode(w) 37 | },)* 38 | } 39 | } 40 | } 41 | 42 | impl Header { 43 | pub fn id(&self) -> u64 { 44 | match self { 45 | $(Self::$name(_) => { 46 | $val 47 | },)* 48 | } 49 | } 50 | 51 | pub fn subscribe_id(&self) -> u64 { 52 | match self { 53 | $(Self::$name(o) => o.subscribe_id,)* 54 | } 55 | } 56 | 57 | pub fn track_alias(&self) -> u64 { 58 | match self { 59 | $(Self::$name(o) => o.track_alias,)* 60 | } 61 | } 62 | 63 | pub fn publisher_priority(&self) -> u8 { 64 | match self { 65 | $(Self::$name(o) => o.publisher_priority,)* 66 | } 67 | } 68 | } 69 | 70 | $(impl From] }> for Header { 71 | fn from(m: paste! { [<$name Header>] }) -> Self { 72 | Self::$name(m) 73 | } 74 | })* 75 | 76 | impl fmt::Debug for Header { 77 | // Delegate to the message formatter 78 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 79 | match self { 80 | $(Self::$name(ref m) => m.fmt(f),)* 81 | } 82 | } 83 | } 84 | } 85 | } 86 | 87 | // Each stream type is prefixed with the given VarInt type. 88 | // https://www.ietf.org/archive/id/draft-ietf-moq-transport-06.html#section-7 89 | header_types! { 90 | //Datagram = 0x1, 91 | Track = 0x2, 92 | Subgroup = 0x4, 93 | } 94 | -------------------------------------------------------------------------------- /moq-transport/src/data/mod.rs: -------------------------------------------------------------------------------- 1 | mod datagram; 2 | mod header; 3 | mod object; 4 | mod subgroup; 5 | mod track; 6 | 7 | pub use datagram::*; 8 | pub use header::*; 9 | pub use object::*; 10 | pub use subgroup::*; 11 | pub use track::*; 12 | -------------------------------------------------------------------------------- /moq-transport/src/data/object.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | 3 | #[derive(Clone, Copy, Debug, PartialEq)] 4 | pub enum ObjectStatus { 5 | Object = 0x0, 6 | ObjectDoesNotExist = 0x1, 7 | EndOfGroup = 0x3, 8 | EndOfTrack = 0x4, 9 | EndOfSubgroup = 0x5, 10 | } 11 | 12 | impl Decode for ObjectStatus { 13 | fn decode(r: &mut B) -> Result { 14 | match u64::decode(r)? { 15 | 0x0 => Ok(Self::Object), 16 | 0x1 => Ok(Self::ObjectDoesNotExist), 17 | 0x3 => Ok(Self::EndOfGroup), 18 | 0x4 => Ok(Self::EndOfTrack), 19 | 0x5 => Ok(Self::EndOfSubgroup), 20 | _ => Err(DecodeError::InvalidObjectStatus), 21 | } 22 | } 23 | } 24 | 25 | impl Encode for ObjectStatus { 26 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 27 | match self { 28 | Self::Object => (0x0_u64).encode(w), 29 | Self::ObjectDoesNotExist => (0x1_u64).encode(w), 30 | Self::EndOfGroup => (0x3_u64).encode(w), 31 | Self::EndOfTrack => (0x4_u64).encode(w), 32 | Self::EndOfSubgroup => (0x5_u64).encode(w), 33 | } 34 | } 35 | } 36 | 37 | #[derive(Clone, Debug)] 38 | pub struct ObjectHeader { 39 | // The subscribe ID. 40 | pub subscribe_id: u64, 41 | 42 | // The track alias. 43 | pub track_alias: u64, 44 | 45 | // The sequence number within the track. 46 | pub group_id: u64, 47 | 48 | // The sequence number within the group. 49 | pub object_id: u64, 50 | 51 | // Publisher priority, where **smaller** values are sent first. 52 | pub publisher_priority: u8, 53 | 54 | // The object status 55 | pub object_status: ObjectStatus, 56 | } 57 | 58 | impl Decode for ObjectHeader { 59 | fn decode(r: &mut R) -> Result { 60 | Ok(Self { 61 | subscribe_id: u64::decode(r)?, 62 | track_alias: u64::decode(r)?, 63 | group_id: u64::decode(r)?, 64 | object_id: u64::decode(r)?, 65 | publisher_priority: u8::decode(r)?, 66 | object_status: ObjectStatus::decode(r)?, 67 | }) 68 | } 69 | } 70 | 71 | impl Encode for ObjectHeader { 72 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 73 | self.subscribe_id.encode(w)?; 74 | self.track_alias.encode(w)?; 75 | self.group_id.encode(w)?; 76 | self.object_id.encode(w)?; 77 | self.publisher_priority.encode(w)?; 78 | self.object_status.encode(w)?; 79 | 80 | Ok(()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /moq-transport/src/data/subgroup.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | use crate::data::ObjectStatus; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct SubgroupHeader { 6 | // The subscribe ID. 7 | pub subscribe_id: u64, 8 | 9 | // The track alias. 10 | pub track_alias: u64, 11 | 12 | // The group sequence number 13 | pub group_id: u64, 14 | 15 | // The subgroup sequence number 16 | pub subgroup_id: u64, 17 | 18 | // Publisher priority, where **smaller** values are sent first. 19 | pub publisher_priority: u8, 20 | } 21 | 22 | impl Decode for SubgroupHeader { 23 | fn decode(r: &mut R) -> Result { 24 | Ok(Self { 25 | subscribe_id: u64::decode(r)?, 26 | track_alias: u64::decode(r)?, 27 | group_id: u64::decode(r)?, 28 | subgroup_id: u64::decode(r)?, 29 | publisher_priority: u8::decode(r)?, 30 | }) 31 | } 32 | } 33 | 34 | impl Encode for SubgroupHeader { 35 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 36 | self.subscribe_id.encode(w)?; 37 | self.track_alias.encode(w)?; 38 | self.group_id.encode(w)?; 39 | self.subgroup_id.encode(w)?; 40 | self.publisher_priority.encode(w)?; 41 | 42 | Ok(()) 43 | } 44 | } 45 | 46 | #[derive(Clone, Debug)] 47 | pub struct SubgroupObject { 48 | pub object_id: u64, 49 | pub size: usize, 50 | pub status: ObjectStatus, 51 | } 52 | 53 | impl Decode for SubgroupObject { 54 | fn decode(r: &mut R) -> Result { 55 | let object_id = u64::decode(r)?; 56 | let size = usize::decode(r)?; 57 | 58 | // If the size is 0, then the status is sent explicitly. 59 | // Otherwise, the status is assumed to be 0x0 (Object). 60 | let status = if size == 0 { 61 | ObjectStatus::decode(r)? 62 | } else { 63 | ObjectStatus::Object 64 | }; 65 | 66 | Ok(Self { 67 | object_id, 68 | size, 69 | status, 70 | }) 71 | } 72 | } 73 | 74 | impl Encode for SubgroupObject { 75 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 76 | self.object_id.encode(w)?; 77 | self.size.encode(w)?; 78 | 79 | // If the size is 0, then the status is sent explicitly. 80 | // Otherwise, the status is assumed to be 0x0 (Object). 81 | if self.size == 0 { 82 | self.status.encode(w)?; 83 | } 84 | 85 | Ok(()) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /moq-transport/src/data/track.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | use crate::data::ObjectStatus; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct TrackHeader { 6 | // The subscribe ID. 7 | pub subscribe_id: u64, 8 | 9 | // The track ID. 10 | pub track_alias: u64, 11 | 12 | // Publisher priority, where **smaller** values are sent first. 13 | pub publisher_priority: u8, 14 | } 15 | 16 | impl Decode for TrackHeader { 17 | fn decode(r: &mut R) -> Result { 18 | Ok(Self { 19 | subscribe_id: u64::decode(r)?, 20 | track_alias: u64::decode(r)?, 21 | publisher_priority: u8::decode(r)?, 22 | }) 23 | } 24 | } 25 | 26 | impl Encode for TrackHeader { 27 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 28 | self.subscribe_id.encode(w)?; 29 | self.track_alias.encode(w)?; 30 | self.publisher_priority.encode(w)?; 31 | 32 | Ok(()) 33 | } 34 | } 35 | 36 | #[derive(Clone, Debug)] 37 | pub struct TrackObject { 38 | pub group_id: u64, 39 | pub object_id: u64, 40 | pub size: usize, 41 | pub status: ObjectStatus, 42 | } 43 | 44 | impl Decode for TrackObject { 45 | fn decode(r: &mut R) -> Result { 46 | let group_id = u64::decode(r)?; 47 | 48 | let object_id = u64::decode(r)?; 49 | let size = usize::decode(r)?; 50 | 51 | // If the size is 0, then the status is sent explicitly. 52 | // Otherwise, the status is assumed to be 0x0 (Object). 53 | let status = if size == 0 { 54 | ObjectStatus::decode(r)? 55 | } else { 56 | ObjectStatus::Object 57 | }; 58 | 59 | Ok(Self { 60 | group_id, 61 | object_id, 62 | size, 63 | status, 64 | }) 65 | } 66 | } 67 | 68 | impl Encode for TrackObject { 69 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 70 | self.group_id.encode(w)?; 71 | self.object_id.encode(w)?; 72 | self.size.encode(w)?; 73 | 74 | // If the size is 0, then the status is sent explicitly. 75 | // Otherwise, the status is assumed to be 0x0 (Object). 76 | if self.size == 0 { 77 | self.status.encode(w)?; 78 | } 79 | 80 | Ok(()) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /moq-transport/src/error.rs: -------------------------------------------------------------------------------- 1 | /// An error that causes the session to close. 2 | #[derive(thiserror::Error, Debug)] 3 | pub enum SessionError { 4 | // Official error codes 5 | #[error("no error")] 6 | NoError, 7 | 8 | #[error("internal error")] 9 | InternalError, 10 | 11 | #[error("unauthorized")] 12 | Unauthorized, 13 | 14 | #[error("protocol violation")] 15 | ProtocolViolation, 16 | 17 | #[error("duplicate track alias")] 18 | DuplicateTrackAlias, 19 | 20 | #[error("parameter length mismatch")] 21 | ParameterLengthMismatch, 22 | 23 | #[error("too many subscribes")] 24 | TooManySubscribes, 25 | 26 | #[error("goaway timeout")] 27 | GoawayTimeout, 28 | 29 | #[error("unknown error: code={0}")] 30 | Unknown(u64), 31 | // Unofficial error codes 32 | } 33 | 34 | pub trait MoqError { 35 | /// An integer code that is sent over the wire. 36 | fn code(&self) -> u64; 37 | } 38 | 39 | impl MoqError for SessionError { 40 | /// An integer code that is sent over the wire. 41 | fn code(&self) -> u64 { 42 | match self { 43 | // Official error codes 44 | Self::NoError => 0x0, 45 | Self::InternalError => 0x1, 46 | Self::Unauthorized => 0x2, 47 | Self::ProtocolViolation => 0x3, 48 | Self::DuplicateTrackAlias => 0x4, 49 | Self::ParameterLengthMismatch => 0x5, 50 | Self::TooManySubscribes => 0x6, 51 | Self::GoawayTimeout => 0x10, 52 | Self::Unknown(code) => *code, 53 | // Unofficial error codes 54 | } 55 | } 56 | } 57 | 58 | /// An error that causes the subscribe to be rejected immediately. 59 | #[derive(thiserror::Error, Debug)] 60 | pub enum SubscribeError { 61 | // Official error codes 62 | #[error("internal error")] 63 | InternalError, 64 | 65 | #[error("invalid range")] 66 | InvalidRange, 67 | 68 | #[error("retry track alias")] 69 | RetryTrackAlias, 70 | 71 | #[error("track does not exist")] 72 | TrackDoesNotExist, 73 | 74 | #[error("unauthorized")] 75 | Unauthorized, 76 | 77 | #[error("timeout")] 78 | Timeout, 79 | 80 | #[error("unknown error: code={0}")] 81 | Unknown(u64), 82 | // Unofficial error codes 83 | } 84 | 85 | impl MoqError for SubscribeError { 86 | /// An integer code that is sent over the wire. 87 | fn code(&self) -> u64 { 88 | match self { 89 | // Official error codes 90 | Self::InternalError => 0x0, 91 | Self::InvalidRange => 0x1, 92 | Self::RetryTrackAlias => 0x2, 93 | Self::TrackDoesNotExist => 0x3, 94 | Self::Unauthorized => 0x4, 95 | Self::Timeout => 0x5, 96 | Self::Unknown(code) => *code, 97 | // Unofficial error codes 98 | } 99 | } 100 | } 101 | 102 | /// An error that causes the subscribe to be terminated. 103 | #[derive(thiserror::Error, Debug)] 104 | pub enum SubscribeDone { 105 | // Official error codes 106 | #[error("unsubscribed")] 107 | Unsubscribed, 108 | 109 | #[error("internal error")] 110 | InternalError, 111 | 112 | // TODO This should be in SubscribeError 113 | #[error("unauthorized")] 114 | Unauthorized, 115 | 116 | #[error("track ended")] 117 | TrackEnded, 118 | 119 | // TODO What the heck is this? 120 | #[error("subscription ended")] 121 | SubscriptionEnded, 122 | 123 | #[error("going away")] 124 | GoingAway, 125 | 126 | #[error("expired")] 127 | Expired, 128 | 129 | #[error("unknown error: code={0}")] 130 | Unknown(u64), 131 | } 132 | 133 | impl From for SubscribeDone { 134 | fn from(code: u64) -> Self { 135 | match code { 136 | 0x0 => Self::Unsubscribed, 137 | 0x1 => Self::InternalError, 138 | 0x2 => Self::Unauthorized, 139 | 0x3 => Self::TrackEnded, 140 | 0x4 => Self::SubscriptionEnded, 141 | 0x5 => Self::GoingAway, 142 | 0x6 => Self::Expired, 143 | _ => Self::Unknown(code), 144 | } 145 | } 146 | } 147 | 148 | impl MoqError for SubscribeDone { 149 | /// An integer code that is sent over the wire. 150 | fn code(&self) -> u64 { 151 | match self { 152 | // Official error codes 153 | Self::Unsubscribed => 0x0, 154 | Self::InternalError => 0x1, 155 | Self::Unauthorized => 0x2, 156 | Self::TrackEnded => 0x3, 157 | Self::SubscriptionEnded => 0x4, 158 | Self::GoingAway => 0x5, 159 | Self::Expired => 0x6, 160 | Self::Unknown(code) => *code, 161 | // Unofficial error codes 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /moq-transport/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! An implementation of the MoQ Transport protocol. 2 | //! 3 | //! MoQ Transport is a pub/sub protocol over QUIC. 4 | //! While originally designed for live media, MoQ Transport is generic and can be used for other live applications. 5 | //! The specification is a work in progress and will change. 6 | //! See the [specification](https://datatracker.ietf.org/doc/draft-ietf-moq-transport/) and [github](https://github.com/moq-wg/moq-transport) for any updates. 7 | pub mod coding; 8 | pub mod data; 9 | pub mod error; 10 | pub mod message; 11 | pub mod serve; 12 | pub mod session; 13 | pub mod setup; 14 | pub mod watch; 15 | -------------------------------------------------------------------------------- /moq-transport/src/message/announce.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Params, Tuple}; 2 | 3 | /// Sent by the publisher to announce the availability of a group of tracks. 4 | #[derive(Clone, Debug)] 5 | pub struct Announce { 6 | /// The track namespace 7 | pub namespace: Tuple, 8 | 9 | /// Optional parameters 10 | pub params: Params, 11 | } 12 | 13 | impl Decode for Announce { 14 | fn decode(r: &mut R) -> Result { 15 | let namespace = Tuple::decode(r)?; 16 | let params = Params::decode(r)?; 17 | 18 | Ok(Self { namespace, params }) 19 | } 20 | } 21 | 22 | impl Encode for Announce { 23 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 24 | self.namespace.encode(w)?; 25 | self.params.encode(w)?; 26 | 27 | Ok(()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /moq-transport/src/message/announce_cancel.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Tuple}; 2 | 3 | /// Sent by the subscriber to reject an Announce after ANNOUNCE_OK 4 | #[derive(Clone, Debug)] 5 | pub struct AnnounceCancel { 6 | // Echo back the namespace that was reset 7 | pub namespace: Tuple, 8 | // An error code. 9 | pub error_code: u64, 10 | // An optional, human-readable reason. 11 | pub reason_phrase: String, 12 | } 13 | 14 | impl Decode for AnnounceCancel { 15 | fn decode(r: &mut R) -> Result { 16 | let namespace = Tuple::decode(r)?; 17 | let error_code = u64::decode(r)?; 18 | let reason_phrase = String::decode(r)?; 19 | 20 | Ok(Self { 21 | namespace, 22 | error_code, 23 | reason_phrase, 24 | }) 25 | } 26 | } 27 | 28 | impl Encode for AnnounceCancel { 29 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 30 | self.namespace.encode(w)?; 31 | self.error_code.encode(w)?; 32 | self.reason_phrase.encode(w)?; 33 | 34 | Ok(()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /moq-transport/src/message/announce_error.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Tuple}; 2 | 3 | /// Sent by the subscriber to reject an Announce. 4 | #[derive(Clone, Debug)] 5 | pub struct AnnounceError { 6 | // Echo back the namespace that was reset 7 | pub namespace: Tuple, 8 | 9 | // An error code. 10 | pub error_code: u64, 11 | 12 | // An optional, human-readable reason. 13 | pub reason_phrase: String, 14 | } 15 | 16 | impl Decode for AnnounceError { 17 | fn decode(r: &mut R) -> Result { 18 | let namespace = Tuple::decode(r)?; 19 | let error_code = u64::decode(r)?; 20 | let reason_phrase = String::decode(r)?; 21 | 22 | Ok(Self { 23 | namespace, 24 | error_code, 25 | reason_phrase, 26 | }) 27 | } 28 | } 29 | 30 | impl Encode for AnnounceError { 31 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 32 | self.namespace.encode(w)?; 33 | self.error_code.encode(w)?; 34 | self.reason_phrase.encode(w)?; 35 | 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /moq-transport/src/message/announce_ok.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Tuple}; 2 | 3 | /// Sent by the subscriber to accept an Announce. 4 | #[derive(Clone, Debug)] 5 | pub struct AnnounceOk { 6 | // Echo back the namespace that was announced. 7 | // TODO Propose using an ID to save bytes. 8 | pub namespace: Tuple, 9 | } 10 | 11 | impl Decode for AnnounceOk { 12 | fn decode(r: &mut R) -> Result { 13 | let namespace = Tuple::decode(r)?; 14 | Ok(Self { namespace }) 15 | } 16 | } 17 | 18 | impl Encode for AnnounceOk { 19 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 20 | self.namespace.encode(w) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /moq-transport/src/message/fetch.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Params, Tuple}; 2 | use crate::message::GroupOrder; 3 | 4 | /// Sent by the subscriber to request to request a range 5 | /// of already published objects within a track. 6 | #[derive(Clone, Debug)] 7 | pub struct Fetch { 8 | /// The subscription ID 9 | pub id: u64, 10 | 11 | /// Track properties 12 | pub track_namespace: Tuple, 13 | pub track_name: String, 14 | 15 | /// Subscriber Priority 16 | pub subscriber_priority: u8, 17 | 18 | pub group_order: GroupOrder, 19 | 20 | /// The start/end group/object. 21 | pub start_group: u64, 22 | pub start_object: u64, 23 | pub end_group: u64, 24 | pub end_object: u64, 25 | 26 | /// Optional parameters 27 | pub params: Params, 28 | } 29 | 30 | impl Decode for Fetch { 31 | fn decode(r: &mut R) -> Result { 32 | let id = u64::decode(r)?; 33 | 34 | let track_namespace = Tuple::decode(r)?; 35 | let track_name = String::decode(r)?; 36 | 37 | let subscriber_priority = u8::decode(r)?; 38 | 39 | let group_order = GroupOrder::decode(r)?; 40 | 41 | let start_group = u64::decode(r)?; 42 | let start_object = u64::decode(r)?; 43 | let end_group = u64::decode(r)?; 44 | let end_object = u64::decode(r)?; 45 | 46 | let params = Params::decode(r)?; 47 | 48 | Ok(Self { 49 | id, 50 | track_namespace, 51 | track_name, 52 | subscriber_priority, 53 | group_order, 54 | start_group, 55 | start_object, 56 | end_group, 57 | end_object, 58 | params, 59 | }) 60 | } 61 | } 62 | 63 | impl Encode for Fetch { 64 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 65 | self.id.encode(w)?; 66 | 67 | self.track_namespace.encode(w)?; 68 | self.track_name.encode(w)?; 69 | 70 | self.subscriber_priority.encode(w)?; 71 | 72 | self.group_order.encode(w)?; 73 | 74 | self.start_group.encode(w)?; 75 | self.start_object.encode(w)?; 76 | self.end_group.encode(w)?; 77 | self.end_object.encode(w)?; 78 | 79 | self.params.encode(w)?; 80 | 81 | Ok(()) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /moq-transport/src/message/fetch_cancel.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | 3 | /// A subscriber issues a FETCH_CANCEL message to a publisher indicating it is 4 | /// no longer interested in receiving Objects for the fetch specified by 'Subscribe ID'. 5 | #[derive(Clone, Debug)] 6 | pub struct FetchCancel { 7 | /// The subscription ID 8 | pub id: u64, 9 | } 10 | 11 | impl Decode for FetchCancel { 12 | fn decode(r: &mut R) -> Result { 13 | let id = u64::decode(r)?; 14 | Ok(Self { id }) 15 | } 16 | } 17 | 18 | impl Encode for FetchCancel { 19 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 20 | self.id.encode(w)?; 21 | 22 | Ok(()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /moq-transport/src/message/fetch_error.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | 3 | /// Sent by the server to indicate that the client should connect to a different server. 4 | #[derive(Clone, Debug)] 5 | pub struct FetchError { 6 | /// The ID for this subscription. 7 | pub id: u64, 8 | 9 | /// An error code. 10 | pub code: u64, 11 | 12 | /// An optional, human-readable reason. 13 | pub reason: String, 14 | } 15 | 16 | impl Decode for FetchError { 17 | fn decode(r: &mut R) -> Result { 18 | let id = u64::decode(r)?; 19 | 20 | let code = u64::decode(r)?; 21 | let reason = String::decode(r)?; 22 | 23 | Ok(Self { id, code, reason }) 24 | } 25 | } 26 | 27 | impl Encode for FetchError { 28 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 29 | self.id.encode(w)?; 30 | 31 | self.code.encode(w)?; 32 | self.reason.encode(w)?; 33 | 34 | Ok(()) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /moq-transport/src/message/fetch_ok.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Params}; 2 | 3 | /// A publisher sends a FETCH_OK control message in response to successful fetches. 4 | #[derive(Clone, Debug)] 5 | pub struct FetchOk { 6 | /// The subscription ID 7 | pub id: u64, 8 | 9 | /// Order groups will be delivered in 10 | pub group_order: u8, 11 | 12 | /// True if all objects have been published on this track 13 | pub end_of_track: u8, 14 | 15 | /// The largest Group ID available for this track (last if end_of_track) 16 | pub largest_group_id: u64, 17 | /// The largest Object ID available within the largest Group ID for this track (last if end_of_track) 18 | pub largest_object_id: u64, 19 | 20 | /// Optional parameters 21 | pub params: Params, 22 | } 23 | 24 | impl Decode for FetchOk { 25 | fn decode(r: &mut R) -> Result { 26 | let id = u64::decode(r)?; 27 | 28 | let group_order = u8::decode(r)?; 29 | 30 | let end_of_track = u8::decode(r)?; 31 | 32 | let largest_group_id = u64::decode(r)?; 33 | let largest_object_id = u64::decode(r)?; 34 | 35 | let params = Params::decode(r)?; 36 | 37 | Ok(Self { 38 | id, 39 | group_order, 40 | end_of_track, 41 | largest_group_id, 42 | largest_object_id, 43 | params, 44 | }) 45 | } 46 | } 47 | 48 | impl Encode for FetchOk { 49 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 50 | self.id.encode(w)?; 51 | 52 | self.group_order.encode(w)?; 53 | 54 | self.end_of_track.encode(w)?; 55 | 56 | self.largest_group_id.encode(w)?; 57 | self.largest_object_id.encode(w)?; 58 | 59 | self.params.encode(w)?; 60 | 61 | Ok(()) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /moq-transport/src/message/filter_type.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | 3 | /// Filter Types 4 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-04.html#name-filter-types 5 | #[derive(Clone, Debug, PartialEq)] 6 | pub enum FilterType { 7 | LatestGroup = 0x1, 8 | LatestObject = 0x2, 9 | AbsoluteStart = 0x3, 10 | AbsoluteRange = 0x4, 11 | } 12 | 13 | impl Encode for FilterType { 14 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 15 | match self { 16 | Self::LatestGroup => (0x1_u64).encode(w), 17 | Self::LatestObject => (0x2_u64).encode(w), 18 | Self::AbsoluteStart => (0x3_u64).encode(w), 19 | Self::AbsoluteRange => (0x4_u64).encode(w), 20 | } 21 | } 22 | } 23 | 24 | impl Decode for FilterType { 25 | fn decode(r: &mut R) -> Result { 26 | match u64::decode(r)? { 27 | 0x01 => Ok(Self::LatestGroup), 28 | 0x02 => Ok(Self::LatestObject), 29 | 0x03 => Ok(Self::AbsoluteStart), 30 | 0x04 => Ok(Self::AbsoluteRange), 31 | _ => Err(DecodeError::InvalidFilterType), 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /moq-transport/src/message/go_away.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | 3 | /// Sent by the server to indicate that the client should connect to a different server. 4 | #[derive(Clone, Debug)] 5 | pub struct GoAway { 6 | pub url: String, 7 | } 8 | 9 | impl Decode for GoAway { 10 | fn decode(r: &mut R) -> Result { 11 | let url = String::decode(r)?; 12 | Ok(Self { url }) 13 | } 14 | } 15 | 16 | impl Encode for GoAway { 17 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 18 | self.url.encode(w) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /moq-transport/src/message/group_order.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | 3 | /// Group Order 4 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-05.html#section-6.4.2-4.6.1 5 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-05.html#priorities 6 | #[derive(Clone, Debug, PartialEq)] 7 | pub enum GroupOrder { 8 | Publisher = 0x0, 9 | Ascending = 0x1, 10 | Descending = 0x2, 11 | } 12 | 13 | impl Encode for GroupOrder { 14 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 15 | match self { 16 | Self::Publisher => (0x0_u8).encode(w), 17 | Self::Ascending => (0x1_u8).encode(w), 18 | Self::Descending => (0x2_u8).encode(w), 19 | } 20 | } 21 | } 22 | 23 | impl Decode for GroupOrder { 24 | fn decode(r: &mut R) -> Result { 25 | match u8::decode(r)? { 26 | 0x0 => Ok(Self::Publisher), 27 | 0x1 => Ok(Self::Ascending), 28 | 0x2 => Ok(Self::Descending), 29 | _ => Err(DecodeError::InvalidGroupOrder), 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /moq-transport/src/message/max_subscribe_id.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | 3 | /// Sent by the publisher to update the max allowed subscription ID for the session. 4 | #[derive(Clone, Debug)] 5 | pub struct MaxSubscribeId { 6 | /// The max allowed subscription ID 7 | pub id: u64, 8 | } 9 | 10 | impl Decode for MaxSubscribeId { 11 | fn decode(r: &mut R) -> Result { 12 | let id = u64::decode(r)?; 13 | 14 | Ok(Self { id }) 15 | } 16 | } 17 | 18 | impl Encode for MaxSubscribeId { 19 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 20 | self.id.encode(w)?; 21 | 22 | Ok(()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /moq-transport/src/message/publisher.rs: -------------------------------------------------------------------------------- 1 | use crate::message::{self, Message}; 2 | use std::fmt; 3 | 4 | macro_rules! publisher_msgs { 5 | {$($name:ident,)*} => { 6 | #[derive(Clone)] 7 | pub enum Publisher { 8 | $($name(message::$name)),* 9 | } 10 | 11 | $(impl From for Publisher { 12 | fn from(msg: message::$name) -> Self { 13 | Publisher::$name(msg) 14 | } 15 | })* 16 | 17 | impl From for Message { 18 | fn from(p: Publisher) -> Self { 19 | match p { 20 | $(Publisher::$name(m) => Message::$name(m),)* 21 | } 22 | } 23 | } 24 | 25 | impl TryFrom for Publisher { 26 | type Error = Message; 27 | 28 | fn try_from(m: Message) -> Result { 29 | match m { 30 | $(Message::$name(m) => Ok(Publisher::$name(m)),)* 31 | _ => Err(m), 32 | } 33 | } 34 | } 35 | 36 | impl fmt::Debug for Publisher { 37 | // Delegate to the message formatter 38 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 39 | match self { 40 | $(Self::$name(ref m) => m.fmt(f),)* 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | publisher_msgs! { 48 | Announce, 49 | Unannounce, 50 | SubscribeOk, 51 | SubscribeError, 52 | SubscribeDone, 53 | MaxSubscribeId, 54 | TrackStatus, 55 | FetchOk, 56 | FetchError, 57 | } 58 | -------------------------------------------------------------------------------- /moq-transport/src/message/subscribe_done.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | 3 | /// Sent by the publisher to cleanly terminate a Subscribe. 4 | #[derive(Clone, Debug)] 5 | pub struct SubscribeDone { 6 | /// The ID for this subscription. 7 | pub id: u64, 8 | 9 | /// The error code 10 | pub code: u64, 11 | 12 | /// An optional error reason 13 | pub reason: String, 14 | 15 | /// The final group/object sent on this subscription. 16 | pub last: Option<(u64, u64)>, 17 | } 18 | 19 | impl Decode for SubscribeDone { 20 | fn decode(r: &mut R) -> Result { 21 | let id = u64::decode(r)?; 22 | let code = u64::decode(r)?; 23 | let reason = String::decode(r)?; 24 | 25 | Self::decode_remaining(r, 1)?; 26 | let last = match r.get_u8() { 27 | 0 => None, 28 | 1 => Some((u64::decode(r)?, u64::decode(r)?)), 29 | _ => return Err(DecodeError::InvalidValue), 30 | }; 31 | 32 | Ok(Self { 33 | id, 34 | code, 35 | reason, 36 | last, 37 | }) 38 | } 39 | } 40 | 41 | impl Encode for SubscribeDone { 42 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 43 | self.id.encode(w)?; 44 | self.code.encode(w)?; 45 | self.reason.encode(w)?; 46 | 47 | Self::encode_remaining(w, 1)?; 48 | 49 | if let Some((group, object)) = self.last { 50 | w.put_u8(1); 51 | group.encode(w)?; 52 | object.encode(w)?; 53 | } else { 54 | w.put_u8(0); 55 | } 56 | 57 | Ok(()) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /moq-transport/src/message/subscribe_error.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | 3 | /// Sent by the publisher to reject a Subscribe. 4 | #[derive(Clone, Debug)] 5 | pub struct SubscribeError { 6 | // The ID for this subscription. 7 | pub id: u64, 8 | 9 | // An error code. 10 | pub code: u64, 11 | 12 | // An optional, human-readable reason. 13 | pub reason: String, 14 | 15 | /// An optional track alias, only used when error == Retry Track Alias 16 | pub alias: u64, 17 | } 18 | 19 | impl Decode for SubscribeError { 20 | fn decode(r: &mut R) -> Result { 21 | let id = u64::decode(r)?; 22 | let code = u64::decode(r)?; 23 | let reason = String::decode(r)?; 24 | let alias = u64::decode(r)?; 25 | 26 | Ok(Self { 27 | id, 28 | code, 29 | reason, 30 | alias, 31 | }) 32 | } 33 | } 34 | 35 | impl Encode for SubscribeError { 36 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 37 | self.id.encode(w)?; 38 | self.code.encode(w)?; 39 | self.reason.encode(w)?; 40 | self.alias.encode(w)?; 41 | 42 | Ok(()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /moq-transport/src/message/subscribe_namespace.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Params, Tuple}; 2 | 3 | /// Subscribe Namespace 4 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-06.html#section-6.11 5 | #[derive(Clone, Debug)] 6 | pub struct SubscribeNamespace { 7 | /// The track namespace 8 | pub namespace_prefix: Tuple, 9 | 10 | /// Optional parameters 11 | pub params: Params, 12 | } 13 | 14 | impl Decode for SubscribeNamespace { 15 | fn decode(r: &mut R) -> Result { 16 | let namespace_prefix = Tuple::decode(r)?; 17 | let params = Params::decode(r)?; 18 | 19 | Ok(Self { 20 | namespace_prefix, 21 | params, 22 | }) 23 | } 24 | } 25 | 26 | impl Encode for SubscribeNamespace { 27 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 28 | self.namespace_prefix.encode(w)?; 29 | self.params.encode(w)?; 30 | 31 | Ok(()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /moq-transport/src/message/subscribe_namespace_error.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Tuple}; 2 | 3 | /// Subscribe Namespace Error 4 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-06.html#name-subscribe_namespace_error 5 | #[derive(Clone, Debug)] 6 | pub struct SubscribeNamespaceError { 7 | // Echo back the namespace that was reset 8 | pub namespace_prefix: Tuple, 9 | 10 | // An error code. 11 | pub code: u64, 12 | 13 | // An optional, human-readable reason. 14 | pub reason: String, 15 | } 16 | 17 | impl Decode for SubscribeNamespaceError { 18 | fn decode(r: &mut R) -> Result { 19 | let namespace_prefix = Tuple::decode(r)?; 20 | let code = u64::decode(r)?; 21 | let reason = String::decode(r)?; 22 | 23 | Ok(Self { 24 | namespace_prefix, 25 | code, 26 | reason, 27 | }) 28 | } 29 | } 30 | 31 | impl Encode for SubscribeNamespaceError { 32 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 33 | self.namespace_prefix.encode(w)?; 34 | self.code.encode(w)?; 35 | self.reason.encode(w)?; 36 | 37 | Ok(()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /moq-transport/src/message/subscribe_namespace_ok.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Tuple}; 2 | 3 | /// Subscribe Namespace Ok 4 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-06.html#name-subscribe_namespace_ok 5 | #[derive(Clone, Debug)] 6 | pub struct SubscribeNamespaceOk { 7 | // Echo back the namespace that was announced. 8 | pub namespace_prefix: Tuple, 9 | } 10 | 11 | impl Decode for SubscribeNamespaceOk { 12 | fn decode(r: &mut R) -> Result { 13 | let namespace_prefix = Tuple::decode(r)?; 14 | Ok(Self { namespace_prefix }) 15 | } 16 | } 17 | 18 | impl Encode for SubscribeNamespaceOk { 19 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 20 | self.namespace_prefix.encode(w) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /moq-transport/src/message/subscribe_ok.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | use crate::message::GroupOrder; 3 | 4 | /// Sent by the publisher to accept a Subscribe. 5 | #[derive(Clone, Debug)] 6 | pub struct SubscribeOk { 7 | /// The ID for this subscription. 8 | pub id: u64, 9 | 10 | /// The subscription will expire in this many milliseconds. 11 | pub expires: Option, 12 | 13 | // Order groups will be delivered in 14 | pub group_order: GroupOrder, 15 | 16 | /// The latest group and object for the track. 17 | pub latest: Option<(u64, u64)>, 18 | } 19 | 20 | impl Decode for SubscribeOk { 21 | fn decode(r: &mut R) -> Result { 22 | let id = u64::decode(r)?; 23 | let expires = match u64::decode(r)? { 24 | 0 => None, 25 | expires => Some(expires), 26 | }; 27 | 28 | let group_order = GroupOrder::decode(r)?; 29 | 30 | Self::decode_remaining(r, 1)?; 31 | 32 | let latest = match r.get_u8() { 33 | 0 => None, 34 | 1 => Some((u64::decode(r)?, u64::decode(r)?)), 35 | _ => return Err(DecodeError::InvalidValue), 36 | }; 37 | 38 | // Skip the parameters. 39 | // TODO: Implement parameters for SubscribeOk 40 | let _ = u8::decode(r)?; 41 | 42 | Ok(Self { 43 | id, 44 | expires, 45 | group_order, 46 | latest, 47 | }) 48 | } 49 | } 50 | 51 | impl Encode for SubscribeOk { 52 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 53 | self.id.encode(w)?; 54 | self.expires.unwrap_or(0).encode(w)?; 55 | 56 | self.group_order.encode(w)?; 57 | 58 | Self::encode_remaining(w, 1)?; 59 | 60 | match self.latest { 61 | Some((group, object)) => { 62 | w.put_u8(1); 63 | group.encode(w)?; 64 | object.encode(w)?; 65 | } 66 | None => { 67 | w.put_u8(0); 68 | } 69 | } 70 | 71 | // Add 0 for the length of the parameters 72 | w.put_u8(0); 73 | 74 | Ok(()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /moq-transport/src/message/subscribe_update.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Params, Tuple}; 2 | use crate::message::subscribe::{SubscribeLocation, SubscribePair}; 3 | use crate::message::FilterType; 4 | use crate::message::GroupOrder; 5 | 6 | /// Sent by the subscriber to request all future objects for the given track. 7 | /// 8 | /// Objects will use the provided ID instead of the full track name, to save bytes. 9 | #[derive(Clone, Debug)] 10 | pub struct SubscribeUpdate { 11 | /// The subscription ID 12 | pub id: u64, 13 | 14 | /// Track properties 15 | pub track_alias: u64, // This alias is useless but part of the spec 16 | pub track_namespace: Tuple, 17 | pub track_name: String, 18 | 19 | // Subscriber Priority 20 | pub subscriber_priority: u8, 21 | pub group_order: GroupOrder, 22 | 23 | /// Filter type 24 | pub filter_type: FilterType, 25 | 26 | /// The start/end group/object. (TODO: Make optional) 27 | pub start: Option, // TODO: Make optional 28 | pub end: Option, // TODO: Make optional 29 | 30 | /// Optional parameters 31 | pub params: Params, 32 | } 33 | 34 | impl Decode for SubscribeUpdate { 35 | fn decode(r: &mut R) -> Result { 36 | let id = u64::decode(r)?; 37 | let track_alias = u64::decode(r)?; 38 | let track_namespace = Tuple::decode(r)?; 39 | let track_name = String::decode(r)?; 40 | 41 | let subscriber_priority = u8::decode(r)?; 42 | let group_order = GroupOrder::decode(r)?; 43 | 44 | let filter_type = FilterType::decode(r)?; 45 | 46 | let start: Option; 47 | let end: Option; 48 | match filter_type { 49 | FilterType::AbsoluteStart => { 50 | if r.remaining() < 2 { 51 | return Err(DecodeError::MissingField); 52 | } 53 | start = Some(SubscribePair::decode(r)?); 54 | end = None; 55 | } 56 | FilterType::AbsoluteRange => { 57 | if r.remaining() < 4 { 58 | return Err(DecodeError::MissingField); 59 | } 60 | start = Some(SubscribePair::decode(r)?); 61 | end = Some(SubscribePair::decode(r)?); 62 | } 63 | _ => { 64 | start = None; 65 | end = None; 66 | } 67 | } 68 | 69 | if let Some(s) = &start { 70 | // You can't have a start object without a start group. 71 | if s.group == SubscribeLocation::None && s.object != SubscribeLocation::None { 72 | return Err(DecodeError::InvalidSubscribeLocation); 73 | } 74 | } 75 | if let Some(e) = &end { 76 | // You can't have an end object without an end group. 77 | if e.group == SubscribeLocation::None && e.object != SubscribeLocation::None { 78 | return Err(DecodeError::InvalidSubscribeLocation); 79 | } 80 | } 81 | 82 | // NOTE: There's some more location restrictions in the draft, but they're enforced at a higher level. 83 | 84 | let params = Params::decode(r)?; 85 | 86 | Ok(Self { 87 | id, 88 | track_alias, 89 | track_namespace, 90 | track_name, 91 | subscriber_priority, 92 | group_order, 93 | filter_type, 94 | start, 95 | end, 96 | params, 97 | }) 98 | } 99 | } 100 | 101 | impl Encode for SubscribeUpdate { 102 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 103 | self.id.encode(w)?; 104 | self.track_alias.encode(w)?; 105 | self.track_namespace.encode(w)?; 106 | self.track_name.encode(w)?; 107 | 108 | self.subscriber_priority.encode(w)?; 109 | self.group_order.encode(w)?; 110 | 111 | self.filter_type.encode(w)?; 112 | 113 | if self.filter_type == FilterType::AbsoluteStart 114 | || self.filter_type == FilterType::AbsoluteRange 115 | { 116 | if self.start.is_none() || self.end.is_none() { 117 | return Err(EncodeError::MissingField); 118 | } 119 | if let Some(start) = &self.start { 120 | start.encode(w)?; 121 | } 122 | if let Some(end) = &self.end { 123 | end.encode(w)?; 124 | } 125 | } 126 | 127 | self.params.encode(w)?; 128 | 129 | Ok(()) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /moq-transport/src/message/subscriber.rs: -------------------------------------------------------------------------------- 1 | use crate::message::{self, Message}; 2 | use std::fmt; 3 | 4 | macro_rules! subscriber_msgs { 5 | {$($name:ident,)*} => { 6 | #[derive(Clone)] 7 | pub enum Subscriber { 8 | $($name(message::$name)),* 9 | } 10 | 11 | $(impl From for Subscriber { 12 | fn from(msg: message::$name) -> Self { 13 | Subscriber::$name(msg) 14 | } 15 | })* 16 | 17 | impl From for Message { 18 | fn from(p: Subscriber) -> Self { 19 | match p { 20 | $(Subscriber::$name(m) => Message::$name(m),)* 21 | } 22 | } 23 | } 24 | 25 | impl TryFrom for Subscriber { 26 | type Error = Message; 27 | 28 | fn try_from(m: Message) -> Result { 29 | match m { 30 | $(Message::$name(m) => Ok(Subscriber::$name(m)),)* 31 | _ => Err(m), 32 | } 33 | } 34 | } 35 | 36 | impl fmt::Debug for Subscriber { 37 | // Delegate to the message formatter 38 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 39 | match self { 40 | $(Self::$name(ref m) => m.fmt(f),)* 41 | } 42 | } 43 | } 44 | } 45 | } 46 | 47 | subscriber_msgs! { 48 | AnnounceOk, 49 | AnnounceError, 50 | AnnounceCancel, 51 | Subscribe, 52 | Unsubscribe, 53 | SubscribeUpdate, 54 | TrackStatusRequest, 55 | SubscribeNamespace, 56 | SubscribeNamespaceOk, 57 | SubscribeNamespaceError, 58 | UnsubscribeNamespace, 59 | Fetch, 60 | FetchCancel, 61 | } 62 | -------------------------------------------------------------------------------- /moq-transport/src/message/track_status.rs: -------------------------------------------------------------------------------- 1 | use super::TrackStatusCode; 2 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Tuple}; 3 | 4 | #[derive(Clone, Debug)] 5 | pub struct TrackStatus { 6 | /// Track Namespace 7 | pub track_namespace: Tuple, 8 | /// Track Name 9 | pub track_name: String, 10 | /// Status Code 11 | pub status_code: TrackStatusCode, 12 | /// Last Group ID 13 | pub last_group_id: u64, 14 | /// Last Object ID 15 | pub last_object_id: u64, 16 | } 17 | 18 | impl Decode for TrackStatus { 19 | fn decode(r: &mut R) -> Result { 20 | Ok(Self { 21 | track_namespace: Tuple::decode(r)?, 22 | track_name: String::decode(r)?, 23 | status_code: TrackStatusCode::decode(r)?, 24 | last_group_id: u64::decode(r)?, 25 | last_object_id: u64::decode(r)?, 26 | }) 27 | } 28 | } 29 | 30 | impl Encode for TrackStatus { 31 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 32 | self.track_namespace.encode(w)?; 33 | self.track_name.encode(w)?; 34 | self.status_code.encode(w)?; 35 | self.last_group_id.encode(w)?; 36 | self.last_object_id.encode(w)?; 37 | Ok(()) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /moq-transport/src/message/track_status_request.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Tuple}; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct TrackStatusRequest { 5 | /// Track Namespace 6 | pub track_namespace: Tuple, 7 | /// Track Name 8 | pub track_name: String, 9 | } 10 | 11 | impl Decode for TrackStatusRequest { 12 | fn decode(r: &mut R) -> Result { 13 | let track_namespace = Tuple::decode(r)?; 14 | let track_name = String::decode(r)?; 15 | 16 | Ok(Self { 17 | track_namespace, 18 | track_name, 19 | }) 20 | } 21 | } 22 | 23 | impl Encode for TrackStatusRequest { 24 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 25 | self.track_namespace.encode(w)?; 26 | self.track_name.encode(w)?; 27 | 28 | Ok(()) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /moq-transport/src/message/unannounce.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Tuple}; 2 | 3 | /// Sent by the publisher to terminate an Announce. 4 | #[derive(Clone, Debug)] 5 | pub struct Unannounce { 6 | // Echo back the namespace that was reset 7 | pub namespace: Tuple, 8 | } 9 | 10 | impl Decode for Unannounce { 11 | fn decode(r: &mut R) -> Result { 12 | let namespace = Tuple::decode(r)?; 13 | 14 | Ok(Self { namespace }) 15 | } 16 | } 17 | 18 | impl Encode for Unannounce { 19 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 20 | self.namespace.encode(w)?; 21 | 22 | Ok(()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /moq-transport/src/message/unsubscribe.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | 3 | /// Sent by the subscriber to terminate a Subscribe. 4 | #[derive(Clone, Debug)] 5 | pub struct Unsubscribe { 6 | // The ID for this subscription. 7 | pub id: u64, 8 | } 9 | 10 | impl Decode for Unsubscribe { 11 | fn decode(r: &mut R) -> Result { 12 | let id = u64::decode(r)?; 13 | Ok(Self { id }) 14 | } 15 | } 16 | 17 | impl Encode for Unsubscribe { 18 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 19 | self.id.encode(w)?; 20 | Ok(()) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /moq-transport/src/message/unsubscribe_namespace.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Tuple}; 2 | 3 | /// Unsubscribe Namespace 4 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-06.html#name-unsubscribe_namespace 5 | #[derive(Clone, Debug)] 6 | pub struct UnsubscribeNamespace { 7 | // Echo back the namespace that was reset 8 | pub namespace_prefix: Tuple, 9 | } 10 | 11 | impl Decode for UnsubscribeNamespace { 12 | fn decode(r: &mut R) -> Result { 13 | let namespace_prefix = Tuple::decode(r)?; 14 | Ok(Self { namespace_prefix }) 15 | } 16 | } 17 | 18 | impl Encode for UnsubscribeNamespace { 19 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 20 | self.namespace_prefix.encode(w)?; 21 | Ok(()) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /moq-transport/src/serve/broadcast.rs: -------------------------------------------------------------------------------- 1 | //! A broadcast is a collection of tracks, split into two handles: [Writer] and [Reader]. 2 | //! 3 | //! The [Writer] can create tracks, either manually or on request. 4 | //! It receives all requests by a [Reader] for a tracks that don't exist. 5 | //! The simplest implementation is to close every unknown track with [ServeError::NotFound]. 6 | //! 7 | //! A [Reader] can request tracks by name. 8 | //! If the track already exists, it will be returned. 9 | //! If the track doesn't exist, it will be sent to [Unknown] to be handled. 10 | //! A [Reader] can be cloned to create multiple subscriptions. 11 | //! 12 | //! The broadcast is automatically closed with [ServeError::Done] when [Writer] is dropped, or all [Reader]s are dropped. 13 | use std::{ 14 | collections::{hash_map, HashMap}, 15 | ops::Deref, 16 | sync::Arc, 17 | }; 18 | 19 | use super::{ServeError, Track, TrackReader, TrackWriter}; 20 | use crate::util::State; 21 | 22 | /// Static information about a broadcast. 23 | #[derive(Debug)] 24 | pub struct Broadcast { 25 | pub namespace: String, 26 | } 27 | 28 | impl Broadcast { 29 | pub fn new(namespace: &str) -> Self { 30 | Self { 31 | namespace: namespace.to_owned(), 32 | } 33 | } 34 | 35 | pub fn produce(self) -> (BroadcastWriter, BroadcastReader) { 36 | let (send, recv) = State::init(); 37 | let info = Arc::new(self); 38 | 39 | let writer = BroadcastWriter::new(send, info.clone()); 40 | let reader = BroadcastReader::new(recv, info); 41 | 42 | (writer, reader) 43 | } 44 | } 45 | 46 | /// Dynamic information about the broadcast. 47 | struct BroadcastState { 48 | tracks: HashMap, 49 | closed: Result<(), ServeError>, 50 | } 51 | 52 | impl BroadcastState { 53 | pub fn get(&self, name: &str) -> Result, ServeError> { 54 | match self.tracks.get(name) { 55 | Some(track) => Ok(Some(track.clone())), 56 | // Return any error if we couldn't find a track. 57 | None => self.closed.clone().map(|_| None), 58 | } 59 | } 60 | 61 | pub fn insert(&mut self, track: TrackReader) -> Result<(), ServeError> { 62 | match self.tracks.entry(track.name.clone()) { 63 | hash_map::Entry::Occupied(_) => return Err(ServeError::Duplicate), 64 | hash_map::Entry::Vacant(v) => v.insert(track), 65 | }; 66 | 67 | Ok(()) 68 | } 69 | 70 | pub fn remove(&mut self, name: &str) -> Option { 71 | self.tracks.remove(name) 72 | } 73 | } 74 | 75 | impl Default for BroadcastState { 76 | fn default() -> Self { 77 | Self { 78 | tracks: HashMap::new(), 79 | closed: Ok(()), 80 | } 81 | } 82 | } 83 | 84 | /// Publish new tracks for a broadcast by name. 85 | pub struct BroadcastWriter { 86 | state: State, 87 | pub info: Arc, 88 | } 89 | 90 | impl BroadcastWriter { 91 | fn new(state: State, broadcast: Arc) -> Self { 92 | Self { state, info: broadcast } 93 | } 94 | 95 | /// Create a new track with the given name, inserting it into the broadcast. 96 | pub fn create_track(&mut self, track: &str) -> Result { 97 | let (writer, reader) = Track { 98 | namespace: self.namespace.clone(), 99 | name: track.to_owned(), 100 | } 101 | .produce(); 102 | 103 | self.state.lock_mut().ok_or(ServeError::Cancel)?.insert(reader)?; 104 | 105 | Ok(writer) 106 | } 107 | 108 | pub fn remove_track(&mut self, track: &str) -> Option { 109 | self.state.lock_mut()?.remove(track) 110 | } 111 | 112 | /// Close the broadcast with an error. 113 | pub fn close(self, err: ServeError) -> Result<(), ServeError> { 114 | let state = self.state.lock(); 115 | state.closed.clone()?; 116 | 117 | if let Some(mut state) = state.into_mut() { 118 | state.closed = Err(err); 119 | } 120 | 121 | Ok(()) 122 | } 123 | } 124 | 125 | impl Deref for BroadcastWriter { 126 | type Target = Broadcast; 127 | 128 | fn deref(&self) -> &Self::Target { 129 | &self.info 130 | } 131 | } 132 | 133 | /// Subscribe to a broadcast by requesting tracks. 134 | /// 135 | /// This can be cloned to create handles. 136 | #[derive(Clone)] 137 | pub struct BroadcastReader { 138 | state: State, 139 | pub info: Arc, 140 | } 141 | 142 | impl BroadcastReader { 143 | fn new(state: State, broadcast: Arc) -> Self { 144 | Self { state, info: broadcast } 145 | } 146 | 147 | /// Get a track from the broadcast by name. 148 | pub fn get_track(&self, name: &str) -> Result, ServeError> { 149 | self.state.lock().get(name) 150 | } 151 | } 152 | 153 | impl Deref for BroadcastReader { 154 | type Target = Broadcast; 155 | 156 | fn deref(&self) -> &Self::Target { 157 | &self.info 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /moq-transport/src/serve/datagram.rs: -------------------------------------------------------------------------------- 1 | use std::{fmt, sync::Arc}; 2 | 3 | use crate::data::ObjectStatus; 4 | use crate::watch::State; 5 | 6 | use super::{ServeError, Track}; 7 | 8 | pub struct Datagrams { 9 | pub track: Arc, 10 | } 11 | 12 | impl Datagrams { 13 | pub fn produce(self) -> (DatagramsWriter, DatagramsReader) { 14 | let (writer, reader) = State::default().split(); 15 | 16 | let writer = DatagramsWriter::new(writer, self.track.clone()); 17 | let reader = DatagramsReader::new(reader, self.track); 18 | 19 | (writer, reader) 20 | } 21 | } 22 | 23 | struct DatagramsState { 24 | // The latest datagram 25 | latest: Option, 26 | 27 | // Increased each time datagram changes. 28 | epoch: u64, 29 | 30 | // Set when the writer or all readers are dropped. 31 | closed: Result<(), ServeError>, 32 | } 33 | 34 | impl Default for DatagramsState { 35 | fn default() -> Self { 36 | Self { 37 | latest: None, 38 | epoch: 0, 39 | closed: Ok(()), 40 | } 41 | } 42 | } 43 | 44 | pub struct DatagramsWriter { 45 | state: State, 46 | pub track: Arc, 47 | } 48 | 49 | impl DatagramsWriter { 50 | fn new(state: State, track: Arc) -> Self { 51 | Self { state, track } 52 | } 53 | 54 | pub fn write(&mut self, datagram: Datagram) -> Result<(), ServeError> { 55 | let mut state = self.state.lock_mut().ok_or(ServeError::Cancel)?; 56 | 57 | state.latest = Some(datagram); 58 | state.epoch += 1; 59 | 60 | Ok(()) 61 | } 62 | 63 | pub fn close(self, err: ServeError) -> Result<(), ServeError> { 64 | let state = self.state.lock(); 65 | state.closed.clone()?; 66 | 67 | let mut state = state.into_mut().ok_or(ServeError::Cancel)?; 68 | state.closed = Err(err); 69 | 70 | Ok(()) 71 | } 72 | } 73 | 74 | #[derive(Clone)] 75 | pub struct DatagramsReader { 76 | state: State, 77 | pub track: Arc, 78 | 79 | epoch: u64, 80 | } 81 | 82 | impl DatagramsReader { 83 | fn new(state: State, track: Arc) -> Self { 84 | Self { 85 | state, 86 | track, 87 | epoch: 0, 88 | } 89 | } 90 | 91 | pub async fn read(&mut self) -> Result, ServeError> { 92 | loop { 93 | { 94 | let state = self.state.lock(); 95 | if self.epoch < state.epoch { 96 | self.epoch = state.epoch; 97 | return Ok(state.latest.clone()); 98 | } 99 | 100 | state.closed.clone()?; 101 | match state.modified() { 102 | Some(notify) => notify, 103 | None => return Ok(None), // No more updates will come 104 | } 105 | } 106 | .await; 107 | } 108 | } 109 | 110 | // Returns the largest group/sequence 111 | pub fn latest(&self) -> Option<(u64, u64)> { 112 | let state = self.state.lock(); 113 | state 114 | .latest 115 | .as_ref() 116 | .map(|datagram| (datagram.group_id, datagram.object_id)) 117 | } 118 | } 119 | 120 | /// Static information about the datagram. 121 | #[derive(Clone)] 122 | pub struct Datagram { 123 | pub group_id: u64, 124 | pub object_id: u64, 125 | pub priority: u8, 126 | pub status: ObjectStatus, 127 | pub payload: bytes::Bytes, 128 | } 129 | 130 | impl fmt::Debug for Datagram { 131 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 132 | f.debug_struct("Datagram") 133 | .field("object_id", &self.object_id) 134 | .field("group_id", &self.group_id) 135 | .field("priority", &self.priority) 136 | .field("status", &self.status) 137 | .field("payload", &self.payload.len()) 138 | .finish() 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /moq-transport/src/serve/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(thiserror::Error, Debug, Clone, PartialEq)] 2 | pub enum ServeError { 3 | // TODO stop using? 4 | #[error("done")] 5 | Done, 6 | 7 | #[error("cancelled")] 8 | Cancel, 9 | 10 | #[error("closed, code={0}")] 11 | Closed(u64), 12 | 13 | #[error("not found")] 14 | NotFound, 15 | 16 | #[error("duplicate")] 17 | Duplicate, 18 | 19 | #[error("multiple stream modes")] 20 | Mode, 21 | 22 | #[error("wrong size")] 23 | Size, 24 | 25 | #[error("internal error: {0}")] 26 | Internal(String), 27 | } 28 | 29 | impl ServeError { 30 | pub fn code(&self) -> u64 { 31 | match self { 32 | Self::Done => 0, 33 | Self::Cancel => 1, 34 | Self::Closed(code) => *code, 35 | Self::NotFound => 404, 36 | Self::Duplicate => 409, 37 | Self::Mode => 400, 38 | Self::Size => 413, 39 | Self::Internal(_) => 500, 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /moq-transport/src/serve/mod.rs: -------------------------------------------------------------------------------- 1 | mod datagram; 2 | mod error; 3 | mod object; 4 | mod stream; 5 | mod subgroup; 6 | mod track; 7 | mod tracks; 8 | 9 | pub use datagram::*; 10 | pub use error::*; 11 | pub use object::*; 12 | pub use stream::*; 13 | pub use subgroup::*; 14 | pub use track::*; 15 | pub use tracks::*; 16 | -------------------------------------------------------------------------------- /moq-transport/src/session/announced.rs: -------------------------------------------------------------------------------- 1 | use std::ops; 2 | 3 | use crate::coding::Tuple; 4 | use crate::watch::State; 5 | use crate::{message, serve::ServeError}; 6 | 7 | use super::{AnnounceInfo, Subscriber}; 8 | 9 | // There's currently no feedback from the peer, so the shared state is empty. 10 | // If Unannounce contained an error code then we'd be talking. 11 | #[derive(Default)] 12 | struct AnnouncedState {} 13 | 14 | pub struct Announced { 15 | session: Subscriber, 16 | state: State, 17 | 18 | pub info: AnnounceInfo, 19 | 20 | ok: bool, 21 | error: Option, 22 | } 23 | 24 | impl Announced { 25 | pub(super) fn new(session: Subscriber, namespace: Tuple) -> (Announced, AnnouncedRecv) { 26 | let info = AnnounceInfo { namespace }; 27 | 28 | let (send, recv) = State::default().split(); 29 | let send = Self { 30 | session, 31 | info, 32 | ok: false, 33 | error: None, 34 | state: send, 35 | }; 36 | let recv = AnnouncedRecv { _state: recv }; 37 | 38 | (send, recv) 39 | } 40 | 41 | // Send an ANNOUNCE_OK 42 | pub fn ok(&mut self) -> Result<(), ServeError> { 43 | if self.ok { 44 | return Err(ServeError::Duplicate); 45 | } 46 | 47 | self.session.send_message(message::AnnounceOk { 48 | namespace: self.namespace.clone(), 49 | }); 50 | 51 | self.ok = true; 52 | 53 | Ok(()) 54 | } 55 | 56 | pub async fn closed(&self) -> Result<(), ServeError> { 57 | loop { 58 | // Wow this is dumb and yet pretty cool. 59 | // Basically loop until the state changes and exit when Recv is dropped. 60 | self.state 61 | .lock() 62 | .modified() 63 | .ok_or(ServeError::Cancel)? 64 | .await; 65 | } 66 | } 67 | 68 | pub fn close(mut self, err: ServeError) -> Result<(), ServeError> { 69 | self.error = Some(err); 70 | Ok(()) 71 | } 72 | } 73 | 74 | impl ops::Deref for Announced { 75 | type Target = AnnounceInfo; 76 | 77 | fn deref(&self) -> &AnnounceInfo { 78 | &self.info 79 | } 80 | } 81 | 82 | impl Drop for Announced { 83 | fn drop(&mut self) { 84 | let err = self.error.clone().unwrap_or(ServeError::Done); 85 | 86 | // TODO: Not sure if the error code is correct. 87 | if self.ok { 88 | self.session.send_message(message::AnnounceCancel { 89 | namespace: self.namespace.clone(), 90 | error_code: 0_u64, 91 | reason_phrase: "".into(), 92 | }); 93 | } else { 94 | self.session.send_message(message::AnnounceError { 95 | namespace: self.namespace.clone(), 96 | error_code: err.code(), 97 | reason_phrase: err.to_string(), 98 | }); 99 | } 100 | } 101 | } 102 | 103 | pub(super) struct AnnouncedRecv { 104 | _state: State, 105 | } 106 | 107 | impl AnnouncedRecv { 108 | pub fn recv_unannounce(self) -> Result<(), ServeError> { 109 | // Will cause the state to be dropped 110 | Ok(()) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /moq-transport/src/session/error.rs: -------------------------------------------------------------------------------- 1 | use crate::{coding, serve, setup}; 2 | 3 | #[derive(thiserror::Error, Debug, Clone)] 4 | pub enum SessionError { 5 | #[error("webtransport session: {0}")] 6 | Session(#[from] web_transport::SessionError), 7 | 8 | #[error("webtransport write: {0}")] 9 | Write(#[from] web_transport::WriteError), 10 | 11 | #[error("webtransport read: {0}")] 12 | Read(#[from] web_transport::ReadError), 13 | 14 | #[error("encode error: {0}")] 15 | Encode(#[from] coding::EncodeError), 16 | 17 | #[error("decode error: {0}")] 18 | Decode(#[from] coding::DecodeError), 19 | 20 | // TODO move to a ConnectError 21 | #[error("unsupported versions: client={0:?} server={1:?}")] 22 | Version(setup::Versions, setup::Versions), 23 | 24 | // TODO move to a ConnectError 25 | #[error("incompatible roles: client={0:?} server={1:?}")] 26 | RoleIncompatible(setup::Role, setup::Role), 27 | 28 | /// The role negiotiated in the handshake was violated. For example, a publisher sent a SUBSCRIBE, or a subscriber sent an OBJECT. 29 | #[error("role violation")] 30 | RoleViolation, 31 | 32 | /// Some VarInt was too large and we were too lazy to handle it 33 | #[error("varint bounds exceeded")] 34 | BoundsExceeded(#[from] coding::BoundsExceeded), 35 | 36 | /// A duplicate ID was used 37 | #[error("duplicate")] 38 | Duplicate, 39 | 40 | #[error("internal error")] 41 | Internal, 42 | 43 | #[error("serve error: {0}")] 44 | Serve(#[from] serve::ServeError), 45 | 46 | #[error("wrong size")] 47 | WrongSize, 48 | } 49 | 50 | impl SessionError { 51 | /// An integer code that is sent over the wire. 52 | pub fn code(&self) -> u64 { 53 | match self { 54 | Self::RoleIncompatible(..) => 406, 55 | Self::RoleViolation => 405, 56 | Self::Session(_) => 503, 57 | Self::Read(_) => 500, 58 | Self::Write(_) => 500, 59 | Self::Version(..) => 406, 60 | Self::Decode(_) => 400, 61 | Self::Encode(_) => 500, 62 | Self::BoundsExceeded(_) => 500, 63 | Self::Duplicate => 409, 64 | Self::Internal => 500, 65 | Self::WrongSize => 400, 66 | Self::Serve(err) => err.code(), 67 | } 68 | } 69 | } 70 | 71 | impl From for serve::ServeError { 72 | fn from(err: SessionError) -> Self { 73 | match err { 74 | SessionError::Serve(err) => err, 75 | _ => serve::ServeError::Internal(err.to_string()), 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /moq-transport/src/session/reader.rs: -------------------------------------------------------------------------------- 1 | use std::{cmp, io}; 2 | 3 | use bytes::{Buf, Bytes, BytesMut}; 4 | 5 | use crate::coding::{Decode, DecodeError}; 6 | 7 | use super::SessionError; 8 | 9 | pub struct Reader { 10 | stream: web_transport::RecvStream, 11 | buffer: BytesMut, 12 | } 13 | 14 | impl Reader { 15 | pub fn new(stream: web_transport::RecvStream) -> Self { 16 | Self { 17 | stream, 18 | buffer: Default::default(), 19 | } 20 | } 21 | 22 | pub async fn decode(&mut self) -> Result { 23 | loop { 24 | let mut cursor = io::Cursor::new(&self.buffer); 25 | 26 | // Try to decode with the current buffer. 27 | let required = match T::decode(&mut cursor) { 28 | Ok(msg) => { 29 | self.buffer.advance(cursor.position() as usize); 30 | return Ok(msg); 31 | } 32 | Err(DecodeError::More(required)) => self.buffer.len() + required, // Try again with more data 33 | Err(err) => return Err(err.into()), 34 | }; 35 | 36 | // Read in more data until we reach the requested amount. 37 | // We always read at least once to avoid an infinite loop if some dingus puts remain=0 38 | loop { 39 | if !self.stream.read_buf(&mut self.buffer).await? { 40 | return Err(DecodeError::More(required - self.buffer.len()).into()); 41 | }; 42 | 43 | if self.buffer.len() >= required { 44 | break; 45 | } 46 | } 47 | } 48 | } 49 | 50 | pub async fn read_chunk(&mut self, max: usize) -> Result, SessionError> { 51 | if !self.buffer.is_empty() { 52 | let size = cmp::min(max, self.buffer.len()); 53 | let data = self.buffer.split_to(size).freeze(); 54 | return Ok(Some(data)); 55 | } 56 | 57 | Ok(self.stream.read_chunk(max).await?) 58 | } 59 | 60 | pub async fn done(&mut self) -> Result { 61 | if !self.buffer.is_empty() { 62 | return Ok(false); 63 | } 64 | 65 | Ok(!self.stream.read_buf(&mut self.buffer).await?) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /moq-transport/src/session/track_status_requested.rs: -------------------------------------------------------------------------------- 1 | use super::{Publisher, SessionError}; 2 | use crate::coding::Tuple; 3 | use crate::message; 4 | 5 | #[derive(Debug, Clone)] 6 | pub struct TrackStatusRequestedInfo { 7 | pub namespace: Tuple, 8 | pub track: String, 9 | } 10 | 11 | pub struct TrackStatusRequested { 12 | publisher: Publisher, 13 | // msg: message::TrackStatusRequest, // TODO: See if we actually need this 14 | pub info: TrackStatusRequestedInfo, 15 | } 16 | 17 | impl TrackStatusRequested { 18 | pub fn new(publisher: Publisher, msg: message::TrackStatusRequest) -> Self { 19 | let namespace = msg.track_namespace.clone(); 20 | let track = msg.track_name.clone(); 21 | Self { 22 | publisher, 23 | info: TrackStatusRequestedInfo { namespace, track }, 24 | } 25 | } 26 | 27 | pub async fn respond(&mut self, status: message::TrackStatus) -> Result<(), SessionError> { 28 | self.publisher.send_message(status); 29 | Ok(()) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /moq-transport/src/session/writer.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | 3 | use crate::coding::{Encode, EncodeError}; 4 | 5 | use super::SessionError; 6 | use bytes::Buf; 7 | 8 | pub struct Writer { 9 | stream: web_transport::SendStream, 10 | buffer: bytes::BytesMut, 11 | } 12 | 13 | impl Writer { 14 | pub fn new(stream: web_transport::SendStream) -> Self { 15 | Self { 16 | stream, 17 | buffer: Default::default(), 18 | } 19 | } 20 | 21 | pub async fn encode(&mut self, msg: &T) -> Result<(), SessionError> { 22 | self.buffer.clear(); 23 | msg.encode(&mut self.buffer)?; 24 | 25 | while !self.buffer.is_empty() { 26 | self.stream.write_buf(&mut self.buffer).await?; 27 | } 28 | 29 | Ok(()) 30 | } 31 | 32 | pub async fn write(&mut self, buf: &[u8]) -> Result<(), SessionError> { 33 | let mut cursor = io::Cursor::new(buf); 34 | 35 | while cursor.has_remaining() { 36 | let size = self.stream.write_buf(&mut cursor).await?; 37 | if size == 0 { 38 | return Err(EncodeError::More(cursor.remaining()).into()); 39 | } 40 | } 41 | 42 | Ok(()) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /moq-transport/src/setup/client.rs: -------------------------------------------------------------------------------- 1 | use super::{Role, Versions}; 2 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Params}; 3 | 4 | /// Sent by the client to setup the session. 5 | // NOTE: This is not a message type, but rather the control stream header. 6 | // Proposal: https://github.com/moq-wg/moq-transport/issues/138 7 | #[derive(Debug)] 8 | pub struct Client { 9 | /// The list of supported versions in preferred order. 10 | pub versions: Versions, 11 | 12 | /// Indicate if the client is a publisher, a subscriber, or both. 13 | pub role: Role, 14 | 15 | /// Unknown parameters. 16 | pub params: Params, 17 | } 18 | 19 | impl Decode for Client { 20 | /// Decode a client setup message. 21 | fn decode(r: &mut R) -> Result { 22 | let typ = u64::decode(r)?; 23 | if typ != 0x40 { 24 | return Err(DecodeError::InvalidMessage(typ)); 25 | } 26 | 27 | let _len = u64::decode(r)?; 28 | 29 | // TODO: Check the length of the message. 30 | 31 | let versions = Versions::decode(r)?; 32 | let mut params = Params::decode(r)?; 33 | 34 | let role = params 35 | .get::(0)? 36 | .ok_or(DecodeError::MissingParameter)?; 37 | 38 | // Make sure the PATH parameter isn't used 39 | // TODO: This assumes WebTransport support only 40 | if params.has(1) { 41 | return Err(DecodeError::InvalidParameter); 42 | } 43 | 44 | Ok(Self { 45 | versions, 46 | role, 47 | params, 48 | }) 49 | } 50 | } 51 | 52 | impl Encode for Client { 53 | /// Encode a server setup message. 54 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 55 | 0x40_u64.encode(w)?; 56 | 57 | // Find out the length of the message 58 | // by encoding it into a buffer and then encoding the length. 59 | // This is a bit wasteful, but it's the only way to know the length. 60 | let mut buf = Vec::new(); 61 | 62 | self.versions.encode(&mut buf).unwrap(); 63 | 64 | let mut params = self.params.clone(); 65 | params.set(0, self.role)?; 66 | params.encode(&mut buf).unwrap(); 67 | 68 | (buf.len() as u64).encode(w)?; 69 | 70 | // At least don't encode the message twice. 71 | // Instead, write the buffer directly to the writer. 72 | w.put_slice(&buf); 73 | 74 | Ok(()) 75 | } 76 | } 77 | 78 | #[cfg(test)] 79 | mod tests { 80 | use super::*; 81 | use crate::setup::Version; 82 | use bytes::BytesMut; 83 | 84 | #[test] 85 | fn encode_decode() { 86 | let mut buf = BytesMut::new(); 87 | let client = Client { 88 | versions: [Version::DRAFT_07].into(), 89 | role: Role::Both, 90 | params: Params::default(), 91 | }; 92 | 93 | client.encode(&mut buf).unwrap(); 94 | assert_eq!( 95 | buf.to_vec(), 96 | vec![ 97 | 0x40, 0x40, 0x0D, 0x01, 0xC0, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x07, 0x01, 0x00, 98 | 0x01, 0x03 99 | ] 100 | ); 101 | 102 | let decoded = Client::decode(&mut buf).unwrap(); 103 | assert_eq!(decoded.versions, client.versions); 104 | assert_eq!(decoded.role, client.role); 105 | //assert_eq!(decoded.params, client.params); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /moq-transport/src/setup/mod.rs: -------------------------------------------------------------------------------- 1 | //! Messages used for the MoQ Transport handshake. 2 | //! 3 | //! After establishing the WebTransport session, the client creates a bidirectional QUIC stream. 4 | //! The client sends the [Client] message and the server responds with the [Server] message. 5 | //! Both sides negotate the [Version] and [Role]. 6 | 7 | mod client; 8 | mod role; 9 | mod server; 10 | mod version; 11 | 12 | pub use client::*; 13 | pub use role::*; 14 | pub use server::*; 15 | pub use version::*; 16 | 17 | pub const ALPN: &[u8] = b"moq-00"; 18 | -------------------------------------------------------------------------------- /moq-transport/src/setup/role.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | 3 | /// Indicates the endpoint is a publisher, subscriber, or both. 4 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 5 | pub enum Role { 6 | Publisher, 7 | Subscriber, 8 | Both, 9 | } 10 | 11 | impl Role { 12 | /// Returns true if the role is publisher. 13 | pub fn is_publisher(&self) -> bool { 14 | match self { 15 | Self::Publisher | Self::Both => true, 16 | Self::Subscriber => false, 17 | } 18 | } 19 | 20 | /// Returns true if the role is a subscriber. 21 | pub fn is_subscriber(&self) -> bool { 22 | match self { 23 | Self::Subscriber | Self::Both => true, 24 | Self::Publisher => false, 25 | } 26 | } 27 | 28 | /// Returns true if two endpoints are compatible. 29 | pub fn is_compatible(&self, other: Role) -> bool { 30 | self.is_publisher() == other.is_subscriber() && self.is_subscriber() == other.is_publisher() 31 | } 32 | } 33 | 34 | impl From for u64 { 35 | fn from(r: Role) -> Self { 36 | match r { 37 | Role::Publisher => 0x1, 38 | Role::Subscriber => 0x2, 39 | Role::Both => 0x3, 40 | } 41 | } 42 | } 43 | 44 | impl TryFrom for Role { 45 | type Error = DecodeError; 46 | 47 | fn try_from(v: u64) -> Result { 48 | match v { 49 | 0x1 => Ok(Self::Publisher), 50 | 0x2 => Ok(Self::Subscriber), 51 | 0x3 => Ok(Self::Both), 52 | _ => Err(DecodeError::InvalidRole(v)), 53 | } 54 | } 55 | } 56 | 57 | impl Decode for Role { 58 | /// Decode the role. 59 | fn decode(r: &mut R) -> Result { 60 | let v = u64::decode(r)?; 61 | v.try_into() 62 | } 63 | } 64 | 65 | impl Encode for Role { 66 | /// Encode the role. 67 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 68 | u64::from(*self).encode(w) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /moq-transport/src/setup/server.rs: -------------------------------------------------------------------------------- 1 | use super::{Role, Version}; 2 | use crate::coding::{Decode, DecodeError, Encode, EncodeError, Params}; 3 | 4 | /// Sent by the server in response to a client setup. 5 | // NOTE: This is not a message type, but rather the control stream header. 6 | // Proposal: https://github.com/moq-wg/moq-transport/issues/138 7 | #[derive(Debug)] 8 | pub struct Server { 9 | /// The list of supported versions in preferred order. 10 | pub version: Version, 11 | 12 | /// Indicate if the server is a publisher, a subscriber, or both. 13 | // Proposal: moq-wg/moq-transport#151 14 | pub role: Role, 15 | 16 | /// Unknown parameters. 17 | pub params: Params, 18 | } 19 | 20 | impl Decode for Server { 21 | /// Decode the server setup. 22 | fn decode(r: &mut R) -> Result { 23 | let typ = u64::decode(r)?; 24 | if typ != 0x41 { 25 | return Err(DecodeError::InvalidMessage(typ)); 26 | } 27 | 28 | let _len = u64::decode(r)?; 29 | 30 | // TODO: Check the length of the message. 31 | 32 | let version = Version::decode(r)?; 33 | let mut params = Params::decode(r)?; 34 | 35 | let role = params 36 | .get::(0)? 37 | .ok_or(DecodeError::MissingParameter)?; 38 | 39 | // Make sure the PATH parameter isn't used 40 | if params.has(1) { 41 | return Err(DecodeError::InvalidParameter); 42 | } 43 | 44 | Ok(Self { 45 | version, 46 | role, 47 | params, 48 | }) 49 | } 50 | } 51 | 52 | impl Encode for Server { 53 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 54 | 0x41_u64.encode(w)?; 55 | 56 | // Find out the length of the message 57 | // by encoding it into a buffer and then encoding the length. 58 | // This is a bit wasteful, but it's the only way to know the length. 59 | let mut buf = Vec::new(); 60 | 61 | self.version.encode(&mut buf).unwrap(); 62 | 63 | let mut params = self.params.clone(); 64 | params.set(0, self.role)?; 65 | params.encode(&mut buf).unwrap(); 66 | 67 | (buf.len() as u64).encode(w)?; 68 | 69 | // At least don't encode the message twice. 70 | // Instead, write the buffer directly to the writer. 71 | w.put_slice(&buf); 72 | 73 | Ok(()) 74 | } 75 | } 76 | 77 | #[cfg(test)] 78 | mod tests { 79 | use super::*; 80 | use crate::setup::Role; 81 | use bytes::BytesMut; 82 | 83 | #[test] 84 | fn encode_decode() { 85 | let mut buf = BytesMut::new(); 86 | let client = Server { 87 | version: Version::DRAFT_07, 88 | role: Role::Both, 89 | params: Params::default(), 90 | }; 91 | 92 | client.encode(&mut buf).unwrap(); 93 | assert_eq!( 94 | buf.to_vec(), 95 | vec![ 96 | 0x40, 0x41, 0x0C, 0xC0, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x07, 0x01, 0x00, 0x01, 97 | 0x03 98 | ] 99 | ); 100 | 101 | let decoded = Server::decode(&mut buf).unwrap(); 102 | assert_eq!(decoded.version, client.version); 103 | assert_eq!(decoded.role, client.role); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /moq-transport/src/setup/version.rs: -------------------------------------------------------------------------------- 1 | use crate::coding::{Decode, DecodeError, Encode, EncodeError}; 2 | 3 | use std::ops::Deref; 4 | 5 | /// A version number negotiated during the setup. 6 | #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] 7 | pub struct Version(pub u64); 8 | 9 | impl Version { 10 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-00.html 11 | pub const DRAFT_00: Version = Version(0xff000000); 12 | 13 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-01.html 14 | pub const DRAFT_01: Version = Version(0xff000001); 15 | 16 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-02.html 17 | pub const DRAFT_02: Version = Version(0xff000002); 18 | 19 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-03.html 20 | pub const DRAFT_03: Version = Version(0xff000003); 21 | 22 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-04.html 23 | pub const DRAFT_04: Version = Version(0xff000004); 24 | 25 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-05.html 26 | pub const DRAFT_05: Version = Version(0xff000005); 27 | 28 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-06.html 29 | pub const DRAFT_06: Version = Version(0xff000006); 30 | 31 | /// https://www.ietf.org/archive/id/draft-ietf-moq-transport-07.html 32 | pub const DRAFT_07: Version = Version(0xff000007); 33 | } 34 | 35 | impl From for Version { 36 | fn from(v: u64) -> Self { 37 | Self(v) 38 | } 39 | } 40 | 41 | impl From for u64 { 42 | fn from(v: Version) -> Self { 43 | v.0 44 | } 45 | } 46 | 47 | impl Decode for Version { 48 | /// Decode the version number. 49 | fn decode(r: &mut R) -> Result { 50 | let v = u64::decode(r)?; 51 | Ok(Self(v)) 52 | } 53 | } 54 | 55 | impl Encode for Version { 56 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 57 | self.0.encode(w)?; 58 | Ok(()) 59 | } 60 | } 61 | 62 | /// A list of versions in arbitrary order. 63 | #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] 64 | pub struct Versions(Vec); 65 | 66 | impl Decode for Versions { 67 | /// Decode the version list. 68 | fn decode(r: &mut R) -> Result { 69 | let count = u64::decode(r)?; 70 | let mut vs = Vec::new(); 71 | 72 | for _ in 0..count { 73 | let v = Version::decode(r)?; 74 | vs.push(v); 75 | } 76 | 77 | Ok(Self(vs)) 78 | } 79 | } 80 | 81 | impl Encode for Versions { 82 | /// Encode the version list. 83 | fn encode(&self, w: &mut W) -> Result<(), EncodeError> { 84 | self.0.len().encode(w)?; 85 | 86 | for v in &self.0 { 87 | v.encode(w)?; 88 | } 89 | 90 | Ok(()) 91 | } 92 | } 93 | 94 | impl Deref for Versions { 95 | type Target = Vec; 96 | 97 | fn deref(&self) -> &Self::Target { 98 | &self.0 99 | } 100 | } 101 | 102 | impl From> for Versions { 103 | fn from(vs: Vec) -> Self { 104 | Self(vs) 105 | } 106 | } 107 | 108 | impl From<[Version; N]> for Versions { 109 | fn from(vs: [Version; N]) -> Self { 110 | Self(vs.to_vec()) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /moq-transport/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | mod queue; 2 | mod state; 3 | mod watch; 4 | 5 | pub use queue::*; 6 | pub use state::*; 7 | pub use watch::*; 8 | -------------------------------------------------------------------------------- /moq-transport/src/util/queue.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use super::Watch; 4 | 5 | // TODO replace with mpsc or similar 6 | pub struct Queue { 7 | state: Watch>, 8 | } 9 | 10 | impl Clone for Queue { 11 | fn clone(&self) -> Self { 12 | Self { 13 | state: self.state.clone(), 14 | } 15 | } 16 | } 17 | 18 | impl Default for Queue { 19 | fn default() -> Self { 20 | Self { 21 | state: Default::default(), 22 | } 23 | } 24 | } 25 | 26 | impl Queue { 27 | pub fn push(&self, item: T) { 28 | self.state.lock_mut().push_back(item); 29 | } 30 | 31 | pub async fn pop(&self) -> T { 32 | loop { 33 | let notify = { 34 | let queue = self.state.lock(); 35 | if !queue.is_empty() { 36 | return queue.into_mut().pop_front().unwrap(); 37 | } 38 | queue.changed() 39 | }; 40 | 41 | notify.await 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /moq-transport/src/util/watch.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt, 3 | future::Future, 4 | ops::{Deref, DerefMut}, 5 | pin::Pin, 6 | sync::{Arc, Mutex, MutexGuard, Weak}, 7 | task, 8 | }; 9 | 10 | struct State { 11 | value: T, 12 | wakers: Vec, 13 | epoch: usize, 14 | } 15 | 16 | impl State { 17 | pub fn new(value: T) -> Self { 18 | Self { 19 | value, 20 | wakers: Vec::new(), 21 | epoch: 0, 22 | } 23 | } 24 | 25 | pub fn register(&mut self, waker: &task::Waker) { 26 | self.wakers.retain(|existing| !existing.will_wake(waker)); 27 | self.wakers.push(waker.clone()); 28 | } 29 | 30 | pub fn notify(&mut self) { 31 | self.epoch += 1; 32 | for waker in self.wakers.drain(..) { 33 | waker.wake(); 34 | } 35 | } 36 | } 37 | 38 | impl Default for State { 39 | fn default() -> Self { 40 | Self::new(T::default()) 41 | } 42 | } 43 | 44 | impl fmt::Debug for State { 45 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 46 | self.value.fmt(f) 47 | } 48 | } 49 | 50 | pub struct Watch { 51 | state: Arc>>, 52 | } 53 | 54 | impl Watch { 55 | pub fn new(initial: T) -> Self { 56 | let state = Arc::new(Mutex::new(State::new(initial))); 57 | Self { state } 58 | } 59 | 60 | pub fn lock(&self) -> WatchRef { 61 | WatchRef { 62 | state: self.state.clone(), 63 | lock: self.state.lock().unwrap(), 64 | } 65 | } 66 | 67 | pub fn lock_mut(&self) -> WatchMut { 68 | WatchMut { 69 | lock: self.state.lock().unwrap(), 70 | } 71 | } 72 | 73 | pub fn downgrade(&self) -> WatchWeak { 74 | WatchWeak { 75 | state: Arc::downgrade(&self.state), 76 | } 77 | } 78 | } 79 | 80 | impl Clone for Watch { 81 | fn clone(&self) -> Self { 82 | Self { 83 | state: self.state.clone(), 84 | } 85 | } 86 | } 87 | 88 | impl Default for Watch { 89 | fn default() -> Self { 90 | Self::new(T::default()) 91 | } 92 | } 93 | 94 | impl fmt::Debug for Watch { 95 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 96 | match self.state.try_lock() { 97 | Ok(lock) => lock.value.fmt(f), 98 | Err(_) => write!(f, ""), 99 | } 100 | } 101 | } 102 | 103 | pub struct WatchRef<'a, T> { 104 | state: Arc>>, 105 | lock: MutexGuard<'a, State>, 106 | } 107 | 108 | impl<'a, T> WatchRef<'a, T> { 109 | // Release the lock and wait for a notification when next updated. 110 | pub fn changed(self) -> WatchChanged { 111 | WatchChanged { 112 | state: self.state, 113 | epoch: self.lock.epoch, 114 | } 115 | } 116 | 117 | // Upgrade to a mutable references that automatically calls notify on drop. 118 | pub fn into_mut(self) -> WatchMut<'a, T> { 119 | WatchMut { lock: self.lock } 120 | } 121 | } 122 | 123 | impl<'a, T> Deref for WatchRef<'a, T> { 124 | type Target = T; 125 | 126 | fn deref(&self) -> &Self::Target { 127 | &self.lock.value 128 | } 129 | } 130 | 131 | impl<'a, T: fmt::Debug> fmt::Debug for WatchRef<'a, T> { 132 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 133 | self.lock.fmt(f) 134 | } 135 | } 136 | 137 | pub struct WatchMut<'a, T> { 138 | lock: MutexGuard<'a, State>, 139 | } 140 | 141 | impl<'a, T> Deref for WatchMut<'a, T> { 142 | type Target = T; 143 | 144 | fn deref(&self) -> &Self::Target { 145 | &self.lock.value 146 | } 147 | } 148 | 149 | impl<'a, T> DerefMut for WatchMut<'a, T> { 150 | fn deref_mut(&mut self) -> &mut Self::Target { 151 | &mut self.lock.value 152 | } 153 | } 154 | 155 | impl<'a, T> Drop for WatchMut<'a, T> { 156 | fn drop(&mut self) { 157 | self.lock.notify(); 158 | } 159 | } 160 | 161 | impl<'a, T: fmt::Debug> fmt::Debug for WatchMut<'a, T> { 162 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 163 | self.lock.fmt(f) 164 | } 165 | } 166 | 167 | pub struct WatchChanged { 168 | state: Arc>>, 169 | epoch: usize, 170 | } 171 | 172 | impl Future for WatchChanged { 173 | type Output = (); 174 | 175 | fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { 176 | // TODO is there an API we can make that doesn't drop this lock? 177 | let mut state = self.state.lock().unwrap(); 178 | 179 | if state.epoch > self.epoch { 180 | task::Poll::Ready(()) 181 | } else { 182 | state.register(cx.waker()); 183 | task::Poll::Pending 184 | } 185 | } 186 | } 187 | 188 | pub struct WatchWeak { 189 | state: Weak>>, 190 | } 191 | 192 | impl WatchWeak { 193 | pub fn upgrade(&self) -> Option> { 194 | self.state.upgrade().map(|state| Watch { state }) 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /moq-transport/src/watch/mod.rs: -------------------------------------------------------------------------------- 1 | mod queue; 2 | mod state; 3 | 4 | pub use queue::*; 5 | pub use state::*; 6 | -------------------------------------------------------------------------------- /moq-transport/src/watch/queue.rs: -------------------------------------------------------------------------------- 1 | use std::collections::VecDeque; 2 | 3 | use super::State; 4 | 5 | pub struct Queue { 6 | state: State>, 7 | } 8 | 9 | impl Queue { 10 | pub fn push(&mut self, item: T) -> Result<(), T> { 11 | match self.state.lock_mut() { 12 | Some(mut state) => state.push_back(item), 13 | None => return Err(item), 14 | }; 15 | 16 | Ok(()) 17 | } 18 | 19 | pub async fn pop(&mut self) -> Option { 20 | loop { 21 | { 22 | let queue = self.state.lock(); 23 | if !queue.is_empty() { 24 | return queue.into_mut()?.pop_front(); 25 | } 26 | queue.modified()? 27 | } 28 | .await; 29 | } 30 | } 31 | 32 | // Drop the state 33 | pub fn close(self) -> Vec { 34 | // Drain the queue of any remaining entries 35 | let res = match self.state.lock_mut() { 36 | Some(mut queue) => queue.drain(..).collect(), 37 | _ => Vec::new(), 38 | }; 39 | 40 | // Prevent any new entries from being added 41 | drop(self.state); 42 | 43 | res 44 | } 45 | 46 | pub fn split(self) -> (Self, Self) { 47 | let state = self.state.split(); 48 | (Self { state: state.0 }, Self { state: state.1 }) 49 | } 50 | } 51 | 52 | impl Clone for Queue { 53 | fn clone(&self) -> Self { 54 | Self { 55 | state: self.state.clone(), 56 | } 57 | } 58 | } 59 | 60 | impl Default for Queue { 61 | fn default() -> Self { 62 | Self { 63 | state: State::new(Default::default()), 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /package.nix: -------------------------------------------------------------------------------- 1 | { 2 | lib, 3 | rustPlatform, 4 | pkg-config, 5 | openssl, 6 | stdenv, 7 | darwin, 8 | }: 9 | 10 | rustPlatform.buildRustPackage { 11 | name = "moq-rs"; 12 | 13 | src = 14 | let 15 | ignoredPaths = [ 16 | "default.nix" 17 | "flake.nix" 18 | "flake.lock" 19 | "package.nix" 20 | ]; 21 | in 22 | lib.cleanSourceWith { 23 | filter = name: type: !(builtins.elem (baseNameOf name) ignoredPaths); 24 | src = lib.cleanSource ./.; 25 | }; 26 | 27 | cargoLock = { 28 | lockFile = ./Cargo.lock; 29 | allowBuiltinFetchGit = true; 30 | }; 31 | 32 | nativeBuildInputs = [ 33 | pkg-config 34 | rustPlatform.bindgenHook 35 | ]; 36 | 37 | buildInputs = 38 | [ 39 | openssl 40 | ] 41 | ++ lib.optionals stdenv.isDarwin [ 42 | darwin.apple_sdk.frameworks.Security 43 | darwin.apple_sdk.frameworks.SystemConfiguration 44 | ]; 45 | 46 | meta = { 47 | description = "Fork of kixelated/moq-rs to continue tracking the IETF MoQ Working Group drafts"; 48 | homepage = "https://github.com/englishm/moq-rs"; 49 | license = with lib.licenses; [ 50 | asl20 51 | mit 52 | ]; 53 | maintainers = with lib.maintainers; [ niklaskorz ]; 54 | mainProgram = "moq-pub"; 55 | }; 56 | } 57 | --------------------------------------------------------------------------------