livekit 0.8.1

Rust Client SDK for LiveKit
// Copyright 2025 LiveKit, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#[cfg(feature = "__lk-e2e-test")]
use {
    crate::common::{test_rooms, test_rooms_with_options, TestRoomOptions},
    anyhow::{anyhow, Ok, Result},
    chrono::{TimeDelta, Utc},
    livekit::{
        data_stream::backend::pseudo_random_text, RoomDataStreamOptions, RoomEvent, RoomOptions,
        StreamByteOptions, StreamError, StreamReader, StreamTextOptions,
    },
    rand::{rngs::StdRng, RngCore, SeedableRng},
    std::time::Duration,
    tokio::{time::timeout, try_join},
};

mod common;

#[cfg(feature = "__lk-e2e-test")]
#[tokio::test]
async fn test_send_bytes() -> Result<()> {
    let mut rooms = test_rooms(2).await?;
    let (sending_room, _) = rooms.pop().unwrap();
    let (_, mut receiving_event_rx) = rooms.pop().unwrap();
    let sender_identity = sending_room.local_participant().identity();

    const BYTES_TO_SEND: &[u8] = &[0xFA; 16];

    let send_text = async move {
        let options = StreamByteOptions::new_with_topic("some-topic");
        let stream_info =
            sending_room.local_participant().send_bytes(BYTES_TO_SEND, options).await?;

        assert!(!stream_info.id.is_empty());
        assert!(
            stream_info.timestamp.signed_duration_since(Utc::now()).abs() <= TimeDelta::seconds(1)
        );
        assert!(stream_info.total_length.is_some());
        assert_eq!(stream_info.mime_type, "application/octet-stream");
        assert_eq!(stream_info.topic, "some-topic");
        assert_eq!(stream_info.is_compressed, true);
        assert_eq!(stream_info.is_inline, true);

        Ok(())
    };
    let receive_text = async move {
        while let Some(event) = receiving_event_rx.recv().await {
            let RoomEvent::ByteStreamOpened { reader, topic, participant_identity } = event else {
                continue;
            };
            assert_eq!(topic, "some-topic");
            assert_eq!(participant_identity, sender_identity);

            let Some(reader) = reader.take() else {
                return Err(anyhow!("Failed to take reader"));
            };
            assert_eq!(reader.read_all().await?, BYTES_TO_SEND);
            break;
        }
        Ok(())
    };

    timeout(Duration::from_secs(5), async { try_join!(send_text, receive_text) }).await??;
    Ok(())
}

/// End-to-end round-trip of a large, somewhat-compressible text. Both peers are Rust SDK
/// clients advertising data streams v2 + deflate-raw, so the sender compresses across multiple
/// chunks and the receiver decompresses — validating the v2 compression path on the real wire.
#[cfg(feature = "__lk-e2e-test")]
#[tokio::test]
async fn test_send_large_compressible_text() -> Result<()> {
    let mut rooms = test_rooms(2).await?;
    let (sending_room, _) = rooms.pop().unwrap();
    let (_, mut receiving_event_rx) = rooms.pop().unwrap();

    // ~50 KB of deterministic pseudo-random lowercase: too big to inline, compresses well
    // under its raw size, exercising the chunked-compressed path.
    let text = pseudo_random_text(50_000);
    let expected = text.clone();

    let send = async move {
        let options = StreamTextOptions::new_with_topic("some-topic");
        let stream_info = sending_room.local_participant().send_text(&text, options).await?;
        assert_eq!(stream_info.is_compressed, true);
        assert_eq!(stream_info.is_inline, false);
        Ok(())
    };
    let receive = async move {
        while let Some(event) = receiving_event_rx.recv().await {
            let RoomEvent::TextStreamOpened { reader, topic, .. } = event else {
                continue;
            };
            assert_eq!(topic, "some-topic");
            let reader = reader.take().ok_or_else(|| anyhow!("Failed to take reader"))?;
            assert_eq!(reader.read_all().await?, expected);
            break;
        }
        Ok(())
    };

    timeout(Duration::from_secs(10), async { try_join!(send, receive) }).await??;
    Ok(())
}

