Skip to content
Merged
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
17 changes: 9 additions & 8 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,13 +124,14 @@ its audio for the next write. A track at 48 kHz holds no audio.
`write_pcm` after a negotiated PeerConnection can send the track. Until then,
the queue keeps the written audio. Before, the pacer sent that audio to a track
with no sender, and the audio was lost. With `with_pace(false)`, `write_pcm`
only queues until `LocalAudioTrack::start_pacing`; `pause_pacing` stops pacing
and keeps the queue. A `pause_pacing` before the first `write_pcm` stays in
force until `start_pacing`. `Call::publish_audio` holds the queue of a new
publication until the SFU publisher first connects, so the audio written
before the publish or the connect is not lost.
Pacing does not pause on a later disconnect: audio written during an outage is
lost, and a reconnect adds no delay.
only queues until `LocalAudioTrack::start_pacing` or until the first SFU
publisher connect of `Call::publish_audio`. `pause_pacing` stops pacing and
keeps the queue. A `pause_pacing` stays in force until `start_pacing`; the
first `write_pcm` and the first SFU publisher connect do not end it.
`Call::publish_audio` holds the queue of a new publication until the SFU
publisher first connects, so the audio written before the publish or the
connect is not lost. Pacing does not pause on a later disconnect: audio
written during an outage is lost, and a reconnect adds no delay.

### A stopped track can be replaced by a new track

Expand All @@ -142,7 +143,7 @@ timestamps of the stopped track, also when one of the two tracks forwards RTP
with `write_rtp`. Before, a second video publish failed with
"participant not found". A simulcast track (`vp8_simulcast`) cannot replace or
be replaced, and a track cannot replace a track with different layers (for
example `vp9_svc` and `vp9`): `publish` returns `RtcError::SimulcastReplace`.
example `vp9_svc` and `vp9`): `publish` returns `RtcError::LayeredReplace`.
Turn such a track off and on with `mute_track` and `unmute_track`. If a
replace fails, the next publish can use the sender again.

