voltkit_rtc 0.7.2
voltkit_rtc: ^0.7.2 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.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 inRecordingException.keptFiles), a session whose finalisation was accepted is never aborted, and a delete that fails after a successful upload is reported onRecordingFile.localDeleteErrorinstead of failing the upload. - VK-08:
BroadcastPublisher.stop()cancels astart()still in flight. That start publishes nothing and throwsRpcErrorunpublished; a camera, peer connection or WHIP session it gets afterstop()is released (the session is DELETEd), and a late result or connection callback can no longer touch a newer start. A secondstart()while one is in flight throwsStateError, as one on air already did;start()afterdispose()throwsStateError; 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
preferHardwareCodecor a forcedcodecMimeTypecould 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'smustReconnect, 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 withdisconnectedinstead 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.
wsUrlparameters such as a tenant or region (repeated keys too) are kept on connect and every reconnect; onlytokenis set, to the current token, exactly once. - VK-04: an
onWirelogger 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 withdisconnectedpromptly; 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 asproducer.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 andclose()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 aclose()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 alate finalfield 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:
ChatClientreconnects apply the fresh join.roleandjoinResulttake the rejoin's reply (role, mute, members) beforeonReconnectedfires, so a listener reads the new snapshot; a reply that cannot be read keeps the previous one (logged toonWire), and the rejoin's history is still not replayed intoonMessage. - 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
startwith "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, andstopnames 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, beforeonClosedfires (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 withclosed). - 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()andstopMusic()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 (newSignaling.rpcWithin, also used forendRoomandrefreshToken), never resent. A quality change still queued whenunpublish()/close()runs fails withunpublished. One failing release step (a track that will not stop) no longer skips the others. - VK-06:
unpublish()no longer stops tracks you handed topublishStream(). Ownership is fixed when a stream is acquired and kept across reconnects: a stream you passed in is never stopped or disposed byunpublish(),close()or a failed publish; onepublish()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()duringpublishMusic()setup cancels it. ThatpublishMusic()fails withunpublishedand nothing goes on air afterwards; a late producer is closed on both ends. A secondpublishMusic()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.
producerClosedlooked the consumer up by the producer's id, so with different ids the consumer stayed open andsetReceiveQualitystill 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 ononTrack.
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()duringconnect()no longer brings the session back. A socket that finishes opening afterclose()is closed instead of joining, and a join answered just asclose()lands leaves the session closed (it used to end upconnected, and a secondclose()could not undo it).connect()on a closed client fails withclosed: create a new client. Calls toconnect()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 noreconnectedevent and no live socket follow a close. - FLUTTER-03:
endRoom()reports a room that ended as a success. The server announcesroomClosedand closes the session before its reply can arrive, which made a successfulendRoom()fail withclosed. It now resolves on the reply or onroomClosedwhile it waits, in either order. A refusal, a dropped socket or your ownclose()is still an error, and no answer at all now fails withtimeoutafter 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 withclosedinstead 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
resultthat is not an object, anerrorwith fields of the wrong type) fails its own call withinternal; frames that are not JSON objects are ignored; nothing throws inside the socket listener any more. Every pending call settles onclose(). close()ends the session at once: the state turnsclosed,onClosedfires 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 theaudioQualitygetter (AudioQuality.std,.hd). Standard (the default) is mono Opus declaringmaxaveragebitrate64000 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.audioQualityand the Opusmaxaveragebitratein 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.
publishMusicstays 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'smedia, else thevideopolicy) andbillingMode. 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 inpublish, 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 atmedia.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 thevideoFrameRategetter;videoQualitynow 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-fallbacknow means only that the camera, device or upload cannot provide 60. EveryonPublishWarningnow starts with its registry code (codec-fallbackanddegradation-preference-failedhad 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 });onRoomClosedcarries why the room ended (quota-exhaustedwhen the funds ran out). - Session.
connectionStateandonConnectionStateChanged(SessionState:idle,connecting,connected,reconnecting,closed; not calledConnectionState, 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 asRpcErrors with registry codes, plusreplacedandreconnect-gave-up),onMustReconnect,onKicked;participantskept current from the roster events;joinResultkeeps 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.isTerminalnow reads it:kickedandbilling-unavailableend a reconnect at once, like the other terminal codes; unknown codes are never terminal. The older spellingsroom-not-foundandquota-exceededstill 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.iosBroadcastEnableddefaults to false;iosStartTimeoutdefaults to 60 seconds. AddedonScreenSharePaused. 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;
onParticipantMutednormalizes 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.maxFramerateremains compatible. Capture/device fallback reportsframe-rate-fallbackononPublishWarning; 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 reportquality-unavailable, with automatic reception still available. -
publishScreen,unpublishScreen,ScreenPublishOptions,screenSharing,screenStream,onScreenShareStartedandonScreenShareEnded: detail (15 fps, 2.5 Mbps) or motion (30 fps, 6 Mbps), both with a 600 kbps lower layer and optional separatescreen-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 whentopBitrateis 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
topBitrateworks as before. - The other encoding defaults are unchanged: two layers, hardware H.264
preferred and
MAINTAIN_RESOLUTION. - Fix: reconnecting gives up when it should.
Signalingrestarted 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, andonClosednever saidgaveUp. The count now restarts only once a join succeeds, the waits keep growing, and aftermaxReconnectAttempts(8) failed attempts in a row the session ends withClosedReason.gaveUp.StreamClientandChatClientboth reconnect through it. - Fix: a camera that is off stays off, on both ends. A track handed to
publishStreamswitched 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 constraintspublish()used, so afterunpublish()and thenpublishMusic(), a reconnect asked for the camera and microphone again and published them. Music published afterunpublish()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 thatpublish(),publishStream()orpublishMusic()fails at once withRpcErrorcodeunpublishedinstead 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, andSignalingreconnects 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.videoPolicysays what the project may publish (from the join result's newvideofield) andvideoQualitywhat 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. topBitrateis 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 #
onUsageLimitonStreamClientandChatClient, withUsageLimit(reason,graceSeconds,endsAt): the project ran out of minutes or hit a daily or monthly usage limit, and the session will be ended atendsAtunless it is topped up. Show it to the user.- A session the server ended on purpose (WebSocket close code 4003, after
roomClosedor a finalusageLimit) 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.isTerminalnow matches the codes the server sends:no-room,wrong-tenant,quota-exhausted,daily-limit-reachedandmonthly-limit-reachedstop 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_webrtc1.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 onmediasoup_client_flutter0.8.5, which no longer builds, is gone. -
micEnabledandcamEnabledgetters, as in the browser SDK. -
Simulcast layers send three temporal layers each (
L1T3). -
Chat.
StreamClientgainssendMessage,getMessages,deleteMessage,muteChat,setTypingandgetChatState, withonMessage,onMessageDeleted,onChatMutedandonTyping. Direct messages, ephemeral messages and paged history are supported. -
ChatClientfor chat with no media, on its own connection. -
BroadcastPublisherfor 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,setMusicEnabledandstopMusicpublish music as a track of its own;setMicEnabledno longer touches it.onTrackMutedgainssource.
0.1.0 #
Rooms, roles, moderation, exclusive slots, simulcast publishing and reconnection. Not published.