/// End-to-end round-trip of a large in-memory byte payload, validating the v2 byte-stream path.
#[cfg(feature = "__lk-e2e-test")]
#[tokio::test]
async fn test_send_large_incompressible_random_bytes() -> Result<()> {
    let mut rooms = test_rooms(2).await?;
    let (sending_room, _) = rooms.pop().unwrap();
    let (_, mut receiving_event_rx) = rooms.pop().unwrap();

    // Uniform random bytes are genuinely incompressible: deflate cannot shrink them, so the
    // send path must choose CompressionType::None. Seeded for a deterministic, reproducible test.
    let mut rng = StdRng::seed_from_u64(0xC0FFEE);
    let mut payload = vec![0u8; 1_800_000];
    rng.fill_bytes(&mut payload);
    let expected = payload.clone();

    let send = async move {
        let options = StreamByteOptions::new_with_topic("some-topic");
        let stream_info = sending_room.local_participant().send_bytes(&payload, options).await?;
        assert_eq!(stream_info.is_compressed, false, "is_compressed was not false");
        assert_eq!(stream_info.is_inline, false, "is_inline was not false");
        Ok(())
    };
    let receive = async move {
        while let Some(event) = receiving_event_rx.recv().await {
            let RoomEvent::ByteStreamOpened { reader, topic, .. } = event else {
                continue;
            };
            assert_eq!(topic, "some-topic");
            let reader = reader.take().ok_or_else(|| anyhow!("Failed to take reader"))?;
            assert_eq!(reader.read_all().await?, expected);
            break;
        }
        Ok(())
    };

    timeout(Duration::from_secs(10), async { try_join!(send, receive) }).await??;
    Ok(())
}

/// End-to-end round-trip of a large in-memory byte payload, validating the v2 byte-stream path.
#[cfg(feature = "__lk-e2e-test")]
#[tokio::test]
async fn test_send_large_bytes() -> Result<()> {
    let mut rooms = test_rooms(2).await?;
    let (sending_room, _) = rooms.pop().unwrap();
    let (_, mut receiving_event_rx) = rooms.pop().unwrap();

    let payload: Vec<u8> = (0..50_000u32).map(|i| (i % 251) as u8).collect();
    let expected = payload.clone();

    let send = async move {
        let options = StreamByteOptions::new_with_topic("some-topic");
        let stream_info = sending_room.local_participant().send_bytes(&payload, options).await?;
        assert_eq!(stream_info.is_compressed, true, "is_compressed was not true");
        assert_eq!(stream_info.is_inline, true, "is_inline was not true");
        Ok(())
    };
    let receive = async move {
        while let Some(event) = receiving_event_rx.recv().await {
            let RoomEvent::ByteStreamOpened { reader, topic, .. } = event else {
                continue;
            };
            assert_eq!(topic, "some-topic");
            let reader = reader.take().ok_or_else(|| anyhow!("Failed to take reader"))?;
            assert_eq!(reader.read_all().await?, expected);
            break;
        }
        Ok(())
    };

    timeout(Duration::from_secs(10), async { try_join!(send, receive) }).await??;
    Ok(())
}

/// End-to-end round-trip of a large in-memory byte payload set with compress=false doesn't
/// compress the payload
#[cfg(feature = "__lk-e2e-test")]
#[tokio::test]
async fn test_data_stream_compress_false() -> Result<()> {
    let mut rooms = test_rooms(2).await?;
    let (sending_room, _) = rooms.pop().unwrap();
    let (_, mut receiving_event_rx) = rooms.pop().unwrap();

    let payload = vec![0xFFu8; 50_000];
    let expected = payload.clone();

    let send = async move {
        let options = StreamByteOptions::new_with_topic("some-topic").with_compress(false); // <= Explictly disable compression
        let stream_info = sending_room.local_participant().send_bytes(&payload, options).await?;
        assert_eq!(stream_info.is_compressed, false, "is_compressed was not false");
        assert_eq!(stream_info.is_inline, false, "is_inline was not false");
        Ok(())
    };
    let receive = async move {
        while let Some(event) = receiving_event_rx.recv().await {
            let RoomEvent::ByteStreamOpened { reader, topic, .. } = event else {
                continue;
            };
            assert_eq!(topic, "some-topic");
            let reader = reader.take().ok_or_else(|| anyhow!("Failed to take reader"))?;
            assert_eq!(reader.read_all().await?, expected);
            break;
        }
        Ok(())
    };

    timeout(Duration::from_secs(10), async { try_join!(send, receive) }).await??;
    Ok(())
}

