From 48a791470cf5508223b8eddd3f9941aeee05c0f4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 16:15:59 -0700 Subject: [PATCH 01/40] quest(m1): claim migrate Co-Authored-By: Claude Opus 5.5 From 50fa718b57e5071cbd8533a2a2b37706e4b4ea7a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 16:18:49 -0700 Subject: [PATCH 02/40] docs: add a MIGRATE.md skeleton to every published package Co-Authored-By: Claude Opus 5.5 --- cpp/moq/MIGRATE.md | 5 +++++ cpp/obs/MIGRATE.md | 5 +++++ dart/moq/MIGRATE.md | 5 +++++ go/wrapper/MIGRATE.md | 5 +++++ js/auth/MIGRATE.md | 5 +++++ js/clock/MIGRATE.md | 5 +++++ js/flate/MIGRATE.md | 5 +++++ js/hang/MIGRATE.md | 5 +++++ js/json/MIGRATE.md | 5 +++++ js/loc/MIGRATE.md | 5 +++++ js/moq-boy/MIGRATE.md | 5 +++++ js/msf/MIGRATE.md | 5 +++++ js/net/MIGRATE.md | 5 +++++ js/p2p/MIGRATE.md | 5 +++++ js/pattern/MIGRATE.md | 5 +++++ js/publish/MIGRATE.md | 5 +++++ js/room/MIGRATE.md | 5 +++++ js/signals/MIGRATE.md | 5 +++++ js/video/MIGRATE.md | 5 +++++ js/watch/MIGRATE.md | 5 +++++ kt/moq/MIGRATE.md | 5 +++++ py/moq-rs/MIGRATE.md | 5 +++++ rs/hang/MIGRATE.md | 5 +++++ rs/kio/MIGRATE.md | 5 +++++ rs/moq-archive/MIGRATE.md | 5 +++++ rs/moq-audio/MIGRATE.md | 5 +++++ rs/moq-auth/MIGRATE.md | 5 +++++ rs/moq-boy/MIGRATE.md | 5 +++++ rs/moq-c/MIGRATE.md | 5 +++++ rs/moq-cli/MIGRATE.md | 5 +++++ rs/moq-clock/MIGRATE.md | 5 +++++ rs/moq-e2ee/MIGRATE.md | 5 +++++ rs/moq-ffi/MIGRATE.md | 5 +++++ rs/moq-flate/MIGRATE.md | 5 +++++ rs/moq-gst/MIGRATE.md | 5 +++++ rs/moq-hls/MIGRATE.md | 5 +++++ rs/moq-json/MIGRATE.md | 5 +++++ rs/moq-loc/MIGRATE.md | 5 +++++ rs/moq-msf/MIGRATE.md | 5 +++++ rs/moq-mux/MIGRATE.md | 5 +++++ rs/moq-net/MIGRATE.md | 5 +++++ rs/moq-nvenc/MIGRATE.md | 5 +++++ rs/moq-pattern/MIGRATE.md | 5 +++++ rs/moq-quic/MIGRATE.md | 5 +++++ rs/moq-relay/MIGRATE.md | 5 +++++ rs/moq-room/MIGRATE.md | 5 +++++ rs/moq-rtc/MIGRATE.md | 5 +++++ rs/moq-rtmp/MIGRATE.md | 5 +++++ rs/moq-sock/MIGRATE.md | 5 +++++ rs/moq-srt/MIGRATE.md | 5 +++++ rs/moq-stats/MIGRATE.md | 5 +++++ rs/moq-tokio/MIGRATE.md | 5 +++++ rs/moq-transcode/MIGRATE.md | 5 +++++ rs/moq-uring/MIGRATE.md | 5 +++++ rs/moq-v4l/MIGRATE.md | 5 +++++ rs/moq-video/MIGRATE.md | 5 +++++ swift/MIGRATE.md | 5 +++++ 57 files changed, 285 insertions(+) create mode 100644 cpp/moq/MIGRATE.md create mode 100644 cpp/obs/MIGRATE.md create mode 100644 dart/moq/MIGRATE.md create mode 100644 go/wrapper/MIGRATE.md create mode 100644 js/auth/MIGRATE.md create mode 100644 js/clock/MIGRATE.md create mode 100644 js/flate/MIGRATE.md create mode 100644 js/hang/MIGRATE.md create mode 100644 js/json/MIGRATE.md create mode 100644 js/loc/MIGRATE.md create mode 100644 js/moq-boy/MIGRATE.md create mode 100644 js/msf/MIGRATE.md create mode 100644 js/net/MIGRATE.md create mode 100644 js/p2p/MIGRATE.md create mode 100644 js/pattern/MIGRATE.md create mode 100644 js/publish/MIGRATE.md create mode 100644 js/room/MIGRATE.md create mode 100644 js/signals/MIGRATE.md create mode 100644 js/video/MIGRATE.md create mode 100644 js/watch/MIGRATE.md create mode 100644 kt/moq/MIGRATE.md create mode 100644 py/moq-rs/MIGRATE.md create mode 100644 rs/hang/MIGRATE.md create mode 100644 rs/kio/MIGRATE.md create mode 100644 rs/moq-archive/MIGRATE.md create mode 100644 rs/moq-audio/MIGRATE.md create mode 100644 rs/moq-auth/MIGRATE.md create mode 100644 rs/moq-boy/MIGRATE.md create mode 100644 rs/moq-c/MIGRATE.md create mode 100644 rs/moq-cli/MIGRATE.md create mode 100644 rs/moq-clock/MIGRATE.md create mode 100644 rs/moq-e2ee/MIGRATE.md create mode 100644 rs/moq-ffi/MIGRATE.md create mode 100644 rs/moq-flate/MIGRATE.md create mode 100644 rs/moq-gst/MIGRATE.md create mode 100644 rs/moq-hls/MIGRATE.md create mode 100644 rs/moq-json/MIGRATE.md create mode 100644 rs/moq-loc/MIGRATE.md create mode 100644 rs/moq-msf/MIGRATE.md create mode 100644 rs/moq-mux/MIGRATE.md create mode 100644 rs/moq-net/MIGRATE.md create mode 100644 rs/moq-nvenc/MIGRATE.md create mode 100644 rs/moq-pattern/MIGRATE.md create mode 100644 rs/moq-quic/MIGRATE.md create mode 100644 rs/moq-relay/MIGRATE.md create mode 100644 rs/moq-room/MIGRATE.md create mode 100644 rs/moq-rtc/MIGRATE.md create mode 100644 rs/moq-rtmp/MIGRATE.md create mode 100644 rs/moq-sock/MIGRATE.md create mode 100644 rs/moq-srt/MIGRATE.md create mode 100644 rs/moq-stats/MIGRATE.md create mode 100644 rs/moq-tokio/MIGRATE.md create mode 100644 rs/moq-transcode/MIGRATE.md create mode 100644 rs/moq-uring/MIGRATE.md create mode 100644 rs/moq-v4l/MIGRATE.md create mode 100644 rs/moq-video/MIGRATE.md create mode 100644 swift/MIGRATE.md diff --git a/cpp/moq/MIGRATE.md b/cpp/moq/MIGRATE.md new file mode 100644 index 0000000000..857826f310 --- /dev/null +++ b/cpp/moq/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating the C++ moq library + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/cpp/obs/MIGRATE.md b/cpp/obs/MIGRATE.md new file mode 100644 index 0000000000..cdc00b98c6 --- /dev/null +++ b/cpp/obs/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating the obs-moq plugin + +How to update your OBS setup for each breaking change, newest first. + +## Unreleased diff --git a/dart/moq/MIGRATE.md b/dart/moq/MIGRATE.md new file mode 100644 index 0000000000..d95169c422 --- /dev/null +++ b/dart/moq/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating the Dart moq package + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/go/wrapper/MIGRATE.md b/go/wrapper/MIGRATE.md new file mode 100644 index 0000000000..2868a0985b --- /dev/null +++ b/go/wrapper/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating the Go moq.dev/moq module + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/auth/MIGRATE.md b/js/auth/MIGRATE.md new file mode 100644 index 0000000000..27318772e3 --- /dev/null +++ b/js/auth/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/auth + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/clock/MIGRATE.md b/js/clock/MIGRATE.md new file mode 100644 index 0000000000..4fb228b08f --- /dev/null +++ b/js/clock/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/clock + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/flate/MIGRATE.md b/js/flate/MIGRATE.md new file mode 100644 index 0000000000..c8b5be25a6 --- /dev/null +++ b/js/flate/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/flate + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/hang/MIGRATE.md b/js/hang/MIGRATE.md new file mode 100644 index 0000000000..02ad94bcee --- /dev/null +++ b/js/hang/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/hang + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/json/MIGRATE.md b/js/json/MIGRATE.md new file mode 100644 index 0000000000..816dba679e --- /dev/null +++ b/js/json/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/json + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/loc/MIGRATE.md b/js/loc/MIGRATE.md new file mode 100644 index 0000000000..edf53f4aaa --- /dev/null +++ b/js/loc/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/loc + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/moq-boy/MIGRATE.md b/js/moq-boy/MIGRATE.md new file mode 100644 index 0000000000..9548201a3a --- /dev/null +++ b/js/moq-boy/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/boy + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/msf/MIGRATE.md b/js/msf/MIGRATE.md new file mode 100644 index 0000000000..59da93d337 --- /dev/null +++ b/js/msf/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/msf + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/net/MIGRATE.md b/js/net/MIGRATE.md new file mode 100644 index 0000000000..a566b93313 --- /dev/null +++ b/js/net/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/net + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/p2p/MIGRATE.md b/js/p2p/MIGRATE.md new file mode 100644 index 0000000000..1dd9e38670 --- /dev/null +++ b/js/p2p/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/p2p + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/pattern/MIGRATE.md b/js/pattern/MIGRATE.md new file mode 100644 index 0000000000..cd044d9ce2 --- /dev/null +++ b/js/pattern/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/pattern + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/publish/MIGRATE.md b/js/publish/MIGRATE.md new file mode 100644 index 0000000000..355ef22fcf --- /dev/null +++ b/js/publish/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/publish + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/room/MIGRATE.md b/js/room/MIGRATE.md new file mode 100644 index 0000000000..ec9a3825b0 --- /dev/null +++ b/js/room/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/room + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/signals/MIGRATE.md b/js/signals/MIGRATE.md new file mode 100644 index 0000000000..2b78f4c68c --- /dev/null +++ b/js/signals/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/signals + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/video/MIGRATE.md b/js/video/MIGRATE.md new file mode 100644 index 0000000000..df967b406e --- /dev/null +++ b/js/video/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/video + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/js/watch/MIGRATE.md b/js/watch/MIGRATE.md new file mode 100644 index 0000000000..71917d65d2 --- /dev/null +++ b/js/watch/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating @moq/watch + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/kt/moq/MIGRATE.md b/kt/moq/MIGRATE.md new file mode 100644 index 0000000000..10dd50e461 --- /dev/null +++ b/kt/moq/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating the Kotlin dev.moq:moq package + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/py/moq-rs/MIGRATE.md b/py/moq-rs/MIGRATE.md new file mode 100644 index 0000000000..16256db613 --- /dev/null +++ b/py/moq-rs/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating the Python moq-rs package + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/hang/MIGRATE.md b/rs/hang/MIGRATE.md new file mode 100644 index 0000000000..ede8d5b41d --- /dev/null +++ b/rs/hang/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating hang + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/kio/MIGRATE.md b/rs/kio/MIGRATE.md new file mode 100644 index 0000000000..22378a4a29 --- /dev/null +++ b/rs/kio/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating kio + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-archive/MIGRATE.md b/rs/moq-archive/MIGRATE.md new file mode 100644 index 0000000000..6e521f6313 --- /dev/null +++ b/rs/moq-archive/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-archive + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-audio/MIGRATE.md b/rs/moq-audio/MIGRATE.md new file mode 100644 index 0000000000..d82bde2589 --- /dev/null +++ b/rs/moq-audio/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-audio + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-auth/MIGRATE.md b/rs/moq-auth/MIGRATE.md new file mode 100644 index 0000000000..f5ba69a75a --- /dev/null +++ b/rs/moq-auth/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-auth + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-boy/MIGRATE.md b/rs/moq-boy/MIGRATE.md new file mode 100644 index 0000000000..0ebf5eefe8 --- /dev/null +++ b/rs/moq-boy/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-boy + +How to update your invocation and config for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-c/MIGRATE.md b/rs/moq-c/MIGRATE.md new file mode 100644 index 0000000000..cd31ce05bf --- /dev/null +++ b/rs/moq-c/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-c + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-cli/MIGRATE.md b/rs/moq-cli/MIGRATE.md new file mode 100644 index 0000000000..25b46c383d --- /dev/null +++ b/rs/moq-cli/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-cli + +How to update your invocation and config for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-clock/MIGRATE.md b/rs/moq-clock/MIGRATE.md new file mode 100644 index 0000000000..5a47650137 --- /dev/null +++ b/rs/moq-clock/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-clock + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-e2ee/MIGRATE.md b/rs/moq-e2ee/MIGRATE.md new file mode 100644 index 0000000000..7b4a87b055 --- /dev/null +++ b/rs/moq-e2ee/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-e2ee + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-ffi/MIGRATE.md b/rs/moq-ffi/MIGRATE.md new file mode 100644 index 0000000000..176107a91f --- /dev/null +++ b/rs/moq-ffi/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-ffi + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-flate/MIGRATE.md b/rs/moq-flate/MIGRATE.md new file mode 100644 index 0000000000..3eabb58ae7 --- /dev/null +++ b/rs/moq-flate/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-flate + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-gst/MIGRATE.md b/rs/moq-gst/MIGRATE.md new file mode 100644 index 0000000000..784a2c6b6f --- /dev/null +++ b/rs/moq-gst/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-gst + +How to update your pipelines for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-hls/MIGRATE.md b/rs/moq-hls/MIGRATE.md new file mode 100644 index 0000000000..ac5a87f356 --- /dev/null +++ b/rs/moq-hls/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-hls + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-json/MIGRATE.md b/rs/moq-json/MIGRATE.md new file mode 100644 index 0000000000..92ae528753 --- /dev/null +++ b/rs/moq-json/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-json + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-loc/MIGRATE.md b/rs/moq-loc/MIGRATE.md new file mode 100644 index 0000000000..ad61a7f1b1 --- /dev/null +++ b/rs/moq-loc/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-loc + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-msf/MIGRATE.md b/rs/moq-msf/MIGRATE.md new file mode 100644 index 0000000000..dfbf2ebb47 --- /dev/null +++ b/rs/moq-msf/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-msf + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-mux/MIGRATE.md b/rs/moq-mux/MIGRATE.md new file mode 100644 index 0000000000..cbe7dd8463 --- /dev/null +++ b/rs/moq-mux/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-mux + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-net/MIGRATE.md b/rs/moq-net/MIGRATE.md new file mode 100644 index 0000000000..f71dac343a --- /dev/null +++ b/rs/moq-net/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-net + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-nvenc/MIGRATE.md b/rs/moq-nvenc/MIGRATE.md new file mode 100644 index 0000000000..d7b0893b06 --- /dev/null +++ b/rs/moq-nvenc/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-nvenc + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-pattern/MIGRATE.md b/rs/moq-pattern/MIGRATE.md new file mode 100644 index 0000000000..26d95c4c66 --- /dev/null +++ b/rs/moq-pattern/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-pattern + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-quic/MIGRATE.md b/rs/moq-quic/MIGRATE.md new file mode 100644 index 0000000000..3a4504020a --- /dev/null +++ b/rs/moq-quic/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-quic + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-relay/MIGRATE.md b/rs/moq-relay/MIGRATE.md new file mode 100644 index 0000000000..23cffed67b --- /dev/null +++ b/rs/moq-relay/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-relay + +How to update your code, invocation, and config for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-room/MIGRATE.md b/rs/moq-room/MIGRATE.md new file mode 100644 index 0000000000..fab249cc52 --- /dev/null +++ b/rs/moq-room/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-room + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-rtc/MIGRATE.md b/rs/moq-rtc/MIGRATE.md new file mode 100644 index 0000000000..aa40e55f81 --- /dev/null +++ b/rs/moq-rtc/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-rtc + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-rtmp/MIGRATE.md b/rs/moq-rtmp/MIGRATE.md new file mode 100644 index 0000000000..93ad5c141a --- /dev/null +++ b/rs/moq-rtmp/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-rtmp + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-sock/MIGRATE.md b/rs/moq-sock/MIGRATE.md new file mode 100644 index 0000000000..5d99d6b523 --- /dev/null +++ b/rs/moq-sock/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-sock + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-srt/MIGRATE.md b/rs/moq-srt/MIGRATE.md new file mode 100644 index 0000000000..323cad66b1 --- /dev/null +++ b/rs/moq-srt/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-srt + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-stats/MIGRATE.md b/rs/moq-stats/MIGRATE.md new file mode 100644 index 0000000000..7e6549b2bf --- /dev/null +++ b/rs/moq-stats/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-stats + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-tokio/MIGRATE.md b/rs/moq-tokio/MIGRATE.md new file mode 100644 index 0000000000..b67ad2fe11 --- /dev/null +++ b/rs/moq-tokio/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-tokio + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-transcode/MIGRATE.md b/rs/moq-transcode/MIGRATE.md new file mode 100644 index 0000000000..284a4f84fa --- /dev/null +++ b/rs/moq-transcode/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-transcode + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-uring/MIGRATE.md b/rs/moq-uring/MIGRATE.md new file mode 100644 index 0000000000..d517976cdc --- /dev/null +++ b/rs/moq-uring/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-uring + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-v4l/MIGRATE.md b/rs/moq-v4l/MIGRATE.md new file mode 100644 index 0000000000..f2cf389afb --- /dev/null +++ b/rs/moq-v4l/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-v4l + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-video/MIGRATE.md b/rs/moq-video/MIGRATE.md new file mode 100644 index 0000000000..89b05444ee --- /dev/null +++ b/rs/moq-video/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-video + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/swift/MIGRATE.md b/swift/MIGRATE.md new file mode 100644 index 0000000000..da2207e765 --- /dev/null +++ b/swift/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating the Swift Moq package + +How to update your code for each breaking change, newest first. + +## Unreleased From ebb12f60f43355abe28074d99643c2dd9fae8af9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 16:32:04 -0700 Subject: [PATCH 03/40] docs: backfill MIGRATE.md for the core and Rust media crates Co-Authored-By: Claude Opus 5.5 --- js/auth/MIGRATE.md | 25 ++++ js/net/MIGRATE.md | 208 ++++++++++++++++++++++++++++++ js/signals/MIGRATE.md | 35 +++++ rs/hang/MIGRATE.md | 39 ++++++ rs/kio/MIGRATE.md | 23 ++++ rs/moq-archive/MIGRATE.md | 17 +++ rs/moq-audio/MIGRATE.md | 37 ++++++ rs/moq-auth/MIGRATE.md | 27 ++++ rs/moq-clock/MIGRATE.md | 6 + rs/moq-e2ee/MIGRATE.md | 17 +++ rs/moq-flate/MIGRATE.md | 34 +++++ rs/moq-hls/MIGRATE.md | 43 +++++++ rs/moq-json/MIGRATE.md | 40 ++++++ rs/moq-msf/MIGRATE.md | 5 + rs/moq-mux/MIGRATE.md | 204 ++++++++++++++++++++++++++++++ rs/moq-net/MIGRATE.md | 260 ++++++++++++++++++++++++++++++++++++++ rs/moq-rtc/MIGRATE.md | 17 +++ rs/moq-rtmp/MIGRATE.md | 11 ++ rs/moq-sock/MIGRATE.md | 18 +++ rs/moq-srt/MIGRATE.md | 5 + rs/moq-stats/MIGRATE.md | 5 + rs/moq-tokio/MIGRATE.md | 174 +++++++++++++++++++++++++ rs/moq-uring/MIGRATE.md | 39 ++++++ rs/moq-video/MIGRATE.md | 64 ++++++++++ 24 files changed, 1353 insertions(+) diff --git a/js/auth/MIGRATE.md b/js/auth/MIGRATE.md index 27318772e3..49e6a2e894 100644 --- a/js/auth/MIGRATE.md +++ b/js/auth/MIGRATE.md @@ -3,3 +3,28 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### A token must carry its key's `kid` (#3943) + +`Key.verify` refuses a token whose `kid` header does not match the key's, and a key set accepts a token without a `kid` only when it holds one key that has none. +The strict transport schema also accepts `rtmp`, `srt`, and `webrtc`, so an exhaustive `switch` over it needs those arms. + +```ts +// Sign with the key that will verify it, so the kid matches. +const token = await Key.sign(key, claims); +``` + +## @moq/token 0.3 -> @moq/auth 0.2 + +### @moq/token is @moq/auth + +`sign` and `verify` are `Key.sign` and `Key.verify`, and claims are pattern unions: `alice` covers only `alice`, so sign `alice/**` for the subtree. + +```ts +// Before +import { sign } from "@moq/token"; +const token = await sign(key, { put: ["alice"] }); +// After +import { Key } from "@moq/auth"; +const token = await Key.sign(key, { publish: "alice/**" }); +``` diff --git a/js/net/MIGRATE.md b/js/net/MIGRATE.md index a566b93313..3d9f805dc3 100644 --- a/js/net/MIGRATE.md +++ b/js/net/MIGRATE.md @@ -3,3 +3,211 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Superseded groups age by wall clock too (#5548) + +A superseded group is stale once its successor arrived a full `maxDelay` ago, as well as by media time. +Untimed tracks are no longer exempt: with the default zero `maxDelay`, an untimed subscriber starts at the latest group and skips each superseded one. +Give it a budget to replay or tolerate reordering. + +```ts +const sub = track.subscribe({ maxDelay: Milli(1000) }); +``` + +### Time units and timers come from `@moq/clock` (#5706) + +`Time.Nano`, `Time.Micro`, `Time.Milli`, and `Time.Second` are gone. +Import them from `@moq/clock`, which throws on negative or non-finite values. + +```ts +// Before +import { Time } from "@moq/net"; +const delay = Time.Milli(250); +// After +import { Milli } from "@moq/clock"; +const delay = Milli(250); +``` + +### Announced requests follow restarts, and `broadcasts()` is gone + +An unpinned `request(path, { announced: true })` stays open across publisher instances: its `active` moves to each new instance's broadcast and clears between them, instead of ending with `Unroutable`. +Read `active` reactively and close the request when done, or pass `epoch` to keep the old single-instance behavior. +`Origin.Producer` and `Origin.Consumer` drop `broadcasts(scope)`; iterate `announced(scope)` instead. + +```ts +// Before +for (const broadcast of origin.broadcasts(scope)) { ... } +// After +for await (const event of origin.announced(scope)) { + if (event.kind === "start") { ... } +} +``` + +### Discovery follows what something watches, and `discovery` is gone + +A `consume` origin stays empty until a reader watches it with `announced(scope)` or `request(path, { announced: true })`; the session then asks the relay about those prefixes only. +Code that called `request(path)` expecting a route without watching should hold an `announced(scope)` stream. +The `discovery` option on `connect`, `accept`, and `Connection`, and `Established.discovery`, are removed; passing the option throws a `TypeError`. + +```ts +// Before +const connection = new Connection({ url, discovery: true }); +// After +const connection = new Connection({ url }); +``` + +### Announce streams yield `start`, `update`, `end`, and `restart` events + +`Announce.Consumer` yields an `Announce.Event`: an `Announce.Announce` (`prefix`, `captures`, `route`) plus a `kind`. +`Announce.Update`, `Announce.Kind`, and `Announce.isActive` are gone. +A `"restart"` means another publisher instance now serves the prefix: request the path again. + +```ts +// Before +if (!Announce.isActive(update.kind)) continue; +// After +if (event.kind === "end") continue; +if (event.kind === "start" || event.kind === "restart") resolve(event.prefix); +``` + +### Routes carry the publisher epoch (#4942) + +`Path.joinEpoch` and `Path.splitEpoch` are gone. +Set the epoch on the route with `announce({ epoch })` and read `Route.epoch`; pin a request to one instance with `RequestOptions.epoch`. +Nothing mints an epoch by default. + +```ts +// Before +origin.createBroadcast(Path.joinEpoch(path, epoch)).announce(); +// After +origin.createBroadcast(path).announce({ epoch }); +const request = origin.request(path, { epoch }); +``` + +### Publishing never invents a timestamp (#5070) + +`Group.Producer` and `Track.Producer` drop `writeString`, `writeJson`, and `writeBool`, and `@moq/net/zod` drops `write`. +Encode the payload yourself and write it as a frame with an explicit timestamp, or none on an untimed track. + +```ts +// Before +track.writeJson(value); +// After +track.writeFrame({ payload: new TextEncoder().encode(JSON.stringify(value)), timestamp }); +``` + +### A track is timed or untimed (#4968) + +`Track.Info.timescale` is optional; omitted or `null` means untimed, and `infoDefaults()` no longer fills in milliseconds. +`Group.Frame.timestamp` and `Track.Datagram.timestamp` are optional, and `appendDatagram` / `insertDatagram` take `Timestamp | undefined`. +A write whose timedness does not match its track throws `Error.TimestampMismatch`, so declare a timescale on every track you stamp. + +```ts +// Before +const track = broadcast.createTrack("chat"); +// After +const track = broadcast.createTrack("chat", { timescale: Timescale.MILLI }); +``` + +### Subscriber staleness is `maxDelay` + +`Track.Subscription`'s `maxAge` is `maxDelay`; `maxAge` stays the publisher's retention on `Track.Info`. +A `maxAge` key in subscription options throws a `TypeError`, even when `undefined` or beside `maxDelay`. + +```ts +// Before +track.subscribe({ maxAge: Milli(500) }); +// After +track.subscribe({ maxDelay: Milli(500) }); +``` + +### Publisher retention is optional (#4242) + +`Track.Info.maxAge` is optional, absent meaning no publisher limit, and `DEFAULT_MAX_AGE_MS` is gone. + +```ts +// Before +broadcast.createTrack("video", { maxAge: DEFAULT_MAX_AGE_MS }); +// After +broadcast.createTrack("video", { maxAge: Milli(30_000) }); +``` + +### Demand is read through `demand()` + +`Track.Producer` and `Group.Producer` move `used` and `unused()` to `demand()`. +`Allocator.reserve` takes `producer.demand()`, replacing the `Bandwidth.Demand` interface. + +```ts +// Before +await producer.unused(); +// After +await producer.demand().unused(); +``` + +### A replacement track owns its start sequence (#5068) + +`BroadcastProducer.insertTrack` is removed. +`Track.Producer.appendGroup` and `appendDatagram` throw `duplicate sequence` instead of continuing past an earlier producer of the same name; `createTrack` now answers a queued request instead of throwing `duplicate track`. +`Request.accept` closes the request's producer with the error whenever it throws. + +### Route cost is a `bigint` (#4661) + +`Route.cost` is one `bigint` instead of separate warm and cold numbers. + +```ts +// Before +broadcast.announce({ cost: { warm: 10, cold: 10 } }); +// After +broadcast.announce({ cost: 10n }); +``` + +### Datagrams outside the group range are skipped (#4982) + +A subscriber no longer receives datagrams outside its group range, and a FETCH never returns one. + +### Broadcast `close` takes no abort reason (#4152) + +`broadcast.Producer.close(abort)` and `broadcast.Consumer.close(abort)` lose the parameter, and `closed` is a `GetPromise`. + +```ts +// Before +broadcast.close(new Error("done")); +// After +broadcast.close(); +``` + +## 0.3 -> 0.4 + +### One `Connection` (#3614, #3636) + +`Connection.Reload` is `new Connection({ url })`, which pools one connection per relay. +`closed` settles only on `close()`; the error that stopped retrying is `error`. + +```ts +// Before +const connection = new Moq.Connection.Reload({ url }); +// After +const connection = new Moq.Connection({ url }); +``` + +### Origins hold broadcasts (#2705, #3225) + +`connection.publish(path, broadcast)` is `origin.createBroadcast(path)` then `broadcast.announce()`. +`connection.announced(prefix)` is `origin.announced(scope)`, yielding `{ prefix, kind, route }`. +`ignoreSelf` is gone: reflected announces are always dropped. + +```ts +// Before +connection.publish(path, broadcast); +// After +const broadcast = connection.origin.createBroadcast(path); +broadcast.announce(); +``` + +### Names mirror Rust (#3710) + +| Before | After | +| --- | --- | +| `latencyMax` | `maxAge` (a `Time.Milli`) | +| `writeDatagram` | `insertDatagram` | +| `RemoteError` | `Error.Stream` / `Error.Session` | diff --git a/js/signals/MIGRATE.md b/js/signals/MIGRATE.md index 2b78f4c68c..69896fda24 100644 --- a/js/signals/MIGRATE.md +++ b/js/signals/MIGRATE.md @@ -3,3 +3,38 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Timers take a `Nano` (#5706) + +`effect.timer`, `effect.timeout`, and `effect.interval` take a `Nano` from `@moq/clock` instead of milliseconds, and run on the installed clock. + +```ts +// Before +effect.timer(() => tick(), 250); +// After +import { Milli, Nano } from "@moq/clock"; +effect.timer(() => tick(), Nano.fromMilli(Milli(250))); +``` + +### Effect cleanups run last-in, first-out (#4889) + +An effect tears down its cleanups newest first, like `DisposableStack`. +A resource registered later, such as a nested `run` built on an earlier one, is now released before what it depends on. +Check any cleanup that relied on running after one registered later. + +### `Signal.race` returns a `GetPromise` (#4226) + +`Signal.race` releases its losing listeners and returns a `GetPromise` instead of a native `Promise`. +`await` and `.then()` still work; `.catch()` and `.finally()` do not. +Waiting begins when it is awaited or subscribed. + +```ts +// Before +Signal.race(signals).catch(handle); +// After +try { + await Signal.race(signals); +} catch (err) { + handle(err); +} +``` diff --git a/rs/hang/MIGRATE.md b/rs/hang/MIGRATE.md index ede8d5b41d..2fff9311ab 100644 --- a/rs/hang/MIGRATE.md +++ b/rs/hang/MIGRATE.md @@ -3,3 +3,42 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Timeline records are optionally timed (#5720) + +`timeline::Record::pts` and `duration` are `Option`, absent on an untimed track. +Build an untimed record with `Record::untimed(sequence, start, end)`. + +```rust +// Before +let pts: u64 = record.pts; +// After +let Some(pts) = record.pts else { continue }; +``` + +### One `enabled` flag replaces `stalled` (#4915) + +`VideoConfig::stalled: Option` is `enabled: bool`, with the meaning inverted, and `AudioConfig` gains `enabled`. +The `catalog::stalled` module is gone, and `Video::ranked()` puts disabled renditions last. +`stalled` is no longer written and is ignored on read. + +```rust +// Before +video.stalled = Some(true); +// After +video.enabled = false; +``` + +## 0.20 -> 0.21 + +### The catalog's `timeline` entry is `archive` + +The root catalog's `timeline` is `archive` (`catalog::Archive`), and wall time moved to the root `clock: { wall, timescale }`. +An old publisher's catalog has no `archive`, so upgrade publishers before readers that need it. + +```rust +// Before +let wall = catalog.timeline.and_then(|t| t.wall); +// After +let clock = catalog.clock; +``` diff --git a/rs/kio/MIGRATE.md b/rs/kio/MIGRATE.md index 22378a4a29..ff0ef163aa 100644 --- a/rs/kio/MIGRATE.md +++ b/rs/kio/MIGRATE.md @@ -3,3 +3,26 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### `Pollable` merged into `Task`, and unused surface removed (#4156) + +`Fan`, `Hold`, `WeakLock`, `Pollable`, and the public `Lock::downgrade` are gone. +`Task` gained an `Output` type and has a blanket impl for `FnMut(&Waiter) -> Poll`. +`wait(f)` is `Pending::new(f)`. +`poll_used` and `poll_unused` return `Poll>` instead of `Poll>`. +`Waiter::context()` replaces `Context::from_waker(waiter.waker())`. + +```rust +// Before +let value = kio::wait(|waiter: &kio::Waiter| state.poll(waiter)).await; +match producer.poll_unused(waiter) { + Poll::Ready(None) => {} // closed + _ => {} +} +// After +let value = kio::Pending::new(|waiter: &kio::Waiter| state.poll(waiter)).await; +match producer.poll_unused(waiter) { + Poll::Ready(Err(kio::Closed)) => {} + _ => {} +} +``` diff --git a/rs/moq-archive/MIGRATE.md b/rs/moq-archive/MIGRATE.md index 6e521f6313..46b404eb46 100644 --- a/rs/moq-archive/MIGRATE.md +++ b/rs/moq-archive/MIGRATE.md @@ -3,3 +3,20 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Untimed tracks record untimed (#5720) + +`Info::new` and `Info::timescale` take and return `Option`, and `Error::TimescaleMismatch` carries `Option` fields. +`segment::Frame::timestamp` is `Option`. +`Object::decode(buf, timed)` and `Store::get_segments(track, segment, timed)` take whether the track is timed. +`Error::Untimed` now means a DVR cannot record an untimed track; mismatched timing is `Error::Timing`. +Untimed recordings use the version 2 format, which an older version 2 reader refuses. + +```rust +// Before +let info = Info::new(priority, timescale)?; +let segments = store.get_segments(track, segment).await?; +// After +let info = Info::new(priority, Some(timescale))?; +let segments = store.get_segments(track, segment, info.timescale.is_some()).await?; +``` diff --git a/rs/moq-audio/MIGRATE.md b/rs/moq-audio/MIGRATE.md index d82bde2589..a88a8ef10e 100644 --- a/rs/moq-audio/MIGRATE.md +++ b/rs/moq-audio/MIGRATE.md @@ -3,3 +3,40 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Capture handles are `Control` and `Capture` + +`encode::Publication` is `encode::Control`, and `encode::PublicationOptions` is `encode::Capture`, without its `clock` field: the driver stamps on `catalog.clock()`. + +```rust +// Before +let options = PublicationOptions { capture, encode, clock }; +let (publication, driver) = Publication::new(broadcast, catalog, options)?; +// After +let mut options = encode::Capture::default(); +options.capture = capture; +options.encode = encode; +let (control, driver) = encode::Control::new(broadcast, catalog, options)?; +``` + +### `reset_epoch` is `reanchor` (#5146) + +"Epoch" means only the publisher epoch, so the audio re-anchor is renamed. + +```rust +// Before +producer.reset_epoch(); +// After +producer.reanchor(); +``` + +### Decoder staleness is `max_delay` + +`decode::Options::max_age` is `max_delay`, and `decode::Consumer::max_age()` is `max_delay()`. + +```rust +// Before +options.max_age = delay; +// After +options.max_delay = delay; +``` diff --git a/rs/moq-auth/MIGRATE.md b/rs/moq-auth/MIGRATE.md index f5ba69a75a..9340f745f7 100644 --- a/rs/moq-auth/MIGRATE.md +++ b/rs/moq-auth/MIGRATE.md @@ -3,3 +3,30 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Gateway transports and key sets are non-exhaustive (#3943) + +`Transport` gains `Rtmp`, `Srt`, `WebRtc`, and `WebTransport`, and `serve::Keys` gains `Set`; both are `#[non_exhaustive]`, so add a wildcard arm. +A WebTransport session reports `WebTransport` instead of `Quic`, so a policy keyed on `Quic` must accept both. +A JWT verified with one key must carry that key's `kid`; a token without one is accepted only when the key has none. + +```rust +match transport { + Transport::Quic => {} + _ => {} +} +``` + +## moq-token 0.7 -> moq-auth 0.1 + +### moq-token is moq-auth (#3684) + +`Claims` holds `Patterns`, and `Claims::authorize` returns pattern residuals. +A `publish` or `subscribe` grant is a pattern: `alice` covers only `alice`, so sign `alice/**` for the subtree. + +```rust +// Before +use moq_token::Claims; +// After +use moq_auth::Claims; +``` diff --git a/rs/moq-clock/MIGRATE.md b/rs/moq-clock/MIGRATE.md index 5a47650137..5729915579 100644 --- a/rs/moq-clock/MIGRATE.md +++ b/rs/moq-clock/MIGRATE.md @@ -3,3 +3,9 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### moq-clock is a time library (#5465) + +moq-clock 0.10 was the "CLOCK over QUIC" example binary. +0.11 is a library: logical instants, wall time, deadlines, and a deterministic simulator. +The clock example lives on as `cargo run -p moq-tokio --example clock`. diff --git a/rs/moq-e2ee/MIGRATE.md b/rs/moq-e2ee/MIGRATE.md index 7b4a87b055..f36e1f97b9 100644 --- a/rs/moq-e2ee/MIGRATE.md +++ b/rs/moq-e2ee/MIGRATE.md @@ -3,3 +3,20 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### The epoch comes from the route (#4942) + +The protected path is the opaque broadcast path; the publisher epoch is read from the route instead of a path suffix. +On wires that do not carry an epoch, the application supplies it. + +### Timestamps are optional + +`Frame` and `Datagram` timestamps are `Option`, and the writers take `impl Into>`, so passing a `Timestamp` still compiles. +A frame read from an untimed track has no timestamp. + +```rust +// Before +let ts: Timestamp = frame.timestamp; +// After +let ts: Option = frame.timestamp; +``` diff --git a/rs/moq-flate/MIGRATE.md b/rs/moq-flate/MIGRATE.md index 3eabb58ae7..6195842b32 100644 --- a/rs/moq-flate/MIGRATE.md +++ b/rs/moq-flate/MIGRATE.md @@ -3,3 +3,37 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### moq-binary is moq-flate + +The opaque `snapshot` and `stream` tracks moved beside the codec, so `moq_binary::X` is `moq_flate::X`; the wire is unchanged. +`moq_binary::Error::Flate(e)` is the matching `moq_flate::Error` variant (`Decompress`, `TooLarge`). +`moq_flate::Error` carries `moq_net::Error`, so it is no longer `PartialEq`. + +```rust +// Before +use moq_binary::snapshot; +// After +use moq_flate::snapshot; +``` + +### Demand is read through `demand()` + +```rust +// Before +if producer.is_used() {} +// After +if producer.demand().is_used() {} +``` + +### Timestamps are explicit + +Snapshot and stream producers publish `at: None` untimed, so their track must be untimed; pass a timestamp on a timed track. +Snapshot `modify(at)` and `mutate(at, f)` carry the caller's optional time. + +```rust +// Before +producer.update(bytes)?; +// After +producer.update(Timed::from(bytes).at(timestamp))?; +``` diff --git a/rs/moq-hls/MIGRATE.md b/rs/moq-hls/MIGRATE.md index ac5a87f356..de1423430f 100644 --- a/rs/moq-hls/MIGRATE.md +++ b/rs/moq-hls/MIGRATE.md @@ -3,3 +3,46 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Playlists take an optional range (#5705) + +`export::Rendition::playlist(query)` is `playlist(range, query)`. +Pass `None` for the live or history playlist. + +```rust +// Before +let playlist = rendition.playlist(query).await?; +// After +let playlist = rendition.playlist(None, query).await?; +``` + +### Discontinuities are absolute sequences + +`export::segments::Segment::discontinuity` is a `u64` sequence, absolute within one `Broadcaster`, instead of a `bool` or a per-cursor count. +Store the value with each segment and use the first retained one as `EXT-X-DISCONTINUITY-SEQUENCE`. +A new `Broadcaster` starts a new sequence namespace: start a new recording, or map its sequences behind a discontinuity. + +```rust +// Before +if segment.discontinuity { + sequence += 1; +} +// After +let sequence = segment.discontinuity; +``` + +### Playlists are bounded (#4978) + +Every timeline, durable ones included, lists a capped window of 256 segments. +Set `Config::history` to list a durable timeline past the window. +`master.m3u8` lists a video rendition only once a segment starts at a sync point, and answers 404 until one can start. + +```rust +let mut config = moq_hls::export::Config::default(); +config.history = true; +``` + +### `Broadcaster::new` is synchronous + +`Broadcaster::new` no longer awaits or fails. +A rendition whose sibling broadcast ends now ends its playlist instead of rebinding to a replacement. diff --git a/rs/moq-json/MIGRATE.md b/rs/moq-json/MIGRATE.md index 92ae528753..f8d85b99f5 100644 --- a/rs/moq-json/MIGRATE.md +++ b/rs/moq-json/MIGRATE.md @@ -3,3 +3,43 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Demand is read through `demand()` + +```rust +// Before +if producer.is_used() {} +// After +if producer.demand().is_used() {} +``` + +### Timestamps are explicit + +`moq_net::Timed

` drops its clock type parameter. +Snapshot, stream, and window producers publish `at: None` untimed, so their track must be untimed; pass a timestamp on a timed track. +Snapshot `modify(at)` and `mutate(at, f)` carry the caller's optional time through the edit guard. + +```rust +// Before +producer.update(&value)?; +let mut guard = producer.modify()?; +// After +producer.update(Timed::from(&value).at(timestamp))?; +let mut guard = producer.modify(Some(timestamp))?; +``` + +## 0.3 -> 0.4 + +### Configs split by role (#3718) + +`compression: bool` is a `Compression` enum, and options that own a track live in each mode's `producer::Config` and `consumer::Config`. +`lock()` is `modify()?`, and a guard that fails to publish on drop aborts the track (#3644). + +```rust +// Before +config.compression = true; +let mut value = producer.lock(); +// After +config.compression = Compression::Deflate; +let mut value = producer.modify()?; +``` diff --git a/rs/moq-msf/MIGRATE.md b/rs/moq-msf/MIGRATE.md index dfbf2ebb47..9ac0d723c8 100644 --- a/rs/moq-msf/MIGRATE.md +++ b/rs/moq-msf/MIGRATE.md @@ -3,3 +3,8 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### `Track::stalled` is removed (#4915) + +The non-standard `stalled` track field is gone and no longer written. +Drop it from any `Track` you build. diff --git a/rs/moq-mux/MIGRATE.md b/rs/moq-mux/MIGRATE.md index cbe7dd8463..dc59d93630 100644 --- a/rs/moq-mux/MIGRATE.md +++ b/rs/moq-mux/MIGRATE.md @@ -3,3 +3,207 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### The catalog consumer is opaque (#5630) + +Construct `catalog::Consumer` with `Consumer::new(broadcast, format).await`, or convert a `catalog::hang::Consumer` with `.into()`, then read it through `catalog::Stream`. +Its `Hang`, `Msf`, and `Closed` variants and `catalog::msf::Consumer::track` are private. + +```rust +// Before +let catalog = catalog::Consumer::Hang(hang_consumer); +// After +let catalog = catalog::Consumer::new(&broadcast, format).await?; +``` + +### Timelines record untimed tracks (#5720) + +`timeline::Segmenter::frame` takes `pts: impl Into>`, so existing callers compile. +A segmenter fed the wrong timedness for its track fails with `Error::TimelineTiming` or `Error::TimelineUntimed`. + +### An unset host clock publishes no wall mapping (#5429) + +`Clock::wall()` returns `Option` instead of panicking. +`Clock::wall_clock` and `Timebase::place` refuse a clock without one. + +```rust +// Before +let wall = clock.wall(); +// After +let Some(wall) = clock.wall() else { return Ok(()) }; +``` + +### TS export lingers within one publisher instance (#5147) + +`ts::Export::resume` is `ts::Export::follow`, `Source::returned` is gone, and `ts::Follower` follows a path's announcements the way `moq export ts` does. +`ts::Export::new(source, catalog)` takes a catalog stream like the other exporters: subscribe with `source.catalog::(format).await?` to keep MPEG-TS metadata, or `()` for media only, and select renditions on the stream before passing it in. +`follow(broadcast, catalog)` takes the replacement stream too, both methods are synchronous, and `with_ts` and `with_catalog_format` are removed. + +```rust +// Before +let export = ts::Export::new(source).await?.with_catalog_format(format); +// After +let catalog = source.catalog::(format).await?; +let export = ts::Export::new(source, catalog); +``` + +### `Source` is built from the resolved broadcast (#5252) + +`Source::new(origin, path)` is `Source::new(origin, broadcast)`: resolve the broadcast first, and catalog references resolve from its path. +`Source::broadcast()` returns it without awaiting. +Every later request for a path, siblings included, stays on the broadcast first held there and fails once it ends, so build a new `Source` to follow a new instance. + +```rust +// Before +let source = Source::new(origin, path); +// After +let broadcast = origin.request_broadcast(path, None).await?; +let source = Source::new(origin, broadcast); +``` + +### The export delay is `with_delay` (#4645) + +`ts::Export::with_max_age` and the FLV and MKV exports' `with_max_age` are `with_delay`: each frame is written that long after its decode time, on a clock that follows the source's. +`ts::Export` defaults to 1 s, and the send-ahead comes out of the same budget, so the output trails a TS source by the delay instead of twice it. +`ts::stats::Export` gains a dropped-frame count, measured drift, and an out-of-tolerance count, and is no longer `Eq`. + +```rust +// Before +let export = ts::Export::new(source, catalog).with_max_age(delay); +// After +let export = ts::Export::new(source, catalog).with_delay(delay); +``` + +### Subscriber staleness is `max_delay` + +| Before | After | +| --- | --- | +| `container::Consumer::set_max_age` | `set_max_delay` | +| fMP4, H.264, and H.265 exports' `with_max_age` | `with_max_delay` | + +### Importers keep the source's timestamps + +`clock::Anchor`, `clock::Lane`, `SourceMap`, `Clock::source`, and the importers' `live()` are gone. +Importers publish the source's own timestamps and anchor the catalog clock on their first frame. +An importer whose first frame arrives once the clock is in use (taken with `catalog.clock()`, published, or pinned with `Config::with_clock`) shifts its timestamps onto it, so its first frame lands at now. +Importers sharing a timestamp base reserve through one `catalog.timebase()`. + +```rust +// Before +let import = fmp4::Import::new(broadcast, reserved).live(); +// After +let import = fmp4::Import::new(broadcast, reserved); +``` + +### Importers refuse a timestamp rewind + +A backward timestamp is an encoder restart and needs a new broadcast. +Every importer, MPEG-TS included, now ends with `TimestampRewind` on a loop wrap, restart, or join below the edge instead of shifting past it. +`container::Producer` refuses a group that does not start strictly after the previous group's start, or a frame below the start of the group before its own. + +### Importers publish the catalog at their first frame + +The fMP4, MKV, and MPEG-TS importers hold the catalog until the first frame of a selected track, as FLV already did, so the first snapshot carries the final root `clock`. +A track `with_select` deselects does not release it. +A `moov` or `Tracks` decoded after `finish()` is refused with `fmp4::Error::MoovAfterFinish` or `mkv::Error::TracksAfterFinish`. + +### Video importers drop `tick`, `idle`, and `observe_lag` (#4915) + +The H.264, H.265, AV1, VP8, and VP9 `Import`s no longer take liveness hints; a stalled rendition is marked by the catalog's `enabled` flag instead. +Delete the calls. + +### fMP4 export fixes its track set at the init segment + +`fmp4::Error` drops `MissingVideoTrack`, `MissingAudioTrack`, and `NoCatalogSnapshot`, and adds `TrackAdded`, `TrackChanged`, `TrackRewound`, and `TrackUndescribed`. +`fmp4::Export` ends with one of these where it used to write a track missing from the moov, and a broadcast that ends with media queued behind an undescribed track is an error instead of `Ok(None)`. +Restart the export to pick up a new rendition. + +### CMAF timescales must match the init + +CMAF `Wire::write` refuses a track whose timescale is not the init's `mdhd` timescale with `fmp4::Error::TimescaleMismatch`. +A track you create yourself declares it with `track::Info::with_timescale`. + +### Data timestamps use the broadcast clock (#4821) + +JSON and flate producers publish `at: None` untimed, so their track must be untimed too; pass a timestamp on a timed track. +Convert a capture `Instant` through the catalog clock. + +```rust +// Before +producer.update(&value)?; +// After +producer.update(Timed::from(&value).at(catalog.clock().capture(instant)?))?; +``` + +### moq-binary is moq-flate + +`Error::Binary` is `Error::Flate`, carrying a `moq_flate::Error`. + +### AAC refuses channel counts it cannot name (#4973) + +`aac::Config::encode` returns a `Result` and fails with `UnsupportedChannelCount` for a count no `channelConfiguration` names, instead of writing stereo. +`From for hang::catalog::AudioConfig` is `TryFrom`. + +```rust +// Before +let audio: AudioConfig = aac.into(); +// After +let audio: AudioConfig = aac.try_into()?; +``` + +### The Opus mapping family lives only on `mapping` (#4851) + +`codec::opus::Config::mapping_family` is gone. +Family 0 is `mapping: None`; any other family is `mapping.family()`. + +```rust +// Before +let config = opus::Config { mapping_family: 1, mapping: Some(mapping), ..config }; +// After +let config = opus::Config { mapping: Some(mapping), ..config }; +``` + +### TS stats live in `ts::stats` (#4909) + +`ts::Stats` is `ts::stats::Snapshot`, and `ts::StreamStats` is `ts::stats::Stream`, whose `track` is an owned `String`. +`ts::Export::stats` returns `ts::stats::Export`; feed it to `stats::Log` with `.into()`. +`ts::MultipleProgramsError` is `#[non_exhaustive]`: recover it by downcast and read `programs`. + +### FLV export takes a catalog stream (#4987) + +`flv::Export::new` is synchronous and takes `(source, catalog)`, like `fmp4::Export` and `mkv::Export`. +`with_catalog_format` and `with_select` are gone: open the catalog yourself and narrow it with `catalog::Stream::select`. + +```rust +// Before +let export = flv::Export::new(source).await?.with_catalog_format(format).with_select(select); +// After +let catalog = source.catalog(format).await?.select(select); +let export = flv::Export::new(source, catalog); +``` + +## 0.9 -> 0.10 + +### Catalog edits are fallible (#3644, #3813) + +`lock()` is `modify()?`, and a guard that fails to publish on drop aborts the track. +`timeline::Config::wall` is the broadcast `Clock`. + +```rust +// Before +let mut catalog = producer.lock(); +// After +let mut catalog = producer.modify()?; +``` + +### Imports are typed + +`import::Init` splits into `AudioInit`, `VideoInit`, and `ContainerInit`, with typed formats instead of strings. +`Track::new` is `Track::audio` or `Track::video`. + +```rust +// Before +let track = import::Track::new(request, reserved, import::Init { format: "opus".into(), .. })?; +// After +let track = import::Track::audio(request, reserved, import::AudioInit { format: AudioFormat::Opus, .. })?; +``` diff --git a/rs/moq-net/MIGRATE.md b/rs/moq-net/MIGRATE.md index f71dac343a..5fa455ee0b 100644 --- a/rs/moq-net/MIGRATE.md +++ b/rs/moq-net/MIGRATE.md @@ -3,3 +3,263 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Superseded groups age by wall clock too (#5548) + +A superseded group is stale once its successor arrived a full `max_delay` ago, as well as by media time. +Untimed tracks are no longer exempt: with the default zero `max_delay`, an untimed subscriber starts at the latest group and skips each superseded one. +Give it a budget to replay or tolerate reordering. + +```rust +let sub = track::Subscription::default().with_max_delay(Duration::from_secs(1)); +``` + +### `cache::Pool::gc` takes a waiter + +Call it again when the waiter wakes, since a parked read can arm an earlier deadline than the one returned. + +```rust +// Before +let next = pool.gc(now); +// After +let next = pool.gc(now, &waiter); +``` + +### A replacement track owns its start sequence (#5068) + +`append_group` and `append_datagram` on a track that replaces an earlier producer of the same name return `Error::Duplicate` instead of continuing past the earlier producer's sequence. +Pick the start sequence yourself with `create_group`, or publish a new broadcast. + +### Announcements are `Start`, `Update`, `End`, and `Restart` events + +`announce::Consumer` yields `announce::Event`, carrying an `announce::Announce { prefix, captures, route }`. +`announce::Update` and `announce::Kind` are gone. +`Restart` means another publisher instance now serves the prefix: request the path again. +A newer epoch no longer ends open subscriptions with `Unroutable`; they stay on the old instance until dropped or its route goes. + +```rust +// Before +while let Some(update) = announced.next().await { + if !update.kind.is_active() { + continue; + } + let broadcast = origin.request_broadcast(&update.prefix).await?; +} +// After +while let Some(event) = announced.next().await { + match event { + announce::Event::Start(a) | announce::Event::Restart(a) => { + let broadcast = origin.request_broadcast(&a.prefix, a.route.epoch).await?; + } + announce::Event::Update(_) => {} + announce::Event::End(_) => {} + } +} +``` + +### Routes carry the publisher epoch (#4942) + +An epoch is a property of the route, not the path. +`Path::join_epoch` and `Path::split_epoch` are gone; set it with `Route::with_epoch` and read `Route::epoch`. +Nothing mints an epoch by default, and a route without one does not resume across routes. + +```rust +// Before +let path = path.join_epoch(epoch); +origin.publish(path, route); +// After +origin.publish(path, route.with_epoch(epoch)); +``` + +### `request_broadcast` takes an epoch + +Pass `None` to accept any publisher instance, or an `Epoch` to refuse every other one. +A dynamic route updated to another epoch refuses the requests its handler still holds with `Unroutable`. + +```rust +// Before +let broadcast = origin.request_broadcast(path).await?; +// After +let broadcast = origin.request_broadcast(path, None).await?; +``` + +### Demand is read through `demand()` + +`track::Producer`'s `is_used`, `used`, `unused`, and `poll_unused`, and `group::Producer`'s `used` and `unused`, moved to `demand()`. +`track::Request`, `track::Dynamic`, and `group::Request` poll it through `demand().poll_unused`, which returns `Poll>`: treat `Err` (the request closed) as unused too. +A `group::Request` is withdrawn once its last `fetch_group` caller leaves, so a handler that sees it unused just drops it; a later fetch queues a fresh request, and serving the abandoned one returns `Error::Duplicate`. + +```rust +// Before +track.unused().await; +// After +track.demand().unused().await?; +``` + +### moq-net owns its transport traits + +`moq_net::web_transport_trait` is gone. +`transport::poll::{Session, SendStream, RecvStream}` carry their own `poll_*` methods, `transport::Error`, and `transport::Stats`, and `Error::from_transport` takes a `transport::Error`. +A custom transport implements these traits and reports `binding()`: `transport::Binding::Uri` when its handshake carries the request URI, or `InBand` when SETUP carries it. + +```rust +impl moq_net::transport::poll::Session for MySession { + fn binding(&self) -> moq_net::transport::Binding { + moq_net::transport::Binding::Uri + } + // poll_accept_bi, poll_open_uni, ... +} +``` + +### Data publishes keep optional timestamps + +`Timed

` drops its clock type parameter and its `at` is an `Option`. +Publish `at: None` only on an untimed track; pass a timestamp on a timed one. + +```rust +// Before +Timed::::new(value, at) +// After +Timed { value, at: Some(at) } +``` + +### A track is timed or untimed + +`track::Info.timescale` is `Option`, `None` for an untimed track, and an undeclared timescale no longer defaults to milliseconds. +`Frame.timestamp`, `frame::Info.timestamp`, and `Datagram.timestamp` are `Option`, and `group::{Producer, Consumer}::timescale()` return `Option`. +A write whose timedness does not match its track fails with `TimestampMismatch`. +Tracks from moq-lite before 05 and moq-transport without `TIMESCALE` arrive untimed instead of stamped with their arrival time. + +```rust +// Before +let track = broadcast.create_track("video", None)?; // milliseconds +// After +let info = track::Info::default().with_timescale(Timescale::MILLI); +let track = broadcast.create_track("video", info)?; +``` + +### Subscriber staleness is `max_delay` + +How far a group may fall behind the live edge before a subscriber skips it is no longer named `max_age`, which stays the publisher's retention on `track::Info`. + +```rust +// Before +let sub = track::Subscription::default().with_max_age(delay); +// After +let sub = track::Subscription::default().with_max_delay(delay); +``` + +### Publisher retention is optional (#4242) + +`track::Info::max_age` is `Option`, `None` meaning no publisher limit. +`track::DEFAULT_MAX_AGE` and `origin::Config::default_max_age` are gone. + +```rust +// Before +let info = track::Info::default().with_max_age(track::DEFAULT_MAX_AGE); +// After +let info = track::Info::default().with_max_age(Duration::from_secs(30)); +``` + +### Route cost is one value (#4661) + +`Cost` is opaque: construct it with `Cost::new` and read it with `value()`. +The `warm` and `cold` fields are gone. + +```rust +// Before +let cost = Cost { warm: 10, cold: 10 }; +// After +let cost = Cost::new(10); +``` + +### Datagrams are unfetchable and bounded by the group range (#4982) + +A FETCH that reaches a datagram fails with `Error::NotFetchable` (received as `Error::Stream(StreamError::NotFetchable)`). +Datagrams outside a subscriber's group range are skipped. +Treat both like `NotFound` if you match on errors. + +### Sessions cap peer requests (#4820) + +A peer past the session's announce, subscription, or fetch bound loses the session with `TooManyRequests`. +Raise the defaults for a large relay mesh. + +```rust +let client = Client::new().with_limits(session::Limits { subscriptions: 100_000, ..Default::default() }); +``` + +### A path is one broadcast (#4741) + +Broadcast identity is the path, whatever the publisher or first hop. +A publisher that restarts its group numbering must publish under a new broadcast name. + +### Broadcast `finish` and `abort` are removed (#4152) + +Close a broadcast with `close()`; its end carries no cause. +`broadcast::Consumer::is_finished` is gone, and `closed()` on `Consumer` and `Dynamic` returns `()`. + +```rust +// Before +producer.finish(); +let err = consumer.closed().await; +// After +producer.close(); +consumer.closed().await; +``` + +### No `VarInt` + +Varints are plain `u64`s. + +```rust +// Before +let v = VarInt::decode_quic(buf)?.into_inner(); +VarInt::try_from(v)?.encode_quic(buf); +// After +let v = moq_net::varint::decode_quic(buf)?; +moq_net::varint::encode_quic(v, buf)?; // fails past varint::MAX_QUIC +``` + +## 0.2 -> 0.3 + +### Origins (#3400, #3804) + +`create_broadcast(path, route)` is `origin.publish(path, route)`, or `create_broadcast(path)` then `broadcast.announce(route)`. +`with_root` plus `scope(prefixes)` is one `scope(root, &patterns)` returning `Result`. +`origin::Info` is `origin::Config`. + +```rust +// Before +let broadcast = origin.create_broadcast(path, route); +let scoped = origin.with_root(root).scope(&prefixes); +// After +let broadcast = origin.publish(path, route)?; +let scoped = origin.scope(root, &patterns)?; +``` + +### Announcements are prefix routes (#3225, #3770) + +`announce::Update` is `{ prefix, route, kind, captures }`. +Skip inactive updates and resolve the broadcast through the origin. +Serve a subtree on demand with `origin.dynamic(prefix, route)`. + +```rust +if !update.kind.is_active() { + continue; +} +let broadcast = consumer.request_broadcast(&update.prefix).await?; +``` + +### Track renames (#3666) + +| Before | After | +| --- | --- | +| `with_latency_max` / `latency_max` | `with_max_age` / `max_age` | +| `write_datagram(Datagram)` | `insert_datagram(sequence, timestamp, payload)` | +| `track::SubscriberControl` | `track::Control` | +| `track::GroupRequest` | `group::Request` | +| `ConnectionStats` | `session::Stats`, with `estimated_send_rate` / `estimated_recv_rate` | + +### Oversized groups abort (#3585) + +A group past its size limit aborts with `GroupTooLarge` instead of shedding its head. diff --git a/rs/moq-rtc/MIGRATE.md b/rs/moq-rtc/MIGRATE.md index aa40e55f81..de53061b08 100644 --- a/rs/moq-rtc/MIGRATE.md +++ b/rs/moq-rtc/MIGRATE.md @@ -3,3 +3,20 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Sessions return their UDP wire source (#5632) + +`Client::publish` and `Client::subscribe` return `Result` instead of `Result<()>`. +Read per-peer byte counts with `source.stats()`, or discard it. + +```rust +// Before +client.subscribe(url, broadcast).await?; +// After +let _source = client.subscribe(url, broadcast).await?; +``` + +### `tick` is removed (#4915) + +`MediaSink::tick` and `Bridge::tick` are gone; a stalled rendition is marked by the catalog's `enabled` flag. +Delete the calls. diff --git a/rs/moq-rtmp/MIGRATE.md b/rs/moq-rtmp/MIGRATE.md index 93ad5c141a..b292743a7f 100644 --- a/rs/moq-rtmp/MIGRATE.md +++ b/rs/moq-rtmp/MIGRATE.md @@ -3,3 +3,14 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Export staleness is `max_delay` + +| Before | After | +| --- | --- | +| `Play::with_max_age` | `Play::with_max_delay` | +| `Client::with_export_max_age` | `Client::with_export_max_delay` | +| `listen::Config::export_max_age` | `listen::Config::export_max_delay` | +| `DEFAULT_MAX_AGE` | `DEFAULT_MAX_DELAY` | + +FLV egress now releases each frame on that delay after its decode time, instead of dropping frames older than it. diff --git a/rs/moq-sock/MIGRATE.md b/rs/moq-sock/MIGRATE.md index 5d99d6b523..46d88f690c 100644 --- a/rs/moq-sock/MIGRATE.md +++ b/rs/moq-sock/MIGRATE.md @@ -3,3 +3,21 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### A shard group binds in one call (#5688) + +`shard::Group`, `Member`, `Claim`, `cid`, and `CID_LEN` are gone. +`shard::bind(addr, count)` returns the complete `Bound` group; take sockets from it with `member()`. +Connection IDs are steered by `moq_quic::RandomConnectionIdGenerator::set_shard` instead of `shard::cid`. + +```rust +// Before +let mut group = shard::Group::acquire(addr, count)?; +let claims = (0..count).map(|_| group.member().unwrap().bind()).collect::>()?; +let mut bound = group.complete(claims)?; +// After +let mut bound = shard::bind(addr, count)?; +while let Some(socket) = bound.member()? { + // serve socket +} +``` diff --git a/rs/moq-srt/MIGRATE.md b/rs/moq-srt/MIGRATE.md index 323cad66b1..ad74ffb134 100644 --- a/rs/moq-srt/MIGRATE.md +++ b/rs/moq-srt/MIGRATE.md @@ -3,3 +3,8 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Egress runs at least one second behind (#5553) + +SRT egress delays the TS export by the SRT latency or 1 s, whichever is longer, and the send-ahead now comes out of that budget. +A feed whose video HRD buffer declares a longer delay stops with the delay it needs named. diff --git a/rs/moq-stats/MIGRATE.md b/rs/moq-stats/MIGRATE.md index 7e6549b2bf..a14972577b 100644 --- a/rs/moq-stats/MIGRATE.md +++ b/rs/moq-stats/MIGRATE.md @@ -3,3 +3,8 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### A returning group restarts under a new epoch (#4904) + +A stats group that returns announces a new epoch and starts its group numbers at zero, instead of continuing the old sequence. +A reader that keyed history on the path alone should end that history when the announcement ends and start fresh on the next one. diff --git a/rs/moq-tokio/MIGRATE.md b/rs/moq-tokio/MIGRATE.md index b67ad2fe11..44326c2b27 100644 --- a/rs/moq-tokio/MIGRATE.md +++ b/rs/moq-tokio/MIGRATE.md @@ -3,3 +3,177 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### The `noq` feature is `quic` (#5664) + +The QUIC stack is MoQ's own `moq-quic` instead of the `moq-noq` crates. +Rename the cargo feature, and the error types with it. +A qlog directory gets one file per endpoint instead of one per connection. + +```toml +# Before +moq-tokio = { version = "...", features = ["noq"] } +# After +moq-tokio = { version = "...", features = ["quic"] } +``` + +```rust +// Before +Err(moq_tokio::Error::Noq(err)) => {} // err: moq_tokio::noq::Error +// After +Err(moq_tokio::Error::Quic(err)) => {} // err: moq_tokio::quic::Error +``` + +### Workers are unpinned by default (#5688) + +`worker::Config::new(count)` no longer pins worker *i* to core *i*. +Call `with_pin(true)` to keep pinning. + +```rust +let workers = moq_tokio::worker::Config::new(8).with_pin(true); +``` + +### Accepted sessions return their driver task (#5562) + +`server::Request::ok()` returns `(session, driver)`. +Await the driver before classifying the session close, so a driver panic reports `Error::Panic` instead of the cancellation its teardown leaves behind. + +```rust +// Before +let session = request.ok().await?; +// After +let (session, driver) = request.ok().await?; +let result = driver.await?; +``` + +### Config durations are optional + +Wrap durations in `Some(...)` for connect `race`, `resolution_delay`, and `timeout`; listen `timeout`; QUIC `idle_timeout` and `keep_alive`; backoff `initial`, `max`, and `timeout`; GOAWAY `handover`; and WebSocket `delay`. +`None` uses the default, and `Some(Duration::ZERO)` keeps each knob's zero behavior. +A value set in code now wins over the parser default unless the flag or environment variable was actually given. +TOML keys are unchanged. + +```rust +// Before +config.backoff.timeout = Duration::from_secs(30); +// After +config.backoff.timeout = Some(Duration::from_secs(30)); +``` + +### Rotating TLS identities are a `tls::Serve` (#5312) + +`tls::Listen::server_config_reloading` and `tls::ReloadingServerConfig` are gone; load once with `tls::Listen::load` and read each handshake's config from the returned `tls::Serve`. +`Error::WorkerTlsGenerate` is gone because generated certificates now work on workers too. +A TLS trust-root watcher that fails to register now fails client and server construction. + +```rust +// Before +let config = listen.server_config_reloading(alpns)?; +// After +let serve = listen.load()?; +let config = serve.config(alpns)?; +``` + +### "Epoch" means only the publisher epoch (#5146) + +The reconnect counter is `connects()`. + +```rust +// Before +let n = connection.epoch(); +// After +let n = connection.connects(); +``` + +### A QUIC load-balancer nonce needs its server id (#5114) + +`listen.lb_nonce` without `lb_id` fails to start. +Setting `lb_id` (or `lb_nonce`) together with `load_balancer` also fails; `lb_id` used to win silently. +Set one of them. + +```toml +# Before +[listen] +lb_nonce = 8 +# After +[listen] +lb_id = "01" +lb_nonce = 8 +``` + +### Settings no listener reads stop startup (#5006) + +A stream-only listener (TCP or Unix, no QUIC bind) refuses the QUIC-only `preferred_v4` / `preferred_v6`, `lb_id`, pinned `tls.peers`, and a TLS cert, key, or `generate` unless `tcp.tls` serves it. +`unix.allow_*` needs `unix.bind`. +Each used to be ignored: drop it, or add the listener it configures. + +### A client CA needs a QUIC listener + +A stream-only listener with `listen.tls.root` returns `Error::MtlsUnsupported` instead of silently verifying nothing. + +### `Transport` names WebTransport + +`server::Transport` is `moq_tokio::Transport`, with no re-export. +A WebTransport session reports `Transport::WebTransport` (`"webtransport"` in logs); `Quic` now means raw QUIC only. +`Connection::transport()` reports the live transport. + +```rust +// Before +use moq_tokio::server::Transport; +// After +use moq_tokio::Transport; +match transport { + Transport::Quic | Transport::WebTransport => {} + _ => {} +} +``` + +### Custom sessions declare their request binding + +`transport::Session` wraps a session implementing moq-net's own `transport::poll` traits instead of `web_transport_trait`. +The adapter constructors take the session's `moq_net::transport::Binding` explicitly: `Uri` when the transport handshake carries the request URI, `InBand` when SETUP does. +`Client` and `Server` are unchanged. + +### `Redirect::resolve` is removed (#4854) + +`Redirect` and `Goaway` stay, but the lenient `resolve(uri, current)` is gone. +Follow a GOAWAY through the connection, which validates the target itself. + +### The `android-logcat` feature is removed (#4608) + +Android builds always log to logcat. +Drop the feature from your dependency. + +```toml +# Before +moq-tokio = { version = "...", features = ["android-logcat"] } +# After +moq-tokio = { version = "..." } +``` + +## moq-native 0.19 -> moq-tokio 0.19 + +### moq-native is moq-tokio (#2896) + +moq-native 0.20.0 is a stub that fails to compile, pointing at the rename. +Names sit under their modules: `connection::Goaway`, `connection::Monitor`, `transport::Session` (#3745). + +```rust +// Before +let client = moq_native::ClientConfig::default().init()?; +client.with_publisher(&origin).with_subscriber(origin); +// After +let client = moq_tokio::connect::Config::default().init(quic)?; +client.with_origin(origin); +``` + +### Origins + +`Origin::random().produce()` is `moq_tokio::origin::spawn()`. + +```rust +// Before +let origin = Origin::random().produce(); +// After +let origin = moq_tokio::origin::spawn(); +``` diff --git a/rs/moq-uring/MIGRATE.md b/rs/moq-uring/MIGRATE.md index d517976cdc..774deda59b 100644 --- a/rs/moq-uring/MIGRATE.md +++ b/rs/moq-uring/MIGRATE.md @@ -3,3 +3,42 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### The `noq` feature is `quic` (#5664) + +The QUIC stack is MoQ's own `moq-quic` instead of `moq-noq-proto`. +A qlog directory gets one file per endpoint and one per dial instead of one per connection. + +```toml +# Before +moq-uring = { version = "...", features = ["noq"] } +# After +moq-uring = { version = "...", features = ["quic"] } +``` + +### `CID_LEN` is gone (#5688) + +`quic::endpoint::CID_LEN` is removed; endpoints still issue 8-byte connection IDs, sharded or not. + +### The server config takes a rustls config (#5312) + +`quic::server::Config` drops `identity`, `alpn`, and `client_auth` for one `tls: Arc`, and `Config::new` takes it. +Build it from an identity with `Identity::server`, or share a resolver with other listeners. + +```rust +// Before +let mut config = quic::server::Config::new(identity); +config.alpn = alpns; +config.client_auth = client_auth; +// After +let config = quic::server::Config::new(identity.server(&alpns, client_auth)?); +``` + +### Sessions are wrapped for moq-net + +moq-net no longer takes a `web_transport_trait` session. +Wrap a moq-uring session with `transport::Session::new`, naming how the request target travels. + +```rust +let session = moq_uring::transport::Session::new(connection, moq_net::transport::Binding::Uri); +``` diff --git a/rs/moq-video/MIGRATE.md b/rs/moq-video/MIGRATE.md index 89b05444ee..c4fb399a94 100644 --- a/rs/moq-video/MIGRATE.md +++ b/rs/moq-video/MIGRATE.md @@ -3,3 +3,67 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Capture takes one `encode::Capture` and returns a `Control` + +`publish_capture(broadcast, catalog, capture, encode, clock)` is `publish_capture(broadcast, catalog, encode::Capture)`. +`Capture` has no clock: the driver stamps on `catalog.clock()`. +`encode::Control::new` returns the control handle and the driver; dropping the last `Control` cancels an in-flight open or encode. + +```rust +// Before +publish_capture(broadcast, catalog, capture, encode, clock).await?; +// After +let mut options = encode::Capture::default(); +options.capture = capture; +options.encode = encode; +publish_capture(broadcast, catalog, options).await?; +``` + +### `Control::cut` returns a `Result` + +`cut()` fails with `Error::CutUnsupported` once the driver knows its backend cannot force a keyframe, and the publish carries on at its cadence. +It counts cadence keyframes too, so a request may be served without forcing one. + +```rust +// Before +control.cut(); +// After +if let Err(Error::CutUnsupported) = control.cut() { + // wait for the next cadence keyframe +} +``` + +### `Encoded` marks keyframes (#4295) + +`encode::Encoded::new` takes a third `keyframe: bool`, also readable as `Encoded::keyframe`. + +```rust +// Before +let encoded = Encoded::new(payload, timestamp); +// After +let encoded = Encoded::new(payload, timestamp, keyframe); +``` + +### Producers drop `tick`, `idle`, and `observe_lag` (#4915) + +`encode::Producer` no longer takes liveness hints; disable a rendition through the catalog's `enabled` flag instead. + +### External Vulkan and DMA-BUF inputs (#4975) + +On Linux, `Surface::Vulkan` no longer requires the `nvidia` feature. +`vulkan::Slot::new` takes exported handles, an `Image`, and a producer guard, replacing the CUDA-owned producer API. +`vulkan::Importer`, `vulkan::ImportError`, `cuda::Converter`, and `cuda::Slot` leave the public API; `Surface::Cuda` stays for NVDEC output. +`DmaBuf::new` takes an owned FD, a `DmaBufLayout`, and a release guard, and an external DMA-BUF refuses CPU readback. +Bind eager open to an external device with `encode::Config::input`; an unsupported device is refused instead of falling back to software. + +### Decoder staleness is `max_delay` + +`decode::Options::max_age` is `max_delay`. + +```rust +// Before +options.max_age = delay; +// After +options.max_delay = delay; +``` From efed3045cf9a109f4582c468c1799a1bb6ab5806 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 16:36:01 -0700 Subject: [PATCH 04/40] docs: backfill MIGRATE.md for JS media, moq-ffi, and moq-c Co-Authored-By: Claude Opus 5.5 --- js/flate/MIGRATE.md | 35 +++++++++ js/hang/MIGRATE.md | 76 +++++++++++++++++++ js/json/MIGRATE.md | 38 ++++++++++ js/loc/MIGRATE.md | 13 ++++ js/msf/MIGRATE.md | 5 ++ js/publish/MIGRATE.md | 35 +++++++++ js/room/MIGRATE.md | 15 ++++ js/watch/MIGRATE.md | 52 +++++++++++++ rs/moq-c/MIGRATE.md | 118 +++++++++++++++++++++++++++++ rs/moq-ffi/MIGRATE.md | 170 ++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 557 insertions(+) diff --git a/js/flate/MIGRATE.md b/js/flate/MIGRATE.md index c8b5be25a6..f29788487a 100644 --- a/js/flate/MIGRATE.md +++ b/js/flate/MIGRATE.md @@ -3,3 +3,38 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### @moq/binary is @moq/flate + +The opaque `Snapshot` and `Stream` tracks moved beside the codec; the wire and the catalog's `binary` section are unchanged. + +```ts +// Before +import { Snapshot, Stream } from "@moq/binary"; +// After +import { Snapshot, Stream } from "@moq/flate"; +``` + +### Consumers return `Timed` values (#5099) + +The snapshot and stream consumers' `next()` and async iterator yield `{ value, at }`, where `at` is absent on an untimed track. +A snapshot consumer's `next()` yields every state in order; call `latest()` to skip to the newest. + +```ts +// Before +const bytes = await consumer.next(); +// After +const bytes = (await consumer.latest())?.value; +``` + +### Producers never invent a timestamp (#5070) + +`Snapshot.Producer.update` and `Stream.Producer.append` take a `Timed`. +Omit `at` only on an untimed track. + +```ts +// Before +producer.update(bytes); +// After +producer.update({ value: bytes, at: Time.Timestamp.now() }); +``` diff --git a/js/hang/MIGRATE.md b/js/hang/MIGRATE.md index 02ad94bcee..39b6cc9e29 100644 --- a/js/hang/MIGRATE.md +++ b/js/hang/MIGRATE.md @@ -3,3 +3,79 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Timeline records are optionally timed (#5720) + +`Timeline.Record.pts` and `.duration` are optional, absent on an untimed track. + +```ts +// Before +const pts: number = record.pts; +// After +if (record.pts === undefined) continue; +``` + +### Catalog ranking names its media kind + +`Catalog.ranked(renditions)` is `Catalog.Video.ranked(renditions)`. +`Catalog.Audio.ranked(renditions)` orders audio by enabled state, bitrate, sample rate, channel count, and name. +Both put disabled renditions last. + +```ts +// Before +const best = Catalog.ranked(catalog.video.renditions)[0]; +// After +const best = Catalog.Video.ranked(catalog.video.renditions)[0]; +``` + +### One `enabled` flag replaces `stalled` (#4915) + +`Catalog.Stalled` is gone, `VideoConfig.stalled` is `enabled` with the meaning inverted, and `AudioConfig` gains `enabled`. + +```ts +// Before +if (config.stalled) skip(); +// After +if (config.enabled === false) skip(); +``` + +### CMAF decodes at the frame timestamp (#4826) + +A fragment's earliest sample presents at its moq-net frame timestamp; `tfdt` only orders the samples. +`Container.Format.decode` and `Cmaf.decodeDataSegment` take that timestamp, or `undefined` for an untimed frame, whose samples present at `tfdt`. +`Cmaf.decodeTimestamp` is gone. + +```ts +// Before +const frames = format.decode(frame.payload); +// After +const frames = format.decode(frame.payload, frame.timestamp); +``` + +### Container consumers take `maxDelay` + +`Container.Consumer`'s `maxAge` prop is `maxDelay`, and passing `maxAge` throws a `TypeError`. + +```ts +// Before +new Container.Consumer(track, { maxAge }); +// After +new Container.Consumer(track, { maxDelay }); +``` + +### `cut` is `discontinuity` (#4141) + +`Container.Legacy.Producer.cut(end?)` is `discontinuity(end?)`. + +```ts +// Before +producer.cut(end); +// After +producer.discontinuity(end); +``` + +## 0.4 -> 0.5 + +### The catalog's `timeline` entry is `archive` + +@moq/hang reads the root catalog's `archive` entry instead of `timeline`, and wall time from the root `clock`. diff --git a/js/json/MIGRATE.md b/js/json/MIGRATE.md index 816dba679e..07455bead8 100644 --- a/js/json/MIGRATE.md +++ b/js/json/MIGRATE.md @@ -3,3 +3,41 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Consumers return `Timed` values (#5099) + +The snapshot and stream consumers' `next()` and async iterator yield `{ value, at }`, where `at` is the frame's timestamp and absent on an untimed track. +A snapshot consumer's `next()` yields every state in order; call `latest()` to skip to the newest, as `next()` used to. +A caller that passes the result on as `any` or `unknown` still compiles, so search for every `next()` and `for await` rather than relying on type errors. + +```ts +// Before +const value = await consumer.next(); +// After +const value = (await consumer.latest())?.value; +``` + +### Producers never invent a timestamp (#5070) + +`Snapshot.Producer.update`, `Stream.Producer.append`, and `Window.Producer.push` take a `Timed`, and `Window.Producer.pop` and `Snapshot.Producer.mutate` take an optional `at`. +Omit `at` only on an untimed track. + +```ts +// Before +producer.update(value); +// After +producer.update({ value, at: Time.Timestamp.now() }); +``` + +## 0.3 -> 0.4 + +### One options object (#3640) + +Constructors take one options object. + +```ts +// Before +new Json.Snapshot.Consumer(track, config); +// After +new Json.Snapshot.Consumer({ track, ...config }); +``` diff --git a/js/loc/MIGRATE.md b/js/loc/MIGRATE.md index edf53f4aaa..c6d742e983 100644 --- a/js/loc/MIGRATE.md +++ b/js/loc/MIGRATE.md @@ -3,3 +3,16 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Timestamps are `@moq/clock` units (#5706) + +Frame timestamps are typed as `Micro` from `@moq/clock` instead of `Time.Micro` from `@moq/net`. + +```ts +// Before +import { Time } from "@moq/net"; +const ts: Time.Micro = frame.timestamp; +// After +import type { Micro } from "@moq/clock"; +const ts: Micro = frame.timestamp; +``` diff --git a/js/msf/MIGRATE.md b/js/msf/MIGRATE.md index 59da93d337..8d2cbe631b 100644 --- a/js/msf/MIGRATE.md +++ b/js/msf/MIGRATE.md @@ -3,3 +3,8 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### `Track.stalled` is removed (#4915) + +The non-standard `stalled` track field is gone and no longer read or written. +Drop it from any track you build. diff --git a/js/publish/MIGRATE.md b/js/publish/MIGRATE.md index 355ef22fcf..5ce5c32b35 100644 --- a/js/publish/MIGRATE.md +++ b/js/publish/MIGRATE.md @@ -3,3 +3,38 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### A disabled rendition stays in the catalog (#4915) + +`enabled: false` keeps the rendition in the catalog with `enabled: false` instead of removing it, and viewers skip it. +Check any code that read a missing rendition as disabled. + +### Tracks persist for the broadcast's life (#5068) + +`Rendition.close()` no longer closes the network track. +A `maxAge` change, or replacing a closed rendition track, recreates `broadcast.net`, and registering a name under a different kind on the same network broadcast throws. + +### `OpusConfig.usedtx` is removed + +Opus DTX is always off, since Chromium's DTX output shifts the audio timeline. +Remove the field; a plain-JS caller still passing it is ignored. + +```ts +// Before +{ codec: "opus", usedtx: true } +// After +{ codec: "opus" } +``` + +## 0.4 -> 0.5 + +### Publish takes an origin + +Pass the connection's origin instead of a `connection` signal. + +```ts +// Before +new Publish.Broadcast({ connection }); +// After +new Publish.Broadcast({ origin: connection.origin }); +``` diff --git a/js/room/MIGRATE.md b/js/room/MIGRATE.md index ec9a3825b0..73d2f70625 100644 --- a/js/room/MIGRATE.md +++ b/js/room/MIGRATE.md @@ -3,3 +3,18 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### `HISTORY` is a `Nano` (#5706) + +`HISTORY` is a `Nano` from `@moq/clock` instead of milliseconds. + +```ts +// Before +const ms = HISTORY; +// After +const ms = Milli.fromNano(HISTORY); +``` + +### Chat tracks are timed (#4968) + +`Chat.info()` includes a `timescale`, so a chat track you create yourself must declare one before writing timestamps. diff --git a/js/watch/MIGRATE.md b/js/watch/MIGRATE.md index 71917d65d2..6e4f7e4770 100644 --- a/js/watch/MIGRATE.md +++ b/js/watch/MIGRATE.md @@ -3,3 +3,55 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### `"auto"` delay is measured (#4162) + +`"auto"` is sized from how late frames arrive instead of derived from RTT. +A numeric `delay` is taken literally instead of having the rendition's own delay added on top. +`Sync.out.jitter` always equals `Sync.out.delay`, and `"auto"` with no decoder registered resolves to 0 instead of 100 ms. + +### Staleness is `maxDelay` + +`Sync.out.maxAge` is `Sync.out.maxDelay`. + +```ts +// Before +const budget = sync.out.maxAge.peek(); +// After +const budget = sync.out.maxDelay.peek(); +``` + +### Disabled renditions are never selected (#4915) + +A rendition the catalog marks `enabled: false` is skipped, even when named. +With no video rendition selectable, the picture clears to black. + +### `announced` off stays on one publisher + +With `announced` off, the player no longer moves to a later publisher at the same path. +Turn `announced` on to follow restarts. + +## 0.5 -> 0.6 + +### Delay attributes need a unit (#3396, #3817) + +`latency`, `latency-min`, `latency-max`, and `jitter` are `delay` and `buffer`, each with a unit. +`reload` is `announced`. + +```html + + + + +``` + +### `Watch.Broadcast` is `Watch.Player` + +The player takes the connection's origin. + +```ts +// Before +new Watch.Broadcast({ connection, name }); +// After +new Watch.Player({ origin: connection.origin, name }); +``` diff --git a/rs/moq-c/MIGRATE.md b/rs/moq-c/MIGRATE.md index cd31ce05bf..be7628da87 100644 --- a/rs/moq-c/MIGRATE.md +++ b/rs/moq-c/MIGRATE.md @@ -3,3 +3,121 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### libmoq is moq-c (#4288) + +The C bindings ship as the `moq-c` crate, starting at 0.7.0. +The header is still `moq.h` and the library is still `libmoq.a` / `moq.lib` (`-lmoq`); the C ABI did not change with the rename. +The CMake package, pkg-config name, release tags, and archives follow the new name. + +| Before | After | +| --- | --- | +| `find_package(moq)`, target `moq` | `find_package(moq-c)`, target `moq::c` | +| `pkg-config --libs moq` | `pkg-config --libs moq-c` | +| nix `.#libmoq` | nix `.#moq-c` | +| tags `libmoq-v*`, archives `libmoq--` | tags `moq-c-v*`, archives `moq-c--` | + +```cmake +# Before +find_package(moq REQUIRED) +target_link_libraries(app PRIVATE moq) +# After +find_package(moq-c REQUIRED) +target_link_libraries(app PRIVATE moq::c) +``` + +### Background panics are reported (#5562) + +A panic in a background task reports `MOQ_ERROR_PANIC`, and other handles keep running. + +### Announcements carry a kind (#4266, #4384, #5087) + +`moq_announce_update` is `moq_announce_event`, and its `active` flag is a `kind`: `MOQ_ANNOUNCE_KIND_START`, `_UPDATE`, `_END`, or `_RESTART`. +`_RESTART` means another publisher instance now serves the prefix: request the path again. + +```c +// Before +moq_announce_update info; +moq_origin_announced_info(id, &info); +if (info.active) subscribe(info.prefix, info.prefix_len); +// After +moq_announce_event info; +moq_origin_announced_info(id, &info); +if (info.kind == MOQ_ANNOUNCE_KIND_START || info.kind == MOQ_ANNOUNCE_KIND_RESTART) subscribe(info.prefix, info.prefix_len); +``` + +### `moq_origin_announced` takes a config (#4111) + +The prefix and filter arguments moved into a `moq_announce_config`; pass `NULL` to receive everything. + +```c +// Before +moq_origin_announced(origin, prefix, prefix_len, NULL, 0, on_announce, user_data); +// After +moq_announce_config config = { .prefix = prefix, .prefix_len = prefix_len }; +moq_origin_announced(origin, &config, on_announce, user_data); +``` + +### Frames say whether they are timed + +`moq_frame` and `moq_datagram` gain `timestamp_present`, which changes their size, so recompile against the new `moq.h`. +A frame read from an untimed track has `timestamp_present` false. + +```c +if (frame.timestamp_present) use(frame.timestamp_us); +``` + +### Subscriber staleness is `max_delay_us` + +`moq_subscription`, `moq_audio_decoder_output`, `moq_video_decoder_output`, `moq_consume_video`, and `moq_consume_audio` take `max_delay_us` instead of `max_age_us`. + +### `stalled` is `enabled` (#4915) + +`moq_consume_video_stalled` is `moq_consume_video_enabled`, with the meaning inverted. + +```c +// Before +bool stalled; +moq_consume_video_stalled(catalog, index, &stalled); +// After +bool enabled; +moq_consume_video_enabled(catalog, index, &enabled); +``` + +### Routes have one cost (#4661) + +`moq_route.cold` and `has_cold` are removed. + +### `moq_publish_finish` is removed (#4152) + +Close the broadcast with `moq_publish_close`. + +```c +// Before +moq_publish_finish(broadcast); +// After +moq_publish_close(broadcast); +``` + +### A decoded frame can fail on its own (#4094) + +`moq_decode_video_frame` can return a conversion error for a single frame; it used to end the stream at the terminal callback. + +## libmoq 0.5 -> 0.6 + +### One client config + +The 41 `moq_client_*` setters are one zero-initializable `moq_client_config`, whose durations are `_us`. + +### Publish split by kind + +`moq_publish_media` is `moq_publish_audio`, `moq_publish_video`, or `moq_publish_container`, and the raw encoders are `moq_encode_audio*` / `moq_encode_video*`. +Formats are enums instead of strings. + +### Announcement renames + +| Before | After | +| --- | --- | +| `moq_announced` | `moq_announce_update` | +| `moq_origin_consume_announced` | `moq_origin_announced_broadcast` | +| `moq_broadcast_request_abort` | `moq_broadcast_request_reject` | diff --git a/rs/moq-ffi/MIGRATE.md b/rs/moq-ffi/MIGRATE.md index 176107a91f..1d2bd744fc 100644 --- a/rs/moq-ffi/MIGRATE.md +++ b/rs/moq-ffi/MIGRATE.md @@ -1,5 +1,175 @@ # Migrating moq-ffi How to update your code for each breaking change, newest first. +These are the UniFFI names; each language wrapper has its own `MIGRATE.md` in its own casing. ## Unreleased + +### Background panics are reported (#5562) + +A panic in a binding-owned background task reports `MoqError::Task { reason }` with the panic message, and other objects keep running. +Handle the new case where you match on errors. + +### Announcements are flat events with a restart (#4266, #4384, #5087) + +`MoqAnnounceConsumer::next` returns `Option`: `Start`, `Update`, `End`, or `Restart`, each carrying a `MoqAnnounce` record (`prefix`, `captures`, `route`). +The `MoqAnnounceUpdate` object and its `active()` are gone. +`Restart` means another publisher instance now serves the prefix: request the path again. + +```rust +// Before +let update = announced.next().await?; +if update.active() { subscribe(update.prefix()) } +// After +match announced.next().await? { + Some(MoqAnnounceEvent::Start { announce }) | Some(MoqAnnounceEvent::Restart { announce }) => subscribe(announce.prefix), + _ => {} +} +``` + +### "Epoch" means only the publisher epoch (#5146) + +`MoqSession::epoch()` is `connects()`, and `MoqAudioProducer::reset_epoch()` is `reanchor()`. + +### Clients and servers take a config record (#4519) + +`MoqClient::new()` and `MoqServer::new()` plus their setters are `MoqClient::new(MoqClientConfig)` and `MoqServer::new(MoqServerConfig)`, with nested `tls`, `quic`, `websocket`, and `backoff` records and a `versions` list. +A value the native side cannot use fails construction with `MoqError::Config`, including a bad bind address that used to fail later as `Bind`. +The `subscribe` origin is `consume`. +A server request's `set_publish` / `set_consume` are arguments to `accept(publish, consume)`, where null inherits the server's origin. + +```rust +// Before +let client = MoqClient::new(); +client.set_tls_verify(false)?; +client.set_subscribe(origin)?; +// After +let client = MoqClient::new(MoqClientConfig { + tls: MoqClientTls { insecure: true, ..Default::default() }, + consume: Some(origin), + ..Default::default() +})?; +``` + +### Data tracks wrap a track (#4519) + +The broadcast's `publish_binary_*`, `publish_json_*`, and `subscribe_json_*` are gone. +Create the track with `publish_track` or `subscribe_track`, then wrap it. + +```rust +// Before +let producer = broadcast.publish_json_snapshot("state".into(), config)?; +// After +let track = broadcast.publish_track("state".into(), None)?; +let producer = MoqJsonSnapshotProducer::new(&broadcast, &track, config)?; +``` + +Likewise `MoqJsonStreamProducer`, `MoqFlateSnapshotProducer` / `MoqFlateStreamProducer` (taking `MoqFlateConfig`), and `MoqJsonSnapshotConsumer` / `MoqJsonStreamConsumer` built from a `MoqTrackConsumer`. + +### Media lives in a `media` namespace (#4519) + +The broadcast's `publish_audio`, `publish_video`, `publish_container`, their `_on_track` and `_stream` variants, `set_video_properties`, `set_catalog_section`, `subscribe_catalog`, `subscribe_media`, and `fetch_media_group` are gone. +Construct `MoqMediaTrackProducer::audio` / `video` from the broadcast, a `MoqMediaTarget` (`Named` or `Requested`), and the init record; or `MoqMediaContainerProducer`, `MoqMediaCatalogProducer`, `MoqMediaCatalogConsumer::subscribe`, `MoqMediaTrackConsumer::subscribe`, or `MoqMediaGroupConsumer::fetch`. +Producers drop `name`, `used`, and `unused`; read them through `demand()`. + +```rust +// Before +let audio = broadcast.publish_audio(init)?; +// After +let audio = MoqMediaTrackProducer::audio(&broadcast, MoqMediaTarget::Named { name: None }, init)?; +``` + +### Container byte-stream importers take an init record + +`MoqMediaContainerStreamProducer` takes a `MoqContainerStreamInit` holding the format instead of a bare format. +Its optional `timebase`, also on `MoqContainerInit`, shares one source's offset across container imports; leave it unset for independent offsets. + +### Error payloads are named + +`MoqError` cases that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. +The Display text is unchanged. + +### Timestamps are optional + +`MoqFrame.timestamp_us` and `MoqDatagram.timestamp_us` are optional, null on a frame read from an untimed track, and default to null. +`MoqTrackInfo.timescale` is null on a received untimed track, and a null timescale on publish makes the track untimed. +A raw track you publish is timed by default, so a raw write without a timestamp fails with `TimestampMismatch` instead of going out at 0. +Data operations take an optional timestamp in microseconds, and whole-frame media writes require one. +`MoqMediaCatalogProducer::now()` samples the broadcast clock for local-event stamps. + +```rust +// Before +track.write_frame(MoqFrame { payload, timestamp_us: 0 })?; +// After +track.write_frame(MoqFrame { payload, timestamp_us: Some(now_us) })?; +``` + +### Subscriber staleness is `max_delay_us` + +`MoqSubscription`, `MoqAudioDecoderOutput`, and `MoqVideoDecoderOutput` take `max_delay_us` instead of `max_age_us`. +`MoqTrackInfo.max_age_us` keeps its name: it is the publisher's retention. + +### `MoqTransport` gains `WebTransport` + +A WebTransport session reports `MoqTransport::WebTransport`; `Quic` now means raw QUIC only. +An exhaustive match needs one more arm. + +### `stalled` is `enabled` (#4915) + +`MoqVideo.stalled` is `enabled`, defaulting to true with the meaning inverted, and `MoqAudio` gains `enabled`. + +### Routes have one cost (#4661) + +`MoqRoute.cold` is removed. + +### Decoded video frames own their surface + +`MoqVideoConsumer::next` returns an `Arc` object with `timestamp_us()`, `width()`, `height()`, `pixels(format)`, and `surface()`. +`MoqVideoDecoderOutput.format` is gone; set `surface: true` to keep the decoder's native surface, readable as a `MoqVideoSurface`. + +```rust +// Before +let rgba = frame.data; +// After +let rgba = frame.pixels(MoqVideoPixelFormat::Rgba)?; +``` + +### Broadcast `finish` is removed (#4152) + +`MoqBroadcastProducer::finish()` is gone; close the broadcast instead. + +### The `android-logcat` feature is removed (#4608) + +Android builds always log to logcat; drop the feature. + +## 0.3 -> 0.4 + +### Publish split by kind + +`publish_media*` is `publish_audio`, `publish_video`, or `publish_container`, each taking its own init. +The raw encoder paths that used to be `publish_audio` / `publish_video` are `encode_audio` / `encode_video`. + +### Renames + +| Before | After | +| --- | --- | +| durations in milliseconds | microseconds (`max_age_us`, `MoqBackoff.initial_us`) | +| `estimated_send_bitrate` / `estimated_recv_bitrate` | `estimated_send_rate` / `estimated_recv_rate` | +| `set_tls_disable_verify(bool)` | `set_tls_verify(bool)` | +| `MoqAnnounced` | `MoqAnnounceConsumer` | +| `MoqAnnouncement` | `MoqAnnounceUpdate` | +| `MoqBroadcastRequest::abort` | `reject` | +| `MoqOriginOptions` | `MoqOriginConfig` | +| `MoqAudioCodec` enum | an `opus()` object (#3671) | + +### Setters are fallible (#3642) + +Client and server setters return an error (`Busy` during connect or listen) instead of dropping the value. + +### Protocol errors are structured + +`MoqError::Protocol` carries a `MoqProtocolError` (scope, wire code, kind) instead of a flattened message. + +### Track and group `finish()` keep the handle + +`finish()` keeps the handle open, so a later `abort()` can still run. From 28bb534efb02b3e4b4174b550ea478320f58077f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 16:39:25 -0700 Subject: [PATCH 05/40] docs: backfill MIGRATE.md for the Python and Go wrappers Co-Authored-By: Claude Opus 5.5 --- go/wrapper/MIGRATE.md | 174 ++++++++++++++++++++++++++++++++++++++++++ py/moq-rs/MIGRATE.md | 160 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 334 insertions(+) diff --git a/go/wrapper/MIGRATE.md b/go/wrapper/MIGRATE.md index 2868a0985b..f081ff3fb9 100644 --- a/go/wrapper/MIGRATE.md +++ b/go/wrapper/MIGRATE.md @@ -3,3 +3,177 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Background panics are reported (#5562) + +A panic in a background task returns an `ErrTask` error carrying the panic message, and other objects keep running. + +### Announcements are events with a restart + +`AnnounceConsumer.Next` and `All` yield an `AnnounceEvent`: `AnnounceEventStart`, `AnnounceEventUpdate`, `AnnounceEventEnd`, or `AnnounceEventRestart`, each carrying an `Announce`. +`AnnounceUpdate` and its `Active()` are gone. +A restart means another publisher instance now serves the prefix: request the path again. + +```go +// Before +for update, err := range announced.All(ctx) { + if err == nil && update.Active() { + subscribe(update.Prefix()) + } +} +// After +for event, err := range announced.All(ctx) { + switch e := event.(type) { + case moq.AnnounceEventStart: + subscribe(e.Announce.Prefix) + case moq.AnnounceEventRestart: + subscribe(e.Announce.Prefix) + } +} +``` + +### "Epoch" means only the publisher epoch (#5146) + +`Session.Epoch()` is `Connects()`, and `AudioProducer.ResetEpoch()` is `Reanchor()`. + +```go +// Before +n := session.Epoch() +audio.ResetEpoch() +// After +n := session.Connects() +audio.Reanchor() +``` + +### Origins are `consume`, and servers accept with them + +`WithSubscribeOrigin` is `WithConsumeOrigin`. +A server request's `SetPublish` / `SetConsume` are arguments to `Accept(ctx, publish, consume)`, where `nil` inherits the server's origin. +`Server.Requests(ctx)` is `All(ctx)`, and the `Status*` constants are `ConnectionStatus*`. +A value the native side cannot use fails `Dial` with a config error, including a bad bind address. + +```go +// Before +client, err := moq.Dial(ctx, url, moq.WithSubscribeOrigin(origin)) +for request, err := range server.Requests(ctx) { + session, err := request.Accept(ctx) +} +// After +client, err := moq.Dial(ctx, url, moq.WithConsumeOrigin(origin)) +for request, err := range server.All(ctx) { + session, err := request.Accept(ctx, nil, nil) +} +``` + +### Media lives in `moq.dev/moq/media` + +`BroadcastProducer.PublishAudio`, `PublishVideo`, `PublishContainer`, their `OnTrack` and `Stream` variants, `SetVideoProperties`, and `SetCatalogSection`, and the consumer's catalog and media subscriptions, are gone. +Use `media.NewAudioTrackProducer` / `NewVideoTrackProducer`, `NewContainerProducer`, `NewCatalogProducer`, `NewCatalogConsumer`, `NewTrackConsumer`, and `media.FetchGroup`; `media.NewGroupConsumer` decodes a raw group. +Producers drop `Name`, `Used`, and `Unused`; read them through `Demand()`. +JSON and opaque data tracks live in `moq.dev/moq/json` and `moq.dev/moq/flate`, wrapping a track from `PublishTrack`. + +```go +// Before +audio, err := broadcast.PublishAudio(moq.AudioFormatOpus, opusHead) +// After +audio, err := media.NewAudioTrackProducer(broadcast, media.Named{}, media.AudioInit{Format: media.AudioFormatOpus, Data: opusHead}) +``` + +### Container byte-stream importers take an init record + +`media.NewContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. +Its optional `Timebase`, also on `ContainerInit`, shares one source's offset across container imports. + +### Durations are `time.Duration` + +`Frame`, `Datagram`, `Subscription`, `TrackInfo`, and `ConnectionStats` carry `time.Duration` instead of microsecond integers: `TimestampUs` is `Timestamp`, and `RttUs` is `RTT`. +A frame timestamp is a `*time.Duration`, `nil` on a frame read from an untimed track. + +```go +// Before +err := track.WriteFrame(moq.Frame{Payload: payload, TimestampUs: 1000}) +// After +ts := time.Millisecond +err := track.WriteFrame(moq.Frame{Payload: payload, Timestamp: &ts}) +``` + +### Timestamps are optional + +A raw write to a timed track without a timestamp fails with `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. +`media.CatalogProducer.Now()` samples the broadcast clock for local-event stamps. + +### Subscriber staleness is `MaxDelay` + +`Subscription.MaxAgeUs` is `MaxDelay`, and the decoder outputs take `MaxDelayUs`. +`TrackInfo.MaxAge` keeps its name: it is the publisher's retention. + +### Error payloads are named `Reason` + +Errors that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `Reason`. + +### `Transport` gains `WebTransport` + +A WebTransport session reports `TransportWebTransport`; `TransportQuic` now means raw QUIC only. + +### `Stalled` is `Enabled` (#4915) + +`media.Video.Stalled` is `Enabled`, defaulting to true with the meaning inverted, and `media.Audio` gains `Enabled`. + +### Routes have one cost (#4661) + +`Route.Cold` is removed. + +### Decoded video frames are objects + +`VideoDecodedFrame` is a handle with `TimestampUs()`, `Width()`, `Height()`, `Pixels(format)`, `Surface()`, and `Close()`. +`VideoDecoderOutput.Format` is gone; set `Surface: true` to keep the decoder's native surface. + +```go +// Before +rgba := frame.Data +// After +rgba, err := frame.Pixels(moq.VideoPixelFormatRgba) +frame.Close() +``` + +### `BroadcastProducer.Finish()` is removed (#4152) + +```go +// Before +broadcast.Finish() +// After +broadcast.Close() +``` + +## 0.5 -> 0.7 + +### The module path is `moq.dev/moq` + +```go +// Before +import "github.com/moq-dev/moq-go/moq" +// After +import "moq.dev/moq" +``` + +### Publish split by kind + +`PublishMedia*` is `PublishAudio`, `PublishVideo`, or `PublishContainer`, each taking its own init. +The raw encoder paths that used to be `PublishAudio` / `PublishVideo` are `EncodeAudio` / `EncodeVideo`. + +### Renames + +| Before | After | +| --- | --- | +| millisecond durations | microseconds (`MaxAgeUs`, `Backoff.InitialUs`) | +| `EstimatedSendBitrate` / `EstimatedRecvBitrate` | `EstimatedSendRate` / `EstimatedRecvRate` | +| `WithTLSDisableVerify(true)` | `WithTLSVerify(false)` | +| `Announced` / `Announcement` | `AnnounceConsumer` / `AnnounceUpdate` | +| `BroadcastRequest.Abort` | `Reject` | +| `OriginOptions` | `OriginConfig` | + +### Errors are structured + +Protocol errors carry a `ProtocolError` (scope, wire code, kind) instead of a flattened message. +Setters return an error (`Busy` during connect or listen) instead of dropping the value. +Track and group `Finish()` keep the handle open, so a later `Abort()` can still run. diff --git a/py/moq-rs/MIGRATE.md b/py/moq-rs/MIGRATE.md index 16256db613..1c8ec8d268 100644 --- a/py/moq-rs/MIGRATE.md +++ b/py/moq-rs/MIGRATE.md @@ -3,3 +3,163 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Background panics are reported (#5562) + +A panic in a background task raises `moq.Error.Task` with the panic message as `reason`, and other objects keep running. + +### Announcements are events with a restart + +`AnnounceConsumer` yields an `AnnounceEvent`: `AnnounceEventStart`, `AnnounceEventUpdate`, `AnnounceEventEnd`, or `AnnounceEventRestart`, each carrying an `Announce` as `announce`. +`AnnounceUpdate` is gone. +A restart means another publisher instance now serves the prefix: request the path again. + +```python +# Before +async for update in client.announced(): + if update.active: + subscribe(update.prefix) +# After +async for event in client.announced(): + if isinstance(event, (moq.AnnounceEventStart, moq.AnnounceEventRestart)): + subscribe(event.announce.prefix) +``` + +### "Epoch" means only the publisher epoch (#5146) + +`Session.epoch()` is `connects()`, and `AudioProducer.reset_epoch()` is `reanchor()`. + +```python +# Before +n = session.epoch() +audio.reset_epoch() +# After +n = session.connects() +audio.reanchor() +``` + +### The consume origin is `consume` + +`Client(..., subscribe=origin)` is `Client(..., consume=origin)`. +A server request takes its origins in `accept(publish=..., consume=...)`, where `None` inherits the server's origin. +A value the native side cannot use raises `moq.Error.Config` at construction, including a bad bind address. + +```python +# Before +client = moq.Client(url, publish=origin, subscribe=origin) +# After +client = moq.Client(url, publish=origin, consume=origin) +``` + +### JSON data tracks wrap a track + +`BroadcastProducer.publish_json_snapshot` / `publish_json_stream` and the matching consumers are gone. +Create the track, then wrap it with `moq.json` (or `moq.flate` for opaque bytes). + +```python +# Before +state = broadcast.publish_json_snapshot("state") +# After +track = broadcast.publish_track("state") +state = moq.json.SnapshotProducer(broadcast, track) +``` + +### Media lives in `moq.media` + +`BroadcastProducer.publish_audio`, `publish_video`, `publish_container`, their `_on_track` and `_stream` variants, `set_video_properties`, and `set_catalog_section`, and the consumer's catalog and media subscriptions, are gone. +Use `moq.media.TrackProducer.audio` / `video`, `ContainerProducer`, `CatalogProducer`, `CatalogConsumer`, `TrackConsumer`, and `GroupConsumer`. +Producers drop `name`, `used`, and `unused`; read them through `demand()`. + +```python +# Before +audio = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head) +# After +audio = moq.media.TrackProducer.audio(broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head)) +``` + +### Container byte-stream importers take an init record + +`moq.media.ContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. +Its optional `timebase`, also on `ContainerInit`, shares one source's offset across container imports. + +### Durations are `timedelta` + +`Frame`, `Datagram`, `Subscription`, `TrackInfo`, and `ConnectionStats` carry `timedelta` instead of microsecond integers: `timestamp_us` is `timestamp`, and `rtt_us` is `rtt`. +A string passed where a list of strings belongs (`tls_roots="ca.pem"`) raises `TypeError` instead of splitting into characters. + +```python +# Before +track.write_frame(payload, timestamp_us=1_000) +# After +track.write_frame(payload, timestamp=timedelta(milliseconds=1)) +``` + +### Timestamps are optional + +A frame read from an untimed track has `timestamp` set to `None`. +Raw writes no longer default to zero, so a write to a timed track without a timestamp raises `TimestampMismatch`; whole-frame media writes require one. +`moq.media.CatalogProducer.now()` samples the broadcast clock for local-event stamps. + +### Subscriber staleness is `max_delay` + +`Subscription.max_age_us` is `max_delay`, a `timedelta`, and the decoder outputs take `max_delay_us`. +`TrackInfo` keeps its `max_age`: it is the publisher's retention. + +### Error payloads are named `reason` + +Errors that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. + +### `stalled` is `enabled` (#4915) + +`moq.media.Video.stalled` is `enabled`, defaulting to `True` with the meaning inverted, and `moq.media.Audio` gains `enabled`. + +### Routes have one cost (#4661) + +`Route.cold` is removed. + +### Decoded video frames are objects + +A `VideoDecodedFrame`'s fields are methods: `timestamp_us()`, `width()`, `height()`, and `pixels(format)` replacing `data` and `format`. +`VideoDecoderOutput.format` is gone; set `surface=True` to keep the decoder's native surface. + +```python +# Before +rgba = frame.data +# After +rgba = frame.pixels(moq.VideoPixelFormat.RGBA) +``` + +### `BroadcastProducer.finish()` is removed (#4152) + +Close the broadcast instead. + +```python +# Before +broadcast.finish() +# After +broadcast.close() +``` + +## 0.4 -> 0.5 + +### Publish split by kind + +`publish_media*` is `publish_audio`, `publish_video`, or `publish_container`, each taking its own init. +The raw encoder paths that used to be `publish_audio` / `publish_video` are `encode_audio` / `encode_video`. + +### Renames + +| Before | After | +| --- | --- | +| millisecond durations | microseconds (`max_age_us`, `Backoff.initial_us`) | +| `estimated_send_bitrate` / `estimated_recv_bitrate` | `estimated_send_rate` / `estimated_recv_rate` | +| `tls_disable_verify=True` | `tls_verify=False` | +| `Announced` / `Announcement` | `AnnounceConsumer` / `AnnounceUpdate` | +| `BroadcastRequest.abort` | `reject` | +| `OriginOptions` | `OriginConfig` | + +### Errors are structured + +`Error.Protocol` carries a `ProtocolError` (scope, wire code, kind) instead of a flattened message. +Setters raise (`Busy` during connect or listen) instead of dropping the value. +Track and group `finish()` keep the handle open, so a later `abort()` can still run. From 974f8b0b609e716363fed78cc5eb96438dfb586f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 16:42:51 -0700 Subject: [PATCH 06/40] docs: backfill MIGRATE.md for the Kotlin, Swift, and Dart wrappers Co-Authored-By: Claude Opus 5.5 --- dart/moq/MIGRATE.md | 106 ++++++++++++++++++++++++++++++++ kt/moq/MIGRATE.md | 146 ++++++++++++++++++++++++++++++++++++++++++++ swift/MIGRATE.md | 139 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 391 insertions(+) diff --git a/dart/moq/MIGRATE.md b/dart/moq/MIGRATE.md index d95169c422..9068958d6c 100644 --- a/dart/moq/MIGRATE.md +++ b/dart/moq/MIGRATE.md @@ -3,3 +3,109 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Background panics are reported (#5562) + +A panic in a background task throws `MoqException.Task` with the panic message as `reason`, and other objects keep running. + +### Announcements are events with a restart + +`Moq.announcements()` is `announced(...).updates()`, a `Stream`: `AnnounceEventStart`, `AnnounceEventUpdate`, `AnnounceEventEnd`, or `AnnounceEventRestart`, each carrying an `Announce`. +`AnnounceUpdate` is gone. +A restart means another publisher instance now serves the prefix: request the path again. + +```dart +// Before +moq.announcements().listen((update) { + if (update.active()) subscribe(update.prefix()); +}); +// After +moq.announced().updates().listen((event) { + switch (event) { + case AnnounceEventStart(:final announce) || + AnnounceEventRestart(:final announce): + subscribe(announce.prefix); + default: + } +}); +``` + +### "Epoch" means only the publisher epoch (#5146) + +`Moq.epoch` is `connects`. + +### The consume origin is `consume` + +`ConnectOptions.subscribe` is `consume`. +A value the native side cannot use fails `Moq.connect` with `MoqException.Config`, including a bad bind address. +A server request takes its origins in `accept(publish: ..., consume: ...)`, where null inherits the server's origin. + +### Data tracks wrap a track + +`BroadcastProducer.publishJsonSnapshot` / `publishJsonStream`, the binary variants, and the matching consumers are gone. +Create the track, then wrap it with `package:moq/json.dart` (or `package:moq/flate.dart` for opaque bytes). + +```dart +// Before +final state = broadcast.publishJsonSnapshot(name: 'state', config: JsonSnapshotConfig()); +// After +final state = moq_json.SnapshotProducer( + broadcast: broadcast, + track: broadcast.publishTrack(name: 'state', info: null), + config: moq_json.SnapshotConfig(), +); +``` + +### Media lives in `package:moq/media.dart` + +`BroadcastProducer.publishAudio`, `publishVideo`, `publishContainer`, their `OnTrack` and `Stream` variants, `setVideoProperties`, and `setCatalogSection`, and the consumer's catalog and media subscriptions, are gone, along with the `MediaProducer`, `MediaConsumer`, and `MediaGroupConsumer` aliases. +Use `media.TrackProducer.audio` / `video`, `ContainerProducer`, `CatalogProducer`, `CatalogConsumer.subscribe`, `TrackConsumer.subscribe`, and `GroupConsumer.fetch`. +Producers drop `name`, `used`, and `unused`; read them through `demand()`. + +```dart +// Before +final audio = broadcast.publishAudio(init: init); +// After +final audio = media.TrackProducer.audio(broadcast: broadcast, target: media.Named(null), init: init); +``` + +### Container byte-stream importers take an init record + +`media.ContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. +Its optional `timebase`, also on `ContainerInit`, shares one source's offset across container imports. + +### Timestamps are optional + +`Frame.timestampUs` and `Datagram.timestampUs` are nullable, null on a frame read from an untimed track, and so are their `timestamp` getters. +A raw write to a timed track without a timestamp throws `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. +`media.CatalogProducer.now()` samples the broadcast clock for local-event stamps. + +### Subscriber staleness is `maxDelay` + +`Subscription.maxAgeUs` / `maxAge` is `maxDelayUs` / `maxDelay`. +`TrackInfo.maxAge` keeps its name: it is the publisher's retention. + +### Error payloads are named `reason` + +Exceptions that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. + +### `Transport` gains `webTransport` + +A WebTransport session reports `MoqTransport.webTransport`; `quic` now means raw QUIC only, so an exhaustive `switch` needs one more case. + +### `stalled` is `enabled` (#4915) + +`media.Video.stalled` is `enabled`, defaulting to true with the meaning inverted, and `media.Audio` gains `enabled`. + +### Routes have one cost (#4661) + +`MoqRoute.cold` is removed. + +### `BroadcastProducer.finish()` is removed (#4152) + +```dart +// Before +broadcast.finish(); +// After +broadcast.close(); +``` diff --git a/kt/moq/MIGRATE.md b/kt/moq/MIGRATE.md index 10dd50e461..e1b04d2f8c 100644 --- a/kt/moq/MIGRATE.md +++ b/kt/moq/MIGRATE.md @@ -3,3 +3,149 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Background panics are reported (#5562) + +A panic in a background task throws `uniffi.moq.MoqException.Task` with the panic message as `reason`, and other objects keep running. + +### Announcements are events with a restart + +`announced(config).updates()` emits an `AnnounceEvent`: `AnnounceEventStart`, `AnnounceEventUpdate`, `AnnounceEventEnd`, or `AnnounceEventRestart`, each carrying an `Announce` as `announce`. +`Moq.announcements()` and `AnnounceUpdate` are gone. +A restart means another publisher instance now serves the prefix: request the path again. + +```kotlin +// Before +moq.announcements().collect { update -> + if (update.active()) subscribe(update.prefix()) +} +// After +moq.announced().updates().collect { event -> + when (event) { + is AnnounceEventStart -> subscribe(event.announce.prefix) + is AnnounceEventRestart -> subscribe(event.announce.prefix) + else -> {} + } +} +``` + +### "Epoch" means only the publisher epoch (#5146) + +`Moq.epoch()` is `connects()`, and `AudioProducer.resetEpoch()` is `reanchor()`. + +### `connect` takes a `ClientConfig` + +The keyword arguments moved into a `ClientConfig` with nested `tls`, `quic`, `websocket`, and `backoff` records. +The `subscribe` origin is `consume`, and `reconnect = false` is `once = true`. +A value the native side cannot use throws `MoqException.Config`, including a bad bind address. +A server request takes its origins in `accept(publish, consume)`, where `null` inherits the server's origin. + +```kotlin +// Before +val moq = Moq.connect(url, tlsVerify = false, subscribe = origin) +// After +val moq = Moq.connect(url, ClientConfig(tls = ClientTls(insecure = true), consume = origin)) +``` + +### Data tracks wrap a track + +`BroadcastProducer.publishJsonSnapshot` / `publishJsonStream`, the binary variants, and the matching consumers are gone. +Create the track, then wrap it with `dev.moq.json` (or `dev.moq.flate` for opaque bytes). + +```kotlin +// Before +val state = broadcast.publishJsonSnapshot("state", config) +// After +val state = dev.moq.json.SnapshotProducer(broadcast, broadcast.publishTrack("state", null), config) +``` + +### Media lives in `dev.moq.media` + +`BroadcastProducer.publishAudio`, `publishVideo`, `publishContainer`, their `OnTrack` and `Stream` variants, `setVideoProperties`, and `setCatalogSection`, and the consumer's catalog and media subscriptions, are gone. +Use `TrackProducer.audio` / `video`, `ContainerProducer`, `CatalogProducer` (with `setSection`), `CatalogConsumer.subscribe`, `TrackConsumer.subscribe`, and `GroupConsumer.fetch`. +Producers drop `name`, `used`, and `unused`; read them through `demand()`. + +```kotlin +// Before +val audio = broadcast.publishAudio(AudioInit(format = AudioFormat.OPUS, data = opusHead)) +// After +val audio = dev.moq.media.TrackProducer.audio(broadcast, Named(null), AudioInit(format = AudioFormat.OPUS, data = opusHead)) +``` + +### Container byte-stream importers take an init record + +`ContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. +Its optional `timebase`, also on `ContainerInit`, shares one source's offset across container imports. + +### Timestamps are optional + +`Frame.timestamp` and `Datagram.timestamp` are `Duration?`, null on a frame read from an untimed track. +A raw write to a timed track without a timestamp throws `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. +`CatalogProducer.now()` samples the broadcast clock for local-event stamps. + +### Subscriber staleness is `maxDelay` + +`Subscription.maxAge`, `AudioDecoderOutput.maxAge`, and `VideoDecoderOutput.maxAge` are `maxDelay`. +`TrackInfo.maxAge` keeps its name: it is the publisher's retention. + +### Error payloads are named `reason` + +Exceptions that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. + +### `Transport` gains `WEB_TRANSPORT` + +A WebTransport session reports `Transport.WEB_TRANSPORT`; `QUIC` now means raw QUIC only, so an exhaustive `when` needs one more branch. + +### `stalled` is `enabled` (#4915) + +`Video.stalled` is `enabled`, defaulting to true with the meaning inverted, and `Audio` gains `enabled`. + +### Routes have one cost (#4661) + +`Route.cold` is removed. + +### Decoded video frames are objects + +`VideoDecodedFrame` is a closeable handle with `width()`, `height()`, `pixels(format)`, and `surface()`. +`VideoDecoderOutput.format` is gone; set `surface = true` to keep the decoder's native surface. + +```kotlin +// Before +val rgba = frame.data +// After +val rgba = frame.use { it.pixels(VideoPixelFormat.RGBA) } +``` + +### `BroadcastProducer.finish()` is removed (#4152) + +Call `end()` to finish the broadcast and keep the handle, or `close()` to release it. + +## 0.4 -> 0.5 + +### Durations are `kotlin.time.Duration` + +Extension properties expose every microsecond field as a `Duration`: `Frame.timestamp`, `Subscription.maxAge`, `ConnectionStats.rtt`, `Backoff.initial`, and the rest. + +### `connect` grows options + +`Moq.connect` gains `reconnect`, `backoff`, `maxStreams`, and the WebSocket fallback settings as named arguments. + +### Publish split by kind + +`publishMedia*` is `publishAudio`, `publishVideo`, or `publishContainer`, each taking its own init. +The raw encoder paths that used to be `publishAudio` / `publishVideo` are `encodeAudio` / `encodeVideo`. + +### Renames + +| Before | After | +| --- | --- | +| `estimatedSendBitrate` / `estimatedRecvBitrate` | `estimatedSendRate` / `estimatedRecvRate` | +| `Announced` / `Announcement` | `AnnounceConsumer` / `AnnounceUpdate` | +| `BroadcastRequest.abort` | `reject` | +| `OriginOptions` | `OriginConfig` | + +### Errors are structured + +`MoqException.Protocol` carries a `ProtocolError` (scope, wire code, kind) instead of a flattened message. +Setters throw (`Busy` during connect or listen) instead of dropping the value. +Track and group `finish()` keep the handle open, so a later `abort()` can still run. diff --git a/swift/MIGRATE.md b/swift/MIGRATE.md index da2207e765..e5b03aafc0 100644 --- a/swift/MIGRATE.md +++ b/swift/MIGRATE.md @@ -3,3 +3,142 @@ How to update your code for each breaking change, newest first. ## Unreleased + +### Background panics are reported (#5562) + +A panic in a background task throws `MoqError.Task` with the panic message as `reason`, and other objects keep running. + +### Announcements are events with a restart + +`AnnounceConsumer` yields an `AnnounceEvent`: `.start`, `.update`, `.end`, or `.restart`, each carrying an `Announce`. +`AnnounceUpdate` is gone. +A restart means another publisher instance now serves the prefix: request the path again. + +```swift +// Before +for try await update in try session.consume.announced(prefix: "live/") where update.active() { + subscribe(update.prefix()) +} +// After +for try await event in try session.consume.announced(prefix: "live/") { + switch event { + case .start(let announce), .restart(let announce): subscribe(announce.prefix) + default: continue + } +} +``` + +### "Epoch" means only the publisher epoch (#5146) + +`Session.epoch()` is `connects()`, and `AudioProducer.resetEpoch()` is `reanchor()`. + +### `Client` takes its config at init + +The `Client` setters are `Client(...)` arguments, with `tls`, `quic`, `websocket`, and `backoff` records. +`setReconnect(false)` is `once: true`. +A value the native side cannot use throws `MoqError.Config`, including a bad bind address. +`Server` takes its config at init too, and a request takes its origins in `accept(publish:consume:)`, where `nil` inherits the server's origin. +`Session.shutdown()` is `async throws` and drains before returning. + +```swift +// Before +let client = Client() +try client.setTlsVerify(false) +try client.setConsume(origin) +// After +let client = try Client(tls: ClientTls(insecure: true), consume: origin) +``` + +### Data tracks wrap a track + +`BroadcastProducer.publishJsonSnapshot` / `publishJsonStream` and the consumer's `subscribeJson*` are gone. +Create the track, then wrap it with `Json` (or `Flate` for opaque bytes). + +```swift +// Before +let state = try broadcast.publishJsonSnapshot(name: "state", of: State.self) +// After +let state = try Json.SnapshotProducer(broadcast: broadcast, track: try broadcast.publishTrack(name: "state")) +``` + +### Media lives in `Media` + +`BroadcastProducer.publishAudio`, `publishVideo`, `publishContainer`, their `on:` and stream variants, `setVideoProperties`, `setCatalogSection`, and `finish`, and the consumer's `subscribeCatalog`, `subscribeMedia`, and `fetchMediaGroup`, are gone. +Use `Media.TrackProducer.audio` / `video`, `Media.ContainerProducer`, `Media.CatalogProducer`, `Media.CatalogConsumer.subscribe`, `Media.TrackConsumer.subscribe`, and `Media.GroupConsumer.fetch`. +The catalog types (`Catalog`, `Video`, `Audio`, `AudioFormat`, ...) moved under `Media`. +Producers drop `name`, `used`, and `unused`; read them through `demand()`. + +```swift +// Before +let audio = try broadcast.publishAudio(format: .opus, initData: opusHead) +// After +let audio = try Media.TrackProducer.audio(broadcast: broadcast, initData: Media.AudioInit(format: .opus, data: opusHead)) +``` + +### Container byte-stream importers take an init record + +`Media.ContainerStreamProducer` takes a `Media.ContainerStreamInit` holding the format instead of a bare format. +Its optional `timebase`, also on `Media.ContainerInit`, shares one source's offset across container imports. + +### Timestamps are optional + +Raw `writeFrame(_:timestampUs:)` takes a `UInt64?` defaulting to `nil`, and a frame read from an untimed track has a `nil` `timestampUs`. +A write to a timed track without a timestamp throws `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. +`Media.CatalogProducer.now()` samples the broadcast clock for local-event stamps. + +### Subscriber staleness is `maxDelayUs` + +`Subscription`, `AudioDecoderOutput`, and `VideoDecoderOutput` take `maxDelayUs` instead of `maxAgeUs`. +`TrackInfo.maxAgeUs` keeps its name: it is the publisher's retention. + +### Error payloads are named `reason` + +Cases that carried a positional string (`Transport`, `Internal`, `Connect`, ...) take a `reason:` label. + +### `Transport` gains `.webTransport` + +A WebTransport session reports `.webTransport`; `.quic` now means raw QUIC only, so an exhaustive `switch` needs one more case. + +### `stalled` is `enabled` (#4915) + +`Media.Video.stalled` is `enabled`, defaulting to true with the meaning inverted, and `Media.Audio` gains `enabled`. + +### Routes have one cost (#4661) + +`Route.cold` is removed. + +### Decoded video frames are objects + +`VideoDecodedFrame` has `timestampUs()`, `width()`, `height()`, `pixels(format:)`, and `surface()` instead of stored fields. +`VideoDecoderOutput.format` is gone; set `surface: true` to keep the decoder's native surface. + +```swift +// Before +let rgba = frame.data +// After +let rgba = try frame.pixels(format: .rgba) +``` + +## 0.4 -> 0.5 + +### Publish split by kind + +`publishMedia` is `publishAudio`, `publishVideo`, or `publishContainer`, each taking its own init. +The raw encoder paths that used to be `publishAudio` / `publishVideo` are `encodeAudio` / `encodeVideo`. + +### Renames + +| Before | After | +| --- | --- | +| millisecond durations | microseconds (`maxAgeUs`, `Backoff.initialUs`) | +| `estimatedSendBitrate` / `estimatedRecvBitrate` | `estimatedSendRate` / `estimatedRecvRate` | +| `setTlsDisableVerify(true)` | `setTlsVerify(false)` | +| `Announced` / `Announcement` | `AnnounceConsumer` / `AnnounceUpdate` | +| `BroadcastRequest.abort` | `reject` | +| `OriginOptions` | `OriginConfig` | + +### Errors are structured + +`MoqError.Protocol` carries `details` (scope, wire code, kind) instead of a flattened message. +Setters throw (`.Busy` during connect or listen) instead of dropping the value. +Track and group `finish()` keep the handle open, so a later `abort()` can still run. From cb38943ae7134e9757aeac3babae2aa54e5136df Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 16:45:45 -0700 Subject: [PATCH 07/40] docs: backfill MIGRATE.md for moq-relay, moq-cli, and moq-gst Co-Authored-By: Claude Opus 5.5 --- rs/moq-cli/MIGRATE.md | 114 +++++++++++++++++++++++++++++++++++ rs/moq-gst/MIGRATE.md | 13 ++++ rs/moq-relay/MIGRATE.md | 129 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 256 insertions(+) diff --git a/rs/moq-cli/MIGRATE.md b/rs/moq-cli/MIGRATE.md index 25b46c383d..8b799e4424 100644 --- a/rs/moq-cli/MIGRATE.md +++ b/rs/moq-cli/MIGRATE.md @@ -3,3 +3,117 @@ How to update your invocation and config for each breaking change, newest first. ## Unreleased + +### `moq export ts --delay` defaults to 1 s and includes the send-ahead (#5553) + +The output trails a TS source by the delay instead of twice it. +With a multiplex rate, video whose HRD declares a longer buffer delay than `--delay` stops the export with the delay it needs named. + +```bash +# Before +moq export ts --delay 500ms ... +# After +moq export ts --delay 1s ... +``` + +### `--max-age` on fMP4, H.264, H.265, and RTMP exports is `--max-delay` + +`moq export fmp4`, `h264`, `h265`, and `rtmp` refuse `--max-age`. +`moq import --max-age` is unchanged: it is the publisher's retention. + +```bash +# Before +moq export fmp4 --max-age 2s ... +# After +moq export fmp4 --max-delay 2s ... +``` + +### `moq export ts`, `flv`, and `mkv` take `--delay` (#4645) + +Each frame is written that long after its decode time; the older staleness spellings are refused with `--delay` named. + +### `moq export ts --linger` follows only the same publisher instance + +A restarted publisher exits 1 unless `--stitch` follows it as a program switch. +`moq export srt` takes the same two flags. + +```bash +moq export ts --linger 10s --stitch ... +``` + +### `moq export archive` refuses untimed tracks + +Recording a track with no timestamps fails with `Untimed`, so an archive over moq-lite before 05 or moq-transport drafts 14 to 16 fails instead of recording arrival times. + +### `moq export fmp4` fixes its track set at the init segment + +A rendition added, changed, or left undescribed after the init segment ends the export with an error instead of writing a track missing from the moov. +Restart the export to pick up a new rendition. + +### `moq play --delay` defaults to `auto` (#4162) + +`auto` is measured from how late frames arrive instead of derived from RTT. +Pass `--delay 100ms` to keep the old fixed delay. + +### `--hop` is removed; `--epoch` replaces it (#4969) + +A redundant pair shares an epoch, a UUIDv7 such as `uuidgen -7` prints, instead of a Hop ID. +`--hop` is an unknown flag and `MOQ_HOP` is silently ignored, so remove it: a pair still keyed on `MOQ_HOP` mints an epoch per process and each restart replaces the other's broadcast. + +```bash +# Before +MOQ_HOP=7 moq import ... +# After +MOQ_EPOCH=$(uuidgen -7) moq import ... # the same value on both members +``` + +### Settings no listener reads stop startup (#5006) + +`moq` without a listener refuses `--listen-*` and `--auth-*` flags. +A stream-only listener (TCP or Unix, no `--listen`) refuses the QUIC-only flags, a client CA, and a `--listen-tls-*` certificate unless `--listen-tcp-tls` serves it. +`--listen-quic-lb-nonce` needs `--listen-quic-lb-id`. +Each used to be ignored: drop it, or add the listener it configures. + +### `moq --listen` with an invalid auth config stops at startup (#4912) + +It used to start and refuse every session. + +### The `noq` cargo feature is `quic` + +Rename it in `--features` when building from source. + +## 0.13 -> 0.14 + +### Gossip discovery is removed (#4601) + +`--cluster-mesh` and TOML `mesh` are refused: list every peer with `--cluster-connect`. + +## 0.12 -> 0.13 + +### `moq auth serve` matches 0.14 relay auth (#4319) + +`moq auth serve` never re-checks or expires a session by default; `--revalidate` needs `--expires`, and `--limit-*` needs `--revalidate`. +It refuses a session presenting both a JWT and a certificate. +`--key` takes a file, not an https or JWKS URL. + +## 0.11 -> 0.12 + +### Commands and flags are renamed + +The listen and connect flags split by direction, as in [moq-relay](../moq-relay/MIGRATE.md#flags-split-by-direction). +A renamed flag or environment variable is refused with its replacement named. + +| Before | After | +| --- | --- | +| `moq publish`, `moq subscribe` | `moq import`, `moq export` | +| `moq token`, the `moq-token` binary | `moq auth` | +| `--origin` | `--hop`, now `--epoch` | +| `--name` | `--broadcast` | +| `--latency-max` | `--max-age` | +| `--server-bind` | `--listen` | +| `--client-connect` | `--connect` | +| `--tls-disable-verify` | `--connect-tls-insecure` | + +### `moq --listen` needs auth + +A listener refuses to start without `--auth-url` or `--auth-public` instead of accepting everyone. diff --git a/rs/moq-gst/MIGRATE.md b/rs/moq-gst/MIGRATE.md index 784a2c6b6f..0741ecf194 100644 --- a/rs/moq-gst/MIGRATE.md +++ b/rs/moq-gst/MIGRATE.md @@ -3,3 +3,16 @@ How to update your pipelines for each breaking change, newest first. ## Unreleased + +## 0.3 -> 0.4 + +### Rate estimates are `estimated-*-rate` + +The `moqsink` properties `estimated-send-bitrate` and `estimated-recv-bitrate` are `estimated-send-rate` and `estimated-recv-rate`, with no alias: an application or `gst-launch` line naming the old ones fails at runtime. + +```c +// Before +g_object_get(sink, "estimated-send-bitrate", &rate, NULL); +// After +g_object_get(sink, "estimated-send-rate", &rate, NULL); +``` diff --git a/rs/moq-relay/MIGRATE.md b/rs/moq-relay/MIGRATE.md index 23cffed67b..10100d69ce 100644 --- a/rs/moq-relay/MIGRATE.md +++ b/rs/moq-relay/MIGRATE.md @@ -1,5 +1,134 @@ # Migrating moq-relay How to update your code, invocation, and config for each breaking change, newest first. +Rolling a cluster across a release also follows the [upgrade guide](https://moq.dev/setup/upgrade). ## Unreleased + +### Workers default to unpinned (#5688) + +`runtime.pin` defaults to off, so two worker groups on one host no longer crowd onto the first cores. +Set it to keep the old placement. + +```toml +[runtime] +pin = true +``` + +### iroh is opt-in (#5223) + +The default build has no iroh listener and no `--iroh-*` flags or `[iroh]` table. +Build with `--features iroh` to keep it. + +### The `noq` cargo feature is `quic` + +Rename it wherever you enable it; the QUIC stack is MoQ's own `moq-quic`. +A qlog directory gets one file per endpoint instead of one per connection. + +### A QUIC load-balancer nonce needs its server id (#5114) + +`listen.lb_nonce` (`--listen-quic-lb-nonce`, `MOQ_LISTEN_QUIC_LB_NONCE`) without `lb_id` stops startup. +Setting `lb_id` or `lb_nonce` together with `load_balancer` also stops; `lb_id` used to win. +Set one of them. + +### Settings no listener reads stop startup (#5006) + +A stream-only relay (TCP or Unix, no `--listen`) refuses the QUIC-only `--listen-preferred-v4` / `-v6`, `--listen-quic-lb-id`, and pinned `tls.peers`. +It also refuses `--listen-tls-cert`, `-key`, or `-generate` unless `--listen-tcp-tls` serves it, and `listen.tls.root`, since only QUIC verifies a client CA. +`--listen-unix-allow-*` needs `--listen-unix-bind`, and `web.https.cert`, `key`, and `root` need `web.https.listen`. +Each used to be ignored: drop it, or add the listener it configures. + +### `--cluster-mesh` and `--cluster-linger` are unknown flags (#4818) + +They, TOML `mesh` and `linger`, and `MOQ_CLUSTER_MESH` / `MOQ_CLUSTER_LINGER` are no longer read. +List every peer with `--cluster-connect` or `--cluster-connect-api`. +In Rust, `cluster::Config` has no `mesh` or `linger` field. + +### `--cluster-id` no longer falls back to `--hop` (#4969) + +A node that pinned its id with `--hop` passes `--cluster-id`. +A route that names no publisher keeps its 0 instead of gaining a random hop. + +### Embedding: auth takes the client-CA answer (#4912) + +`auth::Config::validate` and `init` take `client_ca: bool`, whether any listener verifies client certificates, and `validate_client_ca` is gone. + +```rust +// Before +config.auth.validate()?; +config.auth.validate_client_ca()?; +// After +config.auth.validate(client_ca)?; +``` + +### Embedding: io_uring workers bind from the server config (#5312) + +`uring::Workers::bind` takes a `moq_tokio::server::Config`, so generated and rotating certificates work in both worker modes. + +## 0.16 -> 0.17 + +### Gossip discovery is removed (#4601) + +A relay dials only the peers it lists or finds on the LAN, never a URL learned from an announcement, and no longer announces `.internal/origins`. +`--cluster-mesh` and TOML `mesh` are refused by name: list every peer with `--cluster-connect` or `--cluster-connect-api`. +The `/nodes` endpoint lists only peers this relay dialed. + +## 0.15 -> 0.16 + +### 0.14 auth behavior is restored (#4319) + +- Removed 0.14 auth settings (`[auth]` `key`, `key_dir`, `auth_api`, `domains`, `mtls_tier`, `[auth.tls]`, their flags and `MOQ_AUTH_*` variables) stop the relay with the replacement named. +- Public and mTLS patterns are rooted at `/`, as in 0.14: `anon/**` admits a client dialed at `/anon`. +- A token's `iss`, `sub`, and `jti` are ignored and `nbf` is enforced; any other claim, `aud` and `cluster` included, refuses the token. +- A relay on `--auth-public` refuses a session presenting a token, including a peer's `cluster.token`, and refuses to start with a client CA. + +## 0.14 -> 0.15 + +### Flags split by direction + +The flags split into `--listen-*` (accepting), `--connect-*` (dialing), and a shared `--quic-*` section. +Environment variables follow the flag (`MOQ_SERVER_BIND` is `MOQ_LISTEN`), and a renamed one is refused with its replacement named. + +| Before | After | +| --- | --- | +| `--server-bind` | `--listen` | +| `--server-*`, `--tls-cert`, `--tls-key`, `--tls-generate` | `--listen-*`, `--listen-tls-cert`, `--listen-tls-key`, `--listen-tls-generate` | +| `--client-connect` | `--connect` | +| `--client-*` | `--connect-*` | +| `--client-failover-delay` | `--connect-race` | +| `--client-reconnect=false` | `--connect-once` | +| `--tls-disable-verify`, `--client-tls-disable-verify` | `--connect-tls-insecure` | +| `--server-quic-*`, `--client-quic-*` | `--quic-*`, applied to both directions | +| TOML `[server]`, `[client]` | `[listen]`, `[connect]` | +| TOML `[server.quic]`, `[client.quic]` | `[quic]` | +| TOML `listen`, `connect`, `failover_delay`, `reconnect`, `disable_verify` | `bind`, `url`, `race`, `once` (inverted), `insecure` | +| `--cluster-linger` | removed; a broadcast closes when its last publisher is lost | +| `--cluster-connect host:port` | a full URL, `https://host/?jwt=TOKEN` | + +### Auth is one contract (#3688) + +The relay asks an auth server per session (`--auth-url`) or applies a static grant (`--auth-public`); exactly one is required. +`--auth-key`, `--auth-key-dir`, `--auth-api`, `--auth-api-mode`, `--auth-public-api`, `--auth-domain`, `--auth-mtls-tier`, and `--auth-tls-*` are gone: run `moq auth serve --key-dir ...` beside the relay and point `--auth-url` at it. +The flag-by-flag mapping is in [Migrating from the relay flags](https://moq.dev/bin/relay/auth#migrating-from-the-relay-flags). +A verified client certificate admits nothing on its own; the auth server grants it. + +```bash +# Before +moq-relay --auth-key-dir keys/ --auth-public anon +# After +moq auth serve --key-dir keys/ --public-publish 'anon/**' --public-subscribe 'anon/**' & +moq-relay --auth-url http://127.0.0.1:4440 +``` + +### Grants are patterns + +`anon` is exactly the broadcast `anon`; write `anon/**` for the subtree. +This applies to `--auth-public`, TOML `public`, and `[auth.public]`, which is now `public_subscribe` / `public_publish`. +A public or mTLS pattern with no wildcard refuses to start, naming the subtree to write. +JWT `publish` and `subscribe` claims are patterns too, so a token granting `alice` covers only `alice`. + +### Embedding + +`Relay` fields are private (#3638): clone the handles you need, mount routes, then call `Relay::run`. +`Cluster::with_cache` moved to `cluster::Options`. +The `quinn` and `quiche` features and the backend setting are gone (#3811). From ea3c9c504bd360c33c27f20520c9ab7f6810762c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 16:48:27 -0700 Subject: [PATCH 08/40] docs: make the upgrade page a rollout guide and MIGRATE.md index Co-Authored-By: Claude Opus 5.5 --- doc/lib/index.md | 5 + doc/lib/rs/moq-flate.md | 4 +- doc/lib/rs/moq-json.md | 5 +- doc/setup/upgrade.md | 679 +++------------------------------------- rs/moq-cli/MIGRATE.md | 1 + rs/moq-mux/MIGRATE.md | 5 + 6 files changed, 66 insertions(+), 633 deletions(-) diff --git a/doc/lib/index.md b/doc/lib/index.md index 12f6a28ca1..5f05c12a26 100644 --- a/doc/lib/index.md +++ b/doc/lib/index.md @@ -47,3 +47,8 @@ how it looks in that language: Dart is the exception on codecs: its published binaries carry no encoder or decoder, so it moves already-encoded frames. + +## Upgrading + +Each package keeps a `MIGRATE.md` with a before/after snippet for every +breaking change; [Upgrade](/setup/upgrade#migration-guides) links them all. diff --git a/doc/lib/rs/moq-flate.md b/doc/lib/rs/moq-flate.md index f0b2853f6a..0a3ab11965 100644 --- a/doc/lib/rs/moq-flate.md +++ b/doc/lib/rs/moq-flate.md @@ -32,8 +32,8 @@ an untimed track has no timescale, and a timed track requires a timestamp on every write. A publisher that wants to stamp now samples its own clock. Writes return the encoded frame size. -When upgrading, replace `Timed` with `Timed

` and pass capture times -explicitly instead of relying on a write-time default. +Upgrading from an older release: see moq-flate's +[MIGRATE.md](https://github.com/moq-dev/moq/blob/main/rs/moq-flate/MIGRATE.md). A stream rides one group, so the whole log shares moq-net's group budget: 32 MiB of payload and 8192 payloads. An `append` that might not fit returns diff --git a/doc/lib/rs/moq-json.md b/doc/lib/rs/moq-json.md index fb0dafc333..2338572eae 100644 --- a/doc/lib/rs/moq-json.md +++ b/doc/lib/rs/moq-json.md @@ -23,9 +23,8 @@ The timestamp must match the track: an untimed track has no timescale, and a timed track requires a timestamp on every write. Editing guards take the same optional capture time when opened, including when they publish on drop. -When upgrading, replace `Timed` with `Timed

` and pass timestamps -explicitly. A publisher that wants to stamp an event now samples its own -clock; the producer never supplies one. +Upgrading from an older release: see moq-json's +[MIGRATE.md](https://github.com/moq-dev/moq/blob/main/rs/moq-json/MIGRATE.md). A stream rides one group, so the whole log shares moq-net's group budget: 32 MiB of payload and 8192 records. An append that might not fit is refused diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index f444becb33..f94fa2823b 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -1,451 +1,60 @@ --- title: Upgrade -description: Breaking changes between the 2026-09-17 releases and the 2026-09-23 release train, with the replacement for each +description: The order to roll out a release across relays, auth servers, and clients, and each package's migration guide --- # Upgrade -This page walks from the 2026-09-17 releases (moq-relay 0.14.18, moq-cli -0.11.2, moq-net 0.2.22, @moq/net 0.3.5, moq-ffi 0.3.19) to the 2026-09-23 -release train (moq-relay 0.15.1, moq-cli 0.12.1, moq-net 0.3.0, @moq/net 0.4.0, -moq-ffi 0.4.1). Each crate's `CHANGELOG.md` has the full list; this page is the -subset that breaks a working setup. +Every published package keeps a `MIGRATE.md` with a before/after snippet for +each breaking change, newest first. Read your package's file from the top down +to the version you run. This page covers what spans packages: the wire changes +that decide the order to upgrade a deployment. A released flag, environment variable, or config key that was renamed is refused at startup with its replacement named, rather than ignored. Fix what the error lists and rerun. -## Unreleased +## Migration guides -These land with the next breaking release, not the 2026-09-23 train. +- **Binaries:** [moq-relay](https://github.com/moq-dev/moq/blob/main/rs/moq-relay/MIGRATE.md), [moq-cli (`moq`)](https://github.com/moq-dev/moq/blob/main/rs/moq-cli/MIGRATE.md), [moq-gst](https://github.com/moq-dev/moq/blob/main/rs/moq-gst/MIGRATE.md), [obs-moq](https://github.com/moq-dev/moq/blob/main/cpp/obs/MIGRATE.md) +- **Bindings:** [moq-ffi (UniFFI)](https://github.com/moq-dev/moq/blob/main/rs/moq-ffi/MIGRATE.md), [moq-c (C)](https://github.com/moq-dev/moq/blob/main/rs/moq-c/MIGRATE.md), [Python](https://github.com/moq-dev/moq/blob/main/py/moq-rs/MIGRATE.md), [Go](https://github.com/moq-dev/moq/blob/main/go/wrapper/MIGRATE.md), [Swift](https://github.com/moq-dev/moq/blob/main/swift/MIGRATE.md), [Kotlin](https://github.com/moq-dev/moq/blob/main/kt/moq/MIGRATE.md), [Dart](https://github.com/moq-dev/moq/blob/main/dart/moq/MIGRATE.md), [C++](https://github.com/moq-dev/moq/blob/main/cpp/moq/MIGRATE.md) +- **Rust:** [hang](https://github.com/moq-dev/moq/blob/main/rs/hang/MIGRATE.md), [kio](https://github.com/moq-dev/moq/blob/main/rs/kio/MIGRATE.md), [moq-archive](https://github.com/moq-dev/moq/blob/main/rs/moq-archive/MIGRATE.md), [moq-audio](https://github.com/moq-dev/moq/blob/main/rs/moq-audio/MIGRATE.md), [moq-auth](https://github.com/moq-dev/moq/blob/main/rs/moq-auth/MIGRATE.md), [moq-boy](https://github.com/moq-dev/moq/blob/main/rs/moq-boy/MIGRATE.md), [moq-clock](https://github.com/moq-dev/moq/blob/main/rs/moq-clock/MIGRATE.md), [moq-e2ee](https://github.com/moq-dev/moq/blob/main/rs/moq-e2ee/MIGRATE.md), [moq-flate](https://github.com/moq-dev/moq/blob/main/rs/moq-flate/MIGRATE.md), [moq-hls](https://github.com/moq-dev/moq/blob/main/rs/moq-hls/MIGRATE.md), [moq-json](https://github.com/moq-dev/moq/blob/main/rs/moq-json/MIGRATE.md), [moq-loc](https://github.com/moq-dev/moq/blob/main/rs/moq-loc/MIGRATE.md), [moq-msf](https://github.com/moq-dev/moq/blob/main/rs/moq-msf/MIGRATE.md), [moq-mux](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/MIGRATE.md), [moq-net](https://github.com/moq-dev/moq/blob/main/rs/moq-net/MIGRATE.md), [moq-nvenc](https://github.com/moq-dev/moq/blob/main/rs/moq-nvenc/MIGRATE.md), [moq-pattern](https://github.com/moq-dev/moq/blob/main/rs/moq-pattern/MIGRATE.md), [moq-quic](https://github.com/moq-dev/moq/blob/main/rs/moq-quic/MIGRATE.md), [moq-room](https://github.com/moq-dev/moq/blob/main/rs/moq-room/MIGRATE.md), [moq-rtc](https://github.com/moq-dev/moq/blob/main/rs/moq-rtc/MIGRATE.md), [moq-rtmp](https://github.com/moq-dev/moq/blob/main/rs/moq-rtmp/MIGRATE.md), [moq-sock](https://github.com/moq-dev/moq/blob/main/rs/moq-sock/MIGRATE.md), [moq-srt](https://github.com/moq-dev/moq/blob/main/rs/moq-srt/MIGRATE.md), [moq-stats](https://github.com/moq-dev/moq/blob/main/rs/moq-stats/MIGRATE.md), [moq-tokio](https://github.com/moq-dev/moq/blob/main/rs/moq-tokio/MIGRATE.md), [moq-transcode](https://github.com/moq-dev/moq/blob/main/rs/moq-transcode/MIGRATE.md), [moq-uring](https://github.com/moq-dev/moq/blob/main/rs/moq-uring/MIGRATE.md), [moq-v4l](https://github.com/moq-dev/moq/blob/main/rs/moq-v4l/MIGRATE.md), [moq-video](https://github.com/moq-dev/moq/blob/main/rs/moq-video/MIGRATE.md) +- **JavaScript:** [`@moq/auth`](https://github.com/moq-dev/moq/blob/main/js/auth/MIGRATE.md), [`@moq/boy`](https://github.com/moq-dev/moq/blob/main/js/moq-boy/MIGRATE.md), [`@moq/clock`](https://github.com/moq-dev/moq/blob/main/js/clock/MIGRATE.md), [`@moq/flate`](https://github.com/moq-dev/moq/blob/main/js/flate/MIGRATE.md), [`@moq/hang`](https://github.com/moq-dev/moq/blob/main/js/hang/MIGRATE.md), [`@moq/json`](https://github.com/moq-dev/moq/blob/main/js/json/MIGRATE.md), [`@moq/loc`](https://github.com/moq-dev/moq/blob/main/js/loc/MIGRATE.md), [`@moq/msf`](https://github.com/moq-dev/moq/blob/main/js/msf/MIGRATE.md), [`@moq/net`](https://github.com/moq-dev/moq/blob/main/js/net/MIGRATE.md), [`@moq/p2p`](https://github.com/moq-dev/moq/blob/main/js/p2p/MIGRATE.md), [`@moq/pattern`](https://github.com/moq-dev/moq/blob/main/js/pattern/MIGRATE.md), [`@moq/publish`](https://github.com/moq-dev/moq/blob/main/js/publish/MIGRATE.md), [`@moq/room`](https://github.com/moq-dev/moq/blob/main/js/room/MIGRATE.md), [`@moq/signals`](https://github.com/moq-dev/moq/blob/main/js/signals/MIGRATE.md), [`@moq/video`](https://github.com/moq-dev/moq/blob/main/js/video/MIGRATE.md), [`@moq/watch`](https://github.com/moq-dev/moq/blob/main/js/watch/MIGRATE.md) -- **moq-mux's catalog consumer is opaque.** Construct `catalog::Consumer` with - `Consumer::new(broadcast, format).await` or convert an existing - `catalog::hang::Consumer` with `.into()`, then read it through `catalog::Stream`. - Its `Hang`, `Msf`, and `Closed` variants and `catalog::msf::Consumer::track` - are private. -- **JS catalog ranking names its media kind.** Replace `Catalog.ranked(renditions)` - with `Catalog.Video.ranked(renditions)`. `Catalog.Audio.ranked(renditions)` - orders audio by enabled state, bitrate, sample rate, channel count, and name. - Both helpers put disabled renditions last. -- **The `noq` cargo feature is `quic`.** Rename it wherever you enable it on - moq-tokio, moq-uring, moq-relay, moq-cli, or the other crates that forward - it. The QUIC stack is now MoQ's own `moq-quic` rather than the `moq-noq` - crates. In Rust, `moq_tokio::noq::Error` is `moq_tokio::quic::Error`, and - `moq_tokio::Error::Noq` is `moq_tokio::Error::Quic`. A qlog directory gets one - file per endpoint instead of one per connection; on io\_uring workers, one per - endpoint and one per dial. -- **moq-tokio config durations are optional.** Wrap durations in `Some(...)` for - connect `race`, `resolution_delay`, and `timeout`; listen `timeout`; QUIC - `idle_timeout` and `keep_alive`; backoff `initial`, `max`, and `timeout`; - GOAWAY `handover`; and WebSocket `delay`. `None` uses the default, while - `Some(Duration::ZERO)` keeps each knob's zero behavior. Values set in code - now take effect on parsed and merged configs unless the flag or environment - variable was actually given. TOML keys, C ABI, binding records, and wire - formats are unchanged. -- **Max age and max delay count wall-clock time too.** A superseded group is - stale once its successor arrived a full budget ago, as well as by media time, - so an untimed track is no longer exempt: with the default zero `max_delay`, - an untimed subscriber starts at the latest group and skips each superseded - one. Give it a budget to replay or tolerate reordering. -- **`cache::Pool::gc` takes a waiter.** In Rust, `gc(now)` is - `gc(now, waiter)`, like `Driver::poll`: call it again when the waiter wakes, - since a parked read can arm an earlier deadline than the one returned. -- **Native background panics.** UniFFI reports `MoqError::Task` with a panic - reason, and C reports `MOQ_ERROR_PANIC`. Other objects keep running. Browser - WebAssembly still aborts on panic with the current toolchain. -- **Accepted sessions return their driver task.** `moq_tokio::server::Request::ok()` - now returns `(session, driver)`. Await the supervised driver before classifying - the session close, so a driver panic reports `moq_tokio::Error::Panic` rather - than the cancellation its teardown leaves on a channel. -- **A QUIC load-balancer nonce needs its server id.** `listen.lb_nonce`, - `--listen-quic-lb-nonce`, and `MOQ_LISTEN_QUIC_LB_NONCE` without `lb_id` stop - startup. Setting `lb_id` (or `lb_nonce`) and `load_balancer` together also - stops; `lb_id` used to win and the other value was ignored. Set one of them. -- **A replaced broadcast restarts announce consumers, and subscriptions stay.** - Rust's `AnnounceEvent` gains `Restart`, `@moq/net`'s announce events gain the - `"restart"` kind, moq-ffi's `MoqAnnounceEvent` gains `Restart`, and libmoq's - `moq_announce_kind` gains `MOQ_ANNOUNCE_KIND_RESTART`: another publisher - instance now serves the prefix (a newer epoch, or another route without one, - including a reconnect), so request the path again. A newer epoch no longer - ends subscriptions with `Unroutable`; they stay on the old instance until - dropped or its route goes. A restart was an end then a start, and still is on - moq-lite 06 and older and moq-transport. moq-lite 07 adds `ANNOUNCE_RESTART` - (0x3), and lite-06's `ANNOUNCE_RESTART` is named `ANNOUNCE_UPDATE`, as it - always meant. -- **@moq/net announced requests follow restarts, and `broadcasts()` is gone.** - An unpinned `request(path, { announced: true })` now stays open across - publisher instances: its `active` moves to each new instance's broadcast and - clears between them, instead of the request ending with `Unroutable`. Read - `active` reactively and close the request when done; add `epoch` to keep the - old single-instance behavior. `Origin.Producer` and `Origin.Consumer` drop - `broadcasts(scope)`; iterate `announced(scope)` and keep the entries you need. - `@moq/watch` with `announced` off no longer moves to a later publisher. -- **@moq/net discovers only what something watches, and `discovery` is gone.** - A `consume` origin stays empty until a reader watches it with - `announced(scope)` or `request(path, { announced: true })`; the session then - asks the relay about those prefixes only. Code that peeked the table - (`request(path)` expecting a route) without watching should hold an - `announced(scope)` stream. The `discovery` option on `connect`, `accept`, and - `Connection` and `Established.discovery` are removed, and passing the option - throws a `TypeError`: drop it. A relay that refuses an announce request - downgrades that session to blind requests on its own. -- **JS time units and timers come from `@moq/clock`.** `Time.Nano`, `Time.Micro`, - `Time.Milli`, and `Time.Second` are gone from `@moq/net`: import `Nano`, - `Micro`, `Milli`, and `Second` from `@moq/clock`, which throws on negative or - non-finite values. `effect.timer`, `effect.timeout`, and `effect.interval` - take a `Nano` (`Nano.fromMilli(Milli(250))`), not milliseconds, and run on the - installed clock. `sleep` and `sleepUntil` take `{ signal }` instead of a bare - signal. `@moq/room`'s `HISTORY` is a `Nano`. -- **"Epoch" means only the publisher epoch.** The reconnect counter is - `connects()`: `moq_tokio::Connection::epoch()` and moq-ffi's - `MoqSession::epoch()` are `connects()`, which is Python's `connects()`, Go's - `Connects()`, Swift's and Kotlin's `connects()`, and Dart's `connects`. The - audio re-anchor is `reanchor()`: `moq_audio::encode::Producer::reset_epoch` and - moq-ffi's `MoqAudioProducer::reset_epoch` are `reanchor`, which is Python's - `reanchor()` (was `reset_epoch()`), Swift's, Kotlin's, and Dart `moq_ffi`'s - `reanchor()` (was `resetEpoch()`), and Go's `Reanchor()` (was `ResetEpoch()`). -- **`moq export ts --linger` waits only for the same publisher instance.** A - restarted publisher (a new epoch, or any return on a version without one) - exits 1 unless `--stitch` follows it as a program switch. `export srt` takes - the same two flags. In Rust, moq-mux's `ts::Export::resume` is - `ts::Export::follow`, `Source::returned` is gone, and `ts::Follower` follows a - path's announcements the way both exports do. - `ts::Export::new(source, catalog)` now takes a catalog stream, like the other - container exporters. Subscribe with `source.catalog::(format).await?` - to retain MPEG-TS metadata, or use `()` for media only; select renditions on the - stream before passing it in. `follow(broadcast, catalog)` also takes the replacement - stream. Both methods are synchronous; `with_ts` and `with_catalog_format` are removed. -- **moq-mux's `Source` is built from the resolved catalog broadcast.** - `Source::new(origin, path)` is `Source::new(origin, broadcast)`: resolve the - broadcast first (`origin.request_broadcast(path, None)`), and its path is - where catalog references resolve from. `Source::broadcast()` returns that - broadcast without awaiting. Every later request for a path, siblings - included, stays on the broadcast the source first held there and fails once - it ends, rather than reaching a replacement; `bind` included. Build a new - `Source` to follow a new instance. moq-hls's `Broadcaster::new` no longer - awaits or fails, and a rendition whose sibling broadcast ends now ends its - playlist instead of rebinding. -- **fMP4 export of Annex-B H.264 and H.265 inits from the catalog.** When the - catalog codec string and dimensions are enough, `moq export fmp4` writes an - `avc3` or `hev1` init segment before the first keyframe and leaves SPS, PPS, - and VPS in the samples. High AVC profiles (110, 122, 244, and the rest whose - chroma or bit depth the string does not carry), HEVC beyond Main and Main - Still Picture, and a catalog missing dimensions still wait for the SPS. A - returning Annex-B rendition is matched on that catalog record, so an encoder - that restarts with a new SPS can return. A keyframe whose parameter sets never - appeared in the track ends the export instead of waiting 30 seconds. -- **moq-binary is moq-flate, and @moq/binary is @moq/flate.** The opaque - `snapshot` and `stream` tracks moved beside the codec; the wire and the - catalog's `binary` section are unchanged. In Rust, `moq_binary::X` is - `moq_flate::X`, and `moq_binary::Error::Flate(e)` is the matching - `moq_flate::Error` variant (`Decompress`, `TooLarge`). `moq_flate::Error` - now carries `moq_net::Error`, so it is no longer `PartialEq`. moq-mux's - `Error::Binary` is `Error::Flate`. In TypeScript, import `Snapshot` and - `Stream` from `@moq/flate`. -- **moq-ffi data tracks wrap a track.** The broadcast's - `publish_binary_*`, `publish_json_*`, and `subscribe_json_*` are gone. - Create the track with `publish_track` or `subscribe_track`, then construct - `MoqFlateSnapshotProducer` / `MoqFlateStreamProducer` (taking - `MoqFlateConfig`) or `MoqJsonSnapshotProducer` / `MoqJsonStreamProducer` - from the broadcast and the track, or `MoqJsonSnapshotConsumer` / - `MoqJsonStreamConsumer` from the track. The wrappers keep JSON under its own - namespace: `moq.json` in Python, `moq.dev/moq/json` in Go, `dev.moq.json` in - Kotlin, `package:moq/json.dart` in Dart, and `Json` in Swift. The C - `moq_publish_binary_*` calls are unchanged. -- **moq-ffi clients and servers take a config record.** `MoqClient::new()` and - `MoqServer::new()` plus their setters are `MoqClient::new(MoqClientConfig)` and - `MoqServer::new(MoqServerConfig)`, with nested `tls`, `quic`, `websocket`, and - `backoff` records and a new `versions` list. A value the native side cannot use - fails construction with `MoqError::Config`, including a bad bind address that - used to fail later as `Bind`. The `subscribe` origin is `consume` everywhere: - Python's `Client(..., subscribe=)` is `consume=`, Go's `WithSubscribeOrigin` is - `WithConsumeOrigin`. A server request's `set_publish` / `set_consume` are - arguments to `accept(publish, consume)`, where null inherits the server's - origin (Go: `Accept(ctx, nil, nil)`). Go's `Requests(ctx)` iterators are - `All(ctx)`, and its `Status*` constants are `ConnectionStatus*`; Kotlin and - Dart read announcements as `announced(config).updates()`. -- **moq-ffi media lives in a `media` namespace.** The broadcast's - `publish_audio`, `publish_video`, `publish_container`, their `_on_track` and - `_stream` variants, `set_video_properties`, `set_catalog_section`, - `subscribe_catalog`, `subscribe_media`, and `fetch_media_group` are gone. - Construct `MoqMediaTrackProducer::audio` / `video` (from the broadcast, a - `MoqMediaTarget::Named` or `Requested` target, and the init record), - `MoqMediaContainerProducer`, `MoqMediaCatalogProducer`, - `MoqMediaCatalogConsumer::subscribe`, `MoqMediaTrackConsumer::subscribe`, - or `MoqMediaGroupConsumer::fetch` instead. Producers drop `name`, - `used`, and `unused`; read them through `demand()`. The wrappers expose these - as `moq.media` in Python, `moq.dev/moq/media` in Go, `dev.moq.media` in - Kotlin, `package:moq/media.dart` in Dart, and `Media` in Swift. Go fetches a - group with `media.FetchGroup`; its `NewGroupConsumer` decodes - a raw group instead. -- **moq-ffi error payloads are named.** `MoqError` cases that carried a - positional string (`Transport`, `Internal`, `Connect`, ...) name it - `reason` (`Reason` in Go, the `reason:` label in Swift). The Display text is - unchanged. -- **Python and Go durations are native.** `Frame`, `Datagram`, `Subscription`, - `TrackInfo`, and `ConnectionStats` carry `timedelta` in Python and - `time.Duration` in Go instead of microsecond integers: `max_delay_us` is - `max_delay`, `timestamp_us` is `timestamp`, and `rtt_us` is `rtt`. A frame - timestamp read from an untimed track is `None` / `nil`. A Python string - passed where a list of strings belongs (`tls_roots="ca.pem"`) raises - `TypeError` instead of splitting into characters. -- **Demand is read through `demand()`.** In Rust, `track::Producer`'s - `is_used`, `used`, `unused`, and `poll_unused` are `producer.demand().X`, - and so are `group::Producer`'s `used` and `unused`. - `track::Request::poll_unused`, `track::Dynamic::poll_unused`, and - `group::Request::poll_unused` are `demand().poll_unused`, which returns - `Poll>` instead of `Poll<()>`: an `Err` means the request closed, - so treat it as unused too. A `group::Request` no longer needs polling to be - withdrawn; the last `fetch_group` caller leaving does it, so a handler that - sees it unused just drops it. A fetch arriving after that queues a fresh - request instead of joining the abandoned one, so a handler that keeps serving - without watching `demand()` may see its `accept` return `Error::Duplicate`. - The moq-json snapshot and moq-flate `is_used()` is `demand().is_used()`. - In TypeScript, `Track.Producer`'s `used` and `unused()` are - `producer.demand().used` and `.unused()`, as are `Group.Producer`'s, and - `Allocator.reserve` takes `producer.demand()`, replacing the - `Bandwidth.Demand` interface. -- **`request_broadcast` takes an epoch.** In Rust, - `origin::Consumer::request_broadcast(path)` is - `request_broadcast(path, None)`; pass an `Epoch` instead to refuse any other - publisher instance. In TypeScript, `RequestOptions.epoch` does the same. A - dynamic route updated to another epoch now refuses the requests its handler - still holds with `Unroutable`, so a handler answering them late sees its - answer dropped. -- **`moq --hop` is removed; `--epoch` replaces it.** A redundant pair shares an - epoch, a UUIDv7 such as `uuidgen -7` prints, instead of a Hop ID: pass the - same `--epoch` (or `MOQ_EPOCH`) to both. Unlike a rename, `--hop` is now an - unknown flag and `MOQ_HOP` is silently ignored, so remove it from any - deployment: a pair still keyed on `MOQ_HOP` mints an epoch per process, and - whenever either member starts or restarts, its new epoch replaces the other's - broadcast and restarts its viewers. `--cluster-id` no longer falls - back to `--hop`, so a node that pinned its Hop ID with `--hop` passes - `--cluster-id`. Relays no longer put a random hop in front of a route that - names no publisher; it keeps its 0. -- **The `"auto"` delay is measured, not derived from RTT** (#4162). It is sized - from how late frames arrive (see [audio jitter](/concept/audio-jitter)) in - `@moq/watch` and `moq play`, which now defaults `--delay` to `auto` instead of - `100ms`. A numeric `@moq/watch` delay is taken literally instead of having - the rendition's own delay added on top. `Sync.out.jitter` now always - equals `Sync.out.delay`, and `"auto"` with no decoder registered resolves to - 0 rather than 100 ms. -- **`moq export ts`, `flv`, and `mkv` take `--delay`** (#4645). Each frame is - written that long after its decode time, on a clock that follows the - source's; the older staleness spellings are refused with the new flag - named. In Rust, moq-mux's `ts::Export::with_max_age` and the FLV and MKV - exports' `with_max_age` are `with_delay`. moq-rtmp's FLV egress releases - frames on its `max_delay` the same way. `ts::stats::Export` gains the - dropped-frame count (late frames, and the video frames then dropped waiting - for a keyframe), measured drift and out-of-tolerance count beside its - `streams` rows. It is no longer `Eq`. -- **moq-mux has no clock translators.** `clock::Anchor`, `clock::Lane`, and - `SourceMap` (#4667) are gone, along with the importers' `live()`. Publish the - source's own timestamps and let the catalog clock map them to wall time. - An importer whose first frame arrives once the clock is in use (taken with - `catalog.clock()`, published in a catalog, or pinned with - `Config::with_clock`) shifts its timestamps onto it, so its first frame lands - at now; a `with_clock` catalog no longer keeps an importer's timestamps - verbatim. Importers sharing a timestamp base reserve through one - `catalog.timebase()`. -- **moq-net owns its transport traits.** `moq_net::web_transport_trait` is - gone, and `transport::poll::{Session, SendStream, RecvStream}` no longer - extend `web_transport_trait::poll`. They carry their own `poll_*` methods, - `transport::Error`, and `transport::Stats`, and `Error::from_transport` takes - a `transport::Error`. moq-tokio's `Client` and `Server` are unchanged. A - custom transport handed straight to moq-net implements these traits; a - `moq-uring` session is wrapped with `moq_uring::transport::Session::new`. - Each session must report `binding()`: `transport::Binding::Uri` when the - transport handshake carries the request URI, or `InBand` when SETUP carries - it. The moq-tokio and moq-uring adapter constructors take this binding - explicitly; browser WebTransport adapters always report `Uri`. -- **`--cluster-mesh` and `--cluster-linger` are unknown flags.** moq-relay - 0.17 refuses them by name; later relays reject them, and TOML `mesh` and - `linger`, like any unknown setting. `MOQ_CLUSTER_MESH` and - `MOQ_CLUSTER_LINGER` are no longer read, so drop them from the environment. - In Rust, `cluster::Config` has no `mesh` or `linger` field. -- **Settings no listener reads stop startup.** A stream-only relay or `moq` - listener (TCP or Unix, no `--listen`) refuses the QUIC-only - `--listen-preferred-v4`/`-v6`, `--listen-quic-lb-id`, and pinned `tls.peers`, - and a `--listen-tls-cert`, `-key`, or `-generate` unless `--listen-tcp-tls` - serves it. `--listen-unix-allow-*` needs `--listen-unix-bind`, and - `web.https.cert`, `key`, and `root` need `web.https.listen`. `moq` without a - listener refuses `--listen-*` and `--auth-*` flags. Each used to be ignored; - drop it, or add the listener it configures. -- **Data publishes preserve optional timestamps.** `moq_net::Timed

` drops - its clock type parameter. JSON and flate snapshot/stream producers and JSON - window `push` / `pop` publish `at: None` untimed, so their track must also be - untimed. Pass a timestamp explicitly on timed tracks. Snapshot `modify(at)` - and `mutate(at, f)` carry the caller's optional time through the edit guard. - moq-mux data timestamps use the broadcast clock; convert a capture `Instant` - with `.at(catalog.clock().capture(instant)?)`. FFI data operations take an - optional timestamp in microseconds, and raw track info's null timescale now - means untimed on publish as well as receive. Python/Swift raw writes no - longer default to zero. Whole-frame media writes now require caller time; - container imports keep their existing timeline handling. `media.CatalogProducer.now()` samples and fixes the - broadcast clock for explicit local-event stamps. A supplied timestamp ahead - of now is published rather than refused. -- **Container byte-stream importers take an init record.** The bindings' - `ContainerStreamProducer` (moq-ffi's `MoqMediaContainerStreamProducer`) takes - a `ContainerStreamInit` holding the format instead of a bare format. Its - optional `timebase`, also on `ContainerInit`, shares one source's offset - across container imports; leave it unset for the previous independent offset. -- **moq-mux importers publish the catalog at their first frame.** The fMP4, - MKV, and MPEG-TS importers used to publish it at their init segment (`moov`, - `Tracks`, or the first PMT) on a provisional clock, then re-anchor it on the - first frame. They now hold it until that frame, as FLV already did, so the - first snapshot carries the final root `clock`. A reader waiting for the - catalog now waits for media of a selected track, not just the init segment: - a track `with_select` deselects doesn't release it, even if its media - arrives first. A `moov` or `Tracks` decoded after `finish()` is refused with - `fmp4::Error::MoovAfterFinish` or `mkv::Error::TracksAfterFinish`, since the - tracks it declares could never finish. -- **fMP4 export fixes its track set at the init segment.** moq-mux's - `fmp4::Error` drops `MissingVideoTrack`, `MissingAudioTrack`, and - `NoCatalogSnapshot`, and adds `TrackAdded`, `TrackChanged`, `TrackRewound`, - and `TrackUndescribed`. `fmp4::Export` and `moq export fmp4` now end with one - of these where they used to write a track missing from the moov, and a - broadcast that ends with media queued behind an undescribed track is an error - rather than an empty `Ok(None)`. Restart the export to pick up a new - rendition. -- **Subscriber staleness is max delay.** How far a group may fall behind the - live edge before a subscriber skips it is now `max_delay`, so it no longer - shares a name with a publisher's retention, which keeps `max_age`. In Rust, - `track::Subscription::max_age` and `with_max_age` are `max_delay` and - `with_max_delay`; moq-mux's `container::Consumer::set_max_age` and the fMP4, - H.264, and H.265 exports' `with_max_age` are `set_max_delay` and - `with_max_delay`; moq-audio's and moq-video's `decode::Options::max_age` is - `max_delay`, as is moq-audio's `decode::Consumer::max_age()`; and moq-rtmp's - `Play::with_max_age`, `Client::with_export_max_age`, - `listen::Config::export_max_age`, and `DEFAULT_MAX_AGE` are `with_max_delay`, - `with_export_max_delay`, `export_max_delay`, and `DEFAULT_MAX_DELAY`. In - TypeScript, `Track.Subscription`'s `maxAge` is `maxDelay`, as are - `Container.Consumer`'s `maxAge` prop and `@moq/watch`'s `Sync.out.maxAge`; - JavaScript refuses a `maxAge` key in subscription options or container consumer - props with a `TypeError` naming `maxDelay`, including when both keys are supplied - or `maxAge` is `undefined`. Untyped callers must rename it. - moq-ffi's `MoqSubscription`, `MoqAudioDecoderOutput`, and - `MoqVideoDecoderOutput` take `max_delay_us` (each binding in its own casing), - and so do C's `moq_subscription`, `moq_audio_decoder_output`, - `moq_video_decoder_output`, `moq_consume_video`, and `moq_consume_audio`. - `moq export fmp4`, `h264`, `h265`, and `rtmp` take `--max-delay`, and - refuse `--max-age`. `track::Info::max_age`, - `MoqTrackInfo.max_age_us`, and `moq import --max-age` are unchanged, as is the wire. -- **moq-relay auth takes the client-CA answer.** `auth::Config::validate` and - `init` take `client_ca: bool`, whether any listener verifies client - certificates, and `validate_client_ca` is gone. `moq --listen` with an - invalid auth config stops at startup instead of refusing every session. -- **A client CA needs a QUIC listener.** A stream-only relay or `moq` listener - (TCP or Unix, no `--listen`) refuses to start with `listen.tls.root`, which - nothing verified; moq-tokio returns `Error::MtlsUnsupported` for it. -- **moq-tokio's `Transport` names WebTransport.** `moq_tokio::server::Transport` - is `moq_tokio::Transport`, with no re-export. A WebTransport session reports - `Transport::WebTransport` (`"webtransport"` in logs) instead of `Quic`, which - now means raw QUIC only, and `Connection::transport()` reports the live - transport. The bindings' `MoqTransport` gains a `WebTransport` case, so an - exhaustive `switch` or `when` needs one more arm. -- **moq-net has no `VarInt`.** Varints are plain `u64`s: - `VarInt::decode_quic(buf)?.into_inner()` is `moq_net::varint::decode_quic(buf)?`, - and `VarInt::try_from(v)?.encode_quic(buf)` is - `moq_net::varint::encode_quic(v, buf)`, which fails past - `varint::MAX_QUIC` (2^62 - 1). -- **Opus mapping family lives only on `mapping`.** - `moq_mux::codec::opus::Config::mapping_family` is gone. Family 0 is - `mapping: None`; any other family is the mapping's own (`mapping.family()`). - Set `mapping` alone when building a surround head. The OpusHead bytes are - unchanged. -- **moq-mux TS stats live in `ts::stats`.** `ts::Stats` is - `ts::stats::Snapshot` and `ts::StreamStats` is `ts::stats::Stream`, whose - `track` is an owned `String`. `ts::Export::stats` returns - `ts::stats::Export`; feed it to `stats::Log` with `.into()`. `ts::MultipleProgramsError` is - `#[non_exhaustive]`: recover it by downcast and read `programs`. -- **@moq/publish drops `OpusConfig.usedtx`.** Chromium's DTX output shifts the - audio timeline, so Opus DTX is always off (the WebCodecs default). Remove the - field; a plain-JS caller still passing it is ignored. -- **FLV export takes a catalog stream.** `flv::Export::new` is synchronous and - takes `(source, catalog)`, like `fmp4::Export` and `mkv::Export`. - `with_catalog_format` and `with_select` are gone. Open the catalog yourself - (`source.catalog(format)`) and narrow it with `catalog::Stream::select` - before constructing the export. `moq export flv` still applies the same - rendition flags. +## Unreleased + +These land with the next breaking release. + +- **Announce restarts.** moq-lite 07 adds `ANNOUNCE_RESTART` (0x3): another + publisher instance now serves the prefix. lite-06's `ANNOUNCE_RESTART` is + named `ANNOUNCE_UPDATE`, as it always meant. On moq-lite 06 and older and + moq-transport, a restart is still an end then a start. +- **Publisher epochs ride the route** (#4942). moq-lite 07 carries the epoch + after the path in `ANNOUNCE_START`, `TRACK`, `SUBSCRIBE`, and `FETCH`. + Routes without an epoch no longer resume across routes, so default lite-06 + sessions, including cluster GOAWAY redials, lose seamless cross-route + failover until lite-07 is the default. +- **Route cost is one value** (#4661). Upgraded nodes write lite-06's cold + cost at 2^62-1, so an older relay that still ranks on `(warm, cold)` breaks + equal-warm ties against routes that crossed an upgraded node. It is only a + tie-break and ends once the fleet is upgraded. - **A track is timed or untimed, and nothing fills in a timestamp on - receive.** An untimed track (no timescale) arrives untimed, except over - moq-lite 05 and later, which can't encode absence yet and carry a send time - instead; see [untimed tracks](/concept/moq-lite#subscriptions). A write whose - timedness doesn't match its track fails with `TimestampMismatch`. - - moq-net: `track::Info.timescale` is `Option`, `None` for an - untimed track (`with_timescale` takes `impl Into>`), and - `group::{Producer,Consumer}::timescale()` return `Option`. - `Frame.timestamp`, `frame::Info.timestamp`, and `Datagram.timestamp` are - `Option`. The `write_frame`, `append_datagram`, and - `insert_datagram` calls take `impl Into>`, so passing a - `Timestamp` still compiles. - An undeclared timescale is untimed: `track::Info::default()`, - `create_track(name, None)`, and `accept(None)` no longer default to - milliseconds, and `Timescale` has no `Default`. A track you write - timestamps to declares one, such as - `Info::default().with_timescale(Timescale::MILLI)`. - - moq-e2ee: `Frame` and `Datagram` timestamps are optional, and its writers - take `impl Into>`. - - moq-archive: recording an untimed track fails with `Error::Untimed`, so - `moq export archive` over moq-lite before 05 or moq-transport drafts - 14–16 now fails instead of recording arrival times. - - moq-ffi: `MoqFrame.timestamp_us` and `MoqDatagram.timestamp_us` are - optional, null on a frame read from an untimed track, and default to null. - A raw track you publish is timed, so a raw write without one now fails - with `TimestampMismatch` instead of going out at 0. - `MoqMediaProducer::write_frame` without one used to pass a PTS of 0; it - now passes none, so the importer derives the time itself (elapsed since - its first frame) or refuses a codec that needs one (avc1, hvc1). - `MoqTrackInfo.timescale` is null on a received untimed track. In Go, - `Frame.TimestampUs` and `Datagram.TimestampUs` are `*uint64`; in Kotlin - and Dart, `Frame.timestamp` and `Datagram.timestamp` are nullable. - - libmoq: `moq_frame` and `moq_datagram` gain `timestamp_present`, which - changes their size, so recompile against the new `moq.h`. - - Receivers: tracks from moq-lite before 05, and moq-transport tracks - without `TIMESCALE` (every track on drafts 14–16), arrive untimed instead - of stamped with their arrival time; an object-scope Timescale is ignored. - On a moq-transport track with `TIMESCALE`, an object without a Timestamp - is malformed. A new subscriber with no start on an untimed track starts at - the latest group. -- **CMAF decodes at the frame timestamp.** A fragment's earliest sample - presents at its moq-net frame timestamp; `tfdt` only orders the samples. In - TypeScript, `Container.Format.decode` and `Cmaf.decodeDataSegment` take that - timestamp: pass `frame.timestamp` alongside `frame.payload`, or `undefined` - for an untimed frame, whose samples present at `tfdt`. - `Cmaf.decodeTimestamp` is gone; the frame timestamp is the fragment's time. - moq-mux's CMAF `Wire::write` refuses a track whose timescale isn't the init's - `mdhd` timescale with `fmp4::Error::TimescaleMismatch`. `import::Track` and - `TrackStream` accept a CMAF rendition's track at that timescale; a track you - create yourself declares it with `track::Info::with_timescale`. Both decoders refuse a `trun` whose - `data_offset` doesn't start at the next sample in the `mdat`. -- **@moq/json and @moq/flate consumers return `Timed` values.** The snapshot - and stream consumers' `next()` and async iterator yield `{ value, at }`, - where `at` is the frame's timestamp and absent on an untimed track; read - `.value` for what they used to return. A snapshot consumer's `next()` now - yields every state in order. Call `latest()` for the old behavior, which - skips to the newest state. A caller that passes the result on as `any` or - `unknown` (logging, `JSON.stringify`, a generic setter) still compiles but - now gets the wrapper and every intermediate state, so search for each - `next()` and `for await` over a snapshot consumer rather than relying on - type errors. On an untimed track nothing marks a group stale, so a `next()` - reader that falls behind replays the whole backlog where it used to skip it. -- **`moq export ts --delay` defaults to 1 s and includes the send-ahead.** Each - frame still goes out up to the delay ahead of its decode time, now no further - than its decoder buffer holds, and that comes out of the same budget: the - output trails a TS source by the delay instead of twice it. With a multiplex - rate, video whose HRD declares a longer buffer delay than `--delay` stops the - export with the delay it needs named, so a broadcast feed run at - `--delay 500ms` needs `--delay 1s`. A source that sends every track at its - decode time (RTMP, a browser) trails by the delay plus the video's - send-ahead: 2 s at the new default, where `--delay 500ms` keeps the 1 s it - had. moq-srt egress runs at its SRT latency or 1 s, whichever is longer. + receive.** Tracks from moq-lite before 05, and moq-transport tracks without + `TIMESCALE` (every track on drafts 14 to 16), arrive untimed instead of + stamped with their arrival time. moq-lite 05 and later can't encode absence + yet and carry a send time instead; see + [untimed tracks](/concept/moq-lite#subscriptions). A new subscriber with no + start on an untimed track starts at the latest group. +- **Max age and max delay count wall-clock time too.** A superseded group is + stale once its successor arrived a full budget ago, as well as by media + time, so an untimed track is no longer exempt. With the default zero + `max_delay`, an untimed subscriber starts at the latest group and skips each + superseded one; give it a budget to replay or tolerate reordering. -## Wire +## Released Older protocol versions still negotiate, so relays and clients can be upgraded -in any order, apart from [pattern-only token grants](#relay-and-cli) and two wire -changes: +in any order, apart from these: - The lite 06 ALPN is `moq-lite-06`, not `moq-lite-06-wip`. An explicit `moq-lite-06-wip` in a version list is refused (#3941). @@ -453,202 +62,16 @@ changes: a root `clock: { wall, timescale }` (#3612, #3675). A new `moq export hls` finds no timeline in an old publisher's catalog, so upgrade publishers before the HLS gateway. - -## Relay and CLI - -The `moq-relay` and `moq` flags split into `--listen-*` (accepting), -`--connect-*` (dialing), and a shared `--quic-*` section. Environment -variables follow the flag (`MOQ_SERVER_BIND` is `MOQ_LISTEN`). - -| Before | After | -| --- | --- | -| `--server-bind` | `--listen` | -| `--server-*`, `--tls-cert`, `--tls-key`, `--tls-generate` | `--listen-*`, `--listen-tls-cert`, `--listen-tls-key`, `--listen-tls-generate` | -| `--client-connect` | `--connect` | -| `--client-*` | `--connect-*` | -| `--client-failover-delay` | `--connect-race` | -| `--client-reconnect=false` | `--connect-once` (inverted) | -| `--tls-disable-verify`, `--client-tls-disable-verify` | `--connect-tls-insecure` | -| `--server-quic-*`, `--client-quic-*` | `--quic-*`, applied to both directions | -| TOML `[server]`, `[client]` | `[listen]`, `[connect]` | -| TOML `[server.quic]`, `[client.quic]` | `[quic]` | -| TOML `listen`, `connect`, `failover_delay`, `reconnect`, `disable_verify` | `bind`, `url`, `race`, `once` (inverted), `insecure` | -| `--cluster-linger` | removed; a broadcast closes when its last publisher is lost | -| `--cluster-connect host:port` | a full URL, `https://host/?jwt=TOKEN` | -| `--cluster-mesh`, TOML `mesh` | removed; list every peer with `--cluster-connect` or `--cluster-connect-api` | -| `moq --origin`, `--name`, `--latency-max` | `--hop` (removed for `--epoch` after [Unreleased](#unreleased)), `--broadcast`, `--max-age` | -| `moq publish`, `moq subscribe` | `moq import`, `moq export` | -| `moq token`, the `moq-token` binary | `moq auth` | - -Other changes to a deployment: - -- **Auth is one contract** (#3688). The relay asks an auth server per session - (`--auth-url`) or applies a static anonymous grant (`--auth-public`); exactly - one is required. `--auth-key`, `--auth-key-dir`, `--auth-api`, - `--auth-api-mode`, `--auth-public-api`, `--auth-domain`, `--auth-mtls-tier`, and `--auth-tls-*` - are gone: run `moq auth serve --key-dir ...` next to the relay and point - `--auth-url` at it. The flag-by-flag mapping is in - [Migrating from the relay flags](/bin/relay/auth#migrating-from-the-relay-flags). -- **Grants are patterns, not prefixes.** `anon` is now exactly the broadcast - `anon`; write `anon/**` for the subtree. This applies to `--auth-public`, - TOML `public`, and the `[auth.public]` table, which is now - `public_subscribe` / `public_publish`. A public or mTLS pattern with no - wildcard refuses to start, naming the subtree to write, rather than pick - one reading silently. The patterns are rooted at `/`, as in 0.14, so - `anon/**` admits a client dialed at `/anon` and refuses one dialed outside - `anon/`. Earlier 0.15 releases rooted them at the dialed path instead. -- **Token grants are patterns.** JWT `publish` and `subscribe` claims are - patterns, so a token granting `alice` covers only `alice`; sign `alice/**` - instead. Existing `put`/`get` tokens and key scopes keep working as subtrees, - and subtree-only grants are still signed in that form, so a `moq-token` - deployment can upgrade issuers and verifiers in either order. Verifiers on - the pattern-only `moq-auth` 0.1.0/0.1.1 or `@moq/auth` 0.1.x/0.2.0 refuse - that form, so upgrade them before their issuers. Grants only a pattern can - express need an upgraded verifier. -- **Removed auth settings refuse to start.** 0.14's `[auth]` `key`, `key_dir`, - `auth_api`, `domains`, `mtls_tier`, and `[auth.tls]`, and their flags and - `MOQ_AUTH_*` variables, stop the relay with the replacement named rather - than being ignored. -- **Token claims.** `iss`, `sub`, and `jti` are ignored and `nbf` is enforced. - Any other claim refuses the token with its name, `aud` and `cluster` - included, where 0.14 ignored all but `aud`: an issuer adding app claims such - as `user_id` must drop them. -- **Every credential is evaluated or refused.** A relay on `--auth-public` - refuses a session presenting a token, as 0.14 did, including peers sending - `cluster.token`, and refuses to start with a client CA. `moq auth serve` - refuses a session presenting both a JWT and a certificate, so a peer - presents one or the other. -- **`moq auth serve` never re-checks or expires by default**, as 0.14 never - did. `--revalidate` needs `--expires`, and `--limit-*` needs `--revalidate`. -- **mTLS admits nothing on its own.** A verified client certificate is reported - to the auth server, which grants it. `moq auth serve --mtls-publish '**' --mtls-subscribe '**' --mtls-peer` restores the old full access for every certificate - the relay's client CA verifies, as a cluster peer, so keep that CA to cluster peers. -- **`moq --listen` needs auth.** A CLI listener refuses to start without - `--auth-url` or `--auth-public` instead of accepting everyone. -- **Gossip discovery is removed.** A relay dials only the peers it lists or - finds on the LAN, never a URL learned from an announcement, and no longer - announces `.internal/origins`. The `/nodes` endpoint lists only peers this - relay dialed. Until every relay that ran `--cluster-mesh` is upgraded, keep - client grants off `.internal/`: an older relay still dials any URL announced - there with `cluster.token`, and an upgraded peer still forwards it. -- **Other 0.14 auth differences kept.** Peers identify by certificate or LAN - path. An auth server's `root` alias may have any depth. - A path in `--cluster-connect` is not refused, although it shifts the mesh - frame. `moq auth serve --key` takes a file, not an https or JWKS URL. - `moq auth sign --root` is the token root and JS `verify --root` the dialed - path. `/.cluster*` roots are reserved. `--auth-public a,b` splits on commas. -- **noq is the only QUIC stack** (#3811). The `quinn` and `quiche` cargo - features and the backend setting are gone. +- **Token grants are patterns.** Existing `put`/`get` tokens and key scopes + keep working as subtrees, and subtree-only grants are still signed in that + form, so a `moq-token` deployment can upgrade issuers and verifiers in either + order. Verifiers on the pattern-only `moq-auth` 0.1.0/0.1.1 or `@moq/auth` + 0.1.x/0.2.0 refuse that form, so upgrade them before their issuers. Grants + only a pattern can express need an upgraded verifier. +- **Gossip discovery is removed.** Until every relay that ran + `--cluster-mesh` is upgraded, keep client grants off `.internal/`: an older + relay still dials any URL announced there with `cluster.token`, and an + upgraded peer still forwards it. - **Stats counters** are `*_started` / `*_ended` (`sessions_started`, - `announces_ended`, ...). This release still writes the old `announced` / + `announces_ended`, ...). Relays still write the old `announced` / `*_closed` names beside them, so move consumers now. - -## GStreamer - -- `moqsink` properties `estimated-send-bitrate` / `estimated-recv-bitrate` are - `estimated-send-rate` / `estimated-recv-rate`, with no alias; a `gst-launch` - line naming the old ones fails at runtime. - -## Rust - -- **moq-native is moq-tokio** (#2896). moq-native 0.20.0 is a stub that fails - to compile with the rename. `ClientConfig::default().init()?` is - `moq_tokio::connect::Config::default().init(quic)?`, and - `with_publisher(&origin).with_subscriber(origin)` is `with_origin(origin)`. - Names sit under their modules (`connection::Goaway`, `connection::Monitor`, - `transport::Session`; #3745). -- **moq-token is moq-auth** (#3684). `Claims` holds `Patterns`, and - `Claims::authorize` returns pattern residuals. -- **Origins** (#3400, #3804). `Origin::random().produce()` is - `moq_tokio::origin::spawn()`. `create_broadcast(path, route)` is - `origin.publish(path, route)`, or `create_broadcast(path)` then - `broadcast.announce(route)`. `with_root` plus `scope(prefixes)` is one - `scope(root, &patterns)` returning `Result`. `origin::Info` is - `origin::Config`. -- **Announcements are prefix routes** (#3225, #3770). `announce::Update` is - `{ prefix, route, kind, captures }`: skip `!update.kind.is_active()` and - resolve the broadcast with `consumer.request_broadcast(&update.prefix, update.route.epoch)`. - Serve a subtree on demand with `origin.dynamic(prefix, route)`. -- **Tracks.** `with_latency_max` / `latency_max` is `with_max_age` / `max_age`. - `write_datagram(Datagram)` is `insert_datagram(sequence, timestamp, payload)` - (#3666); `append_datagram` is unchanged. `track::SubscriberControl` is - `track::Control`, `track::GroupRequest` is `group::Request`, and - `ConnectionStats` is `session::Stats` with `estimated_send_rate` / - `estimated_recv_rate`. -- **Oversized groups abort** with `GroupTooLarge` instead of shedding their head - (#3585). -- **Catalog edits are fallible** (#3644, #3813). moq-mux and moq-json `lock()` - is `modify()?`, and a guard that fails to publish on drop aborts the - track. `timeline::Config::wall` is the broadcast `Clock`. -- **moq-json config** (#3718). `compression: bool` is a `Compression` enum; - track-owning options are `producer::Config` / `consumer::Config`. -- **moq-mux imports are typed.** `import::Init` splits into `AudioInit`, - `VideoInit`, and `ContainerInit`, with typed formats instead of strings, and - `Track::new` is `Track::audio` / `Track::video`. -- **moq-relay embedding** (#3638). `Relay` fields are private: clone the - handles you need, mount routes, then call `Relay::run`. - `Cluster::with_cache` moved to `cluster::Options`. - -## JavaScript - -The JavaScript packages have no changelog; this list follows the breaking -PRs, so a minor rename may be missing. - -- **@moq/token is @moq/auth.** `sign` / `verify` are `Key.sign` / `Key.verify`, - and claims are pattern unions (see [Token grants are patterns](#relay-and-cli)). -- **One `Connection`** (#3614, #3636). `Connection.Reload` is - `new Moq.Connection({ url })`, which pools one connection per relay. `closed` - settles only on `close()`; the error that stopped retrying is `error`. -- **Origins hold broadcasts** (#2705, #3225). `connection.publish(path, - broadcast)` is `origin.createBroadcast(path)` then `broadcast.announce()`, and - `connection.announced(prefix)` is `origin.announced(scope)` yielding - `{ prefix, kind, route }`. `ignoreSelf` is gone: reflected announces - are always dropped. -- **Names mirror Rust** (#3710). `latencyMax` is `maxAge` (a `Time.Milli`), - `writeDatagram` is `insertDatagram`, and `RemoteError` is `Error.Stream` / - `Error.Session`. -- **@moq/watch** (#3396, #3817). `latency`, `latency-min`, `latency-max`, and - `jitter` are `delay` and `buffer`, which need a unit (`delay="100ms"`, - `buffer="30s"`). `reload` is `announced`. `Watch.Broadcast({ connection })` - is `Watch.Player({ origin: connection.origin, ... })`. -- **@moq/publish** takes `origin: connection.origin` instead of a - `connection` signal. -- **@moq/json and @moq/binary** (now `@moq/flate`) take one options object (#3640): - `new Json.Snapshot.Consumer({ track })` instead of `(track, config)`. -- **@moq/hang** reads the catalog `archive` entry instead of `timeline`. - -## Bindings - -Python (`moq-rs` 0.5.0), Swift (0.5.0), Kotlin (`dev.moq:moq` 0.5.0), and Go -wrap moq-ffi 0.4. These are the moq-ffi names; each wrapper follows them in its -own casing: - -- **Go module path** is `moq.dev/moq` (was `github.com/moq-dev/moq-go/moq`). -- **Publish split by kind.** `publish_media*` is `publish_audio`, - `publish_video`, or `publish_container`, each taking its own init. The raw - encoder paths that used to be `publish_audio` / `publish_video` are - `encode_audio` / `encode_video`. -- **Durations are microseconds** (`max_age_us`, `MoqBackoff.initial_us`), and - rate estimates are `estimated_send_rate` / `estimated_recv_rate`. -- **Setters are fallible** (#3642). Client and server configuration setters - return an error (`Busy` during connect or listen) instead of dropping the - value. `set_tls_disable_verify(bool)` is `set_tls_verify(bool)`. -- **Announcements.** `MoqAnnounced` is `MoqAnnounceConsumer` and - `MoqAnnouncement` is `MoqAnnounceUpdate`; `MoqBroadcastRequest::abort` is - `reject`, and `MoqOriginOptions` is `MoqOriginConfig`. -- **`MoqAudioCodec`** is an `opus()` object (#3671). -- **Errors.** `MoqError::Protocol` carries a `MoqProtocolError` (scope, wire - code, kind) instead of a flattened message. -- **Track and group `finish()`** keeps the handle open so a later `abort()` can - still run. - -C (libmoq 0.6): - -- The 41 `moq_client_*` setters are one zero-initializable `moq_client_config`, - whose durations are `_us`. -- `moq_publish_media` is `moq_publish_audio`, `moq_publish_video`, or - `moq_publish_container`; the raw encoders are `moq_encode_audio*` / - `moq_encode_video*`. Formats are enums instead of strings. -- `moq_announced` is `moq_announce_update`, `moq_origin_consume_announced` is - `moq_origin_announced_broadcast`, and `moq_broadcast_request_abort` is - `moq_broadcast_request_reject`. diff --git a/rs/moq-cli/MIGRATE.md b/rs/moq-cli/MIGRATE.md index 8b799e4424..d16e11a19d 100644 --- a/rs/moq-cli/MIGRATE.md +++ b/rs/moq-cli/MIGRATE.md @@ -49,6 +49,7 @@ Recording a track with no timestamps fails with `Untimed`, so an archive over mo A rendition added, changed, or left undescribed after the init segment ends the export with an error instead of writing a track missing from the moov. Restart the export to pick up a new rendition. +Annex-B H.264 and H.265 sources get an `avc3` or `hev1` init from the catalog when it carries enough, and a keyframe whose parameter sets never appeared ends the export instead of waiting 30 seconds. ### `moq play --delay` defaults to `auto` (#4162) diff --git a/rs/moq-mux/MIGRATE.md b/rs/moq-mux/MIGRATE.md index dc59d93630..fe10fba2b0 100644 --- a/rs/moq-mux/MIGRATE.md +++ b/rs/moq-mux/MIGRATE.md @@ -118,6 +118,11 @@ Delete the calls. `fmp4::Export` ends with one of these where it used to write a track missing from the moov, and a broadcast that ends with media queued behind an undescribed track is an error instead of `Ok(None)`. Restart the export to pick up a new rendition. +### fMP4 export of Annex-B H.264 and H.265 starts from the catalog + +When the catalog codec string and dimensions are enough, the export writes an `avc3` or `hev1` init before the first keyframe and leaves the parameter sets in the samples. +A keyframe whose parameter sets never appeared ends the export instead of waiting 30 seconds. + ### CMAF timescales must match the init CMAF `Wire::write` refuses a track whose timescale is not the init's `mdhd` timescale with `fmp4::Error::TimescaleMismatch`. From 4e53df734147a9f10735edc21a1785cff48b722b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 16:50:36 -0700 Subject: [PATCH 09/40] docs: tell breaking PRs to add a MIGRATE.md entry; complete the quest AGENTS.md and CONTRIBUTING.md point breaking changes at the package MIGRATE.md files, active quests stop sending entries to upgrade.md, and the migrate quest is deleted now that its goal holds. Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 1 + CONTRIBUTING.md | 3 +- quest/m1/README.md | 1 - quest/m1/ffi-shape/README.md | 2 +- quest/m1/ffi-shape/stream-producer-name.md | 2 +- quest/m1/json-window-timestamps.md | 2 +- quest/m1/migrate.md | 62 ---------------------- quest/m2/release/README.md | 5 +- quest/m2/release/c-retire.md | 2 +- rs/moq-relay/MIGRATE.md | 2 +- 10 files changed, 11 insertions(+), 71 deletions(-) delete mode 100644 quest/m1/migrate.md diff --git a/AGENTS.md b/AGENTS.md index 667d372704..771d3380e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,7 @@ The API is the most important thing to get right. A bad shape costs a breaking change in every language, and the surface is huge. - Report the public API and wire impact of every change, in the PR description and whenever asked. +- A breaking change adds an entry under `## Unreleased` in each affected package's `MIGRATE.md`, with a before/after snippet in that package's language. - Keep things private until a consumer needs them. Scrutinize every new exported item. - Never add `foo_with_x`, `foo_checked`, or a compatibility shim. Make the breaking change to `foo` instead. - Let the type system make misuse unrepresentable diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d168b341ce..e1fd6f1883 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,7 @@ Have at least these sections: - **Problem**: a summary of the problem and why this PR is needed. - **Approach**: a summary of the approach taken to solve the problem. -- **Impact**: a bullet point for every public API/wire change made. +- **Impact**: a bullet point for every public API/wire change made. A breaking one also gets a `MIGRATE.md` entry in each affected package. - **Alternatives**: any alternative approaches considered. - **Follow-ups**: any issues encountered or quests created. @@ -83,6 +83,7 @@ For an advisory against Quinn, follow the advisory triage in [`rs/moq-quic/READM `main` is the trunk; `release` is what ships, and release-plz and every branch-triggered publish run only there. - A release is cut by hand: a PR merging `main` into `release`, with a merge commit. +- A breaking cut first renames, on `main`, each changed `MIGRATE.md`'s `## Unreleased` to its version transition (`## 0.3 -> 0.4`), leaving an empty `## Unreleased` above it. - Suggest a `release` backport for any serious bug fix (crash, security, data loss, broken interop), whether found on `main` or reported against a release. Land it on `main` first, then port it onto `release` as a separate PR that keeps the published API, so it ships as a patch. - After any release publishes, patch or breaking, run `just test wire-compat --remeasure` on `main` and land the rewritten `test/interop/compat/planned-breaks.json` with refreshed reasons. Otherwise the nightly wire-compat check fails on the stale pins. - Nothing merges `release` into `main` automatically. Before cutting a minor or breaking release, the maintainer merges `release` into `main` once, so trunk carries the published versions, CHANGELOGs, and any `release`-only commits. It lands as a merge commit with `--admin`, never squashed, so the cut starts from an advanced merge base. diff --git a/quest/m1/README.md b/quest/m1/README.md index 9162652ca9..aa8f0030a0 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -48,7 +48,6 @@ explicit exception to the public model audit. - [Release builds](/quest/m0/release-link.md) - package artifacts can be built - [qmux credit](/quest/m0/qmux-credit.md) - dropped/stopped streams return credit and close is delivered -- [Per-package MIGRATE.md](/quest/m1/migrate.md) - every published package documents how to migrate each breaking change, and breaking PRs add their entry - [Restart followers](/quest/m1/restart-followers.md) - first-party consumers follow an epochless `Restart`, from a GOAWAY drain or a publisher replacement, by resolving the path again - [Catalog version](/quest/m1/catalog-version.md) - the hang catalog names the minimum moq-hang draft a reader needs, and readers refuse a newer one with a typed error - [Cluster routing](/quest/m1/cluster-routing/README.md) - prove shared routing contracts, implement per-origin routes, and connect outbound local clusters diff --git a/quest/m1/ffi-shape/README.md b/quest/m1/ffi-shape/README.md index b361218d61..796a631617 100644 --- a/quest/m1/ffi-shape/README.md +++ b/quest/m1/ffi-shape/README.md @@ -81,7 +81,7 @@ Settled shape: Each child reshapes one group end to end: moq-ffi, all five wrappers, the C++ package and `cpp/obs`, the `doc/lib` samples per the cross-package table, -and its entries in the Unreleased section of `doc/setup/upgrade.md`. Each +and its entries under `## Unreleased` in each affected package's `MIGRATE.md`. Each runs `just test interop --all`. ## Required diff --git a/quest/m1/ffi-shape/stream-producer-name.md b/quest/m1/ffi-shape/stream-producer-name.md index 88d43078cf..d5fc043e3b 100644 --- a/quest/m1/ffi-shape/stream-producer-name.md +++ b/quest/m1/ffi-shape/stream-producer-name.md @@ -17,5 +17,5 @@ had owned this review. Ask with 2-3 candidate names and a recommendation. A rename touches moq-ffi, every wrapper, `cpp/moq`'s alias list, the `doc/lib` samples, and the -Unreleased section of `doc/setup/upgrade.md`; keeping the name deletes this +`## Unreleased` of each affected `MIGRATE.md`; keeping the name deletes this quest. diff --git a/quest/m1/json-window-timestamps.md b/quest/m1/json-window-timestamps.md index b3de92da6a..e878e3687b 100644 --- a/quest/m1/json-window-timestamps.md +++ b/quest/m1/json-window-timestamps.md @@ -20,7 +20,7 @@ stream consumers `Timed`. The window consumer (`window::Consumer::next` in - Decided: Rust and JS together, same names and shape. Update moq-ffi and the bindings if they expose the window consumer (see the Cross-Package Sync table). - Update in-repo readers (`js/hang` timeline, `js/room` chat) and add a line to - the Unreleased section of `doc/setup/upgrade.md`. No new doc page. + `## Unreleased` of each affected package's `MIGRATE.md`. No new doc page. - Land before the next `@moq/json` release, so its break ships together with #5099's. Not blocked on the Rust snapshot and stream quest: both Rust breaks use the same `moq_net::Timed` and ship in the next moq-json release together. diff --git a/quest/m1/migrate.md b/quest/m1/migrate.md deleted file mode 100644 index 26fe90d3b6..0000000000 --- a/quest/m1/migrate.md +++ /dev/null @@ -1,62 +0,0 @@ -# [M] Per-package MIGRATE.md - -## Goal - -Every hand-written published package carries a `MIGRATE.md` that tells a -downstream user how to update their code, config, or invocation for each -breaking change, and AGENTS.md and CONTRIBUTING.md tell every breaking PR to -add its entry there. Entries for the breaks already on `main` since the last -release are in place before the breaking cut. - -Non-goals: wire-format and rollout guidance for operators (stays in -`doc/setup/upgrade.md`), and CI enforcement on PRs into `main` (the -[API-diff check](/quest/m2/release/migrate-audit.md) on PRs into `release` -catches misses). - -## Plan - -Decided 2026-10-16 (maintainer interview): - -- Audience is downstream users upgrading across a breaking release. - `CHANGELOG.md` stays release-plz generated and untouched. -- Every hand-written published package gets a `MIGRATE.md` now, even if - empty: the published Rust crates, the `@moq/*` packages with a release - script, `rs/moq-ffi`, and the binding wrappers (`py/moq-rs`, `kt/moq`, - `dart/moq`, `go/wrapper`, `swift`, `cpp/moq`, `cpp/obs`). Generated FFI - layers (`py/moq-ffi`, `kt/moq-ffi`, `dart/moq_ffi`, `go/ffi`, `swift/ffi`) - get none: they have no hand-written API. -- Layout: `## Unreleased` on top, newest first; the release cut renames it to - the version transition (e.g. `## 0.3 -> 0.4`) and history stays. One - before/after snippet per break. -- Each binding wrapper's file shows the change in its own language, matching - the Cross-Package Sync table that already makes a `moq-ffi` change touch - every wrapper. -- Binaries (`moq-relay`, `moq-cli`, `moq-gst`, `obs`, ...) use the same file; - entries map an old flag, env var, or config key to its replacement with an - example invocation. -- `MIGRATE.md` is the single source. Move every API entry out of - `doc/setup/upgrade.md` into its package's file; the page keeps the - cross-cutting wire and relay rollout guidance plus an index linking every - `MIGRATE.md`. `doc/lib` pages link to their package's file instead of - repeating it. -- No CI check on `main`. AGENTS.md (Public API section) and CONTRIBUTING.md - (PRs, Impact) instruct a breaking PR to add its entry; the API-diff check - on PRs into `release` is the backstop. -- Backfill from the 87 `!:` commits since the `release` merge base and the - existing upgrade.md entries, not an API diff; the release-PR check covers - unmarked breaks. As of 2026-10-16 the heaviest are moq-net/@moq/net, moq-mux, - moq-ffi and every wrapper, moq-tokio, moq-relay/moq-cli, - moq-video/moq-audio, and @moq/hang/@moq/publish/@moq/watch. -- Ranked first in m1 so breaking work landing after it carries its entry. - -Repoint every active quest that sends breaking entries to upgrade.md, so -they land in package files instead: `quest/m1/ffi-shape/README.md`, -`quest/m1/ffi-shape/stream-producer-name.md`, -`quest/m1/json-window-timestamps.md`, `quest/m2/release/c-retire.md`, and the -release epic's note that the upgrade guide describes public API -(`quest/m2/release/README.md`). Grep `quest/` for `upgrade.md` again when -landing, since other quests may add references meanwhile. - -## Related - -- [API-diff check](/quest/m2/release/migrate-audit.md) - verifies these files against the API diff on the cut PR diff --git a/quest/m2/release/README.md b/quest/m2/release/README.md index 3979f7dac2..3a28fa6ef6 100644 --- a/quest/m2/release/README.md +++ b/quest/m2/release/README.md @@ -17,8 +17,9 @@ bumps and tags happen at this cut, not in the milestone-planning PR. Run the release's compatibility, package, draft, and all-language interop checks on the candidate. Publish finalized lite-07 with first-party clients preferring it and falling back to supported older versions. Verify artifacts -from their registries before downstream pin migrations. Existing upgrade and -identity guides describe public API, wire, archive-format, and legacy limits. +from their registries before downstream pin migrations. Package `MIGRATE.md` +files describe public API breaks; the upgrade and identity guides describe +wire, archive-format, and legacy limits. Downstream staging, mixed-version upgrade/rollback, canaries, and broad rollout follow publication and require their own operator authorization. Do not mark diff --git a/quest/m2/release/c-retire.md b/quest/m2/release/c-retire.md index 3a8333924f..9b80a7a4bf 100644 --- a/quest/m2/release/c-retire.md +++ b/quest/m2/release/c-retire.md @@ -4,7 +4,7 @@ The hand-written C crate is gone: its final `moq-c` 0.7.x release points users at the generated `moq-c` 0.8.0, and the source, workflows, and docs that only served -it are deleted. `doc/setup/upgrade.md` tells C users how to move. +it are deleted. The generated crate's `MIGRATE.md` tells C users how to move. ## Plan diff --git a/rs/moq-relay/MIGRATE.md b/rs/moq-relay/MIGRATE.md index 10100d69ce..cce10ccc63 100644 --- a/rs/moq-relay/MIGRATE.md +++ b/rs/moq-relay/MIGRATE.md @@ -61,7 +61,7 @@ config.auth.validate_client_ca()?; config.auth.validate(client_ca)?; ``` -### Embedding: io_uring workers bind from the server config (#5312) +### Embedding: io\_uring workers bind from the server config (#5312) `uring::Workers::bind` takes a `moq_tokio::server::Config`, so generated and rotating certificates work in both worker modes. From 916e363892dc52ec731a614779b9e058cf860d4a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 17:06:41 -0700 Subject: [PATCH 10/40] quest: record the settled MIGRATE.md decisions The generated moq-c package ships its own MIGRATE.md, later release transition sections and snippet compile-checks go to the migrate audit. Co-Authored-By: Claude Opus 5.5 --- quest/m1/c/README.md | 6 +++--- quest/m1/c/package.md | 3 +++ quest/m2/release/c-retire.md | 2 +- quest/m2/release/migrate-audit.md | 7 ++++++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/quest/m1/c/README.md b/quest/m1/c/README.md index 2e16a2efd9..debcba5aba 100644 --- a/quest/m1/c/README.md +++ b/quest/m1/c/README.md @@ -34,8 +34,8 @@ Decided: `moq-c` (`rs/moq-c`), and the generated package takes over that name and `moq::c` target, so C users migrate once. Its first release is 0.8.0, a minor bump over the hand-written 0.7.x. -- Docs change inline: the consumer quest rewrites `doc/lib/c`, and retirement - adds an upgrade note. No separate guide. +- Docs change inline: the consumer quest rewrites `doc/lib/c`, and the + package's `MIGRATE.md` carries the move. No separate guide. The line owns the pre-cut check: every `doc/lib/c` sample and the C interop client build and run against the built release archive, not only in-tree. @@ -48,7 +48,7 @@ and fetch quests were abandoned for this line, and hidden is done. Decided in the quest audit: [claim epochs](/quest/m1/claim-answer-epochs.md) is a narrow correctness exception. Its existing claim API migrates with Rust and JS so C cannot retain the unsafe accept path while this line waits. -That change owns its C tests and upgrade notes; other API reshaping stays here. +That change owns its C tests and `MIGRATE.md` entries; other API reshaping stays here. ## Required diff --git a/quest/m1/c/package.md b/quest/m1/c/package.md index db3016200a..310b6ed415 100644 --- a/quest/m1/c/package.md +++ b/quest/m1/c/package.md @@ -21,6 +21,9 @@ under the same names. - A release workflow and nightly/CI jobs follow the C++ package's, on the same generator pin; `v0.11.0-kixelated.4+v0.32.2` already carries the C backend (`--lang c`). +- Ship a `MIGRATE.md` with the package, since it is what C users install. + Carry over `rs/moq-c/MIGRATE.md`'s history and add an `## Unreleased` + entry mapping the hand-written ABI to the generated one. - Ship a hand-written `moq_shutdown` that calls `moq_ffi_shutdown`, then `moq_shutdown_dispatcher`. - moq-c and moq-cpp each embed the moq-ffi staticlib, so one program can't link diff --git a/quest/m2/release/c-retire.md b/quest/m2/release/c-retire.md index 9b80a7a4bf..1c4f12b2e9 100644 --- a/quest/m2/release/c-retire.md +++ b/quest/m2/release/c-retire.md @@ -4,7 +4,7 @@ The hand-written C crate is gone: its final `moq-c` 0.7.x release points users at the generated `moq-c` 0.8.0, and the source, workflows, and docs that only served -it are deleted. The generated crate's `MIGRATE.md` tells C users how to move. +it are deleted. The generated `moq-c` package's `MIGRATE.md` tells C users how to move. ## Plan diff --git a/quest/m2/release/migrate-audit.md b/quest/m2/release/migrate-audit.md index 5caee2ad03..d62a40c685 100644 --- a/quest/m2/release/migrate-audit.md +++ b/quest/m2/release/migrate-audit.md @@ -38,7 +38,12 @@ Decided 2026-10-16 (maintainer interview): cut's audit also reads the diff of those paths by hand. Per-language wrapper checks are out of scope until a missed break shows they are needed. - For this cut, fill every gap the report and the manual pass show in the - matching `MIGRATE.md`. + matching `MIGRATE.md`. Known gaps: breaking releases cut after the + 2026-09-23 train have no transition section yet (moq-json 0.4 -> 0.5, + moq-auth 0.1 -> 0.2, moq-flate 0.1 -> 0.2, and possibly moq-audio and + moq-video), because their breaks predate the backfill's merge base. +- Compile-check the after snippets of every `MIGRATE.md` section the cut + ships against the candidate. Nothing else builds them. - Required by the cut, after m1's breaking work has landed. ## Required From 5ee34d336cd2087de5c37b3db1abb279c3d24380 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 18:04:39 -0700 Subject: [PATCH 11/40] docs: before/after snippets for every rewritable MIGRATE.md break Add a minimal snippet to each prose-only entry that has code, flags, or config to change; pure runtime behavior changes keep prose only. Backfill moq-auth 0.1 -> 0.2, qualify which guide raw UniFFI users follow, link the binary guides from doc/bin, and apply the review's wording fixes. Co-Authored-By: Claude Opus 5.5 --- dart/moq/MIGRATE.md | 67 ++++++++++++++++++++- doc/bin/cli.md | 5 ++ doc/bin/relay/index.md | 5 ++ doc/setup/upgrade.md | 10 ++-- go/wrapper/MIGRATE.md | 65 ++++++++++++++++++++- js/hang/MIGRATE.md | 9 +++ js/msf/MIGRATE.md | 7 +++ js/net/MIGRATE.md | 10 +++- js/publish/MIGRATE.md | 7 +++ js/room/MIGRATE.md | 7 +++ js/watch/MIGRATE.md | 7 +++ kt/moq/MIGRATE.md | 97 +++++++++++++++++++++++++++++++ py/moq-rs/MIGRATE.md | 58 ++++++++++++++++++ quest/m2/release/migrate-audit.md | 4 +- rs/moq-auth/MIGRATE.md | 20 +++++++ rs/moq-c/MIGRATE.md | 32 ++++++++++ rs/moq-cli/MIGRATE.md | 51 +++++++++++++++- rs/moq-clock/MIGRATE.md | 7 +++ rs/moq-e2ee/MIGRATE.md | 9 +++ rs/moq-ffi/MIGRATE.md | 86 +++++++++++++++++++++++++++ rs/moq-hls/MIGRATE.md | 7 +++ rs/moq-msf/MIGRATE.md | 6 ++ rs/moq-mux/MIGRATE.md | 34 ++++++++++- rs/moq-net/MIGRATE.md | 7 +++ rs/moq-relay/MIGRATE.md | 74 +++++++++++++++++++++++ rs/moq-rtc/MIGRATE.md | 5 -- rs/moq-srt/MIGRATE.md | 2 +- rs/moq-tokio/MIGRATE.md | 17 +++++- rs/moq-uring/MIGRATE.md | 7 +++ rs/moq-video/MIGRATE.md | 20 ++++++- swift/MIGRATE.md | 76 ++++++++++++++++++++++++ 31 files changed, 797 insertions(+), 21 deletions(-) diff --git a/dart/moq/MIGRATE.md b/dart/moq/MIGRATE.md index 9068958d6c..c2fb8311de 100644 --- a/dart/moq/MIGRATE.md +++ b/dart/moq/MIGRATE.md @@ -6,7 +6,7 @@ How to update your code for each breaking change, newest first. ### Background panics are reported (#5562) -A panic in a background task throws `MoqException.Task` with the panic message as `reason`, and other objects keep running. +A panic in a background task throws `TaskMoqException` with the panic message as `reason`, and other objects keep running. ### Announcements are events with a restart @@ -34,12 +34,26 @@ moq.announced().updates().listen((event) { `Moq.epoch` is `connects`. +```dart +// Before +final n = moq.epoch; +// After +final n = moq.connects; +``` + ### The consume origin is `consume` `ConnectOptions.subscribe` is `consume`. A value the native side cannot use fails `Moq.connect` with `MoqException.Config`, including a bad bind address. A server request takes its origins in `accept(publish: ..., consume: ...)`, where null inherits the server's origin. +```dart +// Before +final moq = await Moq.connect(url, options: ConnectOptions(subscribe: origin)); +// After +final moq = await Moq.connect(url, options: ConnectOptions(consume: origin)); +``` + ### Data tracks wrap a track `BroadcastProducer.publishJsonSnapshot` / `publishJsonStream`, the binary variants, and the matching consumers are gone. @@ -74,33 +88,84 @@ final audio = media.TrackProducer.audio(broadcast: broadcast, target: media.Name `media.ContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. Its optional `timebase`, also on `ContainerInit`, shares one source's offset across container imports. +```dart +// Before +final producer = media.ContainerStreamProducer(broadcast: broadcast, format: media.ContainerFormat.fmp4); +// After +final producer = media.ContainerStreamProducer(broadcast: broadcast, init: media.ContainerStreamInit(format: media.ContainerFormat.fmp4)); +``` + ### Timestamps are optional `Frame.timestampUs` and `Datagram.timestampUs` are nullable, null on a frame read from an untimed track, and so are their `timestamp` getters. A raw write to a timed track without a timestamp throws `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. `media.CatalogProducer.now()` samples the broadcast clock for local-event stamps. +```dart +// Before +track.writeFrame(frame: Frame(payload: payload)); +final Duration ts = frame.timestamp; +// After +track.writeFrame(frame: Frame(payload: payload, timestampUs: pts)); +final Duration? ts = frame.timestamp; +``` + ### Subscriber staleness is `maxDelay` `Subscription.maxAgeUs` / `maxAge` is `maxDelayUs` / `maxDelay`. `TrackInfo.maxAge` keeps its name: it is the publisher's retention. +```dart +// Before +final sub = Subscription(maxAgeUs: 500000); +// After +final sub = Subscription(maxDelayUs: 500000); +``` + ### Error payloads are named `reason` Exceptions that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. +```dart +// Before +final reason = (error as TransportMoqException).v0; +// After +final reason = (error as TransportMoqException).reason; +``` + ### `Transport` gains `webTransport` A WebTransport session reports `MoqTransport.webTransport`; `quic` now means raw QUIC only, so an exhaustive `switch` needs one more case. +```dart +// Before +case MoqTransport.quic: +// After +case MoqTransport.quic || MoqTransport.webTransport: +``` + ### `stalled` is `enabled` (#4915) `media.Video.stalled` is `enabled`, defaulting to true with the meaning inverted, and `media.Audio` gains `enabled`. +```dart +// Before +final live = !video.stalled; +// After +final live = video.enabled; +``` + ### Routes have one cost (#4661) `MoqRoute.cold` is removed. +```dart +// Before +final route = MoqRoute(cost: 10, cold: 20); +// After +final route = MoqRoute(cost: 10); +``` + ### `BroadcastProducer.finish()` is removed (#4152) ```dart diff --git a/doc/bin/cli.md b/doc/bin/cli.md index d92d2accf6..9ed1c22435 100644 --- a/doc/bin/cli.md +++ b/doc/bin/cli.md @@ -404,3 +404,8 @@ so the other formats refuse `--linger`. relay is reachable and shows what it holds; see [Inspect a relay](/bin/inspect). Connection refused means UDP isn't getting through; certificate errors on a dev relay want `--connect-tls-insecure` or the `http://` fingerprint flow. + +## Upgrading + +Renamed flags, environment variables, and config keys are listed, newest +first, in moq-cli's [MIGRATE.md](https://github.com/moq-dev/moq/blob/main/rs/moq-cli/MIGRATE.md). diff --git a/doc/bin/relay/index.md b/doc/bin/relay/index.md index acd69bfae7..c181e56e87 100644 --- a/doc/bin/relay/index.md +++ b/doc/bin/relay/index.md @@ -101,3 +101,8 @@ See [docs.rs/moq-relay](https://docs.rs/moq-relay) and - **Certificate errors**: the hostname must match the certificate. Local browsers need the fingerprint served over `[web.http]`. - **Connection timeout**: UDP isn't reaching the relay, or the client URL names the wrong port. - **Unauthorized / forbidden**: the token's paths don't cover the connection path, or the broadcast a session asked for. See [path matching](/bin/relay/auth#path-matching). + +## Upgrading + +Renamed flags, environment variables, and config keys are listed, newest +first, in moq-relay's [MIGRATE.md](https://github.com/moq-dev/moq/blob/main/rs/moq-relay/MIGRATE.md). diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index 9125d0e679..20d219f79a 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -7,10 +7,12 @@ description: The order to roll out a release across relays, auth servers, and cl Every hand-written published package keeps a `MIGRATE.md` with a before/after snippet for each breaking change, newest first. Read your package's file from -the top down to the version you run. The generated UniFFI packages (such as -`moq-ffi` on PyPI or Maven) follow -[moq-ffi's](https://github.com/moq-dev/moq/blob/main/rs/moq-ffi/MIGRATE.md). This page covers what spans packages: the wire changes -that decide the order to upgrade a deployment. +the top down to the version you run. Most binding users install a wrapper, +such as `moq-rs` on PyPI or `dev.moq:moq` on Maven, whose guide is in the index +below. The raw UniFFI packages published as `moq-ffi` have no guide of their +own: [moq-ffi's](https://github.com/moq-dev/moq/blob/main/rs/moq-ffi/MIGRATE.md) +covers that layer, in UniFFI names. This page covers what spans packages: the +wire changes that decide the order to upgrade a deployment. A released flag, environment variable, or config key that was renamed is refused at startup with its replacement named, rather than ignored. Fix what the diff --git a/go/wrapper/MIGRATE.md b/go/wrapper/MIGRATE.md index fdca4e223c..b5fb702ed7 100644 --- a/go/wrapper/MIGRATE.md +++ b/go/wrapper/MIGRATE.md @@ -104,6 +104,13 @@ audio, err := media.NewAudioTrackProducer(broadcast, media.Named{}, media.AudioI `media.NewContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. Its optional `Timebase`, also on `ContainerInit`, shares one source's offset across container imports. +```go +// Before +producer, err := media.NewContainerStreamProducer(broadcast, media.ContainerFormatFmp4) +// After +producer, err := media.NewContainerStreamProducer(broadcast, media.ContainerStreamInit{Format: media.ContainerFormatFmp4}) +``` + ### Durations are `time.Duration` `Frame`, `Datagram`, `Subscription`, `TrackInfo`, and `ConnectionStats` carry `time.Duration` instead of microsecond integers: `TimestampUs` is `Timestamp`, and `RttUs` is `RTT`. @@ -122,27 +129,69 @@ err := track.WriteFrame(moq.Frame{Payload: payload, Timestamp: &ts}) A raw write to a timed track without a timestamp fails with `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. `media.CatalogProducer.Now()` samples the broadcast clock for local-event stamps. +```go +// Before +err := track.WriteFrame(moq.Frame{Payload: payload}) +// After +err := track.WriteFrame(moq.Frame{Payload: payload, Timestamp: &pts}) +``` + ### Subscriber staleness is `MaxDelay` `Subscription.MaxAgeUs` is `MaxDelay`, and the decoder outputs take `MaxDelayUs`. `TrackInfo.MaxAge` keeps its name: it is the publisher's retention. +```go +// Before +sub := moq.Subscription{MaxAgeUs: 500_000} +// After +sub := moq.Subscription{MaxDelay: 500 * time.Millisecond} +``` + ### Error payloads are named `Reason` Errors that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `Reason`. +```go +// Before +reason := transport.Field0 // *ffi.MoqErrorTransport from errors.As +// After +reason := transport.Reason +``` + ### `Transport` gains `WebTransport` -A WebTransport session reports `TransportWebTransport`; `TransportQuic` now means raw QUIC only. +A WebTransport session reports `TransportWebTransport`; `TransportQUIC` now means raw QUIC only. + +```go +// Before +quic := request.Transport() == moq.TransportQUIC +// After +quic := request.Transport() == moq.TransportQUIC || request.Transport() == moq.TransportWebTransport +``` ### `Stalled` is `Enabled` (#4915) `media.Video.Stalled` is `Enabled`, defaulting to true with the meaning inverted, and `media.Audio` gains `Enabled`. +```go +// Before +live := !video.Stalled +// After +live := video.Enabled +``` + ### Routes have one cost (#4661) `Route.Cold` is removed. +```go +// Before +route := moq.Route{Cost: 10, Cold: &cold} +// After +route := moq.Route{Cost: 10} +``` + ### Decoded video frames are objects `VideoDecodedFrame` is a handle with `TimestampUs()`, `Width()`, `Height()`, `Pixels(format)`, `Surface()`, and `Close()`. @@ -181,6 +230,13 @@ import "moq.dev/moq" `PublishMedia*` is `PublishAudio`, `PublishVideo`, or `PublishContainer`, each taking its own init. The raw encoder paths that used to be `PublishAudio` / `PublishVideo` are `EncodeAudio` / `EncodeVideo`. +```go +// Before +media, err := broadcast.PublishMedia("opus", opusHead) +// After +media, err := broadcast.PublishAudio(moq.AudioFormatOpus, opusHead) +``` + ### Renames | Before | After | @@ -197,3 +253,10 @@ The raw encoder paths that used to be `PublishAudio` / `PublishVideo` are `Encod Protocol errors carry a `ProtocolError` (scope, wire code, kind) instead of a flattened message. Setters return an error (`Busy` during connect or listen) instead of dropping the value. Track and group `Finish()` keep the handle open, so a later `Abort()` can still run. + +```go +// Before +if errors.Is(err, moq.ErrProtocol) { log.Print(err.Error()) } +// After +if p, ok := moq.ProtocolError(err); ok { log.Print(p.Kind, p.Code) } +``` diff --git a/js/hang/MIGRATE.md b/js/hang/MIGRATE.md index 39b6cc9e29..d77211ed47 100644 --- a/js/hang/MIGRATE.md +++ b/js/hang/MIGRATE.md @@ -79,3 +79,12 @@ producer.discontinuity(end); ### The catalog's `timeline` entry is `archive` @moq/hang reads the root catalog's `archive` entry instead of `timeline`, and wall time from the root `clock`. + +```ts +// Before +const index = catalog.timeline; +const wall = catalog.timeline?.wall; +// After +const index = catalog.archive; +const wall = catalog.clock?.wall; +``` diff --git a/js/msf/MIGRATE.md b/js/msf/MIGRATE.md index 8d2cbe631b..79df0bfb1e 100644 --- a/js/msf/MIGRATE.md +++ b/js/msf/MIGRATE.md @@ -8,3 +8,10 @@ How to update your code for each breaking change, newest first. The non-standard `stalled` track field is gone and no longer read or written. Drop it from any track you build. + +```ts +// Before +const track: Track = { name: "video0", packaging: "loc", stalled: true }; +// After +const track: Track = { name: "video0", packaging: "loc" }; +``` diff --git a/js/net/MIGRATE.md b/js/net/MIGRATE.md index 3d9f805dc3..640797d85b 100644 --- a/js/net/MIGRATE.md +++ b/js/net/MIGRATE.md @@ -39,7 +39,7 @@ Read `active` reactively and close the request when done, or pass `epoch` to kee for (const broadcast of origin.broadcasts(scope)) { ... } // After for await (const event of origin.announced(scope)) { - if (event.kind === "start") { ... } + if (event.kind === "start" || event.kind === "restart") { ... } } ``` @@ -150,6 +150,14 @@ await producer.demand().unused(); `Track.Producer.appendGroup` and `appendDatagram` throw `duplicate sequence` instead of continuing past an earlier producer of the same name; `createTrack` now answers a queued request instead of throwing `duplicate track`. `Request.accept` closes the request's producer with the error whenever it throws. +```ts +// Before +const track = new Track.Producer("chat").accept(info); +broadcast.insertTrack(track); +// After +const track = broadcast.createTrack("chat", info); +``` + ### Route cost is a `bigint` (#4661) `Route.cost` is one `bigint` instead of separate warm and cold numbers. diff --git a/js/publish/MIGRATE.md b/js/publish/MIGRATE.md index 5ce5c32b35..48fc865d2e 100644 --- a/js/publish/MIGRATE.md +++ b/js/publish/MIGRATE.md @@ -9,6 +9,13 @@ How to update your code for each breaking change, newest first. `enabled: false` keeps the rendition in the catalog with `enabled: false` instead of removing it, and viewers skip it. Check any code that read a missing rendition as disabled. +```ts +// Before +const disabled = !catalog.video?.renditions[name]; +// After +const disabled = catalog.video?.renditions[name]?.enabled === false; +``` + ### Tracks persist for the broadcast's life (#5068) `Rendition.close()` no longer closes the network track. diff --git a/js/room/MIGRATE.md b/js/room/MIGRATE.md index 73d2f70625..7aa8275612 100644 --- a/js/room/MIGRATE.md +++ b/js/room/MIGRATE.md @@ -18,3 +18,10 @@ const ms = Milli.fromNano(HISTORY); ### Chat tracks are timed (#4968) `Chat.info()` includes a `timescale`, so a chat track you create yourself must declare one before writing timestamps. + +```ts +// Before +const track = broadcast.createTrack(Chat.TRACK, { priority }); +// After +const track = broadcast.createTrack(Chat.TRACK, Chat.info()); +``` diff --git a/js/watch/MIGRATE.md b/js/watch/MIGRATE.md index 6e4f7e4770..086b963ff7 100644 --- a/js/watch/MIGRATE.md +++ b/js/watch/MIGRATE.md @@ -31,6 +31,13 @@ With no video rendition selectable, the picture clears to black. With `announced` off, the player no longer moves to a later publisher at the same path. Turn `announced` on to follow restarts. +```ts +// Before +new Watch.Player({ origin: connection.origin, name, announced: false }); +// After +new Watch.Player({ origin: connection.origin, name, announced: true }); +``` + ## 0.5 -> 0.6 ### Delay attributes need a unit (#3396, #3817) diff --git a/kt/moq/MIGRATE.md b/kt/moq/MIGRATE.md index 3dffc90c82..8ffdcc5ae8 100644 --- a/kt/moq/MIGRATE.md +++ b/kt/moq/MIGRATE.md @@ -50,6 +50,15 @@ moq.announced().updates().collect { event -> `Moq.epoch()` is `connects()`, and `AudioProducer.resetEpoch()` is `reanchor()`. +```kotlin +// Before +val n = moq.epoch() +audio.resetEpoch() +// After +val n = moq.connects() +audio.reanchor() +``` + ### `connect` takes a `ClientConfig` The keyword arguments moved into a `ClientConfig` with nested `tls`, `quic`, `websocket`, and `backoff` records. @@ -94,33 +103,86 @@ val audio = dev.moq.media.TrackProducer.audio(broadcast, Named(null), AudioInit( `ContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. Its optional `timebase`, also on `ContainerInit`, shares one source's offset across container imports. +```kotlin +// Before +val importer = ContainerStreamProducer(broadcast, ContainerFormat.FMP4) +// After +val importer = ContainerStreamProducer(broadcast, ContainerStreamInit(ContainerFormat.FMP4)) +``` + ### Timestamps are optional `Frame.timestamp` and `Datagram.timestamp` are `Duration?`, null on a frame read from an untimed track. A raw write to a timed track without a timestamp throws `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. `CatalogProducer.now()` samples the broadcast clock for local-event stamps. +```kotlin +// Before +track.writeFrame(Frame(payload = bytes)) +val ts: Duration = frame.timestamp +// After +track.writeFrame(Frame(payload = bytes, timestampUs = nowUs)) +val ts: Duration? = frame.timestamp +``` + ### Subscriber staleness is `maxDelay` `Subscription.maxAge`, `AudioDecoderOutput.maxAge`, and `VideoDecoderOutput.maxAge` are `maxDelay`. `TrackInfo.maxAge` keeps its name: it is the publisher's retention. +```kotlin +// Before +val subscription = Subscription(maxAgeUs = 500_000u) +val budget = subscription.maxAge +// After +val subscription = Subscription(maxDelayUs = 500_000u) +val budget = subscription.maxDelay +``` + ### Error payloads are named `reason` Exceptions that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. +```kotlin +// Before +catch (e: MoqException.Transport) { log(e.v1) } +// After +catch (e: MoqException.Transport) { log(e.reason) } +``` + ### `Transport` gains `WEB_TRANSPORT` A WebTransport session reports `Transport.WEB_TRANSPORT`; `QUIC` now means raw QUIC only, so an exhaustive `when` needs one more branch. +```kotlin +// Before +val quic = request.transport() == Transport.QUIC +// After +val quic = request.transport() == Transport.QUIC || request.transport() == Transport.WEB_TRANSPORT +``` + ### `stalled` is `enabled` (#4915) `Video.stalled` is `enabled`, defaulting to true with the meaning inverted, and `Audio` gains `enabled`. +```kotlin +// Before +if (!video.stalled) select(video) +// After +if (video.enabled) select(video) +``` + ### Routes have one cost (#4661) `Route.cold` is removed. +```kotlin +// Before +val route = Route(cost = 10u, cold = 50u) +// After +val route = Route(cost = 10u) +``` + ### Decoded video frames are objects `VideoDecodedFrame` is a closeable handle with `width()`, `height()`, `pixels(format)`, and `surface()`. @@ -137,21 +199,49 @@ val rgba = frame.use { it.pixels(VideoPixelFormat.RGBA) } Call `end()` to finish the broadcast and keep the handle, or `close()` to release it. +```kotlin +// Before +broadcast.finish() +// After +broadcast.end() +``` + ## 0.4 -> 0.5 ### Durations are `kotlin.time.Duration` Extension properties expose every microsecond field as a `Duration`: `Frame.timestamp`, `Subscription.maxAge`, `ConnectionStats.rtt`, `Backoff.initial`, and the rest. +```kotlin +// Before +val delayMs = backoff.initialMs +// After +val delay = backoff.initial +``` + ### `connect` grows options `Moq.connect` gains `reconnect`, `backoff`, `maxStreams`, and the WebSocket fallback settings as named arguments. +```kotlin +// Before +val moq = Moq.connect(url, ..., origin) +// After +val moq = Moq.connect(url, publish = origin) +``` + ### Publish split by kind `publishMedia*` is `publishAudio`, `publishVideo`, or `publishContainer`, each taking its own init. The raw encoder paths that used to be `publishAudio` / `publishVideo` are `encodeAudio` / `encodeVideo`. +```kotlin +// Before +val media = broadcast.publishMedia(Init(format = "opus", data = opusHead, video = null)) +// After +val media = broadcast.publishAudio(AudioInit(format = AudioFormat.OPUS, data = opusHead)) +``` + ### Renames | Before | After | @@ -166,3 +256,10 @@ The raw encoder paths that used to be `publishAudio` / `publishVideo` are `encod `MoqException.Protocol` carries a `ProtocolError` (scope, wire code, kind) instead of a flattened message. Setters throw (`Busy` during connect or listen) instead of dropping the value. Track and group `finish()` keep the handle open, so a later `abort()` can still run. + +```kotlin +// Before +catch (e: MoqException.Protocol) { log(e.message) } +// After +catch (e: MoqException.Protocol) { log("${e.details.kind} ${e.details.code}") } +``` diff --git a/py/moq-rs/MIGRATE.md b/py/moq-rs/MIGRATE.md index 03d6eeb40f..5185539d9a 100644 --- a/py/moq-rs/MIGRATE.md +++ b/py/moq-rs/MIGRATE.md @@ -107,6 +107,13 @@ audio = moq.media.TrackProducer.audio(broadcast, moq.media.AudioInit(format=moq. `moq.media.ContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. Its optional `timebase`, also on `ContainerInit`, shares one source's offset across container imports. +```python +# Before +producer = moq.media.ContainerStreamProducer(broadcast, moq.media.ContainerFormat.FMP4) +# After +producer = moq.media.ContainerStreamProducer(broadcast, moq.media.ContainerStreamInit(moq.media.ContainerFormat.FMP4)) +``` + ### Durations are `timedelta` `Frame`, `Datagram`, `Subscription`, `TrackInfo`, and `ConnectionStats` carry `timedelta` instead of microsecond integers: `timestamp_us` is `timestamp`, and `rtt_us` is `rtt`. @@ -125,23 +132,58 @@ A frame read from an untimed track has `timestamp` set to `None`. Raw writes no longer default to zero, so a write to a timed track without a timestamp raises `TimestampMismatch`; whole-frame media writes require one. `moq.media.CatalogProducer.now()` samples the broadcast clock for local-event stamps. +```python +# Before +track.write_frame(payload) +# After +track.write_frame(payload, timestamp=pts) +``` + ### Subscriber staleness is `max_delay` `Subscription.max_age_us` is `max_delay`, a `timedelta`, and the decoder outputs take `max_delay_us`. `TrackInfo` keeps its `max_age`: it is the publisher's retention. +```python +# Before +sub = moq.Subscription(max_age_us=500_000) +# After +sub = moq.Subscription(max_delay=timedelta(milliseconds=500)) +``` + ### Error payloads are named `reason` Errors that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. +```python +# Before +reason = err[0] +# After +reason = err.reason +``` + ### `stalled` is `enabled` (#4915) `moq.media.Video.stalled` is `enabled`, defaulting to `True` with the meaning inverted, and `moq.media.Audio` gains `enabled`. +```python +# Before +live = not video.stalled +# After +live = video.enabled +``` + ### Routes have one cost (#4661) `Route.cold` is removed. +```python +# Before +route = moq.Route(cost=10, cold=20) +# After +route = moq.Route(cost=10) +``` + ### Decoded video frames are objects A `VideoDecodedFrame`'s fields are methods: `timestamp_us()`, `width()`, `height()`, and `pixels(format)` replacing `data` and `format`. @@ -172,6 +214,13 @@ broadcast.close() `publish_media*` is `publish_audio`, `publish_video`, or `publish_container`, each taking its own init. The raw encoder paths that used to be `publish_audio` / `publish_video` are `encode_audio` / `encode_video`. +```python +# Before +media = broadcast.publish_media("opus", opus_head) +# After +media = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head) +``` + ### Renames | Before | After | @@ -188,3 +237,12 @@ The raw encoder paths that used to be `publish_audio` / `publish_video` are `enc `Error.Protocol` carries a `ProtocolError` (scope, wire code, kind) instead of a flattened message. Setters raise (`Busy` during connect or listen) instead of dropping the value. Track and group `finish()` keep the handle open, so a later `abort()` can still run. + +```python +# Before +except moq.Error.Protocol as e: + message = str(e) +# After +except moq.Error.Protocol as e: + kind = e.details.kind +``` diff --git a/quest/m2/release/migrate-audit.md b/quest/m2/release/migrate-audit.md index d62a40c685..dbcc2c7a2c 100644 --- a/quest/m2/release/migrate-audit.md +++ b/quest/m2/release/migrate-audit.md @@ -40,8 +40,8 @@ Decided 2026-10-16 (maintainer interview): - For this cut, fill every gap the report and the manual pass show in the matching `MIGRATE.md`. Known gaps: breaking releases cut after the 2026-09-23 train have no transition section yet (moq-json 0.4 -> 0.5, - moq-auth 0.1 -> 0.2, moq-flate 0.1 -> 0.2, and possibly moq-audio and - moq-video), because their breaks predate the backfill's merge base. + moq-flate 0.1 -> 0.2, and possibly moq-audio and moq-video), because their + breaks predate the backfill's merge base. - Compile-check the after snippets of every `MIGRATE.md` section the cut ships against the candidate. Nothing else builds them. - Required by the cut, after m1's breaking work has landed. diff --git a/rs/moq-auth/MIGRATE.md b/rs/moq-auth/MIGRATE.md index 9340f745f7..c2a6090b8a 100644 --- a/rs/moq-auth/MIGRATE.md +++ b/rs/moq-auth/MIGRATE.md @@ -17,6 +17,26 @@ match transport { } ``` +## 0.1 -> 0.2 + +### `serve` matches 0.14 relay auth (#4319) + +`serve::Policy::revalidate` and `expires` are `Option`, defaulting to `None`, so a grant is never re-checked or closed unless you set them. +`serve::Server::new` returns a `Result`: it refuses `revalidate` without `expires` (`Error::UnboundedRevalidate`) and session limits without `revalidate` (`Error::LimitsWithoutRevalidate`). +A session presenting both a JWT and a client certificate is refused with `Refusal::TokenAndCertificate`. +`Key::verify` enforces `nbf` (`Claims::not_before`, failing with `Error::TokenNotYetValid`) and ignores `iss`, `sub`, and `jti`; any other unknown claim still refuses the token. + +```rust +// Before +let policy = Policy::default(); // re-checked every 60 s, bounded at 24 h +let server = Server::new(policy); +// After +let mut policy = Policy::default(); +policy.revalidate = Some(Duration::from_secs(60)); +policy.expires = Some(Duration::from_secs(24 * 60 * 60)); +let server = Server::new(policy)?; +``` + ## moq-token 0.7 -> moq-auth 0.1 ### moq-token is moq-auth (#3684) diff --git a/rs/moq-c/MIGRATE.md b/rs/moq-c/MIGRATE.md index be7628da87..3023b3e7ca 100644 --- a/rs/moq-c/MIGRATE.md +++ b/rs/moq-c/MIGRATE.md @@ -71,6 +71,13 @@ if (frame.timestamp_present) use(frame.timestamp_us); `moq_subscription`, `moq_audio_decoder_output`, `moq_video_decoder_output`, `moq_consume_video`, and `moq_consume_audio` take `max_delay_us` instead of `max_age_us`. +```c +// Before +moq_subscription sub = { .max_age_us = 500000 }; +// After +moq_subscription sub = { .max_delay_us = 500000 }; +``` + ### `stalled` is `enabled` (#4915) `moq_consume_video_stalled` is `moq_consume_video_enabled`, with the meaning inverted. @@ -88,6 +95,13 @@ moq_consume_video_enabled(catalog, index, &enabled); `moq_route.cold` and `has_cold` are removed. +```c +// Before +moq_route route = { .cost = 10, .cold = 20, .has_cold = true }; +// After +moq_route route = { .cost = 10 }; +``` + ### `moq_publish_finish` is removed (#4152) Close the broadcast with `moq_publish_close`. @@ -109,11 +123,29 @@ moq_publish_close(broadcast); The 41 `moq_client_*` setters are one zero-initializable `moq_client_config`, whose durations are `_us`. +```c +// Before +int client = moq_client_create(); +moq_client_set_tls_disable_verify(client, true); +moq_client_connect(url, url_len, client, origin, 0, on_status, user_data); +// After +moq_client_config config = { .tls_disable_verify = true }; +moq_session_connect(url, url_len, &config, origin, 0, on_status, user_data); +``` + ### Publish split by kind `moq_publish_media` is `moq_publish_audio`, `moq_publish_video`, or `moq_publish_container`, and the raw encoders are `moq_encode_audio*` / `moq_encode_video*`. Formats are enums instead of strings. +```c +// Before +int track = moq_publish_media(broadcast, "opus", 4, opus_head, opus_head_len); +// After +moq_audio_init init = { .format = MOQ_AUDIO_FORMAT_OPUS, .init = opus_head, .init_len = opus_head_len }; +int track = moq_publish_audio(broadcast, &init); +``` + ### Announcement renames | Before | After | diff --git a/rs/moq-cli/MIGRATE.md b/rs/moq-cli/MIGRATE.md index d16e11a19d..392630bc8c 100644 --- a/rs/moq-cli/MIGRATE.md +++ b/rs/moq-cli/MIGRATE.md @@ -7,7 +7,7 @@ How to update your invocation and config for each breaking change, newest first. ### `moq export ts --delay` defaults to 1 s and includes the send-ahead (#5553) The output trails a TS source by the delay instead of twice it. -With a multiplex rate, video whose HRD declares a longer buffer delay than `--delay` stops the export with the delay it needs named. +With a multiplex rate, video whose HRD declares a longer buffer delay than `--delay` stops the export, naming the delay it needs. ```bash # Before @@ -32,6 +32,13 @@ moq export fmp4 --max-delay 2s ... Each frame is written that long after its decode time; the older staleness spellings are refused with `--delay` named. +```bash +# Before +moq export ts --max-age 500ms ... +# After +moq export ts --delay 500ms ... +``` + ### `moq export ts --linger` follows only the same publisher instance A restarted publisher exits 1 unless `--stitch` follows it as a program switch. @@ -56,6 +63,13 @@ Annex-B H.264 and H.265 sources get an `avc3` or `hev1` init from the catalog wh `auto` is measured from how late frames arrive instead of derived from RTT. Pass `--delay 100ms` to keep the old fixed delay. +```bash +# Before +moq ... play +# After +moq ... play --delay 100ms +``` + ### `--hop` is removed; `--epoch` replaces it (#4969) A redundant pair shares an epoch, a UUIDv7 such as `uuidgen -7` prints, instead of a Hop ID. @@ -75,6 +89,13 @@ A stream-only listener (TCP or Unix, no `--listen`) refuses the QUIC-only flags, `--listen-quic-lb-nonce` needs `--listen-quic-lb-id`. Each used to be ignored: drop it, or add the listener it configures. +```bash +# Before +moq --connect https://relay.example.com/anon --auth-public 'anon/**' import ... +# After +moq --connect https://relay.example.com/anon import ... +``` + ### `moq --listen` with an invalid auth config stops at startup (#4912) It used to start and refuse every session. @@ -83,12 +104,26 @@ It used to start and refuse every session. Rename it in `--features` when building from source. +```bash +# Before +cargo install moq-cli --no-default-features --features "noq,websocket,play" +# After +cargo install moq-cli --no-default-features --features "quic,websocket,play" +``` + ## 0.13 -> 0.14 ### Gossip discovery is removed (#4601) `--cluster-mesh` and TOML `mesh` are refused: list every peer with `--cluster-connect`. +```bash +# Before +moq --cluster-mesh --cluster-connect https://root.example/ import ... +# After +moq --cluster-connect https://root.example/,https://b.example/ import ... +``` + ## 0.12 -> 0.13 ### `moq auth serve` matches 0.14 relay auth (#4319) @@ -97,6 +132,13 @@ Rename it in `--features` when building from source. It refuses a session presenting both a JWT and a certificate. `--key` takes a file, not an https or JWKS URL. +```bash +# Before +moq auth serve --key-dir keys/ ... +# After +moq auth serve --key-dir keys/ --revalidate 1m --expires 1d ... +``` + ## 0.11 -> 0.12 ### Commands and flags are renamed @@ -118,3 +160,10 @@ A renamed flag or environment variable is refused with its replacement named. ### `moq --listen` needs auth A listener refuses to start without `--auth-url` or `--auth-public` instead of accepting everyone. + +```bash +# Before +moq --listen '[::]:4443' ... import ts +# After +moq --listen '[::]:4443' --auth-public '**' ... import ts +``` diff --git a/rs/moq-clock/MIGRATE.md b/rs/moq-clock/MIGRATE.md index 5729915579..d2fa2069b8 100644 --- a/rs/moq-clock/MIGRATE.md +++ b/rs/moq-clock/MIGRATE.md @@ -9,3 +9,10 @@ How to update your code for each breaking change, newest first. moq-clock 0.10 was the "CLOCK over QUIC" example binary. 0.11 is a library: logical instants, wall time, deadlines, and a deterministic simulator. The clock example lives on as `cargo run -p moq-tokio --example clock`. + +```bash +# Before +cargo run -p moq-clock -- --url https://relay.example.com/anon --broadcast clock publish +# After +cargo run -p moq-tokio --example clock -- --connect https://relay.example.com/anon --broadcast clock publish +``` diff --git a/rs/moq-e2ee/MIGRATE.md b/rs/moq-e2ee/MIGRATE.md index f36e1f97b9..5ba07b8c40 100644 --- a/rs/moq-e2ee/MIGRATE.md +++ b/rs/moq-e2ee/MIGRATE.md @@ -9,6 +9,15 @@ How to update your code for each breaking change, newest first. The protected path is the opaque broadcast path; the publisher epoch is read from the route instead of a path suffix. On wires that do not carry an epoch, the application supplies it. +```rust +// Before +let path = cred.path(name)?.join_epoch(Some(generation.epoch())); +let (_, epoch) = path.split_epoch(); +// After +let path = cred.path(name)?; // announce with Route::default().with_epoch(epoch) +let epoch = consumer.routed(&path).await.and_then(|route| route.epoch); +``` + ### Timestamps are optional `Frame` and `Datagram` timestamps are `Option`, and the writers take `impl Into>`, so passing a `Timestamp` still compiles. diff --git a/rs/moq-ffi/MIGRATE.md b/rs/moq-ffi/MIGRATE.md index 7ab602b903..344dfd559e 100644 --- a/rs/moq-ffi/MIGRATE.md +++ b/rs/moq-ffi/MIGRATE.md @@ -50,6 +50,15 @@ match announced.next().await? { `MoqSession::epoch()` is `connects()`, and `MoqAudioProducer::reset_epoch()` is `reanchor()`. +```rust +// Before +let n = session.epoch(); +audio.reset_epoch()?; +// After +let n = session.connects(); +audio.reanchor()?; +``` + ### Clients and servers take a config record (#4519) `MoqClient::new()` and `MoqServer::new()` plus their setters are `MoqClient::new(MoqClientConfig)` and `MoqServer::new(MoqServerConfig)`, with nested `tls`, `quic`, `websocket`, and `backoff` records and a `versions` list. @@ -103,11 +112,25 @@ let audio = MoqMediaTrackProducer::audio(&broadcast, MoqMediaTarget::Named { nam `MoqMediaContainerStreamProducer` takes a `MoqContainerStreamInit` holding the format instead of a bare format. Its optional `timebase`, also on `MoqContainerInit`, shares one source's offset across container imports; leave it unset for independent offsets. +```rust +// Before +let importer = MoqMediaContainerStreamProducer::new(&broadcast, MoqContainerFormat::Fmp4)?; +// After +let importer = MoqMediaContainerStreamProducer::new(&broadcast, MoqContainerStreamInit { format: MoqContainerFormat::Fmp4, timebase: None })?; +``` + ### Error payloads are named `MoqError` cases that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. The Display text is unchanged. +```rust +// Before +Err(MoqError::Transport(reason)) => log(reason), +// After +Err(MoqError::Transport { reason }) => log(reason), +``` + ### Timestamps are optional `MoqFrame.timestamp_us` and `MoqDatagram.timestamp_us` are optional, null on a frame read from an untimed track, and default to null. @@ -128,19 +151,47 @@ track.write_frame(MoqFrame { payload, timestamp_us: Some(now_us) })?; `MoqSubscription`, `MoqAudioDecoderOutput`, and `MoqVideoDecoderOutput` take `max_delay_us` instead of `max_age_us`. `MoqTrackInfo.max_age_us` keeps its name: it is the publisher's retention. +```rust +// Before +subscription.max_age_us = 500_000; +// After +subscription.max_delay_us = 500_000; +``` + ### `MoqTransport` gains `WebTransport` A WebTransport session reports `MoqTransport::WebTransport`; `Quic` now means raw QUIC only. An exhaustive match needs one more arm. +```rust +// Before +let quic = matches!(request.transport(), MoqTransport::Quic); +// After +let quic = matches!(request.transport(), MoqTransport::Quic | MoqTransport::WebTransport); +``` + ### `stalled` is `enabled` (#4915) `MoqVideo.stalled` is `enabled`, defaulting to true with the meaning inverted, and `MoqAudio` gains `enabled`. +```rust +// Before +if !video.stalled { select(video) } +// After +if video.enabled { select(video) } +``` + ### Routes have one cost (#4661) `MoqRoute.cold` is removed. +```rust +// Before +let route = MoqRoute { cost: 10, cold: Some(50), ..Default::default() }; +// After +let route = MoqRoute { cost: 10, ..Default::default() }; +``` + ### Decoded video frames own their surface `MoqVideoConsumer::next` returns an `Arc` object with `timestamp_us()`, `width()`, `height()`, `pixels(format)`, and `surface()`. @@ -157,10 +208,24 @@ let rgba = frame.pixels(MoqVideoPixelFormat::Rgba)?; `MoqBroadcastProducer::finish()` is gone; close the broadcast instead. +```rust +// Before +broadcast.finish()?; +// After +broadcast.close()?; +``` + ### The `android-logcat` feature is removed (#4608) Android builds always log to logcat; drop the feature. +```sh +# Before +cargo ndk -t arm64-v8a build -p moq-ffi --features android-logcat +# After +cargo ndk -t arm64-v8a build -p moq-ffi +``` + ## 0.3 -> 0.4 ### Publish split by kind @@ -168,6 +233,13 @@ Android builds always log to logcat; drop the feature. `publish_media*` is `publish_audio`, `publish_video`, or `publish_container`, each taking its own init. The raw encoder paths that used to be `publish_audio` / `publish_video` are `encode_audio` / `encode_video`. +```rust +// Before +let media = broadcast.publish_media(MoqInit { format: "opus".into(), data, label: None, video: None })?; +// After +let media = broadcast.publish_audio(MoqAudioInit { format: MoqAudioFormat::Opus, data, label: None })?; +``` + ### Renames | Before | After | @@ -185,10 +257,24 @@ The raw encoder paths that used to be `publish_audio` / `publish_video` are `enc Client and server setters return an error (`Busy` during connect or listen) instead of dropping the value. +```rust +// Before +client.set_publish(Some(origin)); +// After +client.set_publish(Some(origin))?; +``` + ### Protocol errors are structured `MoqError::Protocol` carries a `MoqProtocolError` (scope, wire code, kind) instead of a flattened message. +```rust +// Before +Err(MoqError::Protocol(err)) => log(err.to_string()), +// After +Err(MoqError::Protocol { details }) => log(details.kind, details.code), +``` + ### Track and group `finish()` keep the handle `finish()` keeps the handle open, so a later `abort()` can still run. diff --git a/rs/moq-hls/MIGRATE.md b/rs/moq-hls/MIGRATE.md index de1423430f..9763fba13c 100644 --- a/rs/moq-hls/MIGRATE.md +++ b/rs/moq-hls/MIGRATE.md @@ -46,3 +46,10 @@ config.history = true; `Broadcaster::new` no longer awaits or fails. A rendition whose sibling broadcast ends now ends its playlist instead of rebinding to a replacement. + +```rust +// Before +let broadcaster = Broadcaster::new(source, config).await?; +// After +let broadcaster = Broadcaster::new(source, config); +``` diff --git a/rs/moq-msf/MIGRATE.md b/rs/moq-msf/MIGRATE.md index 9ac0d723c8..a85714961b 100644 --- a/rs/moq-msf/MIGRATE.md +++ b/rs/moq-msf/MIGRATE.md @@ -8,3 +8,9 @@ How to update your code for each breaking change, newest first. The non-standard `stalled` track field is gone and no longer written. Drop it from any `Track` you build. + +```rust +// Before +track.stalled = Some(true); +// After: delete the assignment +``` diff --git a/rs/moq-mux/MIGRATE.md b/rs/moq-mux/MIGRATE.md index fe10fba2b0..96510ef619 100644 --- a/rs/moq-mux/MIGRATE.md +++ b/rs/moq-mux/MIGRATE.md @@ -104,7 +104,7 @@ Every importer, MPEG-TS included, now ends with `TimestampRewind` on a loop wrap ### Importers publish the catalog at their first frame The fMP4, MKV, and MPEG-TS importers hold the catalog until the first frame of a selected track, as FLV already did, so the first snapshot carries the final root `clock`. -A track `with_select` deselects does not release it. +A track deselected by `with_select` does not release it. A `moov` or `Tracks` decoded after `finish()` is refused with `fmp4::Error::MoovAfterFinish` or `mkv::Error::TracksAfterFinish`. ### Video importers drop `tick`, `idle`, and `observe_lag` (#4915) @@ -112,6 +112,14 @@ A `moov` or `Tracks` decoded after `finish()` is refused with `fmp4::Error::Moov The H.264, H.265, AV1, VP8, and VP9 `Import`s no longer take liveness hints; a stalled rendition is marked by the catalog's `enabled` flag instead. Delete the calls. +```rust +// Before +import.tick()?; +import.observe_lag(lag)?; +import.idle()?; +// After: delete the calls +``` + ### fMP4 export fixes its track set at the init segment `fmp4::Error` drops `MissingVideoTrack`, `MissingAudioTrack`, and `NoCatalogSnapshot`, and adds `TrackAdded`, `TrackChanged`, `TrackRewound`, and `TrackUndescribed`. @@ -128,6 +136,14 @@ A keyframe whose parameter sets never appeared ends the export instead of waitin CMAF `Wire::write` refuses a track whose timescale is not the init's `mdhd` timescale with `fmp4::Error::TimescaleMismatch`. A track you create yourself declares it with `track::Info::with_timescale`. +```rust +// Before +let track = broadcast.create_track(name, None)?; +// After +let info = track::Info::default().with_timescale(Timescale::new(mdhd_timescale)?); +let track = broadcast.create_track(name, info)?; +``` + ### Data timestamps use the broadcast clock (#4821) JSON and flate producers publish `at: None` untimed, so their track must be untimed too; pass a timestamp on a timed track. @@ -144,6 +160,13 @@ producer.update(Timed::from(&value).at(catalog.clock().capture(instant)?))?; `Error::Binary` is `Error::Flate`, carrying a `moq_flate::Error`. +```rust +// Before +Err(moq_mux::Error::Binary(err)) => {} // err: moq_binary::Error +// After +Err(moq_mux::Error::Flate(err)) => {} // err: moq_flate::Error +``` + ### AAC refuses channel counts it cannot name (#4973) `aac::Config::encode` returns a `Result` and fails with `UnsupportedChannelCount` for a count no `channelConfiguration` names, instead of writing stereo. @@ -174,6 +197,15 @@ let config = opus::Config { mapping: Some(mapping), ..config }; `ts::Export::stats` returns `ts::stats::Export`; feed it to `stats::Log` with `.into()`. `ts::MultipleProgramsError` is `#[non_exhaustive]`: recover it by downcast and read `programs`. +```rust +// Before +let stats: ts::Stats = import.stats(); +log.sample(export.stats()); +// After +let stats: ts::stats::Snapshot = import.stats(); +log.sample(export.stats().into()); +``` + ### FLV export takes a catalog stream (#4987) `flv::Export::new` is synchronous and takes `(source, catalog)`, like `fmp4::Export` and `mkv::Export`. diff --git a/rs/moq-net/MIGRATE.md b/rs/moq-net/MIGRATE.md index 5fa455ee0b..40364bbb3e 100644 --- a/rs/moq-net/MIGRATE.md +++ b/rs/moq-net/MIGRATE.md @@ -30,6 +30,13 @@ let next = pool.gc(now, &waiter); `append_group` and `append_datagram` on a track that replaces an earlier producer of the same name return `Error::Duplicate` instead of continuing past the earlier producer's sequence. Pick the start sequence yourself with `create_group`, or publish a new broadcast. +```rust +// Before +let group = track.append_group()?; // continued past the earlier producer +// After +let group = track.create_group(group::Info { sequence })?; +``` + ### Announcements are `Start`, `Update`, `End`, and `Restart` events `announce::Consumer` yields `announce::Event`, carrying an `announce::Announce { prefix, captures, route }`. diff --git a/rs/moq-relay/MIGRATE.md b/rs/moq-relay/MIGRATE.md index cce10ccc63..853c3647b5 100644 --- a/rs/moq-relay/MIGRATE.md +++ b/rs/moq-relay/MIGRATE.md @@ -20,17 +20,38 @@ pin = true The default build has no iroh listener and no `--iroh-*` flags or `[iroh]` table. Build with `--features iroh` to keep it. +```bash +# Before +cargo install moq-relay +# After +cargo install moq-relay --features iroh +``` + ### The `noq` cargo feature is `quic` Rename it wherever you enable it; the QUIC stack is MoQ's own `moq-quic`. A qlog directory gets one file per endpoint instead of one per connection. +```bash +# Before +cargo build -p moq-relay --no-default-features --features noq +# After +cargo build -p moq-relay --no-default-features --features quic +``` + ### A QUIC load-balancer nonce needs its server id (#5114) `listen.lb_nonce` (`--listen-quic-lb-nonce`, `MOQ_LISTEN_QUIC_LB_NONCE`) without `lb_id` stops startup. Setting `lb_id` or `lb_nonce` together with `load_balancer` also stops; `lb_id` used to win. Set one of them. +```bash +# Before +moq-relay --listen-quic-lb-nonce 8 ... +# After +moq-relay --listen-quic-lb-id 0a --listen-quic-lb-nonce 8 ... +``` + ### Settings no listener reads stop startup (#5006) A stream-only relay (TCP or Unix, no `--listen`) refuses the QUIC-only `--listen-preferred-v4` / `-v6`, `--listen-quic-lb-id`, and pinned `tls.peers`. @@ -38,12 +59,26 @@ It also refuses `--listen-tls-cert`, `-key`, or `-generate` unless `--listen-tcp `--listen-unix-allow-*` needs `--listen-unix-bind`, and `web.https.cert`, `key`, and `root` need `web.https.listen`. Each used to be ignored: drop it, or add the listener it configures. +```bash +# Before +moq-relay --listen-tcp-bind 127.0.0.1:4443 --listen-preferred-v4 203.0.113.1:443 ... +# After +moq-relay --listen-tcp-bind 127.0.0.1:4443 ... +``` + ### `--cluster-mesh` and `--cluster-linger` are unknown flags (#4818) They, TOML `mesh` and `linger`, and `MOQ_CLUSTER_MESH` / `MOQ_CLUSTER_LINGER` are no longer read. List every peer with `--cluster-connect` or `--cluster-connect-api`. In Rust, `cluster::Config` has no `mesh` or `linger` field. +```bash +# Before +moq-relay --cluster-mesh --cluster-linger 10s --cluster-connect https://root.example/ ... +# After +moq-relay --cluster-connect https://root.example/,https://b.example/ ... +``` + ### `--cluster-id` no longer falls back to `--hop` (#4969) A node that pinned its id with `--hop` passes `--cluster-id`. @@ -65,6 +100,15 @@ config.auth.validate(client_ca)?; `uring::Workers::bind` takes a `moq_tokio::server::Config`, so generated and rotating certificates work in both worker modes. +```rust +// Before +let workers = uring::Workers::bind(&listen, &quic, worker)?; +// After +let mut server = moq_tokio::server::Config::default(); +(server.listen, server.quic) = (listen, quic); +let workers = uring::Workers::bind(server, worker)?; +``` + ## 0.16 -> 0.17 ### Gossip discovery is removed (#4601) @@ -73,6 +117,13 @@ A relay dials only the peers it lists or finds on the LAN, never a URL learned f `--cluster-mesh` and TOML `mesh` are refused by name: list every peer with `--cluster-connect` or `--cluster-connect-api`. The `/nodes` endpoint lists only peers this relay dialed. +```bash +# Before +moq-relay --cluster-mesh --cluster-connect https://root.example/ ... +# After +moq-relay --cluster-connect https://root.example/,https://b.example/ ... +``` + ## 0.15 -> 0.16 ### 0.14 auth behavior is restored (#4319) @@ -82,6 +133,15 @@ The `/nodes` endpoint lists only peers this relay dialed. - A token's `iss`, `sub`, and `jti` are ignored and `nbf` is enforced; any other claim, `aud` and `cluster` included, refuses the token. - A relay on `--auth-public` refuses a session presenting a token, including a peer's `cluster.token`, and refuses to start with a client CA. +```toml +# Before +[auth] +key_dir = "keys/" +# After +[auth] +url = "http://127.0.0.1:4440" # moq auth serve --key-dir keys/ +``` + ## 0.14 -> 0.15 ### Flags split by direction @@ -127,8 +187,22 @@ This applies to `--auth-public`, TOML `public`, and `[auth.public]`, which is no A public or mTLS pattern with no wildcard refuses to start, naming the subtree to write. JWT `publish` and `subscribe` claims are patterns too, so a token granting `alice` covers only `alice`. +```bash +# Before +moq-relay --auth-public anon ... +# After +moq-relay --auth-public 'anon/**' ... +``` + ### Embedding `Relay` fields are private (#3638): clone the handles you need, mount routes, then call `Relay::run`. `Cluster::with_cache` moved to `cluster::Options`. The `quinn` and `quiche` features and the backend setting are gone (#3811). + +```rust +// Before +let origin = relay.cluster.origin.clone(); +// After +let origin = relay.cluster().origin.clone(); +``` diff --git a/rs/moq-rtc/MIGRATE.md b/rs/moq-rtc/MIGRATE.md index de53061b08..044ee3328b 100644 --- a/rs/moq-rtc/MIGRATE.md +++ b/rs/moq-rtc/MIGRATE.md @@ -15,8 +15,3 @@ client.subscribe(url, broadcast).await?; // After let _source = client.subscribe(url, broadcast).await?; ``` - -### `tick` is removed (#4915) - -`MediaSink::tick` and `Bridge::tick` are gone; a stalled rendition is marked by the catalog's `enabled` flag. -Delete the calls. diff --git a/rs/moq-srt/MIGRATE.md b/rs/moq-srt/MIGRATE.md index ad74ffb134..ffd651633b 100644 --- a/rs/moq-srt/MIGRATE.md +++ b/rs/moq-srt/MIGRATE.md @@ -7,4 +7,4 @@ How to update your code for each breaking change, newest first. ### Egress runs at least one second behind (#5553) SRT egress delays the TS export by the SRT latency or 1 s, whichever is longer, and the send-ahead now comes out of that budget. -A feed whose video HRD buffer declares a longer delay stops with the delay it needs named. +A feed whose video HRD buffer declares a longer delay stops, naming the delay it needs. diff --git a/rs/moq-tokio/MIGRATE.md b/rs/moq-tokio/MIGRATE.md index 44326c2b27..7045b187c3 100644 --- a/rs/moq-tokio/MIGRATE.md +++ b/rs/moq-tokio/MIGRATE.md @@ -130,15 +130,28 @@ match transport { ### Custom sessions declare their request binding -`transport::Session` wraps a session implementing moq-net's own `transport::poll` traits instead of `web_transport_trait`. -The adapter constructors take the session's `moq_net::transport::Binding` explicitly: `Uri` when the transport handshake carries the request URI, `InBand` when SETUP does. +`transport::Session::new` takes the session's `moq_net::transport::Binding`: `Uri` when the transport handshake carries the request URI, `InBand` when SETUP does. `Client` and `Server` are unchanged. +```rust +// Before +let session = moq_tokio::transport::Session::new(session); +// After +let session = moq_tokio::transport::Session::new(session, moq_net::transport::Binding::Uri); +``` + ### `Redirect::resolve` is removed (#4854) `Redirect` and `Goaway` stay, but the lenient `resolve(uri, current)` is gone. Follow a GOAWAY through the connection, which validates the target itself. +```rust +// Before +let next = Redirect::SameHost.resolve(uri, ¤t); +// After +config.goaway.redirect = Redirect::SameHost; // the connection follows and validates the GOAWAY +``` + ### The `android-logcat` feature is removed (#4608) Android builds always log to logcat. diff --git a/rs/moq-uring/MIGRATE.md b/rs/moq-uring/MIGRATE.md index 774deda59b..f86b545a65 100644 --- a/rs/moq-uring/MIGRATE.md +++ b/rs/moq-uring/MIGRATE.md @@ -20,6 +20,13 @@ moq-uring = { version = "...", features = ["quic"] } `quic::endpoint::CID_LEN` is removed; endpoints still issue 8-byte connection IDs, sharded or not. +```rust +// Before +let len = moq_uring::quic::endpoint::CID_LEN; +// After +let len = 8; // endpoints still issue 8-byte connection IDs +``` + ### The server config takes a rustls config (#5312) `quic::server::Config` drops `identity`, `alpn`, and `client_auth` for one `tls: Arc`, and `Config::new` takes it. diff --git a/rs/moq-video/MIGRATE.md b/rs/moq-video/MIGRATE.md index c4fb399a94..f2e7bee478 100644 --- a/rs/moq-video/MIGRATE.md +++ b/rs/moq-video/MIGRATE.md @@ -4,7 +4,7 @@ How to update your code for each breaking change, newest first. ## Unreleased -### Capture takes one `encode::Capture` and returns a `Control` +### Capture takes one `encode::Capture`, and `Control::new` returns the driver `publish_capture(broadcast, catalog, capture, encode, clock)` is `publish_capture(broadcast, catalog, encode::Capture)`. `Capture` has no clock: the driver stamps on `catalog.clock()`. @@ -29,7 +29,7 @@ It counts cadence keyframes too, so a request may be served without forcing one. // Before control.cut(); // After -if let Err(Error::CutUnsupported) = control.cut() { +if let Err(Error::CutUnsupported(_)) = control.cut() { // wait for the next cadence keyframe } ``` @@ -49,6 +49,14 @@ let encoded = Encoded::new(payload, timestamp, keyframe); `encode::Producer` no longer takes liveness hints; disable a rendition through the catalog's `enabled` flag instead. +```rust +// Before +producer.tick()?; +producer.observe_lag(lag)?; +producer.idle()?; +// After: delete the calls +``` + ### External Vulkan and DMA-BUF inputs (#4975) On Linux, `Surface::Vulkan` no longer requires the `nvidia` feature. @@ -57,6 +65,14 @@ On Linux, `Surface::Vulkan` no longer requires the `nvidia` feature. `DmaBuf::new` takes an owned FD, a `DmaBufLayout`, and a release guard, and an external DMA-BUF refuses CPU readback. Bind eager open to an external device with `encode::Config::input`; an unsupported device is refused instead of falling back to software. +```rust +// Before +let importer = frame::vulkan::Importer::new(ordinal, capacity)?; +let slot = importer.import(frame::vulkan::Handles::new(memory, timeline), image, owner)?; +// After +let slot = frame::vulkan::Slot::new(frame::vulkan::Handles { memory, timeline }, image, owner)?; +``` + ### Decoder staleness is `max_delay` `decode::Options::max_age` is `max_delay`. diff --git a/swift/MIGRATE.md b/swift/MIGRATE.md index c332a2ecac..5f129d69d2 100644 --- a/swift/MIGRATE.md +++ b/swift/MIGRATE.md @@ -49,6 +49,15 @@ for try await event in try session.consume.announced(prefix: "live/") { `Session.epoch()` is `connects()`, and `AudioProducer.resetEpoch()` is `reanchor()`. +```swift +// Before +let n = session.epoch() +try audio.resetEpoch() +// After +let n = session.connects() +try audio.reanchor() +``` + ### `Client` takes its config at init The `Client` setters are `Client(...)` arguments, with `tls`, `quic`, `websocket`, and `backoff` records. @@ -97,33 +106,84 @@ let audio = try Media.TrackProducer.audio(broadcast: broadcast, initData: Media. `Media.ContainerStreamProducer` takes a `Media.ContainerStreamInit` holding the format instead of a bare format. Its optional `timebase`, also on `Media.ContainerInit`, shares one source's offset across container imports. +```swift +// Before +let importer = try Media.ContainerStreamProducer(broadcast: broadcast, format: .fmp4) +// After +let importer = try Media.ContainerStreamProducer(broadcast: broadcast, initData: Media.ContainerStreamInit(format: .fmp4)) +``` + ### Timestamps are optional Raw `writeFrame(_:timestampUs:)` takes a `UInt64?` defaulting to `nil`, and a frame read from an untimed track has a `nil` `timestampUs`. A write to a timed track without a timestamp throws `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. `Media.CatalogProducer.now()` samples the broadcast clock for local-event stamps. +```swift +// Before +try track.writeFrame(payload) +let ts: UInt64 = frame.timestampUs +// After +try track.writeFrame(payload, timestampUs: nowUs) +let ts: UInt64? = frame.timestampUs +``` + ### Subscriber staleness is `maxDelayUs` `Subscription`, `AudioDecoderOutput`, and `VideoDecoderOutput` take `maxDelayUs` instead of `maxAgeUs`. `TrackInfo.maxAgeUs` keeps its name: it is the publisher's retention. +```swift +// Before +let subscription = Subscription(maxAgeUs: 500_000) +// After +let subscription = Subscription(maxDelayUs: 500_000) +``` + ### Error payloads are named `reason` Cases that carried a positional string (`Transport`, `Internal`, `Connect`, ...) take a `reason:` label. +```swift +// Before +throw MoqError.Transport("reset") +// After +throw MoqError.Transport(reason: "reset") +``` + ### `Transport` gains `.webTransport` A WebTransport session reports `.webTransport`; `.quic` now means raw QUIC only, so an exhaustive `switch` needs one more case. +```swift +// Before +let quic = request.transport == .quic +// After +let quic = request.transport == .quic || request.transport == .webTransport +``` + ### `stalled` is `enabled` (#4915) `Media.Video.stalled` is `enabled`, defaulting to true with the meaning inverted, and `Media.Audio` gains `enabled`. +```swift +// Before +if !video.stalled { select(video) } +// After +if video.enabled { select(video) } +``` + ### Routes have one cost (#4661) `Route.cold` is removed. +```swift +// Before +let route = Route(cost: 10, cold: 50) +// After +let route = Route(cost: 10) +``` + ### Decoded video frames are objects `VideoDecodedFrame` has `timestampUs()`, `width()`, `height()`, `pixels(format:)`, and `surface()` instead of stored fields. @@ -143,6 +203,13 @@ let rgba = try frame.pixels(format: .rgba) `publishMedia` is `publishAudio`, `publishVideo`, or `publishContainer`, each taking its own init. The raw encoder paths that used to be `publishAudio` / `publishVideo` are `encodeAudio` / `encodeVideo`. +```swift +// Before +let media = try broadcast.publishMedia(format: "opus", initData: opusHead) +// After +let media = try broadcast.publishAudio(format: .opus, initData: opusHead) +``` + ### Renames | Before | After | @@ -159,3 +226,12 @@ The raw encoder paths that used to be `publishAudio` / `publishVideo` are `encod `MoqError.Protocol` carries `details` (scope, wire code, kind) instead of a flattened message. Setters throw (`.Busy` during connect or listen) instead of dropping the value. Track and group `finish()` keep the handle open, so a later `abort()` can still run. + +```swift +// Before +catch MoqError.Protocol(let message) { print(message) } +client.setPublish(origin) +// After +catch MoqError.Protocol(let details) { print(details.kind, details.code) } +try client.setPublish(origin) +``` From 0140221d378bd8a484483d229706b8328cbd818e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 18:36:32 -0700 Subject: [PATCH 12/40] docs(moq-net): migrate custom transports to send groups (#5711) Co-Authored-By: Claude Opus 5.5 --- rs/moq-net/MIGRATE.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/rs/moq-net/MIGRATE.md b/rs/moq-net/MIGRATE.md index 40364bbb3e..43ad12e508 100644 --- a/rs/moq-net/MIGRATE.md +++ b/rs/moq-net/MIGRATE.md @@ -4,6 +4,22 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Custom transports declare send groups (#5711) + +A `transport::poll::SendStream` declares `type Group` (`Ungrouped` when the transport has none) and implements `set_group`, and `set_priority` takes an `i64`. +A `transport::poll::Session` implements `send_group`, returning `None` without groups. + +```rust +// Before +fn set_priority(&mut self, order: i32) { ... } +// After +type Group = Ungrouped; +fn set_priority(&mut self, priority: i64) { ... } +fn set_group(&mut self, group: &Ungrouped) { match *group {} } +// and on the Session +fn send_group(&mut self, _priority: i64) -> Option { None } +``` + ### Superseded groups age by wall clock too (#5548) A superseded group is stale once its successor arrived a full `max_delay` ago, as well as by media time. From c8bee58a1b925f44d702873a1b553bc729752399 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 18:44:11 -0700 Subject: [PATCH 13/40] docs: add the moq-clock-macros MIGRATE.md stub Co-Authored-By: Claude Opus 5.5 --- doc/setup/upgrade.md | 2 +- rs/moq-clock/macros/MIGRATE.md | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 rs/moq-clock/macros/MIGRATE.md diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index 20d219f79a..aeb2d58586 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -22,7 +22,7 @@ error lists and rerun. - **Binaries:** [moq-relay](https://github.com/moq-dev/moq/blob/main/rs/moq-relay/MIGRATE.md), [moq-cli (`moq`)](https://github.com/moq-dev/moq/blob/main/rs/moq-cli/MIGRATE.md), [moq-gst](https://github.com/moq-dev/moq/blob/main/rs/moq-gst/MIGRATE.md), [obs-moq](https://github.com/moq-dev/moq/blob/main/cpp/obs/MIGRATE.md) - **Bindings:** [moq-ffi (UniFFI)](https://github.com/moq-dev/moq/blob/main/rs/moq-ffi/MIGRATE.md), [moq-c (C)](https://github.com/moq-dev/moq/blob/main/rs/moq-c/MIGRATE.md), [Python](https://github.com/moq-dev/moq/blob/main/py/moq-rs/MIGRATE.md), [Go](https://github.com/moq-dev/moq/blob/main/go/wrapper/MIGRATE.md), [Swift](https://github.com/moq-dev/moq/blob/main/swift/MIGRATE.md), [Kotlin](https://github.com/moq-dev/moq/blob/main/kt/moq/MIGRATE.md), [Dart](https://github.com/moq-dev/moq/blob/main/dart/moq/MIGRATE.md), [C++](https://github.com/moq-dev/moq/blob/main/cpp/moq/MIGRATE.md) -- **Rust:** [hang](https://github.com/moq-dev/moq/blob/main/rs/hang/MIGRATE.md), [kio](https://github.com/moq-dev/moq/blob/main/rs/kio/MIGRATE.md), [moq-archive](https://github.com/moq-dev/moq/blob/main/rs/moq-archive/MIGRATE.md), [moq-audio](https://github.com/moq-dev/moq/blob/main/rs/moq-audio/MIGRATE.md), [moq-auth](https://github.com/moq-dev/moq/blob/main/rs/moq-auth/MIGRATE.md), [moq-boy](https://github.com/moq-dev/moq/blob/main/rs/moq-boy/MIGRATE.md), [moq-clock](https://github.com/moq-dev/moq/blob/main/rs/moq-clock/MIGRATE.md), [moq-e2ee](https://github.com/moq-dev/moq/blob/main/rs/moq-e2ee/MIGRATE.md), [moq-flate](https://github.com/moq-dev/moq/blob/main/rs/moq-flate/MIGRATE.md), [moq-hls](https://github.com/moq-dev/moq/blob/main/rs/moq-hls/MIGRATE.md), [moq-json](https://github.com/moq-dev/moq/blob/main/rs/moq-json/MIGRATE.md), [moq-loc](https://github.com/moq-dev/moq/blob/main/rs/moq-loc/MIGRATE.md), [moq-msf](https://github.com/moq-dev/moq/blob/main/rs/moq-msf/MIGRATE.md), [moq-mux](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/MIGRATE.md), [moq-net](https://github.com/moq-dev/moq/blob/main/rs/moq-net/MIGRATE.md), [moq-nvenc](https://github.com/moq-dev/moq/blob/main/rs/moq-nvenc/MIGRATE.md), [moq-pattern](https://github.com/moq-dev/moq/blob/main/rs/moq-pattern/MIGRATE.md), [moq-quic](https://github.com/moq-dev/moq/blob/main/rs/moq-quic/MIGRATE.md), [moq-room](https://github.com/moq-dev/moq/blob/main/rs/moq-room/MIGRATE.md), [moq-rtc](https://github.com/moq-dev/moq/blob/main/rs/moq-rtc/MIGRATE.md), [moq-rtmp](https://github.com/moq-dev/moq/blob/main/rs/moq-rtmp/MIGRATE.md), [moq-sock](https://github.com/moq-dev/moq/blob/main/rs/moq-sock/MIGRATE.md), [moq-srt](https://github.com/moq-dev/moq/blob/main/rs/moq-srt/MIGRATE.md), [moq-stats](https://github.com/moq-dev/moq/blob/main/rs/moq-stats/MIGRATE.md), [moq-tokio](https://github.com/moq-dev/moq/blob/main/rs/moq-tokio/MIGRATE.md), [moq-transcode](https://github.com/moq-dev/moq/blob/main/rs/moq-transcode/MIGRATE.md), [moq-uring](https://github.com/moq-dev/moq/blob/main/rs/moq-uring/MIGRATE.md), [moq-v4l](https://github.com/moq-dev/moq/blob/main/rs/moq-v4l/MIGRATE.md), [moq-video](https://github.com/moq-dev/moq/blob/main/rs/moq-video/MIGRATE.md) +- **Rust:** [hang](https://github.com/moq-dev/moq/blob/main/rs/hang/MIGRATE.md), [kio](https://github.com/moq-dev/moq/blob/main/rs/kio/MIGRATE.md), [moq-archive](https://github.com/moq-dev/moq/blob/main/rs/moq-archive/MIGRATE.md), [moq-audio](https://github.com/moq-dev/moq/blob/main/rs/moq-audio/MIGRATE.md), [moq-auth](https://github.com/moq-dev/moq/blob/main/rs/moq-auth/MIGRATE.md), [moq-boy](https://github.com/moq-dev/moq/blob/main/rs/moq-boy/MIGRATE.md), [moq-clock](https://github.com/moq-dev/moq/blob/main/rs/moq-clock/MIGRATE.md), [moq-clock-macros](https://github.com/moq-dev/moq/blob/main/rs/moq-clock/macros/MIGRATE.md), [moq-e2ee](https://github.com/moq-dev/moq/blob/main/rs/moq-e2ee/MIGRATE.md), [moq-flate](https://github.com/moq-dev/moq/blob/main/rs/moq-flate/MIGRATE.md), [moq-hls](https://github.com/moq-dev/moq/blob/main/rs/moq-hls/MIGRATE.md), [moq-json](https://github.com/moq-dev/moq/blob/main/rs/moq-json/MIGRATE.md), [moq-loc](https://github.com/moq-dev/moq/blob/main/rs/moq-loc/MIGRATE.md), [moq-msf](https://github.com/moq-dev/moq/blob/main/rs/moq-msf/MIGRATE.md), [moq-mux](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/MIGRATE.md), [moq-net](https://github.com/moq-dev/moq/blob/main/rs/moq-net/MIGRATE.md), [moq-nvenc](https://github.com/moq-dev/moq/blob/main/rs/moq-nvenc/MIGRATE.md), [moq-pattern](https://github.com/moq-dev/moq/blob/main/rs/moq-pattern/MIGRATE.md), [moq-quic](https://github.com/moq-dev/moq/blob/main/rs/moq-quic/MIGRATE.md), [moq-room](https://github.com/moq-dev/moq/blob/main/rs/moq-room/MIGRATE.md), [moq-rtc](https://github.com/moq-dev/moq/blob/main/rs/moq-rtc/MIGRATE.md), [moq-rtmp](https://github.com/moq-dev/moq/blob/main/rs/moq-rtmp/MIGRATE.md), [moq-sock](https://github.com/moq-dev/moq/blob/main/rs/moq-sock/MIGRATE.md), [moq-srt](https://github.com/moq-dev/moq/blob/main/rs/moq-srt/MIGRATE.md), [moq-stats](https://github.com/moq-dev/moq/blob/main/rs/moq-stats/MIGRATE.md), [moq-tokio](https://github.com/moq-dev/moq/blob/main/rs/moq-tokio/MIGRATE.md), [moq-transcode](https://github.com/moq-dev/moq/blob/main/rs/moq-transcode/MIGRATE.md), [moq-uring](https://github.com/moq-dev/moq/blob/main/rs/moq-uring/MIGRATE.md), [moq-v4l](https://github.com/moq-dev/moq/blob/main/rs/moq-v4l/MIGRATE.md), [moq-video](https://github.com/moq-dev/moq/blob/main/rs/moq-video/MIGRATE.md) - **JavaScript:** [`@moq/auth`](https://github.com/moq-dev/moq/blob/main/js/auth/MIGRATE.md), [`@moq/boy`](https://github.com/moq-dev/moq/blob/main/js/moq-boy/MIGRATE.md), [`@moq/clock`](https://github.com/moq-dev/moq/blob/main/js/clock/MIGRATE.md), [`@moq/flate`](https://github.com/moq-dev/moq/blob/main/js/flate/MIGRATE.md), [`@moq/hang`](https://github.com/moq-dev/moq/blob/main/js/hang/MIGRATE.md), [`@moq/json`](https://github.com/moq-dev/moq/blob/main/js/json/MIGRATE.md), [`@moq/loc`](https://github.com/moq-dev/moq/blob/main/js/loc/MIGRATE.md), [`@moq/msf`](https://github.com/moq-dev/moq/blob/main/js/msf/MIGRATE.md), [`@moq/net`](https://github.com/moq-dev/moq/blob/main/js/net/MIGRATE.md), [`@moq/p2p`](https://github.com/moq-dev/moq/blob/main/js/p2p/MIGRATE.md), [`@moq/pattern`](https://github.com/moq-dev/moq/blob/main/js/pattern/MIGRATE.md), [`@moq/publish`](https://github.com/moq-dev/moq/blob/main/js/publish/MIGRATE.md), [`@moq/room`](https://github.com/moq-dev/moq/blob/main/js/room/MIGRATE.md), [`@moq/signals`](https://github.com/moq-dev/moq/blob/main/js/signals/MIGRATE.md), [`@moq/video`](https://github.com/moq-dev/moq/blob/main/js/video/MIGRATE.md), [`@moq/watch`](https://github.com/moq-dev/moq/blob/main/js/watch/MIGRATE.md) ## Unreleased diff --git a/rs/moq-clock/macros/MIGRATE.md b/rs/moq-clock/macros/MIGRATE.md new file mode 100644 index 0000000000..f7e0a138a8 --- /dev/null +++ b/rs/moq-clock/macros/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-clock-macros + +How to update your code for each breaking change, newest first. + +## Unreleased From b263856f99b36612ffa7583f7a6aa1b6ad689679 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 18:50:45 -0700 Subject: [PATCH 14/40] docs: move the --hop cluster-id note to moq-cli, name the MOQ_HOP exception Co-Authored-By: Claude Opus 5.5 --- doc/setup/upgrade.md | 3 ++- rs/moq-cli/MIGRATE.md | 1 + rs/moq-relay/MIGRATE.md | 3 +-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index aeb2d58586..982edac7e2 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -16,7 +16,8 @@ wire changes that decide the order to upgrade a deployment. A released flag, environment variable, or config key that was renamed is refused at startup with its replacement named, rather than ignored. Fix what the -error lists and rerun. +error lists and rerun. The one exception is moq-cli's removed `MOQ_HOP`, which is +ignored; its guide says what replaces it. ## Migration guides diff --git a/rs/moq-cli/MIGRATE.md b/rs/moq-cli/MIGRATE.md index 392630bc8c..f3b0268392 100644 --- a/rs/moq-cli/MIGRATE.md +++ b/rs/moq-cli/MIGRATE.md @@ -74,6 +74,7 @@ moq ... play --delay 100ms A redundant pair shares an epoch, a UUIDv7 such as `uuidgen -7` prints, instead of a Hop ID. `--hop` is an unknown flag and `MOQ_HOP` is silently ignored, so remove it: a pair still keyed on `MOQ_HOP` mints an epoch per process and each restart replaces the other's broadcast. +A node that pinned its cluster id with `--hop` passes `--cluster-id` instead. ```bash # Before diff --git a/rs/moq-relay/MIGRATE.md b/rs/moq-relay/MIGRATE.md index 853c3647b5..2de8bf3f9f 100644 --- a/rs/moq-relay/MIGRATE.md +++ b/rs/moq-relay/MIGRATE.md @@ -79,9 +79,8 @@ moq-relay --cluster-mesh --cluster-linger 10s --cluster-connect https://root.exa moq-relay --cluster-connect https://root.example/,https://b.example/ ... ``` -### `--cluster-id` no longer falls back to `--hop` (#4969) +### A route without a publisher keeps hop 0 (#4969) -A node that pinned its id with `--hop` passes `--cluster-id`. A route that names no publisher keeps its 0 instead of gaining a random hop. ### Embedding: auth takes the client-CA answer (#4912) From b51918e1b443dbcf046d857a99577c1ac9d96aa9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 19:21:52 -0700 Subject: [PATCH 15/40] docs: fold binary migration links into existing paragraphs, fix review nits Co-Authored-By: Claude Opus 5.5 --- dart/moq/MIGRATE.md | 2 +- doc/bin/cli.md | 8 ++------ doc/bin/relay/index.md | 9 +++------ py/moq-rs/MIGRATE.md | 2 +- rs/moq-relay/MIGRATE.md | 2 +- 5 files changed, 8 insertions(+), 15 deletions(-) diff --git a/dart/moq/MIGRATE.md b/dart/moq/MIGRATE.md index c2fb8311de..b8282bee45 100644 --- a/dart/moq/MIGRATE.md +++ b/dart/moq/MIGRATE.md @@ -44,7 +44,7 @@ final n = moq.connects; ### The consume origin is `consume` `ConnectOptions.subscribe` is `consume`. -A value the native side cannot use fails `Moq.connect` with `MoqException.Config`, including a bad bind address. +A value the native side cannot use fails `Moq.connect` with `ConfigMoqException`, including a bad bind address. A server request takes its origins in `accept(publish: ..., consume: ...)`, where null inherits the server's origin. ```dart diff --git a/doc/bin/cli.md b/doc/bin/cli.md index 9ed1c22435..05f6907631 100644 --- a/doc/bin/cli.md +++ b/doc/bin/cli.md @@ -62,7 +62,8 @@ each session can publish or subscribe to, including stats; see the relay's [statistics configuration](/bin/relay/config#stats). `moq import --help` lists the sources and `moq import rtmp --help` a specific -one; every flag is documented there. +one; every flag is documented there. Flags renamed between releases are listed in +[MIGRATE.md](https://github.com/moq-dev/moq/blob/main/rs/moq-cli/MIGRATE.md). ```bash # Publish a file (remux to MPEG-TS without re-encoding) @@ -404,8 +405,3 @@ so the other formats refuse `--linger`. relay is reachable and shows what it holds; see [Inspect a relay](/bin/inspect). Connection refused means UDP isn't getting through; certificate errors on a dev relay want `--connect-tls-insecure` or the `http://` fingerprint flow. - -## Upgrading - -Renamed flags, environment variables, and config keys are listed, newest -first, in moq-cli's [MIGRATE.md](https://github.com/moq-dev/moq/blob/main/rs/moq-cli/MIGRATE.md). diff --git a/doc/bin/relay/index.md b/doc/bin/relay/index.md index c181e56e87..e0e8d46257 100644 --- a/doc/bin/relay/index.md +++ b/doc/bin/relay/index.md @@ -48,7 +48,9 @@ Every option is also a `--flag` or `MOQ_*` environment variable, and `RUST_LOG` controls logging. The [configuration reference](/bin/relay/config) covers every section, and [`demo/relay/`](https://github.com/moq-dev/moq/tree/main/demo/relay) has -working configs for development, production, and a cluster. +working configs for development, production, and a cluster. Options renamed +between releases are listed in +[MIGRATE.md](https://github.com/moq-dev/moq/blob/main/rs/moq-relay/MIGRATE.md). ## Embed @@ -101,8 +103,3 @@ See [docs.rs/moq-relay](https://docs.rs/moq-relay) and - **Certificate errors**: the hostname must match the certificate. Local browsers need the fingerprint served over `[web.http]`. - **Connection timeout**: UDP isn't reaching the relay, or the client URL names the wrong port. - **Unauthorized / forbidden**: the token's paths don't cover the connection path, or the broadcast a session asked for. See [path matching](/bin/relay/auth#path-matching). - -## Upgrading - -Renamed flags, environment variables, and config keys are listed, newest -first, in moq-relay's [MIGRATE.md](https://github.com/moq-dev/moq/blob/main/rs/moq-relay/MIGRATE.md). diff --git a/py/moq-rs/MIGRATE.md b/py/moq-rs/MIGRATE.md index 5185539d9a..054164f058 100644 --- a/py/moq-rs/MIGRATE.md +++ b/py/moq-rs/MIGRATE.md @@ -141,7 +141,7 @@ track.write_frame(payload, timestamp=pts) ### Subscriber staleness is `max_delay` -`Subscription.max_age_us` is `max_delay`, a `timedelta`, and the decoder outputs take `max_delay_us`. +`Subscription.max_age_us` is `max_delay`, a `timedelta`, and the decoder outputs take `max_delay`, also a `timedelta`. `TrackInfo` keeps its `max_age`: it is the publisher's retention. ```python diff --git a/rs/moq-relay/MIGRATE.md b/rs/moq-relay/MIGRATE.md index 2de8bf3f9f..f5b0649304 100644 --- a/rs/moq-relay/MIGRATE.md +++ b/rs/moq-relay/MIGRATE.md @@ -43,7 +43,7 @@ cargo build -p moq-relay --no-default-features --features quic `listen.lb_nonce` (`--listen-quic-lb-nonce`, `MOQ_LISTEN_QUIC_LB_NONCE`) without `lb_id` stops startup. Setting `lb_id` or `lb_nonce` together with `load_balancer` also stops; `lb_id` used to win. -Set one of them. +Choose either `load_balancer` or the `lb_id` and `lb_nonce` pair. ```bash # Before From c4d42d75403eebdf721dee1a2343960d6a610e17 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 19:38:14 -0700 Subject: [PATCH 16/40] docs(kio): drop the wait -> Pending mapping, wait is still public Co-Authored-By: Claude Opus 5.5 --- rs/kio/MIGRATE.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/rs/kio/MIGRATE.md b/rs/kio/MIGRATE.md index ff0ef163aa..b60760117a 100644 --- a/rs/kio/MIGRATE.md +++ b/rs/kio/MIGRATE.md @@ -8,19 +8,16 @@ How to update your code for each breaking change, newest first. `Fan`, `Hold`, `WeakLock`, `Pollable`, and the public `Lock::downgrade` are gone. `Task` gained an `Output` type and has a blanket impl for `FnMut(&Waiter) -> Poll`. -`wait(f)` is `Pending::new(f)`. `poll_used` and `poll_unused` return `Poll>` instead of `Poll>`. `Waiter::context()` replaces `Context::from_waker(waiter.waker())`. ```rust // Before -let value = kio::wait(|waiter: &kio::Waiter| state.poll(waiter)).await; match producer.poll_unused(waiter) { Poll::Ready(None) => {} // closed _ => {} } // After -let value = kio::Pending::new(|waiter: &kio::Waiter| state.poll(waiter)).await; match producer.poll_unused(waiter) { Poll::Ready(Err(kio::Closed)) => {} _ => {} From 91fbb2233be1e9d951b60461740b2bfa01eb755e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 21:13:31 -0700 Subject: [PATCH 17/40] docs(py): await the video consumer subscription in MIGRATE.md Co-Authored-By: Claude Opus 5.5 --- py/moq-rs/MIGRATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/py/moq-rs/MIGRATE.md b/py/moq-rs/MIGRATE.md index 054164f058..4115bd2d79 100644 --- a/py/moq-rs/MIGRATE.md +++ b/py/moq-rs/MIGRATE.md @@ -8,7 +8,7 @@ How to update your code for each breaking change, newest first. `BroadcastProducer.encode_audio` / `encode_video` are `moq.audio.Producer` / `moq.video.Producer`, built from a `moq.media.CatalogProducer`. The audio track name moves into `EncoderOutput(track=...)`, and durations and timestamps are `timedelta` (`frame_duration`, `Frame.timestamp`) instead of `_us` integers; a zero `frame_duration` takes the codec's own frame. -`BroadcastConsumer.decode_audio` / `decode_video` are `await moq.audio.Consumer.subscribe(broadcast, ...)` / `moq.video.Consumer.subscribe`. +`BroadcastConsumer.decode_audio` / `decode_video` are `await moq.audio.Consumer.subscribe(broadcast, ...)` / `await moq.video.Consumer.subscribe(broadcast, ...)`. Producers expose subscriber state only through `demand()`, and a video `gop` is `moq.video.Gop.KEYFRAME(interval=...)`. `moq.audio.Encoder` / `moq.video.Encoder` encode without publishing. From 5716e12628774815c8a6c6b75d897e3ca9ddfb86 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 21:35:16 -0700 Subject: [PATCH 18/40] docs: migrate archive per-track durations (#5715) Co-Authored-By: Claude Opus 5.5 --- doc/setup/upgrade.md | 5 +++++ js/hang/MIGRATE.md | 15 +++++++++++++++ rs/hang/MIGRATE.md | 14 ++++++++++++++ rs/moq-mux/MIGRATE.md | 14 ++++++++++++++ rs/moq-video/MIGRATE.md | 11 +++++++++++ 5 files changed, 59 insertions(+) diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index 982edac7e2..e72c66813c 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -30,6 +30,11 @@ ignored; its guide says what replaces it. These land with the next breaking release. +- **Archive timelines declare a duration** (#5715). Each catalog + `archive.timelines` entry is an object instead of a track name. Neither side + reads the other's shape, and a catalog it can't read keeps its live media but + drops the archive, so seeking, recording, and HLS history need publishers and + their readers upgraded together. - **Announce restarts.** moq-lite 07 adds `ANNOUNCE_RESTART` (0x3): another publisher instance now serves the prefix. lite-06's `ANNOUNCE_RESTART` is named `ANNOUNCE_UPDATE`, as it always meant. On moq-lite 06 and older and diff --git a/js/hang/MIGRATE.md b/js/hang/MIGRATE.md index d77211ed47..30a1e7e396 100644 --- a/js/hang/MIGRATE.md +++ b/js/hang/MIGRATE.md @@ -4,6 +4,21 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Archive timelines declare a per-track duration (#5715) + +Each `archive.timelines` value is `{ track, duration }` instead of the timeline track's name, and `archive.durationMax` is removed. +`duration` is the nominal record duration in the archive timescale, absent for an untimed track. +A catalog on the other side of this change parses with no `archive`; see the [upgrade guide](https://github.com/moq-dev/moq/blob/main/doc/setup/upgrade.md). + +```ts +// Before +const track = catalog.archive?.timelines.video; +const bound = catalog.archive?.durationMax; +// After +const track = catalog.archive?.timelines.video?.track; +const duration = catalog.archive?.timelines.video?.duration; +``` + ### Timeline records are optionally timed (#5720) `Timeline.Record.pts` and `.duration` are optional, absent on an untimed track. diff --git a/rs/hang/MIGRATE.md b/rs/hang/MIGRATE.md index 2fff9311ab..63a8d89b52 100644 --- a/rs/hang/MIGRATE.md +++ b/rs/hang/MIGRATE.md @@ -4,6 +4,20 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Archive timelines declare a per-track duration (#5715) + +Each `Archive::timelines` value is a `Timeline` (the timeline `track` plus its nominal `duration` in the archive timescale, `None` when untimed) instead of the timeline track's name, and `Archive::duration_max` is removed. +A catalog on the other side of this change decodes with no archive; see the [upgrade guide](https://github.com/moq-dev/moq/blob/main/doc/setup/upgrade.md). + +```rust +// Before +archive.timelines.insert("video".to_string(), "video.timeline.z".to_string()); +let track: &String = &archive.timelines["video"]; +// After +archive.timelines.insert("video".to_string(), Timeline::new("video.timeline.z", 4_000)); +let track: &String = &archive.timelines["video"].track; +``` + ### Timeline records are optionally timed (#5720) `timeline::Record::pts` and `duration` are `Option`, absent on an untimed track. diff --git a/rs/moq-mux/MIGRATE.md b/rs/moq-mux/MIGRATE.md index 96510ef619..eb1ab21cdd 100644 --- a/rs/moq-mux/MIGRATE.md +++ b/rs/moq-mux/MIGRATE.md @@ -4,6 +4,20 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Timeline durations are hints (#5715) + +`timeline::Config::duration_max`, `with_duration_max`, and `DEFAULT_DURATION_MAX` are gone; set `duration_hint` with `with_duration_hint`, the expected record duration such as the GOP, and the split ceiling follows from it. +`DEFAULT_DURATION_MIN` is 4 s instead of 2 s, so set `with_duration_min` to keep the shorter records. + +```rust +// Before +let config = timeline::Config::default().with_duration_max(Duration::from_secs(10)); +// After +let config = timeline::Config::default() + .with_duration_hint(gop) + .with_duration_min(Duration::from_secs(2)); +``` + ### The catalog consumer is opaque (#5630) Construct `catalog::Consumer` with `Consumer::new(broadcast, format).await`, or convert a `catalog::hang::Consumer` with `.into()`, then read it through `catalog::Stream`. diff --git a/rs/moq-video/MIGRATE.md b/rs/moq-video/MIGRATE.md index f2e7bee478..275935ac54 100644 --- a/rs/moq-video/MIGRATE.md +++ b/rs/moq-video/MIGRATE.md @@ -4,6 +4,17 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `encode::Producer` takes a catalog reservation (#5715) + +`encode::Producer::new` and `with_track` take a `moq_mux::catalog::Reserved` instead of the `catalog::Producer`, so the caller declares the encoder's GOP as the timeline hint. + +```rust +// Before +let producer = encode::Producer::new(broadcast, catalog, rendition)?; +// After +let producer = encode::Producer::new(broadcast, catalog.reserve().with_timeline_hint(gop), rendition)?; +``` + ### Capture takes one `encode::Capture`, and `Control::new` returns the driver `publish_capture(broadcast, catalog, capture, encode, clock)` is `publish_capture(broadcast, catalog, encode::Capture)`. From c3b414db03dc659e6dade60994ac56f8eac9fd1c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 22:01:46 -0700 Subject: [PATCH 19/40] docs: migrate auth Access caps, webtransport rollout, binding gaps Cover #5697's Access roles in moq-auth, moq-relay, and @moq/auth, add the upgrade-auth-before-relays rollout bullet for webtransport, fix the Go decoder MaxDelay name, and add Python WEB_TRANSPORT and Swift close() entries. Co-Authored-By: Claude Opus 5.5 --- doc/setup/upgrade.md | 3 +++ go/wrapper/MIGRATE.md | 2 +- js/auth/MIGRATE.md | 14 ++++++++++++++ py/moq-rs/MIGRATE.md | 11 +++++++++++ rs/moq-auth/MIGRATE.md | 15 +++++++++++++++ rs/moq-relay/MIGRATE.md | 13 +++++++++++++ swift/MIGRATE.md | 13 ++++++++++++- 7 files changed, 69 insertions(+), 2 deletions(-) diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index e72c66813c..5e3fe1a25f 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -30,6 +30,9 @@ ignored; its guide says what replaces it. These land with the next breaking release. +- **Auth requests name `webtransport`** (#5293). Relays report a WebTransport + session as `webtransport` instead of `quic`, and older auth servers reject the + new value, so upgrade auth servers before relays. - **Archive timelines declare a duration** (#5715). Each catalog `archive.timelines` entry is an object instead of a track name. Neither side reads the other's shape, and a catalog it can't read keeps its live media but diff --git a/go/wrapper/MIGRATE.md b/go/wrapper/MIGRATE.md index b5fb702ed7..f8938aa082 100644 --- a/go/wrapper/MIGRATE.md +++ b/go/wrapper/MIGRATE.md @@ -138,7 +138,7 @@ err := track.WriteFrame(moq.Frame{Payload: payload, Timestamp: &pts}) ### Subscriber staleness is `MaxDelay` -`Subscription.MaxAgeUs` is `MaxDelay`, and the decoder outputs take `MaxDelayUs`. +`Subscription.MaxAgeUs` is `MaxDelay`, and `audio.DecoderOutput` / `video.DecoderOutput` take `MaxDelay *time.Duration`. `TrackInfo.MaxAge` keeps its name: it is the publisher's retention. ```go diff --git a/js/auth/MIGRATE.md b/js/auth/MIGRATE.md index 49e6a2e894..a8393bf456 100644 --- a/js/auth/MIGRATE.md +++ b/js/auth/MIGRATE.md @@ -4,6 +4,20 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Roles carry `Access` (#5697) + +Parsed `Claims`, `Grant`, and `authorize`'s permissions hold `publish` and `subscribe` as `Access`: path patterns plus an optional per-connection `rate` cap. +Read the patterns from `.path`. `Key.sign` still takes pattern strings, but a hand-built `Claims` value must come from `ClaimsSchema.parse`. +A gateway must enforce a cap or refuse the grant before admitting media. + +```ts +// Before +const allowed = permissions.publish.matches(path); +// After +const allowed = permissions.publish.path.matches(path); +if (permissions.publish.rate !== undefined) throw new Error("bitrate caps are not enforced"); +``` + ### A token must carry its key's `kid` (#3943) `Key.verify` refuses a token whose `kid` header does not match the key's, and a key set accepts a token without a `kid` only when it holds one key that has none. diff --git a/py/moq-rs/MIGRATE.md b/py/moq-rs/MIGRATE.md index 4115bd2d79..4b2f02a504 100644 --- a/py/moq-rs/MIGRATE.md +++ b/py/moq-rs/MIGRATE.md @@ -162,6 +162,17 @@ reason = err[0] reason = err.reason ``` +### `Transport` gains `WEB_TRANSPORT` + +A WebTransport session reports `moq.Transport.WEB_TRANSPORT`; `QUIC` now means raw QUIC only, so a server that admits only `QUIC` refuses every browser. + +```python +# Before +quic = request.transport == moq.Transport.QUIC +# After +quic = request.transport in (moq.Transport.QUIC, moq.Transport.WEB_TRANSPORT) +``` + ### `stalled` is `enabled` (#4915) `moq.media.Video.stalled` is `enabled`, defaulting to `True` with the meaning inverted, and `moq.media.Audio` gains `enabled`. diff --git a/rs/moq-auth/MIGRATE.md b/rs/moq-auth/MIGRATE.md index c2a6090b8a..d671be09e7 100644 --- a/rs/moq-auth/MIGRATE.md +++ b/rs/moq-auth/MIGRATE.md @@ -4,6 +4,21 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Roles carry `Access` (#5697) + +`publish` and `subscribe` on `Claims`, `Permissions`, and `Grant` are `Access`, which holds the path patterns and an optional per-connection `Rate` cap. +Read the patterns from `.path`; constructors and `Claims::with_publish` / `with_subscribe` take anything `Into`, such as a `Pattern`, `Patterns`, or array, so collect a pattern iterator first. +A capped role serializes as `{"path":"alice/**","rate":8000000}`, and key scopes refuse caps. +A gateway that embeds this crate must enforce a cap or refuse the grant before admitting media. + +```rust +// Before +let allowed = permissions.publish.matches(path); +// After +let allowed = permissions.publish.path.matches(path); +let cap = permissions.publish.rate.map(|rate| rate.as_bps()); // enforce, or refuse when Some +``` + ### Gateway transports and key sets are non-exhaustive (#3943) `Transport` gains `Rtmp`, `Srt`, `WebRtc`, and `WebTransport`, and `serve::Keys` gains `Set`; both are `#[non_exhaustive]`, so add a wildcard arm. diff --git a/rs/moq-relay/MIGRATE.md b/rs/moq-relay/MIGRATE.md index f5b0649304..1fff9f9e3a 100644 --- a/rs/moq-relay/MIGRATE.md +++ b/rs/moq-relay/MIGRATE.md @@ -5,6 +5,19 @@ Rolling a cluster across a release also follows the [upgrade guide](https://moq. ## Unreleased +### Auth roles carry `moq_auth::Access` (#5697) + +`auth::Token::publish` and `subscribe` are `moq_auth::Access`, so an embedder reads the patterns from `.path`. +The relay refuses every grant with a bitrate cap, including one added in-band or at revalidation, because no transport meters it yet; HTTP `/fetch` and `/announced` always refuse caps. +An embedder running its own gateway must enforce the cap or refuse the grant before forwarding media. + +```rust +// Before +let patterns = &token.publish; +// After +let patterns = &token.publish.path; +``` + ### Workers default to unpinned (#5688) `runtime.pin` defaults to off, so two worker groups on one host no longer crowd onto the first cores. diff --git a/swift/MIGRATE.md b/swift/MIGRATE.md index 5f129d69d2..16bec8266a 100644 --- a/swift/MIGRATE.md +++ b/swift/MIGRATE.md @@ -89,7 +89,7 @@ let state = try Json.SnapshotProducer(broadcast: broadcast, track: try br ### Media lives in `Media` -`BroadcastProducer.publishAudio`, `publishVideo`, `publishContainer`, their `on:` and stream variants, `setVideoProperties`, `setCatalogSection`, and `finish`, and the consumer's `subscribeCatalog`, `subscribeMedia`, and `fetchMediaGroup`, are gone. +`BroadcastProducer.publishAudio`, `publishVideo`, `publishContainer`, their `on:` and stream variants, `setVideoProperties`, and `setCatalogSection`, and the consumer's `subscribeCatalog`, `subscribeMedia`, and `fetchMediaGroup`, are gone. Use `Media.TrackProducer.audio` / `video`, `Media.ContainerProducer`, `Media.CatalogProducer`, `Media.CatalogConsumer.subscribe`, `Media.TrackConsumer.subscribe`, and `Media.GroupConsumer.fetch`. The catalog types (`Catalog`, `Video`, `Audio`, `AudioFormat`, ...) moved under `Media`. Producers drop `name`, `used`, and `unused`; read them through `demand()`. @@ -196,6 +196,17 @@ let rgba = frame.data let rgba = try frame.pixels(format: .rgba) ``` +### `BroadcastProducer.finish()` is `close()` (#4152) + +Close the broadcast instead; tracks already subscribed carry on to their own end. + +```swift +// Before +try broadcast.finish() +// After +try broadcast.close() +``` + ## 0.4 -> 0.5 ### Publish split by kind From e64ce3284f928a169e3c657e7597001e16c2006d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 16 Oct 2026 22:27:45 -0700 Subject: [PATCH 20/40] docs(upgrade): read MIGRATE.md at the target release's tag Co-Authored-By: Claude Opus 5.5 --- doc/setup/upgrade.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index 5e3fe1a25f..32eeb05db0 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -6,8 +6,10 @@ description: The order to roll out a release across relays, auth servers, and cl # Upgrade Every hand-written published package keeps a `MIGRATE.md` with a before/after -snippet for each breaking change, newest first. Read your package's file from -the top down to the version you run. Most binding users install a wrapper, +snippet for each breaking change, newest first. The links below track `main`; +for a release, open the file at its tag (such as `moq-relay-v0.17.6`) and apply +the sections from the version you run up to that one. `Unreleased` applies only +when you build from `main`. Most binding users install a wrapper, such as `moq-rs` on PyPI or `dev.moq:moq` on Maven, whose guide is in the index below. The raw UniFFI packages published as `moq-ffi` have no guide of their own: [moq-ffi's](https://github.com/moq-dev/moq/blob/main/rs/moq-ffi/MIGRATE.md) From 92b69822241d1ad422dad8fe2b8529fb29e21c3d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 08:20:39 -0700 Subject: [PATCH 21/40] docs(moq-video): close the encoder color example's code fence Co-Authored-By: Claude Opus 5.5 --- rs/moq-video/MIGRATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/rs/moq-video/MIGRATE.md b/rs/moq-video/MIGRATE.md index 994b43c05c..dff7266a14 100644 --- a/rs/moq-video/MIGRATE.md +++ b/rs/moq-video/MIGRATE.md @@ -33,6 +33,7 @@ let encoder = Encoder::new(&encode::Config::new(width, height, rate))?; let mut config = encode::Config::new(width, height, rate); config.color = frame.surface.color(); let encoder = Encoder::new(&config)?; +``` ### `encode::Producer` takes a catalog reservation (#5715) From 1b2e8adf0c4237f71ed963f0106003df4ae06558 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 11:42:46 -0700 Subject: [PATCH 22/40] docs: migrate #5806 catalog version and #5815 data-track breaks Co-Authored-By: Claude Opus 5.5 --- doc/setup/upgrade.md | 4 ++++ rs/hang/MIGRATE.md | 14 ++++++++++++++ rs/moq-mux/MIGRATE.md | 11 +++++++++++ 3 files changed, 29 insertions(+) diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index 08d2abbc5a..4b61931571 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -32,6 +32,10 @@ ignored; its guide says what replaces it. These land with the next breaking release. +- **Data-track breaks are an object with no bytes** (#5815). A hang data + track (TS verbatim PES, sections, `m2ts`, text) marks a discontinuity with a + group of one empty object instead of a skipped group. Older consumers fail + to decode it, so upgrade exporters and players before publishers. - **Auth requests name `webtransport`** (#5293). Relays report a WebTransport session as `webtransport` instead of `quic`, and older auth servers reject the new value, so upgrade auth servers before relays. diff --git a/rs/hang/MIGRATE.md b/rs/hang/MIGRATE.md index 63a8d89b52..16c48766ce 100644 --- a/rs/hang/MIGRATE.md +++ b/rs/hang/MIGRATE.md @@ -4,6 +4,20 @@ How to update your code for each breaking change, newest first. ## Unreleased +### The catalog root carries a reader `version` (#5806) + +`version` is reserved at the catalog root: decoding refuses a catalog newer than this crate reads with `Error::UnsupportedVersion`, and serializing fails if the extension `E` has its own `version` member. +Rename that member. + +```rust +// Before +#[derive(Serialize, Deserialize)] +struct Extra { version: u32 } +// After +#[derive(Serialize, Deserialize)] +struct Extra { app_version: u32 } +``` + ### Archive timelines declare a per-track duration (#5715) Each `Archive::timelines` value is a `Timeline` (the timeline `track` plus its nominal `duration` in the archive timescale, `None` when untimed) instead of the timeline track's name, and `Archive::duration_max` is removed. diff --git a/rs/moq-mux/MIGRATE.md b/rs/moq-mux/MIGRATE.md index bb4cc87b5e..8a9241375f 100644 --- a/rs/moq-mux/MIGRATE.md +++ b/rs/moq-mux/MIGRATE.md @@ -4,6 +4,17 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `fmp4::Wire::new` refuses a track that isn't audio or video (#5815) + +`container::fmp4::Wire::new` returns `Result`, as `Wire::from_init` already did, and refuses any other handler instead of treating it as data. + +```rust +// Before +let wire = Wire::new(trak); +// After +let wire = Wire::new(trak)?; +``` + ### `ts::Programs::finish` returns each program's `Finish` (#5795) `Programs::finish` returns `anyhow::Result>` instead of `anyhow::Result<()>`. From ef891dafd778f4d02ca67aa04f4585cd010dae72 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 12:10:06 -0700 Subject: [PATCH 23/40] docs: migrate #5695 auth, #5770 datagrams, #5845 reserve, #5727 C++ names Also covers @moq/hang's reserved catalog `version` from #5806. Co-Authored-By: Claude Opus 5.5 --- cpp/moq/MIGRATE.md | 23 +++++++++++++++++++++++ cpp/obs/MIGRATE.md | 5 +++++ dart/moq/MIGRATE.md | 11 +++++++++++ go/wrapper/MIGRATE.md | 12 ++++++++++++ js/hang/MIGRATE.md | 12 ++++++++++++ js/net/MIGRATE.md | 11 +++++++++++ kt/moq/MIGRATE.md | 11 +++++++++++ py/moq-rs/MIGRATE.md | 11 +++++++++++ rs/moq-ffi/MIGRATE.md | 11 +++++++++++ rs/moq-net/MIGRATE.md | 28 ++++++++++++++++++++++++++++ rs/moq-quic/MIGRATE.md | 12 ++++++++++++ rs/moq-tokio/MIGRATE.md | 26 ++++++++++++++++++++++++++ swift/MIGRATE.md | 11 +++++++++++ 13 files changed, 184 insertions(+) diff --git a/cpp/moq/MIGRATE.md b/cpp/moq/MIGRATE.md index cf768d84ff..0d84663b75 100644 --- a/cpp/moq/MIGRATE.md +++ b/cpp/moq/MIGRATE.md @@ -4,6 +4,29 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Bandwidth reserves a track's demand (#5845) + +`Bandwidth::reserve` takes the track's `demand()` instead of the producer, and no longer fails. + +```cpp +// Before +auto reservation = ok(bandwidth->reserve(track, max_bps)); +// After +auto reservation = bandwidth->reserve(ok(track->demand()), max_bps); +``` + +### Generated types drop the `Moq` prefix (#5727) + +`moq::MoqFoo` is `moq::Foo`, the name the aliases already gave it. +`moq::expected` is `tl::expected` at every C++ standard instead of `std::expected` under C++23, and `MOQ_ABI`, `MOQ_ABI_NAME`, and `MOQ_IMPLEMENTATION` are gone. + +```cpp +// Before +moq::MoqClientConfig config; +// After +moq::ClientConfig config; +``` + ### Media subscribe and fetch take a catalog rendition (#5834) `moq::MediaTrackConfig` and `moq::MediaGroupConfig` replace `name` and `container` with a `moq::Rendition` and follow its broadcast and track themselves. diff --git a/cpp/obs/MIGRATE.md b/cpp/obs/MIGRATE.md index cdc00b98c6..8407b8b1a1 100644 --- a/cpp/obs/MIGRATE.md +++ b/cpp/obs/MIGRATE.md @@ -3,3 +3,8 @@ How to update your OBS setup for each breaking change, newest first. ## Unreleased + +### The video bitrate follows the bandwidth estimate (#5845) + +The configured video bitrate is now a ceiling: the encoder drops below it when the uplink narrows, with no opt-out. +An encoder shared with a recording follows too, so give the recording its own encoder to keep it at the full rate. diff --git a/dart/moq/MIGRATE.md b/dart/moq/MIGRATE.md index 0d56df315b..70df30637c 100644 --- a/dart/moq/MIGRATE.md +++ b/dart/moq/MIGRATE.md @@ -4,6 +4,17 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Bandwidth reserves a track's demand (#5845) + +`Bandwidth.reserve` takes the track's `demand()` instead of the producer, and no longer throws. + +```dart +// Before +final reservation = bandwidth.reserve(track: track, maxBps: maxBps); +// After +final reservation = bandwidth.reserve(demand: track.demand(), maxBps: maxBps); +``` + ### Media subscribe and fetch take a catalog rendition (#5834) `media.TrackConfig` and `media.GroupConfig` replace `name` and `container` with a `rendition` (`media.VideoRendition` or `media.AudioRendition`) and follow its broadcast and track themselves. diff --git a/go/wrapper/MIGRATE.md b/go/wrapper/MIGRATE.md index 6a243a895a..10e38e45d2 100644 --- a/go/wrapper/MIGRATE.md +++ b/go/wrapper/MIGRATE.md @@ -4,6 +4,18 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Bandwidth reserves a track's demand (#5845) + +`Bandwidth.Reserve` takes the track's `Demand()` instead of the producer, and no longer returns an error. + +```go +// Before +reservation, err := bandwidth.Reserve(track, maxBps) +// After +demand, err := track.Demand() +reservation := bandwidth.Reserve(demand, maxBps) +``` + ### Media subscribe and fetch take a catalog rendition (#5834) `media.TrackConfig` and `media.GroupConfig` replace `Name` and `Container` with a `Rendition`, built by `media.VideoRendition` or `media.AudioRendition`, and follow its broadcast and track themselves. diff --git a/js/hang/MIGRATE.md b/js/hang/MIGRATE.md index 30a1e7e396..27281f9d1f 100644 --- a/js/hang/MIGRATE.md +++ b/js/hang/MIGRATE.md @@ -4,6 +4,18 @@ How to update your code for each breaking change, newest first. ## Unreleased +### The catalog root reserves `version` (#5806) + +`version` is the oldest moq-hang draft that reads the catalog, and parsing throws `UnsupportedVersion` for one newer than this reader. +Rename an extension field that was called `version`. + +```ts +// Before +const Schema = z.extend(RootSchema, { version: z.string() }); +// After +const Schema = z.extend(RootSchema, { appVersion: z.string() }); +``` + ### Archive timelines declare a per-track duration (#5715) Each `archive.timelines` value is `{ track, duration }` instead of the timeline track's name, and `archive.durationMax` is removed. diff --git a/js/net/MIGRATE.md b/js/net/MIGRATE.md index 640797d85b..4d45caf6eb 100644 --- a/js/net/MIGRATE.md +++ b/js/net/MIGRATE.md @@ -4,6 +4,17 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `Auth.Auth` is `Auth.Handle`, and `Auth.Token` is `Auth.Auth` (#5695) + +```ts +// Before +const handle: Auth.Auth = connection.auth; +const token: Auth.Token = await handle.add(bytes); +// After +const handle: Auth.Handle = connection.auth; +const auth: Auth.Auth = await handle.add(bytes); +``` + ### Superseded groups age by wall clock too (#5548) A superseded group is stale once its successor arrived a full `maxDelay` ago, as well as by media time. diff --git a/kt/moq/MIGRATE.md b/kt/moq/MIGRATE.md index 49bd74b345..00057c3127 100644 --- a/kt/moq/MIGRATE.md +++ b/kt/moq/MIGRATE.md @@ -4,6 +4,17 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Bandwidth reserves a track's demand (#5845) + +`Bandwidth.reserve` takes the track's `demand()` instead of the producer, and no longer throws. + +```kotlin +// Before +val reservation = bandwidth.reserve(track, maxBps) +// After +val reservation = bandwidth.reserve(track.demand(), maxBps) +``` + ### Media subscribe and fetch take a catalog rendition (#5834) `TrackConfig` and `GroupConfig` in `dev.moq.media` replace `name` and `container` with a `rendition` (`VideoRendition` or `AudioRendition`) and follow its broadcast and track themselves. diff --git a/py/moq-rs/MIGRATE.md b/py/moq-rs/MIGRATE.md index 0b49fa2ba7..84e23667ce 100644 --- a/py/moq-rs/MIGRATE.md +++ b/py/moq-rs/MIGRATE.md @@ -4,6 +4,17 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Bandwidth reserves a track's demand (#5845) + +`Bandwidth.reserve` takes the track's `demand()` instead of the producer. + +```python +# Before +reservation = bandwidth.reserve(track, max_bps) +# After +reservation = bandwidth.reserve(track.demand(), max_bps) +``` + ### Media subscribe and fetch take a catalog rendition (#5834) `media.TrackConsumer.subscribe` and `media.GroupConsumer.fetch` take the catalog ID and its `Video` or `Audio` config instead of a track name and container, and follow the rendition's broadcast and track; `fetch` takes them before `sequence`. diff --git a/rs/moq-ffi/MIGRATE.md b/rs/moq-ffi/MIGRATE.md index 8fe22f1f2d..706de7828b 100644 --- a/rs/moq-ffi/MIGRATE.md +++ b/rs/moq-ffi/MIGRATE.md @@ -5,6 +5,17 @@ These are the UniFFI names; each language wrapper has its own `MIGRATE.md` in it ## Unreleased +### Bandwidth reserves a track's demand (#5845) + +`MoqBandwidth::reserve` takes a `MoqTrackDemand` instead of a `MoqTrackProducer`, so raw, media, and JSON tracks reserve the same way, and it no longer fails. + +```rust +// Before +let reservation = bandwidth.reserve(&track, max_bps)?; +// After +let reservation = bandwidth.reserve(&track.demand()?, max_bps); +``` + ### Media subscribe and fetch take a catalog rendition (#5834) `MoqMediaTrackConfig` and `MoqMediaGroupConfig` replace `name` and `container` with a `rendition` (`MoqRendition::Video` or `Audio`: the catalog `id` and its config) and follow its `broadcast` and `track` themselves, so drop the `resolve` call and the `track`-or-ID fallback. diff --git a/rs/moq-net/MIGRATE.md b/rs/moq-net/MIGRATE.md index 6c42609467..9365cc040c 100644 --- a/rs/moq-net/MIGRATE.md +++ b/rs/moq-net/MIGRATE.md @@ -4,6 +4,34 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `auth::Token` is `auth::Auth` (#5695) + +`auth::Handle::add` returns an `auth::Auth`, which is `Clone` and compares by identity, so a route can name it with `origin::Route::with_auth`. + +```rust +// Before +let token: auth::Token = session.auth().add(bytes).await?; +// After +let auth: auth::Auth = session.auth().add(bytes).await?; +``` + +### Custom transports send datagrams without polling (#5770) + +`transport::poll::Session::send_datagram` replaces `poll_send_datagram`. +It takes the payload as `Bytes`, the send group to schedule it in (`None` for ungrouped), and an order, and never blocks. + +```rust +// Before +fn poll_send_datagram(&mut self, cx: &mut Context<'_>, payload: &[u8]) -> Poll> { ... } +// After +fn send_datagram( + &mut self, + payload: Bytes, + group: Option<&::Group>, + order: i64, +) -> Result<(), Self::Error> { ... } +``` + ### A bare track handle is not demand (#5820) Holding a `track::Consumer` sends nothing and no longer keeps the producer used. diff --git a/rs/moq-quic/MIGRATE.md b/rs/moq-quic/MIGRATE.md index 87ebaf0444..1eb0821bf1 100644 --- a/rs/moq-quic/MIGRATE.md +++ b/rs/moq-quic/MIGRATE.md @@ -4,6 +4,18 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `Datagrams::send` takes a send group and order (#5770) + +`Datagrams::send(data, group, order)` replaces `send(data, drop)`. +A full buffer drops the oldest datagram of the lowest-priority group, so `SendDatagramError::Blocked` and `Event::DatagramsUnblocked` are gone. + +```rust +// Before +conn.datagrams().send(data, true)?; +// After +conn.datagrams().send(data, None, 0)?; +``` + ### `congestion::NewReno` removed `congestion::NewReno` and `congestion::NewRenoConfig` are gone. diff --git a/rs/moq-tokio/MIGRATE.md b/rs/moq-tokio/MIGRATE.md index 7045b187c3..a9e75d2b74 100644 --- a/rs/moq-tokio/MIGRATE.md +++ b/rs/moq-tokio/MIGRATE.md @@ -4,6 +4,32 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Auth handles live in `connection::auth` (#5695) + +The root `Auth` re-export is gone. +`Connection::auth` returns a `connection::auth::Handle`, and its `add` returns a `connection::auth::Auth` instead of a `Token`. + +```rust +// Before +let handle: moq_tokio::Auth = connection.auth(); +let token: moq_tokio::connection::auth::Token = handle.add(bytes).await?; +// After +let handle: moq_tokio::connection::auth::Handle = connection.auth(); +let auth: moq_tokio::connection::auth::Auth = handle.add(bytes).await?; +``` + +### Datagrams take a send group and order (#5770) + +`quic::driver::Connection::send_datagram` and `quic::web::Session::send_datagram` take the send group (`None` for ungrouped) and an order, and never block. +`send_datagram_wait` and `SendDatagram` are gone. + +```rust +// Before +session.send_datagram_wait(data).await?; +// After +session.send_datagram(data, None, 0)?; +``` + ### The `noq` feature is `quic` (#5664) The QUIC stack is MoQ's own `moq-quic` instead of the `moq-noq` crates. diff --git a/swift/MIGRATE.md b/swift/MIGRATE.md index 6d8149f527..ffed0b455e 100644 --- a/swift/MIGRATE.md +++ b/swift/MIGRATE.md @@ -4,6 +4,17 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Bandwidth reserves a track's demand (#5845) + +`Bandwidth.reserve(demand:maxBps:)` takes the track's `demand()` instead of the producer, and no longer throws. + +```swift +// Before +let reservation = try bandwidth.reserve(track: track, maxBps: maxBps) +// After +let reservation = bandwidth.reserve(demand: try track.demand(), maxBps: maxBps) +``` + ### Media subscribe and fetch take a catalog rendition (#5834) `Media.TrackConfig` and `Media.GroupConfig` replace `name` and `container` with a `Media.Rendition` and follow its broadcast and track themselves. From a7506f60c0362f3189e1984c48874cbd7473727f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 12:32:08 -0700 Subject: [PATCH 24/40] docs(swift): use the id label in the native codec snippet Co-Authored-By: Claude Opus 5.5 --- swift/MIGRATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/swift/MIGRATE.md b/swift/MIGRATE.md index ffed0b455e..7787281398 100644 --- a/swift/MIGRATE.md +++ b/swift/MIGRATE.md @@ -43,7 +43,7 @@ let video = try broadcast.encodeVideo(input: input, output: output) let frames = try await consumer.decodeVideo(name: name, catalogVideo: rendition, output: decoder) // After let video = try Video.Producer(catalog: Media.CatalogProducer(broadcast: broadcast), input: input, output: output) -let frames = try await Video.Consumer.subscribe(broadcast: consumer, name: name, rendition: rendition, output: decoder) +let frames = try await Video.Consumer.subscribe(broadcast: consumer, id: name, rendition: rendition, output: decoder) ``` ### Background panics are reported (#5562) From 515b4f60426719d2bcf088cb5ae2049d39ad9243 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 12:40:44 -0700 Subject: [PATCH 25/40] docs(upgrade): read released sections from main's MIGRATE.md The release branch predates the guides until the next cut, and main keeps the released sections, so point published-version readers there. Co-Authored-By: Claude Opus 5.5 --- doc/setup/upgrade.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index 4b61931571..d7df81ccc1 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -6,13 +6,13 @@ description: The order to roll out a release across relays, auth servers, and cl # Upgrade Every hand-written published package keeps a `MIGRATE.md` with a before/after -snippet for each breaking change, newest first. The links below track `main`; -for a published version, read the file on the `release` branch and apply the -sections from the version you run up to the one you install. `Unreleased` -applies only when you build from `main`. Most binding users install a wrapper, -such as `moq-rs` on PyPI or `dev.moq:moq` on Maven, whose guide is in the index -below. The raw UniFFI packages published as `moq-ffi` have no guide of their -own: [moq-ffi's](https://github.com/moq-dev/moq/blob/main/rs/moq-ffi/MIGRATE.md) +snippet for each breaking change, newest first. The links below track `main`, +which also holds the released sections: apply those from the version you run up +to the one you install. `Unreleased` applies only when you build from `main`. +Most binding users install a wrapper, such as `moq-rs` on PyPI or +`dev.moq:moq` on Maven, whose guide is in the index below. The raw UniFFI +packages published as `moq-ffi` have no guide of their own: +[moq-ffi's](https://github.com/moq-dev/moq/blob/main/rs/moq-ffi/MIGRATE.md) covers that layer, in UniFFI names. This page covers what spans packages: the wire changes that decide the order to upgrade a deployment. From 6cbec52c54b9e41c70772153abc77a7cbecf5f44 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 12:48:58 -0700 Subject: [PATCH 26/40] docs: add moq-ice's guide and moq-net's lingering finish Co-Authored-By: Claude Opus 5.5 --- doc/setup/upgrade.md | 2 +- rs/moq-ice/MIGRATE.md | 5 +++++ rs/moq-net/MIGRATE.md | 12 ++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 rs/moq-ice/MIGRATE.md diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index d7df81ccc1..b2f2734571 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -25,7 +25,7 @@ ignored; its guide says what replaces it. - **Binaries:** [moq-relay](https://github.com/moq-dev/moq/blob/main/rs/moq-relay/MIGRATE.md), [moq-cli (`moq`)](https://github.com/moq-dev/moq/blob/main/rs/moq-cli/MIGRATE.md), [moq-gst](https://github.com/moq-dev/moq/blob/main/rs/moq-gst/MIGRATE.md), [obs-moq](https://github.com/moq-dev/moq/blob/main/cpp/obs/MIGRATE.md) - **Bindings:** [moq-ffi (UniFFI)](https://github.com/moq-dev/moq/blob/main/rs/moq-ffi/MIGRATE.md), [moq-c (C)](https://github.com/moq-dev/moq/blob/main/rs/moq-c/MIGRATE.md), [Python](https://github.com/moq-dev/moq/blob/main/py/moq-rs/MIGRATE.md), [Go](https://github.com/moq-dev/moq/blob/main/go/wrapper/MIGRATE.md), [Swift](https://github.com/moq-dev/moq/blob/main/swift/MIGRATE.md), [Kotlin](https://github.com/moq-dev/moq/blob/main/kt/moq/MIGRATE.md), [Dart](https://github.com/moq-dev/moq/blob/main/dart/moq/MIGRATE.md), [C++](https://github.com/moq-dev/moq/blob/main/cpp/moq/MIGRATE.md) -- **Rust:** [hang](https://github.com/moq-dev/moq/blob/main/rs/hang/MIGRATE.md), [kio](https://github.com/moq-dev/moq/blob/main/rs/kio/MIGRATE.md), [moq-archive](https://github.com/moq-dev/moq/blob/main/rs/moq-archive/MIGRATE.md), [moq-audio](https://github.com/moq-dev/moq/blob/main/rs/moq-audio/MIGRATE.md), [moq-auth](https://github.com/moq-dev/moq/blob/main/rs/moq-auth/MIGRATE.md), [moq-boy](https://github.com/moq-dev/moq/blob/main/rs/moq-boy/MIGRATE.md), [moq-clock](https://github.com/moq-dev/moq/blob/main/rs/moq-clock/MIGRATE.md), [moq-clock-macros](https://github.com/moq-dev/moq/blob/main/rs/moq-clock/macros/MIGRATE.md), [moq-e2ee](https://github.com/moq-dev/moq/blob/main/rs/moq-e2ee/MIGRATE.md), [moq-flate](https://github.com/moq-dev/moq/blob/main/rs/moq-flate/MIGRATE.md), [moq-hls](https://github.com/moq-dev/moq/blob/main/rs/moq-hls/MIGRATE.md), [moq-json](https://github.com/moq-dev/moq/blob/main/rs/moq-json/MIGRATE.md), [moq-loc](https://github.com/moq-dev/moq/blob/main/rs/moq-loc/MIGRATE.md), [moq-msf](https://github.com/moq-dev/moq/blob/main/rs/moq-msf/MIGRATE.md), [moq-mux](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/MIGRATE.md), [moq-net](https://github.com/moq-dev/moq/blob/main/rs/moq-net/MIGRATE.md), [moq-nvenc](https://github.com/moq-dev/moq/blob/main/rs/moq-nvenc/MIGRATE.md), [moq-pattern](https://github.com/moq-dev/moq/blob/main/rs/moq-pattern/MIGRATE.md), [moq-quic](https://github.com/moq-dev/moq/blob/main/rs/moq-quic/MIGRATE.md), [moq-room](https://github.com/moq-dev/moq/blob/main/rs/moq-room/MIGRATE.md), [moq-rtc](https://github.com/moq-dev/moq/blob/main/rs/moq-rtc/MIGRATE.md), [moq-rtmp](https://github.com/moq-dev/moq/blob/main/rs/moq-rtmp/MIGRATE.md), [moq-sock](https://github.com/moq-dev/moq/blob/main/rs/moq-sock/MIGRATE.md), [moq-srt](https://github.com/moq-dev/moq/blob/main/rs/moq-srt/MIGRATE.md), [moq-stats](https://github.com/moq-dev/moq/blob/main/rs/moq-stats/MIGRATE.md), [moq-tokio](https://github.com/moq-dev/moq/blob/main/rs/moq-tokio/MIGRATE.md), [moq-transcode](https://github.com/moq-dev/moq/blob/main/rs/moq-transcode/MIGRATE.md), [moq-uring](https://github.com/moq-dev/moq/blob/main/rs/moq-uring/MIGRATE.md), [moq-v4l](https://github.com/moq-dev/moq/blob/main/rs/moq-v4l/MIGRATE.md), [moq-video](https://github.com/moq-dev/moq/blob/main/rs/moq-video/MIGRATE.md) +- **Rust:** [hang](https://github.com/moq-dev/moq/blob/main/rs/hang/MIGRATE.md), [kio](https://github.com/moq-dev/moq/blob/main/rs/kio/MIGRATE.md), [moq-archive](https://github.com/moq-dev/moq/blob/main/rs/moq-archive/MIGRATE.md), [moq-audio](https://github.com/moq-dev/moq/blob/main/rs/moq-audio/MIGRATE.md), [moq-auth](https://github.com/moq-dev/moq/blob/main/rs/moq-auth/MIGRATE.md), [moq-boy](https://github.com/moq-dev/moq/blob/main/rs/moq-boy/MIGRATE.md), [moq-clock](https://github.com/moq-dev/moq/blob/main/rs/moq-clock/MIGRATE.md), [moq-clock-macros](https://github.com/moq-dev/moq/blob/main/rs/moq-clock/macros/MIGRATE.md), [moq-e2ee](https://github.com/moq-dev/moq/blob/main/rs/moq-e2ee/MIGRATE.md), [moq-flate](https://github.com/moq-dev/moq/blob/main/rs/moq-flate/MIGRATE.md), [moq-hls](https://github.com/moq-dev/moq/blob/main/rs/moq-hls/MIGRATE.md), [moq-ice](https://github.com/moq-dev/moq/blob/main/rs/moq-ice/MIGRATE.md), [moq-json](https://github.com/moq-dev/moq/blob/main/rs/moq-json/MIGRATE.md), [moq-loc](https://github.com/moq-dev/moq/blob/main/rs/moq-loc/MIGRATE.md), [moq-msf](https://github.com/moq-dev/moq/blob/main/rs/moq-msf/MIGRATE.md), [moq-mux](https://github.com/moq-dev/moq/blob/main/rs/moq-mux/MIGRATE.md), [moq-net](https://github.com/moq-dev/moq/blob/main/rs/moq-net/MIGRATE.md), [moq-nvenc](https://github.com/moq-dev/moq/blob/main/rs/moq-nvenc/MIGRATE.md), [moq-pattern](https://github.com/moq-dev/moq/blob/main/rs/moq-pattern/MIGRATE.md), [moq-quic](https://github.com/moq-dev/moq/blob/main/rs/moq-quic/MIGRATE.md), [moq-room](https://github.com/moq-dev/moq/blob/main/rs/moq-room/MIGRATE.md), [moq-rtc](https://github.com/moq-dev/moq/blob/main/rs/moq-rtc/MIGRATE.md), [moq-rtmp](https://github.com/moq-dev/moq/blob/main/rs/moq-rtmp/MIGRATE.md), [moq-sock](https://github.com/moq-dev/moq/blob/main/rs/moq-sock/MIGRATE.md), [moq-srt](https://github.com/moq-dev/moq/blob/main/rs/moq-srt/MIGRATE.md), [moq-stats](https://github.com/moq-dev/moq/blob/main/rs/moq-stats/MIGRATE.md), [moq-tokio](https://github.com/moq-dev/moq/blob/main/rs/moq-tokio/MIGRATE.md), [moq-transcode](https://github.com/moq-dev/moq/blob/main/rs/moq-transcode/MIGRATE.md), [moq-uring](https://github.com/moq-dev/moq/blob/main/rs/moq-uring/MIGRATE.md), [moq-v4l](https://github.com/moq-dev/moq/blob/main/rs/moq-v4l/MIGRATE.md), [moq-video](https://github.com/moq-dev/moq/blob/main/rs/moq-video/MIGRATE.md) - **JavaScript:** [`@moq/auth`](https://github.com/moq-dev/moq/blob/main/js/auth/MIGRATE.md), [`@moq/boy`](https://github.com/moq-dev/moq/blob/main/js/moq-boy/MIGRATE.md), [`@moq/clock`](https://github.com/moq-dev/moq/blob/main/js/clock/MIGRATE.md), [`@moq/flate`](https://github.com/moq-dev/moq/blob/main/js/flate/MIGRATE.md), [`@moq/hang`](https://github.com/moq-dev/moq/blob/main/js/hang/MIGRATE.md), [`@moq/json`](https://github.com/moq-dev/moq/blob/main/js/json/MIGRATE.md), [`@moq/loc`](https://github.com/moq-dev/moq/blob/main/js/loc/MIGRATE.md), [`@moq/msf`](https://github.com/moq-dev/moq/blob/main/js/msf/MIGRATE.md), [`@moq/net`](https://github.com/moq-dev/moq/blob/main/js/net/MIGRATE.md), [`@moq/p2p`](https://github.com/moq-dev/moq/blob/main/js/p2p/MIGRATE.md), [`@moq/pattern`](https://github.com/moq-dev/moq/blob/main/js/pattern/MIGRATE.md), [`@moq/publish`](https://github.com/moq-dev/moq/blob/main/js/publish/MIGRATE.md), [`@moq/room`](https://github.com/moq-dev/moq/blob/main/js/room/MIGRATE.md), [`@moq/signals`](https://github.com/moq-dev/moq/blob/main/js/signals/MIGRATE.md), [`@moq/video`](https://github.com/moq-dev/moq/blob/main/js/video/MIGRATE.md), [`@moq/watch`](https://github.com/moq-dev/moq/blob/main/js/watch/MIGRATE.md) ## Unreleased diff --git a/rs/moq-ice/MIGRATE.md b/rs/moq-ice/MIGRATE.md new file mode 100644 index 0000000000..4be91a0b38 --- /dev/null +++ b/rs/moq-ice/MIGRATE.md @@ -0,0 +1,5 @@ +# Migrating moq-ice + +How to update your code for each breaking change, newest first. + +## Unreleased diff --git a/rs/moq-net/MIGRATE.md b/rs/moq-net/MIGRATE.md index 9365cc040c..87ae6a2067 100644 --- a/rs/moq-net/MIGRATE.md +++ b/rs/moq-net/MIGRATE.md @@ -4,6 +4,18 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `broadcast::Producer::finish` is back, and lingers (#5795) + +`finish()` ends the broadcast cleanly and returns a `Finish` that keeps its finished tracks servable until their media is stale. +Dropping the `Finish` closes the broadcast, so `close()` still ends it now. + +```rust +// Close now +producer.close(); +// Serve late subscribers until the media is stale +producer.finish().wait().await; +``` + ### `auth::Token` is `auth::Auth` (#5695) `auth::Handle::add` returns an `auth::Auth`, which is `Clone` and compares by identity, so a route can name it with `origin::Route::with_auth`. From ab9adf5ff4b4a40e922750c6e9e4ea37d1f4bd03 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 12:57:56 -0700 Subject: [PATCH 27/40] docs(auth): name the webtransport transport in @moq/auth's guide Co-Authored-By: Claude Opus 5.5 --- js/auth/MIGRATE.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/js/auth/MIGRATE.md b/js/auth/MIGRATE.md index a8393bf456..61ba519025 100644 --- a/js/auth/MIGRATE.md +++ b/js/auth/MIGRATE.md @@ -21,11 +21,17 @@ if (permissions.publish.rate !== undefined) throw new Error("bitrate caps are no ### A token must carry its key's `kid` (#3943) `Key.verify` refuses a token whose `kid` header does not match the key's, and a key set accepts a token without a `kid` only when it holds one key that has none. -The strict transport schema also accepts `rtmp`, `srt`, and `webrtc`, so an exhaustive `switch` over it needs those arms. +The strict transport schema also accepts `rtmp`, `srt`, `webrtc`, and `webtransport`, so an exhaustive `switch` over it needs those arms. +A WebTransport session reports `webtransport` instead of `quic` (#5293), so a policy keyed on `quic` must accept both. ```ts // Sign with the key that will verify it, so the kid matches. const token = await Key.sign(key, claims); + +// Before +const overQuic = transport === "quic"; +// After +const overQuic = transport === "quic" || transport === "webtransport"; ``` ## @moq/token 0.3 -> @moq/auth 0.2 From 7ad3bbe430bd8c78f29032a12ee733fe94069526 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 13:26:08 -0700 Subject: [PATCH 28/40] docs: migrate #5700 announced instances, #5777 recv bandwidth, #5803, #5871 Co-Authored-By: Claude Opus 5.5 --- cpp/moq/MIGRATE.md | 13 +++++++++++++ dart/moq/MIGRATE.md | 13 +++++++++++++ go/wrapper/MIGRATE.md | 14 ++++++++++++++ js/net/MIGRATE.md | 5 +++++ kt/moq/MIGRATE.md | 13 +++++++++++++ py/moq-rs/MIGRATE.md | 13 +++++++++++++ rs/moq-c/MIGRATE.md | 25 +++++++++++++++++++++++++ rs/moq-ffi/MIGRATE.md | 18 ++++++++++++++++++ rs/moq-net/MIGRATE.md | 13 +++++++++++++ rs/moq-relay/MIGRATE.md | 5 +++++ rs/moq-tokio/MIGRATE.md | 19 +++++++++++++++++++ rs/moq-uring/MIGRATE.md | 15 +++++++++++++++ swift/MIGRATE.md | 13 +++++++++++++ 13 files changed, 179 insertions(+) diff --git a/cpp/moq/MIGRATE.md b/cpp/moq/MIGRATE.md index 0d84663b75..68b4831fae 100644 --- a/cpp/moq/MIGRATE.md +++ b/cpp/moq/MIGRATE.md @@ -4,6 +4,19 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Receive estimates come from `Session::recv_bandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session::recv_bandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and `cancel()` it to stop. + +```cpp +// Before +auto rate = session->stats().estimated_recv_rate_bps; +// After +auto bandwidth = session->recv_bandwidth(); +auto rate = bandwidth ? bandwidth->peek() : std::nullopt; +``` + ### Bandwidth reserves a track's demand (#5845) `Bandwidth::reserve` takes the track's `demand()` instead of the producer, and no longer fails. diff --git a/dart/moq/MIGRATE.md b/dart/moq/MIGRATE.md index 70df30637c..bd8fd8182b 100644 --- a/dart/moq/MIGRATE.md +++ b/dart/moq/MIGRATE.md @@ -4,6 +4,19 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Receive estimates come from `Session.recvBandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session.recvBandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and `cancel()` it to stop. + +```dart +// Before +final rate = session.stats().estimatedRecvRateBps; +// After +final bandwidth = session.recvBandwidth(); +final rate = bandwidth?.peek(); +``` + ### Bandwidth reserves a track's demand (#5845) `Bandwidth.reserve` takes the track's `demand()` instead of the producer, and no longer throws. diff --git a/go/wrapper/MIGRATE.md b/go/wrapper/MIGRATE.md index 10e38e45d2..b265cf3893 100644 --- a/go/wrapper/MIGRATE.md +++ b/go/wrapper/MIGRATE.md @@ -4,6 +4,20 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Receive estimates come from `Session.RecvBandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session.RecvBandwidth` returns to request PROBE, read it with `Peek()` or wait with `Changed(ctx)`, and `Close()` it to stop. + +```go +// Before +rate := session.Stats().EstimatedRecvRateBps +// After +bandwidth := session.RecvBandwidth() +defer bandwidth.Close() +rate := bandwidth.Peek() +``` + ### Bandwidth reserves a track's demand (#5845) `Bandwidth.Reserve` takes the track's `Demand()` instead of the producer, and no longer returns an error. diff --git a/js/net/MIGRATE.md b/js/net/MIGRATE.md index 4d45caf6eb..74ff734753 100644 --- a/js/net/MIGRATE.md +++ b/js/net/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### A timed track refuses a group that rewinds (#5803) + +A local write whose first frame starts before a lower-sequence group's, or after a higher one's, throws `TimestampMismatch`. +A publisher whose timeline restarts publishes a new broadcast instead. + ### `Auth.Auth` is `Auth.Handle`, and `Auth.Token` is `Auth.Auth` (#5695) ```ts diff --git a/kt/moq/MIGRATE.md b/kt/moq/MIGRATE.md index 00057c3127..3083f0c7d8 100644 --- a/kt/moq/MIGRATE.md +++ b/kt/moq/MIGRATE.md @@ -4,6 +4,19 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Receive estimates come from `Session.recvBandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session.recvBandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and `cancel()` it to stop. + +```kotlin +// Before +val rate = session.stats().estimatedRecvRateBps +// After +val bandwidth = session.recvBandwidth() +val rate = bandwidth?.peek() +``` + ### Bandwidth reserves a track's demand (#5845) `Bandwidth.reserve` takes the track's `demand()` instead of the producer, and no longer throws. diff --git a/py/moq-rs/MIGRATE.md b/py/moq-rs/MIGRATE.md index 84e23667ce..4b56b5dd88 100644 --- a/py/moq-rs/MIGRATE.md +++ b/py/moq-rs/MIGRATE.md @@ -4,6 +4,19 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Receive estimates come from `Session.recv_bandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session.recv_bandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and drop it to stop. + +```python +# Before +rate = session.stats().estimated_recv_rate_bps +# After +bandwidth = session.recv_bandwidth() +rate = bandwidth.peek() if bandwidth else None +``` + ### Bandwidth reserves a track's demand (#5845) `Bandwidth.reserve` takes the track's `demand()` instead of the producer. diff --git a/rs/moq-c/MIGRATE.md b/rs/moq-c/MIGRATE.md index 3023b3e7ca..deacc37446 100644 --- a/rs/moq-c/MIGRATE.md +++ b/rs/moq-c/MIGRATE.md @@ -4,6 +4,31 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Receive estimates come from `moq_session_recv_bandwidth` (#5777) + +`moq_connection_stats` drops `estimated_recv_rate_bps` and `estimated_recv_rate_valid`, which changes its ABI. +Hold the consumer `moq_session_recv_bandwidth` returns to request PROBE, read it with `moq_bandwidth_consumer_peek` or wait with `moq_bandwidth_consumer_changed`, and release it with `moq_bandwidth_consumer_close`. + +```c +// Before +moq_connection_stats stats; +moq_session_stats(session, &stats); +if (stats.estimated_recv_rate_valid) use(stats.estimated_recv_rate_bps); +// After +int32_t consumer = moq_session_recv_bandwidth(session); // 0 when the version has no PROBE +if (consumer > 0) { + uint64_t bps; + bool present; + if (moq_bandwidth_consumer_peek(consumer, &bps, &present) == 0 && present) use(bps); + moq_bandwidth_consumer_close(consumer); +} +``` + +### `moq_origin_announced_broadcast` delivers every publisher instance (#5700) + +Its callback gets a new broadcast handle each time another instance takes over the path or the broadcast returns, and its terminal `0` only on cancel. +Free each handle with `moq_consume_close` once its successor arrives. + ### libmoq is moq-c (#4288) The C bindings ship as the `moq-c` crate, starting at 0.7.0. diff --git a/rs/moq-ffi/MIGRATE.md b/rs/moq-ffi/MIGRATE.md index 706de7828b..5c1e1bedc3 100644 --- a/rs/moq-ffi/MIGRATE.md +++ b/rs/moq-ffi/MIGRATE.md @@ -5,6 +5,24 @@ These are the UniFFI names; each language wrapper has its own `MIGRATE.md` in it ## Unreleased +### Receive estimates come from `MoqSession::recv_bandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `MoqSession::recv_bandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and drop it to stop. `MoqConnectionStats::estimated_recv_rate_bps` is gone. + +```rust +// Before +let rate = session.stats().estimated_recv_rate_bps; +// After +let bandwidth = session.recv_bandwidth(); +let rate = bandwidth.as_ref().and_then(|b| b.peek()); +``` + +### `available()` waits for the next publisher instance (#5700) + +`MoqAnnouncedBroadcast::available` returns the broadcast serving the path on its first call, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + ### Bandwidth reserves a track's demand (#5845) `MoqBandwidth::reserve` takes a `MoqTrackDemand` instead of a `MoqTrackProducer`, so raw, media, and JSON tracks reserve the same way, and it no longer fails. diff --git a/rs/moq-net/MIGRATE.md b/rs/moq-net/MIGRATE.md index 97868e025b..52d50adee4 100644 --- a/rs/moq-net/MIGRATE.md +++ b/rs/moq-net/MIGRATE.md @@ -4,6 +4,19 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Receive estimates come from `Session::recv_bandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session::recv_bandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and drop it to stop. + +```rust +// Before +let rate = session.stats().estimated_recv_rate; +// After +let bandwidth = session.recv_bandwidth(); +let rate = bandwidth.as_ref().and_then(|b| b.peek()); +``` + ### A timed track refuses a group that rewinds (#5803) A local write whose first frame starts before a lower-sequence group's, or after a higher one's, fails with `TimestampMismatch`; `@moq/net` throws it the same way. diff --git a/rs/moq-relay/MIGRATE.md b/rs/moq-relay/MIGRATE.md index 1fff9f9e3a..f4c09a54dc 100644 --- a/rs/moq-relay/MIGRATE.md +++ b/rs/moq-relay/MIGRATE.md @@ -5,6 +5,11 @@ Rolling a cluster across a release also follows the [upgrade guide](https://moq. ## Unreleased +### `max_streams = 0` is refused (#5871) + +A `[quic]` `max_streams` of 0 stops startup, as `[peer.quic]` already did, since zero streams wedge the connection. +Remove it to keep the default. + ### Auth roles carry `moq_auth::Access` (#5697) `auth::Token::publish` and `subscribe` are `moq_auth::Access`, so an embedder reads the patterns from `.path`. diff --git a/rs/moq-tokio/MIGRATE.md b/rs/moq-tokio/MIGRATE.md index a9e75d2b74..347cead6ff 100644 --- a/rs/moq-tokio/MIGRATE.md +++ b/rs/moq-tokio/MIGRATE.md @@ -4,6 +4,25 @@ How to update your code for each breaking change, newest first. ## Unreleased +### QUIC limits are `moq_quic::Limits`, set through `Request::quic` (#5871) + +`server::Request::set_quic_limits` and `Error::QuicLimits` are gone. +`Request::quic()` returns the `quic::Connection` under a QUIC or WebTransport request, or `None` on other transports, and its `set_limits` takes `quic::Limits`, now `moq_quic::Limits`, where each `None` field keeps that limit. +`quic::Resolved`'s `receive_window`, `stream_receive_window`, and `send_window` are plain `u64`s with the defaults filled in, and `quic.max_streams = 0` is refused. + +```rust +// Before +request.set_quic_limits(quic::Limits { max_streams, receive_window, send_window })?; +// After +if let Some(quic) = request.quic() { + let mut limits = quic::Limits::default(); + limits.max_streams = Some(max_streams); + limits.receive_window = Some(receive_window); + limits.send_window = Some(send_window); + quic.set_limits(limits)?; +} +``` + ### Auth handles live in `connection::auth` (#5695) The root `Auth` re-export is gone. diff --git a/rs/moq-uring/MIGRATE.md b/rs/moq-uring/MIGRATE.md index f86b545a65..7b80529b38 100644 --- a/rs/moq-uring/MIGRATE.md +++ b/rs/moq-uring/MIGRATE.md @@ -4,6 +4,21 @@ How to update your code for each breaking change, newest first. ## Unreleased +### QUIC limits are `moq_quic::Limits` (#5871) + +`quic::Limits` is `moq_quic::Limits`, where each `None` field keeps that limit, and an invalid one fails `set_limits` with `Error::Limits` instead of `WindowRange` or `StreamsRange`. + +```rust +// Before +conn.set_limits(quic::Limits { max_streams, receive_window, send_window })?; +// After +let mut limits = quic::Limits::default(); +limits.max_streams = Some(max_streams); +limits.receive_window = Some(receive_window); +limits.send_window = Some(send_window); +conn.set_limits(limits)?; +``` + ### The `noq` feature is `quic` (#5664) The QUIC stack is MoQ's own `moq-quic` instead of `moq-noq-proto`. diff --git a/swift/MIGRATE.md b/swift/MIGRATE.md index 7787281398..ff82325d0c 100644 --- a/swift/MIGRATE.md +++ b/swift/MIGRATE.md @@ -4,6 +4,19 @@ How to update your code for each breaking change, newest first. ## Unreleased +### Receive estimates come from `Session.recvBandwidth` (#5777) + +The connection stats no longer carry a receive estimate. +Hold the consumer `Session.recvBandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and drop it to stop. + +```swift +// Before +let rate = session.stats().estimatedRecvRateBps +// After +let bandwidth = session.recvBandwidth() +let rate = bandwidth?.peek() +``` + ### Bandwidth reserves a track's demand (#5845) `Bandwidth.reserve(demand:maxBps:)` takes the track's `demand()` instead of the producer, and no longer throws. From 3c233fe1612cc0fcd66e0a0845c00a07dc1a68e9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 13:35:15 -0700 Subject: [PATCH 29/40] docs(bindings): cover #5700 and #5803 in each binding guide Also guard Go's nil receive-bandwidth consumer in the #5777 snippet. Co-Authored-By: Claude Opus 5.5 --- cpp/moq/MIGRATE.md | 10 ++++++++++ dart/moq/MIGRATE.md | 10 ++++++++++ go/wrapper/MIGRATE.md | 18 +++++++++++++++--- kt/moq/MIGRATE.md | 10 ++++++++++ py/moq-rs/MIGRATE.md | 10 ++++++++++ rs/moq-c/MIGRATE.md | 5 +++++ rs/moq-ffi/MIGRATE.md | 5 +++++ swift/MIGRATE.md | 10 ++++++++++ 8 files changed, 75 insertions(+), 3 deletions(-) diff --git a/cpp/moq/MIGRATE.md b/cpp/moq/MIGRATE.md index 68b4831fae..bedbafcdbd 100644 --- a/cpp/moq/MIGRATE.md +++ b/cpp/moq/MIGRATE.md @@ -17,6 +17,16 @@ auto bandwidth = session->recv_bandwidth(); auto rate = bandwidth ? bandwidth->peek() : std::nullopt; ``` +### `AnnouncedBroadcast::available()` waits for the next publisher instance (#5700) + +The first call returns the broadcast serving the path, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + +### A timed track refuses a group that rewinds (#5803) + +Writing a frame fails with `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + ### Bandwidth reserves a track's demand (#5845) `Bandwidth::reserve` takes the track's `demand()` instead of the producer, and no longer fails. diff --git a/dart/moq/MIGRATE.md b/dart/moq/MIGRATE.md index bd8fd8182b..1fa860c041 100644 --- a/dart/moq/MIGRATE.md +++ b/dart/moq/MIGRATE.md @@ -17,6 +17,16 @@ final bandwidth = session.recvBandwidth(); final rate = bandwidth?.peek(); ``` +### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) + +The first call returns the broadcast serving the path, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + +### A timed track refuses a group that rewinds (#5803) + +Writing a frame fails with `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + ### Bandwidth reserves a track's demand (#5845) `Bandwidth.reserve` takes the track's `demand()` instead of the producer, and no longer throws. diff --git a/go/wrapper/MIGRATE.md b/go/wrapper/MIGRATE.md index b265cf3893..40c6b97732 100644 --- a/go/wrapper/MIGRATE.md +++ b/go/wrapper/MIGRATE.md @@ -13,11 +13,23 @@ Hold the consumer `Session.RecvBandwidth` returns to request PROBE, read it with // Before rate := session.Stats().EstimatedRecvRateBps // After -bandwidth := session.RecvBandwidth() -defer bandwidth.Close() -rate := bandwidth.Peek() +var rate *uint64 +if bandwidth := session.RecvBandwidth(); bandwidth != nil { // nil when the version has no PROBE + defer bandwidth.Close() + rate = bandwidth.Peek() +} ``` +### `AnnouncedBroadcast.Available` waits for the next publisher instance (#5700) + +The first call returns the broadcast serving the path, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + +### A timed track refuses a group that rewinds (#5803) + +Writing a frame fails with `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + ### Bandwidth reserves a track's demand (#5845) `Bandwidth.Reserve` takes the track's `Demand()` instead of the producer, and no longer returns an error. diff --git a/kt/moq/MIGRATE.md b/kt/moq/MIGRATE.md index 3083f0c7d8..caaba41f9d 100644 --- a/kt/moq/MIGRATE.md +++ b/kt/moq/MIGRATE.md @@ -17,6 +17,16 @@ val bandwidth = session.recvBandwidth() val rate = bandwidth?.peek() ``` +### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) + +The first call returns the broadcast serving the path, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + +### A timed track refuses a group that rewinds (#5803) + +Writing a frame fails with `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + ### Bandwidth reserves a track's demand (#5845) `Bandwidth.reserve` takes the track's `demand()` instead of the producer, and no longer throws. diff --git a/py/moq-rs/MIGRATE.md b/py/moq-rs/MIGRATE.md index 4b56b5dd88..6690cf01ce 100644 --- a/py/moq-rs/MIGRATE.md +++ b/py/moq-rs/MIGRATE.md @@ -17,6 +17,16 @@ bandwidth = session.recv_bandwidth() rate = bandwidth.peek() if bandwidth else None ``` +### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) + +The first call returns the broadcast serving the path, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + +### A timed track refuses a group that rewinds (#5803) + +Writing a frame fails with `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + ### Bandwidth reserves a track's demand (#5845) `Bandwidth.reserve` takes the track's `demand()` instead of the producer. diff --git a/rs/moq-c/MIGRATE.md b/rs/moq-c/MIGRATE.md index deacc37446..7f62f5e7fe 100644 --- a/rs/moq-c/MIGRATE.md +++ b/rs/moq-c/MIGRATE.md @@ -29,6 +29,11 @@ if (consumer > 0) { Its callback gets a new broadcast handle each time another instance takes over the path or the broadcast returns, and its terminal `0` only on cancel. Free each handle with `moq_consume_close` once its successor arrives. +### A timed track refuses a group that rewinds (#5803) + +A frame write fails with a protocol error of kind `MOQ_PROTOCOL_KIND_TIMESTAMP_MISMATCH` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + ### libmoq is moq-c (#4288) The C bindings ship as the `moq-c` crate, starting at 0.7.0. diff --git a/rs/moq-ffi/MIGRATE.md b/rs/moq-ffi/MIGRATE.md index 5c1e1bedc3..c6563a4bb2 100644 --- a/rs/moq-ffi/MIGRATE.md +++ b/rs/moq-ffi/MIGRATE.md @@ -23,6 +23,11 @@ let rate = bandwidth.as_ref().and_then(|b| b.peek()); `MoqAnnouncedBroadcast::available` returns the broadcast serving the path on its first call, as before. Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. +### A timed track refuses a group that rewinds (#5803) + +`write_frame` on a `MoqTrackProducer` or `MoqGroupProducer` fails with a `MoqError::Protocol` of kind `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + ### Bandwidth reserves a track's demand (#5845) `MoqBandwidth::reserve` takes a `MoqTrackDemand` instead of a `MoqTrackProducer`, so raw, media, and JSON tracks reserve the same way, and it no longer fails. diff --git a/swift/MIGRATE.md b/swift/MIGRATE.md index ff82325d0c..07e26471a5 100644 --- a/swift/MIGRATE.md +++ b/swift/MIGRATE.md @@ -17,6 +17,16 @@ let bandwidth = session.recvBandwidth() let rate = bandwidth?.peek() ``` +### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) + +The first call returns the broadcast serving the path, as before. +Each later call waits for the next instance to take over the path, instead of returning whatever serves it now. + +### A timed track refuses a group that rewinds (#5803) + +Writing a frame fails with `TimestampMismatch` when a group's first frame starts before a lower-sequence group's, or after a higher one's. +A publisher whose timeline restarts publishes a new broadcast instead. + ### Bandwidth reserves a track's demand (#5845) `Bandwidth.reserve(demand:maxBps:)` takes the track's `demand()` instead of the producer, and no longer throws. From 0114be2d9689dd35ea4f0194533bc69e6c2894fb Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 13:53:01 -0700 Subject: [PATCH 30/40] docs(upgrade): snippets are for breaks that change code or config Also give moq-relay's max_streams entry its config snippet. Co-Authored-By: Claude Opus 5.5 --- doc/setup/upgrade.md | 13 +++++++------ rs/moq-relay/MIGRATE.md | 8 ++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index b2f2734571..7072aa7203 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -5,12 +5,13 @@ description: The order to roll out a release across relays, auth servers, and cl # Upgrade -Every hand-written published package keeps a `MIGRATE.md` with a before/after -snippet for each breaking change, newest first. The links below track `main`, -which also holds the released sections: apply those from the version you run up -to the one you install. `Unreleased` applies only when you build from `main`. -Most binding users install a wrapper, such as `moq-rs` on PyPI or -`dev.moq:moq` on Maven, whose guide is in the index below. The raw UniFFI +Every hand-written published package keeps a `MIGRATE.md` entry for each +breaking change, newest first, with a before/after snippet wherever code or +config must change. The links below track `main`, which also holds the +released sections: apply those from the version you run up to the one you +install. `Unreleased` applies only when you build from `main`. Most binding +users install a wrapper, such as `moq-rs` on PyPI or `dev.moq:moq` on Maven, +whose guide is in the index below. The raw UniFFI packages published as `moq-ffi` have no guide of their own: [moq-ffi's](https://github.com/moq-dev/moq/blob/main/rs/moq-ffi/MIGRATE.md) covers that layer, in UniFFI names. This page covers what spans packages: the diff --git a/rs/moq-relay/MIGRATE.md b/rs/moq-relay/MIGRATE.md index f4c09a54dc..34fc025942 100644 --- a/rs/moq-relay/MIGRATE.md +++ b/rs/moq-relay/MIGRATE.md @@ -10,6 +10,14 @@ Rolling a cluster across a release also follows the [upgrade guide](https://moq. A `[quic]` `max_streams` of 0 stops startup, as `[peer.quic]` already did, since zero streams wedge the connection. Remove it to keep the default. +```toml +# Before +[quic] +max_streams = 0 +# After: omit max_streams for the default +[quic] +``` + ### Auth roles carry `moq_auth::Access` (#5697) `auth::Token::publish` and `subscribe` are `moq_auth::Access`, so an embedder reads the patterns from `.path`. From f7d63e5f5d6e3b6770c9e1e3cea47e4ee88a3e8b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 14:08:59 -0700 Subject: [PATCH 31/40] docs: MIGRATE.md entries give a migration path Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 2 +- doc/lib/index.md | 4 ++-- doc/setup/upgrade.md | 11 +++++------ quest/m2/release/migrate-audit.md | 4 ++-- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 771d3380e7..d5ca5e3c0c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,7 +58,7 @@ The API is the most important thing to get right. A bad shape costs a breaking change in every language, and the surface is huge. - Report the public API and wire impact of every change, in the PR description and whenever asked. -- A breaking change adds an entry under `## Unreleased` in each affected package's `MIGRATE.md`, with a before/after snippet in that package's language. +- A breaking change adds an entry under `## Unreleased` in each affected package's `MIGRATE.md` saying how to migrate. - Keep things private until a consumer needs them. Scrutinize every new exported item. - Never add `foo_with_x`, `foo_checked`, or a compatibility shim. Make the breaking change to `foo` instead. - Let the type system make misuse unrepresentable diff --git a/doc/lib/index.md b/doc/lib/index.md index a0023128c5..d2a08e444a 100644 --- a/doc/lib/index.md +++ b/doc/lib/index.md @@ -50,5 +50,5 @@ C's audio decoder neither takes nor reports a playout delay. ## Upgrading -Each package keeps a `MIGRATE.md` with a before/after snippet for every -breaking change; [Upgrade](/setup/upgrade#migration-guides) links them all. +Each package keeps a `MIGRATE.md` saying how to migrate past every breaking +change; [Upgrade](/setup/upgrade#migration-guides) links them all. diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index 7072aa7203..51caad6081 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -6,12 +6,11 @@ description: The order to roll out a release across relays, auth servers, and cl # Upgrade Every hand-written published package keeps a `MIGRATE.md` entry for each -breaking change, newest first, with a before/after snippet wherever code or -config must change. The links below track `main`, which also holds the -released sections: apply those from the version you run up to the one you -install. `Unreleased` applies only when you build from `main`. Most binding -users install a wrapper, such as `moq-rs` on PyPI or `dev.moq:moq` on Maven, -whose guide is in the index below. The raw UniFFI +breaking change, newest first, saying how to migrate. The links below track +`main`, which also holds the released sections: apply those from the version you +run up to the one you install. `Unreleased` applies only when you build from +`main`. Most binding users install a wrapper, such as `moq-rs` on PyPI or +`dev.moq:moq` on Maven, whose guide is in the index below. The raw UniFFI packages published as `moq-ffi` have no guide of their own: [moq-ffi's](https://github.com/moq-dev/moq/blob/main/rs/moq-ffi/MIGRATE.md) covers that layer, in UniFFI names. This page covers what spans packages: the diff --git a/quest/m2/release/migrate-audit.md b/quest/m2/release/migrate-audit.md index dbcc2c7a2c..b2eef6250d 100644 --- a/quest/m2/release/migrate-audit.md +++ b/quest/m2/release/migrate-audit.md @@ -42,8 +42,8 @@ Decided 2026-10-16 (maintainer interview): 2026-09-23 train have no transition section yet (moq-json 0.4 -> 0.5, moq-flate 0.1 -> 0.2, and possibly moq-audio and moq-video), because their breaks predate the backfill's merge base. -- Compile-check the after snippets of every `MIGRATE.md` section the cut - ships against the candidate. Nothing else builds them. +- Compile-check the code in every `MIGRATE.md` section the cut ships against + the candidate. Nothing else builds it. - Required by the cut, after m1's breaking work has landed. ## Required From 1263aac02b7bc1b0a19afb4f73be1ec3273e1ba4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 14:10:00 -0700 Subject: [PATCH 32/40] docs(migrate): migration paths in moq-ffi, moq-c, C++ guides Co-Authored-By: Claude Opus 5.5 --- cpp/moq/MIGRATE.md | 25 +------ rs/moq-c/MIGRATE.md | 71 +------------------ rs/moq-ffi/MIGRATE.md | 157 +++--------------------------------------- 3 files changed, 14 insertions(+), 239 deletions(-) diff --git a/cpp/moq/MIGRATE.md b/cpp/moq/MIGRATE.md index bedbafcdbd..a24accaefc 100644 --- a/cpp/moq/MIGRATE.md +++ b/cpp/moq/MIGRATE.md @@ -10,9 +10,6 @@ The connection stats no longer carry a receive estimate. Hold the consumer `Session::recv_bandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and `cancel()` it to stop. ```cpp -// Before -auto rate = session->stats().estimated_recv_rate_bps; -// After auto bandwidth = session->recv_bandwidth(); auto rate = bandwidth ? bandwidth->peek() : std::nullopt; ``` @@ -29,35 +26,18 @@ A publisher whose timeline restarts publishes a new broadcast instead. ### Bandwidth reserves a track's demand (#5845) -`Bandwidth::reserve` takes the track's `demand()` instead of the producer, and no longer fails. - -```cpp -// Before -auto reservation = ok(bandwidth->reserve(track, max_bps)); -// After -auto reservation = bandwidth->reserve(ok(track->demand()), max_bps); -``` +`Bandwidth::reserve` takes the track's `demand()` instead of the producer, and no longer fails: pass `ok(track->demand())`. ### Generated types drop the `Moq` prefix (#5727) `moq::MoqFoo` is `moq::Foo`, the name the aliases already gave it. `moq::expected` is `tl::expected` at every C++ standard instead of `std::expected` under C++23, and `MOQ_ABI`, `MOQ_ABI_NAME`, and `MOQ_IMPLEMENTATION` are gone. -```cpp -// Before -moq::MoqClientConfig config; -// After -moq::ClientConfig config; -``` - ### Media subscribe and fetch take a catalog rendition (#5834) `moq::MediaTrackConfig` and `moq::MediaGroupConfig` replace `name` and `container` with a `moq::Rendition` and follow its broadcast and track themselves. ```cpp -// Before -auto media = ok(moq::MediaTrackConsumer::subscribe(broadcast, {video.track.value_or(id), video.container}).get()); -// After auto media = ok(moq::MediaTrackConsumer::subscribe(broadcast, {moq::Rendition::kVideo{id, video}}).get()); ``` @@ -70,9 +50,6 @@ Producers expose subscriber state only through `demand()`, and a video `gop` is `moq::AudioEncoder` / `moq::VideoEncoder` encode without publishing. ```cpp -// Before -auto audio = ok(broadcast->encode_audio("audio", input, output, nullptr)); -// After moq::AudioEncoderOutput output{moq::AudioCodec::opus()}; output.track = "audio"; auto catalog = ok(moq::MediaCatalogProducer::init(broadcast)); diff --git a/rs/moq-c/MIGRATE.md b/rs/moq-c/MIGRATE.md index 7f62f5e7fe..74e6ca7371 100644 --- a/rs/moq-c/MIGRATE.md +++ b/rs/moq-c/MIGRATE.md @@ -10,11 +10,6 @@ How to update your code for each breaking change, newest first. Hold the consumer `moq_session_recv_bandwidth` returns to request PROBE, read it with `moq_bandwidth_consumer_peek` or wait with `moq_bandwidth_consumer_changed`, and release it with `moq_bandwidth_consumer_close`. ```c -// Before -moq_connection_stats stats; -moq_session_stats(session, &stats); -if (stats.estimated_recv_rate_valid) use(stats.estimated_recv_rate_bps); -// After int32_t consumer = moq_session_recv_bandwidth(session); // 0 when the version has no PROBE if (consumer > 0) { uint64_t bps; @@ -47,15 +42,6 @@ The CMake package, pkg-config name, release tags, and archives follow the new na | nix `.#libmoq` | nix `.#moq-c` | | tags `libmoq-v*`, archives `libmoq--` | tags `moq-c-v*`, archives `moq-c--` | -```cmake -# Before -find_package(moq REQUIRED) -target_link_libraries(app PRIVATE moq) -# After -find_package(moq-c REQUIRED) -target_link_libraries(app PRIVATE moq::c) -``` - ### Background panics are reported (#5562) A panic in a background task reports `MOQ_ERROR_PANIC`, and other handles keep running. @@ -64,26 +50,13 @@ A panic in a background task reports `MOQ_ERROR_PANIC`, and other handles keep r `moq_announce_update` is `moq_announce_event`, and its `active` flag is a `kind`: `MOQ_ANNOUNCE_KIND_START`, `_UPDATE`, `_END`, or `_RESTART`. `_RESTART` means another publisher instance now serves the prefix: request the path again. - -```c -// Before -moq_announce_update info; -moq_origin_announced_info(id, &info); -if (info.active) subscribe(info.prefix, info.prefix_len); -// After -moq_announce_event info; -moq_origin_announced_info(id, &info); -if (info.kind == MOQ_ANNOUNCE_KIND_START || info.kind == MOQ_ANNOUNCE_KIND_RESTART) subscribe(info.prefix, info.prefix_len); -``` +Subscribe on `MOQ_ANNOUNCE_KIND_START` or `_RESTART` where you checked `active`. ### `moq_origin_announced` takes a config (#4111) The prefix and filter arguments moved into a `moq_announce_config`; pass `NULL` to receive everything. ```c -// Before -moq_origin_announced(origin, prefix, prefix_len, NULL, 0, on_announce, user_data); -// After moq_announce_config config = { .prefix = prefix, .prefix_len = prefix_len }; moq_origin_announced(origin, &config, on_announce, user_data); ``` @@ -101,48 +74,18 @@ if (frame.timestamp_present) use(frame.timestamp_us); `moq_subscription`, `moq_audio_decoder_output`, `moq_video_decoder_output`, `moq_consume_video`, and `moq_consume_audio` take `max_delay_us` instead of `max_age_us`. -```c -// Before -moq_subscription sub = { .max_age_us = 500000 }; -// After -moq_subscription sub = { .max_delay_us = 500000 }; -``` - ### `stalled` is `enabled` (#4915) `moq_consume_video_stalled` is `moq_consume_video_enabled`, with the meaning inverted. -```c -// Before -bool stalled; -moq_consume_video_stalled(catalog, index, &stalled); -// After -bool enabled; -moq_consume_video_enabled(catalog, index, &enabled); -``` - ### Routes have one cost (#4661) -`moq_route.cold` and `has_cold` are removed. - -```c -// Before -moq_route route = { .cost = 10, .cold = 20, .has_cold = true }; -// After -moq_route route = { .cost = 10 }; -``` +`moq_route.cold` and `has_cold` are removed; drop them. ### `moq_publish_finish` is removed (#4152) Close the broadcast with `moq_publish_close`. -```c -// Before -moq_publish_finish(broadcast); -// After -moq_publish_close(broadcast); -``` - ### A decoded frame can fail on its own (#4094) `moq_decode_video_frame` can return a conversion error for a single frame; it used to end the stream at the terminal callback. @@ -151,14 +94,9 @@ moq_publish_close(broadcast); ### One client config -The 41 `moq_client_*` setters are one zero-initializable `moq_client_config`, whose durations are `_us`. +The 41 `moq_client_*` setters are one zero-initializable `moq_client_config`, whose durations are `_us`, passed to `moq_session_connect` instead of `moq_client_connect`. ```c -// Before -int client = moq_client_create(); -moq_client_set_tls_disable_verify(client, true); -moq_client_connect(url, url_len, client, origin, 0, on_status, user_data); -// After moq_client_config config = { .tls_disable_verify = true }; moq_session_connect(url, url_len, &config, origin, 0, on_status, user_data); ``` @@ -169,9 +107,6 @@ moq_session_connect(url, url_len, &config, origin, 0, on_status, user_data); Formats are enums instead of strings. ```c -// Before -int track = moq_publish_media(broadcast, "opus", 4, opus_head, opus_head_len); -// After moq_audio_init init = { .format = MOQ_AUDIO_FORMAT_OPUS, .init = opus_head, .init_len = opus_head_len }; int track = moq_publish_audio(broadcast, &init); ``` diff --git a/rs/moq-ffi/MIGRATE.md b/rs/moq-ffi/MIGRATE.md index c6563a4bb2..74b6e7dc8e 100644 --- a/rs/moq-ffi/MIGRATE.md +++ b/rs/moq-ffi/MIGRATE.md @@ -11,9 +11,6 @@ The connection stats no longer carry a receive estimate. Hold the consumer `MoqSession::recv_bandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and drop it to stop. `MoqConnectionStats::estimated_recv_rate_bps` is gone. ```rust -// Before -let rate = session.stats().estimated_recv_rate_bps; -// After let bandwidth = session.recv_bandwidth(); let rate = bandwidth.as_ref().and_then(|b| b.peek()); ``` @@ -30,14 +27,7 @@ A publisher whose timeline restarts publishes a new broadcast instead. ### Bandwidth reserves a track's demand (#5845) -`MoqBandwidth::reserve` takes a `MoqTrackDemand` instead of a `MoqTrackProducer`, so raw, media, and JSON tracks reserve the same way, and it no longer fails. - -```rust -// Before -let reservation = bandwidth.reserve(&track, max_bps)?; -// After -let reservation = bandwidth.reserve(&track.demand()?, max_bps); -``` +`MoqBandwidth::reserve` takes a `MoqTrackDemand` instead of a `MoqTrackProducer`, so raw, media, and JSON tracks reserve the same way, and it no longer fails: pass `track.demand()?`. ### Media subscribe and fetch take a catalog rendition (#5834) @@ -45,10 +35,6 @@ let reservation = bandwidth.reserve(&track.demand()?, max_bps); The audio and video decode consumers' `name` argument is `id`. ```rust -// Before -let track = video.track.clone().unwrap_or(id.clone()); -let config = MoqMediaTrackConfig { name: track, container: video.container, subscription: None }; -// After let config = MoqMediaTrackConfig { rendition: MoqRendition::Video { id, config: video }, subscription: None }; ``` @@ -62,10 +48,6 @@ The producers' `name()`, `used()`, and `unused()` are gone; read subscriber stat Codec-only `MoqAudioEncoder` / `MoqVideoEncoder` encode without publishing. ```rust -// Before -let video = broadcast.encode_video(input, output, None)?; -let frames = consumer.decode_video(name, rendition, decoder).await?; -// After let catalog = MoqMediaCatalogProducer::new(broadcast)?; let video = MoqVideoProducer::new(&catalog, input, output, None)?; let frames = MoqVideoConsumer::subscribe(&consumer, name, rendition, decoder).await?; @@ -81,31 +63,12 @@ Handle the new case where you match on errors. `MoqAnnounceConsumer::next` returns `Option`: `Start`, `Update`, `End`, or `Restart`, each carrying a `MoqAnnounce` record (`prefix`, `captures`, `route`). The `MoqAnnounceUpdate` object and its `active()` are gone. `Restart` means another publisher instance now serves the prefix: request the path again. - -```rust -// Before -let update = announced.next().await?; -if update.active() { subscribe(update.prefix()) } -// After -match announced.next().await? { - Some(MoqAnnounceEvent::Start { announce }) | Some(MoqAnnounceEvent::Restart { announce }) => subscribe(announce.prefix), - _ => {} -} -``` +Subscribe on `Start` or `Restart` where you checked `active()`. ### "Epoch" means only the publisher epoch (#5146) `MoqSession::epoch()` is `connects()`, and `MoqAudioProducer::reset_epoch()` is `reanchor()`. -```rust -// Before -let n = session.epoch(); -audio.reset_epoch()?; -// After -let n = session.connects(); -audio.reanchor()?; -``` - ### Clients and servers take a config record (#4519) `MoqClient::new()` and `MoqServer::new()` plus their setters are `MoqClient::new(MoqClientConfig)` and `MoqServer::new(MoqServerConfig)`, with nested `tls`, `quic`, `websocket`, and `backoff` records and a `versions` list. @@ -114,11 +77,6 @@ The `subscribe` origin is `consume`. A server request's `set_publish` / `set_consume` are arguments to `accept(publish, consume)`, where null inherits the server's origin. ```rust -// Before -let client = MoqClient::new(); -client.set_tls_verify(false)?; -client.set_subscribe(origin)?; -// After let client = MoqClient::new(MoqClientConfig { tls: MoqClientTls { insecure: true, ..Default::default() }, consume: Some(origin), @@ -132,9 +90,6 @@ The broadcast's `publish_binary_*`, `publish_json_*`, and `subscribe_json_*` are Create the track with `publish_track` or `subscribe_track`, then wrap it. ```rust -// Before -let producer = broadcast.publish_json_snapshot("state".into(), config)?; -// After let track = broadcast.publish_track("state".into(), None)?; let producer = MoqJsonSnapshotProducer::new(&broadcast, &track, config)?; ``` @@ -148,9 +103,6 @@ Construct `MoqMediaTrackProducer::audio` / `video` from the broadcast, a `MoqMed Producers drop `name`, `used`, and `unused`; read them through `demand()`. ```rust -// Before -let audio = broadcast.publish_audio(init)?; -// After let audio = MoqMediaTrackProducer::audio(&broadcast, MoqMediaTarget::Named { name: None }, init)?; ``` @@ -159,25 +111,11 @@ let audio = MoqMediaTrackProducer::audio(&broadcast, MoqMediaTarget::Named { nam `MoqMediaContainerStreamProducer` takes a `MoqContainerStreamInit` holding the format instead of a bare format. Its optional `timebase`, also on `MoqContainerInit`, shares one source's offset across container imports; leave it unset for independent offsets. -```rust -// Before -let importer = MoqMediaContainerStreamProducer::new(&broadcast, MoqContainerFormat::Fmp4)?; -// After -let importer = MoqMediaContainerStreamProducer::new(&broadcast, MoqContainerStreamInit { format: MoqContainerFormat::Fmp4, timebase: None })?; -``` - ### Error payloads are named -`MoqError` cases that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. +`MoqError` cases that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`, so match `MoqError::Transport { reason }`. The Display text is unchanged. -```rust -// Before -Err(MoqError::Transport(reason)) => log(reason), -// After -Err(MoqError::Transport { reason }) => log(reason), -``` - ### Timestamps are optional `MoqFrame.timestamp_us` and `MoqDatagram.timestamp_us` are optional, null on a frame read from an untimed track, and default to null. @@ -185,108 +123,47 @@ Err(MoqError::Transport { reason }) => log(reason), A raw track you publish is timed by default, so a raw write without a timestamp fails with `TimestampMismatch` instead of going out at 0. Data operations take an optional timestamp in microseconds, and whole-frame media writes require one. `MoqMediaCatalogProducer::now()` samples the broadcast clock for local-event stamps. - -```rust -// Before -track.write_frame(MoqFrame { payload, timestamp_us: 0 })?; -// After -track.write_frame(MoqFrame { payload, timestamp_us: Some(now_us) })?; -``` +Wrap raw frame timestamps in `Some`. ### Subscriber staleness is `max_delay_us` `MoqSubscription`, `MoqAudioDecoderOutput`, and `MoqVideoDecoderOutput` take `max_delay_us` instead of `max_age_us`. `MoqTrackInfo.max_age_us` keeps its name: it is the publisher's retention. -```rust -// Before -subscription.max_age_us = 500_000; -// After -subscription.max_delay_us = 500_000; -``` - ### `MoqTransport` gains `WebTransport` A WebTransport session reports `MoqTransport::WebTransport`; `Quic` now means raw QUIC only. -An exhaustive match needs one more arm. - -```rust -// Before -let quic = matches!(request.transport(), MoqTransport::Quic); -// After -let quic = matches!(request.transport(), MoqTransport::Quic | MoqTransport::WebTransport); -``` +An exhaustive match needs one more arm, and a `Quic` check that meant any QUIC session also matches `WebTransport`. ### `stalled` is `enabled` (#4915) `MoqVideo.stalled` is `enabled`, defaulting to true with the meaning inverted, and `MoqAudio` gains `enabled`. -```rust -// Before -if !video.stalled { select(video) } -// After -if video.enabled { select(video) } -``` - ### Routes have one cost (#4661) -`MoqRoute.cold` is removed. - -```rust -// Before -let route = MoqRoute { cost: 10, cold: Some(50), ..Default::default() }; -// After -let route = MoqRoute { cost: 10, ..Default::default() }; -``` +`MoqRoute.cold` is removed; drop it. ### Decoded video frames own their surface `MoqVideoConsumer::next` returns an `Arc` object with `timestamp_us()`, `width()`, `height()`, `pixels(format)`, and `surface()`. `MoqVideoDecoderOutput.format` is gone; set `surface: true` to keep the decoder's native surface, readable as a `MoqVideoSurface`. - -```rust -// Before -let rgba = frame.data; -// After -let rgba = frame.pixels(MoqVideoPixelFormat::Rgba)?; -``` +Read RGBA with `pixels(MoqVideoPixelFormat::Rgba)` instead of `data`. ### Broadcast `finish` is removed (#4152) `MoqBroadcastProducer::finish()` is gone; close the broadcast instead. -```rust -// Before -broadcast.finish()?; -// After -broadcast.close()?; -``` - ### The `android-logcat` feature is removed (#4608) Android builds always log to logcat; drop the feature. -```sh -# Before -cargo ndk -t arm64-v8a build -p moq-ffi --features android-logcat -# After -cargo ndk -t arm64-v8a build -p moq-ffi -``` - ## 0.3 -> 0.4 ### Publish split by kind -`publish_media*` is `publish_audio`, `publish_video`, or `publish_container`, each taking its own init. +`publish_media*` is `publish_audio`, `publish_video`, or `publish_container`, each taking its own init (`MoqAudioInit` with a `MoqAudioFormat` for audio). The raw encoder paths that used to be `publish_audio` / `publish_video` are `encode_audio` / `encode_video`. -```rust -// Before -let media = broadcast.publish_media(MoqInit { format: "opus".into(), data, label: None, video: None })?; -// After -let media = broadcast.publish_audio(MoqAudioInit { format: MoqAudioFormat::Opus, data, label: None })?; -``` - ### Renames | Before | After | @@ -302,25 +179,11 @@ let media = broadcast.publish_audio(MoqAudioInit { format: MoqAudioFormat::Opus, ### Setters are fallible (#3642) -Client and server setters return an error (`Busy` during connect or listen) instead of dropping the value. - -```rust -// Before -client.set_publish(Some(origin)); -// After -client.set_publish(Some(origin))?; -``` +Client and server setters return an error (`Busy` during connect or listen) instead of dropping the value, so handle it. ### Protocol errors are structured -`MoqError::Protocol` carries a `MoqProtocolError` (scope, wire code, kind) instead of a flattened message. - -```rust -// Before -Err(MoqError::Protocol(err)) => log(err.to_string()), -// After -Err(MoqError::Protocol { details }) => log(details.kind, details.code), -``` +`MoqError::Protocol` carries a `MoqProtocolError` (scope, wire code, kind) in its `details` field instead of a flattened message. ### Track and group `finish()` keep the handle From 5e8dd2c41ca88b32265774ba949843b79cf8c45e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 14:09:44 -0700 Subject: [PATCH 33/40] docs(migrate): migration paths in Kotlin and Dart guides Co-Authored-By: Claude Opus 5.5 --- dart/moq/MIGRATE.md | 110 +++---------------------------- kt/moq/MIGRATE.md | 154 ++++---------------------------------------- 2 files changed, 21 insertions(+), 243 deletions(-) diff --git a/dart/moq/MIGRATE.md b/dart/moq/MIGRATE.md index 1fa860c041..a6dab5d00c 100644 --- a/dart/moq/MIGRATE.md +++ b/dart/moq/MIGRATE.md @@ -8,14 +8,7 @@ How to update your code for each breaking change, newest first. The connection stats no longer carry a receive estimate. Hold the consumer `Session.recvBandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and `cancel()` it to stop. - -```dart -// Before -final rate = session.stats().estimatedRecvRateBps; -// After -final bandwidth = session.recvBandwidth(); -final rate = bandwidth?.peek(); -``` +Replace `stats().estimatedRecvRateBps` with `recvBandwidth()?.peek()`. ### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) @@ -30,22 +23,13 @@ A publisher whose timeline restarts publishes a new broadcast instead. ### Bandwidth reserves a track's demand (#5845) `Bandwidth.reserve` takes the track's `demand()` instead of the producer, and no longer throws. - -```dart -// Before -final reservation = bandwidth.reserve(track: track, maxBps: maxBps); -// After -final reservation = bandwidth.reserve(demand: track.demand(), maxBps: maxBps); -``` +Pass `demand: track.demand()` instead of `track: track`. ### Media subscribe and fetch take a catalog rendition (#5834) `media.TrackConfig` and `media.GroupConfig` replace `name` and `container` with a `rendition` (`media.VideoRendition` or `media.AudioRendition`) and follow its broadcast and track themselves. ```dart -// Before -final config = media.TrackConfig(name: id, container: video.container); -// After final config = media.TrackConfig(rendition: media.VideoRendition(id: id, config: video)); ``` @@ -60,11 +44,6 @@ A panic in a background task throws `TaskMoqException` with the panic message as A restart means another publisher instance now serves the prefix: request the path again. ```dart -// Before -moq.announcements().listen((update) { - if (update.active()) subscribe(update.prefix()); -}); -// After moq.announced().updates().listen((event) { switch (event) { case AnnounceEventStart(:final announce) || @@ -79,35 +58,18 @@ moq.announced().updates().listen((event) { `Moq.epoch` is `connects`. -```dart -// Before -final n = moq.epoch; -// After -final n = moq.connects; -``` - ### The consume origin is `consume` `ConnectOptions.subscribe` is `consume`. A value the native side cannot use fails `Moq.connect` with `ConfigMoqException`, including a bad bind address. A server request takes its origins in `accept(publish: ..., consume: ...)`, where null inherits the server's origin. -```dart -// Before -final moq = await Moq.connect(url, options: ConnectOptions(subscribe: origin)); -// After -final moq = await Moq.connect(url, options: ConnectOptions(consume: origin)); -``` - ### Data tracks wrap a track `BroadcastProducer.publishJsonSnapshot` / `publishJsonStream`, the binary variants, and the matching consumers are gone. Create the track, then wrap it with `package:moq/json.dart` (or `package:moq/flate.dart` for opaque bytes). ```dart -// Before -final state = broadcast.publishJsonSnapshot(name: 'state', config: JsonSnapshotConfig()); -// After final state = moq_json.SnapshotProducer( broadcast: broadcast, track: broadcast.publishTrack(name: 'state', info: null), @@ -122,9 +84,6 @@ Use `media.TrackProducer.audio` / `video`, `ContainerProducer`, `CatalogProducer Producers drop `name`, `used`, and `unused`; read them through `demand()`. ```dart -// Before -final audio = broadcast.publishAudio(init: init); -// After final audio = media.TrackProducer.audio(broadcast: broadcast, target: media.Named(null), init: init); ``` @@ -132,90 +91,39 @@ final audio = media.TrackProducer.audio(broadcast: broadcast, target: media.Name `media.ContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. Its optional `timebase`, also on `ContainerInit`, shares one source's offset across container imports. - -```dart -// Before -final producer = media.ContainerStreamProducer(broadcast: broadcast, format: media.ContainerFormat.fmp4); -// After -final producer = media.ContainerStreamProducer(broadcast: broadcast, init: media.ContainerStreamInit(format: media.ContainerFormat.fmp4)); -``` +Replace `format: ...` with `init: media.ContainerStreamInit(format: ...)`. ### Timestamps are optional `Frame.timestampUs` and `Datagram.timestampUs` are nullable, null on a frame read from an untimed track, and so are their `timestamp` getters. A raw write to a timed track without a timestamp throws `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. `media.CatalogProducer.now()` samples the broadcast clock for local-event stamps. - -```dart -// Before -track.writeFrame(frame: Frame(payload: payload)); -final Duration ts = frame.timestamp; -// After -track.writeFrame(frame: Frame(payload: payload, timestampUs: pts)); -final Duration? ts = frame.timestamp; -``` +Pass `timestampUs` when writing to a timed track, and handle a null `timestamp` when reading. ### Subscriber staleness is `maxDelay` `Subscription.maxAgeUs` / `maxAge` is `maxDelayUs` / `maxDelay`. `TrackInfo.maxAge` keeps its name: it is the publisher's retention. -```dart -// Before -final sub = Subscription(maxAgeUs: 500000); -// After -final sub = Subscription(maxDelayUs: 500000); -``` - ### Error payloads are named `reason` Exceptions that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. - -```dart -// Before -final reason = (error as TransportMoqException).v0; -// After -final reason = (error as TransportMoqException).reason; -``` +Read `reason` instead of `v0`. ### `Transport` gains `webTransport` A WebTransport session reports `MoqTransport.webTransport`; `quic` now means raw QUIC only, so an exhaustive `switch` needs one more case. - -```dart -// Before -case MoqTransport.quic: -// After -case MoqTransport.quic || MoqTransport.webTransport: -``` +Match `MoqTransport.webTransport` wherever you matched `MoqTransport.quic`. ### `stalled` is `enabled` (#4915) `media.Video.stalled` is `enabled`, defaulting to true with the meaning inverted, and `media.Audio` gains `enabled`. - -```dart -// Before -final live = !video.stalled; -// After -final live = video.enabled; -``` +Replace `!video.stalled` with `video.enabled`. ### Routes have one cost (#4661) -`MoqRoute.cold` is removed. - -```dart -// Before -final route = MoqRoute(cost: 10, cold: 20); -// After -final route = MoqRoute(cost: 10); -``` +`MoqRoute.cold` is removed; drop the `cold` argument. ### `BroadcastProducer.finish()` is removed (#4152) -```dart -// Before -broadcast.finish(); -// After -broadcast.close(); -``` +Call `close()` instead. diff --git a/kt/moq/MIGRATE.md b/kt/moq/MIGRATE.md index caaba41f9d..f923fa5239 100644 --- a/kt/moq/MIGRATE.md +++ b/kt/moq/MIGRATE.md @@ -8,14 +8,7 @@ How to update your code for each breaking change, newest first. The connection stats no longer carry a receive estimate. Hold the consumer `Session.recvBandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and `cancel()` it to stop. - -```kotlin -// Before -val rate = session.stats().estimatedRecvRateBps -// After -val bandwidth = session.recvBandwidth() -val rate = bandwidth?.peek() -``` +Replace `stats().estimatedRecvRateBps` with `recvBandwidth()?.peek()`. ### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) @@ -31,21 +24,11 @@ A publisher whose timeline restarts publishes a new broadcast instead. `Bandwidth.reserve` takes the track's `demand()` instead of the producer, and no longer throws. -```kotlin -// Before -val reservation = bandwidth.reserve(track, maxBps) -// After -val reservation = bandwidth.reserve(track.demand(), maxBps) -``` - ### Media subscribe and fetch take a catalog rendition (#5834) `TrackConfig` and `GroupConfig` in `dev.moq.media` replace `name` and `container` with a `rendition` (`VideoRendition` or `AudioRendition`) and follow its broadcast and track themselves. ```kotlin -// Before -val config = TrackConfig(name = id, container = video.container) -// After val config = TrackConfig(rendition = VideoRendition(id, video)) ``` @@ -58,10 +41,6 @@ Producers expose subscriber state only through `demand()`, and a video `gop` is `dev.moq.audio.Encoder` / `dev.moq.video.Encoder` encode without publishing. ```kotlin -// Before -val video = broadcast.encodeVideo(input, output) -val frames = consumer.decodeVideo(name, rendition, decoder) -// After val video = dev.moq.video.Producer(CatalogProducer(broadcast), input, output) val frames = dev.moq.video.Consumer.subscribe(consumer, name, rendition, decoder) ``` @@ -77,11 +56,6 @@ A panic in a background task throws `uniffi.moq.MoqException.Task` with the pani A restart means another publisher instance now serves the prefix: request the path again. ```kotlin -// Before -moq.announcements().collect { update -> - if (update.active()) subscribe(update.prefix()) -} -// After moq.announced().updates().collect { event -> when (event) { is AnnounceEventStart -> subscribe(event.announce.prefix) @@ -95,15 +69,6 @@ moq.announced().updates().collect { event -> `Moq.epoch()` is `connects()`, and `AudioProducer.resetEpoch()` is `reanchor()`. -```kotlin -// Before -val n = moq.epoch() -audio.resetEpoch() -// After -val n = moq.connects() -audio.reanchor() -``` - ### `connect` takes a `ClientConfig` The keyword arguments moved into a `ClientConfig` with nested `tls`, `quic`, `websocket`, and `backoff` records. @@ -112,9 +77,6 @@ A value the native side cannot use throws `MoqException.Config`, including a bad A server request takes its origins in `accept(publish, consume)`, where `null` inherits the server's origin. ```kotlin -// Before -val moq = Moq.connect(url, tlsVerify = false, subscribe = origin) -// After val moq = Moq.connect(url, ClientConfig(tls = ClientTls(insecure = true), consume = origin)) ``` @@ -124,9 +86,6 @@ val moq = Moq.connect(url, ClientConfig(tls = ClientTls(insecure = true), consum Create the track, then wrap it with `dev.moq.json` (or `dev.moq.flate` for opaque bytes). ```kotlin -// Before -val state = broadcast.publishJsonSnapshot("state", config) -// After val state = dev.moq.json.SnapshotProducer(broadcast, broadcast.publishTrack("state", null), config) ``` @@ -137,9 +96,6 @@ Use `TrackProducer.audio` / `video`, `ContainerProducer`, `CatalogProducer` (wit Producers drop `name`, `used`, and `unused`; read them through `demand()`. ```kotlin -// Before -val audio = broadcast.publishAudio(AudioInit(format = AudioFormat.OPUS, data = opusHead)) -// After val audio = dev.moq.media.TrackProducer.audio(broadcast, Named(null), AudioInit(format = AudioFormat.OPUS, data = opusHead)) ``` @@ -148,144 +104,64 @@ val audio = dev.moq.media.TrackProducer.audio(broadcast, Named(null), AudioInit( `ContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. Its optional `timebase`, also on `ContainerInit`, shares one source's offset across container imports. -```kotlin -// Before -val importer = ContainerStreamProducer(broadcast, ContainerFormat.FMP4) -// After -val importer = ContainerStreamProducer(broadcast, ContainerStreamInit(ContainerFormat.FMP4)) -``` - ### Timestamps are optional `Frame.timestamp` and `Datagram.timestamp` are `Duration?`, null on a frame read from an untimed track. A raw write to a timed track without a timestamp throws `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. `CatalogProducer.now()` samples the broadcast clock for local-event stamps. - -```kotlin -// Before -track.writeFrame(Frame(payload = bytes)) -val ts: Duration = frame.timestamp -// After -track.writeFrame(Frame(payload = bytes, timestampUs = nowUs)) -val ts: Duration? = frame.timestamp -``` +Pass `timestampUs` when writing to a timed track, and handle a null `timestamp` when reading. ### Subscriber staleness is `maxDelay` -`Subscription.maxAge`, `AudioDecoderOutput.maxAge`, and `VideoDecoderOutput.maxAge` are `maxDelay`. +`Subscription.maxAge`, `AudioDecoderOutput.maxAge`, and `VideoDecoderOutput.maxAge` are `maxDelay`, and the `maxAgeUs` argument is `maxDelayUs`. `TrackInfo.maxAge` keeps its name: it is the publisher's retention. -```kotlin -// Before -val subscription = Subscription(maxAgeUs = 500_000u) -val budget = subscription.maxAge -// After -val subscription = Subscription(maxDelayUs = 500_000u) -val budget = subscription.maxDelay -``` - ### Error payloads are named `reason` Exceptions that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. - -```kotlin -// Before -catch (e: MoqException.Transport) { log(e.v1) } -// After -catch (e: MoqException.Transport) { log(e.reason) } -``` +Read `e.reason` instead of `e.v1`. ### `Transport` gains `WEB_TRANSPORT` A WebTransport session reports `Transport.WEB_TRANSPORT`; `QUIC` now means raw QUIC only, so an exhaustive `when` needs one more branch. - -```kotlin -// Before -val quic = request.transport() == Transport.QUIC -// After -val quic = request.transport() == Transport.QUIC || request.transport() == Transport.WEB_TRANSPORT -``` +Check for `WEB_TRANSPORT` wherever you checked for `QUIC`. ### `stalled` is `enabled` (#4915) `Video.stalled` is `enabled`, defaulting to true with the meaning inverted, and `Audio` gains `enabled`. - -```kotlin -// Before -if (!video.stalled) select(video) -// After -if (video.enabled) select(video) -``` +Replace `!video.stalled` with `video.enabled`. ### Routes have one cost (#4661) -`Route.cold` is removed. - -```kotlin -// Before -val route = Route(cost = 10u, cold = 50u) -// After -val route = Route(cost = 10u) -``` +`Route.cold` is removed; drop the `cold` argument. ### Decoded video frames are objects `VideoDecodedFrame` is a closeable handle with `width()`, `height()`, `pixels(format)`, and `surface()`. `VideoDecoderOutput.format` is gone; set `surface = true` to keep the decoder's native surface. - -```kotlin -// Before -val rgba = frame.data -// After -val rgba = frame.use { it.pixels(VideoPixelFormat.RGBA) } -``` +Replace `frame.data` with `frame.use { it.pixels(VideoPixelFormat.RGBA) }`. ### `BroadcastProducer.finish()` is removed (#4152) Call `end()` to finish the broadcast and keep the handle, or `close()` to release it. -```kotlin -// Before -broadcast.finish() -// After -broadcast.end() -``` - ## 0.4 -> 0.5 ### Durations are `kotlin.time.Duration` Extension properties expose every microsecond field as a `Duration`: `Frame.timestamp`, `Subscription.maxAge`, `ConnectionStats.rtt`, `Backoff.initial`, and the rest. - -```kotlin -// Before -val delayMs = backoff.initialMs -// After -val delay = backoff.initial -``` +Replace the millisecond fields, such as `backoff.initialMs`, with these properties. ### `connect` grows options `Moq.connect` gains `reconnect`, `backoff`, `maxStreams`, and the WebSocket fallback settings as named arguments. - -```kotlin -// Before -val moq = Moq.connect(url, ..., origin) -// After -val moq = Moq.connect(url, publish = origin) -``` +Pass the origin by name: `Moq.connect(url, publish = origin)`. ### Publish split by kind `publishMedia*` is `publishAudio`, `publishVideo`, or `publishContainer`, each taking its own init. The raw encoder paths that used to be `publishAudio` / `publishVideo` are `encodeAudio` / `encodeVideo`. - -```kotlin -// Before -val media = broadcast.publishMedia(Init(format = "opus", data = opusHead, video = null)) -// After -val media = broadcast.publishAudio(AudioInit(format = AudioFormat.OPUS, data = opusHead)) -``` +For example, `publishAudio(AudioInit(format = AudioFormat.OPUS, data = opusHead))`. ### Renames @@ -301,10 +177,4 @@ val media = broadcast.publishAudio(AudioInit(format = AudioFormat.OPUS, data = o `MoqException.Protocol` carries a `ProtocolError` (scope, wire code, kind) instead of a flattened message. Setters throw (`Busy` during connect or listen) instead of dropping the value. Track and group `finish()` keep the handle open, so a later `abort()` can still run. - -```kotlin -// Before -catch (e: MoqException.Protocol) { log(e.message) } -// After -catch (e: MoqException.Protocol) { log("${e.details.kind} ${e.details.code}") } -``` +Read `e.details.kind` and `e.details.code` instead of `e.message`. From 0fef09eeab51184e3769f9770737a88dd9475b7e Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 14:10:01 -0700 Subject: [PATCH 34/40] docs(migrate): migration paths in Go and Swift guides Co-Authored-By: Claude Opus 5.5 --- go/wrapper/MIGRATE.md | 164 +++--------------------------------------- swift/MIGRATE.md | 138 ++--------------------------------- 2 files changed, 18 insertions(+), 284 deletions(-) diff --git a/go/wrapper/MIGRATE.md b/go/wrapper/MIGRATE.md index 40c6b97732..a9a4fb43ce 100644 --- a/go/wrapper/MIGRATE.md +++ b/go/wrapper/MIGRATE.md @@ -8,17 +8,7 @@ How to update your code for each breaking change, newest first. The connection stats no longer carry a receive estimate. Hold the consumer `Session.RecvBandwidth` returns to request PROBE, read it with `Peek()` or wait with `Changed(ctx)`, and `Close()` it to stop. - -```go -// Before -rate := session.Stats().EstimatedRecvRateBps -// After -var rate *uint64 -if bandwidth := session.RecvBandwidth(); bandwidth != nil { // nil when the version has no PROBE - defer bandwidth.Close() - rate = bandwidth.Peek() -} -``` +It returns `nil` when the version has no PROBE. ### `AnnouncedBroadcast.Available` waits for the next publisher instance (#5700) @@ -34,22 +24,11 @@ A publisher whose timeline restarts publishes a new broadcast instead. `Bandwidth.Reserve` takes the track's `Demand()` instead of the producer, and no longer returns an error. -```go -// Before -reservation, err := bandwidth.Reserve(track, maxBps) -// After -demand, err := track.Demand() -reservation := bandwidth.Reserve(demand, maxBps) -``` - ### Media subscribe and fetch take a catalog rendition (#5834) `media.TrackConfig` and `media.GroupConfig` replace `Name` and `Container` with a `Rendition`, built by `media.VideoRendition` or `media.AudioRendition`, and follow its broadcast and track themselves. ```go -// Before -config := media.TrackConfig{Name: id, Container: video.Container} -// After config := media.TrackConfig{Rendition: media.VideoRendition(id, video)} ``` @@ -62,11 +41,6 @@ Producers expose subscriber state only through `Demand()`, and a video GOP is `v `audio.NewEncoder` / `video.NewEncoder` encode without publishing. ```go -// Before -video, err := broadcast.EncodeVideo(moq.VideoEncoderInput{...}, moq.VideoEncoderOutput{...}, nil) -err = video.Write(moq.VideoFrame{TimestampUs: pts, Data: rgba}) -frames, err := consumer.DecodeVideo(ctx, name, rendition, output) -// After catalog, err := moqmedia.NewCatalogProducer(broadcast) video, err := moqvideo.NewProducer(catalog, moqvideo.EncoderInput{...}, moqvideo.EncoderOutput{...}, nil) err = video.Write(moqvideo.Frame{Timestamp: time.Duration(pts) * time.Microsecond, Data: rgba}) @@ -84,13 +58,6 @@ A panic in a background task returns an `ErrTask` error carrying the panic messa A restart means another publisher instance now serves the prefix: request the path again. ```go -// Before -for update, err := range announced.All(ctx) { - if err == nil && update.Active() { - subscribe(update.Prefix()) - } -} -// After for event, err := range announced.All(ctx) { switch e := event.(type) { case moq.AnnounceEventStart: @@ -105,15 +72,6 @@ for event, err := range announced.All(ctx) { `Session.Epoch()` is `Connects()`, and `AudioProducer.ResetEpoch()` is `Reanchor()`. -```go -// Before -n := session.Epoch() -audio.ResetEpoch() -// After -n := session.Connects() -audio.Reanchor() -``` - ### Origins are `consume`, and servers accept with them `WithSubscribeOrigin` is `WithConsumeOrigin`. @@ -121,19 +79,6 @@ A server request's `SetPublish` / `SetConsume` are arguments to `Accept(ctx, pub `Server.Requests(ctx)` is `All(ctx)`, and the `Status*` constants are `ConnectionStatus*`. A value the native side cannot use fails `Dial` with a config error, including a bad bind address. -```go -// Before -client, err := moq.Dial(ctx, url, moq.WithSubscribeOrigin(origin)) -for request, err := range server.Requests(ctx) { - session, err := request.Accept(ctx) -} -// After -client, err := moq.Dial(ctx, url, moq.WithConsumeOrigin(origin)) -for request, err := range server.All(ctx) { - session, err := request.Accept(ctx, nil, nil) -} -``` - ### Media lives in `moq.dev/moq/media` `BroadcastProducer.PublishAudio`, `PublishVideo`, `PublishContainer`, their `OnTrack` and `Stream` variants, `SetVideoProperties`, and `SetCatalogSection`, and the consumer's catalog and media subscriptions, are gone. @@ -142,9 +87,6 @@ Producers drop `Name`, `Used`, and `Unused`; read them through `Demand()`. JSON and opaque data tracks live in `moq.dev/moq/json` and `moq.dev/moq/flate`, wrapping a track from `PublishTrack`. ```go -// Before -audio, err := broadcast.PublishAudio(moq.AudioFormatOpus, opusHead) -// After audio, err := media.NewAudioTrackProducer(broadcast, media.Named{}, media.AudioInit{Format: media.AudioFormatOpus, Data: opusHead}) ``` @@ -153,139 +95,61 @@ audio, err := media.NewAudioTrackProducer(broadcast, media.Named{}, media.AudioI `media.NewContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. Its optional `Timebase`, also on `ContainerInit`, shares one source's offset across container imports. -```go -// Before -producer, err := media.NewContainerStreamProducer(broadcast, media.ContainerFormatFmp4) -// After -producer, err := media.NewContainerStreamProducer(broadcast, media.ContainerStreamInit{Format: media.ContainerFormatFmp4}) -``` - ### Durations are `time.Duration` `Frame`, `Datagram`, `Subscription`, `TrackInfo`, and `ConnectionStats` carry `time.Duration` instead of microsecond integers: `TimestampUs` is `Timestamp`, and `RttUs` is `RTT`. A frame timestamp is a `*time.Duration`, `nil` on a frame read from an untimed track. -```go -// Before -err := track.WriteFrame(moq.Frame{Payload: payload, TimestampUs: 1000}) -// After -ts := time.Millisecond -err := track.WriteFrame(moq.Frame{Payload: payload, Timestamp: &ts}) -``` - ### Timestamps are optional A raw write to a timed track without a timestamp fails with `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. `media.CatalogProducer.Now()` samples the broadcast clock for local-event stamps. - -```go -// Before -err := track.WriteFrame(moq.Frame{Payload: payload}) -// After -err := track.WriteFrame(moq.Frame{Payload: payload, Timestamp: &pts}) -``` +Set `Timestamp` on every write to a timed track. ### Subscriber staleness is `MaxDelay` -`Subscription.MaxAgeUs` is `MaxDelay`, and `audio.DecoderOutput` / `video.DecoderOutput` take `MaxDelay *time.Duration`. +`Subscription.MaxAgeUs` is `MaxDelay`, a `time.Duration`, and `audio.DecoderOutput` / `video.DecoderOutput` take `MaxDelay *time.Duration`. `TrackInfo.MaxAge` keeps its name: it is the publisher's retention. -```go -// Before -sub := moq.Subscription{MaxAgeUs: 500_000} -// After -sub := moq.Subscription{MaxDelay: 500 * time.Millisecond} -``` - ### Error payloads are named `Reason` -Errors that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `Reason`. - -```go -// Before -reason := transport.Field0 // *ffi.MoqErrorTransport from errors.As -// After -reason := transport.Reason -``` +Errors that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `Reason`: read `Reason` instead of `Field0`. ### `Transport` gains `WebTransport` A WebTransport session reports `TransportWebTransport`; `TransportQUIC` now means raw QUIC only. - -```go -// Before -quic := request.Transport() == moq.TransportQUIC -// After -quic := request.Transport() == moq.TransportQUIC || request.Transport() == moq.TransportWebTransport -``` +Check for `TransportWebTransport` too where you matched `TransportQUIC`. ### `Stalled` is `Enabled` (#4915) `media.Video.Stalled` is `Enabled`, defaulting to true with the meaning inverted, and `media.Audio` gains `Enabled`. - -```go -// Before -live := !video.Stalled -// After -live := video.Enabled -``` +Read `Enabled` where you read `!Stalled`. ### Routes have one cost (#4661) -`Route.Cold` is removed. - -```go -// Before -route := moq.Route{Cost: 10, Cold: &cold} -// After -route := moq.Route{Cost: 10} -``` +`Route.Cold` is removed; set only `Cost`. ### Decoded video frames are objects `VideoDecodedFrame` is a handle with `TimestampUs()`, `Width()`, `Height()`, `Pixels(format)`, `Surface()`, and `Close()`. `VideoDecoderOutput.Format` is gone; set `Surface: true` to keep the decoder's native surface. - -```go -// Before -rgba := frame.Data -// After -rgba, err := frame.Pixels(moq.VideoPixelFormatRgba) -frame.Close() -``` +Call `frame.Pixels(moq.VideoPixelFormatRgba)` instead of reading `frame.Data`, and `Close()` the frame when done. ### `BroadcastProducer.Finish()` is removed (#4152) -```go -// Before -broadcast.Finish() -// After -broadcast.Close() -``` +Call `Close()` instead. ## 0.5 -> 0.7 ### The module path is `moq.dev/moq` -```go -// Before -import "github.com/moq-dev/moq-go/moq" -// After -import "moq.dev/moq" -``` +Import `moq.dev/moq` instead of `github.com/moq-dev/moq-go/moq`. ### Publish split by kind `PublishMedia*` is `PublishAudio`, `PublishVideo`, or `PublishContainer`, each taking its own init. The raw encoder paths that used to be `PublishAudio` / `PublishVideo` are `EncodeAudio` / `EncodeVideo`. -```go -// Before -media, err := broadcast.PublishMedia("opus", opusHead) -// After -media, err := broadcast.PublishAudio(moq.AudioFormatOpus, opusHead) -``` - ### Renames | Before | After | @@ -302,10 +166,4 @@ media, err := broadcast.PublishAudio(moq.AudioFormatOpus, opusHead) Protocol errors carry a `ProtocolError` (scope, wire code, kind) instead of a flattened message. Setters return an error (`Busy` during connect or listen) instead of dropping the value. Track and group `Finish()` keep the handle open, so a later `Abort()` can still run. - -```go -// Before -if errors.Is(err, moq.ErrProtocol) { log.Print(err.Error()) } -// After -if p, ok := moq.ProtocolError(err); ok { log.Print(p.Kind, p.Code) } -``` +Match with `moq.ProtocolError(err)` instead of `errors.Is(err, moq.ErrProtocol)`, and read its `Kind` and `Code`. diff --git a/swift/MIGRATE.md b/swift/MIGRATE.md index 07e26471a5..7c6ddda498 100644 --- a/swift/MIGRATE.md +++ b/swift/MIGRATE.md @@ -8,14 +8,7 @@ How to update your code for each breaking change, newest first. The connection stats no longer carry a receive estimate. Hold the consumer `Session.recvBandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and drop it to stop. - -```swift -// Before -let rate = session.stats().estimatedRecvRateBps -// After -let bandwidth = session.recvBandwidth() -let rate = bandwidth?.peek() -``` +It returns `nil` when the version has no PROBE. ### `AnnouncedBroadcast.available()` waits for the next publisher instance (#5700) @@ -31,25 +24,13 @@ A publisher whose timeline restarts publishes a new broadcast instead. `Bandwidth.reserve(demand:maxBps:)` takes the track's `demand()` instead of the producer, and no longer throws. -```swift -// Before -let reservation = try bandwidth.reserve(track: track, maxBps: maxBps) -// After -let reservation = bandwidth.reserve(demand: try track.demand(), maxBps: maxBps) -``` - ### Media subscribe and fetch take a catalog rendition (#5834) `Media.TrackConfig` and `Media.GroupConfig` replace `name` and `container` with a `Media.Rendition` and follow its broadcast and track themselves. `Audio.Consumer.subscribe` and `Video.Consumer.subscribe` label the catalog key `id:` instead of `name:`. ```swift -// Before -let config = Media.TrackConfig(name: id, container: video.container) -let decoder = try await Video.Consumer.subscribe(broadcast: consumer, name: id, rendition: video) -// After let config = Media.TrackConfig(rendition: .video(id: id, config: video)) -let decoder = try await Video.Consumer.subscribe(broadcast: consumer, id: id, rendition: video) ``` ### Native codecs have `Audio` and `Video` namespaces (#5694) @@ -61,10 +42,6 @@ Producers expose subscriber state only through `demand()`, and a video GOP is `V `Audio.Encoder` / `Video.Encoder` encode without publishing. ```swift -// Before -let video = try broadcast.encodeVideo(input: input, output: output) -let frames = try await consumer.decodeVideo(name: name, catalogVideo: rendition, output: decoder) -// After let video = try Video.Producer(catalog: Media.CatalogProducer(broadcast: broadcast), input: input, output: output) let frames = try await Video.Consumer.subscribe(broadcast: consumer, id: name, rendition: rendition, output: decoder) ``` @@ -80,11 +57,6 @@ A panic in a background task throws `MoqError.Task` with the panic message as `r A restart means another publisher instance now serves the prefix: request the path again. ```swift -// Before -for try await update in try session.consume.announced(prefix: "live/") where update.active() { - subscribe(update.prefix()) -} -// After for try await event in try session.consume.announced(prefix: "live/") { switch event { case .start(let announce), .restart(let announce): subscribe(announce.prefix) @@ -97,15 +69,6 @@ for try await event in try session.consume.announced(prefix: "live/") { `Session.epoch()` is `connects()`, and `AudioProducer.resetEpoch()` is `reanchor()`. -```swift -// Before -let n = session.epoch() -try audio.resetEpoch() -// After -let n = session.connects() -try audio.reanchor() -``` - ### `Client` takes its config at init The `Client` setters are `Client(...)` arguments, with `tls`, `quic`, `websocket`, and `backoff` records. @@ -115,11 +78,6 @@ A value the native side cannot use throws `MoqError.Config`, including a bad bin `Session.shutdown()` is `async throws` and drains before returning. ```swift -// Before -let client = Client() -try client.setTlsVerify(false) -try client.setConsume(origin) -// After let client = try Client(tls: ClientTls(insecure: true), consume: origin) ``` @@ -129,9 +87,6 @@ let client = try Client(tls: ClientTls(insecure: true), consume: origin) Create the track, then wrap it with `Json` (or `Flate` for opaque bytes). ```swift -// Before -let state = try broadcast.publishJsonSnapshot(name: "state", of: State.self) -// After let state = try Json.SnapshotProducer(broadcast: broadcast, track: try broadcast.publishTrack(name: "state")) ``` @@ -143,9 +98,6 @@ The catalog types (`Catalog`, `Video`, `Audio`, `AudioFormat`, ...) moved under Producers drop `name`, `used`, and `unused`; read them through `demand()`. ```swift -// Before -let audio = try broadcast.publishAudio(format: .opus, initData: opusHead) -// After let audio = try Media.TrackProducer.audio(broadcast: broadcast, initData: Media.AudioInit(format: .opus, data: opusHead)) ``` @@ -154,107 +106,46 @@ let audio = try Media.TrackProducer.audio(broadcast: broadcast, initData: Media. `Media.ContainerStreamProducer` takes a `Media.ContainerStreamInit` holding the format instead of a bare format. Its optional `timebase`, also on `Media.ContainerInit`, shares one source's offset across container imports. -```swift -// Before -let importer = try Media.ContainerStreamProducer(broadcast: broadcast, format: .fmp4) -// After -let importer = try Media.ContainerStreamProducer(broadcast: broadcast, initData: Media.ContainerStreamInit(format: .fmp4)) -``` - ### Timestamps are optional Raw `writeFrame(_:timestampUs:)` takes a `UInt64?` defaulting to `nil`, and a frame read from an untimed track has a `nil` `timestampUs`. A write to a timed track without a timestamp throws `TimestampMismatch` instead of going out at 0, and whole-frame media writes require one. `Media.CatalogProducer.now()` samples the broadcast clock for local-event stamps. - -```swift -// Before -try track.writeFrame(payload) -let ts: UInt64 = frame.timestampUs -// After -try track.writeFrame(payload, timestampUs: nowUs) -let ts: UInt64? = frame.timestampUs -``` +Pass `timestampUs:` on every write to a timed track, and handle a `nil` `timestampUs` on read. ### Subscriber staleness is `maxDelayUs` `Subscription`, `AudioDecoderOutput`, and `VideoDecoderOutput` take `maxDelayUs` instead of `maxAgeUs`. `TrackInfo.maxAgeUs` keeps its name: it is the publisher's retention. -```swift -// Before -let subscription = Subscription(maxAgeUs: 500_000) -// After -let subscription = Subscription(maxDelayUs: 500_000) -``` - ### Error payloads are named `reason` Cases that carried a positional string (`Transport`, `Internal`, `Connect`, ...) take a `reason:` label. -```swift -// Before -throw MoqError.Transport("reset") -// After -throw MoqError.Transport(reason: "reset") -``` - ### `Transport` gains `.webTransport` A WebTransport session reports `.webTransport`; `.quic` now means raw QUIC only, so an exhaustive `switch` needs one more case. - -```swift -// Before -let quic = request.transport == .quic -// After -let quic = request.transport == .quic || request.transport == .webTransport -``` +Check for `.webTransport` too where you matched `.quic`. ### `stalled` is `enabled` (#4915) `Media.Video.stalled` is `enabled`, defaulting to true with the meaning inverted, and `Media.Audio` gains `enabled`. - -```swift -// Before -if !video.stalled { select(video) } -// After -if video.enabled { select(video) } -``` +Read `enabled` where you read `!stalled`. ### Routes have one cost (#4661) -`Route.cold` is removed. - -```swift -// Before -let route = Route(cost: 10, cold: 50) -// After -let route = Route(cost: 10) -``` +`Route.cold` is removed; pass only `cost:`. ### Decoded video frames are objects `VideoDecodedFrame` has `timestampUs()`, `width()`, `height()`, `pixels(format:)`, and `surface()` instead of stored fields. `VideoDecoderOutput.format` is gone; set `surface: true` to keep the decoder's native surface. - -```swift -// Before -let rgba = frame.data -// After -let rgba = try frame.pixels(format: .rgba) -``` +Call `try frame.pixels(format: .rgba)` instead of reading `frame.data`. ### `BroadcastProducer.finish()` is `close()` (#4152) Close the broadcast instead; tracks already subscribed carry on to their own end. -```swift -// Before -try broadcast.finish() -// After -try broadcast.close() -``` - ## 0.4 -> 0.5 ### Publish split by kind @@ -262,13 +153,6 @@ try broadcast.close() `publishMedia` is `publishAudio`, `publishVideo`, or `publishContainer`, each taking its own init. The raw encoder paths that used to be `publishAudio` / `publishVideo` are `encodeAudio` / `encodeVideo`. -```swift -// Before -let media = try broadcast.publishMedia(format: "opus", initData: opusHead) -// After -let media = try broadcast.publishAudio(format: .opus, initData: opusHead) -``` - ### Renames | Before | After | @@ -285,12 +169,4 @@ let media = try broadcast.publishAudio(format: .opus, initData: opusHead) `MoqError.Protocol` carries `details` (scope, wire code, kind) instead of a flattened message. Setters throw (`.Busy` during connect or listen) instead of dropping the value. Track and group `finish()` keep the handle open, so a later `abort()` can still run. - -```swift -// Before -catch MoqError.Protocol(let message) { print(message) } -client.setPublish(origin) -// After -catch MoqError.Protocol(let details) { print(details.kind, details.code) } -try client.setPublish(origin) -``` +Match `MoqError.Protocol(let details)` and read `details.kind` and `details.code`, and call setters with `try`. From 92911dd0a5a69fa1e7a37bff15f2be51b4ee5276 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 14:10:18 -0700 Subject: [PATCH 35/40] docs(migrate): migration paths in Python and moq-net guides Co-Authored-By: Claude Opus 5.5 --- py/moq-rs/MIGRATE.md | 160 +++--------------------------------------- rs/moq-net/MIGRATE.md | 150 ++++----------------------------------- 2 files changed, 22 insertions(+), 288 deletions(-) diff --git a/py/moq-rs/MIGRATE.md b/py/moq-rs/MIGRATE.md index 6690cf01ce..84470fcfbe 100644 --- a/py/moq-rs/MIGRATE.md +++ b/py/moq-rs/MIGRATE.md @@ -10,9 +10,6 @@ The connection stats no longer carry a receive estimate. Hold the consumer `Session.recv_bandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and drop it to stop. ```python -# Before -rate = session.stats().estimated_recv_rate_bps -# After bandwidth = session.recv_bandwidth() rate = bandwidth.peek() if bandwidth else None ``` @@ -31,26 +28,11 @@ A publisher whose timeline restarts publishes a new broadcast instead. `Bandwidth.reserve` takes the track's `demand()` instead of the producer. -```python -# Before -reservation = bandwidth.reserve(track, max_bps) -# After -reservation = bandwidth.reserve(track.demand(), max_bps) -``` - ### Media subscribe and fetch take a catalog rendition (#5834) `media.TrackConsumer.subscribe` and `media.GroupConsumer.fetch` take the catalog ID and its `Video` or `Audio` config instead of a track name and container, and follow the rendition's broadcast and track; `fetch` takes them before `sequence`. The decode consumers' `name` argument is `id`. -```python -# Before -track = audio.track if audio.track is not None else id -frames = await moq.media.TrackConsumer.subscribe(broadcast, track, audio.container) -# After -frames = await moq.media.TrackConsumer.subscribe(broadcast, id, audio) -``` - ### Native codecs have `audio` and `video` modules (#5694) `BroadcastProducer.encode_audio` / `encode_video` are `moq.audio.Producer` / `moq.video.Producer`, built from a `moq.media.CatalogProducer`. @@ -60,14 +42,6 @@ Producers expose subscriber state only through `demand()`, and a video `gop` is `moq.audio.Encoder` / `moq.video.Encoder` encode without publishing. ```python -# Before -audio = broadcast.encode_audio( - "audio", - moq.AudioEncoderInput(format=moq.AudioSampleFormat.F32, sample_rate=48000, channels=1), - moq.AudioEncoderOutput(codec=moq.AudioCodec.opus(), frame_duration_us=20_000), -) -audio.write(moq.AudioFrame(timestamp_us=0, data=pcm)) -# After audio = moq.audio.Producer( moq.media.CatalogProducer(broadcast), moq.audio.EncoderInput(format=moq.audio.SampleFormat.F32, sample_rate=48000, channels=1), @@ -85,53 +59,24 @@ A panic in a background task raises `moq.Error.Task` with the panic message as ` `AnnounceConsumer` yields an `AnnounceEvent`: `AnnounceEventStart`, `AnnounceEventUpdate`, `AnnounceEventEnd`, or `AnnounceEventRestart`, each carrying an `Announce` as `announce`. `AnnounceUpdate` is gone. A restart means another publisher instance now serves the prefix: request the path again. - -```python -# Before -async for update in client.announced(): - if update.active: - subscribe(update.prefix) -# After -async for event in client.announced(): - if isinstance(event, (moq.AnnounceEventStart, moq.AnnounceEventRestart)): - subscribe(event.announce.prefix) -``` +Where you checked `update.active`, act on `AnnounceEventStart` and `AnnounceEventRestart` and read `event.announce.prefix`. ### "Epoch" means only the publisher epoch (#5146) `Session.epoch()` is `connects()`, and `AudioProducer.reset_epoch()` is `reanchor()`. -```python -# Before -n = session.epoch() -audio.reset_epoch() -# After -n = session.connects() -audio.reanchor() -``` - ### The consume origin is `consume` `Client(..., subscribe=origin)` is `Client(..., consume=origin)`. A server request takes its origins in `accept(publish=..., consume=...)`, where `None` inherits the server's origin. A value the native side cannot use raises `moq.Error.Config` at construction, including a bad bind address. -```python -# Before -client = moq.Client(url, publish=origin, subscribe=origin) -# After -client = moq.Client(url, publish=origin, consume=origin) -``` - ### JSON data tracks wrap a track `BroadcastProducer.publish_json_snapshot` / `publish_json_stream` and the matching consumers are gone. Create the track, then wrap it with `moq.json` (or `moq.flate` for opaque bytes). ```python -# Before -state = broadcast.publish_json_snapshot("state") -# After track = broadcast.publish_track("state") state = moq.json.SnapshotProducer(broadcast, track) ``` @@ -143,142 +88,66 @@ Use `moq.media.TrackProducer.audio` / `video`, `ContainerProducer`, `CatalogProd Producers drop `name`, `used`, and `unused`; read them through `demand()`. ```python -# Before -audio = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head) -# After audio = moq.media.TrackProducer.audio(broadcast, moq.media.AudioInit(format=moq.media.AudioFormat.OPUS, data=opus_head)) ``` ### Container byte-stream importers take an init record -`moq.media.ContainerStreamProducer` takes a `ContainerStreamInit` holding the format instead of a bare format. +Wrap the format passed to `moq.media.ContainerStreamProducer` in a `moq.media.ContainerStreamInit`. Its optional `timebase`, also on `ContainerInit`, shares one source's offset across container imports. -```python -# Before -producer = moq.media.ContainerStreamProducer(broadcast, moq.media.ContainerFormat.FMP4) -# After -producer = moq.media.ContainerStreamProducer(broadcast, moq.media.ContainerStreamInit(moq.media.ContainerFormat.FMP4)) -``` - ### Durations are `timedelta` `Frame`, `Datagram`, `Subscription`, `TrackInfo`, and `ConnectionStats` carry `timedelta` instead of microsecond integers: `timestamp_us` is `timestamp`, and `rtt_us` is `rtt`. A string passed where a list of strings belongs (`tls_roots="ca.pem"`) raises `TypeError` instead of splitting into characters. -```python -# Before -track.write_frame(payload, timestamp_us=1_000) -# After -track.write_frame(payload, timestamp=timedelta(milliseconds=1)) -``` - ### Timestamps are optional A frame read from an untimed track has `timestamp` set to `None`. Raw writes no longer default to zero, so a write to a timed track without a timestamp raises `TimestampMismatch`; whole-frame media writes require one. +Pass `timestamp=` on every write to a timed track. `moq.media.CatalogProducer.now()` samples the broadcast clock for local-event stamps. -```python -# Before -track.write_frame(payload) -# After -track.write_frame(payload, timestamp=pts) -``` - ### Subscriber staleness is `max_delay` `Subscription.max_age_us` is `max_delay`, a `timedelta`, and the decoder outputs take `max_delay`, also a `timedelta`. `TrackInfo` keeps its `max_age`: it is the publisher's retention. -```python -# Before -sub = moq.Subscription(max_age_us=500_000) -# After -sub = moq.Subscription(max_delay=timedelta(milliseconds=500)) -``` - ### Error payloads are named `reason` -Errors that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`. - -```python -# Before -reason = err[0] -# After -reason = err.reason -``` +Errors that carried a positional string (`Transport`, `Internal`, `Connect`, ...) name it `reason`: read `err.reason` instead of `err[0]`. ### `Transport` gains `WEB_TRANSPORT` A WebTransport session reports `moq.Transport.WEB_TRANSPORT`; `QUIC` now means raw QUIC only, so a server that admits only `QUIC` refuses every browser. - -```python -# Before -quic = request.transport == moq.Transport.QUIC -# After -quic = request.transport in (moq.Transport.QUIC, moq.Transport.WEB_TRANSPORT) -``` +Check for both where you meant any QUIC session. ### `stalled` is `enabled` (#4915) `moq.media.Video.stalled` is `enabled`, defaulting to `True` with the meaning inverted, and `moq.media.Audio` gains `enabled`. -```python -# Before -live = not video.stalled -# After -live = video.enabled -``` - ### Routes have one cost (#4661) -`Route.cold` is removed. - -```python -# Before -route = moq.Route(cost=10, cold=20) -# After -route = moq.Route(cost=10) -``` +`Route.cold` is removed: drop the argument and keep `cost`. ### Decoded video frames are objects A `VideoDecodedFrame`'s fields are methods: `timestamp_us()`, `width()`, `height()`, and `pixels(format)` replacing `data` and `format`. `VideoDecoderOutput.format` is gone; set `surface=True` to keep the decoder's native surface. - -```python -# Before -rgba = frame.data -# After -rgba = frame.pixels(moq.VideoPixelFormat.RGBA) -``` +Read `frame.pixels(moq.VideoPixelFormat.RGBA)` instead of `frame.data`. ### `BroadcastProducer.finish()` is removed (#4152) -Close the broadcast instead. - -```python -# Before -broadcast.finish() -# After -broadcast.close() -``` +Call `broadcast.close()` instead. ## 0.4 -> 0.5 ### Publish split by kind `publish_media*` is `publish_audio`, `publish_video`, or `publish_container`, each taking its own init. +For example, `publish_media("opus", opus_head)` is `publish_audio(moq.AudioFormat.OPUS, opus_head)`. The raw encoder paths that used to be `publish_audio` / `publish_video` are `encode_audio` / `encode_video`. -```python -# Before -media = broadcast.publish_media("opus", opus_head) -# After -media = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head) -``` - ### Renames | Before | After | @@ -292,15 +161,6 @@ media = broadcast.publish_audio(moq.AudioFormat.OPUS, opus_head) ### Errors are structured -`Error.Protocol` carries a `ProtocolError` (scope, wire code, kind) instead of a flattened message. +`Error.Protocol` carries a `ProtocolError` (scope, wire code, kind) instead of a flattened message: read `e.details.kind` instead of parsing `str(e)`. Setters raise (`Busy` during connect or listen) instead of dropping the value. Track and group `finish()` keep the handle open, so a later `abort()` can still run. - -```python -# Before -except moq.Error.Protocol as e: - message = str(e) -# After -except moq.Error.Protocol as e: - kind = e.details.kind -``` diff --git a/rs/moq-net/MIGRATE.md b/rs/moq-net/MIGRATE.md index 52d50adee4..95cd3e654e 100644 --- a/rs/moq-net/MIGRATE.md +++ b/rs/moq-net/MIGRATE.md @@ -10,9 +10,6 @@ The connection stats no longer carry a receive estimate. Hold the consumer `Session::recv_bandwidth` returns to request PROBE, read it with `peek()` or wait with `changed()`, and drop it to stop. ```rust -// Before -let rate = session.stats().estimated_recv_rate; -// After let bandwidth = session.recv_bandwidth(); let rate = bandwidth.as_ref().and_then(|b| b.peek()); ``` @@ -37,24 +34,15 @@ producer.finish().wait().await; ### `auth::Token` is `auth::Auth` (#5695) +Rename `auth::Token` to `auth::Auth`. `auth::Handle::add` returns an `auth::Auth`, which is `Clone` and compares by identity, so a route can name it with `origin::Route::with_auth`. -```rust -// Before -let token: auth::Token = session.auth().add(bytes).await?; -// After -let auth: auth::Auth = session.auth().add(bytes).await?; -``` - ### Custom transports send datagrams without polling (#5770) `transport::poll::Session::send_datagram` replaces `poll_send_datagram`. It takes the payload as `Bytes`, the send group to schedule it in (`None` for ungrouped), and an order, and never blocks. ```rust -// Before -fn poll_send_datagram(&mut self, cx: &mut Context<'_>, payload: &[u8]) -> Poll> { ... } -// After fn send_datagram( &mut self, payload: Bytes, @@ -67,15 +55,7 @@ fn send_datagram( Holding a `track::Consumer` sends nothing and no longer keeps the producer used. Only a query, subscription, or fetch made from it counts, until it is dropped. -Hold one of those for as long as you want the track. - -```rust -// Before -let track = broadcast.track("video")?; // kept the producer used -// After -let track = broadcast.track("video")?; -let _sub = track.subscribe(None); // or `track.query()` to only learn its info -``` +Hold one of those, such as `track.subscribe(None)` or `track.query()` to only learn its info, for as long as you want the track. ### Dynamic broadcasts see requests when they are made (#5820) @@ -88,9 +68,6 @@ A `transport::poll::SendStream` declares `type Group` (`Ungrouped` when the tran A `transport::poll::Session` implements `send_group`, returning `None` without groups. ```rust -// Before -fn set_priority(&mut self, order: i32) { ... } -// After type Group = Ungrouped; fn set_priority(&mut self, priority: i64) { ... } fn set_group(&mut self, group: &Ungrouped) { match *group {} } @@ -110,26 +87,12 @@ let sub = track::Subscription::default().with_max_delay(Duration::from_secs(1)); ### `cache::Pool::gc` takes a waiter -Call it again when the waiter wakes, since a parked read can arm an earlier deadline than the one returned. - -```rust -// Before -let next = pool.gc(now); -// After -let next = pool.gc(now, &waiter); -``` +Call `pool.gc(now, &waiter)`, and call it again when the waiter wakes, since a parked read can arm an earlier deadline than the one returned. ### A replacement track owns its start sequence (#5068) `append_group` and `append_datagram` on a track that replaces an earlier producer of the same name return `Error::Duplicate` instead of continuing past the earlier producer's sequence. -Pick the start sequence yourself with `create_group`, or publish a new broadcast. - -```rust -// Before -let group = track.append_group()?; // continued past the earlier producer -// After -let group = track.create_group(group::Info { sequence })?; -``` +Pick the start sequence yourself with `create_group(group::Info { sequence })`, or publish a new broadcast. ### Announcements are `Start`, `Update`, `End`, and `Restart` events @@ -139,14 +102,6 @@ let group = track.create_group(group::Info { sequence })?; A newer epoch no longer ends open subscriptions with `Unroutable`; they stay on the old instance until dropped or its route goes. ```rust -// Before -while let Some(update) = announced.next().await { - if !update.kind.is_active() { - continue; - } - let broadcast = origin.request_broadcast(&update.prefix).await?; -} -// After while let Some(event) = announced.next().await { match event { announce::Event::Start(a) | announce::Event::Restart(a) => { @@ -161,42 +116,20 @@ while let Some(event) = announced.next().await { ### Routes carry the publisher epoch (#4942) An epoch is a property of the route, not the path. -`Path::join_epoch` and `Path::split_epoch` are gone; set it with `Route::with_epoch` and read `Route::epoch`. +`Path::join_epoch` and `Path::split_epoch` are gone; publish the plain path with `route.with_epoch(epoch)` and read `Route::epoch`. Nothing mints an epoch by default, and a route without one does not resume across routes. -```rust -// Before -let path = path.join_epoch(epoch); -origin.publish(path, route); -// After -origin.publish(path, route.with_epoch(epoch)); -``` - ### `request_broadcast` takes an epoch Pass `None` to accept any publisher instance, or an `Epoch` to refuse every other one. A dynamic route updated to another epoch refuses the requests its handler still holds with `Unroutable`. -```rust -// Before -let broadcast = origin.request_broadcast(path).await?; -// After -let broadcast = origin.request_broadcast(path, None).await?; -``` - ### Demand is read through `demand()` -`track::Producer`'s `is_used`, `used`, `unused`, and `poll_unused`, and `group::Producer`'s `used` and `unused`, moved to `demand()`. +`track::Producer`'s `is_used`, `used`, `unused`, and `poll_unused`, and `group::Producer`'s `used` and `unused`, moved to `demand()`: `track.unused().await` is `track.demand().unused().await?`. `track::Request`, `track::Dynamic`, and `group::Request` poll it through `demand().poll_unused`, which returns `Poll>`: treat `Err` (the request closed) as unused too. A `group::Request` is withdrawn once its last `fetch_group` caller leaves, so a handler that sees it unused just drops it; a later fetch queues a fresh request, and serving the abandoned one returns `Error::Duplicate`. -```rust -// Before -track.unused().await; -// After -track.demand().unused().await?; -``` - ### moq-net owns its transport traits `moq_net::web_transport_trait` is gone. @@ -215,65 +148,32 @@ impl moq_net::transport::poll::Session for MySession { ### Data publishes keep optional timestamps `Timed

` drops its clock type parameter and its `at` is an `Option`. +Build it as `Timed { value, at: Some(at) }`. Publish `at: None` only on an untimed track; pass a timestamp on a timed one. -```rust -// Before -Timed::::new(value, at) -// After -Timed { value, at: Some(at) } -``` - ### A track is timed or untimed `track::Info.timescale` is `Option`, `None` for an untimed track, and an undeclared timescale no longer defaults to milliseconds. `Frame.timestamp`, `frame::Info.timestamp`, and `Datagram.timestamp` are `Option`, and `group::{Producer, Consumer}::timescale()` return `Option`. A write whose timedness does not match its track fails with `TimestampMismatch`. Tracks from moq-lite before 05 and moq-transport without `TIMESCALE` arrive untimed instead of stamped with their arrival time. - -```rust -// Before -let track = broadcast.create_track("video", None)?; // milliseconds -// After -let info = track::Info::default().with_timescale(Timescale::MILLI); -let track = broadcast.create_track("video", info)?; -``` +To keep millisecond timestamps, pass `track::Info::default().with_timescale(Timescale::MILLI)` to `create_track` instead of `None`. ### Subscriber staleness is `max_delay` How far a group may fall behind the live edge before a subscriber skips it is no longer named `max_age`, which stays the publisher's retention on `track::Info`. - -```rust -// Before -let sub = track::Subscription::default().with_max_age(delay); -// After -let sub = track::Subscription::default().with_max_delay(delay); -``` +Call `track::Subscription::with_max_delay` instead of `with_max_age`. ### Publisher retention is optional (#4242) `track::Info::max_age` is `Option`, `None` meaning no publisher limit. -`track::DEFAULT_MAX_AGE` and `origin::Config::default_max_age` are gone. - -```rust -// Before -let info = track::Info::default().with_max_age(track::DEFAULT_MAX_AGE); -// After -let info = track::Info::default().with_max_age(Duration::from_secs(30)); -``` +`track::DEFAULT_MAX_AGE` and `origin::Config::default_max_age` are gone: pass your own `Duration` to `with_max_age`. ### Route cost is one value (#4661) `Cost` is opaque: construct it with `Cost::new` and read it with `value()`. The `warm` and `cold` fields are gone. -```rust -// Before -let cost = Cost { warm: 10, cold: 10 }; -// After -let cost = Cost::new(10); -``` - ### Datagrams are unfetchable and bounded by the group range (#4982) A FETCH that reaches a datagram fails with `Error::NotFetchable` (received as `Error::Stream(StreamError::NotFetchable)`). @@ -299,45 +199,19 @@ A publisher that restarts its group numbering must publish under a new broadcast Close a broadcast with `close()`; its end carries no cause. `broadcast::Consumer::is_finished` is gone, and `closed()` on `Consumer` and `Dynamic` returns `()`. -```rust -// Before -producer.finish(); -let err = consumer.closed().await; -// After -producer.close(); -consumer.closed().await; -``` - ### No `VarInt` Varints are plain `u64`s. - -```rust -// Before -let v = VarInt::decode_quic(buf)?.into_inner(); -VarInt::try_from(v)?.encode_quic(buf); -// After -let v = moq_net::varint::decode_quic(buf)?; -moq_net::varint::encode_quic(v, buf)?; // fails past varint::MAX_QUIC -``` +Use `moq_net::varint::decode_quic(buf)` and `moq_net::varint::encode_quic(v, buf)`, which fails past `varint::MAX_QUIC`. ## 0.2 -> 0.3 ### Origins (#3400, #3804) -`create_broadcast(path, route)` is `origin.publish(path, route)`, or `create_broadcast(path)` then `broadcast.announce(route)`. +`create_broadcast(path, route)` is `origin.publish(path, route)?`, or `create_broadcast(path)` then `broadcast.announce(route)`. `with_root` plus `scope(prefixes)` is one `scope(root, &patterns)` returning `Result`. `origin::Info` is `origin::Config`. -```rust -// Before -let broadcast = origin.create_broadcast(path, route); -let scoped = origin.with_root(root).scope(&prefixes); -// After -let broadcast = origin.publish(path, route)?; -let scoped = origin.scope(root, &patterns)?; -``` - ### Announcements are prefix routes (#3225, #3770) `announce::Update` is `{ prefix, route, kind, captures }`. From 3a48f7c7f5960fdf2afc453ebc89a4d0d65770cd Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 14:10:15 -0700 Subject: [PATCH 36/40] docs(migrate): migration paths in moq-mux and QUIC runtime guides Co-Authored-By: Claude Opus 5.5 --- rs/moq-mux/MIGRATE.md | 148 +++------------------------------------- rs/moq-quic/MIGRATE.md | 17 +---- rs/moq-sock/MIGRATE.md | 5 -- rs/moq-tokio/MIGRATE.md | 141 +++----------------------------------- rs/moq-uring/MIGRATE.md | 38 +---------- 5 files changed, 20 insertions(+), 329 deletions(-) diff --git a/rs/moq-mux/MIGRATE.md b/rs/moq-mux/MIGRATE.md index 8a9241375f..cf7adb6888 100644 --- a/rs/moq-mux/MIGRATE.md +++ b/rs/moq-mux/MIGRATE.md @@ -7,58 +7,24 @@ How to update your code for each breaking change, newest first. ### `fmp4::Wire::new` refuses a track that isn't audio or video (#5815) `container::fmp4::Wire::new` returns `Result`, as `Wire::from_init` already did, and refuses any other handler instead of treating it as data. - -```rust -// Before -let wire = Wire::new(trak); -// After -let wire = Wire::new(trak)?; -``` +Handle the error from `Wire::new`. ### `ts::Programs::finish` returns each program's `Finish` (#5795) `Programs::finish` returns `anyhow::Result>` instead of `anyhow::Result<()>`. Each handle keeps its program's broadcast servable until its media is stale, and dropping it closes that program. -Await `Finish::wait` to linger, or drop the handles to close every program now. - -```rust -// Before -programs.finish()?; - -// After: serve the finished programs until their media is stale -let finished = programs.finish()?; -futures::future::join_all(finished.into_iter().map(moq_net::broadcast::Finish::wait)).await; - -// After: close every program now -drop(programs.finish()?); -``` +Await `Finish::wait` on each handle to linger, or drop the handles to close every program now. ### Timeline durations are hints (#5715) `timeline::Config::duration_max`, `with_duration_max`, and `DEFAULT_DURATION_MAX` are gone; set `duration_hint` with `with_duration_hint`, the expected record duration such as the GOP, and the split ceiling follows from it. `DEFAULT_DURATION_MIN` is 4 s instead of 2 s, so set `with_duration_min` to keep the shorter records. -```rust -// Before -let config = timeline::Config::default().with_duration_max(Duration::from_secs(10)); -// After -let config = timeline::Config::default() - .with_duration_hint(gop) - .with_duration_min(Duration::from_secs(2)); -``` - ### The catalog consumer is opaque (#5630) Construct `catalog::Consumer` with `Consumer::new(broadcast, format).await`, or convert a `catalog::hang::Consumer` with `.into()`, then read it through `catalog::Stream`. Its `Hang`, `Msf`, and `Closed` variants and `catalog::msf::Consumer::track` are private. -```rust -// Before -let catalog = catalog::Consumer::Hang(hang_consumer); -// After -let catalog = catalog::Consumer::new(&broadcast, format).await?; -``` - ### Timelines record untimed tracks (#5720) `timeline::Segmenter::frame` takes `pts: impl Into>`, so existing callers compile. @@ -66,30 +32,15 @@ A segmenter fed the wrong timedness for its track fails with `Error::TimelineTim ### An unset host clock publishes no wall mapping (#5429) -`Clock::wall()` returns `Option` instead of panicking. +`Clock::wall()` returns `Option` instead of panicking, so handle `None`. `Clock::wall_clock` and `Timebase::place` refuse a clock without one. -```rust -// Before -let wall = clock.wall(); -// After -let Some(wall) = clock.wall() else { return Ok(()) }; -``` - ### TS export lingers within one publisher instance (#5147) `ts::Export::resume` is `ts::Export::follow`, `Source::returned` is gone, and `ts::Follower` follows a path's announcements the way `moq export ts` does. `ts::Export::new(source, catalog)` takes a catalog stream like the other exporters: subscribe with `source.catalog::(format).await?` to keep MPEG-TS metadata, or `()` for media only, and select renditions on the stream before passing it in. `follow(broadcast, catalog)` takes the replacement stream too, both methods are synchronous, and `with_ts` and `with_catalog_format` are removed. -```rust -// Before -let export = ts::Export::new(source).await?.with_catalog_format(format); -// After -let catalog = source.catalog::(format).await?; -let export = ts::Export::new(source, catalog); -``` - ### `Source` is built from the resolved broadcast (#5252) `Source::new(origin, path)` is `Source::new(origin, broadcast)`: resolve the broadcast first, and catalog references resolve from its path. @@ -97,9 +48,6 @@ let export = ts::Export::new(source, catalog); Every later request for a path, siblings included, stays on the broadcast first held there and fails once it ends, so build a new `Source` to follow a new instance. ```rust -// Before -let source = Source::new(origin, path); -// After let broadcast = origin.request_broadcast(path, None).await?; let source = Source::new(origin, broadcast); ``` @@ -110,13 +58,6 @@ let source = Source::new(origin, broadcast); `ts::Export` defaults to 1 s, and the send-ahead comes out of the same budget, so the output trails a TS source by the delay instead of twice it. `ts::stats::Export` gains a dropped-frame count, measured drift, and an out-of-tolerance count, and is no longer `Eq`. -```rust -// Before -let export = ts::Export::new(source, catalog).with_max_age(delay); -// After -let export = ts::Export::new(source, catalog).with_delay(delay); -``` - ### Subscriber staleness is `max_delay` | Before | After | @@ -126,18 +67,11 @@ let export = ts::Export::new(source, catalog).with_delay(delay); ### Importers keep the source's timestamps -`clock::Anchor`, `clock::Lane`, `SourceMap`, `Clock::source`, and the importers' `live()` are gone. +`clock::Anchor`, `clock::Lane`, `SourceMap`, `Clock::source`, and the importers' `live()` are gone; delete the `live()` calls. Importers publish the source's own timestamps and anchor the catalog clock on their first frame. An importer whose first frame arrives once the clock is in use (taken with `catalog.clock()`, published, or pinned with `Config::with_clock`) shifts its timestamps onto it, so its first frame lands at now. Importers sharing a timestamp base reserve through one `catalog.timebase()`. -```rust -// Before -let import = fmp4::Import::new(broadcast, reserved).live(); -// After -let import = fmp4::Import::new(broadcast, reserved); -``` - ### Importers refuse a timestamp rewind A backward timestamp is an encoder restart and needs a new broadcast. @@ -155,14 +89,6 @@ A `moov` or `Tracks` decoded after `finish()` is refused with `fmp4::Error::Moov The H.264, H.265, AV1, VP8, and VP9 `Import`s no longer take liveness hints; a stalled rendition is marked by the catalog's `enabled` flag instead. Delete the calls. -```rust -// Before -import.tick()?; -import.observe_lag(lag)?; -import.idle()?; -// After: delete the calls -``` - ### fMP4 export fixes its track set at the init segment `fmp4::Error` drops `MissingVideoTrack`, `MissingAudioTrack`, and `NoCatalogSnapshot`, and adds `TrackAdded`, `TrackChanged`, `TrackRewound`, and `TrackUndescribed`. @@ -177,12 +103,9 @@ A keyframe whose parameter sets never appeared ends the export instead of waitin ### CMAF timescales must match the init CMAF `Wire::write` refuses a track whose timescale is not the init's `mdhd` timescale with `fmp4::Error::TimescaleMismatch`. -A track you create yourself declares it with `track::Info::with_timescale`. +A track you create yourself declares it with `track::Info::with_timescale`, passed to `create_track`. ```rust -// Before -let track = broadcast.create_track(name, None)?; -// After let info = track::Info::default().with_timescale(Timescale::new(mdhd_timescale)?); let track = broadcast.create_track(name, info)?; ``` @@ -193,9 +116,6 @@ JSON and flate producers publish `at: None` untimed, so their track must be unti Convert a capture `Instant` through the catalog clock. ```rust -// Before -producer.update(&value)?; -// After producer.update(Timed::from(&value).at(catalog.clock().capture(instant)?))?; ``` @@ -203,64 +123,26 @@ producer.update(Timed::from(&value).at(catalog.clock().capture(instant)?))?; `Error::Binary` is `Error::Flate`, carrying a `moq_flate::Error`. -```rust -// Before -Err(moq_mux::Error::Binary(err)) => {} // err: moq_binary::Error -// After -Err(moq_mux::Error::Flate(err)) => {} // err: moq_flate::Error -``` - ### AAC refuses channel counts it cannot name (#4973) `aac::Config::encode` returns a `Result` and fails with `UnsupportedChannelCount` for a count no `channelConfiguration` names, instead of writing stereo. -`From for hang::catalog::AudioConfig` is `TryFrom`. - -```rust -// Before -let audio: AudioConfig = aac.into(); -// After -let audio: AudioConfig = aac.try_into()?; -``` +`From for hang::catalog::AudioConfig` is `TryFrom`, so convert with `try_into()?`. ### The Opus mapping family lives only on `mapping` (#4851) -`codec::opus::Config::mapping_family` is gone. +`codec::opus::Config::mapping_family` is gone; drop the field. Family 0 is `mapping: None`; any other family is `mapping.family()`. -```rust -// Before -let config = opus::Config { mapping_family: 1, mapping: Some(mapping), ..config }; -// After -let config = opus::Config { mapping: Some(mapping), ..config }; -``` - ### TS stats live in `ts::stats` (#4909) `ts::Stats` is `ts::stats::Snapshot`, and `ts::StreamStats` is `ts::stats::Stream`, whose `track` is an owned `String`. `ts::Export::stats` returns `ts::stats::Export`; feed it to `stats::Log` with `.into()`. `ts::MultipleProgramsError` is `#[non_exhaustive]`: recover it by downcast and read `programs`. -```rust -// Before -let stats: ts::Stats = import.stats(); -log.sample(export.stats()); -// After -let stats: ts::stats::Snapshot = import.stats(); -log.sample(export.stats().into()); -``` - ### FLV export takes a catalog stream (#4987) `flv::Export::new` is synchronous and takes `(source, catalog)`, like `fmp4::Export` and `mkv::Export`. -`with_catalog_format` and `with_select` are gone: open the catalog yourself and narrow it with `catalog::Stream::select`. - -```rust -// Before -let export = flv::Export::new(source).await?.with_catalog_format(format).with_select(select); -// After -let catalog = source.catalog(format).await?.select(select); -let export = flv::Export::new(source, catalog); -``` +`with_catalog_format` and `with_select` are gone: open the catalog yourself with `source.catalog(format).await?` and narrow it with `catalog::Stream::select`. ## 0.9 -> 0.10 @@ -269,21 +151,7 @@ let export = flv::Export::new(source, catalog); `lock()` is `modify()?`, and a guard that fails to publish on drop aborts the track. `timeline::Config::wall` is the broadcast `Clock`. -```rust -// Before -let mut catalog = producer.lock(); -// After -let mut catalog = producer.modify()?; -``` - ### Imports are typed `import::Init` splits into `AudioInit`, `VideoInit`, and `ContainerInit`, with typed formats instead of strings. `Track::new` is `Track::audio` or `Track::video`. - -```rust -// Before -let track = import::Track::new(request, reserved, import::Init { format: "opus".into(), .. })?; -// After -let track = import::Track::audio(request, reserved, import::AudioInit { format: AudioFormat::Opus, .. })?; -``` diff --git a/rs/moq-quic/MIGRATE.md b/rs/moq-quic/MIGRATE.md index 1eb0821bf1..7072146ca4 100644 --- a/rs/moq-quic/MIGRATE.md +++ b/rs/moq-quic/MIGRATE.md @@ -6,25 +6,10 @@ How to update your code for each breaking change, newest first. ### `Datagrams::send` takes a send group and order (#5770) -`Datagrams::send(data, group, order)` replaces `send(data, drop)`. +`Datagrams::send(data, group, order)` replaces `send(data, drop)`; pass `None` for an ungrouped datagram. A full buffer drops the oldest datagram of the lowest-priority group, so `SendDatagramError::Blocked` and `Event::DatagramsUnblocked` are gone. -```rust -// Before -conn.datagrams().send(data, true)?; -// After -conn.datagrams().send(data, None, 0)?; -``` - ### `congestion::NewReno` removed `congestion::NewReno` and `congestion::NewRenoConfig` are gone. Use `Bbr3Config` (the default) or `CubicConfig` instead. - -```rust -// Before -config.congestion_controller_factory(Arc::new(congestion::NewRenoConfig::default())); - -// After -config.congestion_controller_factory(Arc::new(congestion::CubicConfig::default())); -``` diff --git a/rs/moq-sock/MIGRATE.md b/rs/moq-sock/MIGRATE.md index 46d88f690c..ed64e78be5 100644 --- a/rs/moq-sock/MIGRATE.md +++ b/rs/moq-sock/MIGRATE.md @@ -11,11 +11,6 @@ How to update your code for each breaking change, newest first. Connection IDs are steered by `moq_quic::RandomConnectionIdGenerator::set_shard` instead of `shard::cid`. ```rust -// Before -let mut group = shard::Group::acquire(addr, count)?; -let claims = (0..count).map(|_| group.member().unwrap().bind()).collect::>()?; -let mut bound = group.complete(claims)?; -// After let mut bound = shard::bind(addr, count)?; while let Some(socket) = bound.member()? { // serve socket diff --git a/rs/moq-tokio/MIGRATE.md b/rs/moq-tokio/MIGRATE.md index 347cead6ff..52a53ae6a4 100644 --- a/rs/moq-tokio/MIGRATE.md +++ b/rs/moq-tokio/MIGRATE.md @@ -8,67 +8,25 @@ How to update your code for each breaking change, newest first. `server::Request::set_quic_limits` and `Error::QuicLimits` are gone. `Request::quic()` returns the `quic::Connection` under a QUIC or WebTransport request, or `None` on other transports, and its `set_limits` takes `quic::Limits`, now `moq_quic::Limits`, where each `None` field keeps that limit. +Call `set_limits` on `request.quic()` instead, starting from `quic::Limits::default()` and setting only the fields you change. `quic::Resolved`'s `receive_window`, `stream_receive_window`, and `send_window` are plain `u64`s with the defaults filled in, and `quic.max_streams = 0` is refused. -```rust -// Before -request.set_quic_limits(quic::Limits { max_streams, receive_window, send_window })?; -// After -if let Some(quic) = request.quic() { - let mut limits = quic::Limits::default(); - limits.max_streams = Some(max_streams); - limits.receive_window = Some(receive_window); - limits.send_window = Some(send_window); - quic.set_limits(limits)?; -} -``` - ### Auth handles live in `connection::auth` (#5695) The root `Auth` re-export is gone. `Connection::auth` returns a `connection::auth::Handle`, and its `add` returns a `connection::auth::Auth` instead of a `Token`. -```rust -// Before -let handle: moq_tokio::Auth = connection.auth(); -let token: moq_tokio::connection::auth::Token = handle.add(bytes).await?; -// After -let handle: moq_tokio::connection::auth::Handle = connection.auth(); -let auth: moq_tokio::connection::auth::Auth = handle.add(bytes).await?; -``` - ### Datagrams take a send group and order (#5770) `quic::driver::Connection::send_datagram` and `quic::web::Session::send_datagram` take the send group (`None` for ungrouped) and an order, and never block. -`send_datagram_wait` and `SendDatagram` are gone. - -```rust -// Before -session.send_datagram_wait(data).await?; -// After -session.send_datagram(data, None, 0)?; -``` +`send_datagram_wait` and `SendDatagram` are gone; call `send_datagram(data, None, 0)` instead. ### The `noq` feature is `quic` (#5664) The QUIC stack is MoQ's own `moq-quic` instead of the `moq-noq` crates. -Rename the cargo feature, and the error types with it. +Rename the `noq` cargo feature to `quic`, and the error types with it: `Error::Noq` is `Error::Quic`, carrying a `moq_tokio::quic::Error`. A qlog directory gets one file per endpoint instead of one per connection. -```toml -# Before -moq-tokio = { version = "...", features = ["noq"] } -# After -moq-tokio = { version = "...", features = ["quic"] } -``` - -```rust -// Before -Err(moq_tokio::Error::Noq(err)) => {} // err: moq_tokio::noq::Error -// After -Err(moq_tokio::Error::Quic(err)) => {} // err: moq_tokio::quic::Error -``` - ### Workers are unpinned by default (#5688) `worker::Config::new(count)` no longer pins worker *i* to core *i*. @@ -83,14 +41,6 @@ let workers = moq_tokio::worker::Config::new(8).with_pin(true); `server::Request::ok()` returns `(session, driver)`. Await the driver before classifying the session close, so a driver panic reports `Error::Panic` instead of the cancellation its teardown leaves behind. -```rust -// Before -let session = request.ok().await?; -// After -let (session, driver) = request.ok().await?; -let result = driver.await?; -``` - ### Config durations are optional Wrap durations in `Some(...)` for connect `race`, `resolution_delay`, and `timeout`; listen `timeout`; QUIC `idle_timeout` and `keep_alive`; backoff `initial`, `max`, and `timeout`; GOAWAY `handover`; and WebSocket `delay`. @@ -98,54 +48,22 @@ Wrap durations in `Some(...)` for connect `race`, `resolution_delay`, and `timeo A value set in code now wins over the parser default unless the flag or environment variable was actually given. TOML keys are unchanged. -```rust -// Before -config.backoff.timeout = Duration::from_secs(30); -// After -config.backoff.timeout = Some(Duration::from_secs(30)); -``` - ### Rotating TLS identities are a `tls::Serve` (#5312) -`tls::Listen::server_config_reloading` and `tls::ReloadingServerConfig` are gone; load once with `tls::Listen::load` and read each handshake's config from the returned `tls::Serve`. +`tls::Listen::server_config_reloading` and `tls::ReloadingServerConfig` are gone; load once with `tls::Listen::load` and read each handshake's config from the returned `tls::Serve` with `config(alpns)`. `Error::WorkerTlsGenerate` is gone because generated certificates now work on workers too. A TLS trust-root watcher that fails to register now fails client and server construction. -```rust -// Before -let config = listen.server_config_reloading(alpns)?; -// After -let serve = listen.load()?; -let config = serve.config(alpns)?; -``` - ### "Epoch" means only the publisher epoch (#5146) -The reconnect counter is `connects()`. - -```rust -// Before -let n = connection.epoch(); -// After -let n = connection.connects(); -``` +The reconnect counter is `connects()`; rename `epoch()` calls. ### A QUIC load-balancer nonce needs its server id (#5114) -`listen.lb_nonce` without `lb_id` fails to start. +`listen.lb_nonce` without `lb_id` fails to start, so set `lb_id` next to it. Setting `lb_id` (or `lb_nonce`) together with `load_balancer` also fails; `lb_id` used to win silently. Set one of them. -```toml -# Before -[listen] -lb_nonce = 8 -# After -[listen] -lb_id = "01" -lb_nonce = 8 -``` - ### Settings no listener reads stop startup (#5006) A stream-only listener (TCP or Unix, no QUIC bind) refuses the QUIC-only `preferred_v4` / `preferred_v6`, `lb_id`, pinned `tls.peers`, and a TLS cert, key, or `generate` unless `tcp.tls` serves it. @@ -159,56 +77,24 @@ A stream-only listener with `listen.tls.root` returns `Error::MtlsUnsupported` i ### `Transport` names WebTransport `server::Transport` is `moq_tokio::Transport`, with no re-export. -A WebTransport session reports `Transport::WebTransport` (`"webtransport"` in logs); `Quic` now means raw QUIC only. +A WebTransport session reports `Transport::WebTransport` (`"webtransport"` in logs); `Quic` now means raw QUIC only, so match both where you meant either. `Connection::transport()` reports the live transport. -```rust -// Before -use moq_tokio::server::Transport; -// After -use moq_tokio::Transport; -match transport { - Transport::Quic | Transport::WebTransport => {} - _ => {} -} -``` - ### Custom sessions declare their request binding `transport::Session::new` takes the session's `moq_net::transport::Binding`: `Uri` when the transport handshake carries the request URI, `InBand` when SETUP does. `Client` and `Server` are unchanged. -```rust -// Before -let session = moq_tokio::transport::Session::new(session); -// After -let session = moq_tokio::transport::Session::new(session, moq_net::transport::Binding::Uri); -``` - ### `Redirect::resolve` is removed (#4854) `Redirect` and `Goaway` stay, but the lenient `resolve(uri, current)` is gone. -Follow a GOAWAY through the connection, which validates the target itself. - -```rust -// Before -let next = Redirect::SameHost.resolve(uri, ¤t); -// After -config.goaway.redirect = Redirect::SameHost; // the connection follows and validates the GOAWAY -``` +Follow a GOAWAY through the connection, which validates the target itself: set `config.goaway.redirect`. ### The `android-logcat` feature is removed (#4608) Android builds always log to logcat. Drop the feature from your dependency. -```toml -# Before -moq-tokio = { version = "...", features = ["android-logcat"] } -# After -moq-tokio = { version = "..." } -``` - ## moq-native 0.19 -> moq-tokio 0.19 ### moq-native is moq-tokio (#2896) @@ -217,10 +103,6 @@ moq-native 0.20.0 is a stub that fails to compile, pointing at the rename. Names sit under their modules: `connection::Goaway`, `connection::Monitor`, `transport::Session` (#3745). ```rust -// Before -let client = moq_native::ClientConfig::default().init()?; -client.with_publisher(&origin).with_subscriber(origin); -// After let client = moq_tokio::connect::Config::default().init(quic)?; client.with_origin(origin); ``` @@ -228,10 +110,3 @@ client.with_origin(origin); ### Origins `Origin::random().produce()` is `moq_tokio::origin::spawn()`. - -```rust -// Before -let origin = Origin::random().produce(); -// After -let origin = moq_tokio::origin::spawn(); -``` diff --git a/rs/moq-uring/MIGRATE.md b/rs/moq-uring/MIGRATE.md index 7b80529b38..05350df5ed 100644 --- a/rs/moq-uring/MIGRATE.md +++ b/rs/moq-uring/MIGRATE.md @@ -7,54 +7,22 @@ How to update your code for each breaking change, newest first. ### QUIC limits are `moq_quic::Limits` (#5871) `quic::Limits` is `moq_quic::Limits`, where each `None` field keeps that limit, and an invalid one fails `set_limits` with `Error::Limits` instead of `WindowRange` or `StreamsRange`. - -```rust -// Before -conn.set_limits(quic::Limits { max_streams, receive_window, send_window })?; -// After -let mut limits = quic::Limits::default(); -limits.max_streams = Some(max_streams); -limits.receive_window = Some(receive_window); -limits.send_window = Some(send_window); -conn.set_limits(limits)?; -``` +Start from `quic::Limits::default()` and set only the fields you change. ### The `noq` feature is `quic` (#5664) The QUIC stack is MoQ's own `moq-quic` instead of `moq-noq-proto`. +Rename the `noq` cargo feature to `quic`. A qlog directory gets one file per endpoint and one per dial instead of one per connection. -```toml -# Before -moq-uring = { version = "...", features = ["noq"] } -# After -moq-uring = { version = "...", features = ["quic"] } -``` - ### `CID_LEN` is gone (#5688) `quic::endpoint::CID_LEN` is removed; endpoints still issue 8-byte connection IDs, sharded or not. -```rust -// Before -let len = moq_uring::quic::endpoint::CID_LEN; -// After -let len = 8; // endpoints still issue 8-byte connection IDs -``` - ### The server config takes a rustls config (#5312) `quic::server::Config` drops `identity`, `alpn`, and `client_auth` for one `tls: Arc`, and `Config::new` takes it. -Build it from an identity with `Identity::server`, or share a resolver with other listeners. - -```rust -// Before -let mut config = quic::server::Config::new(identity); -config.alpn = alpns; -config.client_auth = client_auth; -// After -let config = quic::server::Config::new(identity.server(&alpns, client_auth)?); -``` +Build it from an identity with `identity.server(&alpns, client_auth)?`, or share a resolver with other listeners. ### Sessions are wrapped for moq-net From b74f7f8c425a1122607c490940d469476891c53a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 14:10:59 -0700 Subject: [PATCH 37/40] docs(migrate): migration paths in JavaScript guides Co-Authored-By: Claude Opus 5.5 --- js/auth/MIGRATE.md | 25 +-------- js/flate/MIGRATE.md | 24 ++------- js/hang/MIGRATE.md | 72 ++----------------------- js/json/MIGRATE.md | 24 ++------- js/loc/MIGRATE.md | 10 +--- js/msf/MIGRATE.md | 7 --- js/net/MIGRATE.md | 123 ++++-------------------------------------- js/publish/MIGRATE.md | 25 +-------- js/room/MIGRATE.md | 16 +----- js/signals/MIGRATE.md | 22 +------- js/watch/MIGRATE.md | 28 +--------- 11 files changed, 31 insertions(+), 345 deletions(-) diff --git a/js/auth/MIGRATE.md b/js/auth/MIGRATE.md index 61ba519025..303c3683bf 100644 --- a/js/auth/MIGRATE.md +++ b/js/auth/MIGRATE.md @@ -7,33 +7,16 @@ How to update your code for each breaking change, newest first. ### Roles carry `Access` (#5697) Parsed `Claims`, `Grant`, and `authorize`'s permissions hold `publish` and `subscribe` as `Access`: path patterns plus an optional per-connection `rate` cap. -Read the patterns from `.path`. `Key.sign` still takes pattern strings, but a hand-built `Claims` value must come from `ClaimsSchema.parse`. +Read the patterns from `.path`, as in `permissions.publish.path.matches(path)`. `Key.sign` still takes pattern strings, but a hand-built `Claims` value must come from `ClaimsSchema.parse`. A gateway must enforce a cap or refuse the grant before admitting media. -```ts -// Before -const allowed = permissions.publish.matches(path); -// After -const allowed = permissions.publish.path.matches(path); -if (permissions.publish.rate !== undefined) throw new Error("bitrate caps are not enforced"); -``` - ### A token must carry its key's `kid` (#3943) `Key.verify` refuses a token whose `kid` header does not match the key's, and a key set accepts a token without a `kid` only when it holds one key that has none. +Sign with the key that will verify it, so the `kid` matches. The strict transport schema also accepts `rtmp`, `srt`, `webrtc`, and `webtransport`, so an exhaustive `switch` over it needs those arms. A WebTransport session reports `webtransport` instead of `quic` (#5293), so a policy keyed on `quic` must accept both. -```ts -// Sign with the key that will verify it, so the kid matches. -const token = await Key.sign(key, claims); - -// Before -const overQuic = transport === "quic"; -// After -const overQuic = transport === "quic" || transport === "webtransport"; -``` - ## @moq/token 0.3 -> @moq/auth 0.2 ### @moq/token is @moq/auth @@ -41,10 +24,6 @@ const overQuic = transport === "quic" || transport === "webtransport"; `sign` and `verify` are `Key.sign` and `Key.verify`, and claims are pattern unions: `alice` covers only `alice`, so sign `alice/**` for the subtree. ```ts -// Before -import { sign } from "@moq/token"; -const token = await sign(key, { put: ["alice"] }); -// After import { Key } from "@moq/auth"; const token = await Key.sign(key, { publish: "alice/**" }); ``` diff --git a/js/flate/MIGRATE.md b/js/flate/MIGRATE.md index f29788487a..3c9ea9653a 100644 --- a/js/flate/MIGRATE.md +++ b/js/flate/MIGRATE.md @@ -7,34 +7,16 @@ How to update your code for each breaking change, newest first. ### @moq/binary is @moq/flate The opaque `Snapshot` and `Stream` tracks moved beside the codec; the wire and the catalog's `binary` section are unchanged. - -```ts -// Before -import { Snapshot, Stream } from "@moq/binary"; -// After -import { Snapshot, Stream } from "@moq/flate"; -``` +Import `Snapshot` and `Stream` from `@moq/flate` instead of `@moq/binary`. ### Consumers return `Timed` values (#5099) The snapshot and stream consumers' `next()` and async iterator yield `{ value, at }`, where `at` is absent on an untimed track. A snapshot consumer's `next()` yields every state in order; call `latest()` to skip to the newest. - -```ts -// Before -const bytes = await consumer.next(); -// After -const bytes = (await consumer.latest())?.value; -``` +Where you awaited `next()` for the newest bytes, read `(await consumer.latest())?.value`. ### Producers never invent a timestamp (#5070) `Snapshot.Producer.update` and `Stream.Producer.append` take a `Timed`. +Pass `{ value: bytes, at: Time.Timestamp.now() }` instead of the bytes. Omit `at` only on an untimed track. - -```ts -// Before -producer.update(bytes); -// After -producer.update({ value: bytes, at: Time.Timestamp.now() }); -``` diff --git a/js/hang/MIGRATE.md b/js/hang/MIGRATE.md index 27281f9d1f..eba07b0f59 100644 --- a/js/hang/MIGRATE.md +++ b/js/hang/MIGRATE.md @@ -9,38 +9,17 @@ How to update your code for each breaking change, newest first. `version` is the oldest moq-hang draft that reads the catalog, and parsing throws `UnsupportedVersion` for one newer than this reader. Rename an extension field that was called `version`. -```ts -// Before -const Schema = z.extend(RootSchema, { version: z.string() }); -// After -const Schema = z.extend(RootSchema, { appVersion: z.string() }); -``` - ### Archive timelines declare a per-track duration (#5715) Each `archive.timelines` value is `{ track, duration }` instead of the timeline track's name, and `archive.durationMax` is removed. `duration` is the nominal record duration in the archive timescale, absent for an untimed track. +Read the track name from the value's `track`, and use the per-track `duration` instead of `durationMax`. A catalog on the other side of this change parses with no `archive`; see the [upgrade guide](https://github.com/moq-dev/moq/blob/main/doc/setup/upgrade.md). -```ts -// Before -const track = catalog.archive?.timelines.video; -const bound = catalog.archive?.durationMax; -// After -const track = catalog.archive?.timelines.video?.track; -const duration = catalog.archive?.timelines.video?.duration; -``` - ### Timeline records are optionally timed (#5720) `Timeline.Record.pts` and `.duration` are optional, absent on an untimed track. - -```ts -// Before -const pts: number = record.pts; -// After -if (record.pts === undefined) continue; -``` +Handle a record whose `pts` is `undefined`. ### Catalog ranking names its media kind @@ -48,70 +27,29 @@ if (record.pts === undefined) continue; `Catalog.Audio.ranked(renditions)` orders audio by enabled state, bitrate, sample rate, channel count, and name. Both put disabled renditions last. -```ts -// Before -const best = Catalog.ranked(catalog.video.renditions)[0]; -// After -const best = Catalog.Video.ranked(catalog.video.renditions)[0]; -``` - ### One `enabled` flag replaces `stalled` (#4915) `Catalog.Stalled` is gone, `VideoConfig.stalled` is `enabled` with the meaning inverted, and `AudioConfig` gains `enabled`. - -```ts -// Before -if (config.stalled) skip(); -// After -if (config.enabled === false) skip(); -``` +Replace a `config.stalled` check with `config.enabled === false`. ### CMAF decodes at the frame timestamp (#4826) A fragment's earliest sample presents at its moq-net frame timestamp; `tfdt` only orders the samples. `Container.Format.decode` and `Cmaf.decodeDataSegment` take that timestamp, or `undefined` for an untimed frame, whose samples present at `tfdt`. +Pass `frame.timestamp` after the payload. `Cmaf.decodeTimestamp` is gone. -```ts -// Before -const frames = format.decode(frame.payload); -// After -const frames = format.decode(frame.payload, frame.timestamp); -``` - ### Container consumers take `maxDelay` `Container.Consumer`'s `maxAge` prop is `maxDelay`, and passing `maxAge` throws a `TypeError`. -```ts -// Before -new Container.Consumer(track, { maxAge }); -// After -new Container.Consumer(track, { maxDelay }); -``` - ### `cut` is `discontinuity` (#4141) `Container.Legacy.Producer.cut(end?)` is `discontinuity(end?)`. -```ts -// Before -producer.cut(end); -// After -producer.discontinuity(end); -``` - ## 0.4 -> 0.5 ### The catalog's `timeline` entry is `archive` @moq/hang reads the root catalog's `archive` entry instead of `timeline`, and wall time from the root `clock`. - -```ts -// Before -const index = catalog.timeline; -const wall = catalog.timeline?.wall; -// After -const index = catalog.archive; -const wall = catalog.clock?.wall; -``` +Read `catalog.archive` instead of `catalog.timeline`, and `catalog.clock?.wall` instead of `catalog.timeline?.wall`. diff --git a/js/json/MIGRATE.md b/js/json/MIGRATE.md index 07455bead8..d569462e49 100644 --- a/js/json/MIGRATE.md +++ b/js/json/MIGRATE.md @@ -8,36 +8,18 @@ How to update your code for each breaking change, newest first. The snapshot and stream consumers' `next()` and async iterator yield `{ value, at }`, where `at` is the frame's timestamp and absent on an untimed track. A snapshot consumer's `next()` yields every state in order; call `latest()` to skip to the newest, as `next()` used to. +Where you awaited `next()` for the newest value, read `(await consumer.latest())?.value`. A caller that passes the result on as `any` or `unknown` still compiles, so search for every `next()` and `for await` rather than relying on type errors. -```ts -// Before -const value = await consumer.next(); -// After -const value = (await consumer.latest())?.value; -``` - ### Producers never invent a timestamp (#5070) `Snapshot.Producer.update`, `Stream.Producer.append`, and `Window.Producer.push` take a `Timed`, and `Window.Producer.pop` and `Snapshot.Producer.mutate` take an optional `at`. +Pass `{ value, at: Time.Timestamp.now() }` instead of the value. Omit `at` only on an untimed track. -```ts -// Before -producer.update(value); -// After -producer.update({ value, at: Time.Timestamp.now() }); -``` - ## 0.3 -> 0.4 ### One options object (#3640) Constructors take one options object. - -```ts -// Before -new Json.Snapshot.Consumer(track, config); -// After -new Json.Snapshot.Consumer({ track, ...config }); -``` +Spread the config beside the track: `new Json.Snapshot.Consumer({ track, ...config })`. diff --git a/js/loc/MIGRATE.md b/js/loc/MIGRATE.md index c6d742e983..9c775bae81 100644 --- a/js/loc/MIGRATE.md +++ b/js/loc/MIGRATE.md @@ -7,12 +7,4 @@ How to update your code for each breaking change, newest first. ### Timestamps are `@moq/clock` units (#5706) Frame timestamps are typed as `Micro` from `@moq/clock` instead of `Time.Micro` from `@moq/net`. - -```ts -// Before -import { Time } from "@moq/net"; -const ts: Time.Micro = frame.timestamp; -// After -import type { Micro } from "@moq/clock"; -const ts: Micro = frame.timestamp; -``` +Import the `Micro` type from `@moq/clock`. diff --git a/js/msf/MIGRATE.md b/js/msf/MIGRATE.md index 79df0bfb1e..8d2cbe631b 100644 --- a/js/msf/MIGRATE.md +++ b/js/msf/MIGRATE.md @@ -8,10 +8,3 @@ How to update your code for each breaking change, newest first. The non-standard `stalled` track field is gone and no longer read or written. Drop it from any track you build. - -```ts -// Before -const track: Track = { name: "video0", packaging: "loc", stalled: true }; -// After -const track: Track = { name: "video0", packaging: "loc" }; -``` diff --git a/js/net/MIGRATE.md b/js/net/MIGRATE.md index 74ff734753..bad4da80e3 100644 --- a/js/net/MIGRATE.md +++ b/js/net/MIGRATE.md @@ -11,14 +11,7 @@ A publisher whose timeline restarts publishes a new broadcast instead. ### `Auth.Auth` is `Auth.Handle`, and `Auth.Token` is `Auth.Auth` (#5695) -```ts -// Before -const handle: Auth.Auth = connection.auth; -const token: Auth.Token = await handle.add(bytes); -// After -const handle: Auth.Handle = connection.auth; -const auth: Auth.Auth = await handle.add(bytes); -``` +Rename `Auth.Auth` to `Auth.Handle` first, then `Auth.Token` to `Auth.Auth`. ### Superseded groups age by wall clock too (#5548) @@ -35,15 +28,6 @@ const sub = track.subscribe({ maxDelay: Milli(1000) }); `Time.Nano`, `Time.Micro`, `Time.Milli`, and `Time.Second` are gone. Import them from `@moq/clock`, which throws on negative or non-finite values. -```ts -// Before -import { Time } from "@moq/net"; -const delay = Time.Milli(250); -// After -import { Milli } from "@moq/clock"; -const delay = Milli(250); -``` - ### Announced requests follow restarts, and `broadcasts()` is gone An unpinned `request(path, { announced: true })` stays open across publisher instances: its `active` moves to each new instance's broadcast and clears between them, instead of ending with `Unroutable`. @@ -51,9 +35,6 @@ Read `active` reactively and close the request when done, or pass `epoch` to kee `Origin.Producer` and `Origin.Consumer` drop `broadcasts(scope)`; iterate `announced(scope)` instead. ```ts -// Before -for (const broadcast of origin.broadcasts(scope)) { ... } -// After for await (const event of origin.announced(scope)) { if (event.kind === "start" || event.kind === "restart") { ... } } @@ -64,51 +45,27 @@ for await (const event of origin.announced(scope)) { A `consume` origin stays empty until a reader watches it with `announced(scope)` or `request(path, { announced: true })`; the session then asks the relay about those prefixes only. Code that called `request(path)` expecting a route without watching should hold an `announced(scope)` stream. The `discovery` option on `connect`, `accept`, and `Connection`, and `Established.discovery`, are removed; passing the option throws a `TypeError`. - -```ts -// Before -const connection = new Connection({ url, discovery: true }); -// After -const connection = new Connection({ url }); -``` +Drop the option. ### Announce streams yield `start`, `update`, `end`, and `restart` events `Announce.Consumer` yields an `Announce.Event`: an `Announce.Announce` (`prefix`, `captures`, `route`) plus a `kind`. `Announce.Update`, `Announce.Kind`, and `Announce.isActive` are gone. +Replace `Announce.isActive` with a check on `event.kind`: skip `"end"`, and treat `"start"` and `"restart"` as active. A `"restart"` means another publisher instance now serves the prefix: request the path again. -```ts -// Before -if (!Announce.isActive(update.kind)) continue; -// After -if (event.kind === "end") continue; -if (event.kind === "start" || event.kind === "restart") resolve(event.prefix); -``` - ### Routes carry the publisher epoch (#4942) `Path.joinEpoch` and `Path.splitEpoch` are gone. Set the epoch on the route with `announce({ epoch })` and read `Route.epoch`; pin a request to one instance with `RequestOptions.epoch`. Nothing mints an epoch by default. -```ts -// Before -origin.createBroadcast(Path.joinEpoch(path, epoch)).announce(); -// After -origin.createBroadcast(path).announce({ epoch }); -const request = origin.request(path, { epoch }); -``` - ### Publishing never invents a timestamp (#5070) `Group.Producer` and `Track.Producer` drop `writeString`, `writeJson`, and `writeBool`, and `@moq/net/zod` drops `write`. Encode the payload yourself and write it as a frame with an explicit timestamp, or none on an untimed track. ```ts -// Before -track.writeJson(value); -// After track.writeFrame({ payload: new TextEncoder().encode(JSON.stringify(value)), timestamp }); ``` @@ -116,74 +73,33 @@ track.writeFrame({ payload: new TextEncoder().encode(JSON.stringify(value)), tim `Track.Info.timescale` is optional; omitted or `null` means untimed, and `infoDefaults()` no longer fills in milliseconds. `Group.Frame.timestamp` and `Track.Datagram.timestamp` are optional, and `appendDatagram` / `insertDatagram` take `Timestamp | undefined`. -A write whose timedness does not match its track throws `Error.TimestampMismatch`, so declare a timescale on every track you stamp. - -```ts -// Before -const track = broadcast.createTrack("chat"); -// After -const track = broadcast.createTrack("chat", { timescale: Timescale.MILLI }); -``` +A write whose timedness does not match its track throws `Error.TimestampMismatch`, so declare a timescale, such as `{ timescale: Timescale.MILLI }`, on every track you stamp. ### Subscriber staleness is `maxDelay` `Track.Subscription`'s `maxAge` is `maxDelay`; `maxAge` stays the publisher's retention on `Track.Info`. A `maxAge` key in subscription options throws a `TypeError`, even when `undefined` or beside `maxDelay`. -```ts -// Before -track.subscribe({ maxAge: Milli(500) }); -// After -track.subscribe({ maxDelay: Milli(500) }); -``` - ### Publisher retention is optional (#4242) `Track.Info.maxAge` is optional, absent meaning no publisher limit, and `DEFAULT_MAX_AGE_MS` is gone. - -```ts -// Before -broadcast.createTrack("video", { maxAge: DEFAULT_MAX_AGE_MS }); -// After -broadcast.createTrack("video", { maxAge: Milli(30_000) }); -``` +Pass an explicit value such as `Milli(30_000)` where you used the constant. ### Demand is read through `demand()` -`Track.Producer` and `Group.Producer` move `used` and `unused()` to `demand()`. +`Track.Producer` and `Group.Producer` move `used` and `unused()` to `demand()`, so `producer.unused()` is `producer.demand().unused()`. `Allocator.reserve` takes `producer.demand()`, replacing the `Bandwidth.Demand` interface. -```ts -// Before -await producer.unused(); -// After -await producer.demand().unused(); -``` - ### A replacement track owns its start sequence (#5068) -`BroadcastProducer.insertTrack` is removed. +`BroadcastProducer.insertTrack` is removed; create the track with `broadcast.createTrack(name, info)` instead. `Track.Producer.appendGroup` and `appendDatagram` throw `duplicate sequence` instead of continuing past an earlier producer of the same name; `createTrack` now answers a queued request instead of throwing `duplicate track`. `Request.accept` closes the request's producer with the error whenever it throws. -```ts -// Before -const track = new Track.Producer("chat").accept(info); -broadcast.insertTrack(track); -// After -const track = broadcast.createTrack("chat", info); -``` - ### Route cost is a `bigint` (#4661) `Route.cost` is one `bigint` instead of separate warm and cold numbers. - -```ts -// Before -broadcast.announce({ cost: { warm: 10, cold: 10 } }); -// After -broadcast.announce({ cost: 10n }); -``` +Pass one value, such as `cost: 10n`. ### Datagrams outside the group range are skipped (#4982) @@ -192,13 +108,7 @@ A subscriber no longer receives datagrams outside its group range, and a FETCH n ### Broadcast `close` takes no abort reason (#4152) `broadcast.Producer.close(abort)` and `broadcast.Consumer.close(abort)` lose the parameter, and `closed` is a `GetPromise`. - -```ts -// Before -broadcast.close(new Error("done")); -// After -broadcast.close(); -``` +Call `close()` with no argument. ## 0.3 -> 0.4 @@ -207,27 +117,12 @@ broadcast.close(); `Connection.Reload` is `new Connection({ url })`, which pools one connection per relay. `closed` settles only on `close()`; the error that stopped retrying is `error`. -```ts -// Before -const connection = new Moq.Connection.Reload({ url }); -// After -const connection = new Moq.Connection({ url }); -``` - ### Origins hold broadcasts (#2705, #3225) `connection.publish(path, broadcast)` is `origin.createBroadcast(path)` then `broadcast.announce()`. `connection.announced(prefix)` is `origin.announced(scope)`, yielding `{ prefix, kind, route }`. `ignoreSelf` is gone: reflected announces are always dropped. -```ts -// Before -connection.publish(path, broadcast); -// After -const broadcast = connection.origin.createBroadcast(path); -broadcast.announce(); -``` - ### Names mirror Rust (#3710) | Before | After | diff --git a/js/publish/MIGRATE.md b/js/publish/MIGRATE.md index 48fc865d2e..0e66059ea8 100644 --- a/js/publish/MIGRATE.md +++ b/js/publish/MIGRATE.md @@ -7,14 +7,7 @@ How to update your code for each breaking change, newest first. ### A disabled rendition stays in the catalog (#4915) `enabled: false` keeps the rendition in the catalog with `enabled: false` instead of removing it, and viewers skip it. -Check any code that read a missing rendition as disabled. - -```ts -// Before -const disabled = !catalog.video?.renditions[name]; -// After -const disabled = catalog.video?.renditions[name]?.enabled === false; -``` +Code that read a missing rendition as disabled should check `catalog.video?.renditions[name]?.enabled === false`. ### Tracks persist for the broadcast's life (#5068) @@ -26,22 +19,8 @@ A `maxAge` change, or replacing a closed rendition track, recreates `broadcast.n Opus DTX is always off, since Chromium's DTX output shifts the audio timeline. Remove the field; a plain-JS caller still passing it is ignored. -```ts -// Before -{ codec: "opus", usedtx: true } -// After -{ codec: "opus" } -``` - ## 0.4 -> 0.5 ### Publish takes an origin -Pass the connection's origin instead of a `connection` signal. - -```ts -// Before -new Publish.Broadcast({ connection }); -// After -new Publish.Broadcast({ origin: connection.origin }); -``` +Pass `origin: connection.origin` to `Publish.Broadcast` instead of a `connection` signal. diff --git a/js/room/MIGRATE.md b/js/room/MIGRATE.md index 7aa8275612..f5d865cd37 100644 --- a/js/room/MIGRATE.md +++ b/js/room/MIGRATE.md @@ -7,21 +7,9 @@ How to update your code for each breaking change, newest first. ### `HISTORY` is a `Nano` (#5706) `HISTORY` is a `Nano` from `@moq/clock` instead of milliseconds. - -```ts -// Before -const ms = HISTORY; -// After -const ms = Milli.fromNano(HISTORY); -``` +Use `Milli.fromNano(HISTORY)` where you need milliseconds. ### Chat tracks are timed (#4968) `Chat.info()` includes a `timescale`, so a chat track you create yourself must declare one before writing timestamps. - -```ts -// Before -const track = broadcast.createTrack(Chat.TRACK, { priority }); -// After -const track = broadcast.createTrack(Chat.TRACK, Chat.info()); -``` +Create it with `broadcast.createTrack(Chat.TRACK, Chat.info())`. diff --git a/js/signals/MIGRATE.md b/js/signals/MIGRATE.md index 69896fda24..f25f2892d6 100644 --- a/js/signals/MIGRATE.md +++ b/js/signals/MIGRATE.md @@ -7,14 +7,7 @@ How to update your code for each breaking change, newest first. ### Timers take a `Nano` (#5706) `effect.timer`, `effect.timeout`, and `effect.interval` take a `Nano` from `@moq/clock` instead of milliseconds, and run on the installed clock. - -```ts -// Before -effect.timer(() => tick(), 250); -// After -import { Milli, Nano } from "@moq/clock"; -effect.timer(() => tick(), Nano.fromMilli(Milli(250))); -``` +Convert a millisecond value with `Nano.fromMilli(Milli(250))`, importing both from `@moq/clock`. ### Effect cleanups run last-in, first-out (#4889) @@ -25,16 +18,5 @@ Check any cleanup that relied on running after one registered later. ### `Signal.race` returns a `GetPromise` (#4226) `Signal.race` releases its losing listeners and returns a `GetPromise` instead of a native `Promise`. -`await` and `.then()` still work; `.catch()` and `.finally()` do not. +`await` and `.then()` still work; `.catch()` and `.finally()` do not, so wrap the `await` in `try`/`catch` instead. Waiting begins when it is awaited or subscribed. - -```ts -// Before -Signal.race(signals).catch(handle); -// After -try { - await Signal.race(signals); -} catch (err) { - handle(err); -} -``` diff --git a/js/watch/MIGRATE.md b/js/watch/MIGRATE.md index 086b963ff7..f95f080faf 100644 --- a/js/watch/MIGRATE.md +++ b/js/watch/MIGRATE.md @@ -12,14 +12,7 @@ A numeric `delay` is taken literally instead of having the rendition's own delay ### Staleness is `maxDelay` -`Sync.out.maxAge` is `Sync.out.maxDelay`. - -```ts -// Before -const budget = sync.out.maxAge.peek(); -// After -const budget = sync.out.maxDelay.peek(); -``` +Rename `Sync.out.maxAge` to `Sync.out.maxDelay`. ### Disabled renditions are never selected (#4915) @@ -31,13 +24,6 @@ With no video rendition selectable, the picture clears to black. With `announced` off, the player no longer moves to a later publisher at the same path. Turn `announced` on to follow restarts. -```ts -// Before -new Watch.Player({ origin: connection.origin, name, announced: false }); -// After -new Watch.Player({ origin: connection.origin, name, announced: true }); -``` - ## 0.5 -> 0.6 ### Delay attributes need a unit (#3396, #3817) @@ -46,19 +32,9 @@ new Watch.Player({ origin: connection.origin, name, announced: true }); `reload` is `announced`. ```html - - - ``` ### `Watch.Broadcast` is `Watch.Player` -The player takes the connection's origin. - -```ts -// Before -new Watch.Broadcast({ connection, name }); -// After -new Watch.Player({ origin: connection.origin, name }); -``` +Rename `Watch.Broadcast` to `Watch.Player` and pass `origin: connection.origin` instead of `connection`. From 2d4f96a727383d54c10c3cdcf11e2326a7c46205 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 14:11:21 -0700 Subject: [PATCH 38/40] docs(migrate): migration paths in the remaining Rust guides Co-Authored-By: Claude Opus 5.5 --- rs/hang/MIGRATE.md | 45 ++--------------- rs/kio/MIGRATE.md | 14 +---- rs/moq-archive/MIGRATE.md | 10 +--- rs/moq-audio/MIGRATE.md | 41 +-------------- rs/moq-auth/MIGRATE.md | 28 +--------- rs/moq-cli/MIGRATE.md | 76 +++------------------------- rs/moq-clock/MIGRATE.md | 9 +--- rs/moq-e2ee/MIGRATE.md | 18 +------ rs/moq-flate/MIGRATE.md | 22 +------- rs/moq-gst/MIGRATE.md | 7 --- rs/moq-hls/MIGRATE.md | 24 +-------- rs/moq-json/MIGRATE.md | 27 ++-------- rs/moq-msf/MIGRATE.md | 6 --- rs/moq-relay/MIGRATE.md | 104 ++------------------------------------ rs/moq-rtc/MIGRATE.md | 7 --- rs/moq-video/MIGRATE.md | 87 +++---------------------------- 16 files changed, 38 insertions(+), 487 deletions(-) diff --git a/rs/hang/MIGRATE.md b/rs/hang/MIGRATE.md index 16c48766ce..6e2dd75497 100644 --- a/rs/hang/MIGRATE.md +++ b/rs/hang/MIGRATE.md @@ -7,55 +7,26 @@ How to update your code for each breaking change, newest first. ### The catalog root carries a reader `version` (#5806) `version` is reserved at the catalog root: decoding refuses a catalog newer than this crate reads with `Error::UnsupportedVersion`, and serializing fails if the extension `E` has its own `version` member. -Rename that member. - -```rust -// Before -#[derive(Serialize, Deserialize)] -struct Extra { version: u32 } -// After -#[derive(Serialize, Deserialize)] -struct Extra { app_version: u32 } -``` +Rename that member, such as to `app_version`. ### Archive timelines declare a per-track duration (#5715) Each `Archive::timelines` value is a `Timeline` (the timeline `track` plus its nominal `duration` in the archive timescale, `None` when untimed) instead of the timeline track's name, and `Archive::duration_max` is removed. A catalog on the other side of this change decodes with no archive; see the [upgrade guide](https://github.com/moq-dev/moq/blob/main/doc/setup/upgrade.md). - -```rust -// Before -archive.timelines.insert("video".to_string(), "video.timeline.z".to_string()); -let track: &String = &archive.timelines["video"]; -// After -archive.timelines.insert("video".to_string(), Timeline::new("video.timeline.z", 4_000)); -let track: &String = &archive.timelines["video"].track; -``` +Insert `Timeline::new(track, duration)` and read the track name from `.track`. ### Timeline records are optionally timed (#5720) `timeline::Record::pts` and `duration` are `Option`, absent on an untimed track. Build an untimed record with `Record::untimed(sequence, start, end)`. - -```rust -// Before -let pts: u64 = record.pts; -// After -let Some(pts) = record.pts else { continue }; -``` +Handle `None` where you read `record.pts` or `record.duration`. ### One `enabled` flag replaces `stalled` (#4915) `VideoConfig::stalled: Option` is `enabled: bool`, with the meaning inverted, and `AudioConfig` gains `enabled`. The `catalog::stalled` module is gone, and `Video::ranked()` puts disabled renditions last. `stalled` is no longer written and is ignored on read. - -```rust -// Before -video.stalled = Some(true); -// After -video.enabled = false; -``` +Set `enabled = false` where you set `stalled = Some(true)`. ## 0.20 -> 0.21 @@ -63,10 +34,4 @@ video.enabled = false; The root catalog's `timeline` is `archive` (`catalog::Archive`), and wall time moved to the root `clock: { wall, timescale }`. An old publisher's catalog has no `archive`, so upgrade publishers before readers that need it. - -```rust -// Before -let wall = catalog.timeline.and_then(|t| t.wall); -// After -let clock = catalog.clock; -``` +Read wall time from `catalog.clock` instead of `catalog.timeline`. diff --git a/rs/kio/MIGRATE.md b/rs/kio/MIGRATE.md index b60760117a..2810c099b3 100644 --- a/rs/kio/MIGRATE.md +++ b/rs/kio/MIGRATE.md @@ -10,16 +10,4 @@ How to update your code for each breaking change, newest first. `Task` gained an `Output` type and has a blanket impl for `FnMut(&Waiter) -> Poll`. `poll_used` and `poll_unused` return `Poll>` instead of `Poll>`. `Waiter::context()` replaces `Context::from_waker(waiter.waker())`. - -```rust -// Before -match producer.poll_unused(waiter) { - Poll::Ready(None) => {} // closed - _ => {} -} -// After -match producer.poll_unused(waiter) { - Poll::Ready(Err(kio::Closed)) => {} - _ => {} -} -``` +Match `Poll::Ready(Err(kio::Closed))` where you matched `Poll::Ready(None)`. diff --git a/rs/moq-archive/MIGRATE.md b/rs/moq-archive/MIGRATE.md index 46b404eb46..93e4567040 100644 --- a/rs/moq-archive/MIGRATE.md +++ b/rs/moq-archive/MIGRATE.md @@ -11,12 +11,4 @@ How to update your code for each breaking change, newest first. `Object::decode(buf, timed)` and `Store::get_segments(track, segment, timed)` take whether the track is timed. `Error::Untimed` now means a DVR cannot record an untimed track; mismatched timing is `Error::Timing`. Untimed recordings use the version 2 format, which an older version 2 reader refuses. - -```rust -// Before -let info = Info::new(priority, timescale)?; -let segments = store.get_segments(track, segment).await?; -// After -let info = Info::new(priority, Some(timescale))?; -let segments = store.get_segments(track, segment, info.timescale.is_some()).await?; -``` +Pass `Some(timescale)` to `Info::new`, and `info.timescale.is_some()` as `timed`. diff --git a/rs/moq-audio/MIGRATE.md b/rs/moq-audio/MIGRATE.md index 617525137c..6efb57e67c 100644 --- a/rs/moq-audio/MIGRATE.md +++ b/rs/moq-audio/MIGRATE.md @@ -9,54 +9,17 @@ How to update your code for each breaking change, newest first. `decode::Consumer::read()` returns `decode::Read` events instead of bare frames: `Frame { discontinuity, frame }` and `End { discontinuity, end }`; `None` still means the track is finished. An `End` can be followed by final PCM with the same `discontinuity` before a fresh encoder bumps it, and a bump with no `End` before it is a loss. Binding audio frame APIs keep their existing shape. - -```rust -// Before -while let Some(frame) = consumer.read().await? { - play(frame); -} -// After -while let Some(read) = consumer.read().await? { - match read { - decode::Read::Frame { frame, .. } => play(frame), - decode::Read::End { end, .. } => trim_after(end), - } -} -``` +Play `decode::Read::Frame { frame, .. }` as before, and trim after `decode::Read::End { end, .. }`. ### Capture handles are `Control` and `Capture` `encode::Publication` is `encode::Control`, and `encode::PublicationOptions` is `encode::Capture`, without its `clock` field: the driver stamps on `catalog.clock()`. - -```rust -// Before -let options = PublicationOptions { capture, encode, clock }; -let (publication, driver) = Publication::new(broadcast, catalog, options)?; -// After -let mut options = encode::Capture::default(); -options.capture = capture; -options.encode = encode; -let (control, driver) = encode::Control::new(broadcast, catalog, options)?; -``` +Build it from `encode::Capture::default()`, setting `capture` and `encode`. ### `reset_epoch` is `reanchor` (#5146) "Epoch" means only the publisher epoch, so the audio re-anchor is renamed. -```rust -// Before -producer.reset_epoch(); -// After -producer.reanchor(); -``` - ### Decoder staleness is `max_delay` `decode::Options::max_age` is `max_delay`, and `decode::Consumer::max_age()` is `max_delay()`. - -```rust -// Before -options.max_age = delay; -// After -options.max_delay = delay; -``` diff --git a/rs/moq-auth/MIGRATE.md b/rs/moq-auth/MIGRATE.md index d671be09e7..acf6c6b316 100644 --- a/rs/moq-auth/MIGRATE.md +++ b/rs/moq-auth/MIGRATE.md @@ -11,14 +11,6 @@ Read the patterns from `.path`; constructors and `Claims::with_publish` / `with_ A capped role serializes as `{"path":"alice/**","rate":8000000}`, and key scopes refuse caps. A gateway that embeds this crate must enforce a cap or refuse the grant before admitting media. -```rust -// Before -let allowed = permissions.publish.matches(path); -// After -let allowed = permissions.publish.path.matches(path); -let cap = permissions.publish.rate.map(|rate| rate.as_bps()); // enforce, or refuse when Some -``` - ### Gateway transports and key sets are non-exhaustive (#3943) `Transport` gains `Rtmp`, `Srt`, `WebRtc`, and `WebTransport`, and `serve::Keys` gains `Set`; both are `#[non_exhaustive]`, so add a wildcard arm. @@ -40,17 +32,7 @@ match transport { `serve::Server::new` returns a `Result`: it refuses `revalidate` without `expires` (`Error::UnboundedRevalidate`) and session limits without `revalidate` (`Error::LimitsWithoutRevalidate`). A session presenting both a JWT and a client certificate is refused with `Refusal::TokenAndCertificate`. `Key::verify` enforces `nbf` (`Claims::not_before`, failing with `Error::TokenNotYetValid`) and ignores `iss`, `sub`, and `jti`; any other unknown claim still refuses the token. - -```rust -// Before -let policy = Policy::default(); // re-checked every 60 s, bounded at 24 h -let server = Server::new(policy); -// After -let mut policy = Policy::default(); -policy.revalidate = Some(Duration::from_secs(60)); -policy.expires = Some(Duration::from_secs(24 * 60 * 60)); -let server = Server::new(policy)?; -``` +To keep the old 60 s re-check bounded at 24 h, set `revalidate` and `expires` to those values, and handle the `Result` from `Server::new`. ## moq-token 0.7 -> moq-auth 0.1 @@ -58,10 +40,4 @@ let server = Server::new(policy)?; `Claims` holds `Patterns`, and `Claims::authorize` returns pattern residuals. A `publish` or `subscribe` grant is a pattern: `alice` covers only `alice`, so sign `alice/**` for the subtree. - -```rust -// Before -use moq_token::Claims; -// After -use moq_auth::Claims; -``` +Replace `moq_token` with `moq_auth` in your imports. diff --git a/rs/moq-cli/MIGRATE.md b/rs/moq-cli/MIGRATE.md index f3b0268392..06ae45737d 100644 --- a/rs/moq-cli/MIGRATE.md +++ b/rs/moq-cli/MIGRATE.md @@ -8,36 +8,18 @@ How to update your invocation and config for each breaking change, newest first. The output trails a TS source by the delay instead of twice it. With a multiplex rate, video whose HRD declares a longer buffer delay than `--delay` stops the export, naming the delay it needs. - -```bash -# Before -moq export ts --delay 500ms ... -# After -moq export ts --delay 1s ... -``` +Double an explicit `--delay` to keep the same trail, such as `500ms` to `1s`. ### `--max-age` on fMP4, H.264, H.265, and RTMP exports is `--max-delay` `moq export fmp4`, `h264`, `h265`, and `rtmp` refuse `--max-age`. `moq import --max-age` is unchanged: it is the publisher's retention. - -```bash -# Before -moq export fmp4 --max-age 2s ... -# After -moq export fmp4 --max-delay 2s ... -``` +Rename `--max-age` to `--max-delay` on those exports. ### `moq export ts`, `flv`, and `mkv` take `--delay` (#4645) Each frame is written that long after its decode time; the older staleness spellings are refused with `--delay` named. - -```bash -# Before -moq export ts --max-age 500ms ... -# After -moq export ts --delay 500ms ... -``` +Rename `--max-age` to `--delay`. ### `moq export ts --linger` follows only the same publisher instance @@ -63,25 +45,12 @@ Annex-B H.264 and H.265 sources get an `avc3` or `hev1` init from the catalog wh `auto` is measured from how late frames arrive instead of derived from RTT. Pass `--delay 100ms` to keep the old fixed delay. -```bash -# Before -moq ... play -# After -moq ... play --delay 100ms -``` - ### `--hop` is removed; `--epoch` replaces it (#4969) A redundant pair shares an epoch, a UUIDv7 such as `uuidgen -7` prints, instead of a Hop ID. `--hop` is an unknown flag and `MOQ_HOP` is silently ignored, so remove it: a pair still keyed on `MOQ_HOP` mints an epoch per process and each restart replaces the other's broadcast. A node that pinned its cluster id with `--hop` passes `--cluster-id` instead. - -```bash -# Before -MOQ_HOP=7 moq import ... -# After -MOQ_EPOCH=$(uuidgen -7) moq import ... # the same value on both members -``` +Give both members the same `MOQ_EPOCH`, such as one `uuidgen -7` value. ### Settings no listener reads stop startup (#5006) @@ -90,13 +59,6 @@ A stream-only listener (TCP or Unix, no `--listen`) refuses the QUIC-only flags, `--listen-quic-lb-nonce` needs `--listen-quic-lb-id`. Each used to be ignored: drop it, or add the listener it configures. -```bash -# Before -moq --connect https://relay.example.com/anon --auth-public 'anon/**' import ... -# After -moq --connect https://relay.example.com/anon import ... -``` - ### `moq --listen` with an invalid auth config stops at startup (#4912) It used to start and refuse every session. @@ -105,26 +67,12 @@ It used to start and refuse every session. Rename it in `--features` when building from source. -```bash -# Before -cargo install moq-cli --no-default-features --features "noq,websocket,play" -# After -cargo install moq-cli --no-default-features --features "quic,websocket,play" -``` - ## 0.13 -> 0.14 ### Gossip discovery is removed (#4601) `--cluster-mesh` and TOML `mesh` are refused: list every peer with `--cluster-connect`. -```bash -# Before -moq --cluster-mesh --cluster-connect https://root.example/ import ... -# After -moq --cluster-connect https://root.example/,https://b.example/ import ... -``` - ## 0.12 -> 0.13 ### `moq auth serve` matches 0.14 relay auth (#4319) @@ -132,13 +80,7 @@ moq --cluster-connect https://root.example/,https://b.example/ import ... `moq auth serve` never re-checks or expires a session by default; `--revalidate` needs `--expires`, and `--limit-*` needs `--revalidate`. It refuses a session presenting both a JWT and a certificate. `--key` takes a file, not an https or JWKS URL. - -```bash -# Before -moq auth serve --key-dir keys/ ... -# After -moq auth serve --key-dir keys/ --revalidate 1m --expires 1d ... -``` +Pass `--revalidate 1m --expires 1d` to keep re-checking sessions. ## 0.11 -> 0.12 @@ -161,10 +103,4 @@ A renamed flag or environment variable is refused with its replacement named. ### `moq --listen` needs auth A listener refuses to start without `--auth-url` or `--auth-public` instead of accepting everyone. - -```bash -# Before -moq --listen '[::]:4443' ... import ts -# After -moq --listen '[::]:4443' --auth-public '**' ... import ts -``` +Pass `--auth-public '**'` to keep accepting everyone. diff --git a/rs/moq-clock/MIGRATE.md b/rs/moq-clock/MIGRATE.md index d2fa2069b8..c05e7ad07a 100644 --- a/rs/moq-clock/MIGRATE.md +++ b/rs/moq-clock/MIGRATE.md @@ -8,11 +8,4 @@ How to update your code for each breaking change, newest first. moq-clock 0.10 was the "CLOCK over QUIC" example binary. 0.11 is a library: logical instants, wall time, deadlines, and a deterministic simulator. -The clock example lives on as `cargo run -p moq-tokio --example clock`. - -```bash -# Before -cargo run -p moq-clock -- --url https://relay.example.com/anon --broadcast clock publish -# After -cargo run -p moq-tokio --example clock -- --connect https://relay.example.com/anon --broadcast clock publish -``` +The clock example lives on as `cargo run -p moq-tokio --example clock`, which takes `--connect` instead of `--url`. diff --git a/rs/moq-e2ee/MIGRATE.md b/rs/moq-e2ee/MIGRATE.md index 5ba07b8c40..1d05f563d9 100644 --- a/rs/moq-e2ee/MIGRATE.md +++ b/rs/moq-e2ee/MIGRATE.md @@ -8,24 +8,10 @@ How to update your code for each breaking change, newest first. The protected path is the opaque broadcast path; the publisher epoch is read from the route instead of a path suffix. On wires that do not carry an epoch, the application supplies it. - -```rust -// Before -let path = cred.path(name)?.join_epoch(Some(generation.epoch())); -let (_, epoch) = path.split_epoch(); -// After -let path = cred.path(name)?; // announce with Route::default().with_epoch(epoch) -let epoch = consumer.routed(&path).await.and_then(|route| route.epoch); -``` +Drop `join_epoch` and `split_epoch`: announce with `Route::default().with_epoch(epoch)`, and read it back with `consumer.routed(&path).await.and_then(|route| route.epoch)`. ### Timestamps are optional `Frame` and `Datagram` timestamps are `Option`, and the writers take `impl Into>`, so passing a `Timestamp` still compiles. A frame read from an untimed track has no timestamp. - -```rust -// Before -let ts: Timestamp = frame.timestamp; -// After -let ts: Option = frame.timestamp; -``` +Handle `None` where you read `frame.timestamp`. diff --git a/rs/moq-flate/MIGRATE.md b/rs/moq-flate/MIGRATE.md index 6195842b32..a9fdeef918 100644 --- a/rs/moq-flate/MIGRATE.md +++ b/rs/moq-flate/MIGRATE.md @@ -10,30 +10,12 @@ The opaque `snapshot` and `stream` tracks moved beside the codec, so `moq_binary `moq_binary::Error::Flate(e)` is the matching `moq_flate::Error` variant (`Decompress`, `TooLarge`). `moq_flate::Error` carries `moq_net::Error`, so it is no longer `PartialEq`. -```rust -// Before -use moq_binary::snapshot; -// After -use moq_flate::snapshot; -``` - ### Demand is read through `demand()` -```rust -// Before -if producer.is_used() {} -// After -if producer.demand().is_used() {} -``` +Call `producer.demand().is_used()` instead of `producer.is_used()`. ### Timestamps are explicit Snapshot and stream producers publish `at: None` untimed, so their track must be untimed; pass a timestamp on a timed track. Snapshot `modify(at)` and `mutate(at, f)` carry the caller's optional time. - -```rust -// Before -producer.update(bytes)?; -// After -producer.update(Timed::from(bytes).at(timestamp))?; -``` +Wrap each update as `Timed::from(bytes).at(timestamp)`. diff --git a/rs/moq-gst/MIGRATE.md b/rs/moq-gst/MIGRATE.md index 0741ecf194..cfde233592 100644 --- a/rs/moq-gst/MIGRATE.md +++ b/rs/moq-gst/MIGRATE.md @@ -9,10 +9,3 @@ How to update your pipelines for each breaking change, newest first. ### Rate estimates are `estimated-*-rate` The `moqsink` properties `estimated-send-bitrate` and `estimated-recv-bitrate` are `estimated-send-rate` and `estimated-recv-rate`, with no alias: an application or `gst-launch` line naming the old ones fails at runtime. - -```c -// Before -g_object_get(sink, "estimated-send-bitrate", &rate, NULL); -// After -g_object_get(sink, "estimated-send-rate", &rate, NULL); -``` diff --git a/rs/moq-hls/MIGRATE.md b/rs/moq-hls/MIGRATE.md index 9763fba13c..766df31bae 100644 --- a/rs/moq-hls/MIGRATE.md +++ b/rs/moq-hls/MIGRATE.md @@ -9,28 +9,12 @@ How to update your code for each breaking change, newest first. `export::Rendition::playlist(query)` is `playlist(range, query)`. Pass `None` for the live or history playlist. -```rust -// Before -let playlist = rendition.playlist(query).await?; -// After -let playlist = rendition.playlist(None, query).await?; -``` - ### Discontinuities are absolute sequences `export::segments::Segment::discontinuity` is a `u64` sequence, absolute within one `Broadcaster`, instead of a `bool` or a per-cursor count. Store the value with each segment and use the first retained one as `EXT-X-DISCONTINUITY-SEQUENCE`. A new `Broadcaster` starts a new sequence namespace: start a new recording, or map its sequences behind a discontinuity. -```rust -// Before -if segment.discontinuity { - sequence += 1; -} -// After -let sequence = segment.discontinuity; -``` - ### Playlists are bounded (#4978) Every timeline, durable ones included, lists a capped window of 256 segments. @@ -46,10 +30,4 @@ config.history = true; `Broadcaster::new` no longer awaits or fails. A rendition whose sibling broadcast ends now ends its playlist instead of rebinding to a replacement. - -```rust -// Before -let broadcaster = Broadcaster::new(source, config).await?; -// After -let broadcaster = Broadcaster::new(source, config); -``` +Drop the `.await?`. diff --git a/rs/moq-json/MIGRATE.md b/rs/moq-json/MIGRATE.md index f8d85b99f5..ba870ca1ad 100644 --- a/rs/moq-json/MIGRATE.md +++ b/rs/moq-json/MIGRATE.md @@ -6,27 +6,14 @@ How to update your code for each breaking change, newest first. ### Demand is read through `demand()` -```rust -// Before -if producer.is_used() {} -// After -if producer.demand().is_used() {} -``` +Call `producer.demand().is_used()` instead of `producer.is_used()`. ### Timestamps are explicit `moq_net::Timed

` drops its clock type parameter. Snapshot, stream, and window producers publish `at: None` untimed, so their track must be untimed; pass a timestamp on a timed track. Snapshot `modify(at)` and `mutate(at, f)` carry the caller's optional time through the edit guard. - -```rust -// Before -producer.update(&value)?; -let mut guard = producer.modify()?; -// After -producer.update(Timed::from(&value).at(timestamp))?; -let mut guard = producer.modify(Some(timestamp))?; -``` +Wrap each update as `Timed::from(&value).at(timestamp)`, and pass `Some(timestamp)` to `modify`. ## 0.3 -> 0.4 @@ -34,12 +21,4 @@ let mut guard = producer.modify(Some(timestamp))?; `compression: bool` is a `Compression` enum, and options that own a track live in each mode's `producer::Config` and `consumer::Config`. `lock()` is `modify()?`, and a guard that fails to publish on drop aborts the track (#3644). - -```rust -// Before -config.compression = true; -let mut value = producer.lock(); -// After -config.compression = Compression::Deflate; -let mut value = producer.modify()?; -``` +Set `Compression::Deflate` where you set `true`. diff --git a/rs/moq-msf/MIGRATE.md b/rs/moq-msf/MIGRATE.md index a85714961b..9ac0d723c8 100644 --- a/rs/moq-msf/MIGRATE.md +++ b/rs/moq-msf/MIGRATE.md @@ -8,9 +8,3 @@ How to update your code for each breaking change, newest first. The non-standard `stalled` track field is gone and no longer written. Drop it from any `Track` you build. - -```rust -// Before -track.stalled = Some(true); -// After: delete the assignment -``` diff --git a/rs/moq-relay/MIGRATE.md b/rs/moq-relay/MIGRATE.md index 34fc025942..e0cb27b5cf 100644 --- a/rs/moq-relay/MIGRATE.md +++ b/rs/moq-relay/MIGRATE.md @@ -10,27 +10,12 @@ Rolling a cluster across a release also follows the [upgrade guide](https://moq. A `[quic]` `max_streams` of 0 stops startup, as `[peer.quic]` already did, since zero streams wedge the connection. Remove it to keep the default. -```toml -# Before -[quic] -max_streams = 0 -# After: omit max_streams for the default -[quic] -``` - ### Auth roles carry `moq_auth::Access` (#5697) `auth::Token::publish` and `subscribe` are `moq_auth::Access`, so an embedder reads the patterns from `.path`. The relay refuses every grant with a bitrate cap, including one added in-band or at revalidation, because no transport meters it yet; HTTP `/fetch` and `/announced` always refuse caps. An embedder running its own gateway must enforce the cap or refuse the grant before forwarding media. -```rust -// Before -let patterns = &token.publish; -// After -let patterns = &token.publish.path; -``` - ### Workers default to unpinned (#5688) `runtime.pin` defaults to off, so two worker groups on one host no longer crowd onto the first cores. @@ -46,38 +31,17 @@ pin = true The default build has no iroh listener and no `--iroh-*` flags or `[iroh]` table. Build with `--features iroh` to keep it. -```bash -# Before -cargo install moq-relay -# After -cargo install moq-relay --features iroh -``` - ### The `noq` cargo feature is `quic` Rename it wherever you enable it; the QUIC stack is MoQ's own `moq-quic`. A qlog directory gets one file per endpoint instead of one per connection. -```bash -# Before -cargo build -p moq-relay --no-default-features --features noq -# After -cargo build -p moq-relay --no-default-features --features quic -``` - ### A QUIC load-balancer nonce needs its server id (#5114) `listen.lb_nonce` (`--listen-quic-lb-nonce`, `MOQ_LISTEN_QUIC_LB_NONCE`) without `lb_id` stops startup. Setting `lb_id` or `lb_nonce` together with `load_balancer` also stops; `lb_id` used to win. Choose either `load_balancer` or the `lb_id` and `lb_nonce` pair. -```bash -# Before -moq-relay --listen-quic-lb-nonce 8 ... -# After -moq-relay --listen-quic-lb-id 0a --listen-quic-lb-nonce 8 ... -``` - ### Settings no listener reads stop startup (#5006) A stream-only relay (TCP or Unix, no `--listen`) refuses the QUIC-only `--listen-preferred-v4` / `-v6`, `--listen-quic-lb-id`, and pinned `tls.peers`. @@ -85,26 +49,12 @@ It also refuses `--listen-tls-cert`, `-key`, or `-generate` unless `--listen-tcp `--listen-unix-allow-*` needs `--listen-unix-bind`, and `web.https.cert`, `key`, and `root` need `web.https.listen`. Each used to be ignored: drop it, or add the listener it configures. -```bash -# Before -moq-relay --listen-tcp-bind 127.0.0.1:4443 --listen-preferred-v4 203.0.113.1:443 ... -# After -moq-relay --listen-tcp-bind 127.0.0.1:4443 ... -``` - ### `--cluster-mesh` and `--cluster-linger` are unknown flags (#4818) They, TOML `mesh` and `linger`, and `MOQ_CLUSTER_MESH` / `MOQ_CLUSTER_LINGER` are no longer read. List every peer with `--cluster-connect` or `--cluster-connect-api`. In Rust, `cluster::Config` has no `mesh` or `linger` field. -```bash -# Before -moq-relay --cluster-mesh --cluster-linger 10s --cluster-connect https://root.example/ ... -# After -moq-relay --cluster-connect https://root.example/,https://b.example/ ... -``` - ### A route without a publisher keeps hop 0 (#4969) A route that names no publisher keeps its 0 instead of gaining a random hop. @@ -112,27 +62,12 @@ A route that names no publisher keeps its 0 instead of gaining a random hop. ### Embedding: auth takes the client-CA answer (#4912) `auth::Config::validate` and `init` take `client_ca: bool`, whether any listener verifies client certificates, and `validate_client_ca` is gone. - -```rust -// Before -config.auth.validate()?; -config.auth.validate_client_ca()?; -// After -config.auth.validate(client_ca)?; -``` +Pass `client_ca` to `validate` and delete the `validate_client_ca` call. ### Embedding: io\_uring workers bind from the server config (#5312) `uring::Workers::bind` takes a `moq_tokio::server::Config`, so generated and rotating certificates work in both worker modes. - -```rust -// Before -let workers = uring::Workers::bind(&listen, &quic, worker)?; -// After -let mut server = moq_tokio::server::Config::default(); -(server.listen, server.quic) = (listen, quic); -let workers = uring::Workers::bind(server, worker)?; -``` +Set `listen` and `quic` on a `moq_tokio::server::Config` and pass it to `bind`. ## 0.16 -> 0.17 @@ -142,13 +77,6 @@ A relay dials only the peers it lists or finds on the LAN, never a URL learned f `--cluster-mesh` and TOML `mesh` are refused by name: list every peer with `--cluster-connect` or `--cluster-connect-api`. The `/nodes` endpoint lists only peers this relay dialed. -```bash -# Before -moq-relay --cluster-mesh --cluster-connect https://root.example/ ... -# After -moq-relay --cluster-connect https://root.example/,https://b.example/ ... -``` - ## 0.15 -> 0.16 ### 0.14 auth behavior is restored (#4319) @@ -158,14 +86,7 @@ moq-relay --cluster-connect https://root.example/,https://b.example/ ... - A token's `iss`, `sub`, and `jti` are ignored and `nbf` is enforced; any other claim, `aud` and `cluster` included, refuses the token. - A relay on `--auth-public` refuses a session presenting a token, including a peer's `cluster.token`, and refuses to start with a client CA. -```toml -# Before -[auth] -key_dir = "keys/" -# After -[auth] -url = "http://127.0.0.1:4440" # moq auth serve --key-dir keys/ -``` +Replace `key_dir` with `moq auth serve --key-dir keys/` and point `[auth]` `url` at it, such as `http://127.0.0.1:4440`. ## 0.14 -> 0.15 @@ -198,9 +119,6 @@ The flag-by-flag mapping is in [Migrating from the relay flags](https://moq.dev/ A verified client certificate admits nothing on its own; the auth server grants it. ```bash -# Before -moq-relay --auth-key-dir keys/ --auth-public anon -# After moq auth serve --key-dir keys/ --public-publish 'anon/**' --public-subscribe 'anon/**' & moq-relay --auth-url http://127.0.0.1:4440 ``` @@ -212,22 +130,8 @@ This applies to `--auth-public`, TOML `public`, and `[auth.public]`, which is no A public or mTLS pattern with no wildcard refuses to start, naming the subtree to write. JWT `publish` and `subscribe` claims are patterns too, so a token granting `alice` covers only `alice`. -```bash -# Before -moq-relay --auth-public anon ... -# After -moq-relay --auth-public 'anon/**' ... -``` - ### Embedding -`Relay` fields are private (#3638): clone the handles you need, mount routes, then call `Relay::run`. +`Relay` fields are private (#3638): clone the handles you need, such as `relay.cluster().origin.clone()`, mount routes, then call `Relay::run`. `Cluster::with_cache` moved to `cluster::Options`. The `quinn` and `quiche` features and the backend setting are gone (#3811). - -```rust -// Before -let origin = relay.cluster.origin.clone(); -// After -let origin = relay.cluster().origin.clone(); -``` diff --git a/rs/moq-rtc/MIGRATE.md b/rs/moq-rtc/MIGRATE.md index 044ee3328b..144d254252 100644 --- a/rs/moq-rtc/MIGRATE.md +++ b/rs/moq-rtc/MIGRATE.md @@ -8,10 +8,3 @@ How to update your code for each breaking change, newest first. `Client::publish` and `Client::subscribe` return `Result` instead of `Result<()>`. Read per-peer byte counts with `source.stats()`, or discard it. - -```rust -// Before -client.subscribe(url, broadcast).await?; -// After -let _source = client.subscribe(url, broadcast).await?; -``` diff --git a/rs/moq-video/MIGRATE.md b/rs/moq-video/MIGRATE.md index dff7266a14..8c1b2a21e3 100644 --- a/rs/moq-video/MIGRATE.md +++ b/rs/moq-video/MIGRATE.md @@ -7,97 +7,40 @@ How to update your code for each breaking change, newest first. ### `Color` is a struct of matrix, range, primaries and transfer The four variants become constants plus a range, and the new `color` module holds `Matrix`, `Range`, `Primaries` and `Transfer`. -`Bt601Limited` is `Color::BT601`, `Bt709Limited` is `Color::BT709`, and the `Full` variants set `range: Range::Full`. -Match on the field you need instead of the variant. +`Bt601Limited` is `Color::BT601`, `Bt709Limited` is `Color::BT709`, and the `Full` variants set `range: Range::Full`, as in `Color { range: Range::Full, ..Color::BT709 }`. +Match on the field you need instead of the variant, such as `color.matrix == Matrix::Bt709`. BT.601 now names its primaries (SMPTE 170M), and BT.2020 with PQ or HLG is representable. -```rust -// Before -let color = Color::Bt709Full; -let hd = matches!(color, Color::Bt709Limited | Color::Bt709Full); -// After -use moq_video::color::{Matrix, Range}; -let color = Color { range: Range::Full, ..Color::BT709 }; -let hd = color.matrix == Matrix::Bt709; -``` - ### `Encoder::encode` refuses a frame in another space Decoded and resized frames now report their source's space from `Surface::color`, and `Encoder::encode` fails with `Error::ColorMismatch` when a frame's space differs from the one the encoder signals, rather than mislabelling it. -Open the encoder with the first frame's space, and reopen it when the space changes. - -```rust -// Before -let encoder = Encoder::new(&encode::Config::new(width, height, rate))?; -// After -let mut config = encode::Config::new(width, height, rate); -config.color = frame.surface.color(); -let encoder = Encoder::new(&config)?; -``` +Set `encode::Config::color` to the first frame's `frame.surface.color()`, and reopen the encoder when the space changes. ### `encode::Producer` takes a catalog reservation (#5715) `encode::Producer::new` and `with_track` take a `moq_mux::catalog::Reserved` instead of the `catalog::Producer`, so the caller declares the encoder's GOP as the timeline hint. - -```rust -// Before -let producer = encode::Producer::new(broadcast, catalog, rendition)?; -// After -let producer = encode::Producer::new(broadcast, catalog.reserve().with_timeline_hint(gop), rendition)?; -``` +Pass `catalog.reserve().with_timeline_hint(gop)` instead of the catalog. ### Capture takes one `encode::Capture`, and `Control::new` returns the driver `publish_capture(broadcast, catalog, capture, encode, clock)` is `publish_capture(broadcast, catalog, encode::Capture)`. `Capture` has no clock: the driver stamps on `catalog.clock()`. `encode::Control::new` returns the control handle and the driver; dropping the last `Control` cancels an in-flight open or encode. - -```rust -// Before -publish_capture(broadcast, catalog, capture, encode, clock).await?; -// After -let mut options = encode::Capture::default(); -options.capture = capture; -options.encode = encode; -publish_capture(broadcast, catalog, options).await?; -``` +Build it from `encode::Capture::default()`, setting `capture` and `encode`. ### `Control::cut` returns a `Result` `cut()` fails with `Error::CutUnsupported` once the driver knows its backend cannot force a keyframe, and the publish carries on at its cadence. It counts cadence keyframes too, so a request may be served without forcing one. - -```rust -// Before -control.cut(); -// After -if let Err(Error::CutUnsupported(_)) = control.cut() { - // wait for the next cadence keyframe -} -``` +On `Error::CutUnsupported`, wait for the next cadence keyframe. ### `Encoded` marks keyframes (#4295) `encode::Encoded::new` takes a third `keyframe: bool`, also readable as `Encoded::keyframe`. -```rust -// Before -let encoded = Encoded::new(payload, timestamp); -// After -let encoded = Encoded::new(payload, timestamp, keyframe); -``` - ### Producers drop `tick`, `idle`, and `observe_lag` (#4915) -`encode::Producer` no longer takes liveness hints; disable a rendition through the catalog's `enabled` flag instead. - -```rust -// Before -producer.tick()?; -producer.observe_lag(lag)?; -producer.idle()?; -// After: delete the calls -``` +`encode::Producer` no longer takes liveness hints, so delete the calls; disable a rendition through the catalog's `enabled` flag instead. ### External Vulkan and DMA-BUF inputs (#4975) @@ -106,22 +49,8 @@ On Linux, `Surface::Vulkan` no longer requires the `nvidia` feature. `vulkan::Importer`, `vulkan::ImportError`, `cuda::Converter`, and `cuda::Slot` leave the public API; `Surface::Cuda` stays for NVDEC output. `DmaBuf::new` takes an owned FD, a `DmaBufLayout`, and a release guard, and an external DMA-BUF refuses CPU readback. Bind eager open to an external device with `encode::Config::input`; an unsupported device is refused instead of falling back to software. - -```rust -// Before -let importer = frame::vulkan::Importer::new(ordinal, capacity)?; -let slot = importer.import(frame::vulkan::Handles::new(memory, timeline), image, owner)?; -// After -let slot = frame::vulkan::Slot::new(frame::vulkan::Handles { memory, timeline }, image, owner)?; -``` +Replace `Importer::new` and `import` with `frame::vulkan::Slot::new(frame::vulkan::Handles { memory, timeline }, image, owner)`. ### Decoder staleness is `max_delay` `decode::Options::max_age` is `max_delay`. - -```rust -// Before -options.max_age = delay; -// After -options.max_delay = delay; -``` From 1438bdfcaa113161b12c013d75d1d0e42fac18e0 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 15:46:51 -0700 Subject: [PATCH 39/40] docs(migrate): lb_id alone is a valid load-balancer setting Co-Authored-By: Claude Opus 5.5 --- rs/moq-relay/MIGRATE.md | 2 +- rs/moq-tokio/MIGRATE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/moq-relay/MIGRATE.md b/rs/moq-relay/MIGRATE.md index e0cb27b5cf..8145e26816 100644 --- a/rs/moq-relay/MIGRATE.md +++ b/rs/moq-relay/MIGRATE.md @@ -40,7 +40,7 @@ A qlog directory gets one file per endpoint instead of one per connection. `listen.lb_nonce` (`--listen-quic-lb-nonce`, `MOQ_LISTEN_QUIC_LB_NONCE`) without `lb_id` stops startup. Setting `lb_id` or `lb_nonce` together with `load_balancer` also stops; `lb_id` used to win. -Choose either `load_balancer` or the `lb_id` and `lb_nonce` pair. +Set `load_balancer`, or `lb_id` alone (nonce 8), or `lb_id` with `lb_nonce`. ### Settings no listener reads stop startup (#5006) diff --git a/rs/moq-tokio/MIGRATE.md b/rs/moq-tokio/MIGRATE.md index 52a53ae6a4..d408979627 100644 --- a/rs/moq-tokio/MIGRATE.md +++ b/rs/moq-tokio/MIGRATE.md @@ -62,7 +62,7 @@ The reconnect counter is `connects()`; rename `epoch()` calls. `listen.lb_nonce` without `lb_id` fails to start, so set `lb_id` next to it. Setting `lb_id` (or `lb_nonce`) together with `load_balancer` also fails; `lb_id` used to win silently. -Set one of them. +Set `load_balancer`, or `lb_id` alone (nonce 8), or `lb_id` with `lb_nonce`. ### Settings no listener reads stop startup (#5006) From bc4f487974959df452d736c90b952984c66d0148 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 17 Oct 2026 16:53:36 -0700 Subject: [PATCH 40/40] docs(upgrade): upgrade verifiers before issuing capped grants Co-Authored-By: Claude Opus 5.5 --- doc/setup/upgrade.md | 4 ++++ js/auth/MIGRATE.md | 1 + rs/moq-auth/MIGRATE.md | 1 + 3 files changed, 6 insertions(+) diff --git a/doc/setup/upgrade.md b/doc/setup/upgrade.md index 51caad6081..db894231a0 100644 --- a/doc/setup/upgrade.md +++ b/doc/setup/upgrade.md @@ -39,6 +39,10 @@ These land with the next breaking release. - **Auth requests name `webtransport`** (#5293). Relays report a WebTransport session as `webtransport` instead of `quic`, and older auth servers reject the new value, so upgrade auth servers before relays. +- **Capped grants need upgraded verifiers** (#5697). A token whose `publish` + or `subscribe` carries a `rate` cap encodes that role as an object, which + released verifiers reject, so upgrade every auth server and verifier before + issuing capped tokens. - **Archive timelines declare a duration** (#5715). Each catalog `archive.timelines` entry is an object instead of a track name. Neither side reads the other's shape, and a catalog it can't read keeps its live media but diff --git a/js/auth/MIGRATE.md b/js/auth/MIGRATE.md index 303c3683bf..d1512bd3e8 100644 --- a/js/auth/MIGRATE.md +++ b/js/auth/MIGRATE.md @@ -9,6 +9,7 @@ How to update your code for each breaking change, newest first. Parsed `Claims`, `Grant`, and `authorize`'s permissions hold `publish` and `subscribe` as `Access`: path patterns plus an optional per-connection `rate` cap. Read the patterns from `.path`, as in `permissions.publish.path.matches(path)`. `Key.sign` still takes pattern strings, but a hand-built `Claims` value must come from `ClaimsSchema.parse`. A gateway must enforce a cap or refuse the grant before admitting media. +Released verifiers reject a capped grant, so upgrade every verifier before an issuer sets `rate`. ### A token must carry its key's `kid` (#3943) diff --git a/rs/moq-auth/MIGRATE.md b/rs/moq-auth/MIGRATE.md index acf6c6b316..5d7dc6c794 100644 --- a/rs/moq-auth/MIGRATE.md +++ b/rs/moq-auth/MIGRATE.md @@ -9,6 +9,7 @@ How to update your code for each breaking change, newest first. `publish` and `subscribe` on `Claims`, `Permissions`, and `Grant` are `Access`, which holds the path patterns and an optional per-connection `Rate` cap. Read the patterns from `.path`; constructors and `Claims::with_publish` / `with_subscribe` take anything `Into`, such as a `Pattern`, `Patterns`, or array, so collect a pattern iterator first. A capped role serializes as `{"path":"alice/**","rate":8000000}`, and key scopes refuse caps. +Released verifiers reject that form, so upgrade every verifier before issuing capped tokens. A gateway that embeds this crate must enforce a cap or refuse the grant before admitting media. ### Gateway transports and key sets are non-exhaustive (#3943)