diff --git a/AGENTS.md b/AGENTS.md index 6337f888..fb53b562 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,7 @@ * System-test CLI: `rust/telepathy-cli` * Flutter: `lib/` * Docs: `docs/` +* Documented solutions: `docs/solutions/` stores searchable architecture and troubleshooting learnings with YAML metadata; `docs/CONCEPTS.md` defines shared project vocabulary. Both are relevant when implementing or debugging in documented areas. * System tests: `system-tests/` * Generated, never read/edit: `lib/core/rust/*`, `frb_generated.rs` @@ -33,6 +34,10 @@ dart format . Format only after cleanup is complete. +## Rust Style + +- Import `VideoWorkerStartup` at file top instead of spelling inline paths such as `crate::internal::video::VideoWorkerStartup::Failed`; rename the import only when it conflicts with another name. + ## Rust Tests Prefer nextest. Use `cargo test` only when required, and state why. diff --git a/analysis_options.yaml b/analysis_options.yaml index 1dcf1eff..03210a08 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -12,6 +12,7 @@ include: package:flutter_lints/flutter.yaml analyzer: exclude: - lib/src/** + - lib/core/rust/lib.dart linter: # The lint rules applied to this project can be customized in the diff --git a/docs/CONCEPTS.md b/docs/CONCEPTS.md index 8b16698b..b12758e7 100644 --- a/docs/CONCEPTS.md +++ b/docs/CONCEPTS.md @@ -29,3 +29,24 @@ An attempt remains active while direct dialing or an associated inbound candidat The observable per-peer state that tells a call request whether a direct session already exists, a direct session attempt may still publish one, or no session can be expected. Availability changes wake waiting call requests; a request only acquires call ownership after the session is published. + +## Video Sessions + +### Video Session +A peer-scoped exchange that coordinates one display-media source, its lifecycle controls, and its media transport without owning the underlying call. + +### Video Attempt +One incarnation of a Video Session, scoped so late asynchronous work from an earlier incarnation cannot affect a later use of the same peer slot. + +### Video Slot +The per-peer lifecycle boundary that admits at most one Video Attempt and remains occupied until that attempt is fully finished. + +### Joined Teardown +The terminal process that keeps a Video Slot unavailable until all work for its current Video Attempt has finished. + +### Runtime Video Capability +The current adapter-probed set of video sources and media formats that the running target can send or receive. It is distinct from serializable video configuration, which may name a device or encoder that is no longer available or has no implemented command path. + +## Relationships + +A Video Slot owns one Video Attempt at a time. Joined Teardown preserves that ownership until the attempt has fully finished, after which the slot may admit a replacement attempt. diff --git a/docs/plans/2026-07-17-001-refactor-generic-video-sessions-plan.md b/docs/plans/2026-07-17-001-refactor-generic-video-sessions-plan.md index 1311953a..fc781cae 100644 --- a/docs/plans/2026-07-17-001-refactor-generic-video-sessions-plan.md +++ b/docs/plans/2026-07-17-001-refactor-generic-video-sessions-plan.md @@ -10,7 +10,7 @@ deepened: 2026-07-17 ## Summary -Replace screenshare-specific lifecycle, signaling, transport, and bridge contracts with one typed video-session architecture. Preserve current desktop FFmpeg capture/playback behavior as the first statically selected platform adapter, while making future source and platform additions local to domain/configuration and adapter boundaries. +Replace screenshare-specific lifecycle, signaling, transport, and bridge contracts with one typed video-session architecture. Preserve the implemented Windows FFmpeg capture/playback behavior as the first statically selected platform adapter, while making future source and platform additions local to domain/configuration and adapter boundaries. --- @@ -24,16 +24,16 @@ The current flow also has no direct screenshare tests and represents ownership w ## Requirements -- R1. Existing Windows, macOS, and Linux screensharing must continue using FFmpeg/ffplay with the same supported devices, encoders, command behavior, framed byte path, settings, and user interaction. +- R1. Preserve the implemented Windows FFmpeg capture/playback command behavior, framed byte path, settings, and user interaction. The desktop adapter may compile on Windows, macOS, and Linux, but a capture mode is advertised only where that target has an implemented command path and fresh runtime capability result. macOS and Linux capture are unavailable until implemented and tested command paths exist. - R2. Core lifecycle, signaling, transport framing, session identity, roles, phases, and terminal reasons must use generic video-session vocabulary rather than screenshare or FFmpeg concepts. - R3. A single per-peer coordinator must own video-session state and Iroh stream orchestration; platform adapters must not own signaling or transport negotiation. -- R4. Platform implementation selection must be compile-time and narrow. Runtime checks determine capabilities and availability, not which backend type is loaded. -- R5. Adding a future platform implementation must be limited to a target adapter, static selection, and any source-neutral capability/configuration representation it genuinely needs; it must not require coordinator, transport, wire lifecycle, or Flutter event redesign. +- R4. Platform implementation selection must be compile-time and narrow: the build selects either the desktop FFmpeg adapter or the unsupported adapter. Runtime checks determine only truthful capabilities and availability, never which backend type is loaded. +- R5. Adding a future platform implementation must be limited to a target adapter, static selection, implemented command paths, fresh capability probing, and any source-neutral capability/configuration representation it genuinely needs. It must not require coordinator, transport, wire lifecycle, or Flutter event redesign. - R6. Adding a future video source must be limited to an explicit source/configuration variant, wire serialization coverage, and adapter support; it must not require a parallel lifecycle or transport path. - R7. Start, ready, active, stop, rejection, failure, teardown, and restart behavior must be typed, idempotent, generation-safe, and observable on both peers. - R8. Video wire messages and media preambles must be source-neutral, versioned, identity-checked, and explicitly framed. Control fields, preambles, and media payloads must have distinct inbound/outbound limits enforced before allocation or adapter delivery. - R9. FFmpeg processes, pipes, Iroh streams, and worker tasks must follow one deadlock-safe cleanup order on every terminal path and finish cleanup before the per-peer slot returns to idle. -- R10. Rust-native and Flutter-facing APIs must expose the same unconditional typed video contract on every target; platform unavailability is returned as data or a typed outcome. +- R10. Rust-native and Flutter-facing APIs must expose the same unconditional typed video contract on every target. Persisted or user-selected modes that are unavailable at start return a typed outcome, never a panic; platform unavailability is returned as data or a typed outcome. - R11. Existing Flutter screenshare controls and sending/receiving state must preserve their behavior while consuming generic video events. - R12. Characterization, domain, transport, two-peer integration, Flutter state, and repeated teardown tests must protect the architecture and existing call/session behavior. - R13. Media flow must preserve end-to-end backpressure and complete byte delivery with bounded in-flight memory, no unbounded per-frame queues/tasks, and lifecycle-level rather than frame-rate tracing. @@ -63,8 +63,10 @@ The current flow also has no direct screenshare tests and represents ownership w ### Relevant Code and Patterns -- `rust/telepathy-core/src/internal/helpers.rs`: current sender/receiver branch combines callback, signaling, Iroh stream setup, config lookup, and FFmpeg invocation. -- `rust/telepathy-core/src/internal/screenshare.rs`: current FFmpeg capability discovery, command construction, capture, playback, framing, and process cleanup baseline. +- `rust/telepathy-core/src/internal/video.rs`: generic coordinator, negotiation, lifecycle, and adapter startup boundary. +- `rust/telepathy-core/src/internal/video/platform.rs`: canonical internal device/encoder/decoder vocabulary and compile-time selected adapter boundary. +- `rust/telepathy-core/src/internal/video/platform/desktop_ffmpeg.rs`: fresh FFmpeg capability probing, implemented Windows capture commands, local decoder selection, playback, and process cleanup. +- `rust/telepathy-core/src/internal/video/platform/unsupported.rs`: typed unavailable implementation for unsupported targets. - `rust/telepathy-core/src/internal/state.rs`: peer-owned `SessionState` and teardown ownership; the new video slot belongs here. - `rust/telepathy-core/src/internal/messages.rs`: current `ScreenshareHeader` wire contract and control-message serialization. - `rust/telepathy-core/src/internal/core.rs`: peer session map, incoming control dispatch, and generation-safe session patterns. @@ -76,7 +78,7 @@ The current flow also has no direct screenshare tests and represents ownership w ### Institutional Learnings -- No `docs/solutions/` or `STRATEGY.md` exists. Repository guidance instead requires narrow platform code, source-driven Flutter Rust Bridge generation, real integration coverage, and stress validation for session/network teardown. +- Repository guidance identifies `docs/solutions/` and `docs/CONCEPTS.md` as searchable project knowledge. It also requires narrow platform code, source-driven Flutter Rust Bridge generation, real integration coverage, and stress validation for session/network teardown. - Public `telepathy-core` changes require running exactly `flutter_rust_bridge_codegen generate`; generated bridge files must never be edited manually. - Core session/call/network/teardown work requires the main Rust pass and repeated core integration stress coverage before handoff. System tests must be run manually in WSL. @@ -95,25 +97,25 @@ The current flow also has no direct screenshare tests and represents ownership w | Decision | Choice and rationale | |---|---| -| Ownership boundary | A generic per-peer video coordinator owns lifecycle, signaling, Iroh stream setup, framing, cancellation, and task joining. Platform adapters own preparation, capture/playback, and process/resource cleanup only. | +| Ownership boundary | A generic per-peer video coordinator owns lifecycle, signaling, Iroh stream setup, framing, cancellation, task joining, and the active resource bundle. Platform adapters own preparation, capture/playback, and exclusively clean up their child processes and pipes through their adapter session. | | Adapter granularity | Use a session adapter rather than raw source/sink callbacks. FFmpeg preparation determines negotiated format and owns a child process, pipes, and cleanup as one unit. | -| Platform selection | Compile exactly one private adapter type with target `cfg`; probe binaries, devices, encoders, decoders, and permissions at runtime. Avoid dynamic registries and boxed async backends. | +| Platform selection | Compile exactly one private adapter with target `cfg`: the desktop FFmpeg adapter on Windows, macOS, and Linux, or the unsupported adapter elsewhere. Runtime probing reports capabilities only. A desktop build must not advertise a display capture mode unless its target has an implemented command path and the fresh probe confirms it. Avoid dynamic registries and boxed async backends. | | Extensibility model | Use closed, typed source/configuration and lifecycle variants. Future additions become compiler-visible and cannot silently fall through string matching. | -| Active-session cardinality | Keep one video slot per peer for this scope. The slot contains identity, generation, role, phase, cancellation, and owned task/session handles. | +| Active-session cardinality | Keep one video slot per peer for this scope. Its active resource bundle contains the matching identity and generation, role, phase, cancellation, adapter session, and every transport-worker handle, including `open_uni` and `accept_uni`; neither worker may be detached. Resources install only while the slot still matches that identity and generation. A stale installer immediately cancels and joins its uninstalled bundle. | | Negotiation | Use typed offer, ready, reject, and stop controls. Existing screenshare remains automatically accepted when locally supported; readiness is protocol-internal and creates no new UX. | | Correlation and crossed starts | Each offer carries an initiator-created wire session identity echoed by ready/reject/stop/preamble. Local generation is only a stale-completion guard. Simultaneous offers resolve deterministically from peer identity; the loser cancels local preparation, emits one local terminal observation, then processes the winning remote identity. | | Wire migration | Replace `ScreenshareHeader` in one coordinated cutover. Do not retain parallel legacy signaling or coordinator paths. | | Media descriptor | Send stable source kind, codec/format metadata, dimensions, framing revision, and session identity. Never send local device IDs, FFmpeg options, or platform objects. | | Stream association | Write a bounded video preamble immediately after opening the uni-stream; validate protocol revision and session identity before adapter startup or payload delivery. | | Stream acceptance | Only an accepted matching remote offer may arm one slot-owned `accept_uni`. That wait races cancellation, negotiation timeout, and teardown; no concurrent uni-stream acceptor exists for that connection in this scope. | -| Cancellation and cleanup | Replace `Arc` ownership with durable cancellation. Coordinator owns transport and adapter-session handles; adapter session exclusively owns child/pipes. Cleanup cancels, unblocks/closes I/O, awaits adapter cleanup and transport workers, then emits terminal state and releases the matching slot. | +| Cancellation and cleanup | Replace `Arc` ownership with durable cancellation. `cancel_and_join` claims only the matching reservation into `Stopping` without clearing it, cancels, closes or resets I/O, then joins the adapter session and every transport worker outside the slot lock. It emits exactly one terminal observation, then clears the matching slot only after joins. `call()`, `call_controller`, and all session teardown paths await this operation before returning. | | Stream termination | One transport I/O owner resolves cancellation. Clean EOF intentionally finishes; stop, protocol failure, or interrupted framing resets/abandons the stream. A partially written preamble/frame is never resumed or reused. | | Backpressure | Preserve the current direct read-then-send pressure chain with bounded in-flight media memory. No unbounded media channel, per-frame task spawning, or ignored partial child-stdin write is permitted. | | Frontend contract | Core owns stopping and cleanup. Flutter receives typed lifecycle observations and issues identity-aware stop requests; callbacks never own correctness. | -| Public parity | Native and Flutter surfaces share lifecycle request/stop/events and generic capabilities. Platform configuration ownership may differ internally, but cannot change those public session semantics. | -| Terminal observations | Each peer emits exactly one terminal observation for each resolved wire session identity, only after its own cleanup. Explicit control is best-effort; absent peer control maps to a local transport-ended reason with deterministic precedence and identity-based deduplication. | +| Public parity | Native and Flutter surfaces share lifecycle request/stop/events and generic capabilities. Platform configuration ownership may differ internally, but cannot change those public session semantics or turn unavailable persisted/user-selected configuration into a panic. | +| Terminal observations | Each peer emits exactly one terminal observation for each resolved wire session identity, only after `cancel_and_join` finishes its matching resources. Explicit control is best-effort; absent peer control maps to a local transport-ended reason with deterministic precedence and identity-based deduplication. Stale completion or installation cleans up its own resources without observing or clearing a newer slot. | | Failure scope | Video failures end only the affected video session unless the underlying peer/call transport itself has ended. | -| Configuration | Coordinator accepts only a source request and generic capability result. Selected adapter reads validated source-scoped local settings through the facade; unavailable, receive-only, and send-capable states remain generic, and FFmpeg data never enters coordinator or wire types. | +| Configuration | Coordinator accepts only a source request and generic capability result. The internal `Device`, `Encoder`, and `Decoder` vocabulary remains canonical and serializable for adapter-owned settings and probes. Selected adapters read validated source-scoped local settings through the facade; capabilities come from a fresh runtime probe, not persistence. A receiver selects the first locally probed decoder compatible with the negotiated format. Unavailable, receive-only, and send-capable states remain generic, and FFmpeg data never enters coordinator or wire types. | --- @@ -173,7 +175,7 @@ flowchart TB Coordinator --> Adapter ``` -On desktop targets the selected adapter is FFmpeg-backed; on unsupported targets it is the unavailable implementation. Both are never runtime alternatives in one build. +On desktop targets the selected adapter is FFmpeg-backed; on unsupported targets it is the unavailable implementation. Both are never runtime alternatives in one build. The desktop implementation advertises display capture only for targets with an implemented capture command path, so compiling the adapter does not claim that macOS or Linux capture works. ```mermaid stateDiagram-v2 @@ -184,23 +186,24 @@ stateDiagram-v2 WaitingReady --> Starting: ready and stream association PreparingRemote --> Starting: adapter prepared and preamble validated Starting --> Active: transport and adapter live - Offering --> Stopping: reject, timeout, stop, or teardown - WaitingReady --> Stopping: reject, timeout, stop, or teardown - PreparingRemote --> Stopping: invalid, unsupported, stop, or teardown - Starting --> Stopping: failure, stop, or teardown - Active --> Stopping: local stop, remote stop, EOF, failure, or teardown - Stopping --> Idle: resources joined and generation still matches + Offering --> Stopping: matching reservation claimed + WaitingReady --> Stopping: matching reservation claimed + PreparingRemote --> Stopping: matching reservation claimed + Starting --> Stopping: matching reservation claimed + Active --> Stopping: matching reservation claimed + Stopping --> Idle: cancel, close/reset I/O, join, observe once, then clear matching slot ``` Control and transport sequencing: 1. Local start atomically reserves the peer's idle slot and generation. 2. Sender adapter preparation produces a source-neutral media descriptor without opening Iroh media transport. -3. Offer is validated and auto-accepted by the receiver only when its selected adapter reports support; ready or typed rejection returns over control signaling. +3. Offer is validated and auto-accepted by the receiver only when its selected adapter reports fresh receive support for the negotiated format. Receiver startup selects the first compatible decoder in fresh local probe order; ready or typed rejection returns over control signaling. 4. Receiver arms one cancellation-aware `accept_uni` only for the accepted offer. Sender opens one uni-stream and immediately writes the versioned identity preamble before media bytes. 5. Receiver validates the wire session identity before adapter startup. Local generation never crosses the wire. -6. Coordinator starts adapter I/O, preserves bounded backpressure, publishes active observations, and supervises all terminal causes through one cleanup path. -7. Stop is explicit over control signaling and reinforced by stream closure; either signal is identity-deduplicated and idempotent. +6. Coordinator installs the adapter session and every `open_uni` or `accept_uni` worker only if the slot still matches its identity and generation; stale completions cancel and join their own resources without touching a newer slot. +7. Coordinator starts adapter I/O, preserves bounded backpressure, publishes active observations, and supervises all terminal causes through one cleanup path. +8. Stop is explicit over control signaling and reinforced by stream closure; either signal claims the matching reservation into `Stopping`, cancels, closes or resets I/O, joins all resources outside the lock, emits one terminal observation, and only then clears the matching slot. --- @@ -225,9 +228,7 @@ flowchart TB U4 --> U3 U3 --> U9 U4 --> U9 - U4 --> U5 - U3 --> U6 - U9 --> U6 + U9 --> U5 U5 --> U6 U6 --> U10 U10 --> U7 @@ -352,7 +353,7 @@ flowchart TB ### U3. Build the Video Slot and Control Lifecycle -**Goal:** Establish one lifecycle owner for per-peer video state and control-message transitions before attaching media transport. +**Goal:** Establish one lifecycle owner for per-peer video state, matching resource reservations, and control-message transitions before attaching media transport. **Requirements:** R3, R7, R9 @@ -369,11 +370,13 @@ flowchart TB - Test support: `rust/telepathy-core/tests/core_integration_test/common.rs` **Approach:** -- Replace `stop_screenshare` with a typed video slot whose non-idle states carry identity, generation, role, durable cancellation, and owned task/session handles. +- Replace `stop_screenshare` with a typed video slot whose non-idle reservation carries identity, generation, role, durable cancellation, and an optional active resource bundle. +- Define the bundle boundary now: only a matching identity and generation may install an adapter session or worker handle; a stale installation or completion must cancel and join its own resources without clearing, observing, or replacing the current slot. +- Define `cancel_and_join` as the coordinator-facing terminal operation. It claims the matching reservation into `Stopping` without clearing it, so U9 can add joined adapter and transport cleanup without a teardown race. - Route local starts and incoming video controls through one coordinator. Remove `OutputHelper::start_screenshare` branching after equivalent behavior is covered. - Implement legal offer/ready/reject/stop transitions, auto-accept policy, crossed-offer resolution, and identity/generation deduplication without yet moving framed media. - Define terminal-reason precedence and the invariant of one post-cleanup terminal observation per local peer and wire session identity. -- Converge local stop, remote stop, reject, negotiation timeout, session removal, manager restart, call end, and shutdown on one idempotent slot transition. +- Converge local stop, remote stop, reject, negotiation timeout, session removal, manager restart, call end, and shutdown on one identity- and generation-matched idempotent slot transition; `call()`, `call_controller`, and session teardown await `cancel_and_join`. **Patterns to follow:** - Existing cancellation and task helpers in `rust/telepathy-core/src/internal/utils.rs`. @@ -385,10 +388,11 @@ flowchart TB - Error path: reject, readiness timeout, callback failure, and peer control closure emit deterministic local terminal outcomes and release ownership. - Concurrency: duplicate start, duplicate ready, duplicate stop, crossed start, and simultaneous local/remote stop remain idempotent. - Edge case: stop during preparation and readiness wait cannot hang or emit duplicate terminal observations. -- Integration: call end, session replacement, manager restart, and shutdown cancel the current generation while late completion cannot clear a newer session. +- Integration: call end, `call_controller`, session replacement, manager restart, and shutdown await cancellation of the current generation while late installation or completion cannot clear a newer session. **Verification:** - Coordinator state/control layer contains no platform or FFmpeg branch. +- A resource bundle can be claimed only by matching identity and generation, and a claimed reservation remains visible as `Stopping` until U9 joins its resources. - Every resolved wire session identity has at most one local terminal observation. - Existing audio call/session state behavior remains unchanged. @@ -410,11 +414,13 @@ flowchart TB - Test support: `rust/telepathy-core/tests/core_integration_test/common.rs` **Approach:** -- Permit one slot-owned `accept_uni` only after a matching accepted offer; race it against cancellation, negotiation timeout, peer/session teardown, and manager shutdown. +- Permit one slot-owned `accept_uni` only after a matching accepted offer; race it against cancellation, negotiation timeout, peer/session teardown, and manager shutdown. Store its handle in the matching active resource bundle and never detach it. - Keep immediate preamble, distinct control/preamble/media limits, bounded frame decoding, EOF/reset mapping, and stream finish/reset policy inside the transport module. - Validate each outbound control, preamble, and media payload against its class-specific limit before allocation/encode/write; report deterministic local typed failure on excess. - Preserve one direct backpressure chain from adapter capture through framed Iroh write and from framed read through complete adapter stdin delivery. Avoid unbounded media queues and per-frame tasks. -- Coordinator owns transport workers and the adapter-session handle; adapter session owns child and pipes. Cleanup order is cancel, unblock/close I/O, await adapter cleanup and transport workers, emit one terminal observation, then clear the matching slot. +- Install the adapter session plus every transport worker, including `open_uni` and `accept_uni`, only while the reservation still matches identity and generation. If installation loses that match, cancel and join the uninstalled resources immediately without changing the current slot. +- Make `cancel_and_join` claim the matching active reservation into `Stopping` without clearing it. Under the lock, take no joined resources beyond the matching bundle; outside the lock, cancel, close or reset stream and adapter I/O, join adapter cleanup and every worker, emit exactly one terminal observation, then reacquire the lock and clear only the still-matching slot. +- Require `call()`, `call_controller`, session removal, manager restart, call end, and shutdown to await `cancel_and_join`; no caller may observe idle before the adapter session and all transport workers have joined. - If framing is interrupted, reset/abandon that stream and never resume partial bytes for the same or next generation. **Patterns to follow:** @@ -427,15 +433,17 @@ flowchart TB - Error path: `open_uni`/`accept_uni` failure, reset, EOF, partial preamble/frame, and interrupted writes produce one defined terminal result and release ownership. - Backpressure: slow transport or slow child stdin keeps in-flight memory bounded and preserves every byte, including controlled partial stdin writes. - Cancellation: stop during preamble, frame read/write, child stdin write, and blocked send unblocks cleanup; trailing bytes cannot contaminate the next generation. -- Integration: call/session teardown and immediate restart leave no accept wait, stream, adapter session, or worker from the previous identity. +- Ownership: stale adapter or worker installation/completion cancels and joins only its own resources, leaves the newer slot intact, and emits no terminal observation for that newer identity. +- Integration: `call()`, `call_controller`, call/session teardown, and immediate restart await `cancel_and_join` and leave no `accept_uni` wait, `open_uni` worker, stream, adapter session, or worker from the previous identity. **Verification:** - No concurrent `accept_uni` consumer exists for the connection in this scope. -- No detached video worker, stream, adapter session, or unbounded media queue remains after idle is observed. +- No detached `open_uni` or `accept_uni` worker, stream, adapter session, or unbounded media queue remains after idle is observed. +- Idle is observable only after matching cancellation, I/O closure or reset, joins, and the sole terminal observation complete. ### U4. Extract Statically Selected Platform Session Adapters -**Goal:** Move current FFmpeg capture/playback into the desktop adapter while preserving U1 behavior and providing an unconditional unsupported-target implementation. +**Goal:** Move current FFmpeg capture/playback into the desktop adapter while preserving implemented Windows behavior, truthful runtime capabilities, and an unconditional unsupported-target implementation. **Requirements:** R1, R3, R4, R5, R9, R13 @@ -454,7 +462,9 @@ flowchart TB **Approach:** - Implement the U2 statically dispatched session-adapter boundary with distinct sender preparation and sender/receiver run responsibilities. Preparation returns generic negotiated format; active runs consume bounded coordinator-owned media I/O and cancellation. - Compile the desktop FFmpeg implementation only for Windows, macOS, and Linux. Compile an unsupported adapter elsewhere while retaining identical higher-level APIs. -- Move existing capability probing, command generation, stdout capture, stdin playback, decoder choice, and OS-specific flags without changing their resulting behavior. +- Keep one canonical internal `Device`, `Encoder`, and `Decoder` vocabulary in `platform.rs`. Move capability probing, command generation, stdout capture, stdin playback, decoder choice, and OS-specific flags behind the selected adapter. +- Advertise display capture only when the selected target has a real command implementation and the current runtime probe returns the required device and encoder. Do not infer capture support from a serializable `RecordingConfig` or from the desktop adapter compiling. +- At receiver startup, re-probe local decoders and select the first locally reported decoder compatible with the negotiated descriptor format. Do not treat a persisted sender encoder or a fixed cross-platform preference as receiver capability. - Make active desktop sessions solely own child process and all pipe handles. Close input before graceful wait, concurrently drain every piped output, keep unused outputs null, then use bounded termination/escalation and reap on every terminal path. - Preserve complete child-stdin delivery under partial writes and current direct capture backpressure; no media-rate queue or task is introduced inside the adapter. - Keep Iroh connection/control types out of platform modules. @@ -464,18 +474,19 @@ flowchart TB - U1 characterization tests as the authoritative desktop behavior baseline. **Test scenarios:** -- Happy path: desktop adapter preparation yields the expected generic media format and U1 command/byte tests remain green. -- Validation: runtime-missing FFmpeg, encoder, decoder, or capture device returns typed unavailable/unsupported output before active state. +- Happy path: an implemented Windows capture mode yields the expected generic media format and U1 command/byte tests remain green. +- Validation: runtime-missing FFmpeg, encoder, decoder, or capture device returns typed unavailable/unsupported output before active state; a persisted or user-selected unavailable mode returns the same typed outcome without panicking. - Error path: spawn failure, early exit, broken stdin/stdout, and cancellation all terminate and reap the child once. - Edge case: cancellation during blocked media I/O cannot orphan the child or adapter worker. - Backpressure: partial child-stdin writes preserve the complete framed payload; sustained stdout with a slow transport stays bounded. - Cleanup: a child blocked on stdin, a child producing sustained stdout, and stream reset during blocked I/O each close pipes and reap exactly once. -- Platform: unsupported adapter builds behind the same coordinator contract, reports no send/receive capability, and starts no process. +- Platform: desktop builds with no implemented capture command path report no display-capture mode, and unsupported adapter builds behind the same coordinator contract report no send/receive capability and start no process. - Platform: unsupported constructor/configuration update/start paths have explicit typed outcomes rather than silently accepting unusable settings. - Architecture: adapter tests prove no Iroh control or stream negotiation is required to exercise platform behavior. **Verification:** -- Desktop output matches characterization baseline. +- Implemented Windows desktop output matches the characterization baseline; macOS and Linux capture remain unavailable until their command paths and target coverage exist. +- The receiver decoder is chosen from fresh local probe results in local probe order and must be compatible with the negotiated format. - Adding a target adapter does not require edits to coordinator or transport behavior beyond static module selection. ### U5. Generalize Video Capabilities and Configuration @@ -484,7 +495,7 @@ flowchart TB **Requirements:** R1, R4, R5, R6, R10 -**Dependencies:** U4 +**Dependencies:** U4, U9 **Files:** - Modify: `rust/telepathy-core/src/types.rs` @@ -498,24 +509,24 @@ flowchart TB **Approach:** - Replace top-level screenshare naming with a generic video configuration facade and source-neutral capabilities: send/receive support, supported current source, formats, and typed unavailability. - Keep encoder/device/bitrate/framerate/height persistence as desktop adapter-owned screen configuration. Preserve current serialized values unless an unavoidable public rename requires a documented one-time migration. -- Validate capabilities again at start, not only during Flutter preflight, because binaries/devices can disappear after discovery. +- Validate capabilities again at start, not only during Flutter preflight, because binaries/devices can disappear after discovery. Treat persistence as configuration only: it cannot advertise a source mode that the fresh probe does not support. - Keep platform availability as runtime data under one unconditional API shape. **Patterns to follow:** - Current `ScreenshareConfig`, `Capabilities`, `RecordingConfig`, and disk serialization in `rust/telepathy-core/src/types.rs` and `rust/telepathy-core/src/internal/screenshare.rs`. **Test scenarios:** -- Happy path: existing desktop persisted settings load into equivalent adapter configuration and produce the same selected command. +- Happy path: existing Windows persisted settings load into equivalent adapter configuration and produce the same selected command when the fresh probe supports that mode. - Compatibility: current serialized screenshare settings retain values across the rename/migration decision without silent reset. - Compatibility: old-format bytes load and round-trip recording configuration, width, and height exactly before any optional schema migration. -- Validation: stale/missing encoder or device is rejected at start with a typed outcome rather than relying on UI preflight. -- Platform: unsupported target exposes the same capability query and reports unavailable without constructing FFmpeg state. +- Validation: stale/missing encoder or device is rejected at start with a typed outcome rather than relying on UI preflight; persisted or user-selected unavailable modes never panic. +- Platform: a desktop target without an implemented capture command path exposes no display-capture mode. An unsupported target exposes the same capability query and reports unavailable without constructing FFmpeg state. - Platform: unsupported constructor, configuration update, and start semantics remain internally consistent and cannot report success for an unusable sender. - Edge case: empty capability lists and receive-only/send-only results remain representable without boolean ambiguity. **Verification:** - Generic session/wire types never contain FFmpeg configuration. -- Existing desktop settings remain usable and future platform configuration can stay adapter-local. +- Existing Windows settings remain usable when supported by the fresh probe, and future platform configuration can stay adapter-local without becoming a runtime capability claim. ### U6. Migrate Native and Flutter-Rust Public APIs @@ -523,7 +534,7 @@ flowchart TB **Requirements:** R2, R7, R10, R11, R12 -**Dependencies:** U3, U5, U9 +**Dependencies:** U5, U9 **Files:** - Modify: `rust/telepathy-core/src/internal/callbacks.rs` @@ -616,7 +627,7 @@ Lifecycle mapping preserves current interaction: **Requirements:** R1-R13 -**Dependencies:** U4, U9, U10 +**Dependencies:** U9, U10 **Files:** - Modify: `rust/telepathy-core/tests/core_integration_test.rs` @@ -641,7 +652,7 @@ Lifecycle mapping preserves current interaction: - Edge case: local/remote simultaneous stop, immediate restart, session replacement, manager restart, shutdown, and call end during every nonterminal phase ignore stale completions. - Stress: repeated start/stop and teardown leave no active video slot, orphan task, unreaped child, duplicate callback, or audio/session regression. - Performance: slow sender/receiver paths retain bounded in-flight media and complete stop without frame-rate tracing or per-frame task growth. -- Platform: desktop and CI-covered Android/iOS/web targets compile the same public API; unsupported targets return defined constructor/capability/update/start outcomes. +- Platform: desktop and CI-covered Android/iOS/web targets compile the same public API. Windows capture is covered through implemented command paths; macOS/Linux capture is not advertised without implemented and tested paths; unsupported targets return defined constructor/capability/update/start outcomes. - Observability: one start and terminal cleanup summary is emitted per local generation with no event per media frame. - Regression: U1 command/byte behavior and existing session/call/audio/room suites remain green. @@ -677,10 +688,10 @@ flowchart TB - **Interaction graph:** Start/stop moves from call controls through the bridge to coordinator; peer controls and media stream are coordinated centrally; typed events return to Flutter state; desktop process lifecycle stays behind adapter. - **Error propagation:** Adapter, stream, protocol, timeout, and teardown outcomes become typed video terminal reasons with deterministic precedence. Best-effort peer control failure maps to a local transport-ended result; video-local failures do not end audio calls unless shared peer transport has already failed. -- **State lifecycle risks:** Crossed starts, duplicate control messages, cancellation before stream visibility, stale callbacks, and late task completion are guarded by wire identity plus local generation checks and one slot-owned accept wait. +- **State lifecycle risks:** Crossed starts, duplicate control messages, cancellation before stream visibility, stale callbacks, and late task completion are guarded by wire identity plus local generation checks. The matching slot owns its adapter session and every worker, including `open_uni` and `accept_uni`, until `cancel_and_join` closes or resets I/O and joins them. - **API surface parity:** `TelepathyHandle`, `NativeTelepathy`, Flutter exports, callbacks, config/capability types, handwritten Dart consumers, and generated bindings change together. - **Integration coverage:** Unit tests cannot prove Iroh stream visibility/order, peer agreement, callback propagation, or teardown joining; real two-client integration and stress scenarios cover those paths. -- **Resource behavior:** One I/O owner per direction preserves bounded backpressure and complete pipe writes; cleanup closes/unblocks I/O before awaiting adapter and transport workers. +- **Resource behavior:** One I/O owner per direction preserves bounded backpressure and complete pipe writes. A matching terminal claim moves the slot to `Stopping`, cancels and closes or resets I/O, joins the adapter session and every worker outside the lock, emits one terminal observation, then clears the slot; stale resources clean up without affecting a newer generation. - **Unchanged invariants:** One-to-one audio call and session ownership, room behavior, chat, audio transport, current desktop screenshare UX, and existing FFmpeg media choices remain unchanged. --- @@ -703,6 +714,8 @@ flowchart TB |---|---|---|---| | FFmpeg command or byte behavior drifts during extraction | Medium | High | Characterize first; keep adapter extraction separate; preserve command/payload tests through all later units. | | A cancelled task leaks FFmpeg | Medium | High | Adapter owns child and pipes; all exits converge on terminate/kill/wait; coordinator joins before idle; stress with process probes. | +| A stale resource installation or completion clears a newer session | Medium | High | Install and claim only matching identity/generation bundles; stale resources cancel and join themselves without observing or clearing the current slot. | +| A terminal path returns while transport work still runs | Medium | High | Store adapter, `open_uni`, and `accept_uni` handles in the slot; `cancel_and_join` cancels, closes or resets I/O, joins outside the lock, emits once, and is awaited by call and session teardown. | | Two peers disagree during crossed starts | Medium | High | Canonical identity tie-break, explicit generation, symmetric protocol tests, and idempotent loser cleanup. | | Wrong uni-stream is accepted | Low | High | Single authoritative video acceptor, immediate identity preamble, strict validation before adapter start; defer general dispatcher until competing stream types exist. | | Oversized peer input allocates unbounded memory | Medium | High | Bound control, preamble, and payload decoders; reject before adapter delivery; boundary tests. | @@ -711,6 +724,8 @@ flowchart TB | Child cleanup deadlocks on retained pipes | Medium | High | Adapter exclusively owns pipes, closes stdin, drains piped output, then performs bounded terminate/escalate/reap before resolving. | | Callback delay/reentrancy strands core state | Medium | Medium | Callbacks observe state only; coordinator owns cancellation and terminal cleanup; stale event tests in Rust and Dart. | | Platform API differs after `cfg` | Medium | High | Keep public types and methods unconditional; select private adapter modules statically; run codegen and target builds. | +| A compiled adapter advertises an unimplemented capture path | Medium | High | Keep capture-mode advertisement tied to a real target command implementation and fresh runtime probe. Test that unsupported or unimplemented paths produce typed unavailability, never a process launch or panic. | +| Persisted configuration is mistaken for current capability | Medium | High | Preserve serializable configuration, but re-probe at start and reject unavailable device, encoder, or mode with a typed outcome. | | Persisted settings are silently lost | Medium | Medium | Preserve serialized values or provide explicit one-time migration with round-trip tests before renaming storage. | | Iroh patch behavior differs from researched docs | Low | Medium | Verify resolved lockfile APIs and stream semantics during implementation; preserve explicit framing and cleanup regardless. | | Refactor regresses audio call/session teardown | Medium | High | Keep video slot separate from call slot; run existing suites plus call/session stress scenarios. | @@ -730,14 +745,14 @@ flowchart TB - U4 establishes the static adapter and extracts FFmpeg after U2. - U3 then replaces the old helper branch with generic slot and control ownership against that adapter contract. -- U9 joins coordinator, bounded transport, adapter sessions, and terminal cleanup. -- U5 moves capability/configuration concerns behind the new boundary. +- U9 completes the coordinator-owned resource bundle, bounded transport, and joined terminal cleanup. +- U5 moves capability/configuration concerns behind the completed cleanup boundary. ### Phase 3: Migrate Consumers and Prove the System -- U6 changes Rust-native/Flutter-Rust APIs and regenerates bridge output. -- U10 migrates Dart persistence, state, and existing controls. -- U7 completes two-peer, failure, teardown, and stress coverage plus tracing documentation. +- U6 changes Rust-native/Flutter-Rust APIs and regenerates bridge output after U9 and U5. +- U10 migrates Dart persistence, state, and existing controls after U6. +- U7 completes two-peer, failure, teardown, and stress coverage plus tracing documentation after U9 and U10. --- @@ -746,8 +761,11 @@ flowchart TB - Current desktop screenshare command construction, encoded byte forwarding, playback, settings, and user controls remain behaviorally equivalent. - Generic coordinator, transport, domain, and public API contain no FFmpeg-specific or screenshare-specific lifecycle assumptions. - Unsupported targets compile the same public video API and return typed unavailability. +- A desktop target advertises a capture mode only when its command path is implemented and the fresh runtime probe supports it; persisted configuration never substitutes for that capability. +- Receiver playback uses the first compatible decoder from fresh local probe order for the negotiated format. - Every accepted or rejected start reaches one identity-matched terminal outcome on both peers; stop and teardown are idempotent. -- Repeated start/stop, call end, session replacement, restart, and shutdown leave no stale slot, worker, stream, or child process. +- Repeated start/stop, call end, session replacement, restart, and shutdown await joined cleanup and leave no stale slot, worker, stream, or child process. +- An adapter session and every `open_uni` or `accept_uni` worker install only into their matching identity/generation bundle; stale resources clean up without clearing or observing a newer session. - Slow or blocked media paths retain bounded in-flight memory, preserve complete bytes, and stop without frame-rate task/log growth. - A future platform adapter can be added without coordinator, transport, protocol lifecycle, or Flutter event redesign. - A future source can be added without a parallel session lifecycle or media transport path. @@ -759,15 +777,17 @@ flowchart TB - Update `docs/TRACING.md` with generic video lifecycle fields and terminal reason taxonomy. - Regenerate bridge output only after public Rust contract stabilizes; never edit generated files manually. - Treat protocol migration as coordinated: all peers in a test/deployment set must use the new wire version. -- Verify desktop FFmpeg behavior on Windows, macOS, and Linux where available. Unsupported mobile/web targets must still build and expose capability results. +- Verify implemented Windows FFmpeg capture/playback behavior through its command and adapter tests. Do not claim macOS or Linux capture validation or availability until those targets have implemented and tested command paths. Unsupported mobile/web targets must still build and expose capability results. - Developer must run system tests manually in WSL after automated Rust/Flutter validation. --- ## Sources & References -- Related code: `rust/telepathy-core/src/internal/screenshare.rs` -- Related code: `rust/telepathy-core/src/internal/helpers.rs` +- Related code: `rust/telepathy-core/src/internal/video.rs` +- Related code: `rust/telepathy-core/src/internal/video/platform.rs` +- Related code: `rust/telepathy-core/src/internal/video/platform/desktop_ffmpeg.rs` +- Related code: `rust/telepathy-core/src/internal/video/platform/unsupported.rs` - Related code: `rust/telepathy-core/src/internal/state.rs` - Related code: `rust/telepathy-core/src/internal/messages.rs` - Related code: `rust/telepathy-core/src/internal/core.rs` diff --git a/docs/solutions/architecture-patterns/adapter-safe-video-capabilities.md b/docs/solutions/architecture-patterns/adapter-safe-video-capabilities.md new file mode 100644 index 00000000..6ca48a7a --- /dev/null +++ b/docs/solutions/architecture-patterns/adapter-safe-video-capabilities.md @@ -0,0 +1,64 @@ +--- +title: Adapter-Safe Video Capabilities Separate Runtime Truth From Configuration +date: 2026-07-30 +category: docs/solutions/architecture-patterns/ +module: telepathy-core video platform +problem_type: architecture_pattern +component: tooling +severity: high +applies_when: + - A target-specific adapter exposes serializable device or codec settings + - A compiled adapter has target-dependent command implementations + - Receiver playback must match a negotiated media format +related_components: + - flutter-rust-bridge + - testing-framework +tags: [video-sessions, capabilities, platform-adapters, ffmpeg, configuration] +--- + +# Adapter-Safe Video Capabilities Separate Runtime Truth From Configuration + +## Context + +Generic video sessions retain serializable internal `Device`, `Encoder`, and `Decoder` values, but a stored value does not prove that the current target can run it. The selected adapter must report what this runtime can actually start or receive. + +## Guidance + +Select one private adapter at compile time in `rust/telepathy-core/src/internal/video/platform.rs`: the desktop FFmpeg adapter for Windows, macOS, and Linux, or the unsupported adapter elsewhere. Keep the coordinator and public contract independent of that selection. + +Treat the fresh adapter probe as the source of capability truth. `desktop_ffmpeg::video_capabilities` advertises a display source only when it has both a device and an encoder result. `Device::devices` currently supplies capture devices only on Windows, while `Device::to_args` returns a typed platform-unavailable error for unimplemented paths. Therefore a desktop build on macOS or Linux must not advertise display capture merely because the adapter compiled or a `RecordingConfig` can deserialize. + +Validate the selected configuration again in `desktop_ffmpeg::prepare_sender`. Missing current devices or encoders return `VideoUnavailable::ConfigurationUnavailable`; missing source formats return the corresponding typed unavailability. The unsupported adapter returns `VideoUnavailable::PlatformUnsupported` for capabilities and sender preparation. + +For playback, `desktop_ffmpeg::run_receiver` probes locally immediately before startup. `select_decoder` chooses the first decoder from that fresh local probe list whose codec matches the negotiated `VideoMediaDescriptor`. Decoder preference is local probe order, not sender configuration or a global cross-platform order. + +## Why This Matters + +Configuration is stable enough to save and present later. Capability is a statement about the current binary, target, installed FFmpeg components, and implemented command paths. Combining them can advertise a mode that cannot start, then turn a user action or restored setting into a panic or a false success. + +Keeping the distinction inside the adapter lets the generic session lifecycle remain target-neutral while still reporting precise typed outcomes to native and Flutter callers. + +## When to Apply + +- A target-specific implementation compiles on more targets than it fully supports. +- Device, encoder, decoder, permission, or binary availability can change after settings are stored. +- A receiver must choose a local implementation compatible with peer-negotiated media. + +## Examples + +The adapter keeps unavailable configuration on the typed path rather than starting a process: + +```rust +if !capabilities.encoders.contains(&config.encoder) + || !capabilities.devices.contains(&config.device) +{ + return Err(VideoUnavailable::ConfigurationUnavailable); +} +``` + +The verified adapter tests cover the same boundary: `unimplemented_device_returns_typed_error_without_panicking`, `sender_start_rejects_encoder_removed_after_preflight`, `sender_start_rejects_device_removed_after_preflight`, and `decoder_selection_uses_first_compatible_local_decoder` in `rust/telepathy-core/src/internal/video/platform/desktop_ffmpeg.rs`. `unsupported_adapter_query_and_start_report_typed_unavailable` in `rust/telepathy-core/src/internal/video/platform.rs` covers the selected unsupported contract. + +## Related + +- [Joined Video Session Teardown Keeps Slots Safe for Reuse](joined-video-session-teardown.md) covers lifecycle ownership after a session has started. +- The generic video-session implementation plan is `docs/plans/2026-07-17-001-refactor-generic-video-sessions-plan.md`. diff --git a/docs/solutions/architecture-patterns/joined-video-session-teardown.md b/docs/solutions/architecture-patterns/joined-video-session-teardown.md new file mode 100644 index 00000000..c72a297a --- /dev/null +++ b/docs/solutions/architecture-patterns/joined-video-session-teardown.md @@ -0,0 +1,68 @@ +--- +title: Joined Video Session Teardown Keeps Slots Safe for Reuse +date: 2026-07-25 +category: docs/solutions/architecture-patterns/ +module: telepathy-core video sessions +problem_type: architecture_pattern +component: tooling +severity: high +applies_when: + - A peer-scoped session owns transport workers or platform resources + - Multiple asynchronous paths can terminate the same session + - A slot may be reused after cancellation +related_components: + - session-manager + - flutter-rust-bridge + - testing-framework +tags: [video-sessions, teardown, cancellation, task-ownership, iroh] +--- + +# Joined Video Session Teardown Keeps Slots Safe for Reuse + +## Context + +A video session can finish through local stop, remote control, a timeout, transport failure, or session teardown. Cancellation alone does not release the transport worker or its platform I/O. Reusing the peer slot before that worker joins lets stale work overlap a new session. + +## Guidance + +Make the per-peer `VideoSlot` the single owner of each `VideoAttempt` and its worker. A local start records a fresh session identity and generation in a reservation before it sends an offer. The worker installs only while that exact attempt is still `Starting`; a stale installation cancels and joins itself instead of attaching to a replacement reservation. + +Terminal paths must claim the reservation by moving it to `Stopping`, cancel its token, take and join the worker, then clear the reservation and notify idle. A second terminal path waits for idle instead of joining or clearing the same worker again. `VideoSlot::cancel_and_join` implements this ordering in `rust/telepathy-core/src/internal/video.rs`. + +Session teardown must invoke `cancel_current_and_join` after signalling call and session cancellation. `SessionState::teardown` does this before returning in `rust/telepathy-core/src/internal/state.rs`. + +## Why This Matters + +The corrected teardown regression showed that cancelling only the session token leaves an installed video worker blocked on its own token. The session could appear torn down while the worker still held transport or platform resources. + +The reservation remains occupied until the worker join completes, so a new generation cannot reuse the slot early. The full attempt identity also prevents a late worker result from mutating or terminating a replacement session. + +## When to Apply + +- A logical slot owns sockets, streams, subprocesses, device handles, or long-lived tasks. +- More than one event can end that work. +- A stale task could act after its slot has been reused. + +## Examples + +The installation guard requires matching attempt identity, `Starting` phase, and no installed worker before storing the handle. Otherwise it cancels the launch and awaits the worker. + +```rust +if reservation.attempt == launch.attempt + && reservation.phase == VideoPhase::Starting + && reservation.worker.is_none() +{ + reservation.phase = VideoPhase::Active; + reservation.worker = Some(worker); +} else { + launch.cancellation.cancel(); + let _ = worker.await; +} +``` + +The sender and receiver race stream creation against cancellation and reset or stop interrupted streams in `rust/telepathy-core/src/internal/video/transport.rs`. Integration coverage verifies that teardown does not make the slot idle before a blocked worker is released and joined in `rust/telepathy-core/tests/core_integration_test/video_sessions/lifecycle.rs`. + +## Related + +- [Prepared Identity Switching Requires Runtime Readiness and Token-Owned Commit](prepared-identity-switch-runtime-readiness.md) applies the same ownership principle to a prepared identity operation. +- The generic video-session implementation plan is `docs/plans/2026-07-17-001-refactor-generic-video-sessions-plan.md`. diff --git a/lib/controllers/network_settings_controller.dart b/lib/controllers/network_settings_controller.dart index 5fd83439..647a4516 100644 --- a/lib/controllers/network_settings_controller.dart +++ b/lib/controllers/network_settings_controller.dart @@ -211,6 +211,13 @@ class NetworkSettingsController with ChangeNotifier { 'screenshareConfigBuffer', base64Encode(screenshareConfig.toBytes())); } + Future isVideoSourceConfigured(VideoSource source) async { + switch (source) { + case VideoSource.display: + return (await screenshareConfig.recordingConfig()) != null; + } + } + Future loadCodecConfig() async { return CodecConfig( enabled: await options.getBool('codecEnabled') ?? true, diff --git a/lib/controllers/state_controller.dart b/lib/controllers/state_controller.dart index b597b6de..0b5d5824 100644 --- a/lib/controllers/state_controller.dart +++ b/lib/controllers/state_controller.dart @@ -37,8 +37,11 @@ class StateController extends ChangeNotifier { final Map sessions = {}; ManagerState _sessionManagerState = ManagerState.stopped; - FrontendNotify? _stopSendingScreenshare; - FrontendNotify? _stopReceivingScreenshare; + VideoSessionIdentity? _sendingScreenshareIdentity; + VideoSessionIdentity? _receivingScreenshareIdentity; + VideoSessionIdentity? _stoppedSendingScreenshareIdentity; + final Set _terminalSendingScreenshareIdentities = {}; + final Set _terminalReceivingScreenshareIdentities = {}; bool isSendingScreenshare = false; bool isReceivingScreenshare = false; @@ -345,45 +348,77 @@ class StateController extends ChangeNotifier { }); } - void screenshareStarted((FrontendNotify stop, bool sending) record) { - if (record.$2) { - DebugConsole.log('Sending screenshare started'); - _stopSendingScreenshare = record.$1; - isSendingScreenshare = true; - - // this catches the sending screenshare being closed by the receiver - Future.microtask(() async { - await record.$1.notified(); - // if the screen share is still sending, stop the screenshare - if (isSendingScreenshare) { - stopScreenshare(true, true); + void handleVideoLifecycle(VideoLifecycleEvent event) { + if (event.source != VideoSource.display) return; + + if (event.phase == VideoPhase.active) { + if (!isCallActive) return; + if (event.role == VideoRole.sender) { + if (event.identity == _stoppedSendingScreenshareIdentity) return; + if (_terminalSendingScreenshareIdentities.contains(event.identity)) { + return; + } + _terminalSendingScreenshareIdentities.remove(event.identity); + _sendingScreenshareIdentity = event.identity; + isSendingScreenshare = true; + } else { + if (_terminalReceivingScreenshareIdentities.contains(event.identity)) { + return; } - }); + _terminalReceivingScreenshareIdentities.remove(event.identity); + _receivingScreenshareIdentity = event.identity; + isReceivingScreenshare = true; + } + notifyListeners(); + return; + } + + if (event.phase != VideoPhase.terminal) return; + + var handled = false; + if (event.role == VideoRole.sender) { + _terminalSendingScreenshareIdentities.add(event.identity); + if (event.identity == _sendingScreenshareIdentity) { + _sendingScreenshareIdentity = null; + isSendingScreenshare = false; + handled = true; + } + if (event.identity == _stoppedSendingScreenshareIdentity) { + _stoppedSendingScreenshareIdentity = null; + handled = true; + } } else { - DebugConsole.log('Receiving screenshare started'); - _stopReceivingScreenshare = record.$1; - isReceivingScreenshare = true; + _terminalReceivingScreenshareIdentities.add(event.identity); + if (event.identity == _receivingScreenshareIdentity) { + _receivingScreenshareIdentity = null; + isReceivingScreenshare = false; + handled = true; + } } + if (!handled) return; notifyListeners(); } - void stopScreenshare(bool sending, bool notify) { - DebugConsole.log('Stopping screenshare sending: $sending'); + VideoSessionIdentity? stopSendingScreenshare() { + final identity = _sendingScreenshareIdentity; + if (identity == null) return null; - if (sending) { - _stopSendingScreenshare?.notify(); - _stopSendingScreenshare = null; - isSendingScreenshare = false; - } else { - _stopReceivingScreenshare?.notify(); - _stopReceivingScreenshare = null; - isReceivingScreenshare = false; - } + _sendingScreenshareIdentity = null; + _stoppedSendingScreenshareIdentity = identity; + isSendingScreenshare = false; + notifyListeners(); + return identity; + } - if (notify) { - notifyListeners(); - } + void clearScreenshares() { + _sendingScreenshareIdentity = null; + _receivingScreenshareIdentity = null; + _stoppedSendingScreenshareIdentity = null; + _terminalSendingScreenshareIdentities.clear(); + _terminalReceivingScreenshareIdentities.clear(); + isSendingScreenshare = false; + isReceivingScreenshare = false; } /// A group of actions run when the call ends. @@ -393,8 +428,7 @@ class StateController extends ChangeNotifier { _activeRoom = null; _callTimer.stop(); _callTimer.reset(); - stopScreenshare(true, false); - stopScreenshare(false, false); + clearScreenshares(); if (_startRequestPending) { // Keep the target, attempt, and operation until the original start future diff --git a/lib/core/rust/flutter.dart b/lib/core/rust/flutter.dart index 4c9e95b6..76f876ef 100644 --- a/lib/core/rust/flutter.dart +++ b/lib/core/rust/flutter.dart @@ -27,8 +27,8 @@ abstract class FlutterCallbacks implements RustOpaqueInterface { required FutureOr Function(Statistics) statistics, required FutureOr Function(ChatMessage) messageReceived, required FutureOr Function(ManagerState) managerActive, - required FutureOr Function((FrontendNotify, bool)) - screenshareStarted}) => + required FutureOr Function(VideoLifecycleEvent) + videoLifecycle}) => RustLib.instance.api.crateFlutterFlutterCallbacksNew( acceptCall: acceptCall, getContact: getContact, @@ -38,7 +38,7 @@ abstract class FlutterCallbacks implements RustOpaqueInterface { statistics: statistics, messageReceived: messageReceived, managerActive: managerActive, - screenshareStarted: screenshareStarted); + videoLifecycle: videoLifecycle); } // Rust type: RustOpaqueMoi> @@ -94,6 +94,9 @@ abstract class Telepathy implements RustOpaqueInterface { Future prepareIdentitySwitch( {required List targetKey, required List targetContacts}); + Future requestVideoSource( + {required Contact contact, required VideoSource source}); + /// Restarts the session manager Future restartManager(); @@ -140,14 +143,20 @@ abstract class Telepathy implements RustOpaqueInterface { {required Contact contact, required StartOperation operation}); /// Non-blocking: spawns the manager task and returns. The Dart side observes - /// the eventual `Active` transition via the `managerActive` callback. + /// the eventual `Active` transition via the `managerActive` callback. The + /// non-blocking contract is validated by the CLI system test + /// `test_start_manager_ack_precedes_active_event`; the `()` return type + /// prevents silent reintroduction of blocking semantics. Future startManager(); - Future startScreenshare({required Contact contact}); - /// Tries to start a session for a contact Future startSession({required Contact contact}); /// Stops a specific session (called when a contact is deleted) Future stopSession({required Contact contact}); + + Future stopVideoSource( + {required VideoSessionIdentity identity}); + + Future videoCapabilities(); } diff --git a/lib/core/rust/frb_generated.dart b/lib/core/rust/frb_generated.dart index f14ba7a6..79e82b69 100644 --- a/lib/core/rust/frb_generated.dart +++ b/lib/core/rust/frb_generated.dart @@ -74,7 +74,7 @@ class RustLib extends BaseEntrypoint { String get codegenVersion => '2.12.0'; @override - int get rustContentHash => 226036739; + int get rustContentHash => -1306791125; static const kDefaultExternalLibraryLoaderConfig = ExternalLibraryLoaderConfig( @@ -169,8 +169,7 @@ abstract class RustLibApi extends BaseApi { required FutureOr Function(Statistics) statistics, required FutureOr Function(ChatMessage) messageReceived, required FutureOr Function(ManagerState) managerActive, - required FutureOr Function((FrontendNotify, bool)) - screenshareStarted}); + required FutureOr Function(VideoLifecycleEvent) videoLifecycle}); void cratePlayerFlutterSoundHandleCancel({required FlutterSoundHandle that}); @@ -284,6 +283,9 @@ abstract class RustLibApi extends BaseApi { required int framerate, int? height}); + Future crateTypesScreenshareConfigVideoCapabilities( + {required ScreenshareConfig that}); + ArcHost cratePlayerSoundPlayerHost({required SoundPlayer that}); SoundPlayer cratePlayerSoundPlayerNew({required double outputVolume}); @@ -335,6 +337,11 @@ abstract class RustLibApi extends BaseApi { required List targetKey, required List targetContacts}); + Future crateFlutterTelepathyRequestVideoSource( + {required Telepathy that, + required Contact contact, + required VideoSource source}); + Future crateFlutterTelepathyRestartManager({required Telepathy that}); void crateFlutterTelepathyResumeStatistics({required Telepathy that}); @@ -393,15 +400,18 @@ abstract class RustLibApi extends BaseApi { Future crateFlutterTelepathyStartManager({required Telepathy that}); - Future crateFlutterTelepathyStartScreenshare( - {required Telepathy that, required Contact contact}); - Future crateFlutterTelepathyStartSession( {required Telepathy that, required Contact contact}); Future crateFlutterTelepathyStopSession( {required Telepathy that, required Contact contact}); + Future crateFlutterTelepathyStopVideoSource( + {required Telepathy that, required VideoSessionIdentity identity}); + + Future crateFlutterTelepathyVideoCapabilities( + {required Telepathy that}); + Stream crateFlutterLoggingCreateLogStream(); (String, Uint8List) crateFlutterUtilsGenerateKeys(); @@ -1305,8 +1315,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { required FutureOr Function(Statistics) statistics, required FutureOr Function(ChatMessage) messageReceived, required FutureOr Function(ManagerState) managerActive, - required FutureOr Function((FrontendNotify, bool)) - screenshareStarted}) { + required FutureOr Function(VideoLifecycleEvent) videoLifecycle}) { return handler.executeSync(SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); @@ -1326,8 +1335,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { messageReceived, serializer); sse_encode_DartFn_Inputs_manager_state_Output_unit_AnyhowException( managerActive, serializer); - sse_encode_DartFn_Inputs_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool_Output_unit_AnyhowException( - screenshareStarted, serializer); + sse_encode_DartFn_Inputs_video_lifecycle_event_Output_unit_AnyhowException( + videoLifecycle, serializer); return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 29)!; }, codec: SseCodec( @@ -1345,7 +1354,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { statistics, messageReceived, managerActive, - screenshareStarted + videoLifecycle ], apiImpl: this, )); @@ -1363,7 +1372,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { 'statistics', 'messageReceived', 'managerActive', - 'screenshareStarted' + 'videoLifecycle' ], ); @@ -2382,6 +2391,33 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ], ); + @override + Future crateTypesScreenshareConfigVideoCapabilities( + {required ScreenshareConfig that}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerScreenshareConfig( + that, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 65, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_video_capabilities, + decodeErrorData: null, + ), + constMeta: kCrateTypesScreenshareConfigVideoCapabilitiesConstMeta, + argValues: [that], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateTypesScreenshareConfigVideoCapabilitiesConstMeta => + const TaskConstMeta( + debugName: 'ScreenshareConfig_video_capabilities', + argNames: ['that'], + ); + @override ArcHost cratePlayerSoundPlayerHost({required SoundPlayer that}) { return handler.executeSync(SyncTask( @@ -2389,7 +2425,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSoundPlayer( that, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 65)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 66)!; }, codec: SseCodec( decodeSuccessData: @@ -2413,7 +2449,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_f_32(outputVolume, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 66)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 67)!; }, codec: SseCodec( decodeSuccessData: @@ -2441,7 +2477,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer); sse_encode_list_prim_u_8_loose(bytes, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 67, port: port_); + funcId: 68, port: port_); }, codec: SseCodec( decodeSuccessData: @@ -2469,7 +2505,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer); sse_encode_opt_String(deviceId, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 68, port: port_); + funcId: 69, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2496,7 +2532,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerSoundPlayer( that, serializer); sse_encode_f_32(volume, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 69)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 70)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2521,7 +2557,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerStartOperation( that, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 70)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 71)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2547,7 +2583,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 71, port: port_); + funcId: 72, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2581,7 +2617,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_String(text, serializer); sse_encode_list_record_string_list_prim_u_8_strict( attachments, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 72)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 73)!; }, codec: SseCodec( decodeSuccessData: @@ -2608,7 +2644,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 73, port: port_); + funcId: 74, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2640,7 +2676,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerStartOperation( operation, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 74, port: port_); + funcId: 75, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2667,7 +2703,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 75, port: port_); + funcId: 76, port: port_); }, codec: SseCodec( decodeSuccessData: @@ -2709,7 +2745,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { codecConfig, serializer); sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerFlutterCallbacks( callbacks, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 76)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 77)!; }, codec: SseCodec( decodeSuccessData: @@ -2749,7 +2785,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 77)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 78)!; }, codec: SseCodec( decodeSuccessData: @@ -2775,7 +2811,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 78)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 79)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2807,7 +2843,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact( targetContacts, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 79, port: port_); + funcId: 80, port: port_); }, codec: SseCodec( decodeSuccessData: @@ -2826,6 +2862,38 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { argNames: ['that', 'targetKey', 'targetContacts'], ); + @override + Future crateFlutterTelepathyRequestVideoSource( + {required Telepathy that, + required Contact contact, + required VideoSource source}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( + that, serializer); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact( + contact, serializer); + sse_encode_video_source(source, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 81, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_video_start_outcome, + decodeErrorData: null, + ), + constMeta: kCrateFlutterTelepathyRequestVideoSourceConstMeta, + argValues: [that, contact, source], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateFlutterTelepathyRequestVideoSourceConstMeta => + const TaskConstMeta( + debugName: 'Telepathy_request_video_source', + argNames: ['that', 'contact', 'source'], + ); + @override Future crateFlutterTelepathyRestartManager({required Telepathy that}) { return handler.executeNormal(NormalTask( @@ -2834,7 +2902,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 80, port: port_); + funcId: 82, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2859,7 +2927,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 81)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 83)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2888,7 +2956,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerChatMessage( message, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 82, port: port_); + funcId: 84, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2916,7 +2984,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact( contact, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 83)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 85)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2943,7 +3011,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); sse_encode_bool(deafened, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 84)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 86)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2970,7 +3038,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); sse_encode_bool(denoise, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 85)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 87)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -2997,7 +3065,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); sse_encode_bool(enabled, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 86)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 88)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3025,7 +3093,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer); sse_encode_list_prim_u_8_loose(key, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 87, port: port_); + funcId: 89, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3053,7 +3121,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer); sse_encode_opt_String(deviceId, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 88, port: port_); + funcId: 90, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3080,7 +3148,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); sse_encode_f_32(decibel, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 89)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 91)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3108,7 +3176,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer); sse_encode_opt_list_prim_u_8_strict(model, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 90, port: port_); + funcId: 92, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3135,7 +3203,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); sse_encode_bool(muted, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 91)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 93)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3163,7 +3231,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { that, serializer); sse_encode_opt_String(deviceId, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 92, port: port_); + funcId: 94, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3190,7 +3258,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); sse_encode_f_32(decibel, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 93)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 95)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3217,7 +3285,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); sse_encode_bool(play, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 94)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 96)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3244,7 +3312,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); sse_encode_f_32(decimal, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 95)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 97)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3271,7 +3339,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); sse_encode_bool(send, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 96)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 98)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3297,7 +3365,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 97, port: port_); + funcId: 99, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3330,7 +3398,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerStartOperation( operation, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 98, port: port_); + funcId: 100, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3356,7 +3424,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_RefMut_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 99, port: port_); + funcId: 101, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3375,7 +3443,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { ); @override - Future crateFlutterTelepathyStartScreenshare( + Future crateFlutterTelepathyStartSession( {required Telepathy that, required Contact contact}) { return handler.executeNormal(NormalTask( callFfi: (port_) { @@ -3385,26 +3453,26 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact( contact, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 100, port: port_); + funcId: 102, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, - decodeErrorData: null, + decodeErrorData: sse_decode_dart_error, ), - constMeta: kCrateFlutterTelepathyStartScreenshareConstMeta, + constMeta: kCrateFlutterTelepathyStartSessionConstMeta, argValues: [that, contact], apiImpl: this, )); } - TaskConstMeta get kCrateFlutterTelepathyStartScreenshareConstMeta => + TaskConstMeta get kCrateFlutterTelepathyStartSessionConstMeta => const TaskConstMeta( - debugName: 'Telepathy_start_screenshare', + debugName: 'Telepathy_start_session', argNames: ['that', 'contact'], ); @override - Future crateFlutterTelepathyStartSession( + Future crateFlutterTelepathyStopSession( {required Telepathy that, required Contact contact}) { return handler.executeNormal(NormalTask( callFfi: (port_) { @@ -3414,51 +3482,77 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact( contact, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 101, port: port_); + funcId: 103, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, - decodeErrorData: sse_decode_dart_error, + decodeErrorData: null, ), - constMeta: kCrateFlutterTelepathyStartSessionConstMeta, + constMeta: kCrateFlutterTelepathyStopSessionConstMeta, argValues: [that, contact], apiImpl: this, )); } - TaskConstMeta get kCrateFlutterTelepathyStartSessionConstMeta => + TaskConstMeta get kCrateFlutterTelepathyStopSessionConstMeta => const TaskConstMeta( - debugName: 'Telepathy_start_session', + debugName: 'Telepathy_stop_session', argNames: ['that', 'contact'], ); @override - Future crateFlutterTelepathyStopSession( - {required Telepathy that, required Contact contact}) { + Future crateFlutterTelepathyStopVideoSource( + {required Telepathy that, required VideoSessionIdentity identity}) { return handler.executeNormal(NormalTask( callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( that, serializer); - sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact( - contact, serializer); + sse_encode_box_autoadd_video_session_identity(identity, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 102, port: port_); + funcId: 104, port: port_); }, codec: SseCodec( - decodeSuccessData: sse_decode_unit, + decodeSuccessData: sse_decode_video_stop_outcome, decodeErrorData: null, ), - constMeta: kCrateFlutterTelepathyStopSessionConstMeta, - argValues: [that, contact], + constMeta: kCrateFlutterTelepathyStopVideoSourceConstMeta, + argValues: [that, identity], apiImpl: this, )); } - TaskConstMeta get kCrateFlutterTelepathyStopSessionConstMeta => + TaskConstMeta get kCrateFlutterTelepathyStopVideoSourceConstMeta => const TaskConstMeta( - debugName: 'Telepathy_stop_session', - argNames: ['that', 'contact'], + debugName: 'Telepathy_stop_video_source', + argNames: ['that', 'identity'], + ); + + @override + Future crateFlutterTelepathyVideoCapabilities( + {required Telepathy that}) { + return handler.executeNormal(NormalTask( + callFfi: (port_) { + final serializer = SseSerializer(generalizedFrbRustBinding); + sse_encode_Auto_Ref_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerTelepathy( + that, serializer); + pdeCallFfi(generalizedFrbRustBinding, serializer, + funcId: 105, port: port_); + }, + codec: SseCodec( + decodeSuccessData: sse_decode_video_capabilities, + decodeErrorData: null, + ), + constMeta: kCrateFlutterTelepathyVideoCapabilitiesConstMeta, + argValues: [that], + apiImpl: this, + )); + } + + TaskConstMeta get kCrateFlutterTelepathyVideoCapabilitiesConstMeta => + const TaskConstMeta( + debugName: 'Telepathy_video_capabilities', + argNames: ['that'], ); @override @@ -3468,7 +3562,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_StreamSink_String_Sse(s, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 103)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 106)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3492,7 +3586,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return handler.executeSync(SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 104)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 107)!; }, codec: SseCodec( decodeSuccessData: sse_decode_record_string_list_prim_u_8_strict, @@ -3517,7 +3611,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(path, serializer); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 105, port: port_); + funcId: 108, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3540,7 +3634,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_list_String(peers, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 106)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 109)!; }, codec: SseCodec( decodeSuccessData: sse_decode_String, @@ -3562,7 +3656,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return handler.executeSync(SyncTask( callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 107)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 110)!; }, codec: SseCodec( decodeSuccessData: sse_decode_unit, @@ -3586,7 +3680,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 108, port: port_); + funcId: 111, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_bool, @@ -3610,7 +3704,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: (port_) { final serializer = SseSerializer(generalizedFrbRustBinding); pdeCallFfi(generalizedFrbRustBinding, serializer, - funcId: 109, port: port_); + funcId: 112, port: port_); }, codec: SseCodec( decodeSuccessData: sse_decode_statistics, @@ -3634,7 +3728,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { callFfi: () { final serializer = SseSerializer(generalizedFrbRustBinding); sse_encode_String(peerId, serializer); - return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 110)!; + return pdeCallFfi(generalizedFrbRustBinding, serializer, funcId: 113)!; }, codec: SseCodec( decodeSuccessData: sse_decode_bool, @@ -3788,14 +3882,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } Future Function(int, dynamic) - encode_DartFn_Inputs_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool_Output_unit_AnyhowException( - FutureOr Function((FrontendNotify, bool)) raw) { + encode_DartFn_Inputs_record_string_opt_list_prim_u_8_strict_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_Output_bool_AnyhowException( + FutureOr Function((String, Uint8List?, FrontendNotify)) raw) { return (callId, rawArg0) async { final arg0 = - dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool( + dco_decode_record_string_opt_list_prim_u_8_strict_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify( rawArg0); - Box? rawOutput; + Box? rawOutput; Box? rawError; try { rawOutput = Box(await raw(arg0)); @@ -3807,7 +3901,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { assert((rawOutput != null) ^ (rawError != null)); if (rawOutput != null) { serializer.buffer.putUint8(0); - sse_encode_unit(rawOutput.value, serializer); + sse_encode_bool(rawOutput.value, serializer); } else { serializer.buffer.putUint8(1); sse_encode_AnyhowException(rawError!.value, serializer); @@ -3823,14 +3917,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } Future Function(int, dynamic) - encode_DartFn_Inputs_record_string_opt_list_prim_u_8_strict_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_Output_bool_AnyhowException( - FutureOr Function((String, Uint8List?, FrontendNotify)) raw) { + encode_DartFn_Inputs_record_string_session_status_Output_unit_AnyhowException( + FutureOr Function((String, SessionStatus)) raw) { return (callId, rawArg0) async { - final arg0 = - dco_decode_record_string_opt_list_prim_u_8_strict_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify( - rawArg0); + final arg0 = dco_decode_record_string_session_status(rawArg0); - Box? rawOutput; + Box? rawOutput; Box? rawError; try { rawOutput = Box(await raw(arg0)); @@ -3842,7 +3934,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { assert((rawOutput != null) ^ (rawError != null)); if (rawOutput != null) { serializer.buffer.putUint8(0); - sse_encode_bool(rawOutput.value, serializer); + sse_encode_unit(rawOutput.value, serializer); } else { serializer.buffer.putUint8(1); sse_encode_AnyhowException(rawError!.value, serializer); @@ -3858,10 +3950,10 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } Future Function(int, dynamic) - encode_DartFn_Inputs_record_string_session_status_Output_unit_AnyhowException( - FutureOr Function((String, SessionStatus)) raw) { + encode_DartFn_Inputs_statistics_Output_unit_AnyhowException( + FutureOr Function(Statistics) raw) { return (callId, rawArg0) async { - final arg0 = dco_decode_record_string_session_status(rawArg0); + final arg0 = dco_decode_statistics(rawArg0); Box? rawOutput; Box? rawError; @@ -3891,12 +3983,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } Future Function(int, dynamic) - encode_DartFn_Inputs_statistics_Output_unit_AnyhowException( - FutureOr Function(Statistics) raw) { + encode_DartFn_Inputs_unit_Output_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact_AnyhowException( + FutureOr> Function(void) raw) { return (callId, rawArg0) async { - final arg0 = dco_decode_statistics(rawArg0); + final arg0 = dco_decode_unit(rawArg0); - Box? rawOutput; + Box>? rawOutput; Box? rawError; try { rawOutput = Box(await raw(arg0)); @@ -3908,7 +4000,8 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { assert((rawOutput != null) ^ (rawError != null)); if (rawOutput != null) { serializer.buffer.putUint8(0); - sse_encode_unit(rawOutput.value, serializer); + sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact( + rawOutput.value, serializer); } else { serializer.buffer.putUint8(1); sse_encode_AnyhowException(rawError!.value, serializer); @@ -3924,12 +4017,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } Future Function(int, dynamic) - encode_DartFn_Inputs_unit_Output_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact_AnyhowException( - FutureOr> Function(void) raw) { + encode_DartFn_Inputs_video_lifecycle_event_Output_unit_AnyhowException( + FutureOr Function(VideoLifecycleEvent) raw) { return (callId, rawArg0) async { - final arg0 = dco_decode_unit(rawArg0); + final arg0 = dco_decode_video_lifecycle_event(rawArg0); - Box>? rawOutput; + Box? rawOutput; Box? rawError; try { rawOutput = Box(await raw(arg0)); @@ -3941,8 +4034,7 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { assert((rawOutput != null) ^ (rawError != null)); if (rawOutput != null) { serializer.buffer.putUint8(0); - sse_encode_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact( - rawOutput.value, serializer); + sse_encode_unit(rawOutput.value, serializer); } else { serializer.buffer.putUint8(1); sse_encode_AnyhowException(rawError!.value, serializer); @@ -4405,14 +4497,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { throw UnimplementedError(''); } - @protected - FutureOr Function((FrontendNotify, bool)) - dco_decode_DartFn_Inputs_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool_Output_unit_AnyhowException( - dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - throw UnimplementedError(''); - } - @protected FutureOr Function((String, Uint8List?, FrontendNotify)) dco_decode_DartFn_Inputs_record_string_opt_list_prim_u_8_strict_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_Output_bool_AnyhowException( @@ -4445,6 +4529,14 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { throw UnimplementedError(''); } + @protected + FutureOr Function(VideoLifecycleEvent) + dco_decode_DartFn_Inputs_video_lifecycle_event_Output_unit_AnyhowException( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + throw UnimplementedError(''); + } + @protected Object dco_decode_DartOpaque(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -4642,6 +4734,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as int; } + @protected + VideoMediaFormat dco_decode_box_autoadd_video_media_format(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_video_media_format(raw); + } + + @protected + VideoSessionIdentity dco_decode_box_autoadd_video_session_identity( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_video_session_identity(raw); + } + + @protected + VideoTerminalReason dco_decode_box_autoadd_video_terminal_reason( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_video_terminal_reason(raw); + } + + @protected + VideoUnavailable dco_decode_box_autoadd_video_unavailable(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return dco_decode_video_unavailable(raw); + } + @protected CallState dco_decode_call_state(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -4741,6 +4859,21 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { .toList(); } + @protected + List dco_decode_list_video_media_format(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List).map(dco_decode_video_media_format).toList(); + } + + @protected + List dco_decode_list_video_source_capability( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return (raw as List) + .map(dco_decode_video_source_capability) + .toList(); + } + @protected ManagerState dco_decode_manager_state(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -4799,6 +4932,15 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw == null ? null : dco_decode_box_autoadd_u_32(raw); } + @protected + VideoTerminalReason? dco_decode_opt_box_autoadd_video_terminal_reason( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return raw == null + ? null + : dco_decode_box_autoadd_video_terminal_reason(raw); + } + @protected List? dco_decode_opt_list_String(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -4812,29 +4954,11 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - ( - FrontendNotify, - bool - ) dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool( - dynamic raw) { + (bool, bool, double) dco_decode_record_bool_bool_f_32(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs final arr = raw as List; - if (arr.length != 2) { - throw Exception('Expected 2 elements, got ${arr.length}'); - } - return ( - dco_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerFrontendNotify( - arr[0]), - dco_decode_bool(arr[1]), - ); - } - - @protected - (bool, bool, double) dco_decode_record_bool_bool_f_32(dynamic raw) { - // Codec=Dco (DartCObject based), see doc to use other codecs - final arr = raw as List; - if (arr.length != 3) { - throw Exception('Expected 3 elements, got ${arr.length}'); + if (arr.length != 3) { + throw Exception('Expected 3 elements, got ${arr.length}'); } return ( dco_decode_bool(arr[0]), @@ -4971,6 +5095,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return raw as int; } + @protected + U8Array16 dco_decode_u_8_array_16(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return U8Array16(dco_decode_list_prim_u_8_strict(raw)); + } + @protected void dco_decode_unit(dynamic raw) { // Codec=Dco (DartCObject based), see doc to use other codecs @@ -4983,6 +5113,183 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return dcoDecodeU64(raw); } + @protected + VideoCapabilities dco_decode_video_capabilities(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 4) + throw Exception('unexpected arr length: expect 4 but see ${arr.length}'); + return VideoCapabilities( + send: dco_decode_video_capability_availability(arr[0]), + receive: dco_decode_video_capability_availability(arr[1]), + sendSources: dco_decode_list_video_source_capability(arr[2]), + receiveFormats: dco_decode_list_video_media_format(arr[3]), + ); + } + + @protected + VideoCapabilityAvailability dco_decode_video_capability_availability( + dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + switch (raw[0]) { + case 0: + return const VideoCapabilityAvailability_Available(); + case 1: + return VideoCapabilityAvailability_Unavailable( + dco_decode_box_autoadd_video_unavailable(raw[1]), + ); + default: + throw Exception('unreachable'); + } + } + + @protected + VideoCodec dco_decode_video_codec(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return VideoCodec.values[raw as int]; + } + + @protected + VideoLifecycleEvent dco_decode_video_lifecycle_event(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 5) + throw Exception('unexpected arr length: expect 5 but see ${arr.length}'); + return VideoLifecycleEvent( + identity: dco_decode_video_session_identity(arr[0]), + role: dco_decode_video_role(arr[1]), + source: dco_decode_video_source(arr[2]), + phase: dco_decode_video_phase(arr[3]), + terminalReason: dco_decode_opt_box_autoadd_video_terminal_reason(arr[4]), + ); + } + + @protected + VideoMediaFormat dco_decode_video_media_format(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + switch (raw[0]) { + case 0: + return VideoMediaFormat_MpegTs( + dco_decode_video_codec(raw[1]), + ); + default: + throw Exception('unreachable'); + } + } + + @protected + VideoPhase dco_decode_video_phase(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return VideoPhase.values[raw as int]; + } + + @protected + VideoRole dco_decode_video_role(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return VideoRole.values[raw as int]; + } + + @protected + VideoSessionId dco_decode_video_session_id(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 1) + throw Exception('unexpected arr length: expect 1 but see ${arr.length}'); + return VideoSessionId( + field0: dco_decode_u_8_array_16(arr[0]), + ); + } + + @protected + VideoSessionIdentity dco_decode_video_session_identity(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return VideoSessionIdentity( + peerId: dco_decode_String(arr[0]), + sessionId: dco_decode_video_session_id(arr[1]), + ); + } + + @protected + VideoSource dco_decode_video_source(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return VideoSource.values[raw as int]; + } + + @protected + VideoSourceCapability dco_decode_video_source_capability(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + final arr = raw as List; + if (arr.length != 2) + throw Exception('unexpected arr length: expect 2 but see ${arr.length}'); + return VideoSourceCapability( + source: dco_decode_video_source(arr[0]), + formats: dco_decode_list_video_media_format(arr[1]), + ); + } + + @protected + VideoStartOutcome dco_decode_video_start_outcome(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + switch (raw[0]) { + case 0: + return VideoStartOutcome_Requested( + dco_decode_box_autoadd_video_session_identity(raw[1]), + ); + case 1: + return VideoStartOutcome_Unavailable( + dco_decode_box_autoadd_video_unavailable(raw[1]), + ); + case 2: + return const VideoStartOutcome_NoSession(); + case 3: + return const VideoStartOutcome_AlreadyActive(); + case 4: + return VideoStartOutcome_Failed( + dco_decode_video_terminal_reason(raw[1]), + ); + default: + throw Exception('unreachable'); + } + } + + @protected + VideoStopOutcome dco_decode_video_stop_outcome(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return VideoStopOutcome.values[raw as int]; + } + + @protected + VideoTerminalReason dco_decode_video_terminal_reason(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + return VideoTerminalReason.values[raw as int]; + } + + @protected + VideoUnavailable dco_decode_video_unavailable(dynamic raw) { + // Codec=Dco (DartCObject based), see doc to use other codecs + switch (raw[0]) { + case 0: + return const VideoUnavailable_PlatformUnsupported(); + case 1: + return const VideoUnavailable_RuntimeUnavailable(); + case 2: + return VideoUnavailable_SourceUnavailable( + dco_decode_video_source(raw[1]), + ); + case 3: + return VideoUnavailable_FormatUnavailable( + dco_decode_box_autoadd_video_media_format(raw[1]), + ); + case 4: + return const VideoUnavailable_ConfigurationUnavailable(); + default: + throw Exception('unreachable'); + } + } + @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5508,6 +5815,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return (sse_decode_u_32(deserializer)); } + @protected + VideoMediaFormat sse_decode_box_autoadd_video_media_format( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_video_media_format(deserializer)); + } + + @protected + VideoSessionIdentity sse_decode_box_autoadd_video_session_identity( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_video_session_identity(deserializer)); + } + + @protected + VideoTerminalReason sse_decode_box_autoadd_video_terminal_reason( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_video_terminal_reason(deserializer)); + } + + @protected + VideoUnavailable sse_decode_box_autoadd_video_unavailable( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + return (sse_decode_video_unavailable(deserializer)); + } + @protected CallState sse_decode_call_state(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5625,6 +5960,32 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return ans_; } + @protected + List sse_decode_list_video_media_format( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_video_media_format(deserializer)); + } + return ans_; + } + + @protected + List sse_decode_list_video_source_capability( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var len_ = sse_decode_i_32(deserializer); + var ans_ = []; + for (var idx_ = 0; idx_ < len_; ++idx_) { + ans_.add(sse_decode_video_source_capability(deserializer)); + } + return ans_; + } + @protected ManagerState sse_decode_manager_state(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5700,39 +6061,37 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } @protected - List? sse_decode_opt_list_String(SseDeserializer deserializer) { + VideoTerminalReason? sse_decode_opt_box_autoadd_video_terminal_reason( + SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs if (sse_decode_bool(deserializer)) { - return (sse_decode_list_String(deserializer)); + return (sse_decode_box_autoadd_video_terminal_reason(deserializer)); } else { return null; } } @protected - Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer) { + List? sse_decode_opt_list_String(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs if (sse_decode_bool(deserializer)) { - return (sse_decode_list_prim_u_8_strict(deserializer)); + return (sse_decode_list_String(deserializer)); } else { return null; } } @protected - ( - FrontendNotify, - bool - ) sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool( - SseDeserializer deserializer) { + Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs - var var_field0 = - sse_decode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerFrontendNotify( - deserializer); - var var_field1 = sse_decode_bool(deserializer); - return (var_field0, var_field1); + + if (sse_decode_bool(deserializer)) { + return (sse_decode_list_prim_u_8_strict(deserializer)); + } else { + return null; + } } @protected @@ -5855,6 +6214,13 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getUint8(); } + @protected + U8Array16 sse_decode_u_8_array_16(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_list_prim_u_8_strict(deserializer); + return U8Array16(inner); + } + @protected void sse_decode_unit(SseDeserializer deserializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -5866,6 +6232,188 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { return deserializer.buffer.getBigUint64(); } + @protected + VideoCapabilities sse_decode_video_capabilities( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_send = sse_decode_video_capability_availability(deserializer); + var var_receive = sse_decode_video_capability_availability(deserializer); + var var_sendSources = sse_decode_list_video_source_capability(deserializer); + var var_receiveFormats = sse_decode_list_video_media_format(deserializer); + return VideoCapabilities( + send: var_send, + receive: var_receive, + sendSources: var_sendSources, + receiveFormats: var_receiveFormats); + } + + @protected + VideoCapabilityAvailability sse_decode_video_capability_availability( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var tag_ = sse_decode_i_32(deserializer); + switch (tag_) { + case 0: + return const VideoCapabilityAvailability_Available(); + case 1: + var var_field0 = sse_decode_box_autoadd_video_unavailable(deserializer); + return VideoCapabilityAvailability_Unavailable(var_field0); + default: + throw UnimplementedError(''); + } + } + + @protected + VideoCodec sse_decode_video_codec(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return VideoCodec.values[inner]; + } + + @protected + VideoLifecycleEvent sse_decode_video_lifecycle_event( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_identity = sse_decode_video_session_identity(deserializer); + var var_role = sse_decode_video_role(deserializer); + var var_source = sse_decode_video_source(deserializer); + var var_phase = sse_decode_video_phase(deserializer); + var var_terminalReason = + sse_decode_opt_box_autoadd_video_terminal_reason(deserializer); + return VideoLifecycleEvent( + identity: var_identity, + role: var_role, + source: var_source, + phase: var_phase, + terminalReason: var_terminalReason); + } + + @protected + VideoMediaFormat sse_decode_video_media_format(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var tag_ = sse_decode_i_32(deserializer); + switch (tag_) { + case 0: + var var_field0 = sse_decode_video_codec(deserializer); + return VideoMediaFormat_MpegTs(var_field0); + default: + throw UnimplementedError(''); + } + } + + @protected + VideoPhase sse_decode_video_phase(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return VideoPhase.values[inner]; + } + + @protected + VideoRole sse_decode_video_role(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return VideoRole.values[inner]; + } + + @protected + VideoSessionId sse_decode_video_session_id(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_field0 = sse_decode_u_8_array_16(deserializer); + return VideoSessionId(field0: var_field0); + } + + @protected + VideoSessionIdentity sse_decode_video_session_identity( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_peerId = sse_decode_String(deserializer); + var var_sessionId = sse_decode_video_session_id(deserializer); + return VideoSessionIdentity(peerId: var_peerId, sessionId: var_sessionId); + } + + @protected + VideoSource sse_decode_video_source(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return VideoSource.values[inner]; + } + + @protected + VideoSourceCapability sse_decode_video_source_capability( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var var_source = sse_decode_video_source(deserializer); + var var_formats = sse_decode_list_video_media_format(deserializer); + return VideoSourceCapability(source: var_source, formats: var_formats); + } + + @protected + VideoStartOutcome sse_decode_video_start_outcome( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var tag_ = sse_decode_i_32(deserializer); + switch (tag_) { + case 0: + var var_field0 = + sse_decode_box_autoadd_video_session_identity(deserializer); + return VideoStartOutcome_Requested(var_field0); + case 1: + var var_field0 = sse_decode_box_autoadd_video_unavailable(deserializer); + return VideoStartOutcome_Unavailable(var_field0); + case 2: + return const VideoStartOutcome_NoSession(); + case 3: + return const VideoStartOutcome_AlreadyActive(); + case 4: + var var_field0 = sse_decode_video_terminal_reason(deserializer); + return VideoStartOutcome_Failed(var_field0); + default: + throw UnimplementedError(''); + } + } + + @protected + VideoStopOutcome sse_decode_video_stop_outcome(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return VideoStopOutcome.values[inner]; + } + + @protected + VideoTerminalReason sse_decode_video_terminal_reason( + SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + var inner = sse_decode_i_32(deserializer); + return VideoTerminalReason.values[inner]; + } + + @protected + VideoUnavailable sse_decode_video_unavailable(SseDeserializer deserializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + var tag_ = sse_decode_i_32(deserializer); + switch (tag_) { + case 0: + return const VideoUnavailable_PlatformUnsupported(); + case 1: + return const VideoUnavailable_RuntimeUnavailable(); + case 2: + var var_field0 = sse_decode_video_source(deserializer); + return VideoUnavailable_SourceUnavailable(var_field0); + case 3: + var var_field0 = + sse_decode_box_autoadd_video_media_format(deserializer); + return VideoUnavailable_FormatUnavailable(var_field0); + case 4: + return const VideoUnavailable_ConfigurationUnavailable(); + default: + throw UnimplementedError(''); + } + } + @protected void sse_encode_AnyhowException( AnyhowException self, SseSerializer serializer) { @@ -6241,18 +6789,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer); } - @protected - void - sse_encode_DartFn_Inputs_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool_Output_unit_AnyhowException( - FutureOr Function((FrontendNotify, bool)) self, - SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_DartOpaque( - encode_DartFn_Inputs_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool_Output_unit_AnyhowException( - self), - serializer); - } - @protected void sse_encode_DartFn_Inputs_record_string_opt_list_prim_u_8_strict_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_Output_bool_AnyhowException( @@ -6298,6 +6834,18 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer); } + @protected + void + sse_encode_DartFn_Inputs_video_lifecycle_event_Output_unit_AnyhowException( + FutureOr Function(VideoLifecycleEvent) self, + SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_DartOpaque( + encode_DartFn_Inputs_video_lifecycle_event_Output_unit_AnyhowException( + self), + serializer); + } + @protected void sse_encode_DartOpaque(Object self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6525,6 +7073,34 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { sse_encode_u_32(self, serializer); } + @protected + void sse_encode_box_autoadd_video_media_format( + VideoMediaFormat self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_video_media_format(self, serializer); + } + + @protected + void sse_encode_box_autoadd_video_session_identity( + VideoSessionIdentity self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_video_session_identity(self, serializer); + } + + @protected + void sse_encode_box_autoadd_video_terminal_reason( + VideoTerminalReason self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_video_terminal_reason(self, serializer); + } + + @protected + void sse_encode_box_autoadd_video_unavailable( + VideoUnavailable self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_video_unavailable(self, serializer); + } + @protected void sse_encode_call_state(CallState self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6628,6 +7204,26 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_list_video_media_format( + List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_video_media_format(item, serializer); + } + } + + @protected + void sse_encode_list_video_source_capability( + List self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.length, serializer); + for (final item in self) { + sse_encode_video_source_capability(item, serializer); + } + } + @protected void sse_encode_manager_state(ManagerState self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6695,6 +7291,17 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } + @protected + void sse_encode_opt_box_autoadd_video_terminal_reason( + VideoTerminalReason? self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + + sse_encode_bool(self != null, serializer); + if (self != null) { + sse_encode_box_autoadd_video_terminal_reason(self, serializer); + } + } + @protected void sse_encode_opt_list_String( List? self, SseSerializer serializer) { @@ -6717,16 +7324,6 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { } } - @protected - void - sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool( - (FrontendNotify, bool) self, SseSerializer serializer) { - // Codec=Sse (Serialization based), see doc to use other codecs - sse_encode_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerFrontendNotify( - self.$1, serializer); - sse_encode_bool(self.$2, serializer); - } - @protected void sse_encode_record_bool_bool_f_32( (bool, bool, double) self, SseSerializer serializer) { @@ -6827,6 +7424,12 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { serializer.buffer.putUint8(self); } + @protected + void sse_encode_u_8_array_16(U8Array16 self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_list_prim_u_8_strict(self.inner, serializer); + } + @protected void sse_encode_unit(void self, SseSerializer serializer) { // Codec=Sse (Serialization based), see doc to use other codecs @@ -6837,6 +7440,154 @@ class RustLibApiImpl extends RustLibApiImplPlatform implements RustLibApi { // Codec=Sse (Serialization based), see doc to use other codecs serializer.buffer.putBigUint64(self); } + + @protected + void sse_encode_video_capabilities( + VideoCapabilities self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_video_capability_availability(self.send, serializer); + sse_encode_video_capability_availability(self.receive, serializer); + sse_encode_list_video_source_capability(self.sendSources, serializer); + sse_encode_list_video_media_format(self.receiveFormats, serializer); + } + + @protected + void sse_encode_video_capability_availability( + VideoCapabilityAvailability self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + switch (self) { + case VideoCapabilityAvailability_Available(): + sse_encode_i_32(0, serializer); + case VideoCapabilityAvailability_Unavailable(field0: final field0): + sse_encode_i_32(1, serializer); + sse_encode_box_autoadd_video_unavailable(field0, serializer); + } + } + + @protected + void sse_encode_video_codec(VideoCodec self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + + @protected + void sse_encode_video_lifecycle_event( + VideoLifecycleEvent self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_video_session_identity(self.identity, serializer); + sse_encode_video_role(self.role, serializer); + sse_encode_video_source(self.source, serializer); + sse_encode_video_phase(self.phase, serializer); + sse_encode_opt_box_autoadd_video_terminal_reason( + self.terminalReason, serializer); + } + + @protected + void sse_encode_video_media_format( + VideoMediaFormat self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + switch (self) { + case VideoMediaFormat_MpegTs(field0: final field0): + sse_encode_i_32(0, serializer); + sse_encode_video_codec(field0, serializer); + } + } + + @protected + void sse_encode_video_phase(VideoPhase self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + + @protected + void sse_encode_video_role(VideoRole self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + + @protected + void sse_encode_video_session_id( + VideoSessionId self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_u_8_array_16(self.field0, serializer); + } + + @protected + void sse_encode_video_session_identity( + VideoSessionIdentity self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_String(self.peerId, serializer); + sse_encode_video_session_id(self.sessionId, serializer); + } + + @protected + void sse_encode_video_source(VideoSource self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + + @protected + void sse_encode_video_source_capability( + VideoSourceCapability self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_video_source(self.source, serializer); + sse_encode_list_video_media_format(self.formats, serializer); + } + + @protected + void sse_encode_video_start_outcome( + VideoStartOutcome self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + switch (self) { + case VideoStartOutcome_Requested(field0: final field0): + sse_encode_i_32(0, serializer); + sse_encode_box_autoadd_video_session_identity(field0, serializer); + case VideoStartOutcome_Unavailable(field0: final field0): + sse_encode_i_32(1, serializer); + sse_encode_box_autoadd_video_unavailable(field0, serializer); + case VideoStartOutcome_NoSession(): + sse_encode_i_32(2, serializer); + case VideoStartOutcome_AlreadyActive(): + sse_encode_i_32(3, serializer); + case VideoStartOutcome_Failed(field0: final field0): + sse_encode_i_32(4, serializer); + sse_encode_video_terminal_reason(field0, serializer); + } + } + + @protected + void sse_encode_video_stop_outcome( + VideoStopOutcome self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + + @protected + void sse_encode_video_terminal_reason( + VideoTerminalReason self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + sse_encode_i_32(self.index, serializer); + } + + @protected + void sse_encode_video_unavailable( + VideoUnavailable self, SseSerializer serializer) { + // Codec=Sse (Serialization based), see doc to use other codecs + switch (self) { + case VideoUnavailable_PlatformUnsupported(): + sse_encode_i_32(0, serializer); + case VideoUnavailable_RuntimeUnavailable(): + sse_encode_i_32(1, serializer); + case VideoUnavailable_SourceUnavailable(field0: final field0): + sse_encode_i_32(2, serializer); + sse_encode_video_source(field0, serializer); + case VideoUnavailable_FormatUnavailable(field0: final field0): + sse_encode_i_32(3, serializer); + sse_encode_box_autoadd_video_media_format(field0, serializer); + case VideoUnavailable_ConfigurationUnavailable(): + sse_encode_i_32(4, serializer); + } + } } @sealed @@ -7236,7 +7987,7 @@ class OverlayImpl extends RustOpaque implements Overlay { RustLib.instance.api.crateOverlayOverlayMoveOverlay( that: this, x: x, y: y, width: width, height: height); - /// access the screen resolution for overlay positioning in the front end + /// non-windows platforms don't have an overlay (int, int) screenResolution() => RustLib.instance.api.crateOverlayOverlayScreenResolution( that: this, @@ -7396,6 +8147,11 @@ class ScreenshareConfigImpl extends RustOpaque implements ScreenshareConfig { bitrate: bitrate, framerate: framerate, height: height); + + Future videoCapabilities() => + RustLib.instance.api.crateTypesScreenshareConfigVideoCapabilities( + that: this, + ); } @sealed @@ -7555,6 +8311,11 @@ class TelepathyImpl extends RustOpaque implements Telepathy { RustLib.instance.api.crateFlutterTelepathyPrepareIdentitySwitch( that: this, targetKey: targetKey, targetContacts: targetContacts); + Future requestVideoSource( + {required Contact contact, required VideoSource source}) => + RustLib.instance.api.crateFlutterTelepathyRequestVideoSource( + that: this, contact: contact, source: source); + /// Restarts the session manager Future restartManager() => RustLib.instance.api.crateFlutterTelepathyRestartManager( @@ -7627,16 +8388,15 @@ class TelepathyImpl extends RustOpaque implements Telepathy { that: this, contact: contact, operation: operation); /// Non-blocking: spawns the manager task and returns. The Dart side observes - /// the eventual `Active` transition via the `managerActive` callback. + /// the eventual `Active` transition via the `managerActive` callback. The + /// non-blocking contract is validated by the CLI system test + /// `test_start_manager_ack_precedes_active_event`; the `()` return type + /// prevents silent reintroduction of blocking semantics. Future startManager() => RustLib.instance.api.crateFlutterTelepathyStartManager( that: this, ); - Future startScreenshare({required Contact contact}) => - RustLib.instance.api - .crateFlutterTelepathyStartScreenshare(that: this, contact: contact); - /// Tries to start a session for a contact Future startSession({required Contact contact}) => RustLib.instance.api .crateFlutterTelepathyStartSession(that: this, contact: contact); @@ -7644,4 +8404,14 @@ class TelepathyImpl extends RustOpaque implements Telepathy { /// Stops a specific session (called when a contact is deleted) Future stopSession({required Contact contact}) => RustLib.instance.api .crateFlutterTelepathyStopSession(that: this, contact: contact); + + Future stopVideoSource( + {required VideoSessionIdentity identity}) => + RustLib.instance.api + .crateFlutterTelepathyStopVideoSource(that: this, identity: identity); + + Future videoCapabilities() => + RustLib.instance.api.crateFlutterTelepathyVideoCapabilities( + that: this, + ); } diff --git a/lib/core/rust/frb_generated.io.dart b/lib/core/rust/frb_generated.io.dart index 1b232e0b..a1e1bfd1 100644 --- a/lib/core/rust/frb_generated.io.dart +++ b/lib/core/rust/frb_generated.io.dart @@ -280,11 +280,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dco_decode_DartFn_Inputs_manager_state_Output_unit_AnyhowException( dynamic raw); - @protected - FutureOr Function((FrontendNotify, bool)) - dco_decode_DartFn_Inputs_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool_Output_unit_AnyhowException( - dynamic raw); - @protected FutureOr Function((String, Uint8List?, FrontendNotify)) dco_decode_DartFn_Inputs_record_string_opt_list_prim_u_8_strict_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_Output_bool_AnyhowException( @@ -305,6 +300,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dco_decode_DartFn_Inputs_unit_Output_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact_AnyhowException( dynamic raw); + @protected + FutureOr Function(VideoLifecycleEvent) + dco_decode_DartFn_Inputs_video_lifecycle_event_Output_unit_AnyhowException( + dynamic raw); + @protected Object dco_decode_DartOpaque(dynamic raw); @@ -418,6 +418,19 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_box_autoadd_u_32(dynamic raw); + @protected + VideoMediaFormat dco_decode_box_autoadd_video_media_format(dynamic raw); + + @protected + VideoSessionIdentity dco_decode_box_autoadd_video_session_identity( + dynamic raw); + + @protected + VideoTerminalReason dco_decode_box_autoadd_video_terminal_reason(dynamic raw); + + @protected + VideoUnavailable dco_decode_box_autoadd_video_unavailable(dynamic raw); + @protected CallState dco_decode_call_state(dynamic raw); @@ -454,6 +467,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { List<(String, Uint8List)> dco_decode_list_record_string_list_prim_u_8_strict( dynamic raw); + @protected + List dco_decode_list_video_media_format(dynamic raw); + + @protected + List dco_decode_list_video_source_capability( + dynamic raw); + @protected ManagerState dco_decode_manager_state(dynamic raw); @@ -480,17 +500,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { int? dco_decode_opt_box_autoadd_u_32(dynamic raw); @protected - List? dco_decode_opt_list_String(dynamic raw); + VideoTerminalReason? dco_decode_opt_box_autoadd_video_terminal_reason( + dynamic raw); @protected - Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); + List? dco_decode_opt_list_String(dynamic raw); @protected - ( - FrontendNotify, - bool - ) dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool( - dynamic raw); + Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); @protected (bool, bool, double) dco_decode_record_bool_bool_f_32(dynamic raw); @@ -532,12 +549,61 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_u_8(dynamic raw); + @protected + U8Array16 dco_decode_u_8_array_16(dynamic raw); + @protected void dco_decode_unit(dynamic raw); @protected BigInt dco_decode_usize(dynamic raw); + @protected + VideoCapabilities dco_decode_video_capabilities(dynamic raw); + + @protected + VideoCapabilityAvailability dco_decode_video_capability_availability( + dynamic raw); + + @protected + VideoCodec dco_decode_video_codec(dynamic raw); + + @protected + VideoLifecycleEvent dco_decode_video_lifecycle_event(dynamic raw); + + @protected + VideoMediaFormat dco_decode_video_media_format(dynamic raw); + + @protected + VideoPhase dco_decode_video_phase(dynamic raw); + + @protected + VideoRole dco_decode_video_role(dynamic raw); + + @protected + VideoSessionId dco_decode_video_session_id(dynamic raw); + + @protected + VideoSessionIdentity dco_decode_video_session_identity(dynamic raw); + + @protected + VideoSource dco_decode_video_source(dynamic raw); + + @protected + VideoSourceCapability dco_decode_video_source_capability(dynamic raw); + + @protected + VideoStartOutcome dco_decode_video_start_outcome(dynamic raw); + + @protected + VideoStopOutcome dco_decode_video_stop_outcome(dynamic raw); + + @protected + VideoTerminalReason dco_decode_video_terminal_reason(dynamic raw); + + @protected + VideoUnavailable dco_decode_video_unavailable(dynamic raw); + @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); @@ -825,6 +891,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); + @protected + VideoMediaFormat sse_decode_box_autoadd_video_media_format( + SseDeserializer deserializer); + + @protected + VideoSessionIdentity sse_decode_box_autoadd_video_session_identity( + SseDeserializer deserializer); + + @protected + VideoTerminalReason sse_decode_box_autoadd_video_terminal_reason( + SseDeserializer deserializer); + + @protected + VideoUnavailable sse_decode_box_autoadd_video_unavailable( + SseDeserializer deserializer); + @protected CallState sse_decode_call_state(SseDeserializer deserializer); @@ -861,6 +943,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { List<(String, Uint8List)> sse_decode_list_record_string_list_prim_u_8_strict( SseDeserializer deserializer); + @protected + List sse_decode_list_video_media_format( + SseDeserializer deserializer); + + @protected + List sse_decode_list_video_source_capability( + SseDeserializer deserializer); + @protected ManagerState sse_decode_manager_state(SseDeserializer deserializer); @@ -889,17 +979,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); @protected - List? sse_decode_opt_list_String(SseDeserializer deserializer); + VideoTerminalReason? sse_decode_opt_box_autoadd_video_terminal_reason( + SseDeserializer deserializer); @protected - Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); + List? sse_decode_opt_list_String(SseDeserializer deserializer); @protected - ( - FrontendNotify, - bool - ) sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool( - SseDeserializer deserializer); + Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); @protected (bool, bool, double) sse_decode_record_bool_bool_f_32( @@ -944,12 +1031,66 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_u_8(SseDeserializer deserializer); + @protected + U8Array16 sse_decode_u_8_array_16(SseDeserializer deserializer); + @protected void sse_decode_unit(SseDeserializer deserializer); @protected BigInt sse_decode_usize(SseDeserializer deserializer); + @protected + VideoCapabilities sse_decode_video_capabilities(SseDeserializer deserializer); + + @protected + VideoCapabilityAvailability sse_decode_video_capability_availability( + SseDeserializer deserializer); + + @protected + VideoCodec sse_decode_video_codec(SseDeserializer deserializer); + + @protected + VideoLifecycleEvent sse_decode_video_lifecycle_event( + SseDeserializer deserializer); + + @protected + VideoMediaFormat sse_decode_video_media_format(SseDeserializer deserializer); + + @protected + VideoPhase sse_decode_video_phase(SseDeserializer deserializer); + + @protected + VideoRole sse_decode_video_role(SseDeserializer deserializer); + + @protected + VideoSessionId sse_decode_video_session_id(SseDeserializer deserializer); + + @protected + VideoSessionIdentity sse_decode_video_session_identity( + SseDeserializer deserializer); + + @protected + VideoSource sse_decode_video_source(SseDeserializer deserializer); + + @protected + VideoSourceCapability sse_decode_video_source_capability( + SseDeserializer deserializer); + + @protected + VideoStartOutcome sse_decode_video_start_outcome( + SseDeserializer deserializer); + + @protected + VideoStopOutcome sse_decode_video_stop_outcome(SseDeserializer deserializer); + + @protected + VideoTerminalReason sse_decode_video_terminal_reason( + SseDeserializer deserializer); + + @protected + VideoUnavailable sse_decode_video_unavailable(SseDeserializer deserializer); + @protected void sse_encode_AnyhowException( AnyhowException self, SseSerializer serializer); @@ -1143,12 +1284,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_DartFn_Inputs_manager_state_Output_unit_AnyhowException( FutureOr Function(ManagerState) self, SseSerializer serializer); - @protected - void - sse_encode_DartFn_Inputs_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool_Output_unit_AnyhowException( - FutureOr Function((FrontendNotify, bool)) self, - SseSerializer serializer); - @protected void sse_encode_DartFn_Inputs_record_string_opt_list_prim_u_8_strict_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_Output_bool_AnyhowException( @@ -1171,6 +1306,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { FutureOr> Function(void) self, SseSerializer serializer); + @protected + void + sse_encode_DartFn_Inputs_video_lifecycle_event_Output_unit_AnyhowException( + FutureOr Function(VideoLifecycleEvent) self, + SseSerializer serializer); + @protected void sse_encode_DartOpaque(Object self, SseSerializer serializer); @@ -1285,6 +1426,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_video_media_format( + VideoMediaFormat self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_video_session_identity( + VideoSessionIdentity self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_video_terminal_reason( + VideoTerminalReason self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_video_unavailable( + VideoUnavailable self, SseSerializer serializer); + @protected void sse_encode_call_state(CallState self, SseSerializer serializer); @@ -1323,6 +1480,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_list_record_string_list_prim_u_8_strict( List<(String, Uint8List)> self, SseSerializer serializer); + @protected + void sse_encode_list_video_media_format( + List self, SseSerializer serializer); + + @protected + void sse_encode_list_video_source_capability( + List self, SseSerializer serializer); + @protected void sse_encode_manager_state(ManagerState self, SseSerializer serializer); @@ -1350,6 +1515,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_video_terminal_reason( + VideoTerminalReason? self, SseSerializer serializer); + @protected void sse_encode_opt_list_String(List? self, SseSerializer serializer); @@ -1357,11 +1526,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_opt_list_prim_u_8_strict( Uint8List? self, SseSerializer serializer); - @protected - void - sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool( - (FrontendNotify, bool) self, SseSerializer serializer); - @protected void sse_encode_record_bool_bool_f_32( (bool, bool, double) self, SseSerializer serializer); @@ -1401,11 +1565,70 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_u_8(int self, SseSerializer serializer); + @protected + void sse_encode_u_8_array_16(U8Array16 self, SseSerializer serializer); + @protected void sse_encode_unit(void self, SseSerializer serializer); @protected void sse_encode_usize(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_video_capabilities( + VideoCapabilities self, SseSerializer serializer); + + @protected + void sse_encode_video_capability_availability( + VideoCapabilityAvailability self, SseSerializer serializer); + + @protected + void sse_encode_video_codec(VideoCodec self, SseSerializer serializer); + + @protected + void sse_encode_video_lifecycle_event( + VideoLifecycleEvent self, SseSerializer serializer); + + @protected + void sse_encode_video_media_format( + VideoMediaFormat self, SseSerializer serializer); + + @protected + void sse_encode_video_phase(VideoPhase self, SseSerializer serializer); + + @protected + void sse_encode_video_role(VideoRole self, SseSerializer serializer); + + @protected + void sse_encode_video_session_id( + VideoSessionId self, SseSerializer serializer); + + @protected + void sse_encode_video_session_identity( + VideoSessionIdentity self, SseSerializer serializer); + + @protected + void sse_encode_video_source(VideoSource self, SseSerializer serializer); + + @protected + void sse_encode_video_source_capability( + VideoSourceCapability self, SseSerializer serializer); + + @protected + void sse_encode_video_start_outcome( + VideoStartOutcome self, SseSerializer serializer); + + @protected + void sse_encode_video_stop_outcome( + VideoStopOutcome self, SseSerializer serializer); + + @protected + void sse_encode_video_terminal_reason( + VideoTerminalReason self, SseSerializer serializer); + + @protected + void sse_encode_video_unavailable( + VideoUnavailable self, SseSerializer serializer); } // Section: wire_class diff --git a/lib/core/rust/frb_generated.web.dart b/lib/core/rust/frb_generated.web.dart index f4382b60..5ca0bb2b 100644 --- a/lib/core/rust/frb_generated.web.dart +++ b/lib/core/rust/frb_generated.web.dart @@ -282,11 +282,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dco_decode_DartFn_Inputs_manager_state_Output_unit_AnyhowException( dynamic raw); - @protected - FutureOr Function((FrontendNotify, bool)) - dco_decode_DartFn_Inputs_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool_Output_unit_AnyhowException( - dynamic raw); - @protected FutureOr Function((String, Uint8List?, FrontendNotify)) dco_decode_DartFn_Inputs_record_string_opt_list_prim_u_8_strict_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_Output_bool_AnyhowException( @@ -307,6 +302,11 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { dco_decode_DartFn_Inputs_unit_Output_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact_AnyhowException( dynamic raw); + @protected + FutureOr Function(VideoLifecycleEvent) + dco_decode_DartFn_Inputs_video_lifecycle_event_Output_unit_AnyhowException( + dynamic raw); + @protected Object dco_decode_DartOpaque(dynamic raw); @@ -420,6 +420,19 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_box_autoadd_u_32(dynamic raw); + @protected + VideoMediaFormat dco_decode_box_autoadd_video_media_format(dynamic raw); + + @protected + VideoSessionIdentity dco_decode_box_autoadd_video_session_identity( + dynamic raw); + + @protected + VideoTerminalReason dco_decode_box_autoadd_video_terminal_reason(dynamic raw); + + @protected + VideoUnavailable dco_decode_box_autoadd_video_unavailable(dynamic raw); + @protected CallState dco_decode_call_state(dynamic raw); @@ -456,6 +469,13 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { List<(String, Uint8List)> dco_decode_list_record_string_list_prim_u_8_strict( dynamic raw); + @protected + List dco_decode_list_video_media_format(dynamic raw); + + @protected + List dco_decode_list_video_source_capability( + dynamic raw); + @protected ManagerState dco_decode_manager_state(dynamic raw); @@ -482,17 +502,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { int? dco_decode_opt_box_autoadd_u_32(dynamic raw); @protected - List? dco_decode_opt_list_String(dynamic raw); + VideoTerminalReason? dco_decode_opt_box_autoadd_video_terminal_reason( + dynamic raw); @protected - Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); + List? dco_decode_opt_list_String(dynamic raw); @protected - ( - FrontendNotify, - bool - ) dco_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool( - dynamic raw); + Uint8List? dco_decode_opt_list_prim_u_8_strict(dynamic raw); @protected (bool, bool, double) dco_decode_record_bool_bool_f_32(dynamic raw); @@ -534,12 +551,61 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int dco_decode_u_8(dynamic raw); + @protected + U8Array16 dco_decode_u_8_array_16(dynamic raw); + @protected void dco_decode_unit(dynamic raw); @protected BigInt dco_decode_usize(dynamic raw); + @protected + VideoCapabilities dco_decode_video_capabilities(dynamic raw); + + @protected + VideoCapabilityAvailability dco_decode_video_capability_availability( + dynamic raw); + + @protected + VideoCodec dco_decode_video_codec(dynamic raw); + + @protected + VideoLifecycleEvent dco_decode_video_lifecycle_event(dynamic raw); + + @protected + VideoMediaFormat dco_decode_video_media_format(dynamic raw); + + @protected + VideoPhase dco_decode_video_phase(dynamic raw); + + @protected + VideoRole dco_decode_video_role(dynamic raw); + + @protected + VideoSessionId dco_decode_video_session_id(dynamic raw); + + @protected + VideoSessionIdentity dco_decode_video_session_identity(dynamic raw); + + @protected + VideoSource dco_decode_video_source(dynamic raw); + + @protected + VideoSourceCapability dco_decode_video_source_capability(dynamic raw); + + @protected + VideoStartOutcome dco_decode_video_start_outcome(dynamic raw); + + @protected + VideoStopOutcome dco_decode_video_stop_outcome(dynamic raw); + + @protected + VideoTerminalReason dco_decode_video_terminal_reason(dynamic raw); + + @protected + VideoUnavailable dco_decode_video_unavailable(dynamic raw); + @protected AnyhowException sse_decode_AnyhowException(SseDeserializer deserializer); @@ -827,6 +893,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_box_autoadd_u_32(SseDeserializer deserializer); + @protected + VideoMediaFormat sse_decode_box_autoadd_video_media_format( + SseDeserializer deserializer); + + @protected + VideoSessionIdentity sse_decode_box_autoadd_video_session_identity( + SseDeserializer deserializer); + + @protected + VideoTerminalReason sse_decode_box_autoadd_video_terminal_reason( + SseDeserializer deserializer); + + @protected + VideoUnavailable sse_decode_box_autoadd_video_unavailable( + SseDeserializer deserializer); + @protected CallState sse_decode_call_state(SseDeserializer deserializer); @@ -863,6 +945,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { List<(String, Uint8List)> sse_decode_list_record_string_list_prim_u_8_strict( SseDeserializer deserializer); + @protected + List sse_decode_list_video_media_format( + SseDeserializer deserializer); + + @protected + List sse_decode_list_video_source_capability( + SseDeserializer deserializer); + @protected ManagerState sse_decode_manager_state(SseDeserializer deserializer); @@ -891,17 +981,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { int? sse_decode_opt_box_autoadd_u_32(SseDeserializer deserializer); @protected - List? sse_decode_opt_list_String(SseDeserializer deserializer); + VideoTerminalReason? sse_decode_opt_box_autoadd_video_terminal_reason( + SseDeserializer deserializer); @protected - Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); + List? sse_decode_opt_list_String(SseDeserializer deserializer); @protected - ( - FrontendNotify, - bool - ) sse_decode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool( - SseDeserializer deserializer); + Uint8List? sse_decode_opt_list_prim_u_8_strict(SseDeserializer deserializer); @protected (bool, bool, double) sse_decode_record_bool_bool_f_32( @@ -946,12 +1033,66 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected int sse_decode_u_8(SseDeserializer deserializer); + @protected + U8Array16 sse_decode_u_8_array_16(SseDeserializer deserializer); + @protected void sse_decode_unit(SseDeserializer deserializer); @protected BigInt sse_decode_usize(SseDeserializer deserializer); + @protected + VideoCapabilities sse_decode_video_capabilities(SseDeserializer deserializer); + + @protected + VideoCapabilityAvailability sse_decode_video_capability_availability( + SseDeserializer deserializer); + + @protected + VideoCodec sse_decode_video_codec(SseDeserializer deserializer); + + @protected + VideoLifecycleEvent sse_decode_video_lifecycle_event( + SseDeserializer deserializer); + + @protected + VideoMediaFormat sse_decode_video_media_format(SseDeserializer deserializer); + + @protected + VideoPhase sse_decode_video_phase(SseDeserializer deserializer); + + @protected + VideoRole sse_decode_video_role(SseDeserializer deserializer); + + @protected + VideoSessionId sse_decode_video_session_id(SseDeserializer deserializer); + + @protected + VideoSessionIdentity sse_decode_video_session_identity( + SseDeserializer deserializer); + + @protected + VideoSource sse_decode_video_source(SseDeserializer deserializer); + + @protected + VideoSourceCapability sse_decode_video_source_capability( + SseDeserializer deserializer); + + @protected + VideoStartOutcome sse_decode_video_start_outcome( + SseDeserializer deserializer); + + @protected + VideoStopOutcome sse_decode_video_stop_outcome(SseDeserializer deserializer); + + @protected + VideoTerminalReason sse_decode_video_terminal_reason( + SseDeserializer deserializer); + + @protected + VideoUnavailable sse_decode_video_unavailable(SseDeserializer deserializer); + @protected void sse_encode_AnyhowException( AnyhowException self, SseSerializer serializer); @@ -1145,12 +1286,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_DartFn_Inputs_manager_state_Output_unit_AnyhowException( FutureOr Function(ManagerState) self, SseSerializer serializer); - @protected - void - sse_encode_DartFn_Inputs_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool_Output_unit_AnyhowException( - FutureOr Function((FrontendNotify, bool)) self, - SseSerializer serializer); - @protected void sse_encode_DartFn_Inputs_record_string_opt_list_prim_u_8_strict_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_Output_bool_AnyhowException( @@ -1173,6 +1308,12 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { FutureOr> Function(void) self, SseSerializer serializer); + @protected + void + sse_encode_DartFn_Inputs_video_lifecycle_event_Output_unit_AnyhowException( + FutureOr Function(VideoLifecycleEvent) self, + SseSerializer serializer); + @protected void sse_encode_DartOpaque(Object self, SseSerializer serializer); @@ -1287,6 +1428,22 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_box_autoadd_u_32(int self, SseSerializer serializer); + @protected + void sse_encode_box_autoadd_video_media_format( + VideoMediaFormat self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_video_session_identity( + VideoSessionIdentity self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_video_terminal_reason( + VideoTerminalReason self, SseSerializer serializer); + + @protected + void sse_encode_box_autoadd_video_unavailable( + VideoUnavailable self, SseSerializer serializer); + @protected void sse_encode_call_state(CallState self, SseSerializer serializer); @@ -1325,6 +1482,14 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_list_record_string_list_prim_u_8_strict( List<(String, Uint8List)> self, SseSerializer serializer); + @protected + void sse_encode_list_video_media_format( + List self, SseSerializer serializer); + + @protected + void sse_encode_list_video_source_capability( + List self, SseSerializer serializer); + @protected void sse_encode_manager_state(ManagerState self, SseSerializer serializer); @@ -1352,6 +1517,10 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_opt_box_autoadd_u_32(int? self, SseSerializer serializer); + @protected + void sse_encode_opt_box_autoadd_video_terminal_reason( + VideoTerminalReason? self, SseSerializer serializer); + @protected void sse_encode_opt_list_String(List? self, SseSerializer serializer); @@ -1359,11 +1528,6 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { void sse_encode_opt_list_prim_u_8_strict( Uint8List? self, SseSerializer serializer); - @protected - void - sse_encode_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool( - (FrontendNotify, bool) self, SseSerializer serializer); - @protected void sse_encode_record_bool_bool_f_32( (bool, bool, double) self, SseSerializer serializer); @@ -1403,11 +1567,70 @@ abstract class RustLibApiImplPlatform extends BaseApiImpl { @protected void sse_encode_u_8(int self, SseSerializer serializer); + @protected + void sse_encode_u_8_array_16(U8Array16 self, SseSerializer serializer); + @protected void sse_encode_unit(void self, SseSerializer serializer); @protected void sse_encode_usize(BigInt self, SseSerializer serializer); + + @protected + void sse_encode_video_capabilities( + VideoCapabilities self, SseSerializer serializer); + + @protected + void sse_encode_video_capability_availability( + VideoCapabilityAvailability self, SseSerializer serializer); + + @protected + void sse_encode_video_codec(VideoCodec self, SseSerializer serializer); + + @protected + void sse_encode_video_lifecycle_event( + VideoLifecycleEvent self, SseSerializer serializer); + + @protected + void sse_encode_video_media_format( + VideoMediaFormat self, SseSerializer serializer); + + @protected + void sse_encode_video_phase(VideoPhase self, SseSerializer serializer); + + @protected + void sse_encode_video_role(VideoRole self, SseSerializer serializer); + + @protected + void sse_encode_video_session_id( + VideoSessionId self, SseSerializer serializer); + + @protected + void sse_encode_video_session_identity( + VideoSessionIdentity self, SseSerializer serializer); + + @protected + void sse_encode_video_source(VideoSource self, SseSerializer serializer); + + @protected + void sse_encode_video_source_capability( + VideoSourceCapability self, SseSerializer serializer); + + @protected + void sse_encode_video_start_outcome( + VideoStartOutcome self, SseSerializer serializer); + + @protected + void sse_encode_video_stop_outcome( + VideoStopOutcome self, SseSerializer serializer); + + @protected + void sse_encode_video_terminal_reason( + VideoTerminalReason self, SseSerializer serializer); + + @protected + void sse_encode_video_unavailable( + VideoUnavailable self, SseSerializer serializer); } // Section: wire_class diff --git a/lib/core/rust/lib.dart b/lib/core/rust/lib.dart index d7867674..99ebdab0 100644 --- a/lib/core/rust/lib.dart +++ b/lib/core/rust/lib.dart @@ -4,6 +4,7 @@ // ignore_for_file: invalid_use_of_internal_member, unused_import, unnecessary_import import 'frb_generated.dart'; +import 'package:collection/collection.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `from` @@ -31,3 +32,17 @@ class AudioDevice { name == other.name && id == other.id; } + +class U8Array16 extends NonGrowableListView { + static const arraySize = 16; + + @internal + Uint8List get inner => _inner; + final Uint8List _inner; + + U8Array16(this._inner) + : assert(_inner.length == arraySize), + super(_inner); + + U8Array16.init() : this(Uint8List(arraySize)); +} diff --git a/lib/core/rust/overlay.dart b/lib/core/rust/overlay.dart index 8b2280cc..c3114850 100644 --- a/lib/core/rust/overlay.dart +++ b/lib/core/rust/overlay.dart @@ -6,7 +6,7 @@ import 'frb_generated.dart'; import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; -// These functions are ignored because they are not marked as `pub`: `_disable`, `_enable`, `_hide`, `_move_overlay`, `_show`, `controller`, `redraw`, `start_overlay` +// These functions are ignored because they are not marked as `pub`: `_disable`, `_enable`, `_hide`, `_move_overlay`, `_show` // These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `BACKGROUND_COLOR`, `CONNECTED`, `FONT_COLOR`, `FONT_HEIGHT`, `LATENCY`, `LOSS` // These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `deref`, `deref`, `deref`, `deref`, `deref`, `deref`, `initialize`, `initialize`, `initialize`, `initialize`, `initialize`, `initialize` @@ -32,6 +32,7 @@ abstract class Overlay implements RustOpaqueInterface { required int height}); // HINT: Make it `#[frb(sync)]` to let it become the default constructor of Dart class. + /// non-windows platforms don't have an overlay static Future newInstance( {required bool enabled, required int x, @@ -51,7 +52,7 @@ abstract class Overlay implements RustOpaqueInterface { backgroundColor: backgroundColor, fontColor: fontColor); - /// access the screen resolution for overlay positioning in the front end + /// non-windows platforms don't have an overlay (int, int) screenResolution(); /// change the background color of the overlay diff --git a/lib/core/rust/types.dart b/lib/core/rust/types.dart index 8336f663..51e2f9cf 100644 --- a/lib/core/rust/types.dart +++ b/lib/core/rust/types.dart @@ -9,8 +9,9 @@ import 'package:flutter_rust_bridge/flutter_rust_bridge_for_generated.dart'; import 'package:freezed_annotation/freezed_annotation.dart' hide protected; part 'types.freezed.dart'; -// These functions are ignored because they are not marked as `pub`: `clamp_contact_output_volume`, `contact_output_volume_from_parts`, `contact_output_volume_in_range`, `field_error`, `new`, `parse_bind_addresses`, `poison_field_error`, `relay_map_from_urls`, `serialize_timestamp_rfc3339_utc` -// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `minimum_bytes_needed`, `read_from`, `write_to` +// These functions are ignored because they are not marked as `pub`: `clamp_contact_output_volume`, `contact_output_volume_from_parts`, `contact_output_volume_in_range`, `field_error`, `formats`, `new`, `new`, `parse_bind_addresses`, `poison_field_error`, `prepare_video_sender`, `probe_video_capabilities`, `relay_map_from_urls`, `serialize_timestamp_rfc3339_utc`, `unavailable` +// These types are ignored because they are neither used by any `pub` functions nor (for structs and enums) marked `#[frb(unignore)]`: `ScreenshareConfigDisk` +// These function are ignored because they are on traits that is not defined in current crate (put an empty `#[frb]` on it to unignore): `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `assert_fields_are_eq`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `clone`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `eq`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `fmt`, `from`, `from`, `from`, `from`, `from`, `hash`, `minimum_bytes_needed`, `minimum_bytes_needed`, `minimum_bytes_needed`, `minimum_bytes_needed`, `minimum_bytes_needed`, `minimum_bytes_needed`, `minimum_bytes_needed`, `minimum_bytes_needed`, `read_from`, `read_from`, `read_from`, `read_from`, `read_from`, `read_from`, `read_from`, `read_from`, `speedy_convert_slice_endianness`, `speedy_convert_slice_endianness`, `speedy_flip_endianness`, `speedy_flip_endianness`, `speedy_is_primitive`, `speedy_is_primitive`, `speedy_is_primitive`, `speedy_is_primitive`, `speedy_slice_as_bytes`, `speedy_slice_as_bytes`, `speedy_slice_from_bytes`, `speedy_slice_from_bytes`, `write_to`, `write_to`, `write_to`, `write_to`, `write_to`, `write_to`, `write_to`, `write_to` // Rust type: RustOpaqueMoi> abstract class Capabilities implements RustOpaqueInterface { @@ -198,6 +199,8 @@ abstract class ScreenshareConfig implements RustOpaqueInterface { required int bitrate, required int framerate, int? height}); + + Future videoCapabilities(); } @freezed @@ -379,3 +382,228 @@ class Statistics { downloadBandwidth == other.downloadBandwidth && loss == other.loss; } + +class VideoCapabilities { + final VideoCapabilityAvailability send; + final VideoCapabilityAvailability receive; + final List sendSources; + final List receiveFormats; + + const VideoCapabilities({ + required this.send, + required this.receive, + required this.sendSources, + required this.receiveFormats, + }); + + @override + int get hashCode => + send.hashCode ^ + receive.hashCode ^ + sendSources.hashCode ^ + receiveFormats.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VideoCapabilities && + runtimeType == other.runtimeType && + send == other.send && + receive == other.receive && + sendSources == other.sendSources && + receiveFormats == other.receiveFormats; +} + +@freezed +sealed class VideoCapabilityAvailability with _$VideoCapabilityAvailability { + const VideoCapabilityAvailability._(); + + const factory VideoCapabilityAvailability.available() = + VideoCapabilityAvailability_Available; + const factory VideoCapabilityAvailability.unavailable( + VideoUnavailable field0, + ) = VideoCapabilityAvailability_Unavailable; +} + +enum VideoCodec { + h264, + hevc, + av1, + ; +} + +class VideoLifecycleEvent { + final VideoSessionIdentity identity; + final VideoRole role; + final VideoSource source; + final VideoPhase phase; + final VideoTerminalReason? terminalReason; + + const VideoLifecycleEvent({ + required this.identity, + required this.role, + required this.source, + required this.phase, + this.terminalReason, + }); + + @override + int get hashCode => + identity.hashCode ^ + role.hashCode ^ + source.hashCode ^ + phase.hashCode ^ + terminalReason.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VideoLifecycleEvent && + runtimeType == other.runtimeType && + identity == other.identity && + role == other.role && + source == other.source && + phase == other.phase && + terminalReason == other.terminalReason; +} + +@freezed +sealed class VideoMediaFormat with _$VideoMediaFormat { + const VideoMediaFormat._(); + + const factory VideoMediaFormat.mpegTs( + VideoCodec field0, + ) = VideoMediaFormat_MpegTs; +} + +enum VideoPhase { + offering, + waitingReady, + starting, + active, + stopping, + terminal, + ; +} + +enum VideoRole { + sender, + receiver, + ; +} + +class VideoSessionId { + final U8Array16 field0; + + const VideoSessionId({ + required this.field0, + }); + + @override + int get hashCode => field0.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VideoSessionId && + runtimeType == other.runtimeType && + field0 == other.field0; +} + +class VideoSessionIdentity { + final String peerId; + final VideoSessionId sessionId; + + const VideoSessionIdentity({ + required this.peerId, + required this.sessionId, + }); + + @override + int get hashCode => peerId.hashCode ^ sessionId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VideoSessionIdentity && + runtimeType == other.runtimeType && + peerId == other.peerId && + sessionId == other.sessionId; +} + +enum VideoSource { + display, + ; +} + +class VideoSourceCapability { + final VideoSource source; + final List formats; + + const VideoSourceCapability({ + required this.source, + required this.formats, + }); + + @override + int get hashCode => source.hashCode ^ formats.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is VideoSourceCapability && + runtimeType == other.runtimeType && + source == other.source && + formats == other.formats; +} + +@freezed +sealed class VideoStartOutcome with _$VideoStartOutcome { + const VideoStartOutcome._(); + + const factory VideoStartOutcome.requested( + VideoSessionIdentity field0, + ) = VideoStartOutcome_Requested; + const factory VideoStartOutcome.unavailable( + VideoUnavailable field0, + ) = VideoStartOutcome_Unavailable; + const factory VideoStartOutcome.noSession() = VideoStartOutcome_NoSession; + const factory VideoStartOutcome.alreadyActive() = + VideoStartOutcome_AlreadyActive; + const factory VideoStartOutcome.failed( + VideoTerminalReason field0, + ) = VideoStartOutcome_Failed; +} + +enum VideoStopOutcome { + stopped, + notFound, + ; +} + +enum VideoTerminalReason { + stopped, + rejected, + failed, + transportEnded, + teardown, + ; +} + +@freezed +sealed class VideoUnavailable with _$VideoUnavailable { + const VideoUnavailable._(); + + const factory VideoUnavailable.platformUnsupported() = + VideoUnavailable_PlatformUnsupported; + const factory VideoUnavailable.runtimeUnavailable() = + VideoUnavailable_RuntimeUnavailable; + const factory VideoUnavailable.sourceUnavailable( + VideoSource field0, + ) = VideoUnavailable_SourceUnavailable; + const factory VideoUnavailable.formatUnavailable( + VideoMediaFormat field0, + ) = VideoUnavailable_FormatUnavailable; + const factory VideoUnavailable.configurationUnavailable() = + VideoUnavailable_ConfigurationUnavailable; +} diff --git a/lib/core/rust/types.freezed.dart b/lib/core/rust/types.freezed.dart index 1b8faa42..4d0bdb5b 100644 --- a/lib/core/rust/types.freezed.dart +++ b/lib/core/rust/types.freezed.dart @@ -862,4 +862,1573 @@ class SessionStatus_Unknown extends SessionStatus { } } +/// @nodoc +mixin _$VideoCapabilityAvailability { + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoCapabilityAvailability); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'VideoCapabilityAvailability()'; + } +} + +/// @nodoc +class $VideoCapabilityAvailabilityCopyWith<$Res> { + $VideoCapabilityAvailabilityCopyWith(VideoCapabilityAvailability _, + $Res Function(VideoCapabilityAvailability) __); +} + +/// Adds pattern-matching-related methods to [VideoCapabilityAvailability]. +extension VideoCapabilityAvailabilityPatterns on VideoCapabilityAvailability { + /// A variant of `map` that fallback to returning `orElse`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeMap({ + TResult Function(VideoCapabilityAvailability_Available value)? available, + TResult Function(VideoCapabilityAvailability_Unavailable value)? + unavailable, + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case VideoCapabilityAvailability_Available() when available != null: + return available(_that); + case VideoCapabilityAvailability_Unavailable() when unavailable != null: + return unavailable(_that); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// Callbacks receives the raw object, upcasted. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case final Subclass2 value: + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult map({ + required TResult Function(VideoCapabilityAvailability_Available value) + available, + required TResult Function(VideoCapabilityAvailability_Unavailable value) + unavailable, + }) { + final _that = this; + switch (_that) { + case VideoCapabilityAvailability_Available(): + return available(_that); + case VideoCapabilityAvailability_Unavailable(): + return unavailable(_that); + } + } + + /// A variant of `map` that fallback to returning `null`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(VideoCapabilityAvailability_Available value)? available, + TResult? Function(VideoCapabilityAvailability_Unavailable value)? + unavailable, + }) { + final _that = this; + switch (_that) { + case VideoCapabilityAvailability_Available() when available != null: + return available(_that); + case VideoCapabilityAvailability_Unavailable() when unavailable != null: + return unavailable(_that); + case _: + return null; + } + } + + /// A variant of `when` that fallback to an `orElse` callback. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? available, + TResult Function(VideoUnavailable field0)? unavailable, + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case VideoCapabilityAvailability_Available() when available != null: + return available(); + case VideoCapabilityAvailability_Unavailable() when unavailable != null: + return unavailable(_that.field0); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// As opposed to `map`, this offers destructuring. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case Subclass2(:final field2): + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult when({ + required TResult Function() available, + required TResult Function(VideoUnavailable field0) unavailable, + }) { + final _that = this; + switch (_that) { + case VideoCapabilityAvailability_Available(): + return available(); + case VideoCapabilityAvailability_Unavailable(): + return unavailable(_that.field0); + } + } + + /// A variant of `when` that fallback to returning `null` + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? available, + TResult? Function(VideoUnavailable field0)? unavailable, + }) { + final _that = this; + switch (_that) { + case VideoCapabilityAvailability_Available() when available != null: + return available(); + case VideoCapabilityAvailability_Unavailable() when unavailable != null: + return unavailable(_that.field0); + case _: + return null; + } + } +} + +/// @nodoc + +class VideoCapabilityAvailability_Available + extends VideoCapabilityAvailability { + const VideoCapabilityAvailability_Available() : super._(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoCapabilityAvailability_Available); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'VideoCapabilityAvailability.available()'; + } +} + +/// @nodoc + +class VideoCapabilityAvailability_Unavailable + extends VideoCapabilityAvailability { + const VideoCapabilityAvailability_Unavailable(this.field0) : super._(); + + final VideoUnavailable field0; + + /// Create a copy of VideoCapabilityAvailability + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $VideoCapabilityAvailability_UnavailableCopyWith< + VideoCapabilityAvailability_Unavailable> + get copyWith => _$VideoCapabilityAvailability_UnavailableCopyWithImpl< + VideoCapabilityAvailability_Unavailable>(this, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoCapabilityAvailability_Unavailable && + (identical(other.field0, field0) || other.field0 == field0)); + } + + @override + int get hashCode => Object.hash(runtimeType, field0); + + @override + String toString() { + return 'VideoCapabilityAvailability.unavailable(field0: $field0)'; + } +} + +/// @nodoc +abstract mixin class $VideoCapabilityAvailability_UnavailableCopyWith<$Res> + implements $VideoCapabilityAvailabilityCopyWith<$Res> { + factory $VideoCapabilityAvailability_UnavailableCopyWith( + VideoCapabilityAvailability_Unavailable value, + $Res Function(VideoCapabilityAvailability_Unavailable) _then) = + _$VideoCapabilityAvailability_UnavailableCopyWithImpl; + @useResult + $Res call({VideoUnavailable field0}); + + $VideoUnavailableCopyWith<$Res> get field0; +} + +/// @nodoc +class _$VideoCapabilityAvailability_UnavailableCopyWithImpl<$Res> + implements $VideoCapabilityAvailability_UnavailableCopyWith<$Res> { + _$VideoCapabilityAvailability_UnavailableCopyWithImpl(this._self, this._then); + + final VideoCapabilityAvailability_Unavailable _self; + final $Res Function(VideoCapabilityAvailability_Unavailable) _then; + + /// Create a copy of VideoCapabilityAvailability + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? field0 = null, + }) { + return _then(VideoCapabilityAvailability_Unavailable( + null == field0 + ? _self.field0 + : field0 // ignore: cast_nullable_to_non_nullable + as VideoUnavailable, + )); + } + + /// Create a copy of VideoCapabilityAvailability + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $VideoUnavailableCopyWith<$Res> get field0 { + return $VideoUnavailableCopyWith<$Res>(_self.field0, (value) { + return _then(_self.copyWith(field0: value)); + }); + } +} + +/// @nodoc +mixin _$VideoMediaFormat { + VideoCodec get field0; + + /// Create a copy of VideoMediaFormat + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $VideoMediaFormatCopyWith get copyWith => + _$VideoMediaFormatCopyWithImpl( + this as VideoMediaFormat, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoMediaFormat && + (identical(other.field0, field0) || other.field0 == field0)); + } + + @override + int get hashCode => Object.hash(runtimeType, field0); + + @override + String toString() { + return 'VideoMediaFormat(field0: $field0)'; + } +} + +/// @nodoc +abstract mixin class $VideoMediaFormatCopyWith<$Res> { + factory $VideoMediaFormatCopyWith( + VideoMediaFormat value, $Res Function(VideoMediaFormat) _then) = + _$VideoMediaFormatCopyWithImpl; + @useResult + $Res call({VideoCodec field0}); +} + +/// @nodoc +class _$VideoMediaFormatCopyWithImpl<$Res> + implements $VideoMediaFormatCopyWith<$Res> { + _$VideoMediaFormatCopyWithImpl(this._self, this._then); + + final VideoMediaFormat _self; + final $Res Function(VideoMediaFormat) _then; + + /// Create a copy of VideoMediaFormat + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? field0 = null, + }) { + return _then(_self.copyWith( + field0: null == field0 + ? _self.field0 + : field0 // ignore: cast_nullable_to_non_nullable + as VideoCodec, + )); + } +} + +/// Adds pattern-matching-related methods to [VideoMediaFormat]. +extension VideoMediaFormatPatterns on VideoMediaFormat { + /// A variant of `map` that fallback to returning `orElse`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeMap({ + TResult Function(VideoMediaFormat_MpegTs value)? mpegTs, + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case VideoMediaFormat_MpegTs() when mpegTs != null: + return mpegTs(_that); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// Callbacks receives the raw object, upcasted. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case final Subclass2 value: + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult map({ + required TResult Function(VideoMediaFormat_MpegTs value) mpegTs, + }) { + final _that = this; + switch (_that) { + case VideoMediaFormat_MpegTs(): + return mpegTs(_that); + } + } + + /// A variant of `map` that fallback to returning `null`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(VideoMediaFormat_MpegTs value)? mpegTs, + }) { + final _that = this; + switch (_that) { + case VideoMediaFormat_MpegTs() when mpegTs != null: + return mpegTs(_that); + case _: + return null; + } + } + + /// A variant of `when` that fallback to an `orElse` callback. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(VideoCodec field0)? mpegTs, + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case VideoMediaFormat_MpegTs() when mpegTs != null: + return mpegTs(_that.field0); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// As opposed to `map`, this offers destructuring. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case Subclass2(:final field2): + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult when({ + required TResult Function(VideoCodec field0) mpegTs, + }) { + final _that = this; + switch (_that) { + case VideoMediaFormat_MpegTs(): + return mpegTs(_that.field0); + } + } + + /// A variant of `when` that fallback to returning `null` + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(VideoCodec field0)? mpegTs, + }) { + final _that = this; + switch (_that) { + case VideoMediaFormat_MpegTs() when mpegTs != null: + return mpegTs(_that.field0); + case _: + return null; + } + } +} + +/// @nodoc + +class VideoMediaFormat_MpegTs extends VideoMediaFormat { + const VideoMediaFormat_MpegTs(this.field0) : super._(); + + @override + final VideoCodec field0; + + /// Create a copy of VideoMediaFormat + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $VideoMediaFormat_MpegTsCopyWith get copyWith => + _$VideoMediaFormat_MpegTsCopyWithImpl( + this, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoMediaFormat_MpegTs && + (identical(other.field0, field0) || other.field0 == field0)); + } + + @override + int get hashCode => Object.hash(runtimeType, field0); + + @override + String toString() { + return 'VideoMediaFormat.mpegTs(field0: $field0)'; + } +} + +/// @nodoc +abstract mixin class $VideoMediaFormat_MpegTsCopyWith<$Res> + implements $VideoMediaFormatCopyWith<$Res> { + factory $VideoMediaFormat_MpegTsCopyWith(VideoMediaFormat_MpegTs value, + $Res Function(VideoMediaFormat_MpegTs) _then) = + _$VideoMediaFormat_MpegTsCopyWithImpl; + @override + @useResult + $Res call({VideoCodec field0}); +} + +/// @nodoc +class _$VideoMediaFormat_MpegTsCopyWithImpl<$Res> + implements $VideoMediaFormat_MpegTsCopyWith<$Res> { + _$VideoMediaFormat_MpegTsCopyWithImpl(this._self, this._then); + + final VideoMediaFormat_MpegTs _self; + final $Res Function(VideoMediaFormat_MpegTs) _then; + + /// Create a copy of VideoMediaFormat + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $Res call({ + Object? field0 = null, + }) { + return _then(VideoMediaFormat_MpegTs( + null == field0 + ? _self.field0 + : field0 // ignore: cast_nullable_to_non_nullable + as VideoCodec, + )); + } +} + +/// @nodoc +mixin _$VideoStartOutcome { + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is VideoStartOutcome); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'VideoStartOutcome()'; + } +} + +/// @nodoc +class $VideoStartOutcomeCopyWith<$Res> { + $VideoStartOutcomeCopyWith( + VideoStartOutcome _, $Res Function(VideoStartOutcome) __); +} + +/// Adds pattern-matching-related methods to [VideoStartOutcome]. +extension VideoStartOutcomePatterns on VideoStartOutcome { + /// A variant of `map` that fallback to returning `orElse`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeMap({ + TResult Function(VideoStartOutcome_Requested value)? requested, + TResult Function(VideoStartOutcome_Unavailable value)? unavailable, + TResult Function(VideoStartOutcome_NoSession value)? noSession, + TResult Function(VideoStartOutcome_AlreadyActive value)? alreadyActive, + TResult Function(VideoStartOutcome_Failed value)? failed, + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case VideoStartOutcome_Requested() when requested != null: + return requested(_that); + case VideoStartOutcome_Unavailable() when unavailable != null: + return unavailable(_that); + case VideoStartOutcome_NoSession() when noSession != null: + return noSession(_that); + case VideoStartOutcome_AlreadyActive() when alreadyActive != null: + return alreadyActive(_that); + case VideoStartOutcome_Failed() when failed != null: + return failed(_that); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// Callbacks receives the raw object, upcasted. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case final Subclass2 value: + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult map({ + required TResult Function(VideoStartOutcome_Requested value) requested, + required TResult Function(VideoStartOutcome_Unavailable value) unavailable, + required TResult Function(VideoStartOutcome_NoSession value) noSession, + required TResult Function(VideoStartOutcome_AlreadyActive value) + alreadyActive, + required TResult Function(VideoStartOutcome_Failed value) failed, + }) { + final _that = this; + switch (_that) { + case VideoStartOutcome_Requested(): + return requested(_that); + case VideoStartOutcome_Unavailable(): + return unavailable(_that); + case VideoStartOutcome_NoSession(): + return noSession(_that); + case VideoStartOutcome_AlreadyActive(): + return alreadyActive(_that); + case VideoStartOutcome_Failed(): + return failed(_that); + } + } + + /// A variant of `map` that fallback to returning `null`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(VideoStartOutcome_Requested value)? requested, + TResult? Function(VideoStartOutcome_Unavailable value)? unavailable, + TResult? Function(VideoStartOutcome_NoSession value)? noSession, + TResult? Function(VideoStartOutcome_AlreadyActive value)? alreadyActive, + TResult? Function(VideoStartOutcome_Failed value)? failed, + }) { + final _that = this; + switch (_that) { + case VideoStartOutcome_Requested() when requested != null: + return requested(_that); + case VideoStartOutcome_Unavailable() when unavailable != null: + return unavailable(_that); + case VideoStartOutcome_NoSession() when noSession != null: + return noSession(_that); + case VideoStartOutcome_AlreadyActive() when alreadyActive != null: + return alreadyActive(_that); + case VideoStartOutcome_Failed() when failed != null: + return failed(_that); + case _: + return null; + } + } + + /// A variant of `when` that fallback to an `orElse` callback. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeWhen({ + TResult Function(VideoSessionIdentity field0)? requested, + TResult Function(VideoUnavailable field0)? unavailable, + TResult Function()? noSession, + TResult Function()? alreadyActive, + TResult Function(VideoTerminalReason field0)? failed, + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case VideoStartOutcome_Requested() when requested != null: + return requested(_that.field0); + case VideoStartOutcome_Unavailable() when unavailable != null: + return unavailable(_that.field0); + case VideoStartOutcome_NoSession() when noSession != null: + return noSession(); + case VideoStartOutcome_AlreadyActive() when alreadyActive != null: + return alreadyActive(); + case VideoStartOutcome_Failed() when failed != null: + return failed(_that.field0); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// As opposed to `map`, this offers destructuring. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case Subclass2(:final field2): + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult when({ + required TResult Function(VideoSessionIdentity field0) requested, + required TResult Function(VideoUnavailable field0) unavailable, + required TResult Function() noSession, + required TResult Function() alreadyActive, + required TResult Function(VideoTerminalReason field0) failed, + }) { + final _that = this; + switch (_that) { + case VideoStartOutcome_Requested(): + return requested(_that.field0); + case VideoStartOutcome_Unavailable(): + return unavailable(_that.field0); + case VideoStartOutcome_NoSession(): + return noSession(); + case VideoStartOutcome_AlreadyActive(): + return alreadyActive(); + case VideoStartOutcome_Failed(): + return failed(_that.field0); + } + } + + /// A variant of `when` that fallback to returning `null` + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function(VideoSessionIdentity field0)? requested, + TResult? Function(VideoUnavailable field0)? unavailable, + TResult? Function()? noSession, + TResult? Function()? alreadyActive, + TResult? Function(VideoTerminalReason field0)? failed, + }) { + final _that = this; + switch (_that) { + case VideoStartOutcome_Requested() when requested != null: + return requested(_that.field0); + case VideoStartOutcome_Unavailable() when unavailable != null: + return unavailable(_that.field0); + case VideoStartOutcome_NoSession() when noSession != null: + return noSession(); + case VideoStartOutcome_AlreadyActive() when alreadyActive != null: + return alreadyActive(); + case VideoStartOutcome_Failed() when failed != null: + return failed(_that.field0); + case _: + return null; + } + } +} + +/// @nodoc + +class VideoStartOutcome_Requested extends VideoStartOutcome { + const VideoStartOutcome_Requested(this.field0) : super._(); + + final VideoSessionIdentity field0; + + /// Create a copy of VideoStartOutcome + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $VideoStartOutcome_RequestedCopyWith + get copyWith => _$VideoStartOutcome_RequestedCopyWithImpl< + VideoStartOutcome_Requested>(this, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoStartOutcome_Requested && + (identical(other.field0, field0) || other.field0 == field0)); + } + + @override + int get hashCode => Object.hash(runtimeType, field0); + + @override + String toString() { + return 'VideoStartOutcome.requested(field0: $field0)'; + } +} + +/// @nodoc +abstract mixin class $VideoStartOutcome_RequestedCopyWith<$Res> + implements $VideoStartOutcomeCopyWith<$Res> { + factory $VideoStartOutcome_RequestedCopyWith( + VideoStartOutcome_Requested value, + $Res Function(VideoStartOutcome_Requested) _then) = + _$VideoStartOutcome_RequestedCopyWithImpl; + @useResult + $Res call({VideoSessionIdentity field0}); +} + +/// @nodoc +class _$VideoStartOutcome_RequestedCopyWithImpl<$Res> + implements $VideoStartOutcome_RequestedCopyWith<$Res> { + _$VideoStartOutcome_RequestedCopyWithImpl(this._self, this._then); + + final VideoStartOutcome_Requested _self; + final $Res Function(VideoStartOutcome_Requested) _then; + + /// Create a copy of VideoStartOutcome + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? field0 = null, + }) { + return _then(VideoStartOutcome_Requested( + null == field0 + ? _self.field0 + : field0 // ignore: cast_nullable_to_non_nullable + as VideoSessionIdentity, + )); + } +} + +/// @nodoc + +class VideoStartOutcome_Unavailable extends VideoStartOutcome { + const VideoStartOutcome_Unavailable(this.field0) : super._(); + + final VideoUnavailable field0; + + /// Create a copy of VideoStartOutcome + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $VideoStartOutcome_UnavailableCopyWith + get copyWith => _$VideoStartOutcome_UnavailableCopyWithImpl< + VideoStartOutcome_Unavailable>(this, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoStartOutcome_Unavailable && + (identical(other.field0, field0) || other.field0 == field0)); + } + + @override + int get hashCode => Object.hash(runtimeType, field0); + + @override + String toString() { + return 'VideoStartOutcome.unavailable(field0: $field0)'; + } +} + +/// @nodoc +abstract mixin class $VideoStartOutcome_UnavailableCopyWith<$Res> + implements $VideoStartOutcomeCopyWith<$Res> { + factory $VideoStartOutcome_UnavailableCopyWith( + VideoStartOutcome_Unavailable value, + $Res Function(VideoStartOutcome_Unavailable) _then) = + _$VideoStartOutcome_UnavailableCopyWithImpl; + @useResult + $Res call({VideoUnavailable field0}); + + $VideoUnavailableCopyWith<$Res> get field0; +} + +/// @nodoc +class _$VideoStartOutcome_UnavailableCopyWithImpl<$Res> + implements $VideoStartOutcome_UnavailableCopyWith<$Res> { + _$VideoStartOutcome_UnavailableCopyWithImpl(this._self, this._then); + + final VideoStartOutcome_Unavailable _self; + final $Res Function(VideoStartOutcome_Unavailable) _then; + + /// Create a copy of VideoStartOutcome + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? field0 = null, + }) { + return _then(VideoStartOutcome_Unavailable( + null == field0 + ? _self.field0 + : field0 // ignore: cast_nullable_to_non_nullable + as VideoUnavailable, + )); + } + + /// Create a copy of VideoStartOutcome + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $VideoUnavailableCopyWith<$Res> get field0 { + return $VideoUnavailableCopyWith<$Res>(_self.field0, (value) { + return _then(_self.copyWith(field0: value)); + }); + } +} + +/// @nodoc + +class VideoStartOutcome_NoSession extends VideoStartOutcome { + const VideoStartOutcome_NoSession() : super._(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoStartOutcome_NoSession); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'VideoStartOutcome.noSession()'; + } +} + +/// @nodoc + +class VideoStartOutcome_AlreadyActive extends VideoStartOutcome { + const VideoStartOutcome_AlreadyActive() : super._(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoStartOutcome_AlreadyActive); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'VideoStartOutcome.alreadyActive()'; + } +} + +/// @nodoc + +class VideoStartOutcome_Failed extends VideoStartOutcome { + const VideoStartOutcome_Failed(this.field0) : super._(); + + final VideoTerminalReason field0; + + /// Create a copy of VideoStartOutcome + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $VideoStartOutcome_FailedCopyWith get copyWith => + _$VideoStartOutcome_FailedCopyWithImpl( + this, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoStartOutcome_Failed && + (identical(other.field0, field0) || other.field0 == field0)); + } + + @override + int get hashCode => Object.hash(runtimeType, field0); + + @override + String toString() { + return 'VideoStartOutcome.failed(field0: $field0)'; + } +} + +/// @nodoc +abstract mixin class $VideoStartOutcome_FailedCopyWith<$Res> + implements $VideoStartOutcomeCopyWith<$Res> { + factory $VideoStartOutcome_FailedCopyWith(VideoStartOutcome_Failed value, + $Res Function(VideoStartOutcome_Failed) _then) = + _$VideoStartOutcome_FailedCopyWithImpl; + @useResult + $Res call({VideoTerminalReason field0}); +} + +/// @nodoc +class _$VideoStartOutcome_FailedCopyWithImpl<$Res> + implements $VideoStartOutcome_FailedCopyWith<$Res> { + _$VideoStartOutcome_FailedCopyWithImpl(this._self, this._then); + + final VideoStartOutcome_Failed _self; + final $Res Function(VideoStartOutcome_Failed) _then; + + /// Create a copy of VideoStartOutcome + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? field0 = null, + }) { + return _then(VideoStartOutcome_Failed( + null == field0 + ? _self.field0 + : field0 // ignore: cast_nullable_to_non_nullable + as VideoTerminalReason, + )); + } +} + +/// @nodoc +mixin _$VideoUnavailable { + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is VideoUnavailable); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'VideoUnavailable()'; + } +} + +/// @nodoc +class $VideoUnavailableCopyWith<$Res> { + $VideoUnavailableCopyWith( + VideoUnavailable _, $Res Function(VideoUnavailable) __); +} + +/// Adds pattern-matching-related methods to [VideoUnavailable]. +extension VideoUnavailablePatterns on VideoUnavailable { + /// A variant of `map` that fallback to returning `orElse`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeMap({ + TResult Function(VideoUnavailable_PlatformUnsupported value)? + platformUnsupported, + TResult Function(VideoUnavailable_RuntimeUnavailable value)? + runtimeUnavailable, + TResult Function(VideoUnavailable_SourceUnavailable value)? + sourceUnavailable, + TResult Function(VideoUnavailable_FormatUnavailable value)? + formatUnavailable, + TResult Function(VideoUnavailable_ConfigurationUnavailable value)? + configurationUnavailable, + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case VideoUnavailable_PlatformUnsupported() + when platformUnsupported != null: + return platformUnsupported(_that); + case VideoUnavailable_RuntimeUnavailable() + when runtimeUnavailable != null: + return runtimeUnavailable(_that); + case VideoUnavailable_SourceUnavailable() when sourceUnavailable != null: + return sourceUnavailable(_that); + case VideoUnavailable_FormatUnavailable() when formatUnavailable != null: + return formatUnavailable(_that); + case VideoUnavailable_ConfigurationUnavailable() + when configurationUnavailable != null: + return configurationUnavailable(_that); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// Callbacks receives the raw object, upcasted. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case final Subclass2 value: + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult map({ + required TResult Function(VideoUnavailable_PlatformUnsupported value) + platformUnsupported, + required TResult Function(VideoUnavailable_RuntimeUnavailable value) + runtimeUnavailable, + required TResult Function(VideoUnavailable_SourceUnavailable value) + sourceUnavailable, + required TResult Function(VideoUnavailable_FormatUnavailable value) + formatUnavailable, + required TResult Function(VideoUnavailable_ConfigurationUnavailable value) + configurationUnavailable, + }) { + final _that = this; + switch (_that) { + case VideoUnavailable_PlatformUnsupported(): + return platformUnsupported(_that); + case VideoUnavailable_RuntimeUnavailable(): + return runtimeUnavailable(_that); + case VideoUnavailable_SourceUnavailable(): + return sourceUnavailable(_that); + case VideoUnavailable_FormatUnavailable(): + return formatUnavailable(_that); + case VideoUnavailable_ConfigurationUnavailable(): + return configurationUnavailable(_that); + } + } + + /// A variant of `map` that fallback to returning `null`. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case final Subclass value: + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? mapOrNull({ + TResult? Function(VideoUnavailable_PlatformUnsupported value)? + platformUnsupported, + TResult? Function(VideoUnavailable_RuntimeUnavailable value)? + runtimeUnavailable, + TResult? Function(VideoUnavailable_SourceUnavailable value)? + sourceUnavailable, + TResult? Function(VideoUnavailable_FormatUnavailable value)? + formatUnavailable, + TResult? Function(VideoUnavailable_ConfigurationUnavailable value)? + configurationUnavailable, + }) { + final _that = this; + switch (_that) { + case VideoUnavailable_PlatformUnsupported() + when platformUnsupported != null: + return platformUnsupported(_that); + case VideoUnavailable_RuntimeUnavailable() + when runtimeUnavailable != null: + return runtimeUnavailable(_that); + case VideoUnavailable_SourceUnavailable() when sourceUnavailable != null: + return sourceUnavailable(_that); + case VideoUnavailable_FormatUnavailable() when formatUnavailable != null: + return formatUnavailable(_that); + case VideoUnavailable_ConfigurationUnavailable() + when configurationUnavailable != null: + return configurationUnavailable(_that); + case _: + return null; + } + } + + /// A variant of `when` that fallback to an `orElse` callback. + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return orElse(); + /// } + /// ``` + + @optionalTypeArgs + TResult maybeWhen({ + TResult Function()? platformUnsupported, + TResult Function()? runtimeUnavailable, + TResult Function(VideoSource field0)? sourceUnavailable, + TResult Function(VideoMediaFormat field0)? formatUnavailable, + TResult Function()? configurationUnavailable, + required TResult orElse(), + }) { + final _that = this; + switch (_that) { + case VideoUnavailable_PlatformUnsupported() + when platformUnsupported != null: + return platformUnsupported(); + case VideoUnavailable_RuntimeUnavailable() + when runtimeUnavailable != null: + return runtimeUnavailable(); + case VideoUnavailable_SourceUnavailable() when sourceUnavailable != null: + return sourceUnavailable(_that.field0); + case VideoUnavailable_FormatUnavailable() when formatUnavailable != null: + return formatUnavailable(_that.field0); + case VideoUnavailable_ConfigurationUnavailable() + when configurationUnavailable != null: + return configurationUnavailable(); + case _: + return orElse(); + } + } + + /// A `switch`-like method, using callbacks. + /// + /// As opposed to `map`, this offers destructuring. + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case Subclass2(:final field2): + /// return ...; + /// } + /// ``` + + @optionalTypeArgs + TResult when({ + required TResult Function() platformUnsupported, + required TResult Function() runtimeUnavailable, + required TResult Function(VideoSource field0) sourceUnavailable, + required TResult Function(VideoMediaFormat field0) formatUnavailable, + required TResult Function() configurationUnavailable, + }) { + final _that = this; + switch (_that) { + case VideoUnavailable_PlatformUnsupported(): + return platformUnsupported(); + case VideoUnavailable_RuntimeUnavailable(): + return runtimeUnavailable(); + case VideoUnavailable_SourceUnavailable(): + return sourceUnavailable(_that.field0); + case VideoUnavailable_FormatUnavailable(): + return formatUnavailable(_that.field0); + case VideoUnavailable_ConfigurationUnavailable(): + return configurationUnavailable(); + } + } + + /// A variant of `when` that fallback to returning `null` + /// + /// It is equivalent to doing: + /// ```dart + /// switch (sealedClass) { + /// case Subclass(:final field): + /// return ...; + /// case _: + /// return null; + /// } + /// ``` + + @optionalTypeArgs + TResult? whenOrNull({ + TResult? Function()? platformUnsupported, + TResult? Function()? runtimeUnavailable, + TResult? Function(VideoSource field0)? sourceUnavailable, + TResult? Function(VideoMediaFormat field0)? formatUnavailable, + TResult? Function()? configurationUnavailable, + }) { + final _that = this; + switch (_that) { + case VideoUnavailable_PlatformUnsupported() + when platformUnsupported != null: + return platformUnsupported(); + case VideoUnavailable_RuntimeUnavailable() + when runtimeUnavailable != null: + return runtimeUnavailable(); + case VideoUnavailable_SourceUnavailable() when sourceUnavailable != null: + return sourceUnavailable(_that.field0); + case VideoUnavailable_FormatUnavailable() when formatUnavailable != null: + return formatUnavailable(_that.field0); + case VideoUnavailable_ConfigurationUnavailable() + when configurationUnavailable != null: + return configurationUnavailable(); + case _: + return null; + } + } +} + +/// @nodoc + +class VideoUnavailable_PlatformUnsupported extends VideoUnavailable { + const VideoUnavailable_PlatformUnsupported() : super._(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoUnavailable_PlatformUnsupported); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'VideoUnavailable.platformUnsupported()'; + } +} + +/// @nodoc + +class VideoUnavailable_RuntimeUnavailable extends VideoUnavailable { + const VideoUnavailable_RuntimeUnavailable() : super._(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoUnavailable_RuntimeUnavailable); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'VideoUnavailable.runtimeUnavailable()'; + } +} + +/// @nodoc + +class VideoUnavailable_SourceUnavailable extends VideoUnavailable { + const VideoUnavailable_SourceUnavailable(this.field0) : super._(); + + final VideoSource field0; + + /// Create a copy of VideoUnavailable + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $VideoUnavailable_SourceUnavailableCopyWith< + VideoUnavailable_SourceUnavailable> + get copyWith => _$VideoUnavailable_SourceUnavailableCopyWithImpl< + VideoUnavailable_SourceUnavailable>(this, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoUnavailable_SourceUnavailable && + (identical(other.field0, field0) || other.field0 == field0)); + } + + @override + int get hashCode => Object.hash(runtimeType, field0); + + @override + String toString() { + return 'VideoUnavailable.sourceUnavailable(field0: $field0)'; + } +} + +/// @nodoc +abstract mixin class $VideoUnavailable_SourceUnavailableCopyWith<$Res> + implements $VideoUnavailableCopyWith<$Res> { + factory $VideoUnavailable_SourceUnavailableCopyWith( + VideoUnavailable_SourceUnavailable value, + $Res Function(VideoUnavailable_SourceUnavailable) _then) = + _$VideoUnavailable_SourceUnavailableCopyWithImpl; + @useResult + $Res call({VideoSource field0}); +} + +/// @nodoc +class _$VideoUnavailable_SourceUnavailableCopyWithImpl<$Res> + implements $VideoUnavailable_SourceUnavailableCopyWith<$Res> { + _$VideoUnavailable_SourceUnavailableCopyWithImpl(this._self, this._then); + + final VideoUnavailable_SourceUnavailable _self; + final $Res Function(VideoUnavailable_SourceUnavailable) _then; + + /// Create a copy of VideoUnavailable + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? field0 = null, + }) { + return _then(VideoUnavailable_SourceUnavailable( + null == field0 + ? _self.field0 + : field0 // ignore: cast_nullable_to_non_nullable + as VideoSource, + )); + } +} + +/// @nodoc + +class VideoUnavailable_FormatUnavailable extends VideoUnavailable { + const VideoUnavailable_FormatUnavailable(this.field0) : super._(); + + final VideoMediaFormat field0; + + /// Create a copy of VideoUnavailable + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $VideoUnavailable_FormatUnavailableCopyWith< + VideoUnavailable_FormatUnavailable> + get copyWith => _$VideoUnavailable_FormatUnavailableCopyWithImpl< + VideoUnavailable_FormatUnavailable>(this, _$identity); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoUnavailable_FormatUnavailable && + (identical(other.field0, field0) || other.field0 == field0)); + } + + @override + int get hashCode => Object.hash(runtimeType, field0); + + @override + String toString() { + return 'VideoUnavailable.formatUnavailable(field0: $field0)'; + } +} + +/// @nodoc +abstract mixin class $VideoUnavailable_FormatUnavailableCopyWith<$Res> + implements $VideoUnavailableCopyWith<$Res> { + factory $VideoUnavailable_FormatUnavailableCopyWith( + VideoUnavailable_FormatUnavailable value, + $Res Function(VideoUnavailable_FormatUnavailable) _then) = + _$VideoUnavailable_FormatUnavailableCopyWithImpl; + @useResult + $Res call({VideoMediaFormat field0}); + + $VideoMediaFormatCopyWith<$Res> get field0; +} + +/// @nodoc +class _$VideoUnavailable_FormatUnavailableCopyWithImpl<$Res> + implements $VideoUnavailable_FormatUnavailableCopyWith<$Res> { + _$VideoUnavailable_FormatUnavailableCopyWithImpl(this._self, this._then); + + final VideoUnavailable_FormatUnavailable _self; + final $Res Function(VideoUnavailable_FormatUnavailable) _then; + + /// Create a copy of VideoUnavailable + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? field0 = null, + }) { + return _then(VideoUnavailable_FormatUnavailable( + null == field0 + ? _self.field0 + : field0 // ignore: cast_nullable_to_non_nullable + as VideoMediaFormat, + )); + } + + /// Create a copy of VideoUnavailable + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $VideoMediaFormatCopyWith<$Res> get field0 { + return $VideoMediaFormatCopyWith<$Res>(_self.field0, (value) { + return _then(_self.copyWith(field0: value)); + }); + } +} + +/// @nodoc + +class VideoUnavailable_ConfigurationUnavailable extends VideoUnavailable { + const VideoUnavailable_ConfigurationUnavailable() : super._(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is VideoUnavailable_ConfigurationUnavailable); + } + + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'VideoUnavailable.configurationUnavailable()'; + } +} + // dart format on diff --git a/lib/main.dart b/lib/main.dart index 544a4489..9d82ecb2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -251,7 +251,7 @@ Future main(List args) async { statistics: statisticsController.setStatistics, messageReceived: chatStateController.messageReceived, managerActive: stateController.setSessionManager, - screenshareStarted: stateController.screenshareStarted); + videoLifecycle: stateController.handleVideoLifecycle); final telepathy = Telepathy( host: host, diff --git a/lib/widgets/call/call_controls.dart b/lib/widgets/call/call_controls.dart index ba5f530a..95daf572 100644 --- a/lib/widgets/call/call_controls.dart +++ b/lib/widgets/call/call_controls.dart @@ -7,11 +7,27 @@ import 'package:telepathy/core/utils/index.dart'; import 'package:telepathy/screens/settings/view.dart'; import 'package:telepathy/core/rust/player.dart'; import 'package:telepathy/core/rust/flutter.dart'; -import 'package:telepathy/core/rust/flutter/utils.dart'; +import 'package:telepathy/core/rust/types.dart'; + +class VideoControlActions { + const VideoControlActions({ + required this.videoCapabilities, + required this.isSourceConfigured, + required this.requestDisplay, + required this.stop, + }); + + final Future Function() videoCapabilities; + final Future Function(VideoSource source) isSourceConfigured; + final Future Function(Contact contact) requestDisplay; + final Future Function(VideoSessionIdentity identity) stop; +} /// A widget with commonly used controls for a call. class CallControls extends StatefulWidget { - const CallControls({super.key}); + const CallControls({super.key, this.videoActions}); + + final VideoControlActions? videoActions; @override State createState() => _CallControlsState(); @@ -32,6 +48,21 @@ class _CallControlsState extends State { super.dispose(); } + VideoControlActions _videoActions(Telepathy telepathy) { + return widget.videoActions ?? + VideoControlActions( + videoCapabilities: telepathy.videoCapabilities, + isSourceConfigured: (source) => context + .read() + .isVideoSourceConfigured(source), + requestDisplay: (contact) => telepathy.requestVideoSource( + contact: contact, + source: VideoSource.display, + ), + stop: (identity) => telepathy.stopVideoSource(identity: identity), + ); + } + @override Widget build(BuildContext context) { final telepathy = context.read(); @@ -252,10 +283,18 @@ class _CallControlsState extends State { return; } - final networkSettingsController = context - .read(); + final videoActions = + _videoActions(telepathy); - if (!(await screenshareAvailable())) { + final capabilities = await videoActions + .videoCapabilities(); + final canSendDisplay = capabilities.send + is VideoCapabilityAvailability_Available && + capabilities.sendSources.any( + (capability) => + capability.source == + VideoSource.display); + if (!canSendDisplay) { if (context.mounted) { showErrorDialog( context, @@ -264,10 +303,9 @@ class _CallControlsState extends State { } return; - } else if ((await networkSettingsController - .screenshareConfig - .recordingConfig()) == - null) { + } else if (!(await videoActions + .isSourceConfigured( + VideoSource.display))) { if (context.mounted) { showErrorDialog( context, @@ -280,12 +318,26 @@ class _CallControlsState extends State { if (!stateController .isSendingScreenshare) { - telepathy.startScreenshare( - contact: - stateController.activeContact!); + final outcome = + await videoActions.requestDisplay( + stateController.activeContact!, + ); + if (outcome + is VideoStartOutcome_Unavailable) { + if (context.mounted) { + showErrorDialog( + context, + 'Screenshare Unavailable', + 'ffmpeg must be installed to use the screenshare feature', + ); + } + } } else { - stateController.stopScreenshare( - true, true); + final identity = stateController + .stopSendingScreenshare(); + if (identity != null) { + await videoActions.stop(identity); + } } }, icon: SvgPicture.asset( diff --git a/rust/telepathy-cli/src/callbacks.rs b/rust/telepathy-cli/src/callbacks.rs index 0b4685ab..3841d2b3 100644 --- a/rust/telepathy-cli/src/callbacks.rs +++ b/rust/telepathy-cli/src/callbacks.rs @@ -34,7 +34,7 @@ impl Hub { let tx_for_statistics = self.event_tx.clone(); let tx_for_message = self.event_tx.clone(); let tx_for_manager = self.event_tx.clone(); - let tx_for_screenshare = self.event_tx.clone(); + let tx_for_video = self.event_tx.clone(); NativeCallbacks::new( move |contact_id, ringtone, response_tx, mut cancel_rx| { @@ -120,10 +120,10 @@ impl Hub { let _ = tx.send(Event::ManagerActive(state)); }) }, - move |(_notify, sender)| { - let tx = tx_for_screenshare.clone(); + move |event| { + let tx = tx_for_video.clone(); Box::pin(async move { - let _ = tx.send(Event::ScreenshareStarted { sender }); + let _ = tx.send(Event::VideoLifecycle { event }); }) }, ) diff --git a/rust/telepathy-cli/src/events.rs b/rust/telepathy-cli/src/events.rs index 6ebb113c..2f34aac6 100644 --- a/rust/telepathy-cli/src/events.rs +++ b/rust/telepathy-cli/src/events.rs @@ -1,5 +1,7 @@ use serde::Serialize; -use telepathy_core::types::{CallState, ChatMessage, ManagerState, SessionStatus, Statistics}; +use telepathy_core::types::{ + CallState, ChatMessage, ManagerState, SessionStatus, Statistics, VideoLifecycleEvent, +}; #[derive(Debug, Clone, Serialize)] #[serde(tag = "type", rename_all = "snake_case")] @@ -27,8 +29,8 @@ pub enum Event { #[serde(flatten)] message: ChatMessage, }, - ScreenshareStarted { - sender: bool, + VideoLifecycle { + event: VideoLifecycleEvent, }, AcceptCallPrompt { request_id: String, diff --git a/rust/telepathy-cli/src/runner.rs b/rust/telepathy-cli/src/runner.rs index 8136faf1..92dd02e3 100644 --- a/rust/telepathy-cli/src/runner.rs +++ b/rust/telepathy-cli/src/runner.rs @@ -9,8 +9,7 @@ use serde_json::json; use telepathy_audio::devices::{ AudioHost, CpalAudioHost, MockAudioHost, MockAudioInput, MockAudioOutput, }; -use telepathy_core::internal::TelepathyHandle; -use telepathy_core::native::NativeCallbacks; +use telepathy_core::native::NativeTelepathy; use telepathy_core::types::{CodecConfig, Contact, NetworkConfig}; use tokio::io::{AsyncBufReadExt, BufReader}; @@ -83,11 +82,11 @@ where } }; let codec_config = CodecConfig::new(true, true, 5.0); - let mut telepathy = TelepathyHandle::new( + let video_config = telepathy_core::types::ScreenshareConfig::default(); + let mut telepathy = NativeTelepathy::with_host( audio_host, &network_config, - &Default::default(), - &Default::default(), + &video_config, &codec_config, callbacks, ); @@ -192,7 +191,7 @@ enum CommandOutcome { } async fn handle_command( - telepathy: &mut TelepathyHandle, + telepathy: &mut NativeTelepathy, audio_frame_indices: Option<&FrameCapture>, hub: &Hub, envelope: Envelope, @@ -209,17 +208,9 @@ where } }; - let key = match decoded.try_into() { - Ok(key) => key, - Err(_) => { - return CommandOutcome::AckErr( - telepathy_core::types::IDENTITY_KEY_LENGTH_MESSAGE.to_string(), - ); - } - }; - match telepathy.set_identity(&key).await { + match telepathy.set_identity(decoded).await { Ok(()) => CommandOutcome::AckOk, - Err(err) => CommandOutcome::AckErr(err.to_string()), + Err(err) => CommandOutcome::AckErr(err), } } Command::AddContact { @@ -243,13 +234,13 @@ where } Command::RestartManager => match telepathy.restart_manager().await { Ok(()) => CommandOutcome::AckOk, - Err(err) => CommandOutcome::AckErr(err.to_string()), + Err(err) => CommandOutcome::AckErr(err), }, Command::Shutdown => CommandOutcome::Shutdown, Command::StartSession { contact_id } => match contact_by_id(hub, &contact_id).await { - Ok(contact) => match telepathy.try_start_session(&contact).await { + Ok(contact) => match telepathy.start_session(&contact).await { Ok(()) => CommandOutcome::AckOk, - Err(err) => CommandOutcome::AckErr(err.to_string()), + Err(err) => CommandOutcome::AckErr(err), }, Err(err) => CommandOutcome::AckErr(err), }, @@ -263,7 +254,7 @@ where Command::StartCall { contact_id } => match contact_by_id(hub, &contact_id).await { Ok(contact) => match telepathy.start_call(&contact).await { Ok(()) => CommandOutcome::AckOk, - Err(err) => CommandOutcome::AckErr(err.to_string()), + Err(err) => CommandOutcome::AckErr(err), }, Err(err) => CommandOutcome::AckErr(err), }, @@ -290,7 +281,7 @@ where } Command::JoinRoom { members } => match telepathy.join_room(members).await { Ok(()) => CommandOutcome::AckOk, - Err(err) => CommandOutcome::AckErr(err.to_string()), + Err(err) => CommandOutcome::AckErr(err), }, Command::SendChat { contact_id, @@ -313,14 +304,14 @@ where let mut message = telepathy.build_chat(&contact, text, decoded); match telepathy.send_chat(&mut message).await { Ok(()) => CommandOutcome::AckOk, - Err(err) => CommandOutcome::AckErr(err.to_string()), + Err(err) => CommandOutcome::AckErr(err), } } Err(err) => CommandOutcome::AckErr(err), }, Command::AudioTest => match telepathy.audio_test().await { Ok(()) => CommandOutcome::AckOk, - Err(err) => CommandOutcome::AckErr(err.to_string()), + Err(err) => CommandOutcome::AckErr(err), }, Command::SetMuted { value } => { telepathy.set_muted(value); @@ -336,7 +327,7 @@ where } Command::SetOutputVolumeDb { value } => match telepathy.set_output_volume(value) { Ok(()) => CommandOutcome::AckOk, - Err(err) => CommandOutcome::AckErr(err.to_string()), + Err(err) => CommandOutcome::AckErr(err), }, Command::SetRmsThresholdDb { value } => { telepathy.set_rms_threshold(value); diff --git a/rust/telepathy-core/src/flutter.rs b/rust/telepathy-core/src/flutter.rs index 6a8c4903..f7b2cfb9 100644 --- a/rust/telepathy-core/src/flutter.rs +++ b/rust/telepathy-core/src/flutter.rs @@ -5,6 +5,7 @@ pub mod utils; use crate::AudioDevice; use crate::internal::TelepathyHandle; +use crate::internal::state::PreparedIdentitySwitch as InternalPreparedIdentitySwitch; use crate::overlay::Overlay; pub use crate::types::*; use flutter_rust_bridge::{DartFnFuture, frb}; @@ -18,7 +19,6 @@ type DartVoid = Arc DartFnFuture<()> + Send>>; type DartMethod = Arc DartFnFuture + Send>>; type AcceptCallArgs = (String, Option>, FrontendNotify); type SessionStatusArgs = (String, SessionStatus); -type ScreenshareStartedArgs = (FrontendNotify, bool); type ManagerActiveArgs = ManagerState; #[frb(opaque)] @@ -35,7 +35,7 @@ impl StartOperation { #[frb(opaque)] pub struct PreparedIdentitySwitch { - prepared: Option, + prepared: Option, } impl PreparedIdentitySwitch { @@ -197,8 +197,20 @@ impl Telepathy { .map_err(DartError::from) } - pub async fn start_screenshare(&self, contact: &Contact) { - self.handle.start_screenshare(contact).await + pub async fn request_video_source( + &self, + contact: &Contact, + source: VideoSource, + ) -> VideoStartOutcome { + self.handle.request_video_source(contact, source).await + } + + pub async fn stop_video_source(&self, identity: VideoSessionIdentity) -> VideoStopOutcome { + self.handle.stop_video_source(identity).await + } + + pub async fn video_capabilities(&self) -> VideoCapabilities { + self.handle.video_capabilities().await } #[frb(sync)] @@ -310,9 +322,8 @@ pub struct FlutterCallbacks { /// Alerts the UI when the manager is active and restartable manager_active: DartVoid, - /// Called when a screenshare starts #[allow(dead_code)] - screenshare_started: DartVoid, + video_lifecycle: DartVoid, } impl FlutterCallbacks { @@ -327,7 +338,7 @@ impl FlutterCallbacks { statistics: impl Fn(Statistics) -> DartFnFuture<()> + Send + 'static, message_received: impl Fn(ChatMessage) -> DartFnFuture<()> + Send + 'static, manager_active: impl Fn(ManagerActiveArgs) -> DartFnFuture<()> + Send + 'static, - screenshare_started: impl Fn(ScreenshareStartedArgs) -> DartFnFuture<()> + Send + 'static, + video_lifecycle: impl Fn(VideoLifecycleEvent) -> DartFnFuture<()> + Send + 'static, ) -> Self { Self { accept_call: Arc::new(Mutex::new(accept_call)), @@ -338,7 +349,7 @@ impl FlutterCallbacks { statistics: Arc::new(Mutex::new(statistics)), message_received: Arc::new(Mutex::new(message_received)), manager_active: Arc::new(Mutex::new(manager_active)), - screenshare_started: Arc::new(Mutex::new(screenshare_started)), + video_lifecycle: Arc::new(Mutex::new(video_lifecycle)), } } } diff --git a/rust/telepathy-core/src/flutter/callbacks.rs b/rust/telepathy-core/src/flutter/callbacks.rs index 4c4af816..178136ee 100644 --- a/rust/telepathy-core/src/flutter/callbacks.rs +++ b/rust/telepathy-core/src/flutter/callbacks.rs @@ -1,6 +1,6 @@ use crate::flutter::{ CallState, ChatMessage, Contact, FlutterCallbacks, FlutterStatisticsCallback, FrontendNotify, - SessionStatus, Statistics, invoke, notify, + SessionStatus, Statistics, VideoLifecycleEvent, invoke, notify, }; use crate::internal::callbacks::{CoreCallbacks, CoreStatisticsCallback}; use crate::internal::{JoinHandle, spawn_task}; @@ -32,12 +32,8 @@ impl CoreCallbacks for FlutterCallbacks { notify(&self.manager_active, state) } - fn screenshare_started( - &self, - stop: FrontendNotify, - sender: bool, - ) -> impl Future + Send { - notify(&self.screenshare_started, (stop, sender)) + fn video_lifecycle(&self, event: VideoLifecycleEvent) -> impl Future + Send { + notify(&self.video_lifecycle, event) } fn get_contact(&self, peer_id: Vec) -> impl Future> + Send { diff --git a/rust/telepathy-core/src/frb_generated.rs b/rust/telepathy-core/src/frb_generated.rs index 7b1f1bd4..676c4fc7 100644 --- a/rust/telepathy-core/src/frb_generated.rs +++ b/rust/telepathy-core/src/frb_generated.rs @@ -43,7 +43,7 @@ flutter_rust_bridge::frb_generated_boilerplate!( default_rust_auto_opaque = RustAutoOpaqueMoi, ); pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_VERSION: &str = "2.12.0"; -pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = 226036739; +pub(crate) const FLUTTER_RUST_BRIDGE_CODEGEN_CONTENT_HASH: i32 = -1306791125; // Section: executor @@ -1343,9 +1343,9 @@ let api_get_contacts = decode_DartFn_Inputs_unit_Output_list_Auto_Owned_RustOpaq let api_statistics = decode_DartFn_Inputs_statistics_Output_unit_AnyhowException(::sse_decode(&mut deserializer)); let api_message_received = decode_DartFn_Inputs_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerChatMessage_Output_unit_AnyhowException(::sse_decode(&mut deserializer)); let api_manager_active = decode_DartFn_Inputs_manager_state_Output_unit_AnyhowException(::sse_decode(&mut deserializer)); -let api_screenshare_started = decode_DartFn_Inputs_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool_Output_unit_AnyhowException(::sse_decode(&mut deserializer));deserializer.end(); +let api_video_lifecycle = decode_DartFn_Inputs_video_lifecycle_event_Output_unit_AnyhowException(::sse_decode(&mut deserializer));deserializer.end(); transform_result_sse::<_, ()>((move || { - let output_ok = Result::<_,()>::Ok(crate::flutter::FlutterCallbacks::new(api_accept_call, api_get_contact, api_call_state, api_session_status, api_get_contacts, api_statistics, api_message_received, api_manager_active, api_screenshare_started))?; Ok(output_ok) + let output_ok = Result::<_,()>::Ok(crate::flutter::FlutterCallbacks::new(api_accept_call, api_get_contact, api_call_state, api_session_status, api_get_contacts, api_statistics, api_message_received, api_manager_active, api_video_lifecycle))?; Ok(output_ok) })()) }) } fn wire__crate__player__FlutterSoundHandle_cancel_impl( @@ -2196,28 +2196,28 @@ fn wire__crate__overlay__Overlay_new_impl( }; let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); - let api_enabled = ::sse_decode(&mut deserializer); - let api_x = ::sse_decode(&mut deserializer); - let api_y = ::sse_decode(&mut deserializer); - let api_width = ::sse_decode(&mut deserializer); - let api_height = ::sse_decode(&mut deserializer); - let api_font_height = ::sse_decode(&mut deserializer); - let api_background_color = ::sse_decode(&mut deserializer); - let api_font_color = ::sse_decode(&mut deserializer); + let api__enabled = ::sse_decode(&mut deserializer); + let api__x = ::sse_decode(&mut deserializer); + let api__y = ::sse_decode(&mut deserializer); + let api__width = ::sse_decode(&mut deserializer); + let api__height = ::sse_decode(&mut deserializer); + let api__font_height = ::sse_decode(&mut deserializer); + let api__background_color = ::sse_decode(&mut deserializer); + let api__font_color = ::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { transform_result_sse::<_, ()>( (move || async move { let output_ok = Result::<_, ()>::Ok( crate::overlay::Overlay::new( - api_enabled, - api_x, - api_y, - api_width, - api_height, - api_font_height, - api_background_color, - api_font_color, + api__enabled, + api__x, + api__y, + api__width, + api__height, + api__font_height, + api__background_color, + api__font_color, ) .await, )?; @@ -3080,6 +3080,64 @@ fn wire__crate__types__ScreenshareConfig_update_recording_config_impl( }, ) } +fn wire__crate__types__ScreenshareConfig_video_capabilities_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "ScreenshareConfig_video_capabilities", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok( + crate::types::ScreenshareConfig::video_capabilities(&*api_that_guard) + .await, + )?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__player__SoundPlayer_host_impl( ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -3934,6 +3992,85 @@ fn wire__crate__flutter__Telepathy_prepare_identity_switch_impl( }, ) } +fn wire__crate__flutter__Telepathy_request_video_source_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "Telepathy_request_video_source", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + let api_contact = , + >>::sse_decode(&mut deserializer); + let api_source = ::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let mut api_that_guard = None; + let mut api_contact_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![ + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + ), + flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_contact, + 1, + false, + ), + ], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + 1 => { + api_contact_guard = + Some(api_contact.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let api_contact_guard = api_contact_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok( + crate::flutter::Telepathy::request_video_source( + &*api_that_guard, + &*api_contact_guard, + api_source, + ) + .await, + )?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} fn wire__crate__flutter__Telepathy_restart_manager_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, @@ -5060,7 +5197,7 @@ fn wire__crate__flutter__Telepathy_start_manager_impl( }, ) } -fn wire__crate__flutter__Telepathy_start_screenshare_impl( +fn wire__crate__flutter__Telepathy_start_session_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -5068,7 +5205,7 @@ fn wire__crate__flutter__Telepathy_start_screenshare_impl( ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "Telepathy_start_screenshare", + debug_name: "Telepathy_start_session", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -5090,7 +5227,7 @@ fn wire__crate__flutter__Telepathy_start_screenshare_impl( >>::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { - transform_result_sse::<_, ()>( + transform_result_sse::<_, crate::types::DartError>( (move || async move { let mut api_that_guard = None; let mut api_contact_guard = None; @@ -5122,13 +5259,11 @@ fn wire__crate__flutter__Telepathy_start_screenshare_impl( } let api_that_guard = api_that_guard.unwrap(); let api_contact_guard = api_contact_guard.unwrap(); - let output_ok = Result::<_, ()>::Ok({ - crate::flutter::Telepathy::start_screenshare( - &*api_that_guard, - &*api_contact_guard, - ) - .await; - })?; + let output_ok = crate::flutter::Telepathy::start_session( + &*api_that_guard, + &*api_contact_guard, + ) + .await?; Ok(output_ok) })() .await, @@ -5137,7 +5272,7 @@ fn wire__crate__flutter__Telepathy_start_screenshare_impl( }, ) } -fn wire__crate__flutter__Telepathy_start_session_impl( +fn wire__crate__flutter__Telepathy_stop_session_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -5145,7 +5280,7 @@ fn wire__crate__flutter__Telepathy_start_session_impl( ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "Telepathy_start_session", + debug_name: "Telepathy_stop_session", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -5167,7 +5302,7 @@ fn wire__crate__flutter__Telepathy_start_session_impl( >>::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { - transform_result_sse::<_, crate::types::DartError>( + transform_result_sse::<_, ()>( (move || async move { let mut api_that_guard = None; let mut api_contact_guard = None; @@ -5199,11 +5334,13 @@ fn wire__crate__flutter__Telepathy_start_session_impl( } let api_that_guard = api_that_guard.unwrap(); let api_contact_guard = api_contact_guard.unwrap(); - let output_ok = crate::flutter::Telepathy::start_session( - &*api_that_guard, - &*api_contact_guard, - ) - .await?; + let output_ok = Result::<_, ()>::Ok({ + crate::flutter::Telepathy::stop_session( + &*api_that_guard, + &*api_contact_guard, + ) + .await; + })?; Ok(output_ok) })() .await, @@ -5212,7 +5349,7 @@ fn wire__crate__flutter__Telepathy_start_session_impl( }, ) } -fn wire__crate__flutter__Telepathy_stop_session_impl( +fn wire__crate__flutter__Telepathy_stop_video_source_impl( port_: flutter_rust_bridge::for_generated::MessagePort, ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, rust_vec_len_: i32, @@ -5220,7 +5357,7 @@ fn wire__crate__flutter__Telepathy_stop_session_impl( ) { FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( flutter_rust_bridge::for_generated::TaskInfo { - debug_name: "Telepathy_stop_session", + debug_name: "Telepathy_stop_video_source", port: Some(port_), mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, }, @@ -5237,27 +5374,17 @@ fn wire__crate__flutter__Telepathy_stop_session_impl( let api_that = , >>::sse_decode(&mut deserializer); - let api_contact = , - >>::sse_decode(&mut deserializer); + let api_identity = ::sse_decode(&mut deserializer); deserializer.end(); move |context| async move { transform_result_sse::<_, ()>( (move || async move { let mut api_that_guard = None; - let mut api_contact_guard = None; let decode_indices_ = flutter_rust_bridge::for_generated::lockable_compute_decode_order( - vec![ - flutter_rust_bridge::for_generated::LockableOrderInfo::new( - &api_that, 0, false, - ), - flutter_rust_bridge::for_generated::LockableOrderInfo::new( - &api_contact, - 1, - false, - ), - ], + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], ); for i in decode_indices_ { match i { @@ -5265,22 +5392,74 @@ fn wire__crate__flutter__Telepathy_stop_session_impl( api_that_guard = Some(api_that.lockable_decode_async_ref().await) } - 1 => { - api_contact_guard = - Some(api_contact.lockable_decode_async_ref().await) - } _ => unreachable!(), } } let api_that_guard = api_that_guard.unwrap(); - let api_contact_guard = api_contact_guard.unwrap(); - let output_ok = Result::<_, ()>::Ok({ - crate::flutter::Telepathy::stop_session( + let output_ok = Result::<_, ()>::Ok( + crate::flutter::Telepathy::stop_video_source( &*api_that_guard, - &*api_contact_guard, + api_identity, ) - .await; - })?; + .await, + )?; + Ok(output_ok) + })() + .await, + ) + } + }, + ) +} +fn wire__crate__flutter__Telepathy_video_capabilities_impl( + port_: flutter_rust_bridge::for_generated::MessagePort, + ptr_: flutter_rust_bridge::for_generated::PlatformGeneralizedUint8ListPtr, + rust_vec_len_: i32, + data_len_: i32, +) { + FLUTTER_RUST_BRIDGE_HANDLER.wrap_async::( + flutter_rust_bridge::for_generated::TaskInfo { + debug_name: "Telepathy_video_capabilities", + port: Some(port_), + mode: flutter_rust_bridge::for_generated::FfiCallMode::Normal, + }, + move || { + let message = unsafe { + flutter_rust_bridge::for_generated::Dart2RustMessageSse::from_wire( + ptr_, + rust_vec_len_, + data_len_, + ) + }; + let mut deserializer = + flutter_rust_bridge::for_generated::SseDeserializer::new(message); + let api_that = , + >>::sse_decode(&mut deserializer); + deserializer.end(); + move |context| async move { + transform_result_sse::<_, ()>( + (move || async move { + let mut api_that_guard = None; + let decode_indices_ = + flutter_rust_bridge::for_generated::lockable_compute_decode_order( + vec![flutter_rust_bridge::for_generated::LockableOrderInfo::new( + &api_that, 0, false, + )], + ); + for i in decode_indices_ { + match i { + 0 => { + api_that_guard = + Some(api_that.lockable_decode_async_ref().await) + } + _ => unreachable!(), + } + } + let api_that_guard = api_that_guard.unwrap(); + let output_ok = Result::<_, ()>::Ok( + crate::flutter::Telepathy::video_capabilities(&*api_that_guard).await, + )?; Ok(output_ok) })() .await, @@ -5687,15 +5866,15 @@ fn decode_DartFn_Inputs_manager_state_Output_unit_AnyhowException( )) } } -fn decode_DartFn_Inputs_record_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_bool_Output_unit_AnyhowException( +fn decode_DartFn_Inputs_record_string_opt_list_prim_u_8_strict_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_Output_bool_AnyhowException( dart_opaque: flutter_rust_bridge::DartOpaque, -) -> impl Fn((FrontendNotify, bool)) -> flutter_rust_bridge::DartFnFuture<()> { +) -> impl Fn((String, Option>, FrontendNotify)) -> flutter_rust_bridge::DartFnFuture { use flutter_rust_bridge::IntoDart; async fn body( dart_opaque: flutter_rust_bridge::DartOpaque, - arg0: (FrontendNotify, bool), - ) -> () { + arg0: (String, Option>, FrontendNotify), + ) -> bool { let args = vec![arg0.into_into_dart().into_dart()]; let message = FLUTTER_RUST_BRIDGE_HANDLER .dart_fn_invoke(dart_opaque, args) @@ -5704,7 +5883,7 @@ fn decode_DartFn_Inputs_record_auto_owned_rust_opaque_flutter_rust_bridgefor_gen let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let action = deserializer.cursor.read_u8().unwrap(); let ans = match action { - 0 => std::result::Result::Ok(<()>::sse_decode(&mut deserializer)), + 0 => std::result::Result::Ok(::sse_decode(&mut deserializer)), 1 => std::result::Result::Err( ::sse_decode(&mut deserializer), ), @@ -5715,22 +5894,22 @@ fn decode_DartFn_Inputs_record_auto_owned_rust_opaque_flutter_rust_bridgefor_gen ans } - move |arg0: (FrontendNotify, bool)| { + move |arg0: (String, Option>, FrontendNotify)| { flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body( dart_opaque.clone(), arg0, )) } } -fn decode_DartFn_Inputs_record_string_opt_list_prim_u_8_strict_auto_owned_rust_opaque_flutter_rust_bridgefor_generated_rust_auto_opaque_inner_frontend_notify_Output_bool_AnyhowException( +fn decode_DartFn_Inputs_record_string_session_status_Output_unit_AnyhowException( dart_opaque: flutter_rust_bridge::DartOpaque, -) -> impl Fn((String, Option>, FrontendNotify)) -> flutter_rust_bridge::DartFnFuture { +) -> impl Fn((String, crate::types::SessionStatus)) -> flutter_rust_bridge::DartFnFuture<()> { use flutter_rust_bridge::IntoDart; async fn body( dart_opaque: flutter_rust_bridge::DartOpaque, - arg0: (String, Option>, FrontendNotify), - ) -> bool { + arg0: (String, crate::types::SessionStatus), + ) -> () { let args = vec![arg0.into_into_dart().into_dart()]; let message = FLUTTER_RUST_BRIDGE_HANDLER .dart_fn_invoke(dart_opaque, args) @@ -5739,7 +5918,7 @@ fn decode_DartFn_Inputs_record_string_opt_list_prim_u_8_strict_auto_owned_rust_o let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let action = deserializer.cursor.read_u8().unwrap(); let ans = match action { - 0 => std::result::Result::Ok(::sse_decode(&mut deserializer)), + 0 => std::result::Result::Ok(<()>::sse_decode(&mut deserializer)), 1 => std::result::Result::Err( ::sse_decode(&mut deserializer), ), @@ -5750,21 +5929,21 @@ fn decode_DartFn_Inputs_record_string_opt_list_prim_u_8_strict_auto_owned_rust_o ans } - move |arg0: (String, Option>, FrontendNotify)| { + move |arg0: (String, crate::types::SessionStatus)| { flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body( dart_opaque.clone(), arg0, )) } } -fn decode_DartFn_Inputs_record_string_session_status_Output_unit_AnyhowException( +fn decode_DartFn_Inputs_statistics_Output_unit_AnyhowException( dart_opaque: flutter_rust_bridge::DartOpaque, -) -> impl Fn((String, crate::types::SessionStatus)) -> flutter_rust_bridge::DartFnFuture<()> { +) -> impl Fn(crate::types::Statistics) -> flutter_rust_bridge::DartFnFuture<()> { use flutter_rust_bridge::IntoDart; async fn body( dart_opaque: flutter_rust_bridge::DartOpaque, - arg0: (String, crate::types::SessionStatus), + arg0: crate::types::Statistics, ) -> () { let args = vec![arg0.into_into_dart().into_dart()]; let message = FLUTTER_RUST_BRIDGE_HANDLER @@ -5785,22 +5964,19 @@ fn decode_DartFn_Inputs_record_string_session_status_Output_unit_AnyhowException ans } - move |arg0: (String, crate::types::SessionStatus)| { + move |arg0: crate::types::Statistics| { flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body( dart_opaque.clone(), arg0, )) } } -fn decode_DartFn_Inputs_statistics_Output_unit_AnyhowException( +fn decode_DartFn_Inputs_unit_Output_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact_AnyhowException( dart_opaque: flutter_rust_bridge::DartOpaque, -) -> impl Fn(crate::types::Statistics) -> flutter_rust_bridge::DartFnFuture<()> { +) -> impl Fn(()) -> flutter_rust_bridge::DartFnFuture> { use flutter_rust_bridge::IntoDart; - async fn body( - dart_opaque: flutter_rust_bridge::DartOpaque, - arg0: crate::types::Statistics, - ) -> () { + async fn body(dart_opaque: flutter_rust_bridge::DartOpaque, arg0: ()) -> Vec { let args = vec![arg0.into_into_dart().into_dart()]; let message = FLUTTER_RUST_BRIDGE_HANDLER .dart_fn_invoke(dart_opaque, args) @@ -5809,7 +5985,7 @@ fn decode_DartFn_Inputs_statistics_Output_unit_AnyhowException( let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let action = deserializer.cursor.read_u8().unwrap(); let ans = match action { - 0 => std::result::Result::Ok(<()>::sse_decode(&mut deserializer)), + 0 => std::result::Result::Ok(>::sse_decode(&mut deserializer)), 1 => std::result::Result::Err( ::sse_decode(&mut deserializer), ), @@ -5820,19 +5996,22 @@ fn decode_DartFn_Inputs_statistics_Output_unit_AnyhowException( ans } - move |arg0: crate::types::Statistics| { + move |arg0: ()| { flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body( dart_opaque.clone(), arg0, )) } } -fn decode_DartFn_Inputs_unit_Output_list_Auto_Owned_RustOpaque_flutter_rust_bridgefor_generatedRustAutoOpaqueInnerContact_AnyhowException( +fn decode_DartFn_Inputs_video_lifecycle_event_Output_unit_AnyhowException( dart_opaque: flutter_rust_bridge::DartOpaque, -) -> impl Fn(()) -> flutter_rust_bridge::DartFnFuture> { +) -> impl Fn(crate::types::VideoLifecycleEvent) -> flutter_rust_bridge::DartFnFuture<()> { use flutter_rust_bridge::IntoDart; - async fn body(dart_opaque: flutter_rust_bridge::DartOpaque, arg0: ()) -> Vec { + async fn body( + dart_opaque: flutter_rust_bridge::DartOpaque, + arg0: crate::types::VideoLifecycleEvent, + ) -> () { let args = vec![arg0.into_into_dart().into_dart()]; let message = FLUTTER_RUST_BRIDGE_HANDLER .dart_fn_invoke(dart_opaque, args) @@ -5841,7 +6020,7 @@ fn decode_DartFn_Inputs_unit_Output_list_Auto_Owned_RustOpaque_flutter_rust_brid let mut deserializer = flutter_rust_bridge::for_generated::SseDeserializer::new(message); let action = deserializer.cursor.read_u8().unwrap(); let ans = match action { - 0 => std::result::Result::Ok(>::sse_decode(&mut deserializer)), + 0 => std::result::Result::Ok(<()>::sse_decode(&mut deserializer)), 1 => std::result::Result::Err( ::sse_decode(&mut deserializer), ), @@ -5852,7 +6031,7 @@ fn decode_DartFn_Inputs_unit_Output_list_Auto_Owned_RustOpaque_flutter_rust_brid ans } - move |arg0: ()| { + move |arg0: crate::types::VideoLifecycleEvent| { flutter_rust_bridge::for_generated::convert_into_dart_fn_future(body( dart_opaque.clone(), arg0, @@ -6424,6 +6603,32 @@ impl SseDecode for Vec<(String, Vec)> { } } +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = Vec::with_capacity(len_ as usize); + for idx_ in 0..len_ { + ans_.push(::sse_decode(deserializer)); + } + return ans_; + } +} + +impl SseDecode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut len_ = ::sse_decode(deserializer); + let mut ans_ = Vec::with_capacity(len_ as usize); + for idx_ in 0..len_ { + ans_.push(::sse_decode( + deserializer, + )); + } + return ans_; + } +} + impl SseDecode for crate::types::ManagerState { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { @@ -6511,34 +6716,38 @@ impl SseDecode for Option { } } -impl SseDecode for Option> { +impl SseDecode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { if (::sse_decode(deserializer)) { - return Some(>::sse_decode(deserializer)); + return Some(::sse_decode( + deserializer, + )); } else { return None; } } } -impl SseDecode for Option> { +impl SseDecode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { if (::sse_decode(deserializer)) { - return Some(>::sse_decode(deserializer)); + return Some(>::sse_decode(deserializer)); } else { return None; } } } -impl SseDecode for (FrontendNotify, bool) { +impl SseDecode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { - let mut var_field0 = ::sse_decode(deserializer); - let mut var_field1 = ::sse_decode(deserializer); - return (var_field0, var_field1); + if (::sse_decode(deserializer)) { + return Some(>::sse_decode(deserializer)); + } else { + return None; + } } } @@ -6668,6 +6877,14 @@ impl SseDecode for u8 { } } +impl SseDecode for [u8; 16] { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = >::sse_decode(deserializer); + return flutter_rust_bridge::for_generated::from_vec_to_array(inner); + } +} + impl SseDecode for () { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self {} @@ -6680,6 +6897,248 @@ impl SseDecode for usize { } } +impl SseDecode for crate::types::VideoCapabilities { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_send = ::sse_decode(deserializer); + let mut var_receive = ::sse_decode(deserializer); + let mut var_sendSources = + >::sse_decode(deserializer); + let mut var_receiveFormats = + >::sse_decode(deserializer); + return crate::types::VideoCapabilities { + send: var_send, + receive: var_receive, + send_sources: var_sendSources, + receive_formats: var_receiveFormats, + }; + } +} + +impl SseDecode for crate::types::VideoCapabilityAvailability { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut tag_ = ::sse_decode(deserializer); + match tag_ { + 0 => { + return crate::types::VideoCapabilityAvailability::Available; + } + 1 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::types::VideoCapabilityAvailability::Unavailable(var_field0); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseDecode for crate::types::VideoCodec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::types::VideoCodec::H264, + 1 => crate::types::VideoCodec::Hevc, + 2 => crate::types::VideoCodec::Av1, + _ => unreachable!("Invalid variant for VideoCodec: {}", inner), + }; + } +} + +impl SseDecode for crate::types::VideoLifecycleEvent { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_identity = ::sse_decode(deserializer); + let mut var_role = ::sse_decode(deserializer); + let mut var_source = ::sse_decode(deserializer); + let mut var_phase = ::sse_decode(deserializer); + let mut var_terminalReason = + >::sse_decode(deserializer); + return crate::types::VideoLifecycleEvent { + identity: var_identity, + role: var_role, + source: var_source, + phase: var_phase, + terminal_reason: var_terminalReason, + }; + } +} + +impl SseDecode for crate::types::VideoMediaFormat { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut tag_ = ::sse_decode(deserializer); + match tag_ { + 0 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::types::VideoMediaFormat::MpegTs(var_field0); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseDecode for crate::types::VideoPhase { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::types::VideoPhase::Offering, + 1 => crate::types::VideoPhase::WaitingReady, + 2 => crate::types::VideoPhase::Starting, + 3 => crate::types::VideoPhase::Active, + 4 => crate::types::VideoPhase::Stopping, + 5 => crate::types::VideoPhase::Terminal, + _ => unreachable!("Invalid variant for VideoPhase: {}", inner), + }; + } +} + +impl SseDecode for crate::types::VideoRole { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::types::VideoRole::Sender, + 1 => crate::types::VideoRole::Receiver, + _ => unreachable!("Invalid variant for VideoRole: {}", inner), + }; + } +} + +impl SseDecode for crate::types::VideoSessionId { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_field0 = <[u8; 16]>::sse_decode(deserializer); + return crate::types::VideoSessionId(var_field0); + } +} + +impl SseDecode for crate::types::VideoSessionIdentity { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_peerId = ::sse_decode(deserializer); + let mut var_sessionId = ::sse_decode(deserializer); + return crate::types::VideoSessionIdentity { + peer_id: var_peerId, + session_id: var_sessionId, + }; + } +} + +impl SseDecode for crate::types::VideoSource { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::types::VideoSource::Display, + _ => unreachable!("Invalid variant for VideoSource: {}", inner), + }; + } +} + +impl SseDecode for crate::types::VideoSourceCapability { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut var_source = ::sse_decode(deserializer); + let mut var_formats = >::sse_decode(deserializer); + return crate::types::VideoSourceCapability { + source: var_source, + formats: var_formats, + }; + } +} + +impl SseDecode for crate::types::VideoStartOutcome { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut tag_ = ::sse_decode(deserializer); + match tag_ { + 0 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::types::VideoStartOutcome::Requested(var_field0); + } + 1 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::types::VideoStartOutcome::Unavailable(var_field0); + } + 2 => { + return crate::types::VideoStartOutcome::NoSession; + } + 3 => { + return crate::types::VideoStartOutcome::AlreadyActive; + } + 4 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::types::VideoStartOutcome::Failed(var_field0); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseDecode for crate::types::VideoStopOutcome { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::types::VideoStopOutcome::Stopped, + 1 => crate::types::VideoStopOutcome::NotFound, + _ => unreachable!("Invalid variant for VideoStopOutcome: {}", inner), + }; + } +} + +impl SseDecode for crate::types::VideoTerminalReason { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut inner = ::sse_decode(deserializer); + return match inner { + 0 => crate::types::VideoTerminalReason::Stopped, + 1 => crate::types::VideoTerminalReason::Rejected, + 2 => crate::types::VideoTerminalReason::Failed, + 3 => crate::types::VideoTerminalReason::TransportEnded, + 4 => crate::types::VideoTerminalReason::Teardown, + _ => unreachable!("Invalid variant for VideoTerminalReason: {}", inner), + }; + } +} + +impl SseDecode for crate::types::VideoUnavailable { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_decode(deserializer: &mut flutter_rust_bridge::for_generated::SseDeserializer) -> Self { + let mut tag_ = ::sse_decode(deserializer); + match tag_ { + 0 => { + return crate::types::VideoUnavailable::PlatformUnsupported; + } + 1 => { + return crate::types::VideoUnavailable::RuntimeUnavailable; + } + 2 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::types::VideoUnavailable::SourceUnavailable(var_field0); + } + 3 => { + let mut var_field0 = ::sse_decode(deserializer); + return crate::types::VideoUnavailable::FormatUnavailable(var_field0); + } + 4 => { + return crate::types::VideoUnavailable::ConfigurationUnavailable; + } + _ => { + unimplemented!(""); + } + } + } +} + fn pde_ffi_dispatcher_primary_impl( func_id: i32, port: flutter_rust_bridge::for_generated::MessagePort, @@ -6735,62 +7194,82 @@ fn pde_ffi_dispatcher_primary_impl( rust_vec_len, data_len, ), - 67 => wire__crate__player__SoundPlayer_play_impl(port, ptr, rust_vec_len, data_len), - 68 => wire__crate__player__SoundPlayer_update_output_device_impl( + 65 => wire__crate__types__ScreenshareConfig_video_capabilities_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 68 => wire__crate__player__SoundPlayer_play_impl(port, ptr, rust_vec_len, data_len), + 69 => wire__crate__player__SoundPlayer_update_output_device_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 72 => wire__crate__flutter__Telepathy_audio_test_impl(port, ptr, rust_vec_len, data_len), + 74 => wire__crate__flutter__Telepathy_end_call_impl(port, ptr, rust_vec_len, data_len), + 75 => wire__crate__flutter__Telepathy_join_room_impl(port, ptr, rust_vec_len, data_len), + 76 => wire__crate__flutter__Telepathy_list_devices_impl(port, ptr, rust_vec_len, data_len), + 80 => wire__crate__flutter__Telepathy_prepare_identity_switch_impl( port, ptr, rust_vec_len, data_len, ), - 71 => wire__crate__flutter__Telepathy_audio_test_impl(port, ptr, rust_vec_len, data_len), - 73 => wire__crate__flutter__Telepathy_end_call_impl(port, ptr, rust_vec_len, data_len), - 74 => wire__crate__flutter__Telepathy_join_room_impl(port, ptr, rust_vec_len, data_len), - 75 => wire__crate__flutter__Telepathy_list_devices_impl(port, ptr, rust_vec_len, data_len), - 79 => wire__crate__flutter__Telepathy_prepare_identity_switch_impl( + 81 => wire__crate__flutter__Telepathy_request_video_source_impl( port, ptr, rust_vec_len, data_len, ), - 80 => { + 82 => { wire__crate__flutter__Telepathy_restart_manager_impl(port, ptr, rust_vec_len, data_len) } - 82 => wire__crate__flutter__Telepathy_send_chat_impl(port, ptr, rust_vec_len, data_len), - 87 => wire__crate__flutter__Telepathy_set_identity_impl(port, ptr, rust_vec_len, data_len), - 88 => { + 84 => wire__crate__flutter__Telepathy_send_chat_impl(port, ptr, rust_vec_len, data_len), + 89 => wire__crate__flutter__Telepathy_set_identity_impl(port, ptr, rust_vec_len, data_len), + 90 => { wire__crate__flutter__Telepathy_set_input_device_impl(port, ptr, rust_vec_len, data_len) } - 90 => wire__crate__flutter__Telepathy_set_model_impl(port, ptr, rust_vec_len, data_len), - 92 => wire__crate__flutter__Telepathy_set_output_device_impl( + 92 => wire__crate__flutter__Telepathy_set_model_impl(port, ptr, rust_vec_len, data_len), + 94 => wire__crate__flutter__Telepathy_set_output_device_impl( port, ptr, rust_vec_len, data_len, ), - 97 => wire__crate__flutter__Telepathy_shutdown_impl(port, ptr, rust_vec_len, data_len), - 98 => wire__crate__flutter__Telepathy_start_call_impl(port, ptr, rust_vec_len, data_len), - 99 => wire__crate__flutter__Telepathy_start_manager_impl(port, ptr, rust_vec_len, data_len), - 100 => wire__crate__flutter__Telepathy_start_screenshare_impl( + 99 => wire__crate__flutter__Telepathy_shutdown_impl(port, ptr, rust_vec_len, data_len), + 100 => wire__crate__flutter__Telepathy_start_call_impl(port, ptr, rust_vec_len, data_len), + 101 => { + wire__crate__flutter__Telepathy_start_manager_impl(port, ptr, rust_vec_len, data_len) + } + 102 => { + wire__crate__flutter__Telepathy_start_session_impl(port, ptr, rust_vec_len, data_len) + } + 103 => wire__crate__flutter__Telepathy_stop_session_impl(port, ptr, rust_vec_len, data_len), + 104 => wire__crate__flutter__Telepathy_stop_video_source_impl( port, ptr, rust_vec_len, data_len, ), - 101 => { - wire__crate__flutter__Telepathy_start_session_impl(port, ptr, rust_vec_len, data_len) - } - 102 => wire__crate__flutter__Telepathy_stop_session_impl(port, ptr, rust_vec_len, data_len), - 105 => wire__crate__player__load_ringtone_impl(port, ptr, rust_vec_len, data_len), - 108 => wire__crate__flutter__utils__screenshare_available_impl( + 105 => wire__crate__flutter__Telepathy_video_capabilities_impl( port, ptr, rust_vec_len, data_len, ), - 109 => wire__crate__types__statistics_default_impl(port, ptr, rust_vec_len, data_len), - _ => unreachable!(), - } -} + 108 => wire__crate__player__load_ringtone_impl(port, ptr, rust_vec_len, data_len), + 111 => wire__crate__flutter__utils__screenshare_available_impl( + port, + ptr, + rust_vec_len, + data_len, + ), + 112 => wire__crate__types__statistics_default_impl(port, ptr, rust_vec_len, data_len), + _ => unreachable!(), + } +} fn pde_ffi_dispatcher_sync_impl( func_id: i32, @@ -6861,44 +7340,44 @@ fn pde_ffi_dispatcher_sync_impl( 57 => wire__crate__types__RecordingConfig_framerate_impl(ptr, rust_vec_len, data_len), 58 => wire__crate__types__RecordingConfig_height_impl(ptr, rust_vec_len, data_len), 63 => wire__crate__types__ScreenshareConfig_to_bytes_impl(ptr, rust_vec_len, data_len), - 65 => wire__crate__player__SoundPlayer_host_impl(ptr, rust_vec_len, data_len), - 66 => wire__crate__player__SoundPlayer_new_impl(ptr, rust_vec_len, data_len), - 69 => { + 66 => wire__crate__player__SoundPlayer_host_impl(ptr, rust_vec_len, data_len), + 67 => wire__crate__player__SoundPlayer_new_impl(ptr, rust_vec_len, data_len), + 70 => { wire__crate__player__SoundPlayer_update_output_volume_impl(ptr, rust_vec_len, data_len) } - 70 => wire__crate__flutter__StartOperation_cancel_impl(ptr, rust_vec_len, data_len), - 72 => wire__crate__flutter__Telepathy_build_chat_impl(ptr, rust_vec_len, data_len), - 76 => wire__crate__flutter__Telepathy_new_impl(ptr, rust_vec_len, data_len), - 77 => wire__crate__flutter__Telepathy_new_start_operation_impl(ptr, rust_vec_len, data_len), - 78 => wire__crate__flutter__Telepathy_pause_statistics_impl(ptr, rust_vec_len, data_len), - 81 => wire__crate__flutter__Telepathy_resume_statistics_impl(ptr, rust_vec_len, data_len), - 83 => wire__crate__flutter__Telepathy_set_contact_output_volume_impl( + 71 => wire__crate__flutter__StartOperation_cancel_impl(ptr, rust_vec_len, data_len), + 73 => wire__crate__flutter__Telepathy_build_chat_impl(ptr, rust_vec_len, data_len), + 77 => wire__crate__flutter__Telepathy_new_impl(ptr, rust_vec_len, data_len), + 78 => wire__crate__flutter__Telepathy_new_start_operation_impl(ptr, rust_vec_len, data_len), + 79 => wire__crate__flutter__Telepathy_pause_statistics_impl(ptr, rust_vec_len, data_len), + 83 => wire__crate__flutter__Telepathy_resume_statistics_impl(ptr, rust_vec_len, data_len), + 85 => wire__crate__flutter__Telepathy_set_contact_output_volume_impl( ptr, rust_vec_len, data_len, ), - 84 => wire__crate__flutter__Telepathy_set_deafened_impl(ptr, rust_vec_len, data_len), - 85 => wire__crate__flutter__Telepathy_set_denoise_impl(ptr, rust_vec_len, data_len), - 86 => wire__crate__flutter__Telepathy_set_efficiency_mode_impl(ptr, rust_vec_len, data_len), - 89 => wire__crate__flutter__Telepathy_set_input_volume_impl(ptr, rust_vec_len, data_len), - 91 => wire__crate__flutter__Telepathy_set_muted_impl(ptr, rust_vec_len, data_len), - 93 => wire__crate__flutter__Telepathy_set_output_volume_impl(ptr, rust_vec_len, data_len), - 94 => wire__crate__flutter__Telepathy_set_play_custom_ringtones_impl( + 86 => wire__crate__flutter__Telepathy_set_deafened_impl(ptr, rust_vec_len, data_len), + 87 => wire__crate__flutter__Telepathy_set_denoise_impl(ptr, rust_vec_len, data_len), + 88 => wire__crate__flutter__Telepathy_set_efficiency_mode_impl(ptr, rust_vec_len, data_len), + 91 => wire__crate__flutter__Telepathy_set_input_volume_impl(ptr, rust_vec_len, data_len), + 93 => wire__crate__flutter__Telepathy_set_muted_impl(ptr, rust_vec_len, data_len), + 95 => wire__crate__flutter__Telepathy_set_output_volume_impl(ptr, rust_vec_len, data_len), + 96 => wire__crate__flutter__Telepathy_set_play_custom_ringtones_impl( ptr, rust_vec_len, data_len, ), - 95 => wire__crate__flutter__Telepathy_set_rms_threshold_impl(ptr, rust_vec_len, data_len), - 96 => wire__crate__flutter__Telepathy_set_send_custom_ringtone_impl( + 97 => wire__crate__flutter__Telepathy_set_rms_threshold_impl(ptr, rust_vec_len, data_len), + 98 => wire__crate__flutter__Telepathy_set_send_custom_ringtone_impl( ptr, rust_vec_len, data_len, ), - 103 => wire__crate__flutter__logging__create_log_stream_impl(ptr, rust_vec_len, data_len), - 104 => wire__crate__flutter__utils__generate_keys_impl(ptr, rust_vec_len, data_len), - 106 => wire__crate__flutter__utils__room_hash_impl(ptr, rust_vec_len, data_len), - 107 => wire__crate__flutter__logging__rust_set_up_impl(ptr, rust_vec_len, data_len), - 110 => wire__crate__flutter__utils__validate_peer_id_impl(ptr, rust_vec_len, data_len), + 106 => wire__crate__flutter__logging__create_log_stream_impl(ptr, rust_vec_len, data_len), + 107 => wire__crate__flutter__utils__generate_keys_impl(ptr, rust_vec_len, data_len), + 109 => wire__crate__flutter__utils__room_hash_impl(ptr, rust_vec_len, data_len), + 110 => wire__crate__flutter__logging__rust_set_up_impl(ptr, rust_vec_len, data_len), + 113 => wire__crate__flutter__utils__validate_peer_id_impl(ptr, rust_vec_len, data_len), _ => unreachable!(), } } @@ -7341,6 +7820,333 @@ impl flutter_rust_bridge::IntoIntoDart for crate::type self } } +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoCapabilities { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.send.into_into_dart().into_dart(), + self.receive.into_into_dart().into_dart(), + self.send_sources.into_into_dart().into_dart(), + self.receive_formats.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::types::VideoCapabilities +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::types::VideoCapabilities +{ + fn into_into_dart(self) -> crate::types::VideoCapabilities { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoCapabilityAvailability { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + crate::types::VideoCapabilityAvailability::Available => [0.into_dart()].into_dart(), + crate::types::VideoCapabilityAvailability::Unavailable(field0) => { + [1.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + _ => { + unimplemented!(""); + } + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::types::VideoCapabilityAvailability +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::types::VideoCapabilityAvailability +{ + fn into_into_dart(self) -> crate::types::VideoCapabilityAvailability { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoCodec { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::H264 => 0.into_dart(), + Self::Hevc => 1.into_dart(), + Self::Av1 => 2.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::types::VideoCodec {} +impl flutter_rust_bridge::IntoIntoDart for crate::types::VideoCodec { + fn into_into_dart(self) -> crate::types::VideoCodec { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoLifecycleEvent { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.identity.into_into_dart().into_dart(), + self.role.into_into_dart().into_dart(), + self.source.into_into_dart().into_dart(), + self.phase.into_into_dart().into_dart(), + self.terminal_reason.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::types::VideoLifecycleEvent +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::types::VideoLifecycleEvent +{ + fn into_into_dart(self) -> crate::types::VideoLifecycleEvent { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoMediaFormat { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + crate::types::VideoMediaFormat::MpegTs(field0) => { + [0.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + _ => { + unimplemented!(""); + } + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::types::VideoMediaFormat +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::types::VideoMediaFormat +{ + fn into_into_dart(self) -> crate::types::VideoMediaFormat { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoPhase { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Offering => 0.into_dart(), + Self::WaitingReady => 1.into_dart(), + Self::Starting => 2.into_dart(), + Self::Active => 3.into_dart(), + Self::Stopping => 4.into_dart(), + Self::Terminal => 5.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::types::VideoPhase {} +impl flutter_rust_bridge::IntoIntoDart for crate::types::VideoPhase { + fn into_into_dart(self) -> crate::types::VideoPhase { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoRole { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Sender => 0.into_dart(), + Self::Receiver => 1.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::types::VideoRole {} +impl flutter_rust_bridge::IntoIntoDart for crate::types::VideoRole { + fn into_into_dart(self) -> crate::types::VideoRole { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoSessionId { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [self.0.into_into_dart().into_dart()].into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::types::VideoSessionId {} +impl flutter_rust_bridge::IntoIntoDart + for crate::types::VideoSessionId +{ + fn into_into_dart(self) -> crate::types::VideoSessionId { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoSessionIdentity { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.peer_id.into_into_dart().into_dart(), + self.session_id.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::types::VideoSessionIdentity +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::types::VideoSessionIdentity +{ + fn into_into_dart(self) -> crate::types::VideoSessionIdentity { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoSource { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Display => 0.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive for crate::types::VideoSource {} +impl flutter_rust_bridge::IntoIntoDart for crate::types::VideoSource { + fn into_into_dart(self) -> crate::types::VideoSource { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoSourceCapability { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + [ + self.source.into_into_dart().into_dart(), + self.formats.into_into_dart().into_dart(), + ] + .into_dart() + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::types::VideoSourceCapability +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::types::VideoSourceCapability +{ + fn into_into_dart(self) -> crate::types::VideoSourceCapability { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoStartOutcome { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + crate::types::VideoStartOutcome::Requested(field0) => { + [0.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::types::VideoStartOutcome::Unavailable(field0) => { + [1.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::types::VideoStartOutcome::NoSession => [2.into_dart()].into_dart(), + crate::types::VideoStartOutcome::AlreadyActive => [3.into_dart()].into_dart(), + crate::types::VideoStartOutcome::Failed(field0) => { + [4.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + _ => { + unimplemented!(""); + } + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::types::VideoStartOutcome +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::types::VideoStartOutcome +{ + fn into_into_dart(self) -> crate::types::VideoStartOutcome { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoStopOutcome { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Stopped => 0.into_dart(), + Self::NotFound => 1.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::types::VideoStopOutcome +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::types::VideoStopOutcome +{ + fn into_into_dart(self) -> crate::types::VideoStopOutcome { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoTerminalReason { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + Self::Stopped => 0.into_dart(), + Self::Rejected => 1.into_dart(), + Self::Failed => 2.into_dart(), + Self::TransportEnded => 3.into_dart(), + Self::Teardown => 4.into_dart(), + _ => unreachable!(), + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::types::VideoTerminalReason +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::types::VideoTerminalReason +{ + fn into_into_dart(self) -> crate::types::VideoTerminalReason { + self + } +} +// Codec=Dco (DartCObject based), see doc to use other codecs +impl flutter_rust_bridge::IntoDart for crate::types::VideoUnavailable { + fn into_dart(self) -> flutter_rust_bridge::for_generated::DartAbi { + match self { + crate::types::VideoUnavailable::PlatformUnsupported => [0.into_dart()].into_dart(), + crate::types::VideoUnavailable::RuntimeUnavailable => [1.into_dart()].into_dart(), + crate::types::VideoUnavailable::SourceUnavailable(field0) => { + [2.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::types::VideoUnavailable::FormatUnavailable(field0) => { + [3.into_dart(), field0.into_into_dart().into_dart()].into_dart() + } + crate::types::VideoUnavailable::ConfigurationUnavailable => [4.into_dart()].into_dart(), + _ => { + unimplemented!(""); + } + } + } +} +impl flutter_rust_bridge::for_generated::IntoDartExceptPrimitive + for crate::types::VideoUnavailable +{ +} +impl flutter_rust_bridge::IntoIntoDart + for crate::types::VideoUnavailable +{ + fn into_into_dart(self) -> crate::types::VideoUnavailable { + self + } +} impl SseEncode for flutter_rust_bridge::for_generated::anyhow::Error { // Codec=Sse (Serialization based), see doc to use other codecs @@ -7805,6 +8611,26 @@ impl SseEncode for Vec<(String, Vec)> { } } +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + +impl SseEncode for Vec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.len() as _, serializer); + for item in self { + ::sse_encode(item, serializer); + } + } +} + impl SseEncode for crate::types::ManagerState { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { @@ -7892,31 +8718,33 @@ impl SseEncode for Option { } } -impl SseEncode for Option> { +impl SseEncode for Option { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { ::sse_encode(self.is_some(), serializer); if let Some(value) = self { - >::sse_encode(value, serializer); + ::sse_encode(value, serializer); } } } -impl SseEncode for Option> { +impl SseEncode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { ::sse_encode(self.is_some(), serializer); if let Some(value) = self { - >::sse_encode(value, serializer); + >::sse_encode(value, serializer); } } } -impl SseEncode for (FrontendNotify, bool) { +impl SseEncode for Option> { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { - ::sse_encode(self.0, serializer); - ::sse_encode(self.1, serializer); + ::sse_encode(self.is_some(), serializer); + if let Some(value) = self { + >::sse_encode(value, serializer); + } } } @@ -8031,6 +8859,19 @@ impl SseEncode for u8 { } } +impl SseEncode for [u8; 16] { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + >::sse_encode( + { + let boxed: Box<[_]> = Box::new(self); + boxed.into_vec() + }, + serializer, + ); + } +} + impl SseEncode for () { // Codec=Sse (Serialization based), see doc to use other codecs fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) {} @@ -8046,6 +8887,243 @@ impl SseEncode for usize { } } +impl SseEncode for crate::types::VideoCapabilities { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.send, serializer); + ::sse_encode(self.receive, serializer); + >::sse_encode(self.send_sources, serializer); + >::sse_encode(self.receive_formats, serializer); + } +} + +impl SseEncode for crate::types::VideoCapabilityAvailability { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + match self { + crate::types::VideoCapabilityAvailability::Available => { + ::sse_encode(0, serializer); + } + crate::types::VideoCapabilityAvailability::Unavailable(field0) => { + ::sse_encode(1, serializer); + ::sse_encode(field0, serializer); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseEncode for crate::types::VideoCodec { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::types::VideoCodec::H264 => 0, + crate::types::VideoCodec::Hevc => 1, + crate::types::VideoCodec::Av1 => 2, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + +impl SseEncode for crate::types::VideoLifecycleEvent { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.identity, serializer); + ::sse_encode(self.role, serializer); + ::sse_encode(self.source, serializer); + ::sse_encode(self.phase, serializer); + >::sse_encode(self.terminal_reason, serializer); + } +} + +impl SseEncode for crate::types::VideoMediaFormat { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + match self { + crate::types::VideoMediaFormat::MpegTs(field0) => { + ::sse_encode(0, serializer); + ::sse_encode(field0, serializer); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseEncode for crate::types::VideoPhase { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::types::VideoPhase::Offering => 0, + crate::types::VideoPhase::WaitingReady => 1, + crate::types::VideoPhase::Starting => 2, + crate::types::VideoPhase::Active => 3, + crate::types::VideoPhase::Stopping => 4, + crate::types::VideoPhase::Terminal => 5, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + +impl SseEncode for crate::types::VideoRole { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::types::VideoRole::Sender => 0, + crate::types::VideoRole::Receiver => 1, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + +impl SseEncode for crate::types::VideoSessionId { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + <[u8; 16]>::sse_encode(self.0, serializer); + } +} + +impl SseEncode for crate::types::VideoSessionIdentity { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.peer_id, serializer); + ::sse_encode(self.session_id, serializer); + } +} + +impl SseEncode for crate::types::VideoSource { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::types::VideoSource::Display => 0, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + +impl SseEncode for crate::types::VideoSourceCapability { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode(self.source, serializer); + >::sse_encode(self.formats, serializer); + } +} + +impl SseEncode for crate::types::VideoStartOutcome { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + match self { + crate::types::VideoStartOutcome::Requested(field0) => { + ::sse_encode(0, serializer); + ::sse_encode(field0, serializer); + } + crate::types::VideoStartOutcome::Unavailable(field0) => { + ::sse_encode(1, serializer); + ::sse_encode(field0, serializer); + } + crate::types::VideoStartOutcome::NoSession => { + ::sse_encode(2, serializer); + } + crate::types::VideoStartOutcome::AlreadyActive => { + ::sse_encode(3, serializer); + } + crate::types::VideoStartOutcome::Failed(field0) => { + ::sse_encode(4, serializer); + ::sse_encode(field0, serializer); + } + _ => { + unimplemented!(""); + } + } + } +} + +impl SseEncode for crate::types::VideoStopOutcome { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::types::VideoStopOutcome::Stopped => 0, + crate::types::VideoStopOutcome::NotFound => 1, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + +impl SseEncode for crate::types::VideoTerminalReason { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + ::sse_encode( + match self { + crate::types::VideoTerminalReason::Stopped => 0, + crate::types::VideoTerminalReason::Rejected => 1, + crate::types::VideoTerminalReason::Failed => 2, + crate::types::VideoTerminalReason::TransportEnded => 3, + crate::types::VideoTerminalReason::Teardown => 4, + _ => { + unimplemented!(""); + } + }, + serializer, + ); + } +} + +impl SseEncode for crate::types::VideoUnavailable { + // Codec=Sse (Serialization based), see doc to use other codecs + fn sse_encode(self, serializer: &mut flutter_rust_bridge::for_generated::SseSerializer) { + match self { + crate::types::VideoUnavailable::PlatformUnsupported => { + ::sse_encode(0, serializer); + } + crate::types::VideoUnavailable::RuntimeUnavailable => { + ::sse_encode(1, serializer); + } + crate::types::VideoUnavailable::SourceUnavailable(field0) => { + ::sse_encode(2, serializer); + ::sse_encode(field0, serializer); + } + crate::types::VideoUnavailable::FormatUnavailable(field0) => { + ::sse_encode(3, serializer); + ::sse_encode(field0, serializer); + } + crate::types::VideoUnavailable::ConfigurationUnavailable => { + ::sse_encode(4, serializer); + } + _ => { + unimplemented!(""); + } + } + } +} + #[cfg(not(target_family = "wasm"))] mod io { // This file is automatically generated, so please do not edit it. diff --git a/rust/telepathy-core/src/internal.rs b/rust/telepathy-core/src/internal.rs index 882a12f1..63fd4c60 100644 --- a/rust/telepathy-core/src/internal.rs +++ b/rust/telepathy-core/src/internal.rs @@ -8,9 +8,12 @@ pub mod error; /// helper methods used by telepathy core mod helpers; pub(crate) mod messages; -pub(crate) mod screenshare; pub mod state; mod utils; +#[cfg(feature = "integration-testing")] +pub mod video; +#[cfg(not(feature = "integration-testing"))] +pub(crate) mod video; use crate::AudioDevice; use crate::internal::callbacks::CoreCallbacks; @@ -22,10 +25,15 @@ use crate::internal::state::{ PreparedSwitchLease, RoomState, SessionState, }; pub(crate) use crate::internal::utils::{JoinHandle, spawn_task}; +use crate::internal::video::VideoControl; use crate::overlay::Overlay; -use crate::types::{ChatMessage, CodecConfig, Contact, NetworkConfig, ScreenshareConfig}; +use crate::types::{ + CallState, ChatMessage, CodecConfig, Contact, NetworkConfig, ScreenshareConfig, + VideoCapabilities, VideoPhase, VideoSessionIdentity, VideoSource, VideoStartOutcome, + VideoStopOutcome, VideoTerminalReason, +}; use chrono::Local; -use iroh::SecretKey; +use iroh::{PublicKey, SecretKey}; use speedy::{LittleEndian, Writable, Writer}; use std::collections::HashSet; use std::mem; @@ -629,7 +637,7 @@ where if let Some(message) = outcome.into_message() { self_clone .callbacks - .call_state(crate::types::CallState::CallEnded(message, false)) + .call_state(CallState::CallEnded(message, false)) .await; } stop_io.cancel(); @@ -810,7 +818,7 @@ where error!("release_if_pending_for_peer failed: {}", error); } if let Some(state) = removed_state { - state.stop_session.cancel(); + state.teardown().await; } self.inner.request_room_reconcile(); } @@ -931,10 +939,52 @@ where Ok(()) } - pub async fn start_screenshare(&self, contact: &Contact) { - if let Some(state) = self.inner.session_states.read().await.get(&contact.peer_id) { - state.start_screenshare.notify_one(); + pub async fn request_video_source( + &self, + contact: &Contact, + source: VideoSource, + ) -> VideoStartOutcome { + self.inner + .request_video_source(contact.peer_id, source) + .await + } + + pub async fn stop_video_source(&self, identity: VideoSessionIdentity) -> VideoStopOutcome { + let Ok(peer) = identity.peer_id.parse::() else { + return VideoStopOutcome::NotFound; + }; + let Some(state) = self.inner.session_states.read().await.get(&peer).cloned() else { + return VideoStopOutcome::NotFound; + }; + let Some(event) = state + .video_slot + .current_event(peer.to_string(), VideoPhase::Stopping, None) + .await + else { + return VideoStopOutcome::NotFound; + }; + if event.identity.session_id != identity.session_id { + return VideoStopOutcome::NotFound; } + self.inner.observe_video_lifecycle(event); + let _ = state + .message_sender + .send(ProtocolMessage::Video { + control: VideoControl::stop(identity.session_id, VideoTerminalReason::Stopped), + }) + .await; + self.inner + .finish_current_video(&state, peer, VideoTerminalReason::Stopped) + .await; + VideoStopOutcome::Stopped + } + + pub async fn video_capabilities(&self) -> VideoCapabilities { + self.inner + .core_state + .screenshare_config + .video_capabilities() + .await } pub fn set_rms_threshold(&self, decimal: f32) { diff --git a/rust/telepathy-core/src/internal/callbacks.rs b/rust/telepathy-core/src/internal/callbacks.rs index af3d0d3f..c6eff480 100644 --- a/rust/telepathy-core/src/internal/callbacks.rs +++ b/rust/telepathy-core/src/internal/callbacks.rs @@ -1,6 +1,6 @@ use crate::internal::utils::JoinHandle; use crate::types::{ - CallState, ChatMessage, Contact, FrontendNotify, ManagerState, SessionStatus, Statistics, + CallState, ChatMessage, Contact, ManagerState, SessionStatus, Statistics, VideoLifecycleEvent, }; #[cfg(feature = "integration-testing")] use async_trait::async_trait; @@ -31,11 +31,7 @@ pub trait CoreCallbacks { fn manager_state(&self, state: ManagerState) -> impl Future + Send; - fn screenshare_started( - &self, - stop: FrontendNotify, - sender: bool, - ) -> impl Future + Send; + fn video_lifecycle(&self, event: VideoLifecycleEvent) -> impl Future + Send; fn get_contact(&self, peer_id: Vec) -> impl Future> + Send; diff --git a/rust/telepathy-core/src/internal/core.rs b/rust/telepathy-core/src/internal/core.rs index bc5aad4f..53ff50b1 100644 --- a/rust/telepathy-core/src/internal/core.rs +++ b/rust/telepathy-core/src/internal/core.rs @@ -15,7 +15,6 @@ use crate::internal::helpers::OutputHelper; use crate::internal::helpers::{RoomTaskOutcome, join_room_task_bounded}; use crate::internal::messages::{ AudioHeader, GoodbyeReason, ProtocolMessage, RoomControl, RoomJoinAdmission, RoomMessage, - StartScreenshare, }; use crate::internal::state::{ CallSlot, CallSlotAcquireResult, CallSlotSnapshot, CallSlotState, CoreState, RuntimeSnapshot, @@ -25,6 +24,7 @@ use crate::internal::utils::{JoinHandle, spawn_task}; #[cfg(target_os = "ios")] use crate::internal::utils::{configure_audio_session, deactivate_audio_session}; use crate::internal::utils::{loopback, read_message, statistics_collector, write_message}; +use crate::internal::video::{VIDEO_NEGOTIATION_TIMEOUT, VideoControl}; use crate::internal::{ ALPN, EarlyCallState, HELLO_TIMEOUT, KEEP_ALIVE, MAX_RINGTONE_LENGTH, Result, RoomState, SESSION_MAX_FRAME_LENGTH, SessionState, @@ -33,7 +33,7 @@ use crate::overlay::CONNECTED; use crate::overlay::Overlay; use crate::types::{ CallState, ChatMessage, CodecConfig, Contact, ManagerState, NetworkConfig, ScreenshareConfig, - SessionStatus, + SessionStatus, VideoTerminalReason, }; use chrono::Local; use iroh::endpoint::{ @@ -77,6 +77,29 @@ const ROOM_DIAL_BACKOFF_MAX_MS: u64 = 30_000; const ROOM_DIAL_MAX_RETRIES: u32 = 10; const ROOM_DIAL_EXISTING_SESSION_BACKOFF: Duration = Duration::from_secs(5); +fn update_video_negotiation_deadline( + deadline: &mut Option, + message: &ProtocolMessage, + timeout: Duration, +) { + if matches!( + message, + ProtocolMessage::Video { + control: VideoControl::Offer(_), + } + ) { + *deadline = Some(Instant::now() + timeout); + } +} + +async fn wait_for_video_negotiation_deadline(deadline: Option) { + if let Some(deadline) = deadline { + sleep_until(deadline).await; + } else { + std::future::pending().await + } +} + pub struct TelepathyCore where C: CoreCallbacks + Send + Sync + 'static, @@ -2066,6 +2089,7 @@ where call_state.remote_configuration.sample_rate, )); + let video_state = Arc::clone(o.state); let controller_future = self.call_controller(o, call_state.peer, end_call, &mut stream_error_receiver); @@ -2084,6 +2108,9 @@ where _ => None, }; + self.finish_current_video(&video_state, call_state.peer, VideoTerminalReason::Teardown) + .await; + info!(event = "call_controller_done_notifying_stop_io"); stop_io.cancel(); @@ -2149,6 +2176,7 @@ where ) -> Result { let identity = self.peer_id().await; let mut stream_errors_open = true; + let mut video_negotiation_deadline = None; CONNECTED.store(true, Relaxed); // Race Connected delivery against the two authoritative teardown signals @@ -2196,19 +2224,15 @@ where write_message(o.control_send, &ProtocolMessage::goodbye()).await?; break Ok(CallControllerOutcome::Silent); }, - _ = o.state.start_screenshare.notified() => { - info!(event = "starting_screenshare", peer.id = ?peer); - - #[cfg(not(target_family = "wasm"))] - { - let message = StartScreenshare::new_sender(peer, o.connection.clone()); - let self_clone = self.clone(); - spawn_task(async move { - let result = self_clone.start_screenshare(message).await; - if let Err(error) = result { - error!(event = "screenshare_start_failed", error = ?error); - } - }.in_current_span()); + _ = wait_for_video_negotiation_deadline(video_negotiation_deadline) => { + video_negotiation_deadline = None; + if let Some((attempt, reason)) = o.state.video_slot.expire_waiting_ready().await { + self.finish_video_attempt(o.state, peer, attempt, reason).await; + } + } + _ = o.state.video_slot.terminal_notified() => { + if let Some((attempt, reason)) = o.state.video_slot.take_terminal().await { + self.finish_video_attempt(o.state, peer, attempt, reason).await; } } // receives and handles messages from the callee @@ -2232,21 +2256,8 @@ where attachments, }).await; } - ProtocolMessage::ScreenshareHeader { .. } => { - info!(event = "screenshare_header_received", ?message, peer.id = ?peer); - - #[cfg(not(target_family = "wasm"))] - { - let message = StartScreenshare::new_receiver(peer, message, o.connection.clone()); - let self_clone = self.clone(); - spawn_task(async move { - let result = self_clone.start_screenshare(message).await; - if let Err(error) = result { - error!(event = "screenshare_start_failed", error = ?error); - } - }.in_current_span()); - } - + ProtocolMessage::Video { control } => { + self.handle_video_control(peer, o.connection, control).await?; } _ => error!(event = "call_controller_unexpected_message", ?message), } @@ -2254,6 +2265,11 @@ where // sends messages to the callee result = o.message_receiver.recv() => { if let Some(message) = result { + update_video_negotiation_deadline( + &mut video_negotiation_deadline, + &message, + VIDEO_NEGOTIATION_TIMEOUT, + ); write_message(o.control_send, &message).await?; } else { // if the channel closes, the call has ended @@ -4117,6 +4133,43 @@ mod tests { use iroh::SecretKey; use speedy::{Readable, Writable}; + #[tokio::test] + async fn video_negotiation_deadline_survives_non_progress_messages() { + let mut deadline = Some(tokio::time::Instant::now() + Duration::from_millis(50)); + let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel(); + let traffic = tokio::spawn(async move { + for _ in 0..20 { + tokio::time::sleep(Duration::from_millis(5)).await; + sender.send(ProtocolMessage::KeepAlive).unwrap(); + } + }); + let mut messages = 0; + + loop { + tokio::select! { + _ = wait_for_video_negotiation_deadline(deadline) => break, + Some(message) = receiver.recv() => { + update_video_negotiation_deadline( + &mut deadline, + &message, + Duration::from_millis(50), + ); + messages += 1; + }, + } + } + + assert!( + messages > 1, + "non-progress traffic must arrive before expiry" + ); + assert!( + messages < 20, + "non-progress traffic must not postpone expiry" + ); + traffic.abort(); + } + #[test] fn oversized_hello_ringtone_is_rejected_before_prompting() { let message = ProtocolMessage::Hello { diff --git a/rust/telepathy-core/src/internal/error.rs b/rust/telepathy-core/src/internal/error.rs index 4f060301..8b5b76ba 100644 --- a/rust/telepathy-core/src/internal/error.rs +++ b/rust/telepathy-core/src/internal/error.rs @@ -54,6 +54,7 @@ pub enum ErrorKind { TransportRecv, #[cfg(not(target_family = "wasm"))] InvalidEncoder, + PlatformUnavailable, RoomStateMissing, NoEncoderAvailable, NoIdentityAvailable, @@ -240,6 +241,7 @@ impl Display for Error { ErrorKind::TransportRecv => "Transport failed on receive".to_string(), #[cfg(not(target_family = "wasm"))] ErrorKind::InvalidEncoder => "Invalid encoder".to_string(), + ErrorKind::PlatformUnavailable => "Video platform unavailable".to_string(), ErrorKind::RoomStateMissing => "Room state missing".to_string(), ErrorKind::NoEncoderAvailable => "No encoder available".to_string(), ErrorKind::NoIdentityAvailable => "No identity available".to_string(), diff --git a/rust/telepathy-core/src/internal/helpers.rs b/rust/telepathy-core/src/internal/helpers.rs index 6c4eb8d1..4f982ce7 100644 --- a/rust/telepathy-core/src/internal/helpers.rs +++ b/rust/telepathy-core/src/internal/helpers.rs @@ -1,24 +1,31 @@ +#[cfg(not(target_family = "wasm"))] +use crate::internal::MAX_RINGTONE_LENGTH; use crate::internal::callbacks::CoreCallbacks; use crate::internal::core::{ OutgoingSlotDecision, PendingDirectCallSlot, RoomControllerCleanup, RoomControllerOutcome, TelepathyCore, }; use crate::internal::error::{AudioStreamError, Error, ErrorKind}; +use crate::internal::messages::ProtocolMessage; use crate::internal::messages::{AudioHeader, RoomMessage}; -#[cfg(not(target_family = "wasm"))] -use crate::internal::messages::{ProtocolMessage, StartScreenshare}; -#[cfg(not(target_family = "wasm"))] -use crate::internal::screenshare; -use crate::internal::state::{CallSlot, EarlyCallState, StatisticsCollectorState}; +use crate::internal::state::{ + CallSlot, CallSlotState, EarlyCallState, SessionState, StatisticsCollectorState, +}; #[cfg(target_os = "ios")] use crate::internal::utils::deactivate_audio_session; -use crate::internal::utils::{JoinHandle, KanalSink, KanalSource}; -use crate::internal::{ALPN, MAX_RINGTONE_LENGTH, Result}; -#[cfg(not(target_family = "wasm"))] -use crate::types::FrontendNotify; -use crate::types::{ManagerState, SessionStatus}; +use crate::internal::utils::{JoinHandle, KanalSink, KanalSource, spawn_task}; +use crate::internal::video::transport::{run_receiver, run_sender}; +use crate::internal::video::{ + VideoAttempt, VideoControl, VideoLaunch, VideoPreamble, VideoRole, VideoSlotEffect, +}; +use crate::internal::{ALPN, Result}; +use crate::types::{ + ManagerState, SessionStatus, VideoLifecycleEvent, VideoPhase, VideoSessionIdentity, + VideoSource, VideoStartOutcome, VideoTerminalReason, +}; use bytes::Bytes; use iroh::address_lookup::PkarrPublisher; +use iroh::endpoint::Connection; use iroh::endpoint::{default_relay_mode, presets}; use iroh::{Endpoint, PublicKey, RelayMode, SecretKey}; #[cfg(not(target_family = "wasm"))] @@ -237,82 +244,290 @@ where Ok(Some(endpoint)) } - #[cfg(not(target_family = "wasm"))] - #[instrument( - name = "screenshare", - skip_all, - fields( - peer.id = %message.peer, - role = if message.header.is_some() { "receiver" } else { "sender" } - ) - )] - pub(crate) async fn start_screenshare(&self, message: StartScreenshare) -> Result<()> { - let state = if let Some(s) = self.session_states.read().await.get(&message.peer) { + #[instrument(name = "video", skip_all)] + pub(crate) async fn request_video_source( + &self, + peer: PublicKey, + source: VideoSource, + ) -> VideoStartOutcome { + let state = if let Some(s) = self.session_states.read().await.get(&peer) { s.clone() } else { + warn!("video started for a peer without a session: {}", peer); + return VideoStartOutcome::NoSession; + }; + + if !self.core_state.call_slot.snapshot().is_ok_and(|slot| { + slot.state == CallSlotState::ActiveDirect && slot.direct_peer == Some(peer) + }) { warn!( - "screenshare started for a peer without a session: {}", - message.peer + "video started for a peer without an active direct call: {}", + peer ); - return Ok(()); - }; + return VideoStartOutcome::NoSession; + } - let stop = Arc::new(Notify::new()); - *state.stop_screenshare.lock().await = Some(stop.clone()); - let dart_stop = FrontendNotify::new(&stop); - - if let Some(ProtocolMessage::ScreenshareHeader { encoder_name }) = message.header { - // alert the frontend - self.callbacks.screenshare_started(dart_stop, false).await; - let stream = message.connection.accept_uni().await?; - // start playing back the screenshare - screenshare::playback( - stream, - stop, - encoder_name, - self.core_state.screenshare_config.width.load(Relaxed), - self.core_state.screenshare_config.height.load(Relaxed), - ) - .await?; - } else { - let config = if let Some(c) = self - .core_state - .screenshare_config - .recording_config - .read() + let descriptor = match self + .core_state + .screenshare_config + .prepare_video_sender(source) + .await + { + Ok((_, descriptor)) => descriptor, + Err(reason) => { + warn!(?reason, "video source unavailable at start"); + return VideoStartOutcome::Unavailable(reason); + } + }; + if let Some(control) = state.video_slot.start_local(descriptor).await { + let identity = VideoSessionIdentity { + peer_id: peer.to_string(), + session_id: control.session_id(), + }; + if let Some(event) = state + .video_slot + .current_event(peer.to_string(), VideoPhase::Offering, None) .await - .as_ref() { - c.clone() - } else { - // the frontend blocks this case - warn!("screenshare started without recording configuration"); - return Ok(()); - }; - - // send the peer a screenshare header - // the peer will open a stream after receiving it + self.observe_video_lifecycle(event); + } let result = state .message_sender - .send(ProtocolMessage::ScreenshareHeader { - encoder_name: config.encoder.to_string(), - }) + .send(ProtocolMessage::Video { control }) .await; - - if result.is_ok() { - // alert the frontend & provide the stop object - self.callbacks.screenshare_started(dart_stop, true).await; - let stream = message.connection.open_uni().await?; - // start recording the screenshare - screenshare::record(stream, stop, config).await?; - } else { + if result.is_err() { + self.finish_current_video(&state, peer, VideoTerminalReason::Failed) + .await; warn!("giving up on screenshare start, state closed"); + return VideoStartOutcome::Failed(VideoTerminalReason::Failed); } + return VideoStartOutcome::Requested(identity); } + VideoStartOutcome::AlreadyActive + } + pub(crate) async fn handle_video_control( + &self, + peer: PublicKey, + connection: &Connection, + control: VideoControl, + ) -> Result<()> { + let Some(state) = self.session_states.read().await.get(&peer).cloned() else { + return Ok(()); + }; + let local_offer_wins = self.peer_id().await.to_string() < peer.to_string(); + let effect = match control { + VideoControl::Offer(offer) => { + let capabilities = self + .core_state + .screenshare_config + .video_capabilities() + .await; + state + .video_slot + .receive_offer(offer, local_offer_wins, &capabilities.receive_formats) + .await + } + _ => state.video_slot.receive(control, local_offer_wins).await, + }; + match effect { + VideoSlotEffect::Send(control) => { + let _ = state + .message_sender + .send(ProtocolMessage::Video { control }) + .await; + } + VideoSlotEffect::Launch(launch) => { + self.launch_video_worker(&state, peer, connection, launch) + .await; + } + VideoSlotEffect::SendAndLaunch(control, launch) => { + self.launch_video_worker(&state, peer, connection, launch) + .await; + let _ = state + .message_sender + .send(ProtocolMessage::Video { control }) + .await; + } + VideoSlotEffect::DisplaceAndSendAndLaunch(displaced, control, launch) => { + let event = displaced + .cancel_and_join(peer.to_string(), VideoTerminalReason::Rejected) + .await; + self.observe_video_lifecycle(event); + self.launch_video_worker(&state, peer, connection, launch) + .await; + let _ = state + .message_sender + .send(ProtocolMessage::Video { control }) + .await; + } + VideoSlotEffect::Terminal(attempt, reason) => { + self.finish_video_attempt(&state, peer, attempt, reason) + .await; + } + VideoSlotEffect::Ignored => {} + } Ok(()) } + async fn launch_video_worker( + &self, + state: &Arc, + peer: PublicKey, + connection: &Connection, + launch: VideoLaunch, + ) { + self.observe_video_lifecycle(VideoLifecycleEvent { + identity: VideoSessionIdentity { + peer_id: peer.to_string(), + session_id: launch.attempt().session_id(), + }, + role: launch.role(), + source: launch.descriptor().source(), + phase: VideoPhase::Starting, + terminal_reason: None, + }); + let slot = Arc::clone(&state.video_slot); + let connection = connection.clone(); + let worker_launch = launch.clone(); + let (startup_sender, startup_receiver) = tokio::sync::oneshot::channel(); + let worker = match launch.role() { + VideoRole::Sender => { + let Ok((config, descriptor)) = self + .core_state + .screenshare_config + .prepare_video_sender(worker_launch.descriptor().source()) + .await + else { + self.finish_video_attempt( + state, + peer, + launch.attempt(), + VideoTerminalReason::Failed, + ) + .await; + return; + }; + if descriptor != worker_launch.descriptor() { + self.finish_video_attempt( + state, + peer, + launch.attempt(), + VideoTerminalReason::Failed, + ) + .await; + return; + } + spawn_task(async move { + let preamble = VideoPreamble::new( + worker_launch.attempt().session_id(), + worker_launch.descriptor(), + ); + let result = run_sender( + &connection, + preamble, + config, + worker_launch.cancellation(), + startup_sender, + ) + .await; + if !worker_launch.cancellation().is_cancelled() { + let reason = if result.is_ok() { + VideoTerminalReason::TransportEnded + } else { + VideoTerminalReason::Failed + }; + slot.report_terminal(worker_launch.attempt(), reason).await; + } + }) + } + VideoRole::Receiver => spawn_task(async move { + let preamble = VideoPreamble::new( + worker_launch.attempt().session_id(), + worker_launch.descriptor(), + ); + let result = run_receiver( + &connection, + preamble, + worker_launch.cancellation(), + startup_sender, + ) + .await; + if !worker_launch.cancellation().is_cancelled() { + let reason = if result.is_ok() { + VideoTerminalReason::TransportEnded + } else { + VideoTerminalReason::Failed + }; + slot.report_terminal(worker_launch.attempt(), reason).await; + } + }), + }; + if state.video_slot.install(&launch, worker).await { + let callbacks = Arc::clone(&self.callbacks); + let slot = Arc::clone(&state.video_slot); + spawn_task(async move { + let Ok(startup) = startup_receiver.await else { + return; + }; + if let Some(event) = slot + .complete_startup(&launch, startup, peer.to_string()) + .await + { + callbacks.video_lifecycle(event).await; + } + }); + } + } + + pub(crate) fn observe_video_lifecycle(&self, event: VideoLifecycleEvent) { + let callbacks = Arc::clone(&self.callbacks); + spawn_task(async move { callbacks.video_lifecycle(event).await }); + } + + pub(crate) async fn finish_video_attempt( + &self, + state: &Arc, + peer: PublicKey, + attempt: VideoAttempt, + reason: VideoTerminalReason, + ) { + let event = state + .video_slot + .current_event(peer.to_string(), VideoPhase::Terminal, Some(reason)) + .await; + if state + .video_slot + .cancel_and_join(attempt, reason) + .await + .is_some() + && let Some(event) = event + { + self.observe_video_lifecycle(event); + } + } + + pub(crate) async fn finish_current_video( + &self, + state: &Arc, + peer: PublicKey, + reason: VideoTerminalReason, + ) -> bool { + let event = state + .video_slot + .current_event(peer.to_string(), VideoPhase::Terminal, Some(reason)) + .await; + let finished = state + .video_slot + .cancel_current_and_join(reason) + .await + .is_some(); + if finished && let Some(event) = event { + self.observe_video_lifecycle(event); + } + finished + } + /// helper method to set up audio input stack using the telepathy-audio library pub(crate) async fn setup_input( &self, diff --git a/rust/telepathy-core/src/internal/messages.rs b/rust/telepathy-core/src/internal/messages.rs index f22b2573..619f0d64 100644 --- a/rust/telepathy-core/src/internal/messages.rs +++ b/rust/telepathy-core/src/internal/messages.rs @@ -1,5 +1,6 @@ use crate::internal::error::Error; use crate::internal::state::EarlyCallState; +use crate::internal::video::VideoControl; use iroh::PublicKey; use iroh::endpoint::Connection; use serde::Serialize; @@ -56,8 +57,8 @@ pub(crate) enum ProtocolMessage { attachments: Vec, }, KeepAlive, - ScreenshareHeader { - encoder_name: String, + Video { + control: VideoControl, }, } @@ -130,31 +131,36 @@ pub(crate) enum RoomJoinAdmission { Aborted, } -#[derive(Debug)] -pub(crate) struct StartScreenshare { - pub(crate) peer: PublicKey, - pub(crate) header: Option, - pub(crate) connection: Connection, -} - -impl StartScreenshare { - pub(crate) fn new_sender(peer: PublicKey, connection: Connection) -> Self { - Self { - peer, - header: None, - connection, - } - } - - pub(crate) fn new_receiver( - peer: PublicKey, - message: ProtocolMessage, - connection: Connection, - ) -> Self { - Self { - peer, - header: Some(message), - connection, +#[cfg(test)] +mod tests { + use super::ProtocolMessage; + use crate::internal::video::{ + VideoCodec, VideoControl, VideoMediaDescriptor, VideoRejectReason, VideoSessionId, + VideoTerminalReason, + }; + use speedy::{Readable, Writable}; + #[test] + fn video_controls_round_trip_with_the_initiator_identity() { + let session_id = VideoSessionId::new(); + let controls = [ + VideoControl::offer( + session_id, + VideoMediaDescriptor::display(VideoCodec::H264, 1920, 1080), + ), + VideoControl::ready(session_id), + VideoControl::reject(session_id, VideoRejectReason::UnsupportedCodec), + VideoControl::stop(session_id, VideoTerminalReason::Stopped), + ]; + + for control in controls { + let message = ProtocolMessage::Video { control }; + let encoded = message.write_to_vec().expect("control encodes"); + let decoded = ProtocolMessage::read_from_buffer(&encoded).expect("control decodes"); + let ProtocolMessage::Video { control: decoded } = decoded else { + panic!("video control must remain a video message"); + }; + assert_eq!(decoded, control); + assert_eq!(decoded.session_id(), session_id); } } } diff --git a/rust/telepathy-core/src/internal/screenshare.rs b/rust/telepathy-core/src/internal/screenshare.rs deleted file mode 100644 index 26beb163..00000000 --- a/rust/telepathy-core/src/internal/screenshare.rs +++ /dev/null @@ -1,557 +0,0 @@ -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -use crate::types::Capabilities; -use crate::types::{RecordingConfig, ScreenshareConfig}; -use bytes::Bytes; -use futures_util::{SinkExt, StreamExt}; -use iroh::endpoint::{RecvStream, SendStream}; -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -use regex::Regex; -use speedy::{Readable, Writable}; -use std::fmt::Display; -#[cfg(not(target_family = "wasm"))] -use std::process::Stdio; -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -use std::process::{ExitStatus, Output}; -use std::str::FromStr; -#[cfg(not(target_family = "wasm"))] -use std::sync::Arc; -use std::sync::atomic::Ordering::Relaxed; -#[cfg(not(target_family = "wasm"))] -use tokio::io::{AsyncReadExt, AsyncWriteExt}; -#[cfg(not(target_family = "wasm"))] -use tokio::process::Command; -#[cfg(not(target_family = "wasm"))] -use tokio::select; -#[cfg(not(target_family = "wasm"))] -use tokio::sync::Notify; -use tokio_util::codec::LengthDelimitedCodec; -#[cfg(not(target_family = "wasm"))] -use tracing::{error, info, instrument}; - -#[cfg(not(target_family = "wasm"))] -use crate::internal::error::{Error, ErrorKind}; - -#[cfg(not(target_family = "wasm"))] -type Result = std::result::Result; - -#[cfg(not(target_family = "wasm"))] -const BUFFER_SIZE: usize = 512; -#[cfg(target_os = "windows")] -const CREATION_FLAGS: u32 = 0x08000000; - -#[derive(Readable, Writable)] -pub(crate) struct ScreenshareConfigDisk { - pub(crate) recording_config: Option, - pub(crate) width: u32, - pub(crate) height: u32, -} - -impl From<&ScreenshareConfig> for ScreenshareConfigDisk { - fn from(cfg: &ScreenshareConfig) -> Self { - Self { - recording_config: cfg.recording_config.blocking_read().clone(), - width: cfg.width.load(Relaxed), - height: cfg.height.load(Relaxed), - } - } -} - -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -impl Capabilities { - pub(crate) async fn new() -> Self { - let codec_regex = Regex::new("V....[D.] ([^= ]+)\\s+(.+)").unwrap(); - - let mut command = Command::new("ffmpeg"); - command.arg("-hide_banner").arg("-encoders"); - - #[cfg(target_os = "windows")] - { - command.creation_flags(CREATION_FLAGS); - } - - let encoders_result = command.output().await; - - let mut command = Command::new("ffplay"); - command.arg("-hide_banner").arg("-decoders"); - - #[cfg(target_os = "windows")] - { - command.creation_flags(CREATION_FLAGS); - } - - let decoders_result = command.output().await; - - match (encoders_result, decoders_result) { - (Ok(encoders_output), Ok(decoders_output)) => { - let encoders = parse_codecs(encoders_output, &codec_regex) - .into_iter() - .filter_map(|codec| Encoder::from_str(&codec).ok()) - .collect(); - - let decoders = parse_codecs(decoders_output, &codec_regex) - .into_iter() - .filter_map(|codec| Decoder::from_str(&codec).ok()) - .collect(); - - Self { - _available: true, - encoders, - // TODO verify decoders here - _decoders: decoders, - devices: Device::devices(), - } - } - _ => Self { - _available: false, - encoders: Vec::new(), - _decoders: Vec::new(), - devices: Device::devices(), - }, - } - } -} - -#[derive(Clone, Debug, Readable, Writable)] -pub(crate) enum Device { - DirectShow, - GdiGrab, - DesktopDuplication, - AVFoundation(Vec), - X11Grab, -} - -impl Device { - #[cfg(target_os = "windows")] - fn devices() -> Vec { - vec![Self::DesktopDuplication, Self::GdiGrab, Self::DirectShow] - } - - #[cfg(target_os = "macos")] - fn devices() -> Vec { - // let devices_output = Command::new("ffmpeg") - // .arg("-hide_banner") - // .arg("-f") - // .arg("avfoundation") - // .arg("-list_devices") - // .arg("true") - // .arg("-i") - // .arg("\"\"") - // .output() - // .await; - - // TODO parse the output and use it for devices - - vec![Self::AVFoundation(vec![])] - } - - #[cfg(target_os = "linux")] - fn devices() -> Vec { - vec![Self::X11Grab] - } - - #[cfg(not(target_family = "wasm"))] - fn to_args(&self, encoder: Encoder) -> Vec<&str> { - // TODO figure out a way to only add the video size for encoders if needed - match self { - Self::DesktopDuplication => match encoder { - Encoder::H264Nvenc | Encoder::H264Qsv => vec![ - "-init_hw_device", - "d3d11va", - "-filter_complex", - "ddagrab=video_size=1920x1080", - ], - Encoder::HevcNvenc | Encoder::Av1Nvenc => { - vec!["-init_hw_device", "d3d11va", "-filter_complex", "ddagrab=0"] - } - _ => vec![ - "-init_hw_device", - "d3d11va", - "-filter_complex", - "ddagrab=0,hwdownload,format=bgra", - ], - }, - Self::GdiGrab => match encoder { - Encoder::H264Nvenc | Encoder::H264Qsv => vec![ - "-f", - "gdigrab", - "-framerate", - "30", - "-video_size", - "1920x1080", - "-i", - "desktop", - ], - _ => vec!["-f", "gdigrab", "-framerate", "30", "-i", "desktop"], - }, - _ => todo!(), - } - } -} - -impl Display for Device { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::DirectShow => write!(f, "DirectShow"), - Self::GdiGrab => write!(f, "GDI Grab"), - Self::DesktopDuplication => write!(f, "Desktop Duplication"), - Self::AVFoundation(devices) => write!(f, "AVFoundation: {:?}", devices), - Self::X11Grab => write!(f, "X11 Grab"), - } - } -} - -impl FromStr for Device { - type Err = (); - - fn from_str(s: &str) -> std::result::Result { - Ok(match s { - "DirectShow" => Self::DirectShow, - "GDI Grab" => Self::GdiGrab, - "Desktop Duplication" => Self::DesktopDuplication, - "X11 Grab" => Self::X11Grab, - _ => Self::AVFoundation(Vec::new()), // TODO handle the devices - }) - } -} - -#[derive(Clone, Copy, Debug, Readable, Writable)] -pub(crate) enum Encoder { - Libx264, - H264Nvenc, - H264Amf, - H264Qsv, - H264Vaapi, - Libx265, - HevcNvenc, - HevcAmf, - HevcQsv, - HevcVaapi, - Av1Nvenc, - Av1Amf, - Av1Qsv, - Av1Vaapi, -} - -impl From for &'static str { - fn from(val: Encoder) -> Self { - match val { - Encoder::Libx264 => "libx264", - Encoder::H264Nvenc => "h264_nvenc", - Encoder::H264Amf => "h264_amf", - Encoder::H264Qsv => "h264_qsv", - Encoder::H264Vaapi => "h264_vaapi", - Encoder::Libx265 => "libx265", - Encoder::HevcNvenc => "hevc_nvenc", - Encoder::HevcAmf => "hevc_amf", - Encoder::HevcQsv => "hevc_qsv", - Encoder::HevcVaapi => "hevc_vaapi", - Encoder::Av1Nvenc => "av1_nvenc", - Encoder::Av1Amf => "av1_amf", - Encoder::Av1Qsv => "av1_qsv", - Encoder::Av1Vaapi => "av1_vaapi", - } - } -} - -impl Display for Encoder { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", Into::<&'static str>::into(*self)) - } -} - -impl FromStr for Encoder { - type Err = (); - - fn from_str(s: &str) -> std::result::Result { - match s { - "libx264" => Ok(Self::Libx264), - "h264_nvenc" => Ok(Self::H264Nvenc), - "h264_amf" => Ok(Self::H264Amf), - "h264_qsv" => Ok(Self::H264Qsv), - "h264_vaapi" => Ok(Self::H264Vaapi), - "libx265" => Ok(Self::Libx265), - "hevc_nvenc" => Ok(Self::HevcNvenc), - "hevc_amf" => Ok(Self::HevcAmf), - "hevc_qsv" => Ok(Self::HevcQsv), - "hevc_vaapi" => Ok(Self::HevcVaapi), - "av1_nvenc" => Ok(Self::Av1Nvenc), - "av1_amf" => Ok(Self::Av1Amf), - "av1_qsv" => Ok(Self::Av1Qsv), - "av1_vaapi" => Ok(Self::Av1Vaapi), - _ => Err(()), - } - } -} - -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -pub(crate) fn encoder_from_str(value: &str) -> std::result::Result { - Encoder::from_str(value) -} - -#[cfg(not(target_family = "wasm"))] -impl Encoder { - /// returns the valid decoders for this encoder in preferred order - fn decoders(&self) -> Vec { - match self { - Self::Libx264 | Self::H264Nvenc | Self::H264Amf | Self::H264Qsv | Self::H264Vaapi => { - vec![Decoder::H264Cuvid, Decoder::H264Qsv, Decoder::H264] - } - Self::Libx265 | Self::HevcNvenc | Self::HevcAmf | Self::HevcQsv | Self::HevcVaapi => { - vec![Decoder::HevcCuvid, Decoder::HevcQsv, Decoder::Hevc] - } - Self::Av1Nvenc | Self::Av1Amf | Self::Av1Qsv | Self::Av1Vaapi => { - vec![Decoder::Av1Cuvid, Decoder::Av1Qsv] - } - } - } -} - -#[derive(Clone, Copy, Debug)] -pub(crate) enum Decoder { - H264, - H264Cuvid, - H264Qsv, - Hevc, - HevcCuvid, - HevcQsv, - Av1Cuvid, - Av1Qsv, -} - -impl From for &'static str { - fn from(val: Decoder) -> Self { - match val { - Decoder::H264 => "h264", - Decoder::H264Cuvid => "h264_cuvid", - Decoder::Hevc => "hevc", - Decoder::HevcCuvid => "hevc_cuvid", - Decoder::H264Qsv => "h264_qsv", - Decoder::HevcQsv => "hevc_qsv", - Decoder::Av1Cuvid => "av1_cuvid", - Decoder::Av1Qsv => "av1_qsv", - } - } -} - -impl FromStr for Decoder { - type Err = (); - - fn from_str(s: &str) -> std::result::Result { - match s { - "h264" => Ok(Self::H264), - "h264_cuvid" => Ok(Self::H264Cuvid), - "h264_qsv" => Ok(Self::H264Qsv), - "hevc" => Ok(Self::Hevc), - "hevc_cuvid" => Ok(Self::HevcCuvid), - "hevc_qsv" => Ok(Self::HevcQsv), - "av1_cuvid" => Ok(Self::Av1Cuvid), - "av1_qsv" => Ok(Self::Av1Qsv), - _ => Err(()), - } - } -} -impl RecordingConfig { - #[cfg(not(target_family = "wasm"))] - fn make_command(&self, test: bool) -> Command { - let mut command = Command::new("ffmpeg"); - command.args(self.device.to_args(self.encoder)); - - // sets the video size if specified - if let Some(height) = self.height { - command.arg("-vf"); - command.arg(format!("trunc(oh*a/2)*2:{}", height)); - } - - if test { - command.arg("-frames:v"); - command.arg("1"); - } - - command.args([ - "-c:v", - self.encoder.into(), - "-delay", - "0", - "-b:v", - self.bitrate.to_string().as_str(), - "-bufsize", - "1M", - "-f", - "mpegts", - "-", - ]); - - command - } - - #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] - pub(crate) async fn test_config(&self) -> Result { - let mut command = self.make_command(true); - command - .stdin(Stdio::null()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - - #[cfg(target_os = "windows")] - { - command.creation_flags(CREATION_FLAGS); - } - - let mut child = command.spawn()?; - child.wait().await.map_err(Into::into) - } -} - -#[cfg(not(target_family = "wasm"))] -struct PlaybackConfig { - decoder: Decoder, -} - -#[cfg(not(target_family = "wasm"))] -impl PlaybackConfig { - fn make_command(&self) -> Command { - let mut command = Command::new("ffplay"); - - command.args(["-vcodec", self.decoder.into(), "-f", "mpegts", "-i", "-"]); - - command - } -} - -#[cfg(not(target_family = "wasm"))] -#[instrument(name = "screenshare.record", skip_all)] -pub(crate) async fn record( - stream: SendStream, - stop: Arc, - config: RecordingConfig, -) -> Result<()> { - let mut transport = LengthDelimitedCodec::builder().new_write(stream); - - info!(event = "screenshare_record_start", ?config); - - let mut command = config.make_command(false); - - command.stdout(Stdio::piped()).stderr(Stdio::null()); - - #[cfg(target_os = "windows")] - { - command.creation_flags(CREATION_FLAGS); - } - - let mut child = command.spawn()?; - - let mut stdout = child.stdout.take().expect("Failed to capture stdout"); - - let future = async { - let mut frame = [0u8; BUFFER_SIZE]; - - while let Ok(read) = stdout.read(&mut frame).await { - if read == 0 { - break; - } - - if let Err(error) = transport.send(Bytes::copy_from_slice(&frame[..read])).await { - error!("Failed to write frame to ffmpeg {}", error); - break; - } - } - }; - - select! { - _ = future => { - stop.notify_waiters(); - info!("Recording finished"); - } - _ = stop.notified() => { - info!("Recording stopped"); - } - } - - _ = child.kill().await; - Ok(()) -} - -#[cfg(not(target_family = "wasm"))] -#[instrument(name = "screenshare.playback", skip_all)] -pub(crate) async fn playback( - stream: RecvStream, - stop: Arc, - encoder: String, - width: u32, - height: u32, -) -> Result<()> { - let mut transport = LengthDelimitedCodec::builder().new_read(stream); - - info!("Starting screen playback"); - let encoder = Encoder::from_str(&encoder).map_err(|_| ErrorKind::InvalidEncoder)?; - let decoders = encoder.decoders(); - - // TODO intelligently chose a decoder instead of using the first one - let config = PlaybackConfig { - decoder: decoders - .into_iter() - .next() - .ok_or(ErrorKind::NoEncoderAvailable)?, - }; - - let mut command = config.make_command(); - - command - .args([ - "-x", - &width.to_string(), - "-y", - &height.to_string(), - "-flags", - "low_delay", - "-analyzeduration", - "1", - // TODO -framedrop - "-window_title", - "Telepathy Screenshare", - ]) - .stdin(Stdio::piped()) - .stdout(Stdio::null()) - .stderr(Stdio::null()); - - #[cfg(target_os = "windows")] - { - command.creation_flags(CREATION_FLAGS); - } - - let mut child = command.spawn()?; - - let mut stdin = child.stdin.take().expect("Failed to capture stdin"); - - let future = async { - while let Some(Ok(message)) = transport.next().await { - if let Err(error) = stdin.write(&message).await { - error!("Failed to write frame to ffmpeg {}", error); - break; - } - } - }; - - select! { - _ = future => { - info!("Playback finished"); - } - _ = stop.notified() => { - info!("Playback stopped"); - } - } - - _ = child.kill().await; - Ok(()) -} - -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -fn parse_codecs(output: Output, regex: &Regex) -> Vec { - let output_str = String::from_utf8_lossy(&output.stdout); - - regex - .captures_iter(&output_str) - .filter_map(|cap| cap.get(1)) - .map(|cap| cap.as_str().to_string()) - .collect() -} diff --git a/rust/telepathy-core/src/internal/state.rs b/rust/telepathy-core/src/internal/state.rs index c5c488d1..756348fa 100644 --- a/rust/telepathy-core/src/internal/state.rs +++ b/rust/telepathy-core/src/internal/state.rs @@ -2,7 +2,10 @@ use crate::internal::Result; use crate::internal::callbacks::CoreCallbacks; use crate::internal::error::ErrorKind; use crate::internal::messages::{AudioHeader, ProtocolMessage, RoomMessage}; -use crate::types::{CodecConfig, Contact, NetworkConfig, ScreenshareConfig, SessionStatus}; +use crate::internal::video::VideoSlot; +use crate::types::{ + CodecConfig, Contact, NetworkConfig, ScreenshareConfig, SessionStatus, VideoTerminalReason, +}; use atomic_float::AtomicF32; use iroh::endpoint::{Connection, Path}; use iroh::{PublicKey, SecretKey, TransportAddr}; @@ -1018,9 +1021,7 @@ pub struct SessionState { pub(crate) end_call: Arc, - pub(crate) start_screenshare: Notify, - - pub(crate) stop_screenshare: Arc>>>, + pub(crate) video_slot: Arc, finished: CancellationToken, @@ -1043,8 +1044,7 @@ impl SessionState { upload_bandwidth: Default::default(), download_bandwidth: Default::default(), end_call: Default::default(), - start_screenshare: Default::default(), - stop_screenshare: Default::default(), + video_slot: Arc::new(VideoSlot::default()), finished: Default::default(), room_admission: AtomicU64::new(0), reconcile_room_generation: AtomicU64::new(0), @@ -1130,10 +1130,9 @@ impl SessionState { self.end_call.notify_one(); // stops the session loop self.stop_session.cancel(); - // stops any active screenshare threads - if let Some(notify) = self.stop_screenshare.lock().await.take() { - notify.notify_waiters(); - } + self.video_slot + .cancel_current_and_join(VideoTerminalReason::Teardown) + .await; } /// monitors the session connection to update bandwidth, latency, and push session statuses diff --git a/rust/telepathy-core/src/internal/utils.rs b/rust/telepathy-core/src/internal/utils.rs index 99dbca57..2fefc5ff 100644 --- a/rust/telepathy-core/src/internal/utils.rs +++ b/rust/telepathy-core/src/internal/utils.rs @@ -2,6 +2,7 @@ use crate::internal::callbacks::CoreStatisticsCallback; use crate::internal::error::{AudioStreamError, Error, ErrorKind}; use crate::internal::messages::ProtocolMessage; use crate::internal::state::StatisticsCollectorState; +use crate::internal::video::VIDEO_CONTROL_MAX_FRAME_LENGTH; use crate::overlay::{CONNECTED, LATENCY, LOSS}; use crate::types::Statistics; use bytes::Bytes; @@ -101,8 +102,18 @@ pub(crate) async fn read_message( transport: &mut FramedRead, ) -> Result { if let Some(Ok(buffer)) = transport.next().await { + if buffer.len() > VIDEO_CONTROL_MAX_FRAME_LENGTH { + return Err(speedy::Error::custom("control frame exceeds maximum size").into()); + } let message = ProtocolMessage::read_from_buffer(&buffer[..])?; - Ok(message) + if let ProtocolMessage::Video { control } = message { + control + .validate() + .map_err(|_| speedy::Error::custom("invalid video control"))?; + Ok(ProtocolMessage::Video { control }) + } else { + Ok(message) + } } else { Err(ErrorKind::TransportRecv.into()) } @@ -223,6 +234,114 @@ where } } +#[cfg(test)] +mod tests { + use super::read_message; + use crate::internal::ALPN; + use crate::internal::messages::ProtocolMessage; + use crate::internal::video::{ + VIDEO_CONTROL_MAX_FRAME_LENGTH, VideoCodec, VideoControl, VideoMediaDescriptor, + VideoSessionId, + }; + use bytes::Bytes; + use futures_util::SinkExt; + use iroh::endpoint::{Connection, presets}; + use speedy::Writable; + use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; + + async fn iroh_pair() -> (iroh::Endpoint, iroh::Endpoint, Connection, Connection) { + let server = iroh::Endpoint::builder(presets::N0) + .relay_mode(iroh::RelayMode::Disabled) + .alpns(vec![ALPN.to_vec()]) + .bind() + .await + .expect("server endpoint binds"); + let client = iroh::Endpoint::builder(presets::N0) + .relay_mode(iroh::RelayMode::Disabled) + .bind() + .await + .expect("client endpoint binds"); + let server_addr = server.addr(); + let (outbound, inbound) = tokio::join!(client.connect(server_addr, ALPN), async { + server + .accept() + .await + .expect("server receives connection") + .await + }); + ( + client, + server, + outbound.expect("client connects"), + inbound.expect("server accepts"), + ) + } + + async fn send_control_bytes(connection: &Connection, bytes: Vec) { + let stream = connection.open_uni().await.expect("stream opens"); + let codec = LengthDelimitedCodec::builder() + .max_frame_length(VIDEO_CONTROL_MAX_FRAME_LENGTH + 1) + .new_codec(); + let mut framed = FramedWrite::new(stream, codec); + framed.send(Bytes::from(bytes)).await.expect("frame writes"); + framed.into_inner().finish().expect("stream finishes"); + } + + async fn receive_control(connection: &Connection) -> super::Result { + let stream = connection.accept_uni().await.expect("stream accepted"); + let codec = LengthDelimitedCodec::builder() + .max_frame_length(VIDEO_CONTROL_MAX_FRAME_LENGTH + 1) + .new_codec(); + read_message(&mut FramedRead::new(stream, codec)).await + } + + #[tokio::test(flavor = "multi_thread")] + async fn read_message_validates_video_controls_on_real_iroh_streams() { + let (client, server, outbound, inbound) = iroh_pair().await; + let session_id = VideoSessionId::new(); + let valid = ProtocolMessage::Video { + control: VideoControl::offer( + session_id, + VideoMediaDescriptor::display(VideoCodec::H264, 1_280, 720), + ), + }; + send_control_bytes( + &outbound, + valid.write_to_vec().expect("valid control encodes"), + ) + .await; + assert!(matches!( + receive_control(&inbound).await.expect("valid control reads"), + ProtocolMessage::Video { control } if control.session_id() == session_id + )); + + send_control_bytes(&outbound, vec![0xFF]).await; + assert!(receive_control(&inbound).await.is_err()); + + let invalid = ProtocolMessage::Video { + control: VideoControl::offer( + VideoSessionId::new(), + VideoMediaDescriptor::display(VideoCodec::H264, 0, 720), + ), + }; + send_control_bytes( + &outbound, + invalid.write_to_vec().expect("invalid control encodes"), + ) + .await; + assert!(receive_control(&inbound).await.is_err()); + + let ((), oversized_result) = tokio::join!( + send_control_bytes(&outbound, vec![0; VIDEO_CONTROL_MAX_FRAME_LENGTH + 1]), + receive_control(&inbound), + ); + assert!(oversized_result.is_err()); + + client.close().await; + server.close().await; + } +} + #[cfg(target_os = "ios")] pub(crate) fn configure_audio_session() { use objc2::runtime::{AnyObject, Bool}; diff --git a/rust/telepathy-core/src/internal/video.rs b/rust/telepathy-core/src/internal/video.rs new file mode 100644 index 00000000..cc00e216 --- /dev/null +++ b/rust/telepathy-core/src/internal/video.rs @@ -0,0 +1,1043 @@ +pub mod platform; +pub mod transport; + +use crate::internal::utils::JoinHandle; +use speedy::{Readable, Writable}; +use tokio::sync::{Mutex, Notify}; +use tokio_util::sync::CancellationToken; + +pub(crate) const VIDEO_PROTOCOL_REVISION: u8 = 1; +pub const VIDEO_CONTROL_MAX_FRAME_LENGTH: usize = 8 * 1024 * 1024; +pub const VIDEO_PREAMBLE_MAX_LENGTH: usize = 512; +pub const VIDEO_MEDIA_MAX_FRAME_LENGTH: usize = 64 * 1024; +pub(crate) const VIDEO_NEGOTIATION_TIMEOUT: std::time::Duration = + std::time::Duration::from_secs(15); +const MAX_VIDEO_DIMENSION: u32 = 16_384; + +pub(crate) use crate::types::{ + VideoCodec, VideoLifecycleEvent, VideoMediaFormat, VideoPhase, VideoRole, VideoSessionId, + VideoSessionIdentity, VideoSource, VideoTerminalReason, +}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub(crate) struct LocalVideoGeneration(u64); + +impl LocalVideoGeneration { + pub(crate) const fn initial() -> Self { + Self(0) + } + + pub(crate) const fn next(self) -> Self { + Self(self.0 + 1) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VideoWorkerStartup { + Ready, + Failed, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct VideoAttempt { + session_id: VideoSessionId, + generation: LocalVideoGeneration, +} + +impl VideoAttempt { + pub(crate) const fn new(session_id: VideoSessionId, generation: LocalVideoGeneration) -> Self { + Self { + session_id, + generation, + } + } + + pub(crate) fn accepts(self, control: VideoControl) -> bool { + self.session_id == control.session_id() + } + + pub const fn session_id(self) -> VideoSessionId { + self.session_id + } +} + +struct VideoReservation { + attempt: VideoAttempt, + role: VideoRole, + phase: VideoPhase, + descriptor: VideoMediaDescriptor, + cancellation: CancellationToken, + worker: Option>, +} + +impl std::fmt::Debug for VideoReservation { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter + .debug_struct("VideoReservation") + .field("attempt", &self.attempt) + .field("role", &self.role) + .field("phase", &self.phase) + .field("descriptor", &self.descriptor) + .field("cancellation", &self.cancellation) + .field("worker_installed", &self.worker.is_some()) + .finish() + } +} + +#[derive(Debug)] +struct VideoSlotState { + generation: LocalVideoGeneration, + reservation: Option, + pending_terminal: Option<(VideoAttempt, VideoTerminalReason)>, +} + +impl Default for VideoSlotState { + fn default() -> Self { + Self { + generation: LocalVideoGeneration::initial(), + reservation: None, + pending_terminal: None, + } + } +} + +#[derive(Debug, Default)] +pub struct VideoSlot { + state: Mutex, + idle: Notify, + terminal: Notify, +} + +#[derive(Debug, Clone)] +pub struct VideoLaunch { + attempt: VideoAttempt, + role: VideoRole, + descriptor: VideoMediaDescriptor, + cancellation: CancellationToken, +} + +impl VideoLaunch { + pub const fn attempt(&self) -> VideoAttempt { + self.attempt + } + + pub const fn role(&self) -> VideoRole { + self.role + } + + pub const fn descriptor(&self) -> VideoMediaDescriptor { + self.descriptor + } + + pub const fn cancellation(&self) -> &CancellationToken { + &self.cancellation + } +} + +#[derive(Debug)] +pub struct VideoDisplacement { + reservation: VideoReservation, +} + +impl VideoDisplacement { + fn new(reservation: VideoReservation) -> Self { + reservation.cancellation.cancel(); + Self { reservation } + } + + pub async fn cancel_and_join( + mut self, + peer_id: String, + reason: VideoTerminalReason, + ) -> VideoLifecycleEvent { + if let Some(worker) = self.reservation.worker.take() { + let _ = worker.await; + } + VideoLifecycleEvent { + identity: VideoSessionIdentity { + peer_id, + session_id: self.reservation.attempt.session_id(), + }, + role: self.reservation.role, + source: self.reservation.descriptor.source(), + phase: VideoPhase::Terminal, + terminal_reason: Some(reason), + } + } +} + +#[derive(Debug)] +pub enum VideoSlotEffect { + Ignored, + Send(VideoControl), + Launch(VideoLaunch), + SendAndLaunch(VideoControl, VideoLaunch), + DisplaceAndSendAndLaunch(VideoDisplacement, VideoControl, VideoLaunch), + Terminal(VideoAttempt, VideoTerminalReason), +} + +impl VideoSlotEffect { + #[cfg(test)] + fn launch(self) -> Option { + match self { + Self::Launch(launch) + | Self::SendAndLaunch(_, launch) + | Self::DisplaceAndSendAndLaunch(_, _, launch) => Some(launch), + Self::Ignored | Self::Send(_) | Self::Terminal(_, _) => None, + } + } +} + +impl VideoSlot { + pub async fn start_local(&self, descriptor: VideoMediaDescriptor) -> Option { + let mut state = self.state.lock().await; + if state.reservation.is_some() { + return None; + } + state.generation = state.generation.next(); + let session_id = VideoSessionId::new(); + state.reservation = Some(VideoReservation { + attempt: VideoAttempt::new(session_id, state.generation), + role: VideoRole::Sender, + phase: VideoPhase::WaitingReady, + descriptor, + cancellation: CancellationToken::new(), + worker: None, + }); + Some(VideoControl::offer(session_id, descriptor)) + } + + pub async fn receive(&self, control: VideoControl, local_offer_wins: bool) -> VideoSlotEffect { + let mut state = self.state.lock().await; + match control { + VideoControl::Offer(offer) => match state.reservation.as_ref() { + None => { + let (control, launch) = Self::accept_remote_offer(&mut state, offer); + VideoSlotEffect::SendAndLaunch(control, launch) + } + Some(current) + if current.attempt.accepts(control) && current.role == VideoRole::Receiver => + { + VideoSlotEffect::Send(VideoControl::ready(control.session_id())) + } + Some(current) + if current.role == VideoRole::Sender + && current.phase == VideoPhase::WaitingReady + && !local_offer_wins => + { + let displaced = VideoDisplacement::new( + state + .reservation + .take() + .expect("matching reservation checked"), + ); + let (control, launch) = Self::accept_remote_offer(&mut state, offer); + VideoSlotEffect::DisplaceAndSendAndLaunch(displaced, control, launch) + } + Some(_) => VideoSlotEffect::Send(VideoControl::reject( + control.session_id(), + VideoRejectReason::SessionUnavailable, + )), + }, + VideoControl::Ready { .. } => match state.reservation.as_mut() { + Some(current) + if current.role == VideoRole::Sender + && current.phase == VideoPhase::WaitingReady + && current.attempt.accepts(control) => + { + current.phase = VideoPhase::Starting; + VideoSlotEffect::Launch(Self::launch(current)) + } + Some(current) + if current.role == VideoRole::Sender + && current.phase == VideoPhase::Starting + && current.attempt.accepts(control) => + { + VideoSlotEffect::Ignored + } + Some(_) | None => VideoSlotEffect::Ignored, + }, + VideoControl::Reject { .. } => { + Self::terminal_if_current(&mut state, control, VideoTerminalReason::Rejected) + } + VideoControl::Stop { reason, .. } => { + Self::terminal_if_current(&mut state, control, reason) + } + } + } + + pub async fn receive_offer( + &self, + offer: VideoOffer, + local_offer_wins: bool, + receive_formats: &[VideoMediaFormat], + ) -> VideoSlotEffect { + if !receive_formats.contains(&offer.descriptor.format) { + return VideoSlotEffect::Send(VideoControl::reject( + offer.session_id, + VideoRejectReason::UnsupportedCodec, + )); + } + self.receive(VideoControl::Offer(offer), local_offer_wins) + .await + } + + pub async fn install(&self, launch: &VideoLaunch, worker: JoinHandle<()>) -> bool { + let mut state = self.state.lock().await; + let matches = state.reservation.as_ref().is_some_and(|reservation| { + reservation.attempt == launch.attempt + && reservation.phase == VideoPhase::Starting + && reservation.worker.is_none() + }); + if matches { + let reservation = state + .reservation + .as_mut() + .expect("matching reservation checked above"); + reservation.worker = Some(worker); + return true; + } + drop(state); + launch.cancellation.cancel(); + let _ = worker.await; + false + } + + pub async fn complete_startup( + &self, + launch: &VideoLaunch, + startup: VideoWorkerStartup, + peer_id: String, + ) -> Option { + if startup != VideoWorkerStartup::Ready { + return None; + } + let mut state = self.state.lock().await; + let reservation = state.reservation.as_mut()?; + if reservation.attempt != launch.attempt + || reservation.phase != VideoPhase::Starting + || reservation.worker.is_none() + { + return None; + } + reservation.phase = VideoPhase::Active; + Some(VideoLifecycleEvent { + identity: VideoSessionIdentity { + peer_id, + session_id: reservation.attempt.session_id(), + }, + role: reservation.role, + source: reservation.descriptor.source(), + phase: VideoPhase::Active, + terminal_reason: None, + }) + } + + pub(crate) async fn cancel_current_and_join( + &self, + reason: VideoTerminalReason, + ) -> Option { + let attempt = self.state.lock().await.reservation.as_ref()?.attempt; + self.cancel_and_join(attempt, reason).await + } + + pub async fn cancel_and_join( + &self, + attempt: VideoAttempt, + reason: VideoTerminalReason, + ) -> Option { + loop { + let idle = self.idle.notified(); + tokio::pin!(idle); + idle.as_mut().enable(); + let worker = { + let mut state = self.state.lock().await; + let current = state.reservation.as_mut()?; + if current.attempt != attempt { + return None; + } + if current.phase == VideoPhase::Stopping { + None + } else { + current.phase = VideoPhase::Stopping; + current.cancellation.cancel(); + Some(current.worker.take()) + } + }; + let Some(worker) = worker else { + idle.await; + continue; + }; + if let Some(worker) = worker { + let _ = worker.await; + } + let mut state = self.state.lock().await; + if state + .reservation + .as_ref() + .is_some_and(|current| current.attempt == attempt) + { + state.reservation = None; + state.pending_terminal = None; + drop(state); + self.idle.notify_waiters(); + return Some(reason); + } + return None; + } + } + + pub async fn current_event( + &self, + peer_id: String, + phase: VideoPhase, + terminal_reason: Option, + ) -> Option { + let state = self.state.lock().await; + let current = state.reservation.as_ref()?; + Some(VideoLifecycleEvent { + identity: VideoSessionIdentity { + peer_id, + session_id: current.attempt.session_id(), + }, + role: current.role, + source: current.descriptor.source(), + phase, + terminal_reason, + }) + } + + pub(crate) async fn report_terminal(&self, attempt: VideoAttempt, reason: VideoTerminalReason) { + let mut state = self.state.lock().await; + if state.pending_terminal.is_none() + && state.reservation.as_ref().is_some_and(|current| { + current.attempt == attempt && current.phase != VideoPhase::Stopping + }) + { + state.pending_terminal = Some((attempt, reason)); + drop(state); + self.terminal.notify_one(); + } + } + + pub(crate) async fn terminal_notified(&self) { + self.terminal.notified().await; + } + + pub(crate) async fn take_terminal(&self) -> Option<(VideoAttempt, VideoTerminalReason)> { + self.state.lock().await.pending_terminal.take() + } + + pub(crate) async fn expire_waiting_ready(&self) -> Option<(VideoAttempt, VideoTerminalReason)> { + let state = self.state.lock().await; + let current = state.reservation.as_ref()?; + if current.role != VideoRole::Sender || current.phase != VideoPhase::WaitingReady { + return None; + } + Some((current.attempt, VideoTerminalReason::Failed)) + } + + fn accept_remote_offer( + state: &mut VideoSlotState, + offer: VideoOffer, + ) -> (VideoControl, VideoLaunch) { + state.generation = state.generation.next(); + let session_id = VideoControl::Offer(offer).session_id(); + state.reservation = Some(VideoReservation { + attempt: VideoAttempt::new(session_id, state.generation), + role: VideoRole::Receiver, + phase: VideoPhase::Starting, + descriptor: offer.descriptor, + cancellation: CancellationToken::new(), + worker: None, + }); + let launch = Self::launch(state.reservation.as_ref().expect("reservation inserted")); + (VideoControl::ready(session_id), launch) + } + + fn terminal_if_current( + state: &mut VideoSlotState, + control: VideoControl, + reason: VideoTerminalReason, + ) -> VideoSlotEffect { + let Some(current) = state.reservation.as_ref() else { + return VideoSlotEffect::Ignored; + }; + if !current.attempt.accepts(control) { + return VideoSlotEffect::Ignored; + } + VideoSlotEffect::Terminal(current.attempt, reason) + } + + fn launch(reservation: &VideoReservation) -> VideoLaunch { + VideoLaunch { + attempt: reservation.attempt, + role: reservation.role, + descriptor: reservation.descriptor, + cancellation: reservation.cancellation.clone(), + } + } +} + +#[derive(Readable, Writable, Debug, Clone, Copy, PartialEq, Eq)] +pub enum VideoRejectReason { + UnsupportedSource, + UnsupportedCodec, + InvalidDescriptor, + SessionUnavailable, +} + +#[derive(Readable, Writable, Debug, Clone, Copy, PartialEq, Eq)] +pub struct VideoMediaDescriptor { + source: VideoSource, + format: VideoMediaFormat, + width: u32, + height: u32, +} + +impl VideoMediaDescriptor { + #[cfg_attr( + not(any(target_os = "windows", target_os = "macos", target_os = "linux")), + expect( + dead_code, + reason = "only used by the desktop ffmpeg backend and tests" + ) + )] + pub const fn display(codec: VideoCodec, width: u32, height: u32) -> Self { + Self { + source: VideoSource::Display, + format: VideoMediaFormat::MpegTs(codec), + width, + height, + } + } + + pub(crate) const fn source(self) -> VideoSource { + self.source + } + + #[cfg_attr( + not(any(target_os = "windows", target_os = "macos", target_os = "linux")), + expect( + dead_code, + reason = "only used by the desktop ffmpeg backend and tests" + ) + )] + pub(crate) const fn codec(self) -> VideoCodec { + match self.format { + VideoMediaFormat::MpegTs(codec) => codec, + } + } + + #[cfg_attr( + not(any(target_os = "windows", target_os = "macos", target_os = "linux")), + expect( + dead_code, + reason = "only used by the desktop ffmpeg backend and tests" + ) + )] + pub(crate) const fn dimensions(self) -> (u32, u32) { + (self.width, self.height) + } + + pub(crate) const fn is_valid(self) -> bool { + self.width > 0 + && self.height > 0 + && self.width <= MAX_VIDEO_DIMENSION + && self.height <= MAX_VIDEO_DIMENSION + } +} + +#[derive(Readable, Writable, Debug, Clone, Copy, PartialEq, Eq)] +pub struct VideoOffer { + revision: u8, + session_id: VideoSessionId, + descriptor: VideoMediaDescriptor, +} + +#[derive(Readable, Writable, Debug, Clone, Copy, PartialEq, Eq)] +pub enum VideoControl { + Offer(VideoOffer), + Ready { + revision: u8, + session_id: VideoSessionId, + }, + Reject { + revision: u8, + session_id: VideoSessionId, + reason: VideoRejectReason, + }, + Stop { + revision: u8, + session_id: VideoSessionId, + reason: VideoTerminalReason, + }, +} + +impl VideoControl { + pub const fn offer(session_id: VideoSessionId, descriptor: VideoMediaDescriptor) -> Self { + Self::Offer(VideoOffer { + revision: VIDEO_PROTOCOL_REVISION, + session_id, + descriptor, + }) + } + + pub const fn ready(session_id: VideoSessionId) -> Self { + Self::Ready { + revision: VIDEO_PROTOCOL_REVISION, + session_id, + } + } + + pub(crate) const fn reject(session_id: VideoSessionId, reason: VideoRejectReason) -> Self { + Self::Reject { + revision: VIDEO_PROTOCOL_REVISION, + session_id, + reason, + } + } + + pub const fn stop(session_id: VideoSessionId, reason: VideoTerminalReason) -> Self { + Self::Stop { + revision: VIDEO_PROTOCOL_REVISION, + session_id, + reason, + } + } + + pub const fn session_id(self) -> VideoSessionId { + match self { + Self::Offer(offer) => offer.session_id, + Self::Ready { session_id, .. } + | Self::Reject { session_id, .. } + | Self::Stop { session_id, .. } => session_id, + } + } + + pub(crate) const fn validate(self) -> Result<(), VideoProtocolError> { + let revision = match self { + Self::Offer(offer) => { + if !offer.descriptor.is_valid() { + return Err(VideoProtocolError::InvalidDimensions); + } + offer.revision + } + Self::Ready { revision, .. } + | Self::Reject { revision, .. } + | Self::Stop { revision, .. } => revision, + }; + if revision == VIDEO_PROTOCOL_REVISION { + Ok(()) + } else { + Err(VideoProtocolError::UnsupportedRevision(revision)) + } + } +} + +#[derive(Readable, Writable, Debug, Clone, Copy, PartialEq, Eq)] +pub struct VideoPreamble { + revision: u8, + session_id: VideoSessionId, + descriptor: VideoMediaDescriptor, +} + +impl VideoPreamble { + pub const fn new(session_id: VideoSessionId, descriptor: VideoMediaDescriptor) -> Self { + Self::with_revision(VIDEO_PROTOCOL_REVISION, session_id, descriptor) + } + + pub(crate) const fn with_revision( + revision: u8, + session_id: VideoSessionId, + descriptor: VideoMediaDescriptor, + ) -> Self { + Self { + revision, + session_id, + descriptor, + } + } + + const fn validate(self) -> Result<(), VideoProtocolError> { + if self.revision != VIDEO_PROTOCOL_REVISION { + return Err(VideoProtocolError::UnsupportedRevision(self.revision)); + } + if !self.descriptor.is_valid() { + return Err(VideoProtocolError::InvalidDimensions); + } + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum VideoProtocolError { + FrameTooLarge, + Malformed, + UnsupportedRevision(u8), + InvalidDimensions, +} + +pub(crate) fn encode_preamble(preamble: &VideoPreamble) -> Result, VideoProtocolError> { + preamble.validate()?; + let encoded = preamble + .write_to_vec() + .map_err(|_| VideoProtocolError::Malformed)?; + if encoded.len() > VIDEO_PREAMBLE_MAX_LENGTH { + return Err(VideoProtocolError::FrameTooLarge); + } + Ok(encoded) +} + +pub(crate) fn decode_preamble(bytes: &[u8]) -> Result { + if bytes.len() > VIDEO_PREAMBLE_MAX_LENGTH { + return Err(VideoProtocolError::FrameTooLarge); + } + let preamble = + VideoPreamble::read_from_buffer(bytes).map_err(|_| VideoProtocolError::Malformed)?; + preamble.validate()?; + Ok(preamble) +} + +#[cfg(test)] +mod tests { + use super::{ + LocalVideoGeneration, VideoAttempt, VideoCodec, VideoControl, VideoMediaDescriptor, + VideoPreamble, VideoProtocolError, VideoSessionId, decode_preamble, encode_preamble, + }; + use crate::internal::state::SessionState; + use speedy::Writable; + + #[test] + fn preamble_round_trip_preserves_identity_and_descriptor() { + let session_id = VideoSessionId::new(); + let descriptor = VideoMediaDescriptor::display(VideoCodec::H264, 1920, 1080); + let preamble = VideoPreamble::new(session_id, descriptor); + + let encoded = encode_preamble(&preamble).expect("valid preamble encodes"); + let decoded = decode_preamble(&encoded).expect("encoded preamble decodes"); + + assert_eq!(decoded, preamble); + } + + #[test] + fn preamble_rejects_unknown_revision_and_malformed_dimensions() { + let session_id = VideoSessionId::new(); + let unsupported_revision = VideoPreamble::with_revision( + 99, + session_id, + VideoMediaDescriptor::display(VideoCodec::H264, 1920, 1080), + ); + let malformed_dimensions = VideoPreamble::new( + session_id, + VideoMediaDescriptor::display(VideoCodec::H264, 0, 1080), + ); + + assert_eq!( + decode_preamble(&unsupported_revision.write_to_vec().expect("encodes")), + Err(VideoProtocolError::UnsupportedRevision(99)) + ); + assert_eq!( + decode_preamble(&malformed_dimensions.write_to_vec().expect("encodes")), + Err(VideoProtocolError::InvalidDimensions) + ); + } + + #[test] + fn preamble_rejects_unknown_codec_and_oversize_before_decode() { + let preamble = VideoPreamble::new( + VideoSessionId::new(), + VideoMediaDescriptor::display(VideoCodec::H264, 1920, 1080), + ); + let mut unknown_codec = encode_preamble(&preamble).expect("encodes"); + unknown_codec[25..29].copy_from_slice(&99_u32.to_le_bytes()); + + assert_eq!( + decode_preamble(&unknown_codec), + Err(VideoProtocolError::Malformed) + ); + assert_eq!( + decode_preamble(&vec![0; super::VIDEO_PREAMBLE_MAX_LENGTH + 1]), + Err(VideoProtocolError::FrameTooLarge) + ); + } + + #[test] + fn controls_preserve_identity_and_distinguish_sessions() { + let session_id = VideoSessionId::new(); + let offer = VideoControl::offer( + session_id, + VideoMediaDescriptor::display(VideoCodec::H264, 1920, 1080), + ); + + assert_eq!(offer.session_id(), session_id); + assert_ne!( + offer, + VideoControl::offer( + VideoSessionId::new(), + VideoMediaDescriptor::display(VideoCodec::H264, 1920, 1080) + ) + ); + } + + #[test] + fn local_generation_advances() { + let generation = LocalVideoGeneration::initial(); + + assert_ne!(generation, generation.next()); + } + + #[test] + fn attempt_accepts_duplicate_control_but_not_replaced_identity() { + let session_id = VideoSessionId::new(); + let generation = LocalVideoGeneration::initial(); + let attempt = VideoAttempt::new(session_id, generation); + let duplicate = VideoControl::ready(session_id); + let replacement = VideoControl::ready(VideoSessionId::new()); + + assert!(attempt.accepts(duplicate)); + assert!(!attempt.accepts(replacement)); + } + + #[tokio::test] + async fn slot_remains_reserved_until_matching_worker_joins() { + let slot = std::sync::Arc::new(super::VideoSlot::default()); + let descriptor = VideoMediaDescriptor::display(VideoCodec::H264, 1920, 1080); + let offer = slot + .start_local(descriptor) + .await + .expect("first offer reserves slot"); + let launch = slot + .receive(VideoControl::ready(offer.session_id()), true) + .await + .launch() + .expect("ready starts matching sender"); + let worker_exited = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let worker_exited_clone = std::sync::Arc::clone(&worker_exited); + let cancellation = launch.cancellation().clone(); + let worker = tokio::spawn(async move { + cancellation.cancelled().await; + tokio::task::yield_now().await; + worker_exited_clone.store(true, std::sync::atomic::Ordering::Relaxed); + }); + slot.install(&launch, worker).await; + + let cleanup = tokio::spawn({ + let slot = std::sync::Arc::clone(&slot); + async move { + slot.cancel_and_join(launch.attempt(), super::VideoTerminalReason::Stopped) + .await + } + }); + + assert_eq!( + cleanup.await.expect("cleanup joins"), + Some(super::VideoTerminalReason::Stopped) + ); + assert!(worker_exited.load(std::sync::atomic::Ordering::Relaxed)); + assert!( + slot.current_event("peer".to_string(), super::VideoPhase::Terminal, None) + .await + .is_none() + ); + } + + #[tokio::test] + async fn stale_worker_installation_cancels_and_joins_without_clearing_replacement() { + let slot = super::VideoSlot::default(); + let descriptor = VideoMediaDescriptor::display(VideoCodec::H264, 1920, 1080); + let first = slot.start_local(descriptor).await.expect("first offer"); + let first_launch = slot + .receive(VideoControl::ready(first.session_id()), true) + .await + .launch() + .expect("first launch"); + slot.cancel_and_join(first_launch.attempt(), super::VideoTerminalReason::Stopped) + .await; + let second = slot + .start_local(descriptor) + .await + .expect("replacement offer"); + let stale_joined = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); + let stale_joined_clone = std::sync::Arc::clone(&stale_joined); + let cancellation = first_launch.cancellation().clone(); + let stale_worker = tokio::spawn(async move { + cancellation.cancelled().await; + stale_joined_clone.store(true, std::sync::atomic::Ordering::Relaxed); + }); + + slot.install(&first_launch, stale_worker).await; + + assert!(stale_joined.load(std::sync::atomic::Ordering::Relaxed)); + assert!( + slot.current_event("peer".to_string(), super::VideoPhase::WaitingReady, None) + .await + .is_some() + ); + assert_ne!(first.session_id(), second.session_id()); + } + + #[tokio::test] + async fn concurrent_terminal_claims_wait_for_one_join_and_emit_once() { + let slot = std::sync::Arc::new(super::VideoSlot::default()); + let descriptor = VideoMediaDescriptor::display(VideoCodec::H264, 1920, 1080); + let offer = slot + .start_local(descriptor) + .await + .expect("offer reserves slot"); + let launch = slot + .receive(VideoControl::ready(offer.session_id()), true) + .await + .launch() + .expect("ready launches sender"); + let release = std::sync::Arc::new(tokio::sync::Notify::new()); + let (worker_cancelled, cancelled) = tokio::sync::oneshot::channel(); + let worker_release = std::sync::Arc::clone(&release); + let cancellation = launch.cancellation().clone(); + let worker = tokio::spawn(async move { + cancellation.cancelled().await; + let _ = worker_cancelled.send(()); + worker_release.notified().await; + }); + slot.install(&launch, worker).await; + let attempt = launch.attempt(); + let first = tokio::spawn({ + let slot = std::sync::Arc::clone(&slot); + async move { + slot.cancel_and_join(attempt, super::VideoTerminalReason::Stopped) + .await + } + }); + let second = tokio::spawn({ + let slot = std::sync::Arc::clone(&slot); + async move { + slot.cancel_and_join(attempt, super::VideoTerminalReason::Teardown) + .await + } + }); + cancelled.await.expect("worker observes cancellation"); + tokio::task::yield_now().await; + assert!(!first.is_finished()); + assert!(!second.is_finished()); + + release.notify_one(); + + let outcomes = [ + first.await.expect("first cleanup joins"), + second.await.expect("second cleanup joins"), + ]; + assert_eq!( + outcomes.iter().filter(|outcome| outcome.is_some()).count(), + 1 + ); + assert!( + slot.current_event("peer".to_string(), super::VideoPhase::Terminal, None) + .await + .is_none() + ); + } + + #[tokio::test] + async fn session_teardown_waits_for_installed_video_worker() { + let (sender, _receiver) = tokio::sync::mpsc::channel(1); + let state = std::sync::Arc::new(SessionState::new(&sender)); + let descriptor = VideoMediaDescriptor::display(VideoCodec::H264, 1920, 1080); + let offer = state + .video_slot + .start_local(descriptor) + .await + .expect("offer reserves slot"); + let launch = state + .video_slot + .receive(VideoControl::ready(offer.session_id()), true) + .await + .launch() + .expect("ready launches sender"); + let release = std::sync::Arc::new(tokio::sync::Notify::new()); + let (worker_cancelled, cancelled) = tokio::sync::oneshot::channel(); + let worker_release = std::sync::Arc::clone(&release); + let cancellation = launch.cancellation().clone(); + let worker = tokio::spawn(async move { + cancellation.cancelled().await; + let _ = worker_cancelled.send(()); + worker_release.notified().await; + }); + state.video_slot.install(&launch, worker).await; + let teardown = tokio::spawn({ + let state = std::sync::Arc::clone(&state); + async move { state.teardown().await } + }); + cancelled.await.expect("worker observes cancellation"); + assert!(!teardown.is_finished()); + + release.notify_one(); + + teardown.await.expect("session teardown joins"); + assert!( + state + .video_slot + .current_event("peer".to_string(), super::VideoPhase::Terminal, None) + .await + .is_none() + ); + } + + #[tokio::test] + async fn slot_resolves_crossed_offers_and_expires_only_waiting_sender() { + let slot = super::VideoSlot::default(); + let descriptor = VideoMediaDescriptor::display(VideoCodec::H264, 1920, 1080); + let local_offer = slot + .start_local(descriptor) + .await + .expect("local offer reserves slot"); + let remote_id = VideoSessionId::new(); + + assert_eq!(slot.start_local(descriptor).await, None); + let remote_launch = slot + .receive(VideoControl::offer(remote_id, descriptor), false) + .await + .launch() + .expect("winning remote offer starts receiver"); + assert_eq!(remote_launch.role(), super::VideoRole::Receiver); + assert_eq!(slot.expire_waiting_ready().await, None); + assert!(matches!( + slot.receive( + VideoControl::stop( + local_offer.session_id(), + super::VideoTerminalReason::Stopped + ), + false + ) + .await, + super::VideoSlotEffect::Ignored + )); + let terminal = slot + .receive( + VideoControl::stop(remote_id, super::VideoTerminalReason::Stopped), + false, + ) + .await; + assert!(matches!( + terminal, + super::VideoSlotEffect::Terminal(_, super::VideoTerminalReason::Stopped) + )); + slot.cancel_and_join(remote_launch.attempt(), super::VideoTerminalReason::Stopped) + .await; + + let offer = slot + .start_local(descriptor) + .await + .expect("second local offer reserves slot"); + let expired = slot + .expire_waiting_ready() + .await + .expect("waiting offer expires"); + assert_eq!(expired.1, super::VideoTerminalReason::Failed); + slot.cancel_and_join(expired.0, expired.1).await; + assert!(matches!( + slot.receive(VideoControl::ready(offer.session_id()), true) + .await, + super::VideoSlotEffect::Ignored + )); + } +} diff --git a/rust/telepathy-core/src/internal/video/platform.rs b/rust/telepathy-core/src/internal/video/platform.rs new file mode 100644 index 00000000..f73679f0 --- /dev/null +++ b/rust/telepathy-core/src/internal/video/platform.rs @@ -0,0 +1,366 @@ +use crate::internal::error::Error; +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +use crate::internal::video::VideoCodec; +use crate::types::{Capabilities, VideoCapabilities}; +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +use bytes::Bytes; +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +use futures_util::{SinkExt, StreamExt}; +use speedy::{Readable, Writable}; +use std::fmt::Display; +use std::str::FromStr; +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +#[path = "platform/desktop_ffmpeg.rs"] +mod selected; +#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] +#[path = "platform/unsupported.rs"] +mod selected; +pub(crate) use selected::{prepare_sender, probe_capabilities, run_receiver, run_sender}; + +type Result = std::result::Result; +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +const BUFFER_SIZE: usize = 512; + +#[derive(Clone, Debug, PartialEq, Eq, Readable, Writable)] +pub(crate) enum Device { + DirectShow, + GdiGrab, + DesktopDuplication, + AVFoundation(Vec), + X11Grab, +} + +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +impl Device { + #[cfg(target_os = "windows")] + fn devices() -> Vec { + vec![Self::DesktopDuplication, Self::GdiGrab] + } + + #[cfg(not(target_os = "windows"))] + fn devices() -> Vec { + Vec::new() + } +} + +impl Display for Device { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::DirectShow => formatter.write_str("DirectShow"), + Self::GdiGrab => formatter.write_str("GDI Grab"), + Self::DesktopDuplication => formatter.write_str("Desktop Duplication"), + Self::AVFoundation(devices) => write!(formatter, "AVFoundation: {devices:?}"), + Self::X11Grab => formatter.write_str("X11 Grab"), + } + } +} + +impl FromStr for Device { + type Err = (); + + fn from_str(value: &str) -> std::result::Result { + match value { + "DirectShow" => Ok(Self::DirectShow), + "GDI Grab" => Ok(Self::GdiGrab), + "Desktop Duplication" => Ok(Self::DesktopDuplication), + "X11 Grab" => Ok(Self::X11Grab), + _ => Err(()), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Readable, Writable)] +pub(crate) enum Encoder { + Libx264, + H264Nvenc, + H264Amf, + H264Qsv, + H264Vaapi, + Libx265, + HevcNvenc, + HevcAmf, + HevcQsv, + HevcVaapi, + Av1Nvenc, + Av1Amf, + Av1Qsv, + Av1Vaapi, +} + +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +impl Encoder { + const fn codec(self) -> VideoCodec { + match self { + Self::Libx264 | Self::H264Nvenc | Self::H264Amf | Self::H264Qsv | Self::H264Vaapi => { + VideoCodec::H264 + } + Self::Libx265 | Self::HevcNvenc | Self::HevcAmf | Self::HevcQsv | Self::HevcVaapi => { + VideoCodec::Hevc + } + Self::Av1Nvenc | Self::Av1Amf | Self::Av1Qsv | Self::Av1Vaapi => VideoCodec::Av1, + } + } +} + +impl From for &'static str { + fn from(encoder: Encoder) -> Self { + match encoder { + Encoder::Libx264 => "libx264", + Encoder::H264Nvenc => "h264_nvenc", + Encoder::H264Amf => "h264_amf", + Encoder::H264Qsv => "h264_qsv", + Encoder::H264Vaapi => "h264_vaapi", + Encoder::Libx265 => "libx265", + Encoder::HevcNvenc => "hevc_nvenc", + Encoder::HevcAmf => "hevc_amf", + Encoder::HevcQsv => "hevc_qsv", + Encoder::HevcVaapi => "hevc_vaapi", + Encoder::Av1Nvenc => "av1_nvenc", + Encoder::Av1Amf => "av1_amf", + Encoder::Av1Qsv => "av1_qsv", + Encoder::Av1Vaapi => "av1_vaapi", + } + } +} + +impl Display for Encoder { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str((*self).into()) + } +} + +impl FromStr for Encoder { + type Err = (); + + fn from_str(value: &str) -> std::result::Result { + match value { + "libx264" => Ok(Self::Libx264), + "h264_nvenc" => Ok(Self::H264Nvenc), + "h264_amf" => Ok(Self::H264Amf), + "h264_qsv" => Ok(Self::H264Qsv), + "h264_vaapi" => Ok(Self::H264Vaapi), + "libx265" => Ok(Self::Libx265), + "hevc_nvenc" => Ok(Self::HevcNvenc), + "hevc_amf" => Ok(Self::HevcAmf), + "hevc_qsv" => Ok(Self::HevcQsv), + "hevc_vaapi" => Ok(Self::HevcVaapi), + "av1_nvenc" => Ok(Self::Av1Nvenc), + "av1_amf" => Ok(Self::Av1Amf), + "av1_qsv" => Ok(Self::Av1Qsv), + "av1_vaapi" => Ok(Self::Av1Vaapi), + _ => Err(()), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) enum Decoder { + H264, + H264Cuvid, + H264Qsv, + Hevc, + HevcCuvid, + HevcQsv, + Av1Cuvid, + Av1Qsv, +} + +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +impl Decoder { + const fn codec(self) -> VideoCodec { + match self { + Self::H264 | Self::H264Cuvid | Self::H264Qsv => VideoCodec::H264, + Self::Hevc | Self::HevcCuvid | Self::HevcQsv => VideoCodec::Hevc, + Self::Av1Cuvid | Self::Av1Qsv => VideoCodec::Av1, + } + } +} + +impl From for &'static str { + fn from(decoder: Decoder) -> Self { + match decoder { + Decoder::H264 => "h264", + Decoder::H264Cuvid => "h264_cuvid", + Decoder::H264Qsv => "h264_qsv", + Decoder::Hevc => "hevc", + Decoder::HevcCuvid => "hevc_cuvid", + Decoder::HevcQsv => "hevc_qsv", + Decoder::Av1Cuvid => "av1_cuvid", + Decoder::Av1Qsv => "av1_qsv", + } + } +} + +impl FromStr for Decoder { + type Err = (); + + fn from_str(value: &str) -> std::result::Result { + match value { + "h264" => Ok(Self::H264), + "h264_cuvid" => Ok(Self::H264Cuvid), + "h264_qsv" => Ok(Self::H264Qsv), + "hevc" => Ok(Self::Hevc), + "hevc_cuvid" => Ok(Self::HevcCuvid), + "hevc_qsv" => Ok(Self::HevcQsv), + "av1_cuvid" => Ok(Self::Av1Cuvid), + "av1_qsv" => Ok(Self::Av1Qsv), + _ => Err(()), + } + } +} + +pub(crate) struct CapabilityProbe { + compatibility: Capabilities, + video: VideoCapabilities, +} + +impl CapabilityProbe { + pub(crate) const fn new(compatibility: Capabilities, video: VideoCapabilities) -> Self { + Self { + compatibility, + video, + } + } + + pub(crate) fn into_parts(self) -> (Capabilities, VideoCapabilities) { + (self.compatibility, self.video) + } +} + +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +pub(crate) fn encoder_from_str(value: &str) -> std::result::Result { + selected::encoder_from_str(value) +} + +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +pub async fn forward_capture_chunks(stdout: &mut R, transport: &mut S) +where + R: AsyncRead + Unpin, + S: futures_util::Sink + Unpin, + S::Error: std::fmt::Display, +{ + let mut frame = [0_u8; BUFFER_SIZE]; + while let Ok(read) = stdout.read(&mut frame).await { + if read == 0 { + break; + } + if transport + .send(Bytes::copy_from_slice(&frame[..read])) + .await + .is_err() + { + break; + } + } +} + +#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] +pub async fn forward_playback_frames(transport: &mut S, stdin: &mut W) +where + S: futures_util::Stream> + Unpin, + W: AsyncWrite + Unpin, +{ + while let Some(Ok(message)) = transport.next().await { + if stdin.write_all(&message).await.is_err() { + break; + } + } +} + +#[cfg(test)] +mod tests { + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + use super::{Device, Encoder}; + use crate::internal::video::{VideoCodec, VideoMediaFormat, VideoSource}; + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + use crate::types::RecordingConfig; + use crate::types::{ + VideoCapabilities, VideoCapabilityAvailability, VideoSourceCapability, VideoUnavailable, + }; + + #[test] + fn capabilities_represent_send_only_source_formats_without_boolean_ambiguity() { + let format = VideoMediaFormat::MpegTs(VideoCodec::H264); + let capabilities = VideoCapabilities { + send: VideoCapabilityAvailability::Available, + receive: VideoCapabilityAvailability::Unavailable(VideoUnavailable::RuntimeUnavailable), + send_sources: vec![VideoSourceCapability { + source: VideoSource::Display, + formats: vec![format], + }], + receive_formats: Vec::new(), + }; + + assert_eq!( + capabilities.formats(VideoSource::Display), + Ok(&[format][..]) + ); + assert_eq!( + capabilities.receive, + VideoCapabilityAvailability::Unavailable(VideoUnavailable::RuntimeUnavailable) + ); + } + + #[test] + fn unsupported_capabilities_use_the_same_typed_shape() { + let capabilities = VideoCapabilities { + send: VideoCapabilityAvailability::Unavailable(VideoUnavailable::PlatformUnsupported), + receive: VideoCapabilityAvailability::Unavailable( + VideoUnavailable::PlatformUnsupported, + ), + send_sources: Vec::new(), + receive_formats: Vec::new(), + }; + + assert_eq!( + capabilities.formats(VideoSource::Display), + Err(VideoUnavailable::PlatformUnsupported) + ); + assert_eq!( + capabilities.receive, + VideoCapabilityAvailability::Unavailable(VideoUnavailable::PlatformUnsupported) + ); + } + + #[test] + fn available_empty_capabilities_remain_distinct_from_unavailable() { + let capabilities = VideoCapabilities { + send: VideoCapabilityAvailability::Available, + receive: VideoCapabilityAvailability::Available, + send_sources: Vec::new(), + receive_formats: Vec::new(), + }; + + assert_eq!( + capabilities.formats(VideoSource::Display), + Err(VideoUnavailable::SourceUnavailable(VideoSource::Display)) + ); + assert_eq!(capabilities.receive, VideoCapabilityAvailability::Available); + } + + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + #[tokio::test] + async fn unsupported_adapter_query_and_start_report_typed_unavailable() { + let (compatibility, capabilities) = + super::selected::probe_capabilities().await.into_parts(); + let config = RecordingConfig { + encoder: Encoder::H264Nvenc, + device: Device::X11Grab, + bitrate: 4_000_000, + framerate: 60, + height: Some(720), + }; + + assert_eq!( + capabilities.formats(VideoSource::Display), + Err(VideoUnavailable::PlatformUnsupported) + ); + assert_eq!( + super::selected::prepare_sender(&config, 1_280, 720, &compatibility, &capabilities,), + Err(VideoUnavailable::PlatformUnsupported) + ); + } +} diff --git a/rust/telepathy-core/src/internal/video/platform/desktop_ffmpeg.rs b/rust/telepathy-core/src/internal/video/platform/desktop_ffmpeg.rs new file mode 100644 index 00000000..4a8f80f7 --- /dev/null +++ b/rust/telepathy-core/src/internal/video/platform/desktop_ffmpeg.rs @@ -0,0 +1,681 @@ +// allow: SIZE_OK - target-specific FFmpeg configuration and process adapter must compile together. +use super::{CapabilityProbe, Decoder, Device, Encoder, Result}; +use crate::internal::video::{ + VideoMediaDescriptor, VideoMediaFormat, VideoSource, VideoWorkerStartup, +}; +use crate::types::{ + Capabilities, RecordingConfig, VideoCapabilities, VideoCapabilityAvailability, + VideoSourceCapability, VideoUnavailable, +}; +use bytes::Bytes; +use regex::Regex; +use std::process::Stdio; +use std::process::{ExitStatus, Output}; +use std::str::FromStr; +use tokio::process::Command; +use tokio::select; +use tokio_util::sync::CancellationToken; +use tracing::{info, instrument}; + +use crate::internal::error::{Error, ErrorKind}; + +#[cfg(target_os = "windows")] +const CREATION_FLAGS: u32 = 0x08000000; + +pub(crate) async fn probe_capabilities() -> CapabilityProbe { + let codec_regex = Regex::new("V....[D.] ([^= ]+)\\s+(.+)").unwrap(); + + let mut command = Command::new("ffmpeg"); + command.arg("-hide_banner").arg("-encoders"); + + #[cfg(target_os = "windows")] + { + command.creation_flags(CREATION_FLAGS); + } + + let encoders_result = command.output().await; + + let mut command = Command::new("ffplay"); + command.arg("-hide_banner").arg("-decoders"); + + #[cfg(target_os = "windows")] + { + command.creation_flags(CREATION_FLAGS); + } + + let decoders_result = command.output().await; + + let encoders = encoders_result.ok().map(|output| { + parse_codecs(output, &codec_regex) + .into_iter() + .filter_map(|codec| Encoder::from_str(&codec).ok()) + .collect::>() + }); + let decoders = decoders_result.ok().map(|output| { + parse_codecs(output, &codec_regex) + .into_iter() + .filter_map(|codec| Decoder::from_str(&codec).ok()) + .collect::>() + }); + let devices = Device::devices(); + let video = video_capabilities(encoders.as_deref(), decoders.as_deref(), devices.as_slice()); + let compatibility = Capabilities { + _available: encoders.is_some() && decoders.is_some(), + encoders: encoders.unwrap_or_default(), + _decoders: decoders.unwrap_or_default(), + devices, + }; + CapabilityProbe::new(compatibility, video) +} + +fn video_capabilities( + encoders: Option<&[Encoder]>, + decoders: Option<&[Decoder]>, + devices: &[Device], +) -> VideoCapabilities { + let (send, send_sources) = match encoders { + Some(encoders) => { + let mut formats = Vec::new(); + for encoder in encoders { + let format = VideoMediaFormat::MpegTs(encoder.codec()); + if !formats.contains(&format) { + formats.push(format); + } + } + let sources = if devices.is_empty() || formats.is_empty() { + Vec::new() + } else { + vec![VideoSourceCapability { + source: VideoSource::Display, + formats, + }] + }; + (VideoCapabilityAvailability::Available, sources) + } + None => ( + VideoCapabilityAvailability::Unavailable(VideoUnavailable::RuntimeUnavailable), + Vec::new(), + ), + }; + let (receive, receive_formats) = match decoders { + Some(decoders) => { + let mut formats = Vec::new(); + for decoder in decoders { + let format = VideoMediaFormat::MpegTs(decoder.codec()); + if !formats.contains(&format) { + formats.push(format); + } + } + (VideoCapabilityAvailability::Available, formats) + } + None => ( + VideoCapabilityAvailability::Unavailable(VideoUnavailable::RuntimeUnavailable), + Vec::new(), + ), + }; + VideoCapabilities { + send, + receive, + send_sources, + receive_formats, + } +} + +impl Device { + fn to_args(&self, encoder: Encoder) -> std::result::Result, ErrorKind> { + let arguments = match self { + Self::DesktopDuplication => match encoder { + Encoder::H264Nvenc | Encoder::H264Qsv => vec![ + "-init_hw_device", + "d3d11va", + "-filter_complex", + "ddagrab=video_size=1920x1080", + ], + Encoder::HevcNvenc | Encoder::Av1Nvenc => { + vec!["-init_hw_device", "d3d11va", "-filter_complex", "ddagrab=0"] + } + _ => vec![ + "-init_hw_device", + "d3d11va", + "-filter_complex", + "ddagrab=0,hwdownload,format=bgra", + ], + }, + Self::GdiGrab => match encoder { + Encoder::H264Nvenc | Encoder::H264Qsv => vec![ + "-f", + "gdigrab", + "-framerate", + "30", + "-video_size", + "1920x1080", + "-i", + "desktop", + ], + _ => vec!["-f", "gdigrab", "-framerate", "30", "-i", "desktop"], + }, + _ => return Err(ErrorKind::PlatformUnavailable), + }; + Ok(arguments) + } +} + +pub(crate) fn encoder_from_str(value: &str) -> std::result::Result { + Encoder::from_str(value) +} + +pub(crate) fn prepare_sender( + config: &RecordingConfig, + width: u32, + height: u32, + capabilities: &Capabilities, + video_capabilities: &VideoCapabilities, +) -> std::result::Result { + prepare_sender_from_capabilities(config, width, height, capabilities, video_capabilities) +} + +fn prepare_sender_from_capabilities( + config: &RecordingConfig, + width: u32, + height: u32, + capabilities: &Capabilities, + generic: &VideoCapabilities, +) -> std::result::Result { + let available = generic.formats(VideoSource::Display)?; + if !capabilities.encoders.contains(&config.encoder) + || !capabilities.devices.contains(&config.device) + { + return Err(VideoUnavailable::ConfigurationUnavailable); + } + + let descriptor = VideoMediaDescriptor::display(config.encoder.codec(), width, height); + let format = VideoMediaFormat::MpegTs(config.encoder.codec()); + if available.contains(&format) { + Ok(descriptor) + } else { + Err(VideoUnavailable::FormatUnavailable(format)) + } +} + +impl RecordingConfig { + fn make_command(&self, test: bool) -> Result { + let mut command = Command::new("ffmpeg"); + command.args(self.device.to_args(self.encoder)?); + + // sets the video size if specified + if let Some(height) = self.height { + command.arg("-vf"); + command.arg(format!("trunc(oh*a/2)*2:{}", height)); + } + + if test { + command.arg("-frames:v"); + command.arg("1"); + } + + command.args([ + "-c:v", + self.encoder.into(), + "-delay", + "0", + "-b:v", + self.bitrate.to_string().as_str(), + "-bufsize", + "1M", + "-f", + "mpegts", + "-", + ]); + + Ok(command) + } + + pub(crate) async fn test_config(&self) -> Result { + let mut command = self.make_command(true)?; + command + .stdin(Stdio::null()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + + #[cfg(target_os = "windows")] + { + command.creation_flags(CREATION_FLAGS); + } + + let mut child = command.spawn()?; + child.wait().await.map_err(Into::into) + } +} + +struct PlaybackConfig { + decoder: Decoder, +} + +impl PlaybackConfig { + fn make_command(&self) -> Command { + let mut command = Command::new("ffplay"); + + command.args(["-vcodec", self.decoder.into(), "-f", "mpegts", "-i", "-"]); + + command + } +} + +fn make_playback_command( + descriptor: VideoMediaDescriptor, + decoders: &[Decoder], +) -> std::result::Result { + let config = PlaybackConfig { + decoder: select_decoder(decoders, descriptor)?, + }; + let mut command = config.make_command(); + command.args([ + "-x", + &descriptor.dimensions().0.to_string(), + "-y", + &descriptor.dimensions().1.to_string(), + "-flags", + "low_delay", + "-analyzeduration", + "1", + "-window_title", + "Telepathy Screenshare", + ]); + Ok(command) +} + +fn select_decoder( + decoders: &[Decoder], + descriptor: VideoMediaDescriptor, +) -> std::result::Result { + decoders + .iter() + .copied() + .find(|decoder| decoder.codec() == descriptor.codec()) + .ok_or(ErrorKind::NoEncoderAvailable) +} + +#[instrument(name = "screenshare.record", skip_all)] +pub(crate) async fn run_sender( + transport: &mut S, + stop: &CancellationToken, + config: RecordingConfig, + startup: tokio::sync::oneshot::Sender, +) -> Result<()> +where + S: futures_util::Sink + Unpin, + S::Error: std::fmt::Display, +{ + info!(event = "screenshare_record_start", ?config); + + let startup_result: Result<_> = (|| { + let mut command = config.make_command(false)?; + command.stdout(Stdio::piped()).stderr(Stdio::null()); + + #[cfg(target_os = "windows")] + command.creation_flags(CREATION_FLAGS); + + let mut child = command.spawn()?; + let stdout = child + .stdout + .take() + .ok_or_else(|| Error::from(ErrorKind::PlatformUnavailable))?; + Ok((child, stdout)) + })(); + let (mut child, mut stdout) = match startup_result { + Ok(state) => { + let _ = startup.send(VideoWorkerStartup::Ready); + state + } + Err(error) => { + let _ = startup.send(VideoWorkerStartup::Failed); + return Err(error); + } + }; + + let future = super::forward_capture_chunks(&mut stdout, transport); + + select! { + _ = future => { + info!("Recording finished"); + } + _ = stop.cancelled() => { + info!("Recording stopped"); + } + } + + drop(stdout); + terminate_and_reap(&mut child).await; + Ok(()) +} + +#[instrument(name = "screenshare.playback", skip_all)] +pub(crate) async fn run_receiver( + transport: &mut S, + stop: &CancellationToken, + descriptor: VideoMediaDescriptor, + startup: tokio::sync::oneshot::Sender, +) -> Result<()> +where + S: futures_util::Stream> + Unpin, +{ + info!("Starting screen playback"); + let (capabilities, _) = probe_capabilities().await.into_parts(); + let startup_result: Result<_> = (|| { + let mut command = make_playback_command(descriptor, &capabilities._decoders)?; + command + .stdin(Stdio::piped()) + .stdout(Stdio::null()) + .stderr(Stdio::null()); + #[cfg(target_os = "windows")] + command.creation_flags(CREATION_FLAGS); + let mut child = command.spawn()?; + let stdin = child + .stdin + .take() + .ok_or_else(|| Error::from(ErrorKind::PlatformUnavailable))?; + Ok((child, stdin)) + })(); + let (mut child, mut stdin) = match startup_result { + Ok(state) => { + let _ = startup.send(VideoWorkerStartup::Ready); + state + } + Err(error) => { + let _ = startup.send(VideoWorkerStartup::Failed); + return Err(error); + } + }; + + let future = super::forward_playback_frames(transport, &mut stdin); + + select! { + _ = future => { + info!("Playback finished"); + } + _ = stop.cancelled() => { + info!("Playback stopped"); + } + } + + drop(stdin); + terminate_and_reap(&mut child).await; + Ok(()) +} + +async fn terminate_and_reap(child: &mut tokio::process::Child) { + if tokio::time::timeout(std::time::Duration::from_secs(1), child.wait()) + .await + .is_ok() + { + return; + } + let _ = child.kill().await; + let _ = child.wait().await; +} + +fn parse_codecs(output: Output, regex: &Regex) -> Vec { + let output_str = String::from_utf8_lossy(&output.stdout); + + regex + .captures_iter(&output_str) + .filter_map(|cap| cap.get(1)) + .map(|cap| cap.as_str().to_string()) + .collect() +} + +#[cfg(test)] +mod tests { + use super::{ + Device, Encoder, make_playback_command, prepare_sender_from_capabilities, select_decoder, + }; + use crate::internal::error::{Error, ErrorKind}; + use crate::internal::video::platform::Decoder; + use crate::internal::video::{VideoCodec, VideoMediaDescriptor}; + use crate::types::{ + Capabilities, RecordingConfig, VideoCapabilityAvailability, VideoUnavailable, + }; + + fn recording_config() -> RecordingConfig { + RecordingConfig { + encoder: Encoder::H264Nvenc, + device: Device::X11Grab, + bitrate: 4_000_000, + framerate: 60, + height: Some(720), + } + } + + fn command_parts(command: &tokio::process::Command) -> (String, Vec) { + let command = command.as_std(); + ( + command.get_program().to_string_lossy().into_owned(), + command + .get_args() + .map(|argument| argument.to_string_lossy().into_owned()) + .collect(), + ) + } + + #[test] + fn sender_command_preserves_current_ffmpeg_arguments() { + let config = RecordingConfig { + encoder: Encoder::H264Nvenc, + device: Device::GdiGrab, + bitrate: 4_000_000, + framerate: 60, + height: Some(720), + }; + let command = config.make_command(false).unwrap(); + let (program, arguments) = command_parts(&command); + + assert_eq!(program, "ffmpeg"); + assert_eq!( + arguments, + [ + "-f", + "gdigrab", + "-framerate", + "30", + "-video_size", + "1920x1080", + "-i", + "desktop", + "-vf", + "trunc(oh*a/2)*2:720", + "-c:v", + "h264_nvenc", + "-delay", + "0", + "-b:v", + "4000000", + "-bufsize", + "1M", + "-f", + "mpegts", + "-", + ] + ); + } + + #[test] + fn receiver_command_preserves_current_ffplay_arguments() { + let descriptor = VideoMediaDescriptor::display(VideoCodec::H264, 1280, 720); + let command = make_playback_command( + descriptor, + &[Decoder::H264Cuvid, Decoder::H264Qsv, Decoder::H264], + ) + .unwrap(); + let (program, arguments) = command_parts(&command); + + assert_eq!(program, "ffplay"); + assert_eq!( + arguments, + [ + "-vcodec", + "h264_cuvid", + "-f", + "mpegts", + "-i", + "-", + "-x", + "1280", + "-y", + "720", + "-flags", + "low_delay", + "-analyzeduration", + "1", + "-window_title", + "Telepathy Screenshare", + ] + ); + } + + #[test] + fn implemented_devices_preserve_command_arguments() { + assert_eq!( + Device::DesktopDuplication + .to_args(Encoder::H264Nvenc) + .unwrap(), + [ + "-init_hw_device", + "d3d11va", + "-filter_complex", + "ddagrab=video_size=1920x1080", + ] + ); + assert_eq!( + Device::GdiGrab.to_args(Encoder::H264Nvenc).unwrap(), + [ + "-f", + "gdigrab", + "-framerate", + "30", + "-video_size", + "1920x1080", + "-i", + "desktop", + ] + ); + assert!( + Device::devices() + .iter() + .all(|device| device.to_args(Encoder::Libx264).is_ok()) + ); + #[cfg(target_os = "windows")] + assert_eq!( + Device::devices(), + [Device::DesktopDuplication, Device::GdiGrab] + ); + #[cfg(not(target_os = "windows"))] + assert!(Device::devices().is_empty()); + } + + #[test] + fn unimplemented_device_returns_typed_error_without_panicking() { + assert!(matches!( + recording_config().make_command(false), + Err(Error { + kind: ErrorKind::PlatformUnavailable + }) + )); + } + + #[test] + fn decoder_selection_uses_first_compatible_local_decoder() { + let descriptor = VideoMediaDescriptor::display(VideoCodec::H264, 1_280, 720); + let decoders = [Decoder::Hevc, Decoder::H264Qsv, Decoder::H264]; + + assert!(matches!( + select_decoder(&decoders, descriptor), + Ok(Decoder::H264Qsv) + )); + } + + #[test] + fn decoder_selection_fails_when_local_probe_has_no_compatible_decoder() { + let descriptor = VideoMediaDescriptor::display(VideoCodec::Av1, 1_280, 720); + + assert!(matches!( + select_decoder(&[Decoder::H264, Decoder::Hevc], descriptor), + Err(ErrorKind::NoEncoderAvailable) + )); + } + + #[test] + fn sender_start_rejects_encoder_removed_after_preflight() { + let capabilities = Capabilities { + _available: true, + encoders: vec![Encoder::Libx264], + _decoders: Vec::new(), + devices: vec![Device::X11Grab], + }; + + let generic = super::video_capabilities( + Some(&capabilities.encoders), + Some(&capabilities._decoders), + &capabilities.devices, + ); + let result = prepare_sender_from_capabilities( + &recording_config(), + 1_280, + 720, + &capabilities, + &generic, + ); + + assert_eq!(result, Err(VideoUnavailable::ConfigurationUnavailable)); + } + + #[test] + fn sender_start_rejects_device_removed_after_preflight() { + let capabilities = Capabilities { + _available: true, + encoders: vec![Encoder::H264Nvenc], + _decoders: Vec::new(), + devices: vec![Device::GdiGrab], + }; + + let generic = super::video_capabilities( + Some(&capabilities.encoders), + Some(&capabilities._decoders), + &capabilities.devices, + ); + let result = prepare_sender_from_capabilities( + &recording_config(), + 1_280, + 720, + &capabilities, + &generic, + ); + + assert_eq!(result, Err(VideoUnavailable::ConfigurationUnavailable)); + } + + #[test] + fn sender_start_uses_directional_capability_when_receiver_is_unavailable() { + let capabilities = Capabilities { + _available: false, + encoders: vec![Encoder::H264Nvenc], + _decoders: Vec::new(), + devices: vec![Device::X11Grab], + }; + let generic = + super::video_capabilities(Some(&capabilities.encoders), None, &capabilities.devices); + + let result = prepare_sender_from_capabilities( + &recording_config(), + 1_280, + 720, + &capabilities, + &generic, + ); + + assert!(result.is_ok()); + assert_eq!( + generic.receive, + VideoCapabilityAvailability::Unavailable(VideoUnavailable::RuntimeUnavailable) + ); + } +} diff --git a/rust/telepathy-core/src/internal/video/platform/unsupported.rs b/rust/telepathy-core/src/internal/video/platform/unsupported.rs new file mode 100644 index 00000000..7f2b05bd --- /dev/null +++ b/rust/telepathy-core/src/internal/video/platform/unsupported.rs @@ -0,0 +1,58 @@ +use super::CapabilityProbe; +use super::Result; +use crate::internal::error::ErrorKind; +use crate::internal::video::{VideoMediaDescriptor, VideoWorkerStartup}; +use crate::types::{ + Capabilities, RecordingConfig, VideoCapabilities, VideoCapabilityAvailability, VideoUnavailable, +}; +use bytes::Bytes; +use std::fmt::Display; +use tokio_util::sync::CancellationToken; + +pub(crate) async fn probe_capabilities() -> CapabilityProbe { + CapabilityProbe::new( + Capabilities::default(), + VideoCapabilities { + send: VideoCapabilityAvailability::Unavailable(VideoUnavailable::PlatformUnsupported), + receive: VideoCapabilityAvailability::Unavailable( + VideoUnavailable::PlatformUnsupported, + ), + send_sources: Vec::new(), + receive_formats: Vec::new(), + }, + ) +} +pub(crate) fn prepare_sender( + _: &RecordingConfig, + _: u32, + _: u32, + _: &Capabilities, + _: &VideoCapabilities, +) -> std::result::Result { + Err(VideoUnavailable::PlatformUnsupported) +} +pub(crate) async fn run_sender( + _: &mut S, + _: &CancellationToken, + _: RecordingConfig, + startup: tokio::sync::oneshot::Sender, +) -> Result<()> +where + S: futures_util::Sink + Unpin, + S::Error: Display, +{ + let _ = startup.send(VideoWorkerStartup::Failed); + Err(ErrorKind::PlatformUnavailable.into()) +} +pub(crate) async fn run_receiver( + _: &mut S, + _: &CancellationToken, + _: VideoMediaDescriptor, + startup: tokio::sync::oneshot::Sender, +) -> Result<()> +where + S: futures_util::Stream> + Unpin, +{ + let _ = startup.send(VideoWorkerStartup::Failed); + Err(ErrorKind::PlatformUnavailable.into()) +} diff --git a/rust/telepathy-core/src/internal/video/transport.rs b/rust/telepathy-core/src/internal/video/transport.rs new file mode 100644 index 00000000..cb508d26 --- /dev/null +++ b/rust/telepathy-core/src/internal/video/transport.rs @@ -0,0 +1,401 @@ +use crate::internal::error::{Error as CoreError, ErrorKind as CoreErrorKind}; +use crate::internal::video::platform; +use crate::internal::video::{ + VIDEO_MEDIA_MAX_FRAME_LENGTH, VIDEO_NEGOTIATION_TIMEOUT, VIDEO_PREAMBLE_MAX_LENGTH, + VideoPreamble, VideoProtocolError, VideoWorkerStartup, decode_preamble, encode_preamble, +}; +use crate::types::RecordingConfig; +use iroh::endpoint::{Connection, VarInt}; +use std::io::{Error, ErrorKind, Result}; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use tokio::sync::oneshot; +use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; +use tokio_util::sync::CancellationToken; + +fn protocol_error(error: VideoProtocolError) -> Error { + Error::new( + ErrorKind::InvalidData, + format!("invalid video transport data: {error:?}"), + ) +} + +pub async fn write_preamble(writer: &mut W, preamble: VideoPreamble) -> Result<()> +where + W: AsyncWrite + Unpin, +{ + let bytes = encode_preamble(&preamble).map_err(protocol_error)?; + let length = u16::try_from(bytes.len()) + .map_err(|_| Error::new(ErrorKind::InvalidInput, "video preamble exceeds u16 length"))?; + writer.write_u16(length).await?; + writer.write_all(&bytes).await +} + +pub async fn read_preamble(reader: &mut R) -> Result +where + R: AsyncRead + Unpin, +{ + let length = usize::from(reader.read_u16().await?); + if length > VIDEO_PREAMBLE_MAX_LENGTH { + return Err(Error::new( + ErrorKind::InvalidData, + "video preamble exceeds limit", + )); + } + let mut bytes = vec![0; length]; + reader.read_exact(&mut bytes).await?; + decode_preamble(&bytes).map_err(protocol_error) +} + +pub(crate) async fn read_preamble_until_cancelled( + reader: &mut R, + cancellation: &CancellationToken, +) -> Result +where + R: AsyncRead + Unpin, +{ + tokio::select! { + biased; + _ = cancellation.cancelled() => Err(Error::new(ErrorKind::Interrupted, "video transport cancelled")), + preamble = read_preamble(reader) => preamble, + } +} + +pub(crate) async fn run_sender( + connection: &Connection, + preamble: VideoPreamble, + config: RecordingConfig, + cancellation: &CancellationToken, + startup: oneshot::Sender, +) -> std::result::Result<(), CoreError> { + let mut stream = tokio::select! { + biased; + _ = cancellation.cancelled() => { + let _ = startup.send(VideoWorkerStartup::Failed); + return Ok(()); + }, + _ = tokio::time::sleep(VIDEO_NEGOTIATION_TIMEOUT) => { + let _ = startup.send(VideoWorkerStartup::Failed); + return Err(CoreErrorKind::TransportSend.into()); + }, + stream = connection.open_uni() => match stream { + Ok(stream) => stream, + Err(_) => { + let _ = startup.send(VideoWorkerStartup::Failed); + return Err(CoreErrorKind::TransportSend.into()); + } + }, + }; + let preamble_result = tokio::select! { + biased; + _ = cancellation.cancelled() => { + let _ = stream.reset(VarInt::from_u32(1)); + let _ = startup.send(VideoWorkerStartup::Failed); + return Ok(()); + }, + result = write_preamble(&mut stream, preamble) => result, + }; + if let Err(error) = preamble_result { + let _ = stream.reset(VarInt::from_u32(1)); + let _ = startup.send(VideoWorkerStartup::Failed); + return Err(error.into()); + } + let codec = LengthDelimitedCodec::builder() + .max_frame_length(VIDEO_MEDIA_MAX_FRAME_LENGTH) + .new_codec(); + let mut framed = FramedWrite::new(stream, codec); + let result = platform::run_sender(&mut framed, cancellation, config, startup).await; + let mut stream = framed.into_inner(); + if result.is_ok() || cancellation.is_cancelled() { + let _ = stream.finish(); + } else { + let _ = stream.reset(VarInt::from_u32(1)); + } + result +} + +pub(crate) async fn run_receiver( + connection: &Connection, + expected: VideoPreamble, + cancellation: &CancellationToken, + startup: oneshot::Sender, +) -> std::result::Result<(), CoreError> { + let mut stream = tokio::select! { + biased; + _ = cancellation.cancelled() => { + let _ = startup.send(VideoWorkerStartup::Failed); + return Ok(()); + }, + _ = tokio::time::sleep(VIDEO_NEGOTIATION_TIMEOUT) => { + let _ = startup.send(VideoWorkerStartup::Failed); + return Err(CoreErrorKind::TransportRecv.into()); + }, + stream = connection.accept_uni() => match stream { + Ok(stream) => stream, + Err(_) => { + let _ = startup.send(VideoWorkerStartup::Failed); + return Err(CoreErrorKind::TransportRecv.into()); + } + }, + }; + let preamble = match read_preamble_until_cancelled(&mut stream, cancellation).await { + Ok(preamble) => preamble, + Err(error) => { + let _ = stream.stop(VarInt::from_u32(1)); + let _ = startup.send(VideoWorkerStartup::Failed); + return Err(error.into()); + } + }; + if preamble != expected { + let _ = stream.stop(VarInt::from_u32(1)); + let _ = startup.send(VideoWorkerStartup::Failed); + return Err(CoreErrorKind::TransportRecv.into()); + } + let codec = LengthDelimitedCodec::builder() + .max_frame_length(VIDEO_MEDIA_MAX_FRAME_LENGTH) + .new_codec(); + let mut framed = FramedRead::new(stream, codec); + let result = + platform::run_receiver(&mut framed, cancellation, expected.descriptor, startup).await; + let mut stream = framed.into_inner(); + if result.is_err() && !cancellation.is_cancelled() { + let _ = stream.stop(VarInt::from_u32(1)); + } + result +} + +#[cfg(test)] +mod tests { + use super::{read_preamble, read_preamble_until_cancelled, run_receiver, write_preamble}; + use crate::internal::ALPN; + use crate::internal::video::{ + VIDEO_MEDIA_MAX_FRAME_LENGTH, VideoCodec, VideoControl, VideoMediaDescriptor, VideoPhase, + VideoPreamble, VideoSessionId, VideoSlot, VideoTerminalReason, VideoWorkerStartup, + }; + use bytes::Bytes; + use futures_util::{SinkExt, StreamExt}; + use iroh::endpoint::Connection; + use tokio::io::duplex; + use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; + use tokio_util::sync::CancellationToken; + + async fn iroh_pair() -> (iroh::Endpoint, iroh::Endpoint, Connection, Connection) { + use iroh::endpoint::presets; + + let server = iroh::Endpoint::builder(presets::N0) + .relay_mode(iroh::RelayMode::Disabled) + .alpns(vec![ALPN.to_vec()]) + .bind() + .await + .expect("server endpoint binds"); + let client = iroh::Endpoint::builder(presets::N0) + .relay_mode(iroh::RelayMode::Disabled) + .bind() + .await + .expect("client endpoint binds"); + let server_addr = server.addr(); + let (outbound, inbound) = tokio::join!(client.connect(server_addr, ALPN), async { + server + .accept() + .await + .expect("server receives connection") + .await + }); + ( + client, + server, + outbound.expect("client connects"), + inbound.expect("server accepts"), + ) + } + + #[tokio::test] + async fn preamble_round_trips_before_media() { + let (mut sender, mut receiver) = duplex(1024); + let preamble = VideoPreamble::new( + VideoSessionId::new(), + VideoMediaDescriptor::display(VideoCodec::H264, 1280, 720), + ); + + let send = tokio::spawn(async move { write_preamble(&mut sender, preamble).await }); + let received = read_preamble(&mut receiver).await; + + assert_eq!(received.expect("preamble is received"), preamble); + assert!(send.await.expect("writer joins").is_ok()); + } + + #[tokio::test] + async fn cancelled_preamble_read_returns_without_waiting_for_peer() { + let (_sender, mut receiver) = duplex(1024); + let cancel = tokio_util::sync::CancellationToken::new(); + cancel.cancel(); + + let result = read_preamble_until_cancelled(&mut receiver, &cancel).await; + + assert_eq!( + result.expect_err("cancelled read fails").kind(), + std::io::ErrorKind::Interrupted + ); + } + + #[tokio::test] + async fn partial_preamble_returns_unexpected_eof() { + let (mut sender, mut receiver) = duplex(1024); + tokio::io::AsyncWriteExt::write_all(&mut sender, &[0, 4, 1]) + .await + .expect("prefix writes"); + drop(sender); + + let result = read_preamble(&mut receiver).await; + + assert_eq!( + result.expect_err("partial preamble fails").kind(), + std::io::ErrorKind::UnexpectedEof + ); + } + + #[tokio::test(flavor = "multi_thread")] + async fn real_iroh_peers_exchange_preamble_and_bounded_media_frame() { + let (client, server, outbound, inbound) = iroh_pair().await; + let preamble = VideoPreamble::new( + VideoSessionId::new(), + VideoMediaDescriptor::display(VideoCodec::H264, 1280, 720), + ); + let payload = vec![0x5A; VIDEO_MEDIA_MAX_FRAME_LENGTH]; + let expected = payload.clone(); + + let sender_connection = outbound.clone(); + let sender = tokio::spawn(async move { + let mut stream = sender_connection + .open_uni() + .await + .expect("uni stream opens"); + write_preamble(&mut stream, preamble) + .await + .expect("preamble writes"); + let codec = LengthDelimitedCodec::builder() + .max_frame_length(VIDEO_MEDIA_MAX_FRAME_LENGTH) + .new_codec(); + let mut framed = FramedWrite::new(stream, codec); + framed + .send(Bytes::from(payload)) + .await + .expect("media frame writes"); + framed.into_inner().finish().expect("stream finishes"); + }); + let mut stream = inbound.accept_uni().await.expect("uni stream accepted"); + assert_eq!( + read_preamble(&mut stream).await.expect("preamble reads"), + preamble + ); + let codec = LengthDelimitedCodec::builder() + .max_frame_length(VIDEO_MEDIA_MAX_FRAME_LENGTH) + .new_codec(); + let mut framed = FramedRead::new(stream, codec); + assert_eq!( + framed + .next() + .await + .expect("media frame arrives") + .expect("media frame reads") + .as_ref(), + expected.as_slice() + ); + sender.await.expect("sender joins"); + client.close().await; + server.close().await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn receiver_reports_failed_startup_when_preamble_does_not_match() { + let (client, server, outbound, inbound) = iroh_pair().await; + let expected = VideoPreamble::new( + VideoSessionId::new(), + VideoMediaDescriptor::display(VideoCodec::H264, 1280, 720), + ); + let mismatched = VideoPreamble::new( + VideoSessionId::new(), + VideoMediaDescriptor::display(VideoCodec::H264, 1280, 720), + ); + let cancellation = CancellationToken::new(); + let (startup_sender, startup_receiver) = tokio::sync::oneshot::channel(); + let receiver = tokio::spawn(async move { + run_receiver(&inbound, expected, &cancellation, startup_sender).await + }); + let mut stream = outbound.open_uni().await.expect("uni stream opens"); + write_preamble(&mut stream, mismatched) + .await + .expect("mismatched preamble writes"); + + assert_eq!( + startup_receiver.await.expect("startup result is reported"), + VideoWorkerStartup::Failed + ); + assert!(receiver.await.expect("receiver worker joins").is_err()); + client.close().await; + server.close().await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn real_iroh_accept_wait_is_cancelled_and_joined() { + let (client, server, _outbound, inbound) = iroh_pair().await; + let cancellation = CancellationToken::new(); + let worker_cancellation = cancellation.clone(); + let worker = tokio::spawn(async move { + tokio::select! { + biased; + _ = worker_cancellation.cancelled() => true, + _ = inbound.accept_uni() => false, + } + }); + + cancellation.cancel(); + + assert!( + worker + .await + .expect("accept worker joins after cancellation") + ); + client.close().await; + server.close().await; + } + + #[tokio::test(flavor = "multi_thread")] + async fn slot_becomes_idle_only_after_real_iroh_accept_worker_joins() { + use std::sync::Arc; + use std::sync::atomic::{AtomicBool, Ordering}; + + let (client, server, _outbound, inbound) = iroh_pair().await; + let slot = Arc::new(VideoSlot::default()); + let descriptor = VideoMediaDescriptor::display(VideoCodec::H264, 1280, 720); + let session_id = VideoSessionId::new(); + let launch = slot + .receive(VideoControl::offer(session_id, descriptor), true) + .await + .launch() + .expect("accepted offer arms receiver"); + let joined = Arc::new(AtomicBool::new(false)); + let worker_joined = Arc::clone(&joined); + let cancellation = launch.cancellation().clone(); + let worker = tokio::spawn(async move { + tokio::select! { + biased; + _ = cancellation.cancelled() => {} + _ = inbound.accept_uni() => {} + } + worker_joined.store(true, Ordering::Relaxed); + }); + assert!(slot.install(&launch, worker).await); + + slot.cancel_and_join(launch.attempt(), VideoTerminalReason::Teardown) + .await; + + assert!(joined.load(Ordering::Relaxed)); + assert!( + slot.current_event("peer".to_string(), VideoPhase::Terminal, None,) + .await + .is_none() + ); + client.close().await; + server.close().await; + } +} diff --git a/rust/telepathy-core/src/native.rs b/rust/telepathy-core/src/native.rs index c8d26650..864ca15f 100644 --- a/rust/telepathy-core/src/native.rs +++ b/rust/telepathy-core/src/native.rs @@ -3,12 +3,15 @@ use crate::internal::TelepathyHandle; use crate::internal::callbacks::{CoreCallbacks, CoreStatisticsCallback}; use crate::internal::{JoinHandle, spawn_task}; use crate::types::{ - CallState, ChatMessage, Contact, FrontendNotify, ManagerState, SessionStatus, Statistics, + CallState, ChatMessage, CodecConfig, Contact, IDENTITY_KEY_LENGTH_MESSAGE, ManagerState, + NetworkConfig, ScreenshareConfig, SessionStatus, Statistics, VideoCapabilities, + VideoLifecycleEvent, VideoSessionIdentity, VideoSource, VideoStartOutcome, VideoStopOutcome, }; use iroh::PublicKey; use std::future::Future; use std::pin::Pin; use std::sync::Arc; +use telepathy_audio::devices::AudioHost; #[cfg(not(feature = "integration-testing"))] use telepathy_audio::devices::CpalAudioHost; #[cfg(feature = "integration-testing")] @@ -23,35 +26,58 @@ type NativeAcceptCall = Arc< >; #[cfg(not(feature = "integration-testing"))] -type NativeHandle = TelepathyHandle; +type DefaultNativeHost = CpalAudioHost; #[cfg(feature = "integration-testing")] -type NativeHandle = TelepathyHandle< - NativeCallbacks, - MockAudioHost< - telepathy_audio::devices::MockAudioInput, - telepathy_audio::devices::MockAudioOutput, - >, +type DefaultNativeHost = MockAudioHost< + telepathy_audio::devices::MockAudioInput, + telepathy_audio::devices::MockAudioOutput, >; /// Rust-native runtime client for `telepathy-core`. /// /// This mirrors the Flutter-facing API but accepts [`NativeCallbacks`] and does /// not depend on FRB runtime semantics. -pub struct NativeTelepathy { - handle: NativeHandle, +pub struct NativeTelepathy +where + H: AudioHost + Send + Sync + Clone + 'static, +{ + handle: TelepathyHandle, } -impl NativeTelepathy { +impl NativeTelepathy { pub fn new( - network_config: &crate::types::NetworkConfig, - codec_config: &crate::types::CodecConfig, + network_config: &NetworkConfig, + video_config: &ScreenshareConfig, + codec_config: &CodecConfig, + callbacks: NativeCallbacks, + ) -> Self { + Self::with_host( + Default::default(), + network_config, + video_config, + codec_config, + callbacks, + ) + } +} + +impl NativeTelepathy +where + H: AudioHost + Send + Sync + Clone + 'static, +{ + /// Builds a client around a caller-provided audio host (e.g. a mock host for headless tests). + pub fn with_host( + audio_host: H, + network_config: &NetworkConfig, + video_config: &ScreenshareConfig, + codec_config: &CodecConfig, callbacks: NativeCallbacks, ) -> Self { Self { handle: TelepathyHandle::new( - Default::default(), + audio_host, network_config, - &Default::default(), + video_config, &Default::default(), codec_config, callbacks, @@ -106,7 +132,7 @@ impl NativeTelepathy { .set_identity( &(key .try_into() - .map_err(|_| crate::types::IDENTITY_KEY_LENGTH_MESSAGE.to_string())?), + .map_err(|_| IDENTITY_KEY_LENGTH_MESSAGE.to_string())?), ) .await .map_err(|e| e.to_string()) @@ -136,8 +162,20 @@ impl NativeTelepathy { .map_err(|e| e.to_string()) } - pub async fn start_screenshare(&self, contact: &Contact) { - self.handle.start_screenshare(contact).await; + pub async fn request_video_source( + &self, + contact: &Contact, + source: VideoSource, + ) -> VideoStartOutcome { + self.handle.request_video_source(contact, source).await + } + + pub async fn stop_video_source(&self, identity: VideoSessionIdentity) -> VideoStopOutcome { + self.handle.stop_video_source(identity).await + } + + pub async fn video_capabilities(&self) -> VideoCapabilities { + self.handle.video_capabilities().await } pub fn set_rms_threshold(&self, decimal: f32) { @@ -242,7 +280,7 @@ pub struct NativeCallbacks { statistics: NativeVoid, message_received: NativeVoid, manager_active: NativeVoid, - screenshare_started: NativeVoid<(FrontendNotify, bool)>, + video_lifecycle: NativeVoid, } impl NativeCallbacks { @@ -259,7 +297,7 @@ impl NativeCallbacks { statistics: impl Fn(Statistics) -> NativeFuture<()> + Send + Sync + 'static, message_received: impl Fn(ChatMessage) -> NativeFuture<()> + Send + Sync + 'static, manager_active: impl Fn(ManagerState) -> NativeFuture<()> + Send + Sync + 'static, - screenshare_started: impl Fn((FrontendNotify, bool)) -> NativeFuture<()> + Send + Sync + 'static, + video_lifecycle: impl Fn(VideoLifecycleEvent) -> NativeFuture<()> + Send + Sync + 'static, ) -> Self { Self { accept_call: Arc::new(accept_call), @@ -270,7 +308,7 @@ impl NativeCallbacks { statistics: Arc::new(statistics), message_received: Arc::new(message_received), manager_active: Arc::new(manager_active), - screenshare_started: Arc::new(screenshare_started), + video_lifecycle: Arc::new(video_lifecycle), } } } @@ -294,8 +332,8 @@ impl CoreCallbacks for NativeCallbacks { (self.manager_active)(state).await } - async fn screenshare_started(&self, stop: FrontendNotify, sender: bool) { - (self.screenshare_started)((stop, sender)).await + async fn video_lifecycle(&self, event: VideoLifecycleEvent) { + (self.video_lifecycle)(event).await } async fn get_contact(&self, peer_id: Vec) -> Option { diff --git a/rust/telepathy-core/src/player.rs b/rust/telepathy-core/src/player.rs index 926294e9..c01cc157 100644 --- a/rust/telepathy-core/src/player.rs +++ b/rust/telepathy-core/src/player.rs @@ -4,6 +4,7 @@ //! wrapping the framework-agnostic `telepathy-audio::AudioPlayer` with //! Flutter Rust Bridge attributes for Dart interop. +use crate::internal::MAX_RINGTONE_LENGTH; use crate::types::DartError; #[cfg(not(target_family = "wasm"))] use std::path::Path; @@ -156,7 +157,7 @@ pub async fn load_ringtone(path: String) -> Result<(), DartError> { let sea_bytes = wav_to_sea(wav_bytes, 5_f32) .await .map_err(|error| error.to_string())?; - if sea_bytes.len() > crate::internal::MAX_RINGTONE_LENGTH { + if sea_bytes.len() > MAX_RINGTONE_LENGTH { return Err("Encoded ringtone is too large".to_string().into()); } File::create("ringtone.sea") diff --git a/rust/telepathy-core/src/types.rs b/rust/telepathy-core/src/types.rs index 173d3059..f1e93722 100644 --- a/rust/telepathy-core/src/types.rs +++ b/rust/telepathy-core/src/types.rs @@ -1,10 +1,9 @@ use crate::internal::error::{Error, ErrorKind}; use crate::internal::messages::Attachment; #[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] -use crate::internal::screenshare::encoder_from_str; -use crate::internal::screenshare::{Decoder, Device, Encoder, ScreenshareConfigDisk}; -#[cfg(any(target_os = "windows", target_os = "macos", target_os = "linux"))] use crate::internal::spawn_task; +use crate::internal::video::VideoMediaDescriptor; +use crate::internal::video::platform::{self, Decoder, Device, Encoder}; use atomic_float::AtomicF32; use chrono::{DateTime, Local, SecondsFormat, Utc}; use iroh::RelayMap; @@ -230,6 +229,7 @@ pub struct FrontendNotify { } impl FrontendNotify { + #[cfg(feature = "flutter")] pub(crate) fn new(inner: &Arc) -> Self { Self { inner: inner.clone(), @@ -237,6 +237,139 @@ impl FrontendNotify { } } +#[derive(Readable, Writable, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)] +pub struct VideoSessionId(pub [u8; 16]); + +impl VideoSessionId { + pub(crate) fn new() -> Self { + Self(Uuid::new_v4().into_bytes()) + } +} + +#[derive(Readable, Writable, Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum VideoSource { + Display, +} + +#[derive(Readable, Writable, Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum VideoRole { + Sender, + Receiver, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum VideoPhase { + Offering, + WaitingReady, + Starting, + Active, + Stopping, + Terminal, +} + +#[derive(Readable, Writable, Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum VideoTerminalReason { + Stopped, + Rejected, + Failed, + TransportEnded, + Teardown, +} + +#[derive(Readable, Writable, Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum VideoCodec { + H264, + Hevc, + Av1, +} + +#[derive(Readable, Writable, Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum VideoMediaFormat { + MpegTs(VideoCodec), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum VideoUnavailable { + PlatformUnsupported, + RuntimeUnavailable, + SourceUnavailable(VideoSource), + FormatUnavailable(VideoMediaFormat), + ConfigurationUnavailable, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct VideoSessionIdentity { + pub peer_id: String, + pub session_id: VideoSessionId, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum VideoStartOutcome { + Requested(VideoSessionIdentity), + Unavailable(VideoUnavailable), + NoSession, + AlreadyActive, + Failed(VideoTerminalReason), +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum VideoStopOutcome { + Stopped, + NotFound, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct VideoLifecycleEvent { + pub identity: VideoSessionIdentity, + pub role: VideoRole, + pub source: VideoSource, + pub phase: VideoPhase, + pub terminal_reason: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +pub enum VideoCapabilityAvailability { + Available, + Unavailable(VideoUnavailable), +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct VideoSourceCapability { + pub source: VideoSource, + pub formats: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct VideoCapabilities { + pub send: VideoCapabilityAvailability, + pub receive: VideoCapabilityAvailability, + pub send_sources: Vec, + pub receive_formats: Vec, +} + +impl VideoCapabilities { + #[cfg_attr( + not(any(target_os = "windows", target_os = "macos", target_os = "linux")), + expect( + dead_code, + reason = "only used by the desktop ffmpeg backend and tests" + ) + )] + pub(crate) fn formats( + &self, + source: VideoSource, + ) -> std::result::Result<&[VideoMediaFormat], VideoUnavailable> { + if let VideoCapabilityAvailability::Unavailable(reason) = self.send { + return Err(reason); + } + self.send_sources + .iter() + .find(|capability| capability.source == source) + .map(|capability| capability.formats.as_slice()) + .ok_or(VideoUnavailable::SourceUnavailable(source)) + } +} + impl FrontendNotify { /// public notified function for dart pub async fn notified(&self) { @@ -525,6 +658,23 @@ pub struct ScreenshareConfig { pub(crate) height: Arc, } +#[derive(Readable, Writable)] +pub(crate) struct ScreenshareConfigDisk { + pub(crate) recording_config: Option, + pub(crate) width: u32, + pub(crate) height: u32, +} + +impl From<&ScreenshareConfig> for ScreenshareConfigDisk { + fn from(config: &ScreenshareConfig) -> Self { + Self { + recording_config: config.recording_config.blocking_read().clone(), + width: config.width.load(Relaxed), + height: config.height.load(Relaxed), + } + } +} + impl Default for ScreenshareConfig { fn default() -> Self { Self { @@ -545,22 +695,60 @@ impl ScreenshareConfig { let capabilities_clone = Arc::clone(&config.capabilities); spawn_task(async move { - let c = Capabilities::new().await; - *capabilities_clone.write().await = c; + let (compatibility, _) = platform::probe_capabilities().await.into_parts(); + *capabilities_clone.write().await = compatibility; }); config } #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] - pub async fn new(_buffer: Vec) -> Self { - Self::default() + pub async fn new(buffer: Vec) -> Self { + ScreenshareConfigDisk::read_from_buffer(&buffer) + .map(ScreenshareConfig::from) + .unwrap_or_default() } pub async fn capabilities(&self) -> Capabilities { self.capabilities.read().await.clone() } + pub async fn video_capabilities(&self) -> VideoCapabilities { + self.probe_video_capabilities().await + } + + async fn probe_video_capabilities(&self) -> VideoCapabilities { + let (compatibility, capabilities) = platform::probe_capabilities().await.into_parts(); + *self.capabilities.write().await = compatibility; + capabilities + } + + pub(crate) async fn prepare_video_sender( + &self, + source: VideoSource, + ) -> std::result::Result<(RecordingConfig, VideoMediaDescriptor), VideoUnavailable> { + let config = self + .recording_config + .read() + .await + .clone() + .ok_or(VideoUnavailable::ConfigurationUnavailable)?; + match source { + VideoSource::Display => { + let capabilities = self.probe_video_capabilities().await; + let compatibility = self.capabilities.read().await; + let descriptor = platform::prepare_sender( + &config, + self.width.load(Relaxed), + self.height.load(Relaxed), + &compatibility, + &capabilities, + )?; + Ok((config, descriptor)) + } + } + } + pub async fn recording_config(&self) -> Option { self.recording_config.read().await.clone() } @@ -574,7 +762,8 @@ impl ScreenshareConfig { framerate: u32, height: Option, ) -> Result<(), DartError> { - let encoder = encoder_from_str(&encoder).map_err(|_| ErrorKind::InvalidEncoder)?; + let encoder = + platform::encoder_from_str(&encoder).map_err(|_| ErrorKind::InvalidEncoder)?; let recording_config = RecordingConfig { encoder, @@ -603,7 +792,7 @@ impl ScreenshareConfig { _framerate: u32, _height: Option, ) -> Result<(), DartError> { - Ok(()) + Err(ErrorKind::PlatformUnavailable.into()) } #[cfg_attr(feature = "flutter", flutter_rust_bridge::frb(sync))] @@ -893,7 +1082,11 @@ fn poison_field_error( #[cfg(test)] mod tests { - use super::{NetworkConfig, NetworkConfigField}; + use super::{ + Device, Encoder, NetworkConfig, NetworkConfigField, RecordingConfig, Relaxed, + ScreenshareConfig, ScreenshareConfigDisk, + }; + use speedy::{Readable, Writable}; const VALID_RELAY_A: &str = "https://relay-us.iroh.example/"; const VALID_RELAY_B: &str = "https://relay-eu.iroh.example/"; @@ -901,6 +1094,60 @@ mod tests { const VALID_DNS_ENDPOINT: &str = "1.1.1.1:53"; const VALID_ORIGIN_DOMAIN: &str = "dns.iroh.example"; + #[test] + fn screenshare_disk_bytes_preserve_desktop_recording_values() { + let disk = ScreenshareConfigDisk { + recording_config: Some(RecordingConfig { + encoder: Encoder::H264Nvenc, + device: Device::X11Grab, + bitrate: 4_000_000, + framerate: 60, + height: Some(720), + }), + width: 1_280, + height: 720, + }; + + let bytes = disk.write_to_vec().expect("serialize legacy settings"); + + assert_eq!( + bytes, + [ + 1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 9, 61, 0, 60, 0, 0, 0, 1, 208, 2, 0, 0, 0, 5, 0, 0, + 208, 2, 0, 0, + ] + ); + } + + #[test] + fn old_screenshare_bytes_load_and_roundtrip_exactly() { + let old_bytes = [ + 1, 1, 0, 0, 0, 4, 0, 0, 0, 0, 9, 61, 0, 60, 0, 0, 0, 1, 208, 2, 0, 0, 0, 5, 0, 0, 208, + 2, 0, 0, + ]; + + let disk = ScreenshareConfigDisk::read_from_buffer(&old_bytes) + .expect("load persisted screenshare settings"); + let config = ScreenshareConfig::from(disk); + + assert_eq!( + config.to_bytes().expect("roundtrip screenshare settings"), + old_bytes + ); + let recording = config + .recording_config + .blocking_read() + .clone() + .expect("persisted recording config"); + assert_eq!(recording.encoder(), "h264_nvenc"); + assert_eq!(recording.device(), "X11 Grab"); + assert_eq!(recording.bitrate(), 4_000_000); + assert_eq!(recording.framerate(), 60); + assert_eq!(recording.height(), Some(720)); + assert_eq!(config.width.load(Relaxed), 1_280); + assert_eq!(config.height.load(Relaxed), 720); + } + fn vec_of(items: &[&str]) -> Vec { items.iter().map(|s| s.to_string()).collect() } diff --git a/rust/telepathy-core/tests/core_integration_test.rs b/rust/telepathy-core/tests/core_integration_test.rs index 3db5275b..5141c8e2 100644 --- a/rust/telepathy-core/tests/core_integration_test.rs +++ b/rust/telepathy-core/tests/core_integration_test.rs @@ -18,3 +18,5 @@ mod room_lifecycle; mod runtime_readiness; #[path = "core_integration_test/session_lifecycle.rs"] mod session_lifecycle; +#[path = "core_integration_test/video_sessions.rs"] +mod video_sessions; diff --git a/rust/telepathy-core/tests/core_integration_test/common.rs b/rust/telepathy-core/tests/core_integration_test/common.rs index 816153ee..5671f3ed 100644 --- a/rust/telepathy-core/tests/core_integration_test/common.rs +++ b/rust/telepathy-core/tests/core_integration_test/common.rs @@ -39,6 +39,52 @@ pub(super) const MOCK_DEVICE_ID: &str = "mock"; pub(super) const STALE_INPUT_DEVICE_ID: &str = "stale-input"; pub(super) const STALE_OUTPUT_DEVICE_ID: &str = "stale-output"; +#[derive(Clone, Default)] +pub(super) struct ProcessBoundaryProbe { + started: Arc, + reaped: Arc, +} + +pub(super) struct ProcessBoundaryObservation { + pub(super) stdout: Vec, + pub(super) status: std::process::ExitStatus, +} + +impl ProcessBoundaryProbe { + pub(super) async fn spawn_pipe_exit_and_reap( + &self, + ) -> std::io::Result { + use tokio::io::AsyncReadExt; + + let executable = std::env::current_exe()?; + let mut child = tokio::process::Command::new(executable) + .arg("--help") + .stdout(std::process::Stdio::piped()) + .spawn()?; + self.started.fetch_add(1, Relaxed); + + let mut stdout = Vec::new(); + child + .stdout + .take() + .ok_or_else(|| std::io::Error::other("test process stdout was not piped"))? + .read_to_end(&mut stdout) + .await?; + let status = child.wait().await?; + self.reaped.fetch_add(1, Relaxed); + + Ok(ProcessBoundaryObservation { stdout, status }) + } + + pub(super) fn started(&self) -> usize { + self.started.load(Relaxed) + } + + pub(super) fn reaped(&self) -> usize { + self.reaped.load(Relaxed) + } +} + pub(super) type MockTelepathyHandle = TelepathyHandle; pub(super) struct ClientHarness @@ -1468,6 +1514,10 @@ fn construct_mock_callbacks_with_contact_lookup( }) }); + mock.expect_video_lifecycle() + .times(..) + .returning(|_| Box::pin(async move {})); + if let Some(probe) = accept_probe { mock.expect_get_accept_handle() .returning(move |_, _, cancel| { diff --git a/rust/telepathy-core/tests/core_integration_test/video_sessions.rs b/rust/telepathy-core/tests/core_integration_test/video_sessions.rs new file mode 100644 index 00000000..5fe770fc --- /dev/null +++ b/rust/telepathy-core/tests/core_integration_test/video_sessions.rs @@ -0,0 +1,277 @@ +use super::common::{ + DEFAULT_SAMPLE_RATE, ManagerLifecycle, ProcessBoundaryProbe, TwoClientShutdownGuard, + build_client_with_options, init_test_tracing, shared_relay_map, wait_for_connected, + wait_for_sessions, +}; +use bytes::BytesMut; +use futures_util::stream; +use std::io; +use std::pin::Pin; +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, Mutex}; +use std::task::{Context, Poll}; +use std::time::Duration; +use telepathy_audio::devices::{MockAudioHost, MockAudioInput, MockAudioOutput}; +use telepathy_core::internal::state::CallSlotState; +use telepathy_core::internal::video::platform::{forward_capture_chunks, forward_playback_frames}; +use telepathy_core::types::{ + CallState, CodecConfig, Contact, VideoSource, VideoStartOutcome, VideoUnavailable, +}; +use tokio::io::{AsyncWrite, AsyncWriteExt}; +use tokio::sync::Notify; +use tokio::time::timeout; +use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; + +#[path = "video_sessions/lifecycle.rs"] +mod lifecycle; +#[path = "video_sessions/protocol.rs"] +mod protocol; + +#[tokio::test] +async fn capture_preserves_512_byte_chunk_boundaries_in_length_frames() { + let source = [7_u8; 513]; + let (mut source_writer, mut source_reader) = tokio::io::duplex(1024); + source_writer + .write_all(&source) + .await + .expect("write source"); + drop(source_writer); + + let (frame_writer, frame_reader) = tokio::io::duplex(2048); + let mut transport = FramedWrite::new(frame_writer, LengthDelimitedCodec::new()); + forward_capture_chunks(&mut source_reader, &mut transport).await; + drop(transport); + + let mut frames = FramedRead::new(frame_reader, LengthDelimitedCodec::new()); + let first = futures_util::StreamExt::next(&mut frames) + .await + .expect("first frame") + .expect("first frame decode"); + let second = futures_util::StreamExt::next(&mut frames) + .await + .expect("second frame") + .expect("second frame decode"); + assert_eq!(first, &source[..512]); + assert_eq!(second, &source[512..]); +} + +struct PartialWriter(Vec); + +impl AsyncWrite for PartialWriter { + fn poll_write( + mut self: Pin<&mut Self>, + _context: &mut Context<'_>, + source: &[u8], + ) -> Poll> { + let written = source.len().min(2); + self.0.extend_from_slice(&source[..written]); + Poll::Ready(Ok(written)) + } + + fn poll_flush(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _context: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } +} + +#[tokio::test] +async fn receiver_writes_each_framed_payload_completely() { + let payload = BytesMut::from(&b"complete frame payload"[..]); + let mut frames = stream::iter([Ok::<_, io::Error>(payload.clone())]); + let mut writer = PartialWriter(Vec::new()); + + forward_playback_frames(&mut frames, &mut writer).await; + + assert_eq!(writer.0, payload); +} + +#[tokio::test] +async fn process_probe_spawns_pipes_exits_and_reaps_once_without_ffmpeg() { + let probe = ProcessBoundaryProbe::default(); + + let observation = timeout(Duration::from_secs(5), probe.spawn_pipe_exit_and_reap()) + .await + .expect("current test executable must finish its help process promptly") + .expect("current test executable must spawn with a piped stdout"); + + assert!(observation.status.success()); + assert!(!observation.stdout.is_empty()); + assert_eq!(probe.started(), 1); + assert_eq!(probe.reaped(), 1); +} + +#[tokio::test] +async fn process_spawn_failure_is_reported_without_waiting_for_ffmpeg() { + let missing_program = std::env::temp_dir().join(format!( + "telepathy-missing-screenshare-process-{}", + std::process::id() + )); + + let error = tokio::process::Command::new(missing_program) + .spawn() + .expect_err("a missing process must fail during spawn"); + + assert_eq!(error.kind(), io::ErrorKind::NotFound); +} + +#[tokio::test] +async fn playback_stops_on_stream_reset_without_writing_a_partial_frame() { + let mut frames = stream::iter([Err::(io::Error::new( + io::ErrorKind::ConnectionReset, + "simulated media stream reset", + ))]); + let mut writer = PartialWriter(Vec::new()); + + forward_playback_frames(&mut frames, &mut writer).await; + + assert!(writer.0.is_empty()); +} + +#[tokio::test] +async fn blocked_capture_stops_when_current_record_select_observes_stop() { + let (mut source_writer, mut source_reader) = tokio::io::duplex(128); + let source = tokio::spawn(async move { + let _ = source_writer.write_all(&vec![9_u8; 64 * 1024]).await; + }); + + let (frame_writer, _frame_reader) = tokio::io::duplex(128); + let mut transport = FramedWrite::new(frame_writer, LengthDelimitedCodec::new()); + let stop = Arc::new(Notify::new()); + let transfer_stop = Arc::clone(&stop); + let transfer = tokio::spawn(async move { + tokio::select! { + _ = forward_capture_chunks(&mut source_reader, &mut transport) => false, + _ = transfer_stop.notified() => true, + } + }); + let mut transfer = Box::pin(transfer); + + timeout(Duration::from_millis(100), &mut transfer) + .await + .expect_err("capture must remain blocked while its framed receiver is not reading"); + stop.notify_waiters(); + assert!( + timeout(Duration::from_secs(1), transfer) + .await + .expect("blocked capture must observe stop promptly") + .expect("capture task must not panic") + ); + timeout(Duration::from_secs(1), source) + .await + .expect("capture source must close after stop") + .expect("capture source task must not panic"); +} + +#[tokio::test(flavor = "multi_thread")] +async fn idle_session_rejects_video_before_active_call_uses_existing_behavior() { + init_test_tracing(); + let relay_map = shared_relay_map(); + let codec_config = CodecConfig::new(true, true, 5.0); + let key_a = iroh::SecretKey::generate(); + let key_b = iroh::SecretKey::generate(); + let contact_a = Contact::new("video-client-a".to_string(), key_a.public().to_string()) + .expect("contact a must be valid"); + let contact_b = Contact::new("video-client-b".to_string(), key_b.public().to_string()) + .expect("contact b must be valid"); + let call_states_a = Arc::new(Mutex::new(Vec::new())); + let call_states_b = Arc::new(Mutex::new(Vec::new())); + let client_a = build_client_with_options( + relay_map, + key_a, + vec![contact_b.clone()], + &codec_config, + MockAudioHost::new( + MockAudioInput::default(), + DEFAULT_SAMPLE_RATE, + MockAudioOutput, + DEFAULT_SAMPLE_RATE, + ), + Arc::clone(&call_states_a), + None, + ManagerLifecycle::Restartable, + ) + .await; + let client_b = build_client_with_options( + relay_map, + key_b, + vec![contact_a.clone()], + &codec_config, + MockAudioHost::new( + MockAudioInput::default(), + DEFAULT_SAMPLE_RATE, + MockAudioOutput, + DEFAULT_SAMPLE_RATE, + ), + Arc::clone(&call_states_b), + None, + ManagerLifecycle::Restartable, + ) + .await; + let shutdown_guard = TwoClientShutdownGuard { + a: &client_a, + b: &client_b, + dropped: AtomicBool::new(false), + }; + + client_a.telepathy.start_session(&contact_b).await; + client_b.telepathy.start_session(&contact_a).await; + wait_for_sessions(&client_a, &contact_b, &client_b, &contact_a).await; + + for _ in 0..2 { + assert_eq!( + client_a + .telepathy + .request_video_source(&contact_b, VideoSource::Display) + .await, + VideoStartOutcome::NoSession + ); + } + assert_eq!( + client_a.telepathy.inner.core_state.call_slot.current(), + CallSlotState::Idle + ); + + client_a + .telepathy + .start_call(&contact_b) + .await + .expect("caller must start the real two-peer call"); + wait_for_connected(&call_states_a, "video sender").await; + wait_for_connected(&call_states_b, "video receiver").await; + + let outcome = client_a + .telepathy + .request_video_source(&contact_b, VideoSource::Display) + .await; + + assert_eq!( + outcome, + VideoStartOutcome::Unavailable(VideoUnavailable::ConfigurationUnavailable) + ); + + assert!( + !call_states_a + .lock() + .unwrap() + .iter() + .any(|state| matches!(state, CallState::CallEnded(_, _))) + ); + + timeout(Duration::from_secs(15), client_a.telepathy.end_call()) + .await + .expect("call teardown must not hang after a no-config screenshare request"); + client_a.stop_session_and_wait_for_runtime(&contact_b).await; + client_b.stop_session_and_wait_for_runtime(&contact_a).await; + assert_eq!( + client_a.telepathy.inner.core_state.call_slot.current(), + CallSlotState::Idle + ); + + shutdown_guard.disarm(); + drop(shutdown_guard); + client_a.telepathy.shutdown().await; + client_b.telepathy.shutdown().await; +} diff --git a/rust/telepathy-core/tests/core_integration_test/video_sessions/lifecycle.rs b/rust/telepathy-core/tests/core_integration_test/video_sessions/lifecycle.rs new file mode 100644 index 00000000..28eff664 --- /dev/null +++ b/rust/telepathy-core/tests/core_integration_test/video_sessions/lifecycle.rs @@ -0,0 +1,436 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use bytes::Bytes; +use futures_util::{SinkExt, StreamExt}; +use iroh::endpoint::{Connection, presets}; +use telepathy_core::internal::video::transport::{read_preamble, write_preamble}; +use telepathy_core::internal::video::{ + VideoControl, VideoMediaDescriptor, VideoPreamble, VideoRejectReason, VideoSlot, + VideoSlotEffect, VideoWorkerStartup, +}; +use telepathy_core::types::{ + VideoCodec, VideoMediaFormat, VideoPhase, VideoRole, VideoTerminalReason, +}; +use tokio::time::timeout; +use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; + +pub(super) struct IrohPair { + pub(super) client: iroh::Endpoint, + pub(super) server: iroh::Endpoint, + pub(super) outbound: Connection, + pub(super) inbound: Connection, +} + +impl IrohPair { + pub(super) async fn connect() -> Self { + let server = iroh::Endpoint::builder(presets::N0) + .relay_mode(iroh::RelayMode::Disabled) + .alpns(vec![b"telepathy/session/1".to_vec()]) + .bind() + .await + .expect("server endpoint binds"); + let client = iroh::Endpoint::builder(presets::N0) + .relay_mode(iroh::RelayMode::Disabled) + .bind() + .await + .expect("client endpoint binds"); + let server_addr = server.addr(); + let (outbound, inbound) = + tokio::join!(client.connect(server_addr, b"telepathy/session/1"), async { + server + .accept() + .await + .expect("server receives connection") + .await + }); + Self { + client, + server, + outbound: outbound.expect("client connects"), + inbound: inbound.expect("server accepts"), + } + } + + pub(super) async fn close(self) { + self.client.close().await; + self.server.close().await; + } +} + +#[tokio::test] +async fn incoming_offer_admission_rejects_incompatible_format_before_reserving_receiver() { + let slot = VideoSlot::default(); + let incompatible_session_id = VideoSlot::default() + .start_local(VideoMediaDescriptor::display(VideoCodec::Hevc, 1280, 720)) + .await + .expect("test session starts") + .session_id(); + let incompatible = VideoControl::offer( + incompatible_session_id, + VideoMediaDescriptor::display(VideoCodec::Hevc, 1280, 720), + ); + let VideoControl::Offer(incompatible) = incompatible else { + unreachable!(); + }; + + assert!(matches!( + slot.receive_offer( + incompatible, + true, + &[VideoMediaFormat::MpegTs(VideoCodec::H264)] + ) + .await, + VideoSlotEffect::Send(VideoControl::Reject { + reason: VideoRejectReason::UnsupportedCodec, + .. + }) + )); + assert!( + slot.current_event("peer".to_string(), VideoPhase::Terminal, None) + .await + .is_none() + ); + + let compatible_session_id = VideoSlot::default() + .start_local(VideoMediaDescriptor::display(VideoCodec::H264, 1280, 720)) + .await + .expect("test session starts") + .session_id(); + let compatible = VideoControl::offer( + compatible_session_id, + VideoMediaDescriptor::display(VideoCodec::H264, 1280, 720), + ); + let VideoControl::Offer(compatible) = compatible else { + unreachable!(); + }; + assert!(matches!( + slot.receive_offer( + compatible, + true, + &[VideoMediaFormat::MpegTs(VideoCodec::H264)] + ) + .await, + VideoSlotEffect::SendAndLaunch(_, _) + )); + assert!( + slot.current_event("peer".to_string(), VideoPhase::Starting, None) + .await + .is_some() + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn two_real_peers_negotiate_activate_stop_and_restart_from_both_sides() { + let pair = IrohPair::connect().await; + let descriptor = VideoMediaDescriptor::display(VideoCodec::H264, 1280, 720); + let slot_a = Arc::new(VideoSlot::default()); + let slot_b = Arc::new(VideoSlot::default()); + + for sender_is_a in [true, false] { + let (sender_slot, receiver_slot, sender_connection, receiver_connection) = if sender_is_a { + (&slot_a, &slot_b, &pair.outbound, &pair.inbound) + } else { + (&slot_b, &slot_a, &pair.inbound, &pair.outbound) + }; + let offer = sender_slot + .start_local(descriptor) + .await + .expect("idle sender reserves a generation"); + assert!(sender_slot.start_local(descriptor).await.is_none()); + let (ready, receiver_launch) = match receiver_slot.receive(offer, true).await { + VideoSlotEffect::SendAndLaunch(ready, launch) => (ready, launch), + other => panic!("receiver must accept the offer, got {other:?}"), + }; + let sender_launch = match sender_slot.receive(ready, true).await { + VideoSlotEffect::Launch(launch) => launch, + other => panic!("sender must launch after ready, got {other:?}"), + }; + let session_id = offer.session_id(); + let sender_exited = Arc::new(AtomicBool::new(false)); + let receiver_exited = Arc::new(AtomicBool::new(false)); + let sender_cancel = sender_launch.cancellation().clone(); + let sender_connection = sender_connection.clone(); + let sender_done = Arc::clone(&sender_exited); + let sender_worker = tokio::spawn(async move { + let mut stream = sender_connection + .open_uni() + .await + .expect("media stream opens"); + write_preamble(&mut stream, VideoPreamble::new(session_id, descriptor)) + .await + .expect("preamble writes"); + let codec = LengthDelimitedCodec::builder() + .max_frame_length(telepathy_core::internal::video::VIDEO_MEDIA_MAX_FRAME_LENGTH) + .new_codec(); + let mut framed = FramedWrite::new(stream, codec); + framed + .send(Bytes::from_static(b"bounded-video-frame")) + .await + .expect("media frame writes"); + sender_cancel.cancelled().await; + let _ = framed.into_inner().finish(); + sender_done.store(true, Ordering::Relaxed); + }); + let receiver_cancel = receiver_launch.cancellation().clone(); + let receiver_connection = receiver_connection.clone(); + let receiver_done = Arc::clone(&receiver_exited); + let receiver_worker = tokio::spawn(async move { + let mut stream = receiver_connection + .accept_uni() + .await + .expect("media stream accepted"); + assert_eq!( + read_preamble(&mut stream).await.expect("preamble reads"), + VideoPreamble::new(session_id, descriptor) + ); + let codec = LengthDelimitedCodec::builder() + .max_frame_length(telepathy_core::internal::video::VIDEO_MEDIA_MAX_FRAME_LENGTH) + .new_codec(); + let mut framed = FramedRead::new(stream, codec); + assert_eq!( + framed + .next() + .await + .expect("media frame arrives") + .expect("media frame reads") + .as_ref(), + b"bounded-video-frame" + ); + receiver_cancel.cancelled().await; + receiver_done.store(true, Ordering::Relaxed); + }); + assert!(sender_slot.install(&sender_launch, sender_worker).await); + assert!( + sender_slot + .complete_startup( + &sender_launch, + VideoWorkerStartup::Ready, + "sender".to_string() + ) + .await + .is_some() + ); + assert!( + receiver_slot + .install(&receiver_launch, receiver_worker) + .await + ); + assert!( + receiver_slot + .complete_startup( + &receiver_launch, + VideoWorkerStartup::Ready, + "receiver".to_string() + ) + .await + .is_some() + ); + assert_eq!( + sender_slot + .current_event("sender".to_string(), VideoPhase::Active, None) + .await + .expect("sender event") + .role, + VideoRole::Sender + ); + assert_eq!( + receiver_slot + .current_event("receiver".to_string(), VideoPhase::Active, None) + .await + .expect("receiver event") + .role, + VideoRole::Receiver + ); + + let stop = VideoControl::stop(session_id, VideoTerminalReason::Stopped); + let receiver_attempt = match receiver_slot.receive(stop, true).await { + VideoSlotEffect::Terminal(attempt, VideoTerminalReason::Stopped) => attempt, + other => panic!("remote stop must terminate receiver, got {other:?}"), + }; + let (sender_result, receiver_result) = tokio::join!( + sender_slot.cancel_and_join(sender_launch.attempt(), VideoTerminalReason::Stopped), + receiver_slot.cancel_and_join(receiver_attempt, VideoTerminalReason::Stopped) + ); + assert_eq!(sender_result, Some(VideoTerminalReason::Stopped)); + assert_eq!(receiver_result, Some(VideoTerminalReason::Stopped)); + assert!(sender_exited.load(Ordering::Relaxed)); + assert!(receiver_exited.load(Ordering::Relaxed)); + assert!( + sender_slot + .current_event("sender".to_string(), VideoPhase::Terminal, None) + .await + .is_none() + ); + assert!( + receiver_slot + .current_event("receiver".to_string(), VideoPhase::Terminal, None) + .await + .is_none() + ); + } + + pair.close().await; +} + +#[tokio::test] +async fn failed_worker_startup_never_activates_and_still_joins_on_terminal_cleanup() { + let slot = VideoSlot::default(); + let descriptor = VideoMediaDescriptor::display(VideoCodec::H264, 1280, 720); + let offer = slot + .start_local(descriptor) + .await + .expect("sender reserves a generation"); + let launch = match slot + .receive(VideoControl::ready(offer.session_id()), true) + .await + { + VideoSlotEffect::Launch(launch) => launch, + other => panic!("ready must launch sender, got {other:?}"), + }; + let cancellation = launch.cancellation().clone(); + let worker = tokio::spawn(async move { cancellation.cancelled().await }); + + assert!(slot.install(&launch, worker).await); + assert!( + slot.complete_startup(&launch, VideoWorkerStartup::Failed, "sender".to_string()) + .await + .is_none() + ); + assert_eq!( + slot.cancel_and_join(launch.attempt(), VideoTerminalReason::Failed) + .await, + Some(VideoTerminalReason::Failed) + ); + assert!( + slot.current_event("sender".to_string(), VideoPhase::Terminal, None) + .await + .is_none() + ); +} + +#[tokio::test(flavor = "multi_thread")] +async fn crossed_starts_and_stale_completions_preserve_replacement_generation_under_stress() { + let descriptor = VideoMediaDescriptor::display(VideoCodec::H264, 1280, 720); + let slot = VideoSlot::default(); + + for _ in 0..20 { + let local = slot + .start_local(descriptor) + .await + .expect("local generation starts"); + let remote_id = VideoSlot::default() + .start_local(descriptor) + .await + .expect("remote identity is generated") + .session_id(); + let (displaced, replacement) = match slot + .receive(VideoControl::offer(remote_id, descriptor), false) + .await + { + VideoSlotEffect::DisplaceAndSendAndLaunch(displaced, _, launch) => (displaced, launch), + other => panic!("canonical remote offer must replace local, got {other:?}"), + }; + let terminal = displaced + .cancel_and_join("peer".to_string(), VideoTerminalReason::Rejected) + .await; + assert_eq!(terminal.identity.session_id, local.session_id()); + assert_eq!(terminal.role, VideoRole::Sender); + assert_eq!(terminal.phase, VideoPhase::Terminal); + assert_eq!( + terminal.terminal_reason, + Some(VideoTerminalReason::Rejected) + ); + assert!(matches!( + slot.receive(VideoControl::ready(local.session_id()), false) + .await, + VideoSlotEffect::Ignored + )); + assert!(matches!( + slot.receive( + VideoControl::stop(local.session_id(), VideoTerminalReason::Failed), + false + ) + .await, + VideoSlotEffect::Ignored + )); + let worker_cancel = replacement.cancellation().clone(); + let worker = tokio::spawn(async move { worker_cancel.cancelled().await }); + assert!(slot.install(&replacement, worker).await); + let active = slot + .complete_startup(&replacement, VideoWorkerStartup::Ready, "peer".to_string()) + .await + .expect("winning receiver starts"); + assert_eq!(active.identity.session_id, remote_id); + assert_eq!(active.role, VideoRole::Receiver); + timeout( + Duration::from_secs(2), + slot.cancel_and_join(replacement.attempt(), VideoTerminalReason::Stopped), + ) + .await + .expect("replacement cleanup is bounded"); + assert!( + slot.current_event("peer".to_string(), VideoPhase::Terminal, None) + .await + .is_none() + ); + } +} + +#[tokio::test(flavor = "multi_thread")] +async fn teardown_does_not_publish_idle_before_blocked_worker_joins() { + let slot = Arc::new(VideoSlot::default()); + let descriptor = VideoMediaDescriptor::display(VideoCodec::H264, 1280, 720); + let offer = slot + .start_local(descriptor) + .await + .expect("generation starts"); + let launch = match slot + .receive(VideoControl::ready(offer.session_id()), true) + .await + { + VideoSlotEffect::Launch(launch) => launch, + other => panic!("ready must launch sender, got {other:?}"), + }; + let release = Arc::new(tokio::sync::Notify::new()); + let cancelled = Arc::new(AtomicBool::new(false)); + let worker_release = Arc::clone(&release); + let worker_cancelled = Arc::clone(&cancelled); + let cancellation = launch.cancellation().clone(); + let worker = tokio::spawn(async move { + cancellation.cancelled().await; + worker_cancelled.store(true, Ordering::Relaxed); + worker_release.notified().await; + }); + assert!(slot.install(&launch, worker).await); + let cleanup_slot = Arc::clone(&slot); + let attempt = launch.attempt(); + let cleanup = tokio::spawn(async move { + cleanup_slot + .cancel_and_join(attempt, VideoTerminalReason::Teardown) + .await + }); + while !cancelled.load(Ordering::Relaxed) { + tokio::task::yield_now().await; + } + assert!( + slot.current_event("peer".to_string(), VideoPhase::Stopping, None) + .await + .is_some() + ); + assert!(!cleanup.is_finished()); + release.notify_one(); + assert_eq!( + timeout(Duration::from_secs(2), cleanup) + .await + .expect("cleanup is bounded") + .expect("cleanup task joins"), + Some(VideoTerminalReason::Teardown) + ); + assert!( + slot.current_event("peer".to_string(), VideoPhase::Terminal, None) + .await + .is_none() + ); +} diff --git a/rust/telepathy-core/tests/core_integration_test/video_sessions/protocol.rs b/rust/telepathy-core/tests/core_integration_test/video_sessions/protocol.rs new file mode 100644 index 00000000..8a777c0e --- /dev/null +++ b/rust/telepathy-core/tests/core_integration_test/video_sessions/protocol.rs @@ -0,0 +1,102 @@ +use super::lifecycle::IrohPair; +use bytes::Bytes; +use futures_util::SinkExt; +use telepathy_core::internal::video::transport::read_preamble; +use telepathy_core::internal::video::{VIDEO_MEDIA_MAX_FRAME_LENGTH, VIDEO_PREAMBLE_MAX_LENGTH}; +use tokio::io::AsyncWriteExt; +use tokio::time::{Duration, timeout}; +use tokio_util::codec::{FramedRead, FramedWrite, LengthDelimitedCodec}; +use tokio_util::sync::CancellationToken; + +#[tokio::test(flavor = "multi_thread")] +async fn malformed_preamble_and_over_limit_frame_fail_on_real_iroh_streams() { + let pair = IrohPair::connect().await; + let sender_connection = pair.outbound.clone(); + let malformed = tokio::spawn(async move { + let mut stream = sender_connection.open_uni().await.expect("stream opens"); + stream + .write_u16((VIDEO_PREAMBLE_MAX_LENGTH + 1) as u16) + .await + .expect("length writes"); + stream.finish().expect("stream finishes"); + }); + let mut malformed_stream = pair.inbound.accept_uni().await.expect("stream accepted"); + assert_eq!( + read_preamble(&mut malformed_stream) + .await + .expect_err("oversized preamble fails") + .kind(), + std::io::ErrorKind::InvalidData + ); + malformed.await.expect("malformed sender joins"); + + let sender_connection = pair.outbound.clone(); + let oversized = tokio::spawn(async move { + let mut stream = sender_connection.open_uni().await.expect("stream opens"); + stream + .write_u32((VIDEO_MEDIA_MAX_FRAME_LENGTH + 1) as u32) + .await + .expect("length writes"); + stream.finish().expect("stream finishes"); + }); + let oversized_stream = pair.inbound.accept_uni().await.expect("stream accepted"); + let codec = LengthDelimitedCodec::builder() + .max_frame_length(VIDEO_MEDIA_MAX_FRAME_LENGTH) + .new_codec(); + let mut framed = FramedRead::new(oversized_stream, codec); + assert!( + futures_util::StreamExt::next(&mut framed) + .await + .expect("oversized frame is observed") + .is_err() + ); + oversized.await.expect("oversized sender joins"); + pair.close().await; +} + +#[tokio::test(flavor = "multi_thread")] +async fn slow_real_iroh_receiver_applies_bounded_backpressure_and_stop_joins_sender() { + let pair = IrohPair::connect().await; + let cancellation = CancellationToken::new(); + let worker_cancellation = cancellation.clone(); + let sender_connection = pair.outbound.clone(); + let sender = tokio::spawn(async move { + let stream = sender_connection.open_uni().await.expect("stream opens"); + let codec = LengthDelimitedCodec::builder() + .max_frame_length(VIDEO_MEDIA_MAX_FRAME_LENGTH) + .new_codec(); + let mut framed = FramedWrite::new(stream, codec); + let frame = vec![0x5A; VIDEO_MEDIA_MAX_FRAME_LENGTH]; + let mut sent = 0_usize; + loop { + tokio::select! { + biased; + _ = worker_cancellation.cancelled() => { + let _ = framed + .get_mut() + .reset(iroh::endpoint::VarInt::from_u32(1)); + return sent; + } + result = framed.send(Bytes::copy_from_slice(&frame)) => { + result.expect("frame writes until cancellation"); + sent += 1; + assert!(sent < 2_048, "sender bypassed transport backpressure"); + } + } + } + }); + let _held_stream = pair.inbound.accept_uni().await.expect("stream accepted"); + + tokio::time::sleep(Duration::from_millis(100)).await; + assert!( + !sender.is_finished(), + "slow receiver must backpressure sender" + ); + cancellation.cancel(); + let sent = timeout(Duration::from_secs(2), sender) + .await + .expect("cancelled sender joins promptly") + .expect("sender task does not panic"); + assert!(sent < 2_048); + pair.close().await; +} diff --git a/test/controllers/state_controller_test.dart b/test/controllers/state_controller_test.dart index e6e1fee1..b71e70b4 100644 --- a/test/controllers/state_controller_test.dart +++ b/test/controllers/state_controller_test.dart @@ -1,7 +1,9 @@ import 'dart:async'; +import 'dart:typed_data'; import 'package:flutter_test/flutter_test.dart'; import 'package:telepathy/controllers/state_controller.dart'; +import 'package:telepathy/core/rust/lib.dart'; import 'package:telepathy/core/rust/types.dart'; import 'package:telepathy/models/room.dart'; import '../support/fake_contact.dart'; @@ -12,6 +14,32 @@ Room _roomFixture(String id) => Room( nickname: 'Room $id', ); +VideoSessionIdentity _videoIdentity(String peerId, int value) => + VideoSessionIdentity( + peerId: peerId, + sessionId: VideoSessionId( + field0: U8Array16(Uint8List.fromList(List.filled(16, value))), + ), + ); + +VideoLifecycleEvent _videoEvent({ + required VideoSessionIdentity identity, + required VideoRole role, + required VideoPhase phase, +}) => + VideoLifecycleEvent( + identity: identity, + role: role, + source: VideoSource.display, + phase: phase, + ); + +void _activateVideoCall(StateController controller) { + controller.promotePendingCallAttempt( + controller.setPendingRoom(_roomFixture('video-call')), + ); +} + void main() { group('StateController.runAudioTest', () { test('clears inAudioTest after a successful audio test', () async { @@ -394,4 +422,137 @@ void main() { expect(controller.blockAudioChanges, isFalse); }); }); + + group('StateController video lifecycle', () { + test('active and terminal events only affect their owned identity', () { + final controller = StateController(); + _activateVideoCall(controller); + final first = _videoIdentity('peer-a', 1); + final second = _videoIdentity('peer-a', 2); + + controller.handleVideoLifecycle(_videoEvent( + identity: first, + role: VideoRole.sender, + phase: VideoPhase.active, + )); + controller.handleVideoLifecycle(_videoEvent( + identity: second, + role: VideoRole.sender, + phase: VideoPhase.active, + )); + controller.handleVideoLifecycle(_videoEvent( + identity: first, + role: VideoRole.sender, + phase: VideoPhase.terminal, + )); + + expect(controller.isSendingScreenshare, isTrue, + reason: 'a stale terminal must not clear a newer active identity'); + + controller.handleVideoLifecycle(_videoEvent( + identity: second, + role: VideoRole.sender, + phase: VideoPhase.terminal, + )); + expect(controller.isSendingScreenshare, isFalse); + }); + + test('sender terminal before active does not resurrect the sender', () { + final controller = StateController(); + _activateVideoCall(controller); + final identity = _videoIdentity('peer-a', 3); + + controller.handleVideoLifecycle(_videoEvent( + identity: identity, + role: VideoRole.sender, + phase: VideoPhase.terminal, + )); + + controller.handleVideoLifecycle(_videoEvent( + identity: identity, + role: VideoRole.sender, + phase: VideoPhase.active, + )); + expect(controller.isSendingScreenshare, isFalse); + }); + + test('receiver terminal before active does not resurrect the receiver', () { + final controller = StateController(); + _activateVideoCall(controller); + final identity = _videoIdentity('peer-a', 4); + + controller.handleVideoLifecycle(_videoEvent( + identity: identity, + role: VideoRole.receiver, + phase: VideoPhase.terminal, + )); + controller.handleVideoLifecycle(_videoEvent( + identity: identity, + role: VideoRole.receiver, + phase: VideoPhase.active, + )); + expect(controller.isReceivingScreenshare, isFalse); + }); + + test('a distinct identity can become active after a terminal event', () { + final controller = StateController(); + _activateVideoCall(controller); + final terminal = _videoIdentity('peer-a', 5); + final active = _videoIdentity('peer-a', 6); + + controller.handleVideoLifecycle(_videoEvent( + identity: terminal, + role: VideoRole.receiver, + phase: VideoPhase.terminal, + )); + controller.handleVideoLifecycle(_videoEvent( + identity: active, + role: VideoRole.receiver, + phase: VideoPhase.active, + )); + expect(controller.isReceivingScreenshare, isTrue); + + controller.handleVideoLifecycle(_videoEvent( + identity: active, + role: VideoRole.sender, + phase: VideoPhase.active, + )); + final stopped = controller.stopSendingScreenshare(); + expect(stopped, active); + + controller.handleVideoLifecycle(_videoEvent( + identity: active, + role: VideoRole.sender, + phase: VideoPhase.active, + )); + expect(controller.isSendingScreenshare, isFalse, + reason: 'a local stop must not be undone by a delayed active event'); + }); + + test('call teardown clears active video state and ignores late terminals', + () { + final controller = StateController(); + _activateVideoCall(controller); + final identity = _videoIdentity('peer-a', 4); + controller.handleVideoLifecycle(_videoEvent( + identity: identity, + role: VideoRole.receiver, + phase: VideoPhase.active, + )); + + controller.endOfCall(); + controller.handleVideoLifecycle(_videoEvent( + identity: identity, + role: VideoRole.receiver, + phase: VideoPhase.terminal, + )); + controller.handleVideoLifecycle(_videoEvent( + identity: identity, + role: VideoRole.receiver, + phase: VideoPhase.active, + )); + + expect(controller.isReceivingScreenshare, isFalse); + }); + }); } diff --git a/test/screens/settings/sections/audio_settings_test.dart b/test/screens/settings/sections/audio_settings_test.dart index a91727e4..659c7cf7 100644 --- a/test/screens/settings/sections/audio_settings_test.dart +++ b/test/screens/settings/sections/audio_settings_test.dart @@ -445,7 +445,26 @@ class _FakeTelepathy implements Telepathy { Future startManager() async {} @override - Future startScreenshare({required Contact contact}) async {} + Future requestVideoSource({ + required Contact contact, + required VideoSource source, + }) async => + const VideoStartOutcome.noSession(); + + @override + Future stopVideoSource({ + required VideoSessionIdentity identity, + }) async => + VideoStopOutcome.notFound; + + @override + Future videoCapabilities() async => + const VideoCapabilities( + send: VideoCapabilityAvailability.available(), + receive: VideoCapabilityAvailability.available(), + sendSources: [], + receiveFormats: [], + ); @override Future startSession({required Contact contact}) async {} diff --git a/test/screens/settings/sections/networking_test.dart b/test/screens/settings/sections/networking_test.dart index 01bc7f47..5329d4d9 100644 --- a/test/screens/settings/sections/networking_test.dart +++ b/test/screens/settings/sections/networking_test.dart @@ -652,7 +652,26 @@ class _FakeTelepathy implements Telepathy { Future startManager() async {} @override - Future startScreenshare({required Contact contact}) async {} + Future requestVideoSource({ + required Contact contact, + required VideoSource source, + }) async => + const VideoStartOutcome.noSession(); + + @override + Future stopVideoSource({ + required VideoSessionIdentity identity, + }) async => + VideoStopOutcome.notFound; + + @override + Future videoCapabilities() async => + const VideoCapabilities( + send: VideoCapabilityAvailability.available(), + receive: VideoCapabilityAvailability.available(), + sendSources: [], + receiveFormats: [], + ); @override Future startSession({required Contact contact}) async {} diff --git a/test/widgets/call/call_controls_test.dart b/test/widgets/call/call_controls_test.dart new file mode 100644 index 00000000..2208d195 --- /dev/null +++ b/test/widgets/call/call_controls_test.dart @@ -0,0 +1,158 @@ +import 'dart:typed_data'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart'; +import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart'; +import 'package:telepathy/controllers/audio_settings_controller.dart'; +import 'package:telepathy/controllers/state_controller.dart'; +import 'package:telepathy/core/rust/flutter.dart'; +import 'package:telepathy/core/rust/lib.dart'; +import 'package:telepathy/core/rust/player.dart'; +import 'package:telepathy/core/rust/types.dart'; +import 'package:telepathy/widgets/call/call_controls.dart'; + +import '../../support/fake_contact.dart'; + +class _OpaqueTelepathy implements Telepathy { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +class _OpaquePlayer implements SoundPlayer { + @override + dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); +} + +VideoSessionIdentity _identity() => VideoSessionIdentity( + peerId: 'desktop-peer', + sessionId: VideoSessionId( + field0: U8Array16(Uint8List.fromList(List.filled(16, 1))), + ), + ); + +VideoCapabilities _capabilities({required bool available}) => VideoCapabilities( + send: available + ? const VideoCapabilityAvailability.available() + : const VideoCapabilityAvailability.unavailable( + VideoUnavailable.runtimeUnavailable(), + ), + receive: const VideoCapabilityAvailability.available(), + sendSources: available + ? const [ + VideoSourceCapability(source: VideoSource.display, formats: []) + ] + : const [], + receiveFormats: const [], + ); + +Future _pumpControls( + WidgetTester tester, { + required StateController state, + required VideoControlActions actions, +}) async { + final audio = AudioSettingsController(options: SharedPreferencesAsync()); + await audio.init(); + addTearDown(audio.dispose); + await tester.pumpWidget( + MultiProvider( + providers: [ + ChangeNotifierProvider.value(value: state), + ChangeNotifierProvider.value(value: audio), + Provider.value(value: _OpaqueTelepathy()), + Provider.value(value: _OpaquePlayer()), + ], + child: MaterialApp( + home: Scaffold(body: CallControls(videoActions: actions))), + ), + ); +} + +void main() { + setUp(() { + SharedPreferencesAsyncPlatform.instance = + InMemorySharedPreferencesAsync.empty(); + }); + + tearDown(() { + SharedPreferencesAsyncPlatform.instance = null; + }); + + testWidgets('desktop control requests display and locally stops its identity', + (tester) async { + final state = StateController(); + final contact = FakeContact(id: 'desktop-peer', contactNickname: 'Desktop'); + final identity = _identity(); + var requestCalls = 0; + final stopped = []; + final actions = VideoControlActions( + videoCapabilities: () async => _capabilities(available: true), + isSourceConfigured: (_) async => true, + requestDisplay: (_) async { + requestCalls += 1; + return VideoStartOutcome.requested(identity); + }, + stop: (value) async { + stopped.add(value); + return VideoStopOutcome.stopped; + }, + ); + state.promotePendingCallAttempt(state.setPendingContact(contact)); + + await _pumpControls(tester, state: state, actions: actions); + expect(find.bySemanticsLabel('Screenshare icon'), findsOneWidget); + final screenshareButton = find.ancestor( + of: find.bySemanticsLabel('Screenshare icon'), + matching: find.byType(IconButton), + ); + tester.widget(screenshareButton).onPressed!(); + await tester.pump(); + + expect(requestCalls, 1); + expect(state.isSendingScreenshare, isFalse, + reason: 'request acceptance must not create pending UI'); + + state.handleVideoLifecycle(VideoLifecycleEvent( + identity: identity, + role: VideoRole.sender, + source: VideoSource.display, + phase: VideoPhase.active, + )); + await tester.pump(); + tester.widget(screenshareButton).onPressed!(); + await tester.pump(); + + expect(stopped, [identity]); + expect(state.isSendingScreenshare, isFalse, + reason: 'local stop clears the existing sending state immediately'); + }); + + testWidgets('unavailable generic start preserves the existing message', + (tester) async { + final state = StateController(); + state.promotePendingCallAttempt(state.setPendingContact( + FakeContact(id: 'desktop-peer', contactNickname: 'Desktop'), + )); + final actions = VideoControlActions( + videoCapabilities: () async => _capabilities(available: true), + isSourceConfigured: (_) async => true, + requestDisplay: (_) async => const VideoStartOutcome.unavailable( + VideoUnavailable.runtimeUnavailable(), + ), + stop: (_) async => VideoStopOutcome.stopped, + ); + + await _pumpControls(tester, state: state, actions: actions); + final screenshareButton = find.ancestor( + of: find.bySemanticsLabel('Screenshare icon'), + matching: find.byType(IconButton), + ); + tester.widget(screenshareButton).onPressed!(); + await tester.pumpAndSettle(); + + expect(find.text('Screenshare Unavailable'), findsOneWidget); + expect(state.isSendingScreenshare, isFalse); + }); +} diff --git a/test/widgets/contacts/contacts_call_target_test.dart b/test/widgets/contacts/contacts_call_target_test.dart index 09c7e2e0..5136ed75 100644 --- a/test/widgets/contacts/contacts_call_target_test.dart +++ b/test/widgets/contacts/contacts_call_target_test.dart @@ -308,7 +308,26 @@ class _RecordingTelepathy implements Telepathy { @override Future startManager() async {} @override - Future startScreenshare({required Contact contact}) async {} + Future requestVideoSource({ + required Contact contact, + required VideoSource source, + }) async => + const VideoStartOutcome.noSession(); + + @override + Future stopVideoSource({ + required VideoSessionIdentity identity, + }) async => + VideoStopOutcome.notFound; + + @override + Future videoCapabilities() async => + const VideoCapabilities( + send: VideoCapabilityAvailability.available(), + receive: VideoCapabilityAvailability.available(), + sendSources: [], + receiveFormats: [], + ); @override Future startSession({required Contact contact}) async { startSessionCalls += 1;