Skip to content

Windows: each video publish_track retains ~40MB until Room close (unpublish does not free it) #1277

Description

@smaschin

Summary

On Windows, every publish_track of a video track retains roughly 40 MB of private memory for the lifetime of the Room. unpublish_track does not release it; only closing/dropping the Room does. An application that starts and stops screen share repeatedly within one session accumulates ~40 MB per start.

Audio track publishes do not leak. Creating the NativeVideoSource + LocalVideoTrack and feeding frames without publishing does not leak either — the retention happens specifically in the publish path.

Environment

  • Windows 11 (10.0.26200), x64
  • rust-sdks master @ 0067e100 (also reproduced on 83fa5f2b)
  • livekit with native-tls, NativeVideoSource 1280×720, is_screencast: true
  • Server: local livekit-server --dev

Reproduction

One Room connection; per cycle: create source + track, publish, feed 30 synthetic I420 frames at ~15 fps, unpublish, drop everything, sleep, measure process memory (K32GetProcessMemoryInfo).

let (room, _events) = Room::connect(URL, &token, RoomOptions::default()).await?;
for i in 1..=5 {
    let source = NativeVideoSource::new(VideoResolution { width: 1280, height: 720 }, true);
    let track = LocalVideoTrack::create_video_track("t", RtcVideoSource::Native(source.clone()));
    let publication = room.local_participant().publish_track(
        LocalTrack::Video(track),
        TrackPublishOptions {
            source: TrackSource::Screenshare,
            video_encoding: Some(VideoEncoding { max_bitrate: 4_000_000, max_framerate: 15.0 }),
            simulcast: false,
            ..Default::default()
        },
    ).await?;
    for f in 0..30 {
        let buffer = I420Buffer::new(1280, 720);
        source.capture_frame(&VideoFrame {
            rotation: VideoRotation::VideoRotation0,
            timestamp_us: f * 66_000,
            frame_metadata: None,
            buffer,
        });
        tokio::time::sleep(Duration::from_millis(66)).await;
    }
    room.local_participant().unpublish_track(&publication.sid()).await?;
    drop(source);
    tokio::time::sleep(Duration::from_millis(1000)).await;
    // measure working set + private bytes here
}

Measurements

Private bytes after each cycle (baseline 9.1 MB, one Room, 5 publish/unpublish cycles):

cycle 1: +41.4 MB
cycle 2: +81.7 MB
cycle 3: +122.2 MB
cycle 4: +162.4 MB
cycle 5: +202.6 MB

Variations tried, all with the same result unless noted:

  • Skipping unpublish_track (publish, then just drop the source/publication): identical growth — unpublish is not the trigger, publish is.
  • Feeding 15 vs 60 frames per cycle: identical growth (~41 MB either way) — the retention is a fixed-size per-publish allocation, not per-frame.
  • Creating source + track + feeding frames but never publishing: flat (+0.1 MB per cycle).
  • Audio (NativeAudioSource → publish/unpublish per session): flat.
  • Closing the Room releases the memory — reconnect-per-cycle tests do not accumulate.

Impact / workaround

Long-lived voice sessions with repeated screen-share start/stop grow unboundedly. We are working around it by publishing the screen-share track once per Room and muting/reusing it across share sessions instead of unpublishing, which avoids the accumulation but keeps one ~40 MB allocation live per Room.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions