How to initialize a room with publishing resources...
# ask-ai
f
How to initialize a room with publishing resources in a clean way?
t
Thanks for reaching out, hardeep. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I’ll walk you through a clean, minimal pattern to initialize a LiveKit room with publishing resources and proper cleanup. Which platform are you using (browser JS, React, React-Native, or native mobile)? If browser/React, I’ll give concrete code; otherwise I’ll adapt the steps. Quick overview (browser / JS): 1) Create a server token with publish grants • Issue an AccessToken on your server that grants roomJoin and publish rights:
Copy code
const 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:
Copy code
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:
Copy code
// 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:
Copy code
// 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 WebAudio
f
im using self hosted livkit with flutter sdk
t
Thanks — I’ll give a clean, Flutter-specific pattern for initializing a self-hosted LiveKit room, publishing resources, and handling cleanup. I’ll assume you already generate a JWT on your server and have your self-hosted URL ready. If you want that server-side token snippet, tell me. 1) Add SDK
Copy code
flutter 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).
Copy code
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.
Copy code
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:
Copy code
// 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 &amp; microphone | LiveKit docs | Subscribing to tracks | LiveKit docs | Generating tokens | LiveKit docs | LiveKit CLI Setup | LiveKit Docs
f
Copy code
/// 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 review
t
Thanks — this is a solid implementation. I reviewed your function and list the concise, actionable suggestions below, tied to the exact spots in your code. Quick acknowledgement: I’m responding to your initialize/connect flow, publish timing, listener setup, error handling, and cleanup. 1) Remove fixed delays • Don’t use
await 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&amp;#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 | DeepWiki
f
provide a revised version
t
Thanks — below is a revised, self-contained version of your
initializeRoom
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.
Copy code
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?
f
sure. lets have a look on both implementations publishInitialTracks() and disposeRoom()
t
Below are concrete, self-contained Flutter implementations for
_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.
Copy code
// 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?
f
Copy code
/// 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 this
t
Nice — this implementation is mostly good. Below are concise, practical suggestions and a slightly revised version that addresses cleanup, permission checks, error handling, and consistent state updates. Key issues to address • Ensure permissions are granted before creating tracks. • Keep references to created LocalAudioTrack/LocalVideoTrack so you can unpublish and stop them in dispose. • Use
setCameraEnabled(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
):
Copy code
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:
Copy code
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?