PieSocket Realtime Flutter Client
PieSocket SDK for Flutter written in Dart.
Installation
Add PieSocket into your project.
flutter pub add piesocket_channels
Usage
Import the library
import 'package:piesocket_channels/channels.dart';
Stand-alone Usage
Create a Channel instance as shown below.
Chanel channel = Channel.connect("wss://example.com", true)
channel.listen("system:message", (PieSocketEvent event) {
log("WebSocket message arrived!");
print(event.toString());
});
PieSocket's managed WebSocket server
Use following code to create a Channel with PieSocket's managed WebSocket servers.
Get your API key and Cluster ID here: Get API Key
PieSocketOptions options = PieSocketOptions();
options.setClusterId("demo");
options.setApiKey("VCXCEuvhGcBDP7XhiJJUDvR1e1D3eiVjgZ9VRiaV");
PieSocket piesocket = PieSocket(options);
Channel channel = piesocket.join("chat-room");
v4 — multi-channel over one connection
Set version: "4" to share a single WebSocket across every join()
call. join() still returns a Channel synchronously, same as v3 — the
multiplexing happens in the background:
PieSocketOptions options = PieSocketOptions();
options.setClusterId("demo");
options.setApiKey("VCXCEuvhGcBDP7XhiJJUDvR1e1D3eiVjgZ9VRiaV");
options.setVersion("4");
PieSocket piesocket = PieSocket(options);
Channel chat = piesocket.join("chat-room"); // opens the socket
Channel alerts = piesocket.join("alerts"); // rides the same socket
chat.listen("message", (event) { /* ... */ });
alerts.publishEvent("ping", data: {"at": DateTime.now().toIso8601String()});
Notes for v4:
join()stays synchronous even for a multiplexed channel — thesystem::subscribecontrol frame is sent in the background. Listen forsystem:connected(primary channel) the same way you would under v3. A secondary channel's subscribe failing (e.g.system::subscribe_error) is not delivered to the channel's own listeners — it's only logged — matching how the JS SDK handles this same case.- Presence is delta-based. The full roster arrives once; after that
Channel's member list is kept in sync from join/leave deltas. Callchannel.refreshMembers()to re-sync from the server on demand. - All v4 system events are double-colon (
system::member_joined,system::binary, etc.), unlike v3's single-colonsystem:events. - Binary needs no opt-in. Any binary frame arrives as a
system::binaryevent whosedatais a base64 string — decode it withbase64Decode. To send, callchannel.sendBinary(bytes)— a raw binary frame the server re-wraps assystem::binaryfor every other client (JS peers included). Primary channel only; a secondary channel'ssendBinarythrows. publishEvent(event, {data, meta})sends a structured payload (a Map, List, or primitive) directly, without needing to pre-jsonEncodeit into aPieSocketEventfirst — use this instead ofpublish(PieSocketEvent)for anything beyond a plain string, to avoid double-encoding it on the wire.notifySelfis connection-wide, not per-channel — if the firstjoin()call is the one that opens the shared socket, every other channel multiplexed onto it also gets that socket'snotifySelfsetting.- Unsubscribing the connect-time channel promotes another joined channel to keep the connection alive; a few in-flight frames may be missed during the swap.
- Guarded channels (
private-prefix, orforceAuth: true) resolve their JWT fromauthEndpointthe same as v3, withoutjoin()waiting on the fetch — ajoin()for another room that races in while that fetch is still in flight attaches once it resolves, rather than opening a second shared connection.
PieRTC — WebRTC video/audio rooms (v4 only)
PieRTC is programmable WebRTC over v4 — the Flutter counterpart to
piesocket-js's PieRTC. There's no v3 equivalent in this SDK. It depends on
flutter_webrtc (already a
dependency of this package) — your app still needs to add that package's own
camera/microphone permission entries to its AndroidManifest.xml/
Info.plist; this package has no platform folders of its own to put them in.
options.setVersion("4");
PieSocket piesocket = PieSocket(options);
Channel room = piesocket.join(
"video-room",
video: true,
cameraFacing: 'user', // 'user' (front, default) or 'environment' (rear)
onLocalVideo: (stream, pieRTC) { /* attach to a renderer */ },
onParticipantJoined: (uuid, stream) { /* attach remote stream */ },
onParticipantLeft: (uuid) { /* remove remote stream */ },
);
// Flip the camera on the live call (no renegotiation):
await room.pieRTC?.switchCamera(); // -> room.pieRTC.isFrontCamera
- Pass
video: true,audio: true, orpieRTC: truetojoin()to mark a room as PieRTC —room.pieRTCis attached once the room's connection resolves (may be afterjoin()already returned, same as everything else under v4). - Signalling uses its own
rtc::namespace (rtc::offer,rtc::answer,rtc::candidate,rtc::renegotiate, etc.) — a plain PieSocket relay, no server-side special-casing, so a Flutter and a JS/web client can share the same room. - Negotiation is collision-free: for each peer pair the client with the
larger uuid is the sole offerer and the other side only answers (it sends
rtc::renegotiateto ask for a fresh offer when it adds a track). This is what makes a symmetric 1:1 call — both sides sending audio + video — work without SDP glare. onParticipantJoinedfires once per remote stream, for audio-only peers as well as video ones.- Camera:
cameraFacingonjoin()picks the starting lens ('user'front /'environment'rear; default front).room.pieRTC.switchCamera()flips it live and returns the newroom.pieRTC.isFrontCamera. room.pieRTC.shareScreen()renegotiates a screen track onto every peer (alongside the camera);stopScreenShare()removes it. Both fireonScreenSharingStopped(with this client's own uuid) and publishrtc::stopped_screen; the SDK also stops if the user ends the share from the OS UI. Zero-config on web, desktop, macOS and iOS (iOS uses in-app ReplayKit capture). Android additionally needs the app to run a foreground service of typemediaProjectionwhile sharing — the simplest way is theflutter_backgroundpackage (FlutterBackground.enableBackgroundExecution()beforeshareScreen()), which ships the<service>its manifest needs.
PieCall — native incoming calls (CallKit / PushKit / full-screen intent)
PieCall turns PieRTC + call signalling + flutter_callkit_incoming into a
single object. It shows the native incoming-call UI, rings, wakes the screen,
answers from the lock screen, and — with a push — does all of that when the app
is force-quit.
On construction it also asks for notification permission itself (a rationale
alert, then the OS dialog) so incoming calls can show a native notification.
If your app already asks for notification permission elsewhere (e.g. via
firebase_messaging), pass PieCallConfig(requestNotificationPermission: false) to skip PieCall's own ask — otherwise the two can show as a
back-to-back duplicate prompt.
final call = PieCall(
socket: () => piesocket, // your PieSocket instance (nullable ok)
selfUserId: () => myUserId,
config: const PieCallConfig(appName: 'MyApp'),
)
..onEnsureConnected = () async { if (!realtime.connected) await realtime.reconnect(); }
..onAccept = (callId) async {
final r = await api.post('calls/$callId/accept');
return r['status'] != 'answered_elsewhere'; // false ⇒ another device won, stand down
}
..onDecline = (callId) => api.post('calls/$callId/decline')
..onHangUp = (callId, reason) => api.post('calls/$callId/${reason == 'missed' ? 'timeout' : 'end'}');
// feed it the events you already receive on private-user-<id>
socketChannel.listen('call_invite', (e) => call.handleSignal('call_invite', e.data));
// outgoing
final r = await api.post('calls', {'to_id': peerId, 'media': 'video'});
call.startOutgoing(invite: PieCallInvite.fromMap(r['call']));
// drive your UI
call.updates.listen((s) { /* s.phase, s.invite, s.muted, … */ });
Push wake-up. Your server sends a data payload on call_invite /
call_cancel (see the schema in PieCallPush). In your FCM background handler:
@pragma('vm:entry-point')
Future<void> _bg(RemoteMessage m) async {
if (PieCallPush.isCallPush(m.data)) {
await Firebase.initializeApp();
await PieCallPush.handleData(m.data, appName: 'MyApp');
}
}
Register the iOS VoIP token with your backend:
final token = await PieCall.voipToken(); (and re-register on
call.voipTokenChanges).
iOS setup (real device only):
Info.plist→UIBackgroundModesmust includevoipandremote-notification.- Xcode capabilities: Push Notifications + Background Modes → Voice over IP.
AppDelegate.swift— implement PushKit +CallkitIncomingAppDelegateand forward the VoIP push to the plugin synchronously (or iOS kills the app):
import PushKit
import CallKit
import flutter_callkit_incoming
@main
@objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate {
override func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let registry = PKPushRegistry(queue: .main)
registry.delegate = self
registry.desiredPushTypes = [.voIP]
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func pushRegistry(_ r: PKPushRegistry, didUpdate c: PKPushCredentials, for t: PKPushType) {
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(
c.token.map { String(format: "%02x", $0) }.joined())
}
func pushRegistry(_ r: PKPushRegistry, didInvalidatePushTokenFor t: PKPushType) {
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("")
}
func pushRegistry(_ r: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload,
for type: PKPushType, completion: @escaping () -> Void) {
guard type == .voIP else { return }
let p = payload.dictionaryPayload
let data = flutter_callkit_incoming.Data(
id: p["id"] as? String ?? "",
nameCaller: p["nameCaller"] as? String ?? "",
handle: p["handle"] as? String ?? "",
type: (p["isVideo"] as? Bool ?? false) ? 1 : 0)
data.extra = p as NSDictionary
SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true) { completion() }
}
// onAccept/onDecline/onEnd/onTimeOut: just call action.fulfill() — the Dart side hits your REST API.
}
Android setup:
AndroidManifest.xml: addPOST_NOTIFICATIONS,USE_FULL_SCREEN_INTENT,WAKE_LOCK,FOREGROUND_SERVICE,FOREGROUND_SERVICE_MICROPHONE; keep<application android:name="${applicationName}">.MainActivitymust extendFlutterFragmentActivity.app/build.gradle(.kts):manifestPlaceholders["applicationName"] = "<pkg>.MainApplication"and aMainApplicationthat callsFlutterCallkitIncomingPlugin.registerEventCallback(...).proguard-rules.pro:-keep class com.hiennv.flutter_callkit_incoming.** { *; }.- Java 17.
PieSocket is scalable WebSocket API service with following features:
- Authentication
- Private Channels
- Presence Channels
- Publish messages with REST API
- Auto-scalability
- Webhooks
- Analytics
- Authentication
- Upto 60% cost savings
We highly recommend using PieSocket over self hosted WebSocket servers for production applications.
Events
system:connected is the event fired when WebSocket connection is ready, get a full list system messages here: PieSocket System Messages
Documentation
For usage examples and more information, refer to: Official SDK docs