Expand Down
2 changes: 1 addition & 1 deletion src/rtc/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ impl RtcCall {
/// Stop publishing a local media track. A new track of the same kind that
/// is published later takes over its sender, except for a simulcast track
/// or a track with different layers than the stopped track
/// ([`RtcError::SimulcastReplace`]); use [`mute_track`](Self::mute_track)
/// ([`RtcError::LayeredReplace`]); use [`mute_track`](Self::mute_track)
/// for that.
pub async fn stop_publish(&self, track: LocalTrack) -> Result<()> {
self.core.stop_publish(track).await
Expand Down
2 changes: 1 addition & 1 deletion src/rtc/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ pub enum RtcError {
track is simulcast or the tracks have different layers; use mute_track and \
unmute_track"
)]
SimulcastReplace {
LayeredReplace {
/// Publication kind requested by the caller.
track_type: models::TrackType,
},
Expand Down
2 changes: 1 addition & 1 deletion src/rtc/join/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ impl RtcCore {
.retired(track.track_type(), publish_option_id)
.is_some_and(|retired| retired.is_simulcast() || track.is_simulcast())
{
return Err(RtcError::SimulcastReplace {
return Err(RtcError::LayeredReplace {
track_type: track.track_type(),
});
}
Expand Down
66 changes: 64 additions & 2 deletions src/rtc/join/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,68 @@ async fn pcm_written_before_publish_plays_after_the_publisher_connects() {
core.leave("test cleanup").await.expect("leave");
}

#[tokio::test]
async fn a_pause_before_publish_stays_in_force_after_the_publisher_connects() {
let core = test_core();
let generation = prepare_joined_core(&core, "alice");
let (connection, _sfu) = establish_fake(&core, generation).await;
let publisher = connection.publisher.clone();
*core.connection.lock().await = Some(connection);
let audio = LocalAudioTrack::opus().expect("opus track");
let tone_500ms = (0..24_000)
.map(|index| {
let time = f64::from(index) / 48_000.0;
(16_000.0 * (std::f64::consts::TAU * 440.0 * time).sin()) as i16
})
.collect();
audio.pause_pacing();
audio
.write_pcm(PcmFrame::mono(tone_500ms, 48_000))
.await
.expect("write before publish");

publisher
.add_track(audio.webrtc_track())
.await
.expect("add track");
core.media
.lock()
.await
.begin_publish(LocalTrack::Audio(audio.clone()), 0);
let (receiver, mut remote) = peer::connect_audio_receiver(&publisher).await;
assert!(
tokio::time::timeout(Duration::from_millis(300), remote.recv())
.await
.is_err(),
"a paused track sends no audio after the publisher connects"
);
audio.start_pacing().await;
let remote = tokio::time::timeout(Duration::from_secs(5), remote.recv())
.await
.expect("start_pacing starts the paused track")
.expect("remote track");
let mut decoder = opus::Decoder::new(48_000, opus::Channels::Mono).expect("opus decoder");
let mut peak = 0;
for _ in 0..5 {
let (packet, _) = remote.read_rtp().await.expect("paced packet");
let mut decoded = vec![0; 5_760];
let length = decoder
.decode(&packet.payload, &mut decoded, false)
.expect("decode");
peak = decoded[..length]
.iter()
.fold(peak, |peak, sample| peak.max(sample.unsigned_abs()));
}

assert!(
peak > 5_000,
"the first packets carry silence (peak {peak}), not the PCM written while paused"
);
audio.stop();
let _ = receiver.close().await;
core.leave("test cleanup").await.expect("leave");
}

#[tokio::test]
async fn a_failed_publish_does_not_hold_the_track() {
let core = test_core();
Expand Down Expand Up @@ -1192,7 +1254,7 @@ async fn a_stopped_simulcast_publication_is_not_replaced() {
assert!(
matches!(
result,
Err(RtcError::SimulcastReplace {
Err(RtcError::LayeredReplace {
track_type: TrackType::ScreenShare
})
),
Expand Down Expand Up @@ -1323,7 +1385,7 @@ async fn a_stopped_publication_is_not_replaced_by_a_track_with_other_layers() {
assert!(
matches!(
result,
Err(RtcError::SimulcastReplace {
Err(RtcError::LayeredReplace {
track_type: TrackType::Video
})
),
Expand Down
10 changes: 5 additions & 5 deletions src/rtc/peer/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,11 @@ fn register_supported_codecs(media_engine: &mut MediaEngine) -> Result<()> {
/// (NACK, RTCP reports, receiver-side TWCC).
///
/// Known gap versus Pion/videosdk: webrtc-rs ships no publisher-side congestion
/// controller (TWCC *sender* estimator / GCC) and no RTX/NACK retransmission
/// sender. Opus audio is unaffected — low bitrate, loss-tolerant, single layer —
/// but high-bitrate video publishing runs without bandwidth estimation or
/// retransmission. The default interceptor set below is wired as-is rather than
/// worked around.
/// controller (TWCC *sender* estimator / GCC) and no RTX. The default NACK
/// responder sends a lost packet again on the media SSRC. Opus audio is
/// unaffected — low bitrate, loss-tolerant, single layer — but high-bitrate
/// video publishing runs without bandwidth estimation. The default interceptor
/// set below is wired as-is rather than worked around.
fn build_api() -> Result<API> {
let mut media_engine = MediaEngine::default();
register_supported_codecs(&mut media_engine)?;
Expand Down
2 changes: 1 addition & 1 deletion src/rtc/peer/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ pub(crate) async fn replace_retired_track(
}
let option = publish_option(track, publish_options)?;
if track.configure_for_publish(option)? != retired.planned_layers_for_publish(option)? {
return Err(RtcError::SimulcastReplace {
return Err(RtcError::LayeredReplace {
track_type: track.track_type(),
});
}
Expand Down
Loading
Loading