metered_realtime 0.2.0
metered_realtime: ^0.2.0 copied to clipboard
Flutter WebRTC SDK for video & voice calls, screen sharing, and realtime pub/sub messaging — signaling, presence, auto-reconnect, and TURN, over flutter_webrtc.
Changelog #
0.2.0 #
Reliability release — hardens the automatic-recovery paths (network
switches, dropped connections, unanswered call setup) and closes several
lifecycle leaks. One API note: ErrorCode gained values, so an exhaustive
switch over it needs a new case — prefer a default branch and treat the
enum as open-ended from here on.
Changed #
ErrorCodeis future-proof. AddedErrorCode.backendErrorandErrorCode.internalError(retry-safe service-side failures), plusErrorCode.unknownfor codes newer than this SDK version. A service error the SDK doesn't recognize now rejects the matchingsubscribe/publish/sendimmediately with a typedSignallingServerError— previously the call sat out the full ack timeout and failed with a generic error. The on-the-wire code string is preserved on the newrawCodefield ofServerErrorEventandSignallingServerError.- Option validation now runs in every build mode. Out-of-range
ReconnectOptionsvalues and a non-positivetokenProviderTimeoutMsthrowArgumentErrorwhen the client is constructed. Previously these were debug-only assertions, and a bad value in a release build could produce a hot reconnect loop. - Delivery failures of connection-setup messages during routine recovery
are now logged (via your
Logger) rather than surfaced asonNegotiationError— a transport hiccup mid-reconnect is not a negotiation failure, and recovery proceeds on its own.
Fixed #
close()on an idleMeteredPeernow closes the underlying connection and completes the peer's event streams.join()can fail at the channel step with the connection already up; closing the peer previously left that connection running in the background and leftawait forloops on the peer's streams waiting forever.- A network drop on an unjoined peer no longer leaves a connection
retrying in the background. If
join()had failed with the connection still up and the network then dropped, the peer reported itself closed while the connection kept reconnecting beneath it. The connection is now released the moment the wrapper closes, and the peer's event streams complete. A drop arriving duringjoin()itself now settles the peer cleanly closed as well — a laterjoin()on that instance reports the closed state clearly instead of failing with an opaque internal error. - A stalled connection attempt fails fast and clean. If the service
accepts the socket but the connection never becomes ready,
connect()now rejects AND tears the socket down — so a retry can proceed immediately, and a late service handshake can't flip an already-failedconnect()toconnectedbehind your back. The same stall during automatic reconnection moves on to the next attempt in seconds instead of waiting on the inactivity watchdog (or stalling indefinitely with the watchdog disabled). - Recovery after a drop is watched by a deadline. If the channel roster
doesn't arrive after a reconnect, the SDK re-issues the subscription a
bounded number of times and then surfaces a typed
ReconcileTimeoutErrorononError— instead of showingreconnectingforever. Channel re-subscriptions that fail after a reconnect are also retried automatically a bounded number of times. A peer alone in its channel now completes recovery immediately on reconnect. - An unanswered connection-recovery offer no longer stalls the retry ladder. If the other side never answers (its connection died mid-recovery), the SDK times the attempt out and moves on, preserving the guarantee that a dead peer connection eventually surfaces the terminal negotiation error.
- Back-to-back renegotiations are no longer dropped. An offer arriving
immediately behind an answer (e.g. the remote starts a screen-share right
after connecting) was misread as a collision and ignored by one side; it
is now accepted per the WebRTC perfect-negotiation rules. A local offer
that races an inbound remote description is abandoned cleanly instead of
surfacing a spurious
onNegotiationErroron native platforms. connect()→close()→connect()no longer opens two connections when a token fetch is in flight: the superseded attempt aborts (itsconnect()rejects) instead of racing — or disrupting — the new one.- Subscription bookkeeping hygiene. An invalid channel name now rejects
with an
ArgumentErrorbefore being remembered (it used to be re-attempted, and re-fail, on every reconnect); asubscribeattempted while disconnected rejects without leaving a surprise subscription behind for the next reconnect — while a failed re-subscribe of an already-established channel keeps the original subscription intact. sendTowarns when a payload's shape collides with the SDK's connection-control messages (such payloads are consumed by the receiving SDK and never surface ononData).
0.1.0 #
Initial release — the Flutter/Dart SDK for Metered Realtime Messaging.
Added — Signalling core #
- Wire-protocol types mirroring the Metered Realtime Messaging server
(
WelcomePayload,ChannelMessage,DirectMessageEvent,PresenceEvent,PresencePeer,ServerErrorEvent,GoingAwayEvent,IceServerConfig,ErrorCode,WsCloseCode). - Strict inbound frame parser (malformed frames are dropped, never thrown) and
outbound frame builders with client-side validation +
generateRequestId. - Internal helpers: base-URL validation + connect-URL builder, credential
isolation (
CredentialStore),welcome.metadata.iceServersextraction with fail-closed validation, and credential/SDP scrubbing for log/error surfaces. Loggerinterface withNoopLogger/ConsoleLogger, and a UTF-8 byte-length helper for outbound size checks.SignallingClient: WebSocket pub/sub engine withconnect/close/dispose/subscribe/unsubscribe/publish/send, ack/requestId correlation, auto-reconnect (exponential backoff + jitter, terminal/slow-backoff close codes), token refresh + timeout on thetokenProviderpath, an inactivity watchdog with keepalive ping (disable withinactivityTimeoutMs <= 0), and subscription replay on reconnect. Events are exposed as idiomatic Dart broadcastStreams (onConnected,onDisconnected,stateChanges,onMessage,onDirect,onPresence,onServerError,onGoingAway,onTokenProviderError);dispose()closes them.- Public event/value types
StateChange<T>,ConnectedEvent,DisconnectedEvent,TokenProviderError, and the injectableWebSocketLikeseam (WsCloseInfo,WsReadyState,WebSocketFactory) over a defaultweb_socket_channeladapter (plus a test fake). - Typed exceptions:
SignallingConnectError,SignallingServerError,SignallingDisconnectedError. - Construction-time validation:
ReconnectOptionsandSignallingClientOptionsassert sane values; base URLs reject path/query/fragment/userinfo and non-localhost plaintextws://.
Added — WebRTC orchestration #
- WebRTC abstractions behind an injectable factory (
RtcPeerConnectionLikemirroringflutter_webrtc's surface, plusMediaStream/Track/RtpSender/DataChannel*Liketypes,RtcSessionDescription/RtcIceCandidate,RtcTrackEvent,PeerConnectionState), and aPeerConnectionengine with perfect-negotiation (polite/impolite collision + rollback) and an ICE-restart ladder (9 attempts, give-up-once, reset on connected) + inbound ICE flood caps. MeteredPeer/RemotePeer— the basic 1:N call:join/close, presence-drivenonPeerJoined/onPeerLeft, one connection per remote with lexical-politeness perfect-negotiation, local-track fan-out (addStream/addTrack/removeStream/removeTrack), inbound RTC-signal routing (never surfaced as data), broadcast + directonData, andonTrack/onStreamAdded/onStreamRemoved/stateChanges. Events are Stream-based;send/sendTowith client-side size + state checks; typedMeteredPeerSendError/MeteredPeerStateError/MeteredPeerOversizedError.
Added — flutter_webrtc binding #
- The package now depends on the Flutter SDK and
flutter_webrtc. A single adapter maps the plugin'sRTCPeerConnection/MediaStream/Track/RtpSender/DataChannelto the*Likeabstractions, so the orchestration core stays plugin-agnostic and fake-testable. rtcPeerConnectionFactorynow defaults to theflutter_webrtcbinding —MeteredPeer(MeteredPeerOptions(apiKey: ...))works with no factory; tests still inject a fake.- Send/receive real media with
wrapMediaStream/wrapMediaStreamTrack(orFlutterWebrtcMediaStream/FlutterWebrtcMediaStreamTrack); inbound streams expose.nativefor rendering. Seeexample/.
Added — Resilience, media metadata, data channels #
- Transient signalling drops no longer tear the call down:
MeteredPeer.state(and eachRemotePeer.state) goesreconnecting, peer references stay valid, and on reconnect each surviving peer's transport is refreshed in place before state returns tojoined. Remote streams re-announce viaonStreamAddedwith a fresh stream object (same id) — re-bind your renderer on every event.remote.pcgoes stale across the refresh; re-read it onstateChanges → connected. - Per-track/stream metadata:
addStream(s, metadata: {...})/addTrack(t, metadata: {...})deliver aStreamMetadatabag to every current and future peer ahead of the media; receivers read it on theonTrack/onStreamAddedpayloads. Sender-side lookups viagetTrackMetadata/getStreamMetadata. The bag rides thedirectchannel under the reserved__meteredTrackMetakey — don't use that key in your ownsendTopayloads (doing so logs a warning: the receiving SDK intercepts such payloads and they never surface ononData). MeteredPeer.replaceTrack(oldTrack, newTrack)— swap a sender's track across every peer without renegotiation (or pass null to silence). Partial failures throwMeteredPeerReplaceTrackErrorwith per-peersucceeded/failedoutcomes so you can retry just the failed subset.DataChannel— an opt-in wrapper over a raw channel (fromremote.pc.createDataChannel(...)orRemotePeer.onDataChannel) with typedonOpen/onClose/onError/onMessagestreams and a backpressure-awaresend()(suspends while the transport buffer is abovemaxBufferedAmount— confirmed against a fresh platform reading, with a backstop poll so a missed drain event can't hang sends — and rejects withDataChannelOverflowErrorpastmaxQueuedSends). Configure viaDataChannelOptions(non-positive values throwArgumentError);RtcDataChannelMessagecarries text or binary payloads.- Terminal close codes surfaced on
onErrornow include the code name (e.g.channel_not_authorized).