/// End-to-end validation of the receiver-side max-payload guard: the receiving room is
/// configured with a tiny `max_payload_byte_length`, and the sender pushes a large,
/// compressible text (chunked + deflate-raw on the wire). As the receiver decompresses, the
/// running total blows past the limit, so its reader must terminate with
/// [`StreamError::PayloadTooLarge`] rather than yielding the full payload.
#[cfg(feature = "__lk-e2e-test")]
#[tokio::test]
async fn test_receiver_rejects_oversized_payload() -> Result<()> {
    // Cap the receiver's accepted (decompressed) payload well below what the sender will emit.
    const MAX_PAYLOAD_BYTES: usize = 1_000;

    let mut receiver_options = RoomOptions::default();
    receiver_options.data_stream =
        RoomDataStreamOptions::default().with_max_payload_byte_length(MAX_PAYLOAD_BYTES);

    // Order matters: index 0 is the size-capped receiver, index 1 is the default sender.
    let mut rooms =
        test_rooms_with_options([receiver_options.into(), TestRoomOptions::default()]).await?;
    let (_, mut receiving_event_rx) = rooms.remove(0);
    let (sending_room, _) = rooms.remove(0);

    // ~50 KB of deterministic pseudo-random lowercase: compresses under its raw size and spans
    // multiple chunks, so the receiver takes the streaming-decompress path where the guard lives.
    let text = pseudo_random_text(50_000);

    let send = async move {
        let options = StreamTextOptions::new_with_topic("some-topic");
        // The send succeeds locally; rejection happens on the receiver as it decompresses.
        let stream_info = sending_room.local_participant().send_text(&text, options).await?;
        // Guard the test's premise: it only exercises the limit if the stream is actually sent
        // compressed and multi-packet (not inlined).
        assert_eq!(stream_info.is_compressed, true, "expected a compressed stream");
        assert_eq!(stream_info.is_inline, false, "expected a chunked (non-inline) stream");
        Ok(())
    };
    let receive = async move {
        while let Some(event) = receiving_event_rx.recv().await {
            let RoomEvent::TextStreamOpened { reader, topic, .. } = event else {
                continue;
            };
            assert_eq!(topic, "some-topic");
            let reader = reader.take().ok_or_else(|| anyhow!("Failed to take reader"))?;
            let result = reader.read_all().await;
            assert!(
                matches!(result, Err(StreamError::PayloadTooLarge)),
                "expected StreamError::PayloadTooLarge, got {:?}",
                result
            );
            break;
        }
        Ok(())
    };

    timeout(Duration::from_secs(10), async { try_join!(send, receive) }).await??;
    Ok(())
}

#[cfg(feature = "__lk-e2e-test")]
#[tokio::test]
async fn test_send_text() -> Result<()> {
    let mut rooms = test_rooms(2).await?;
    let (sending_room, _) = rooms.pop().unwrap();
    let (_, mut receiving_event_rx) = rooms.pop().unwrap();
    let sender_identity = sending_room.local_participant().identity();

    const TEXT_TO_SEND: &str = "some-text";

    let send_text = async move {
        let options = StreamTextOptions::new_with_topic("some-topic");
        let stream_info = sending_room.local_participant().send_text(TEXT_TO_SEND, options).await?;

        assert!(!stream_info.id.is_empty());
        assert!(
            stream_info.timestamp.signed_duration_since(Utc::now()).abs() <= TimeDelta::seconds(1)
        );
        assert!(stream_info.total_length.is_some());
        assert_eq!(stream_info.mime_type, "text/plain");
        assert_eq!(stream_info.topic, "some-topic");

        Ok(())
    };
    let receive_text = async move {
        while let Some(event) = receiving_event_rx.recv().await {
            let RoomEvent::TextStreamOpened { reader, topic, participant_identity } = event else {
                continue;
            };
            assert_eq!(topic, "some-topic");
            assert_eq!(participant_identity, sender_identity);

            let Some(reader) = reader.take() else {
                return Err(anyhow!("Failed to take reader"));
            };
            assert_eq!(reader.read_all().await?, TEXT_TO_SEND);
            break;
        }
        Ok(())
    };

    timeout(Duration::from_secs(5), async { try_join!(send_text, receive_text) }).await??;
    Ok(())
}