Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fix_for_dynacast_error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
livekit: patch
livekit-ffi: patch
---

Fix for dynacast error - #1213 (@MaxHeimbrock)
23 changes: 22 additions & 1 deletion livekit/src/room/participant/local_participant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,26 @@ impl LocalParticipant {
&self,
track: LocalTrack,
options: TrackPublishOptions,
) -> RoomResult<LocalTrackPublication> {
self.publish_track_with_video_send_encodings(track, options, None).await
}

/// Publishes a track without providing video send encodings to WebRTC.
#[doc(hidden)]
#[cfg(feature = "__lk-e2e-test")]
pub async fn publish_track_without_video_send_encodings(
&self,
track: LocalTrack,
options: TrackPublishOptions,
) -> RoomResult<LocalTrackPublication> {
self.publish_track_with_video_send_encodings(track, options, Some(Vec::new())).await
}

async fn publish_track_with_video_send_encodings(
&self,
track: LocalTrack,
options: TrackPublishOptions,
video_send_encodings: Option<Vec<RtpEncodingParameters>>,
) -> RoomResult<LocalTrackPublication> {
let disable_red = self.local.encryption_type != EncryptionType::None || !options.red;

Expand Down Expand Up @@ -388,7 +408,8 @@ impl LocalParticipant {
req.width = resolution.width;
req.height = resolution.height;

encodings = compute_video_encodings(req.width, req.height, &options);
encodings = video_send_encodings
.unwrap_or_else(|| compute_video_encodings(req.width, req.height, &options));
req.layers = video_layers_from_encodings(req.width, req.height, &encodings);

// Populate simulcast_codecs so the server knows this track has
Expand Down
42 changes: 30 additions & 12 deletions livekit/src/room/track/local_video_track.rs
Original file line number Diff line number Diff line change
Expand Up @@ -370,17 +370,19 @@ impl LocalVideoTrack {

let mut changed = false;
for encoding in &mut params.encodings {
let quality = crate::options::video_quality_for_rid_or_default(&encoding.rid);
// The SFU addresses layers by spatial index (0 = Low), so a
// single rid-less encoding is addressed as Low, not High.
let rid = if encoding.rid.is_empty() { "q" } else { encoding.rid.as_str() };
let quality = crate::options::video_quality_for_rid_or_default(rid);

let should_active = qualities
.iter()
.find(|q| q.quality == quality as i32)
.map(|q| q.enabled)
.unwrap_or(false);
// A quality missing from the update is left untouched.
let Some(subscribed) = qualities.iter().find(|q| q.quality == quality as i32) else {
continue;
};

if encoding.active != should_active {
if encoding.active != subscribed.enabled {
changed = true;
encoding.active = should_active;
encoding.active = subscribed.enabled;
}
}

Expand All @@ -394,16 +396,32 @@ impl LocalVideoTrack {
})
.collect();

sender
.set_parameters(params)
.map_err(|e| RoomError::Internal(format!("failed to set sender parameters: {}", e)))?;

if changed {
sender.set_parameters(params).map_err(|e| {
RoomError::Internal(format!("failed to set sender parameters: {}", e))
})?;
log::debug!("dynacast: layers changed -> [{}]", layers.join(", "));
} else {
log::debug!("dynacast: layers unchanged [{}]", layers.join(", "));
}

Ok(())
}

/// Applies publishing-layer quality state for end-to-end tests.
#[doc(hidden)]
#[cfg(feature = "__lk-e2e-test")]
pub fn set_publishing_layers_for_test(
&self,
qualities: &[(PublishingLayerQuality, bool)],
) -> RoomResult<()> {
let qualities: Vec<proto::SubscribedQuality> = qualities
.iter()
.map(|(quality, enabled)| proto::SubscribedQuality {
quality: proto::VideoQuality::from(*quality) as i32,
enabled: *enabled,
})
.collect();
self.set_publishing_layers(&qualities)
}
}
45 changes: 41 additions & 4 deletions livekit/tests/common/e2e/video.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,46 @@ impl SolidColorTrack {
}

pub async fn publish(&mut self, codec: VideoCodec, simulcast: bool) -> RoomResult<()> {
self.publish_with_options(TrackPublishOptions {
video_codec: codec,
simulcast,
..Default::default()
})
.await
}

#[cfg(feature = "__lk-e2e-test")]
pub async fn publish_without_video_send_encodings(
&mut self,
codec: VideoCodec,
) -> RoomResult<()> {
let options =
TrackPublishOptions { video_codec: codec, simulcast: false, ..Default::default() };
self.publish_with_options_without_video_send_encodings(options).await
}

async fn publish_with_options(&mut self, options: TrackPublishOptions) -> RoomResult<()> {
let (close_tx, close_rx) = oneshot::channel();
let track = LocalVideoTrack::create_video_track(
"solid-color-track",
RtcVideoSource::Native(self.rtc_source.clone()),
);
let task =
tokio::spawn(Self::track_task(close_rx, self.rtc_source.clone(), self.params.clone()));
self.room
.local_participant()
.publish_track(LocalTrack::Video(track.clone()), options)
.await?;
let handle = TrackHandle { close_tx, track, task };
self.handle = Some(handle);
Ok(())
}

#[cfg(feature = "__lk-e2e-test")]
async fn publish_with_options_without_video_send_encodings(
&mut self,
options: TrackPublishOptions,
) -> RoomResult<()> {
let (close_tx, close_rx) = oneshot::channel();
let track = LocalVideoTrack::create_video_track(
"solid-color-track",
Expand All @@ -72,10 +112,7 @@ impl SolidColorTrack {
tokio::spawn(Self::track_task(close_rx, self.rtc_source.clone(), self.params.clone()));
self.room
.local_participant()
.publish_track(
LocalTrack::Video(track.clone()),
TrackPublishOptions { video_codec: codec, simulcast, ..Default::default() },
)
.publish_track_without_video_send_encodings(LocalTrack::Video(track.clone()), options)
.await?;
let handle = TrackHandle { close_tx, track, task };
self.handle = Some(handle);
Expand Down
111 changes: 111 additions & 0 deletions livekit/tests/dynacast_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,117 @@ async fn test_dynacast() -> Result<()> {
Ok(())
}

/// Verifies dynacast with a publisher that does not provide RTP send encodings.
///
/// WebRTC creates a single default encoding without a RID in this case. The SFU
/// addresses that layer as low quality, so the publisher should still be able
/// to pause and resume it from low-quality `SubscribedQualityUpdate` messages.
#[cfg(feature = "__lk-e2e-test")]
#[test_log::test(tokio::test)]
async fn test_dynacast_without_video_send_encodings() -> Result<()> {
let mut pub_room_opts = RoomOptions::default();
pub_room_opts.dynacast = true;
let pub_options = TestRoomOptions { room: pub_room_opts, ..Default::default() };
let sub_options = TestRoomOptions::default();

let mut rooms = test_rooms_with_options([pub_options, sub_options]).await?;
let (pub_room, _pub_events) = rooms.remove(0);
let (_sub_room, mut sub_events) = rooms.remove(0);

let pub_room = Arc::new(pub_room);
let solid_params = SolidColorParams { width: 1280, height: 720, luma: 128 };
let mut solid_track = SolidColorTrack::new(pub_room.clone(), solid_params);
solid_track.publish_without_video_send_encodings(VideoCodec::VP8).await?;

let sub_publication: RemoteTrackPublication = timeout(Duration::from_secs(15), async {
loop {
let Some(event) = sub_events.recv().await else {
return Err(anyhow!("Event channel closed before TrackSubscribed"));
};
if let RoomEvent::TrackSubscribed { publication, .. } = event {
return Ok(publication);
}
}
})
.await??;

let pub_video_track = publisher_video_track(&pub_room)?;

let layers = wait_for_layers(
&pub_video_track,
"no encodings baseline",
Duration::from_secs(15),
|layers| layers.len() == 1 && layers[0].rid.is_empty() && layers[0].active,
)
.await?;
assert_eq!(layers.len(), 1, "expected one default layer, got {:?}", layers);
assert!(
layers[0].rid.is_empty(),
"expected WebRTC-created default encoding to have no RID, got {:?}",
layers
);

drop(sub_publication);

log::info!("dynacast no-encodings test: applying LOW=false quality update");
pub_video_track
.set_publishing_layers_for_test(&[(PublishingLayerQuality::Low, false)])
.map_err(|e| anyhow!("failed to disable rid-less layer: {}", e))?;
let layers = pub_video_track.publishing_layers();
assert!(
layers.len() == 1 && layers[0].rid.is_empty() && !layers[0].active,
"expected default layer to be inactive after LOW=false update, got {:?}",
layers
);

log::info!("dynacast no-encodings test: applying LOW=true quality update");
pub_video_track
.set_publishing_layers_for_test(&[(PublishingLayerQuality::Low, true)])
.map_err(|e| anyhow!("failed to enable rid-less layer: {}", e))?;
let layers = pub_video_track.publishing_layers();
assert!(
layers.len() == 1 && layers[0].rid.is_empty() && layers[0].active,
"expected default layer to be active after LOW=true update, got {:?}",
layers
);

log::info!("dynacast no-encodings test: applying HIGH=false quality update");
pub_video_track
.set_publishing_layers_for_test(&[(PublishingLayerQuality::High, false)])
.map_err(|e| anyhow!("failed to apply unrelated high quality update: {}", e))?;
let layers = pub_video_track.publishing_layers();
assert!(
layers.len() == 1 && layers[0].rid.is_empty() && layers[0].active,
"expected default layer to ignore HIGH-only update, got {:?}",
layers
);

log::info!("dynacast no-encodings test: applying LOW=false quality update again");
pub_video_track
.set_publishing_layers_for_test(&[(PublishingLayerQuality::Low, false)])
.map_err(|e| anyhow!("failed to disable rid-less layer: {}", e))?;
let layers = pub_video_track.publishing_layers();
assert!(
layers.len() == 1 && layers[0].rid.is_empty() && !layers[0].active,
"expected default layer to be inactive after second LOW=false update, got {:?}",
layers
);

pub_video_track
.set_publishing_layers_for_test(&[(PublishingLayerQuality::Low, true)])
.map_err(|e| anyhow!("failed to re-enable rid-less layer: {}", e))?;
let layers = pub_video_track.publishing_layers();
assert!(
layers.len() == 1 && layers[0].rid.is_empty() && layers[0].active,
"expected default layer to be active before cleanup, got {:?}",
layers
);

solid_track.unpublish().await?;

Ok(())
}

/// Verifies that dynacast only publishes video tracks requested by subscribers.
///
/// A single publisher publishes three simulcast VP8 video tracks while two subscribers manually
Expand Down
Loading