Skip to main content

voltkit_rtc 0.9.8 copy "voltkit_rtc: ^0.9.8" to clipboard
voltkit_rtc: ^0.9.8 copied to clipboard

Flutter SDK for VoltKit RTC: realtime audio and video rooms, in-room and standalone chat, and live broadcast publishing over WHIP.

0.9.8 #

  • The camera comes back when the publisher fallback's hold ends during a reconnect (the testers' note on 0.9.7). Ended between the rejoin letting the old producers go and publishing again, the hold left the captured camera off, and the republished camera stayed off for good. It now comes back with the next producer (in the background, when the app returns).
  • README: an Errors section. Catching connect() and publish() failures by code, what a closed session means, and the streams that report problems while connected. Corrected there too: Android's minimum SDK is 23 (it said 24 and 21), the plugin is flutter_webrtc 1.6 (not 0.9.x), the package does not build for the web (it uses dart:io), chat moderation and role changes are not a co-host's (promote and endRoom are the host's), and a remote music track's volume is set with Helper.setVolume (a renderer does not control remote sound on Android or iOS).

0.9.7 #

Fixes from the testers' report on 0.9.6 (issues/VOLTKIT_SDK_BUGS.md); each reproduced with a test first.

  • A switch made in a reconnect's gap no longer undoes the background pause (F096-1). With pauseCameraInBackground / pauseMicInBackground, a camera or microphone switched on after the rejoin let the old producers go, with the app then going to the background, was republished on air there; one switched off then was switched back on when the app returned. The switch-on now comes on with the app, and the switch-off stays off.
  • A camera or microphone switched off stays off when a reconnect has to capture the devices again (the testers' note on 0.9.6). The fresh capture started them on, and the republish put them back on air.

0.9.6 #

Fixes from the testers' report on 0.9.5 (issues/VOLTKIT_SDK_BUGS (2).md), and the classes of their Kotlin report found in this SDK (issues/VOLTKIT_SDK_BUGS (3)_part2.md); each reproduced with a test first.

  • A first connect() that never joined closes as a leave (F095-1). Refused at the join, or answered after joinTimeout, its socket closed with 4000 ("reconnecting") although nothing reconnects a session that never joined: a join the server admitted late stayed disconnected there, a ghost in the room until the grace period, and the webhook said disconnected, not left. It now closes with 1000. A Retry connect() on a joined session, and every reconnect, still close with 4000.
  • A reconnect republishing while the app is in the background keeps the camera and microphone off (the K-AND-2 class). A rejoin lets the old producers go before it publishes again; with pauseCameraInBackground / pauseMicInBackground, an app that went to the background in between found nothing to hold, and the republish put both on air there. They now come back off, and on with the app. A publish started in the background (from a notification, say) is the user's own for that stay, and a reconnect there keeps it on.
  • switchDevice(DeviceKind.videoinput, id) refuses a camera the platform does not have (the K-AND-4 class). Given an id no camera has, flutter_webrtc opens another camera instead (Android and iOS), and the switch reported success. Now it throws RpcError('bad-params'), the camera opened instead is released, and the old one stays on air.

0.9.5 #

  • Leaving versus reconnecting. A socket the client lets go to reconnect (a new socket, a media path that would not recover) now closes with 4000, not
    1. The server reads a normal close as the participant leaving, so the room and the participant.left webhook said left for a session that was only moving; they now say disconnected. With a server from 2026-10-21, the participantLeft event carries that reason for every departure.
  • A moderator's mute wins over a switch-on made while reconnecting (V094-1). A setMicEnabled(true) / setCamEnabled(true) made in a reconnect's gap, kept for the next producer since 0.9.4, put the microphone or camera back on after the rejoin's roster said a moderator had muted it: micEnabled true, the track live, and the server never told it was paused. A camera or microphone published while a moderator holds it off (a reconnect's republish included) now starts off, and the server is told; a moderator's mute also drops a switch-on still kept, so after a release switching back on is the user's. A switch-on with no moderation still comes back on, and a switch-off made in the gap still holds.
  • A switch made in the background is kept (V094-2). With pauseMicInBackground / pauseCameraInBackground, a publish asked for in the foreground that finished in the background switched off every kind, including a microphone the user had switched on there from a notification; a mute made there could be undone on return by a foreground unmute answered late. What the user switched during that stay in the background is left as they left it; the rest is still held.
  • Resume retries start afresh after a reconnect (V094-3). Retries used up while the socket was away, each failing at once, carried over to the new session's videos, so a video whose resume the server refused once stayed black.
  • A token given during the last rejoin attempt is tried (V094-4). Refused with the old token, the last attempt ended the session with gaveUp instead of trying the one updateToken() gave meanwhile. Each extra attempt needs a newer token, so reconnecting stays bounded.
  • A video first reported hidden while it is set up is paused (V094-5). With no view reported when it arrived, a setVideoView(visible: false) made while its resume was answered was never applied, and the video kept its full bandwidth; it is paused after the usual delay.
  • Timeouts and attempt counts that cannot work are refused (the NODE-041-4 class). A joinTimeout or openTimeout of zero or less failed every join or socket open with timeout at once, and a negative maxReconnectAttempts (not "forever") gave up at the first drop. StreamClient, ChatClient and Signaling now throw an ArgumentError naming the value when they are made, and Signaling.rpcWithin and Whip.offer refuse a timeout of zero or less before sending anything. maxReconnectAttempts: 0 (never reconnect) is still allowed.

0.9.4 #

Fixes from a second tester report (on 0.9.3); each was reproduced with a test first.

  • No token in error text (V093-C3, V093-D3). A WebSocket that could not be opened (a proxy or load balancer answering HTTP, a wrong path) threw dart:io's own error, whose text names the URL with the room token; connect() now throws RpcError('connect-failed') saying what happened without the URL, and StreamClient.preflight reports that. A WHIP publish whose server closed the connection, or answered with something that is not text, now throws WhipError(0, ...) instead of an HttpException carrying the publish token, or a FormatException.
  • Camera and microphone stay off in the background (V093-B1, V093-B2). With pauseCameraInBackground / pauseMicInBackground, a camera or microphone that comes on while the app is in the background through something started before it went there (a switch-on answered late, a publish that finished, the publisher fallback letting go) is switched off for the background too, and comes back with the app. One the user switches on in the background stays on.
  • A moderator's mute is for the camera or the microphone (V093-B3, V093-B4). A video force-mute no longer pauses (for good, unknown to the server) the screen share, nor an audio one the music. A camera force-mute also ends the publisher fallback's hold, so the fallback cannot switch the camera back on after the release. On a release the server resumes every producer of that kind; what is still off here is paused there again, so viewers are not told a camera is on that sends nothing.
  • Reconnecting (V093-C1, V093-C2, V093-C4, V093-A1):
    • a connect() that fails on a joined session (a Retry button) no longer leaves it stuck reconnecting, or "connected" with no socket: the reconnect goes on;
    • a token given with updateToken() while a rejoin is in flight is tried when the old one is refused, instead of the session ending;
    • a rejoin refused because the room ended (room-ended, no-room) or the client was removed (kicked) ends the session as roomClosed / kicked, not gaveUp;
    • a video or sound that ended while the socket was away ends on the reconnect (onTrackEnded), so no frozen tile is left;
    • a second connect() that succeeds on a joined session sets the media up again, as a reconnect does, instead of keeping transports the server closed with the old socket.
  • Publishing (V093-B5, V093-B6, V093-B7):
    • unpublish(), close() and stopMusic() give the app's own tracks back as they were handed in; a camera or microphone switched off while published was left disabled;
    • the publisher fallback's hold ends with unpublish() (onPublishFallback(false)), and the next camera is watched afresh;
    • a microphone published after the camera no longer puts the camera's profile (quality, codec, layers) back on the defaults, for the camera and for a reconnect;
    • a switch-off made while nothing of that kind is on air (a publish still under way, a reconnect's gap) holds for what is published next, instead of being lost.
  • Receiving (V093-A2 to V093-A5):
    • a video hidden, or the app sent to the background, while it is being resumed is paused after the usual delay;
    • a resume that fails is tried again (it left the video black);
    • the audio-only fallback stops sampling once nothing remote is left;
    • on servers without pauseConsumer, a quality picked while the fallback holds a camera keeps it held until the fallback lets go.
  • Broadcast and recording (V093-D1, V093-D2):
    • a broadcast whose connection fails is released (the connection, a camera it captured, the media server session), and start() begins again, as documented;
    • Android recording: when a later segment cannot start, stop() waits for the segments still uploading and names a file they keep.

0.9.3 #

Fixes from a tester report on 0.9.2:

  • A mute made in the background is kept (V090-01). With pauseMicInBackground or pauseCameraInBackground, a microphone or camera the user switched off while the app was in the background (from a notification, say) was switched back on when the app returned, and the room heard the user again. Only what the SDK switched off, and nobody switched since, comes back on; an unmute made in the background is no longer sent twice.
  • A camera switch that fails releases the new camera (V090-02). switchDevice(DeviceKind.videoinput, ...) left the new camera running, even after close(), when the sender refused the swap, when the camera gave no video, or when unpublish()/close() landed during the switch. The new camera is now released on every way out but success, and the old one stays on air. An interrupted switch throws StateError.
  • A camera handed in with publishStream is not switched by the SDK (V090-03). The SDK stopped the app's own camera track and put a camera of its own into the app's stream, which close() then never released. It now throws StateError before capturing anything: capture the new camera and publish that stream.
  • A hidden video stays at the smallest layer on servers that cannot pause it (V092-01). A quality, auto or a ceiling chosen while a setVideoView view was hidden was asked for at once; it now applies when the view is shown.
  • A receive choice that fails is not applied later (V092-02). With a view, setReceiveQuality and setPreferredLayers stored the choice before the server answered and kept it when it refused, so it was applied on the next view change. A failed call now leaves the last accepted choice in place, and only the latest call can go back.

0.9.2 #

  • A quality a viewer picks is the quality they get, as in the browser SDK 0.9.2. setReceiveQuality(producerId, '1080p' | '720p' | '480p') now gives that layer (the publisher's biggest at or below it) while the video is shown, whatever size setVideoView reports. Up to 0.9.1 it was only a ceiling, so a viewer who picked 1080p still got the smaller copy that fit the view. A hidden view still pauses the video, and auto goes back to the view's size. setPreferredLayers stays a ceiling.
  • Adaptive stream never stretches a layer: a view bigger than a layer gets the next one up (it used to accept a layer shown up to 15% bigger, which looks soft on a face; 2% is allowed for rounding).

0.9.1 #

  • Viewers get smooth video, not a slideshow. A camera's simulcast rungs are now sent as one full-frame-rate stream each (L1T1), as the browser SDK 0.9.1 does, instead of three frame-rate steps. With the steps the media server forwarded the bigger picture at a quarter of the frame rate whenever a viewer's bandwidth fell short of the top layer (1080p at 7.5 fps instead of 540p at 30). The vendored mediasoup handler now tells the server the temporal layers the encodings really have, not always three. Screen shares keep the steps.

0.9.0 #

  • switchDevice(DeviceKind.videoinput, deviceId): switch to a specific camera (a desktop's USB camera, say). It is captured with the settings publish used and swapped into the published camera in place (no pause for viewers; a switched-off camera stays off), the old camera is released, and a reconnect captures the chosen one.
  • onLocalStreamChanged: the stream this client publishes changed (a camera switched by id, or devices captured again after a reconnect); bind the local preview again. The browser SDK's localStreamChanged.
  • A faster first picture, as in the browser SDK: the join asks for the media transports and uses the ones the server made with it (controllers from 2026-10-20; older ones are asked as before); the DTLS parameters are sent without waiting for the reply (a refusal rejoins), and a video's first layers no longer wait before its resume. Up to three round trips fewer before the first publish or the first video.

0.8.0 #

  • getDiagnostics() and sdkVersion, as in the browser SDK: a snapshot for a support ticket (Diagnostics.toJson()); no token, no media.
  • In the background (Stream Video's background options): videos reported with setVideoView pause at the server while the app is in the background and resume on return; opt-in pauseCameraInBackground / pauseMicInBackground switch the camera / microphone off and back on (only what the SDK switched off; a user's or moderator's choice stays).
  • StreamClient.preflight(wsUrl:, token:, duration:, audio:, video:), as in the browser SDK: joins a test room, publishes the camera and microphone for a few seconds, leaves, and reports steps, media path, round trip, loss, upload and warnings (PreflightReport). Never throws.
  • onMediaWarning (MediaWarning, MediaWarningCode.micSilent | cameraFpsLow | cameraInterrupted | micInterrupted), as in the browser SDK: from the send transport's media sources every 2 s while listened to, and the tracks' system mute.
  • onAudioLevels and onActiveSpeakers, as in the browser SDK: levels about every 500 ms while listened to (remote audio from the receive transport's stats by track, the own microphone from its media source; AudioLevel.speaking above -35 dBov) and the speaker order when it changes (microphones only).
  • Publisher-side audio-only fallback (opt-in), as in the browser SDK: setPublishFallback(true), onPublishFallback.
  • Camera, speaker and devices (Agora/LiveKit/Stream Video native controls): switchCamera() (front/back on the same track, kept across reconnects), setCameraZoom, cameraHasTorch/setCameraTorch, setCameraFocus(point) (focus + exposure), setSpeakerphoneOn, listDevices([kind]), switchDevice(audioinput | audiooutput, id) (native routing; videoinput refused → switchCamera), onDevicesChanged. audioMode (AudioMode.communication / AudioMode.media), opt-in, applied before connecting: media for listen-only audiences (no voice processing, media volume, Bluetooth A2DP). Without it the platform audio is untouched, as before.
  • Audio-only fallback and onStreamFallback, as in the browser SDK: remote cameras paused while the received sound breaks up badly (25 % lost over ~15 s; screen shares stay), back after 20 s of recovery (longer after a relapse); the media server's bandwidth cuts reported too. StreamFallback {producerId, identity, source, fallback, reason} with StreamFallbackReason.bandwidth / audioProtection. On by default: StreamClient(..., audioOnlyFallback: false) or setAudioOnlyFallback(false); audioOnlyFallbackActive.
  • Media recovery: send and receive transports restart ICE after failure or 3 s disconnected, before a full rejoin. The entire recovery is bounded to 10 s; unsupported servers rejoin. onMediaReconnecting reports recovery.
  • Socket liveness: ping after 15 s idle, reconnect after 8 s without any response (checked every 5 s). Recovery detaches a dead socket without waiting for its close handshake. Late transport setup is discarded across sessions.

Performance on busy devices (owner's demo test, 2026-10-19), as in the browser SDK 0.8.0. Ideas from LiveKit (adaptive stream, dynacast, CpuConstrained), Tencent TRTC (small stream, receiveWhenViewVisible) and Agora (automatic low stream). Every 0.7 call works as before.

  • Adaptive stream: setVideoView(producerId, width:, height:, pixelRatio:, visible:, cover:) and clearVideoView. Report how a remote video is shown (e.g. from a LayoutBuilder): the SDK receives the smallest layer that fills it sharply and pauses it at the server while not visible. Bigger at once, smaller after ~1 s, paused after 1.5 s; reporting the same view again sends nothing. setReceiveQuality/setPreferredLayers stay a ceiling. A server without pauseConsumer gets the smallest layer instead of a pause.
  • Dynacast. The server's producerLayers event switches the camera's unused simulcast encodings off (the smallest stays on), through the vendored mediasoup Producer.setMaxSpatialLayer. Nothing to call.
  • onCpuConstrained (Stream The camera's send stats are read every 2 s while someone listens; a CPU limitation held for two samples is reported, and false when it clears or the camera stops. prioritizePerformance() stops encoding the camera's largest layer; performancePrioritized.

0.7.3 #

Fixes from the 2026-10-19 tester report on 0.7.2 (V072-01…08), each with regression tests that failed on 0.7.2.

  • V072-01: every capture publish() made is released and restored. Publishing twice (the microphone, then the camera) kept only the last capture: close()/unpublish() left the first one running and a reconnect republished only the last. The client now keeps every capture still published; each is stopped and disposed once (streams you handed in are never touched), a reconnect publishes each again, and localStream is the newest.
  • V072-02: a capture that failed is never asked for again by a reconnect. The failed request's constraints were remembered, so a later music-only publish made the reconnect open the camera (a permission prompt, or a camera going live the app never got). Constraints now belong to the capture that succeeded with them; a capture whose tracks ended is captured again with its own.
  • V072-03: a mic or camera switch survives an unpublish() or close() that lands while it waits. It threw ConcurrentModificationError (or closed); it now completes without doing more. Switches run one at a time in turn with publishing and quality changes, so a mute and a microphone being replaced no longer interleave (the new microphone stays muted).
  • V072-04: an accepted recording finalisation is never aborted. /complete answered success: true with unreadable data threw a TypeError and sent /abort; it now throws RecordingException, keeps the file and aborts nothing. A lost or unclear answer (no reply, unreadable, 5xx, 408/409/425/429) is asked again twice; still unclear, the file is kept and the session is not aborted. Only a readable refusal (4xx or success: false) aborts.
  • V072-05: the join handshake has a deadline. An unanswered join (first connect or a reconnect attempt) kept the client reconnecting forever whatever maxReconnectAttempts said. Opening the socket and the join are now each bounded (15 s; new optional joinTimeout on StreamClient, ChatClient and Signaling, openTimeout on Signaling): a stalled attempt fails with timeout, its socket is closed and a late reply ignored, and reconnecting goes on to the next attempt.
  • V072-06: an unmute answered late cannot undo a newer forced mute. A forceMuted arriving while setMicEnabled(true)/setCamEnabled(true) waited was reversed by the older reply (the track and micEnabled came back on). That switch now leaves it off, tells the server so, and fails with force-muted. Microphone and camera are tracked separately; music and screen are untouched.
  • V072-07: a reconnect takes the role from its own join reply. It was refreshed only by a follow-up getRoomState; if that failed, a client demoted or promoted while away kept its old role (and a demoted publisher could republish). Every field the rejoin reply carries is applied first (servers that send no ICE servers too); getRoomState only enriches it.
  • V072-08: a screen track's ended callback that throws no longer keeps the share on air. The SDK's cleanup starts first, then your callback runs once (its error still reaches you). An earlier share's ended callback no longer calls a newer share's callback.

0.7.2 #

  • VK-12: a failed recording upload keeps its input file. uploadFile(deleteAfterUpload: true) deletes the file only after the server accepted the recording and its reply was read; a reservation, storage, or finalisation failure keeps it (the Android preview names kept segments in RecordingException.keptFiles), a session whose finalisation was accepted is never aborted, and a delete that fails after a successful upload is reported on RecordingFile.localDeleteError instead of failing the upload.
  • VK-08: BroadcastPublisher.stop() cancels a start() still in flight. That start publishes nothing and throws RpcError unpublished; a camera, peer connection or WHIP session it gets after stop() is released (the session is DELETEd), and a late result or connection callback can no longer touch a newer start. A second start() while one is in flight throws StateError, as one on air already did; start() after dispose() throws StateError; a stopped publisher can start again.
  • VK-16: choosing a codec explicitly no longer fails when it is the last one offered. The bundled mediasoup read past the end of the codec list looking for RTX, so publishing with preferHardwareCodec or a forced codecMimeType could fail before any media was sent; RTX is now optional, and only the RTX entry that repairs the chosen codec is sent with it.
  • VK-03: a late updateToken() reply no longer overwrites a newer token. Refreshes run one at a time in call order (the last one asked for is kept), and a token the server installs while one waits (tokenRefreshed, or a promotion's mustReconnect, which carries the new role) supersedes it: the call installs nothing, is not sent if still queued, and returns the held token's expiry. The same guard covers the older-server/dropped-socket fallback, and a session closed meanwhile installs nothing (closed).
  • VK-01: a call waiting on a socket that connect() replaces fails with disconnected instead of waiting forever (its replies could no longer be read). Each pending call belongs to the socket it was sent on; only that socket's calls are failed, the new join is untouched, and nothing is replayed.
  • VK-02: the connection URL keeps its own query parameters. wsUrl parameters such as a tenant or region (repeated keys too) are kept on connect and every reconnect; only token is set, to the current token, exactly once.
  • VK-04: an onWire logger that throws no longer changes outcomes. A call already sent returns the server's answer, a reply or refusal keeps its result or code, and token and event handling carry on; the logger's exception is ignored.
  • VK-07: a reconnect no longer leaves queued camera/microphone work stuck behind a stale task. Resetting the per-source queue cancels every task still waiting in it at once with disconnected (they were made for the torn-down transport and never run), instead of leaving them blocked until the stale task ended; within a session tasks still run one at a time in call order, and a stale task finishing late cannot release a newer one. A live quality or frame-rate switch caught waiting by a reconnect therefore fails with disconnected promptly; the republish applies the setting.
  • VK-18: replacing a track on a producer switches the sender and the producer together. In the bundled mediasoup, Producer.replaceTrack() left an active sender on the old track, kept the old track as producer.track (so mute and ended handling still used it), disposed the whole old stream, and the send handler forgot the transceiver after one replacement (later replacements, layer changes and close() then failed to reach the sender). Replacements now run one at a time; the sender is switched whenever it should carry a track (zero-RTP pause keeps it empty until resume), the producer adopts the new track only after the sender has, with the current paused state, and a refusal or a close() meanwhile leaves the old track in place. Zero-RTP pause/resume now actually detach and re-attach the sender.
  • VK-17: Producer.setMaxSpatialLayer() no longer throws after the layer was applied. The layer was a late final field set twice; it is now recorded only after the transport applied it, changes run in order, a refusal keeps the previous layer (refusals used to be swallowed), and asking for the current layer sends nothing.
  • VK-15: ChatClient reconnects apply the fresh join. role and joinResult take the rejoin's reply (role, mute, members) before onReconnected fires, so a listener reads the new snapshot; a reply that cannot be read keeps the previous one (logged to onWire), and the rejoin's history is still not replayed into onMessage.
  • VK-13: a refused Android recording start no longer stops the recording already running. The recording channel is shared by every client in the app, and the native plugin answered a second start with "Recording is already active" and then stopped (and deleted) the active recording. It now refuses before creating anything, rolls back only what a failed start itself created, and stop names the segment it ends ({'path': ...}; a stop for a segment that is no longer active does nothing). The Dart side stops only a segment it started and names it. Compiled with Gradle against flutter_webrtc 1.6.2; not yet run on a device.
  • VK-11: however a session ends, the SDK releases the media it opened. Kicked, the room closed, replaced (4001) and reconnecting given up now run the same cleanup as close(): the camera, microphone and screen capture the SDK opened are stopped and disposed, its producers, consumers and transports closed, recording capture and the screen foreground service stopped, before onClosed fires (at most 5 s later if a platform call hangs). One cleanup however many paths reach it; the first reason is the one reported. Nothing is captured, produced or subscribed to afterwards (publish* fail with closed).
  • VK-05: releasing media never waits on the server. close() ends the session at once (the server releases a client's producers when its socket goes) and releases local media without waiting for any reply. unpublish(), unpublishScreen() and stopMusic() release locally first, ask the server to close the producers all at once and give it 5 s; a call that goes unanswered is failed and forgotten at its deadline (new Signaling.rpcWithin, also used for endRoom and refreshToken), never resent. A quality change still queued when unpublish()/close() runs fails with unpublished. One failing release step (a track that will not stop) no longer skips the others.
  • VK-06: unpublish() no longer stops tracks you handed to publishStream(). Ownership is fixed when a stream is acquired and kept across reconnects: a stream you passed in is never stopped or disposed by unpublish(), close() or a failed publish; one publish() captured is stopped and disposed exactly once, also after a reconnect published it again (it used to lose its ownership and never be disposed). A publish that fails before anything went on air releases what it captured and is not republished by a reconnect.
  • VK-14: stopMusic() during publishMusic() setup cancels it. That publishMusic() fails with unpublished and nothing goes on air afterwards; a late producer is closed on both ends. A second publishMusic() replaces one still starting, whose producer can no longer take the new one's place. Camera, microphone and screen are untouched, and your music stream is never stopped. The music producer's appData carries its operation number (musicGeneration).
  • VK-09: a video that fails to subscribe hands its slot to the next one waiting. Under a wallet project's video limit, a failed subscription now admits the oldest waiting video at once (it used to wait for some other video to end). Each producer is subscribed to once, the limit is never exceeded, and a failed video is not retried in a loop.
  • VK-10: a closed producer's consumer is closed. producerClosed looked the consumer up by the producer's id, so with different ids the consumer stayed open and setReceiveQuality still found it. Consumers are now found by their producer id and closed once; a consumer that arrives after its producer closed (or for nobody) is closed and never reported on onTrack.

0.7.1 #

Fixes from the 2026-10-16 review of 0.7.0, each with regression tests. No API changes: every 0.7.0 call works as before.

  • FLUTTER-01: close() during connect() no longer brings the session back. A socket that finishes opening after close() is closed instead of joining, and a join answered just as close() lands leaves the session closed (it used to end up connected, and a second close() could not undo it). connect() on a closed client fails with closed: create a new client. Calls to connect() made while one is in flight share it: one socket, one join.
  • FLUTTER-02: close() during a reconnect wait stops the reconnect. The wait is cut short and nothing is opened after it, so no reconnected event and no live socket follow a close.
  • FLUTTER-03: endRoom() reports a room that ended as a success. The server announces roomClosed and closes the session before its reply can arrive, which made a successful endRoom() fail with closed. It now resolves on the reply or on roomClosed while it waits, in either order. A refusal, a dropped socket or your own close() is still an error, and no answer at all now fails with timeout after 15 seconds instead of waiting forever.
  • FLUTTER-04: a session that ends on its own releases its socket. When the server refuses a reconnect for good (for example bad-token), the attempts run out, or the session is replaced (4001) or ended by the server (4003), the socket is closed and every later call fails with closed instead of being sent on a session that reported itself closed.
  • FLUTTER-05: a malformed server reply can no longer leave a call waiting forever. A reply that is not shaped like one (a result that is not an object, an error with fields of the wrong type) fails its own call with internal; frames that are not JSON objects are ignored; nothing throws inside the socket listener any more. Every pending call settles on close().
  • close() ends the session at once: the state turns closed, onClosed fires and pending calls fail without waiting for the socket's closing handshake.
  • Docs: the README says 60 fps is delivered only when the capture source provides it (most phone and laptop cameras give apps 30 fps), how to check a camera's rates, and that billing follows the frame rate delivered; and it describes the close and endRoom() behaviour above.

0.7.0 (prepared, not published; not yet verified on a device) #

Additive: every 0.6 call works as before. Pairs with the controller's wallet billing (join result media and billing, walletLow, refreshToken) and works with older controllers too. The same behaviour as @voltkit-rtc/client 0.7.0, following the shared SDK contract.

  • Audio quality. publish(audioQuality:) / publishStream(audioQuality:), setAudioQuality() live and the audioQuality getter (AudioQuality.std, .hd). Standard (the default) is mono Opus declaring maxaveragebitrate 64000 with the sender held at 60 kbps, so it is never measured above 64 kbps and stays standard on a server that bills what it measures; HD is stereo Opus at up to 128 kbps. The microphone declares its quality to the server (appData.audioQuality and the Opus maxaveragebitrate in the produce request). Switching re-creates the microphone's producer and keeps it muted if it was. Music is unchanged (128 kbps stereo). In 0.6 the microphone was mono with no declared rate.
  • Music is HD and free. publishMusic stays a 128 kbps stereo track in every project, also one limited to standard audio (the server allows it there and never bills music: the microphone and video alone decide the tier).
  • Project media settings. mediaPermissions (from the join result's media, else the video policy) and billingMode. A kind the project switched off is refused before any device opens or anything is produced (media-disabled: publish, publishStream, publishScreen, publishMusic); setAudioQuality(hd) without HD is refused (audio-quality-not-allowed); HD in publish, 60 fps without 60 fps modes and a 60 fps size that is not enabled are capped with a warning carrying the server's code. Media modes narrow wallet sessions only. A wallet session waits for a video slot at media.fanout.maxVideoSubscriptions (video-subscription-limit) instead of asking for a refusal. Screen video follows the project's maximum quality, and screen audio is dropped with a warning where audio is off.
  • Live video switch. setVideoQuality(), setVideoFrameRate() and the videoFrameRate getter; videoQuality now accounts for the 60 fps limits. getVideoStats() returns the camera's layers (VideoSendStats).
  • Changed: 60 fps refused by the project now warns frame-rate-not-allowed (the server's code); frame-rate-fallback now means only that the camera, device or upload cannot provide 60. Every onPublishWarning now starts with its registry code (codec-fallback and degradation-preference-failed had none).
  • Tokens. updateToken(token) sends a newer token on the live connection (refreshToken; older servers keep it for the next reconnect) and resolves to its expiry; onTokenExpiring (TokenExpiring) fires a minute before the held token expires when nobody refreshed it. Server-pushed tokens carry their expiry.
  • Wallet. onWalletLow (WalletLow { remainingSeconds, cleared }); onRoomClosed carries why the room ended (quota-exhausted when the funds ran out).
  • Session. connectionState and onConnectionStateChanged (SessionState: idle, connecting, connected, reconnecting, closed; not called ConnectionState, which Flutter's widgets library already exports). onNetworkQuality (NetworkQuality { level, rttMs, lossPct, availableKbps }) about every 3 s while listened to, with the shared thresholds.
  • Events and state like the browser: onError (warnings and refusals as RpcErrors with registry codes, plus replaced and reconnect-gave-up), onMustReconnect, onKicked; participants kept current from the roster events; joinResult keeps the slots and room metadata current; remoteTracks; getRoomState().
  • Errors. One registry shared by every VoltKit SDK: errorCodes (ErrorInfo: origin, terminal, retryable), errorInfo(), isTerminalError(), RpcError.info. RpcError.isTerminal now reads it: kicked and billing-unavailable end a reconnect at once, like the other terminal codes; unknown codes are never terminal. The older spellings room-not-found and quota-exceeded still count as their registry codes. screen-share-ended (iOS: the broadcast was stopped from the system UI before it started), raised since 0.6.0, is now in the registry.
  • No package is published by this change: the owner publishes.

0.6.0 (prepared; iOS preview unverified) #

  • Hardened Android/shared screen cleanup: cancellation during consent prevents late capture startup; partially failed foreground-service starts invoke stop; individual track/stream/service cleanup failures warn without preventing remaining cleanup or room close. Existing service callbacks stay compatible.

  • Prepared an explicitly opt-in iOS ReplayKit preview: native lifecycle plugin, complete Broadcast Upload Extension template and safe app-specific preparation tool. Checks embedded extension/App Groups, handles start timeout/cancellation, system stop, pause/resume and failed-start cleanup independently of the camera.

  • ScreenPublishOptions.iosBroadcastEnabled defaults to false; iosStartTimeout defaults to 60 seconds. Added onScreenSharePaused. Caller-supplied iOS captures are supported and remain caller-owned.

  • Extension forwards bounded, orientation-aware video frames only; existing mic/music and Android/desktop APIs remain unchanged. Native compilation and Apple-device acceptance are unverified: see doc/IOS-SCREEN-SHARING.md.

  • Paid direct-to-R2 file uploads with bounded multipart concurrency; authoritative recording policy and completed-file ID/playback/90-day expiry metadata. Android camera-only recording preview is opt-in, video-only and device-unverified; iOS recording remains unavailable.

0.5.0 #

  • Roster moderation locks and request metadata; onParticipantMuted normalizes wire state. Reconnect preserves restrictions, rejected unmute calls remain paused, and screen restoration failures clean up the share without blocking the camera. Screen video respects per-project abuse restrictions. Receive preferences are applied before resuming the consumer.

  • All qualities included: 1080p, 720p and 480p. Default policy is 1080p/60 fps; administrators can still restrict an individual project.

  • New 30 fps top-layer ladder: 6 Mbps / 3 Mbps / 1.2 Mbps. Opt-in VideoPublishOptions(frameRate: 60) raises only the top layer to 9 / 4.5 / 1.8 Mbps; lower layers stay at 30 fps. maxFramerate remains compatible. Capture/device fallback reports frame-rate-fallback on onPublishWarning; sender statistics monitor sustained slow 60 fps output.

  • setReceiveQuality(producerId, 'auto' | '1080p' | '720p' | '480p') uses advertised actual layer dimensions and remembers the choice by participant and source across reconnects. Missing layouts report quality-unavailable, with automatic reception still available.

  • publishScreen, unpublishScreen, ScreenPublishOptions, screenSharing, screenStream, onScreenShareStarted and onScreenShareEnded: detail (15 fps, 2.5 Mbps) or motion (30 fps, 6 Mbps), both with a 600 kbps lower layer and optional separate screen-audio. Camera, mic and music remain independent. Live screen captures are republished on reconnect.

  • Android requires a MediaProjection foreground service supplied by the app; desktop apps select a source through desktopCapturer. Native screen frame dimensions are measured before publishing instead of assuming a 1080p monitor. iOS is explicitly unsupported until a complete ReplayKit extension is provided. See the README for setup and native capture limitations.

0.4.0 #

  • Sharper video: higher bitrates. layerBitrate, which sets each layer's bitrate from its size when topBitrate is unset, now gives about 4 Mbps at 1080p, 2.5 Mbps at 720p, 1.5 Mbps at 540p, 1 Mbps at 480p, 700 kbps at 360p and 400 kbps at 240p (was 2.5 Mbps, 1.8 Mbps, 1.2 Mbps, 750 kbps, 600 kbps and 250 kbps). On a 720p camera picture, 2.5 Mbps brought the encoder's quantiser down from about 64 to about 55: finer detail, most of all in motion. The default two layers at 720p are now 640x360 at 700 kbps and 1280x720 at 2.5 Mbps, about 3.2 Mbps of upload. The same table as the browser SDK 0.4.0.
  • The service's per-publisher caps rose to match: 4 Mbps up to 720p, 7 Mbps with 1080p (were 3.2 and 5). An explicit topBitrate works as before.
  • The other encoding defaults are unchanged: two layers, hardware H.264 preferred and MAINTAIN_RESOLUTION.
  • Fix: reconnecting gives up when it should. Signaling restarted its attempt count whenever the socket opened, so a join refused with a code worth retrying (no-capacity, no-node, internal) was retried about twice a second, forever, and onClosed never said gaveUp. The count now restarts only once a join succeeds, the waits keep growing, and after maxReconnectAttempts (8) failed attempts in a row the session ends with ClosedReason.gaveUp. StreamClient and ChatClient both reconnect through it.
  • Fix: a camera that is off stays off, on both ends. A track handed to publishStream switched off, or a camera, microphone or music switched off before a reconnect, made a producer paused in the app but live on the server, so viewers got black video or silence under a camera or microphone shown as on. The server is now told. And a reconnect with the camera and microphone both off took their switched-off tracks for ended ones: it asked for the devices again while still holding them, and published them switched on. It now publishes the same tracks again, still off.
  • Fix: unpublish() forgets the devices. It kept the capture constraints publish() used, so after unpublish() and then publishMusic(), a reconnect asked for the camera and microphone again and published them. Music published after unpublish() now reconnects alone.
  • Fix: unpublish() while a publish is still being made leaves nothing published. A producer that arrives afterwards is closed on both ends, and that publish(), publishStream() or publishMusic() fails at once with RpcError code unpublished instead of waiting out its 20 s timeout. unpublish() makes all its changes at once and only then waits, so a publish called while it still waits is not undone by it.
  • Fix: a first connect() that fails at the join closes its socket, and Signaling reconnects only a session that has joined. Before, the server's join deadline closed that socket about 15 s later and started reconnecting a session that had never begun.

0.3.0 #

  • Video quality. VideoPublishOptions(quality: VideoQuality.p1080 / p720 / p480). StreamClient.videoPolicy says what the project may publish (from the join result's new video field) and videoQuality what is being sent. 1080p needs the project to allow it (it does during the free trial); asking for more publishes the highest allowed.
  • Layers follow the picture's real size. A picture too small for the layers asked for used to lose its top ones, so viewers got half the resolution; now 480p is sent as two layers and small pictures as fewer.
  • Fix: publish() captures 720p by default. It asked the camera for 640x360, so the default broadcast was 360p while the docs said 720p. It now captures at the quality being published.
  • topBitrate is optional: unset, it follows the picture's size (2.5 Mbps at 1080p, 1.8 Mbps at 720p, 750 kbps at 480p). Setting it works as before.
  • New exports: VideoQuality, VideoPolicy, layerBitrate.

0.2.1 #

  • onUsageLimit on StreamClient and ChatClient, with UsageLimit (reason, graceSeconds, endsAt): the project ran out of minutes or hit a daily or monthly usage limit, and the session will be ended at endsAt unless it is topped up. Show it to the user.
  • A session the server ended on purpose (WebSocket close code 4003, after roomClosed or a final usageLimit) is final and is not reconnected.
  • Fix: a promoted viewer now moves to a new connection with the token for its new role. Before, it kept the old connection and its viewer token, so the promotion did nothing and publishing was refused.
  • Fix: after a reconnect, the client no longer subscribes to its own tracks (the room state lists them), which played a host a delayed copy of itself.
  • A publisher who is demoted stops publishing and releases the camera and microphone (the server now closes a demoted publisher's tracks itself).
  • After a reconnect, the client uses the fresh TURN credentials from the new join instead of the original ones, which expire after 12 hours by default.
  • RpcError.isTerminal now matches the codes the server sends: no-room, wrong-tenant, quota-exhausted, daily-limit-reached and monthly-limit-reached stop reconnecting instead of retrying.

0.2.0 #

First release on pub.dev. The package was previously distributed by path as stream_client; only the package name changed, so migrating is the dependency line and the import.

  • Works on current Flutter and Android. Built on flutter_webrtc 1.6. The mediasoup client is now included in the package with fixes that publishing from a real Android device needed: a race between the transport and its peer connection, simulcast RID parsing, a crash when an encoding has no scalability mode, and header extension IDs current libwebrtc rejected. The previous dependency on mediasoup_client_flutter 0.8.5, which no longer builds, is gone.

  • micEnabled and camEnabled getters, as in the browser SDK.

  • Simulcast layers send three temporal layers each (L1T3).

  • Chat. StreamClient gains sendMessage, getMessages, deleteMessage, muteChat, setTyping and getChatState, with onMessage, onMessageDeleted, onChatMuted and onTyping. Direct messages, ephemeral messages and paged history are supported.

  • ChatClient for chat with no media, on its own connection.

  • BroadcastPublisher for publishing a live broadcast over WHIP, H.264 only so the media server never has to transcode.

  • Music. Tracks now say what they carry (RemoteTrack.source: mic, music, camera, screen), so a viewer app can give music its own volume. publishMusic, setMusicEnabled and stopMusic publish music as a track of its own; setMicEnabled no longer touches it. onTrackMuted gains source.

0.1.0 #

Rooms, roles, moderation, exclusive slots, simulcast publishing and reconnection. Not published.

0
likes
0
points
1.21k
downloads

Documentation

Documentation

Publisher

unverified uploader

Weekly Downloads

Flutter SDK for VoltKit RTC: realtime audio and video rooms, in-room and standalone chat, and live broadcast publishing over WHIP.

Homepage

Topics

#webrtc #video #live-streaming #chat #realtime

License

unknown (license)

Dependencies

collection, events2, flutter, flutter_webrtc, h264_profile_level_id, sdp_transform

More

Packages that depend on voltkit_rtc

Packages that implement voltkit_rtc