flaky-jelly-17724
12/19/2025, 5:30 AMtall-airline-16756
12/19/2025, 5:30 AMtall-airline-16756
12/19/2025, 5:30 AMconst token = new AccessToken(apiKey, apiSecret);
token.identity = 'user123';
token.addGrant({ roomJoin: true, room: 'my-room', canPublish: true, canSubscribe: true, canPublishData: true });
const jwt = token.toJwt();
2) Connect the client and wait for room readiness
• Connect using the token and wait until connection is stable:
const room = new Room();
await room.connect(wsUrl, jwt);
// room.localParticipant is ready to publish
3) Acquire or create media tracks, then publish
• Use getUserMedia for mic/camera or create a MediaStream (Web Audio/element) for programmatic audio:
// mic + camera
const stream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true });
await room.localParticipant.publishTrack(stream.getAudioTracks()[0]);
await room.localParticipant.publishTrack(stream.getVideoTracks()[0]);
// OR publish custom audio from an <audio> via WebAudio
const audioCtx = new AudioContext();
const audioEl = new Audio(url);
const src = audioCtx.createMediaElementSource(audioEl);
const dest = audioCtx.createMediaStreamDestination();
src.connect(dest);
await room.localParticipant.publishTrack(dest.stream.getAudioTracks()[0], { name: 'music' });
4) Keep lifecycle tidy (recommended pattern)
• Track published track objects, remove event listeners, and stop underlying sources when done:
// when unpublishing/cleanup
room.localParticipant.unpublishTrack(publishedTrack);
publishedTrack.stop?.();
stream.getTracks().forEach(t => t.stop()); // stop camera/mic
src.disconnect();
audioCtx.close();
await room.disconnect();
Best practices
• Grant minimal permissions per participant (only what they need).
• Publish after connect succeeds to avoid race conditions.
• Keep references to published tracks so you can unpublish/stop them reliably.
• Close audio contexts and stop MediaStream tracks to avoid memory leaks.
Tell me your platform (browser/React/React-Native/mobile) and whether you need examples for data publishing or simulcast/quality layers — I’ll provide the exact code you need.
Sources: server-sdk-js | livekit/livekit_composite | DeepWiki | Connecting to LiveKit | LiveKit docs | Stream music over WebRTC using React and WebAudioflaky-jelly-17724
12/19/2025, 5:31 AMtall-airline-16756
12/19/2025, 5:32 AMflutter pub add livekit_client
2) Platform permissions
• Android: add CAMERA, RECORD_AUDIO, MODIFY_AUDIO_SETTINGS in AndroidManifest.
• iOS: add NSCameraUsageDescription and NSMicrophoneUsageDescription in Info.plist.
3) Create and connect the Room
• Use Room() and connect with RoomOptions (enable adaptiveStream/dynacast for best performance).
final room = Room();
final options = RoomOptions(adaptiveStream: true, dynacast: true);
await room.connect('<wss://your-self-hosted-domain.com>', 'YOUR_JWT', roomOptions: options);
4) Publish camera and microphone (after connect)
• Use the local participant helpers to enable and publish tracks.
await room.localParticipant.setCameraEnabled(true);
await room.localParticipant.setMicrophoneEnabled(true);
• If you need to create or publish custom tracks, obtain a MediaStream and call publishTrack(...) on localParticipant.
5) Optional: manual subscribe control
• If you want selective subscription, connect with RoomOptions(autoSubscribe: false) and subscribe to remote tracks when needed.
6) Track and manage resources (clean pattern)
• Keep references to published tracks and any created audio contexts/streams.
• On disconnect/unpublish:
// example cleanup steps
room.localParticipant.unpublishTrack(publishedTrack);
publishedTrack.stop(); // if available
stream.getTracks().forEach((t) => t.stop());
await room.disconnect();
• Also remove event listeners and close any audio contexts.
Best practices summary
• Publish only after connect succeeds to avoid races.
• Grant minimal permissions in the JWT for each participant.
• Stop MediaStream tracks and unpublish before disconnecting to avoid leaks.
• Use `adaptiveStream`/`dynacast` to optimize bandwidth for mobile clients.
Do you want a full Flutter example file (including token fetch, error handling, and UI hooks), or a server-side snippet to generate the JWT for your self-hosted instance?
Sources: Flutter quickstart | LiveKit docs | Camera & microphone | LiveKit docs | Subscribing to tracks | LiveKit docs | Generating tokens | LiveKit docs | LiveKit CLI Setup | LiveKit Docsflaky-jelly-17724
12/19/2025, 5:33 AM/// Initialize and connect to the room
Future<void> initializeRoom(String url, String token) async {
if (_meetingState == MeetingState.connected) {
fw.debugPrint("initializeRoom ignored: already connecting/connected");
return;
}
if (_roomOperationInProgress) {
fw.debugPrint("Room operation already in progress");
return;
}
_roomOperationInProgress = true;
_room = Room(
roomOptions: RoomOptions(
adaptiveStream: true,
dynacast: true,
defaultVideoPublishOptions: VideoPublishOptions(simulcast: true),
),
);
_meetingState = MeetingState.connecting;
_notifyImmediate();
try {
await _room!.connect(
url,
token,
// connectOptions: const ConnectOptions(
// timeouts: Timeouts(
// connection: Duration(seconds: 30),
// peerConnection: Duration(seconds: 30),
// iceRestart: Duration(seconds: 20),
// publish: Duration(seconds: 20),
// debounce: Duration(seconds: 5),
// ),
// ),
);
await Future.delayed(const Duration(seconds: 1));
// Publish camera & mic
await _publishInitialTracks();
await Future.delayed(const Duration(seconds: 1));
// Setup listeners
_setupRoomListeners();
_setupEventListeners();
//reset before sync event
await resetCtrl();
MeetingHelper.fireSyncEvent('', MeetingEventType.initialSync);
_meetingState = MeetingState.connected;
_notifyImmediate();
} catch (e, s) {
await Logger.logException(e, s);
fw.debugPrint(
'exception in join meeting ${e.toString()}, ${s.toString()}',
);
_meetingState = MeetingState.error;
_notifyImmediate();
String msg =
e.toString().contains("expired")
? "Meeting link has expired. Please request a new one."
: "Unable to join meeting. Please try again.";
UserFeedback.error(msg);
await disposeRoom();
} finally {
_roomOperationInProgress = false;
}
}
i have implemented this. pls reviewtall-airline-16756
12/19/2025, 5:34 AMawait Future.delayed(const Duration(seconds: 1)); before/after connect and before setting up listeners. Publish tracks immediately after connect() completes or wait for room-ready events to avoid added latency and race conditions.
2) Attach listeners earlier
• Call _setupRoomListeners() (and any other event subscriptions) right after creating the Room() instance — before connect() — so you don’t miss early events (e.g., trackPublished, participant updates).
3) Publish after ensuring permissions
• Ensure camera/microphone permissions are granted before `_publishInitialTracks()`; otherwise publishing may fail. Request/check permissions (e.g., permission_handler) prior to enabling tracks.
4) Consider connect timeouts
• Re-enable ConnectOptions.timeouts with reasonable values to avoid indefinite waits on poor networks:
◦ connection/peerConnection: ~20–30s
◦ publish: ~10–20s
◦ debounce: a few seconds
5) Keep references to published tracks & clean them up
• Store returned PublishedTrack (or local track references) from _publishInitialTracks() so you can reliably unpublish and stop() them in disposeRoom().
6) Ensure _roomOperationInProgress and state transitions are robust
• You correctly guard against concurrent ops. Also set _meetingState to error/connected as you already do, but ensure any UI consumers react to changes (notify listeners) after final state set.
7) Improve error differentiation (optional)
• Distinguish token/auth errors vs. transient network errors. For transient errors consider retry/backoff; for token errors, prompt re-auth.
8) Cleanup ordering on failure
• In your catch branch you call `disposeRoom()`—ensure dispose unpublishes/stops tracks, removes listeners, and nulls _room before returning so subsequent reconnect attempts start clean.
9) Simulcast/dynacast/sizing notes
• Your RoomOptions are good. If you need finer control, specify custom simulcast layers or tune adaptive behavior for mobile devices.
If you want, I can:
• Provide a revised version of this function with those changes applied (moving listeners, removing delays, adding timeouts and permission checks).
• Show a safe _publishInitialTracks() pattern that returns track references for cleanup.
Which would you prefer?
Sources: Bringing Zoom&#39;s end-to-end optimizations to WebRTC | Codecs and more | LiveKit docs | Configuring the Client SDK for Optimal Video Quality | Connecting to LiveKit | LiveKit docs | client-sdk-flutter | livekit/livekit_composite | DeepWikiflaky-jelly-17724
12/19/2025, 5:36 AMtall-airline-16756
12/19/2025, 5:36 AMinitializeRoom function with the suggested improvements applied:
• listeners attached early (before connect)
• no fixed delays; publishes after connect success
• permission checks before publishing
• sensible connect timeouts
• returns published track references for cleanup
• robust error handling and cleanup ordering
• preserves your state-locking logic
If you want, I can also return a matching disposeRoom() or _publishInitialTracks() helper implementation.
Future<void> initializeRoom(String url, String token) async {
if (_meetingState == MeetingState.connected) {
fw.debugPrint("initializeRoom ignored: already connecting/connected");
return;
}
if (_roomOperationInProgress) {
fw.debugPrint("Room operation already in progress");
return;
}
_roomOperationInProgress = true;
_meetingState = MeetingState.connecting;
_notifyImmediate();
// Create room and attach listeners immediately to avoid missing early events
_room = Room(
roomOptions: RoomOptions(
adaptiveStream: true,
dynacast: true,
defaultVideoPublishOptions: VideoPublishOptions(simulcast: true),
),
);
_setupRoomListeners(); // attach listeners as soon as _room exists
_setupEventListeners();
try {
// Optional: check/request permissions before connecting (recommended UX)
final hasPerm = await _ensureMediaPermissions();
if (!hasPerm) {
throw Exception('Missing camera/microphone permissions');
}
// Connect with reasonable timeouts to avoid indefinite hangs
await _room!.connect(
url,
token,
connectOptions: ConnectOptions(
timeouts: Timeouts(
connection: Duration(seconds: 30),
peerConnection: Duration(seconds: 30),
publish: Duration(seconds: 20),
debounce: Duration(seconds: 5),
),
),
);
// Publish immediately after connect completes
// _publishInitialTracks should return references to published tracks/streams for later cleanup
final published = await _publishInitialTracks();
// store published references for cleanup
_publishedTracks = published;
// reset and fire initial sync after room is connected and tracks published
await resetCtrl();
MeetingHelper.fireSyncEvent('', MeetingEventType.initialSync);
_meetingState = MeetingState.connected;
_notifyImmediate();
} catch (e, s) {
await Logger.logException(e, s);
fw.debugPrint('exception in join meeting ${e.toString()}, ${s.toString()}');
_meetingState = MeetingState.error;
_notifyImmediate();
final msg = e.toString().contains("expired")
? "Meeting link has expired. Please request a new one."
: "Unable to join meeting. Please try again.";
UserFeedback.error(msg);
// Ensure cleanup runs and leaves the app in a consistent state
await disposeRoom();
} finally {
_roomOperationInProgress = false;
}
}
// Example helper to ensure permissions (adjust to your permission package)
Future<bool> _ensureMediaPermissions() async {
try {
// using permission_handler as an example
// import 'package:permission_handler/permission_handler.dart';
final micStatus = await Permission.microphone.request();
final camStatus = await Permission.camera.request();
return micStatus.isGranted && camStatus.isGranted;
} catch (e) {
fw.debugPrint('permission check error: $e');
return false;
}
}
// Example signature for _publishInitialTracks
// Implement this to create MediaStream tracks (camera/mic) and publish them,
// returning a list of objects you can use to unpublish/stop later.
Future<List<PublishedTrackInfo>> _publishInitialTracks() async {
// Your implementation should:
// 1. create/get MediaStream tracks (camera + mic)
// 2. call room.localParticipant.publishTrack(...) for each
// 3. return references (e.g., PublishedTrack and underlying MediaStream)
//
// Placeholder:
return [];
}
// Placeholder model for storing published track info (replace with real types)
class PublishedTrackInfo {
final dynamic publishedTrack;
final MediaStream? stream;
PublishedTrackInfo({this.publishedTrack, this.stream});
}
Would you like me to:
• Provide a concrete _publishInitialTracks() implementation for Flutter (including camera/mic capture and publish calls)?
• Provide a disposeRoom() implementation that reliably unpublishes/stops tracks and removes listeners?flaky-jelly-17724
12/19/2025, 5:43 AMtall-airline-16756
12/19/2025, 5:44 AM_publishInitialTracks() and disposeRoom() that fit your revised initializeRoom flow. They:
• Capture camera + microphone using mediaDevices via flutter_webrtc APIs (commonly used with LiveKit Flutter).
• Publish tracks via room.localParticipant.publishTrack.
• Store published track + underlying MediaStream for reliable cleanup.
• Remove listeners, unpublish/stop tracks, close resources, and disconnect the room.
Adjust imports and types to match your project and SDK versions.
// Required imports (adjust based on your project)
import 'package:livekit_client/livekit_client.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'package:permission_handler/permission_handler.dart';
// Model to keep published track info for cleanup
class PublishedTrackInfo {
final RemoteTrackPublication? publication; // or PublishedTrack if available
final MediaStream? stream;
final LocalAudioTrack? localAudioTrack; // optional typed refs
final LocalVideoTrack? localVideoTrack;
PublishedTrackInfo({
this.publication,
this.stream,
this.localAudioTrack,
this.localVideoTrack,
});
}
// Store published tracks on the class
List<PublishedTrackInfo> _publishedTracks = [];
// Example _publishInitialTracks implementation
Future<List<PublishedTrackInfo>> _publishInitialTracks() async {
final result = <PublishedTrackInfo>[];
if (_room == null) {
throw Exception('Room is not initialized');
}
final participant = _room!.localParticipant;
// Ensure permissions (already checked in initializeRoom, but double-check)
final micPerm = await Permission.microphone.status;
final camPerm = await Permission.camera.status;
if (!micPerm.isGranted || !camPerm.isGranted) {
final micReq = await Permission.microphone.request();
final camReq = await Permission.camera.request();
if (!micReq.isGranted || !camReq.isGranted) {
throw Exception('Permissions not granted for camera/microphone');
}
}
try {
// 1) Get local media stream (camera + mic)
final Map<String, dynamic> mediaConstraints = {
'audio': true,
'video': {
'facingMode': 'user',
'width': {'ideal': 1280},
'height': {'ideal': 720},
'frameRate': {'ideal': 30},
},
};
final MediaStream localStream =
await navigator.mediaDevices.getUserMedia(mediaConstraints);
// 2) Extract tracks
final audioTracks = localStream.getAudioTracks();
final videoTracks = localStream.getVideoTracks();
// 3) Publish audio track (if present)
if (audioTracks.isNotEmpty) {
final audioTrack = audioTracks.first;
// publishTrack returns a PublishedTrack / RemoteTrackPublication depending on SDK version
final pub = await participant.publishTrack(
audioTrack,
// optional publish options
PublishOptions(
name: 'microphone',
source: TrackSource.microphone,
),
);
// If your SDK returns a typed LocalAudioTrack, capture it as well (nullable)
LocalAudioTrack? localAudio;
try {
localAudio = pub.track as LocalAudioTrack?;
} catch (_) {
localAudio = null;
}
result.add(PublishedTrackInfo(
publication: pub,
stream: localStream,
localAudioTrack: localAudio,
));
}
// 4) Publish video track (if present)
if (videoTracks.isNotEmpty) {
final videoTrack = videoTracks.first;
final pub = await participant.publishTrack(
videoTrack,
PublishOptions(
name: 'camera',
source: TrackSource.camera,
// You can set simulcast layers here if SDK supports per-track options
),
);
LocalVideoTrack? localVideo;
try {
localVideo = pub.track as LocalVideoTrack?;
} catch (_) {
localVideo = null;
}
result.add(PublishedTrackInfo(
publication: pub,
stream: localStream,
localVideoTrack: localVideo,
));
}
// Keep references for cleanup
_publishedTracks = result;
return result;
} catch (e) {
// If publish fails, attempt to stop any created tracks to avoid leaks
try {
for (var t in _publishedTracks) {
t.stream?.getTracks().forEach((tr) {
try {
tr.stop();
} catch (_) {}
});
}
} catch (_) {}
rethrow;
}
}
// Example disposeRoom implementation
Future<void> disposeRoom() async {
// Prevent re-entrancy
if (_roomOperationInProgress) {
fw.debugPrint('disposeRoom ignored: operation in progress');
return;
}
_roomOperationInProgress = true;
try {
// 1) Unpublish and stop published tracks
try {
if (_room != null && _room!.localParticipant != null) {
final participant = _room!.localParticipant;
// Unpublish by using stored publications if available
for (final info in _publishedTracks) {
try {
if (info.publication != null) {
await participant.unpublishTrack(info.publication!);
}
} catch (e) {
fw.debugPrint('unpublish error: $e');
}
// Stop underlying MediaStream tracks
try {
info.stream?.getTracks().forEach((t) {
try {
t.stop();
} catch (_) {}
});
} catch (_) {}
}
// Clear local list
_publishedTracks.clear();
}
} catch (e) {
fw.debugPrint('error while unpublishing/stopping tracks: $e');
}
// 2) Remove listeners (implement these to detach callbacks you set earlier)
try {
_removeRoomListeners();
_removeEventListeners();
} catch (e) {
fw.debugPrint('error while removing listeners: $e');
}
// 3) Disconnect room
try {
if (_room != null) {
await _room!.disconnect();
}
} catch (e) {
fw.debugPrint('error while disconnecting room: $e');
}
} finally {
// Null out room and reset state
try {
_room = null;
} catch (_) {}
_meetingState = MeetingState.disconnected;
_notifyImmediate();
_roomOperationInProgress = false;
}
}
// Placeholder listener removal helpers - implement to reverse _setupRoomListeners/_setupEventListeners
void _removeRoomListeners() {
// Example:
// _room?.removeListener(_onRoomEvent); // replace with actual removal calls
}
void _removeEventListeners() {
// Remove any other global or widget listeners you added
}
Notes and adjustments:
• Depending on your LiveKit Flutter SDK version, publishTrack may return different types (a publication or a track object). I used a nullable publication field; adapt types (e.g., RemoteTrackPublication, LocalTrackPublication, or PublishedTrack) to match your SDK.
• If your project already uses another media capture mechanism (e.g., LiveKit's own helpers), replace flutter_webrtc capture calls with that API.
• Ensure you implement _removeRoomListeners() and _removeEventListeners() to detach all callbacks you registered in _setupRoomListeners() and _setupEventListeners().
• If you enable simulcast per-track, pass simulcast/publish options in PublishOptions.
• Always test on device for permissions and camera selection behavior.
Would you like me to adapt these samples to your exact SDK types (share your LiveKit Flutter SDK version) or produce a combined file including initializeRoom, _publishInitialTracks, and disposeRoom wired together?flaky-jelly-17724
12/19/2025, 5:47 AM/// Publish initial camera & microphone tracks
Future<void> _publishInitialTracks() async {
try {
final lp = _room!.localParticipant!;
if (_enableVideo) {
final cameraTrack = await LocalVideoTrack.createCameraTrack(
CameraCaptureOptions(deviceId: _selectedVideoDevice?.deviceId),
);
await lp.publishVideoTrack(cameraTrack);
await lp.setCameraEnabled(true);
}
if (_enableAudio) {
final micTrack = await LocalAudioTrack.create(
AudioCaptureOptions(
deviceId: _selectedAudioInputDevice?.deviceId,
noiseSuppression: false,
echoCancellation: false,
autoGainControl: false,
highPassFilter: false,
voiceIsolation: false,
),
);
final publishOptions = AudioPublishOptions(
dtx: false,
red: false,
audioBitrate: 320000,
);
await lp.publishAudioTrack(micTrack, publishOptions: publishOptions);
await lp.setMicrophoneEnabled(enableAudio);
}
_notify();
} catch (e, st) {
await Logger.logException(e, st);
}
}
i had implemented thistall-airline-16756
12/19/2025, 5:47 AMsetCameraEnabled(true) / setMicrophoneEnabled(true) only after successful publish.
• Handle SDK return types and errors explicitly so you can cleanup partially-published resources.
• Avoid silent failures: surface errors to UI or logs.
Revised version (keeps your APIs: publishAudioTrack, publishVideoTrack, LocalAudioTrack.create, LocalVideoTrack.createCameraTrack):
Future<void> _publishInitialTracks() async {
if (_room == null) return;
final lp = _room!.localParticipant!;
final published = <PublishedTrackInfo>[];
try {
// Ensure permissions
final micStatus = await Permission.microphone.request();
final camStatus = await Permission.camera.request();
if (_enableAudio && !micStatus.isGranted) {
throw Exception('Microphone permission denied');
}
if (_enableVideo && !camStatus.isGranted) {
throw Exception('Camera permission denied');
}
// Video
if (_enableVideo) {
final cameraTrack = await LocalVideoTrack.createCameraTrack(
CameraCaptureOptions(deviceId: _selectedVideoDevice?.deviceId),
);
// publish and keep reference
final videoPub = await lp.publishVideoTrack(cameraTrack);
published.add(PublishedTrackInfo(
publishedTrack: videoPub,
localVideoTrack: cameraTrack,
));
await lp.setCameraEnabled(true);
}
// Audio
if (_enableAudio) {
final micTrack = await LocalAudioTrack.create(
AudioCaptureOptions(
deviceId: _selectedAudioInputDevice?.deviceId,
noiseSuppression: false,
echoCancellation: false,
autoGainControl: false,
highPassFilter: false,
voiceIsolation: false,
),
);
final publishOptions = AudioPublishOptions(
dtx: false,
red: false,
audioBitrate: 320000,
);
final audioPub = await lp.publishAudioTrack(micTrack, publishOptions: publishOptions);
published.add(PublishedTrackInfo(
publishedTrack: audioPub,
localAudioTrack: micTrack,
));
await lp.setMicrophoneEnabled(true);
}
// store for cleanup
_publishedTracks = published;
_notify();
} catch (e, st) {
await Logger.logException(e, st);
// cleanup any partially-created tracks
for (final info in published) {
try {
if (info.publishedTrack != null) {
await lp.unpublishTrack(info.publishedTrack);
}
} catch (_) {}
try {
info.localAudioTrack?.stop();
} catch (_) {}
try {
info.localVideoTrack?.stop();
} catch (_) {}
}
rethrow;
}
}
Also ensure your PublishedTrackInfo holds the types you need:
class PublishedTrackInfo {
final dynamic publishedTrack;
final LocalAudioTrack? localAudioTrack;
final LocalVideoTrack? localVideoTrack;
PublishedTrackInfo({this.publishedTrack, this.localAudioTrack, this.localVideoTrack});
}
If you want, I can now:
• Provide a matching disposeRoom() that unpublishes and stops these tracks and removes listeners, or
• Adapt the code to specific SDK types if you share your LiveKit Flutter SDK version. Which next step would you like?