feat: add ffi datachannel & mute events (#88)
This commit is contained in:
@@ -1,7 +1,18 @@
|
||||
use crate::server::audio_frame::{FfiAudioSource, FfiAudioSream};
|
||||
use crate::{proto, FfiHandleId};
|
||||
use livekit::webrtc::audio_source::AudioSourceOptions;
|
||||
use livekit::webrtc::prelude::*;
|
||||
|
||||
impl From<proto::AudioSourceOptions> for AudioSourceOptions {
|
||||
fn from(opts: proto::AudioSourceOptions) -> Self {
|
||||
Self {
|
||||
echo_cancellation: opts.echo_cancellation,
|
||||
auto_gain_control: opts.auto_gain_control,
|
||||
noise_suppression: opts.noise_suppression,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl proto::AudioFrameBufferInfo {
|
||||
pub fn from(handle_id: FfiHandleId, buffer: &AudioFrame) -> Self {
|
||||
Self {
|
||||
@@ -20,7 +31,6 @@ impl From<&FfiAudioSream> for proto::AudioStreamInfo {
|
||||
handle: Some(proto::FfiHandleId {
|
||||
id: stream.handle_id() as u64,
|
||||
}),
|
||||
track_sid: stream.track_sid().clone().into(),
|
||||
r#type: stream.stream_type() as i32,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +1,23 @@
|
||||
use crate::{proto, FfiHandleId, INVALID_HANDLE};
|
||||
use crate::{proto, FfiHandleId};
|
||||
use livekit::options::{AudioEncoding, TrackPublishOptions, VideoEncoding};
|
||||
use livekit::prelude::*;
|
||||
|
||||
impl proto::RoomEvent {
|
||||
pub fn from(room_handle: FfiHandleId, event: RoomEvent) -> Option<Self> {
|
||||
let message = match event {
|
||||
RoomEvent::ParticipantConnected(participant) => Some(
|
||||
proto::room_event::Message::ParticipantConnected(proto::ParticipantConnected {
|
||||
info: Some((&participant).into()),
|
||||
}),
|
||||
),
|
||||
RoomEvent::ParticipantDisconnected(participant) => {
|
||||
Some(proto::room_event::Message::ParticipantDisconnected(
|
||||
proto::ParticipantDisconnected {
|
||||
info: Some((&participant).into()),
|
||||
},
|
||||
))
|
||||
}
|
||||
RoomEvent::TrackPublished {
|
||||
publication,
|
||||
participant,
|
||||
} => Some(proto::room_event::Message::TrackPublished(
|
||||
proto::TrackPublished {
|
||||
participant_sid: participant.sid().to_string(),
|
||||
publication: Some((&publication).into()),
|
||||
},
|
||||
)),
|
||||
RoomEvent::TrackUnpublished {
|
||||
publication,
|
||||
participant,
|
||||
} => Some(proto::room_event::Message::TrackUnpublished(
|
||||
proto::TrackUnpublished {
|
||||
participant_sid: participant.sid().to_string(),
|
||||
publication_sid: publication.sid().into(),
|
||||
},
|
||||
)),
|
||||
RoomEvent::TrackSubscribed {
|
||||
track,
|
||||
publication: _,
|
||||
participant,
|
||||
} => Some(proto::room_event::Message::TrackSubscribed(
|
||||
proto::TrackSubscribed {
|
||||
participant_sid: participant.sid().to_string(),
|
||||
track: Some(proto::TrackInfo::from_remote_track(INVALID_HANDLE, &track)),
|
||||
},
|
||||
)),
|
||||
RoomEvent::TrackUnsubscribed {
|
||||
track,
|
||||
publication: _,
|
||||
participant,
|
||||
} => Some(proto::room_event::Message::TrackUnsubscribed(
|
||||
proto::TrackUnsubscribed {
|
||||
participant_sid: participant.sid().to_string(),
|
||||
track_sid: track.sid().to_string(),
|
||||
},
|
||||
)),
|
||||
_ => None,
|
||||
};
|
||||
impl From<proto::RoomOptions> for RoomOptions {
|
||||
fn from(value: proto::RoomOptions) -> Self {
|
||||
Self {
|
||||
adaptive_stream: value.adaptive_stream,
|
||||
auto_subscribe: value.auto_subscribe,
|
||||
dynacast: value.dynacast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
message.map(|message| proto::RoomEvent {
|
||||
room_handle: Some(room_handle.into()),
|
||||
message: Some(message),
|
||||
})
|
||||
impl From<proto::DataPacketKind> for DataPacketKind {
|
||||
fn from(value: proto::DataPacketKind) -> Self {
|
||||
match value {
|
||||
proto::DataPacketKind::KindReliable => Self::Reliable,
|
||||
proto::DataPacketKind::KindLossy => Self::Lossy,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +49,6 @@ impl From<proto::TrackPublishOptions> for TrackPublishOptions {
|
||||
dtx: opts.dtx,
|
||||
red: opts.red,
|
||||
simulcast: opts.simulcast,
|
||||
name: opts.name,
|
||||
source: proto::TrackSource::from_i32(opts.source).unwrap().into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,6 @@
|
||||
use crate::{proto, FfiHandleId};
|
||||
use livekit::options::{AudioCaptureOptions, VideoCaptureOptions};
|
||||
use livekit::prelude::*;
|
||||
|
||||
impl From<proto::VideoCaptureOptions> for VideoCaptureOptions {
|
||||
fn from(opts: proto::VideoCaptureOptions) -> Self {
|
||||
Self {
|
||||
resolution: opts.resolution.unwrap_or_default().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<proto::AudioCaptureOptions> for AudioCaptureOptions {
|
||||
fn from(opts: proto::AudioCaptureOptions) -> Self {
|
||||
Self {
|
||||
echo_cancellation: opts.echo_cancellation,
|
||||
auto_gain_control: opts.auto_gain_control,
|
||||
noise_suppression: opts.noise_suppression,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TrackSource> for proto::TrackSource {
|
||||
fn from(source: TrackSource) -> proto::TrackSource {
|
||||
match source {
|
||||
@@ -63,7 +44,7 @@ macro_rules! impl_track_into {
|
||||
#[allow(dead_code)]
|
||||
pub fn $fnc(handle_id: FfiHandleId, track: $t) -> Self {
|
||||
Self {
|
||||
opt_handle: Some(handle_id.into()),
|
||||
handle: Some(handle_id.into()),
|
||||
name: track.name(),
|
||||
stream_state: proto::StreamState::from(track.stream_state()).into(),
|
||||
sid: track.sid().to_string(),
|
||||
|
||||
@@ -4,6 +4,16 @@ use crate::FfiHandleId;
|
||||
use livekit::options::{VideoCodec, VideoResolution};
|
||||
use livekit::webrtc::prelude::*;
|
||||
use livekit::webrtc::video_frame;
|
||||
use livekit::webrtc::video_source::VideoResolution as VideoSourceResolution;
|
||||
|
||||
impl From<proto::VideoSourceResolution> for VideoSourceResolution {
|
||||
fn from(res: proto::VideoSourceResolution) -> Self {
|
||||
Self {
|
||||
width: res.width,
|
||||
height: res.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_yuv_into {
|
||||
(@fields, $buffer:ident, $data_y:ident, $data_u:ident, $data_v: ident) => {
|
||||
@@ -72,7 +82,7 @@ impl proto::VideoFrameInfo {
|
||||
T: AsRef<dyn VideoFrameBuffer>,
|
||||
{
|
||||
Self {
|
||||
timestamp: frame.timestamp,
|
||||
timestamp_us: frame.timestamp_us,
|
||||
rotation: proto::VideoRotation::from(frame.rotation).into(),
|
||||
}
|
||||
}
|
||||
@@ -236,7 +246,6 @@ impl From<&FfiVideoStream> for proto::VideoStreamInfo {
|
||||
handle: Some(proto::FfiHandleId {
|
||||
id: stream.handle_id() as u64,
|
||||
}),
|
||||
track_sid: stream.track_sid().clone().into(),
|
||||
r#type: stream.stream_type() as i32,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,9 @@ use crate::{proto, server, FfiError, FfiHandleId, FfiResult};
|
||||
use futures_util::StreamExt;
|
||||
use livekit::prelude::*;
|
||||
use livekit::webrtc::audio_frame::AudioFrame;
|
||||
use livekit::webrtc::audio_source::native::NativeAudioSource;
|
||||
use livekit::webrtc::audio_stream::native::NativeAudioStream;
|
||||
use livekit::webrtc::prelude::*;
|
||||
use log::warn;
|
||||
use server::utils;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
// ===== FFIAudioStream =====
|
||||
@@ -14,7 +12,6 @@ use tokio::sync::oneshot;
|
||||
pub struct FfiAudioSream {
|
||||
handle_id: FfiHandleId,
|
||||
stream_type: proto::AudioStreamType,
|
||||
track_sid: TrackSid,
|
||||
|
||||
#[allow(dead_code)]
|
||||
close_tx: oneshot::Sender<()>, // Close the stream on drop
|
||||
@@ -35,42 +32,45 @@ impl FfiAudioSream {
|
||||
) -> FfiResult<proto::AudioStreamInfo> {
|
||||
let (close_tx, close_rx) = oneshot::channel();
|
||||
let stream_type = proto::AudioStreamType::from_i32(new_stream.r#type).unwrap();
|
||||
let track_sid: TrackSid = new_stream.track_sid.into();
|
||||
|
||||
let room_handle = new_stream
|
||||
.room_handle
|
||||
.ok_or(FfiError::InvalidRequest("room_handle is empty"))?
|
||||
let handle_id = new_stream
|
||||
.track_handle
|
||||
.ok_or(FfiError::InvalidRequest("track_handle is empty"))?
|
||||
.id as FfiHandleId;
|
||||
|
||||
let track = utils::find_remote_track(
|
||||
server,
|
||||
&track_sid,
|
||||
&new_stream.participant_sid.into(),
|
||||
room_handle,
|
||||
)?
|
||||
.rtc_track();
|
||||
let track = server
|
||||
.ffi_handles()
|
||||
.get(&handle_id)
|
||||
.ok_or(FfiError::InvalidRequest("track not found"))?;
|
||||
|
||||
let MediaStreamTrack::Audio(track) = track else {
|
||||
let track = track
|
||||
.downcast_ref::<Track>()
|
||||
.ok_or(FfiError::InvalidRequest("handle is not a Track"))?;
|
||||
|
||||
let rtc_track = track.rtc_track();
|
||||
|
||||
let MediaStreamTrack::Audio(rtc_track) = rtc_track else {
|
||||
return Err(FfiError::InvalidRequest("not an audio track"));
|
||||
};
|
||||
|
||||
let audio_stream = match stream_type {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
proto::AudioStreamType::AudioStreamNative => {
|
||||
let audio_stream = Self {
|
||||
handle_id: server.next_id(),
|
||||
stream_type,
|
||||
close_tx,
|
||||
track_sid,
|
||||
};
|
||||
|
||||
let native_stream = NativeAudioStream::new(rtc_track);
|
||||
server.async_runtime.spawn(Self::native_audio_stream_task(
|
||||
server,
|
||||
audio_stream.handle_id,
|
||||
NativeAudioStream::new(track),
|
||||
native_stream,
|
||||
close_rx,
|
||||
));
|
||||
Ok::<FfiAudioSream, FfiError>(audio_stream)
|
||||
}
|
||||
// TODO(theomonnom): Support other stream types
|
||||
_ => return Err(FfiError::InvalidRequest("unsupported audio stream type")),
|
||||
}?;
|
||||
|
||||
@@ -91,10 +91,6 @@ impl FfiAudioSream {
|
||||
self.stream_type
|
||||
}
|
||||
|
||||
pub fn track_sid(&self) -> &TrackSid {
|
||||
&self.track_sid
|
||||
}
|
||||
|
||||
async fn native_audio_stream_task(
|
||||
server: &'static server::FfiServer,
|
||||
stream_handle_id: FfiHandleId,
|
||||
@@ -139,12 +135,7 @@ impl FfiAudioSream {
|
||||
pub struct FfiAudioSource {
|
||||
handle_id: FfiHandleId,
|
||||
source_type: proto::AudioSourceType,
|
||||
source: AudioSource,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum AudioSource {
|
||||
Native(NativeAudioSource),
|
||||
source: RtcAudioSource,
|
||||
}
|
||||
|
||||
impl FfiAudioSource {
|
||||
@@ -153,12 +144,17 @@ impl FfiAudioSource {
|
||||
new_source: proto::NewAudioSourceRequest,
|
||||
) -> FfiResult<proto::AudioSourceInfo> {
|
||||
let source_type = proto::AudioSourceType::from_i32(new_source.r#type).unwrap();
|
||||
#[allow(unreachable_patterns)]
|
||||
let source_inner = match source_type {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
proto::AudioSourceType::AudioSourceNative => {
|
||||
let audio_source = NativeAudioSource::default();
|
||||
Ok::<AudioSource, FfiError>(AudioSource::Native(audio_source))
|
||||
} //_ => return Err(FfiError::InvalidRequest("unsupported audio source type")),
|
||||
}?;
|
||||
use livekit::webrtc::audio_source::native::NativeAudioSource;
|
||||
let audio_source =
|
||||
NativeAudioSource::new(new_source.options.map(Into::into).unwrap_or_default());
|
||||
RtcAudioSource::Native(audio_source)
|
||||
}
|
||||
_ => return Err(FfiError::InvalidRequest("unsupported audio source type")),
|
||||
};
|
||||
|
||||
let audio_source = Self {
|
||||
handle_id: server.next_id(),
|
||||
@@ -180,7 +176,8 @@ impl FfiAudioSource {
|
||||
capture: proto::CaptureAudioFrameRequest,
|
||||
) -> FfiResult<()> {
|
||||
match self.source {
|
||||
AudioSource::Native(ref source) => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
RtcAudioSource::Native(ref source) => {
|
||||
let buffer_handle = capture
|
||||
.buffer_handle
|
||||
.ok_or(FfiError::InvalidRequest("buffer_handle is empty"))?
|
||||
@@ -197,6 +194,7 @@ impl FfiAudioSource {
|
||||
|
||||
source.capture_frame(frame);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -210,7 +208,7 @@ impl FfiAudioSource {
|
||||
self.source_type
|
||||
}
|
||||
|
||||
pub fn inner_source(&self) -> &AudioSource {
|
||||
pub fn inner_source(&self) -> &RtcAudioSource {
|
||||
&self.source
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ use std::sync::Arc;
|
||||
|
||||
pub mod audio_frame;
|
||||
pub mod room;
|
||||
pub mod utils;
|
||||
pub mod video_frame;
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -31,7 +30,7 @@ pub struct FfiConfig {
|
||||
|
||||
pub struct FfiServer {
|
||||
rooms: Mutex<HashMap<RoomSid, FfiHandleId>>,
|
||||
/// Store all FFI handles inside an HashMap, if this isn't efficient enough
|
||||
/// Store all Ffi handles inside an HashMap, if this isn't efficient enough
|
||||
/// We can still use Box::into_raw & Box::from_raw in the future (but keep it safe for now)
|
||||
ffi_handles: DashMap<FfiHandleId, FfiHandle>,
|
||||
next_id: AtomicUsize,
|
||||
@@ -116,11 +115,9 @@ impl FfiServer {
|
||||
}
|
||||
|
||||
// # SAFETY: The foreign language is responsible for ensuring that the callback function is valid
|
||||
unsafe {
|
||||
*self.config.lock() = Some(FfiConfig {
|
||||
callback_fn: std::mem::transmute(init.event_callback_ptr),
|
||||
});
|
||||
}
|
||||
*self.config.lock() = Some(FfiConfig {
|
||||
callback_fn: unsafe { std::mem::transmute(init.event_callback_ptr) },
|
||||
});
|
||||
|
||||
Ok(proto::InitializeResponse::default())
|
||||
}
|
||||
@@ -191,9 +188,37 @@ impl FfiServer {
|
||||
|
||||
fn on_disconnect(
|
||||
&'static self,
|
||||
_disconnect: proto::DisconnectRequest,
|
||||
disconnect: proto::DisconnectRequest,
|
||||
) -> FfiResult<proto::DisconnectResponse> {
|
||||
Ok(proto::DisconnectResponse::default())
|
||||
let async_id = self.next_id() as FfiAsyncId;
|
||||
let room_handle = disconnect
|
||||
.room_handle
|
||||
.as_ref()
|
||||
.ok_or(FfiError::InvalidRequest("room_handle is empty"))?
|
||||
.id as FfiHandleId;
|
||||
|
||||
let ffi_room = self
|
||||
.ffi_handles
|
||||
.remove(&room_handle)
|
||||
.ok_or(FfiError::InvalidRequest("room not found"))?
|
||||
.1;
|
||||
|
||||
let ffi_room = ffi_room
|
||||
.downcast::<room::FfiRoom>()
|
||||
.map_err(|_| FfiError::InvalidRequest("room is not a FfiRoom"))?;
|
||||
|
||||
self.async_runtime.spawn(async move {
|
||||
ffi_room.close().await;
|
||||
let _ = self.send_event(proto::ffi_event::Message::Disconnect(
|
||||
proto::DisconnectCallback {
|
||||
async_id: Some(async_id.into()),
|
||||
},
|
||||
));
|
||||
});
|
||||
|
||||
Ok(proto::DisconnectResponse {
|
||||
async_id: Some(async_id.into()),
|
||||
})
|
||||
}
|
||||
|
||||
fn on_publish_track(
|
||||
@@ -230,14 +255,17 @@ impl FfiServer {
|
||||
.ok_or(FfiError::InvalidRequest("track not found"))?;
|
||||
|
||||
let track = track
|
||||
.downcast_ref::<LocalTrack>()
|
||||
.ok_or(FfiError::InvalidRequest("track is not a LocalTrack"))?;
|
||||
.downcast_ref::<Track>()
|
||||
.ok_or(FfiError::InvalidRequest("track is not a Track"))?;
|
||||
|
||||
let local_track = LocalTrack::try_from(track.clone())
|
||||
.map_err(|_| FfiError::InvalidRequest("track is not a LocalTrack"))?;
|
||||
|
||||
let publication = ffi_room
|
||||
.room()
|
||||
.local_participant()
|
||||
.publish_track(
|
||||
track.clone(),
|
||||
local_track,
|
||||
publish.options.map(Into::into).unwrap_or_default(),
|
||||
)
|
||||
.await?;
|
||||
@@ -269,6 +297,29 @@ impl FfiServer {
|
||||
Ok(proto::UnpublishTrackResponse::default())
|
||||
}
|
||||
|
||||
fn on_publish_data(
|
||||
&'static self,
|
||||
publish: proto::PublishDataRequest,
|
||||
) -> FfiResult<proto::PublishDataResponse> {
|
||||
let room_handle = publish
|
||||
.room_handle
|
||||
.as_ref()
|
||||
.ok_or(FfiError::InvalidRequest("room_handle is empty"))?
|
||||
.id as FfiHandleId;
|
||||
|
||||
let ffi_room = self
|
||||
.ffi_handles
|
||||
.get(&room_handle)
|
||||
.ok_or(FfiError::InvalidRequest("room not found"))?;
|
||||
|
||||
let ffi_room = ffi_room
|
||||
.downcast_ref::<room::FfiRoom>()
|
||||
.ok_or(FfiError::InvalidRequest("room is not a FfiRoom"))?;
|
||||
|
||||
// Push the data to an async queue (avoid blocking and keep the order)
|
||||
ffi_room.publish_data(self, publish)
|
||||
}
|
||||
|
||||
// Track
|
||||
fn on_create_video_track(
|
||||
&'static self,
|
||||
@@ -276,7 +327,6 @@ impl FfiServer {
|
||||
) -> FfiResult<proto::CreateVideoTrackResponse> {
|
||||
let handle_id = create
|
||||
.source_handle
|
||||
.as_ref()
|
||||
.ok_or(FfiError::InvalidRequest("source_handle is empty"))?
|
||||
.id as FfiHandleId;
|
||||
|
||||
@@ -290,19 +340,13 @@ impl FfiServer {
|
||||
.ok_or(FfiError::InvalidRequest("handle is not a video source"))?;
|
||||
|
||||
let source = source.inner_source().clone();
|
||||
let video_track = match source {
|
||||
video_frame::VideoSource::Native(native_source) => LocalVideoTrack::create_video_track(
|
||||
&create.name,
|
||||
create.options.unwrap_or_default().into(),
|
||||
native_source,
|
||||
),
|
||||
};
|
||||
let video_track = LocalVideoTrack::create_video_track(&create.name, source);
|
||||
|
||||
let handle_id = self.next_id() as FfiHandleId;
|
||||
let track_info = proto::TrackInfo::from_local_video_track(handle_id, &video_track);
|
||||
|
||||
self.ffi_handles
|
||||
.insert(handle_id, Box::new(LocalTrack::Video(video_track)));
|
||||
.insert(handle_id, Box::new(Track::LocalVideo(video_track)));
|
||||
|
||||
Ok(proto::CreateVideoTrackResponse {
|
||||
track: Some(track_info),
|
||||
@@ -315,7 +359,6 @@ impl FfiServer {
|
||||
) -> FfiResult<proto::CreateAudioTrackResponse> {
|
||||
let handle_id = create
|
||||
.source_handle
|
||||
.as_ref()
|
||||
.ok_or(FfiError::InvalidRequest("source_handle is empty"))?
|
||||
.id as FfiHandleId;
|
||||
|
||||
@@ -329,19 +372,13 @@ impl FfiServer {
|
||||
.ok_or(FfiError::InvalidRequest("handle is not an audio source"))?;
|
||||
|
||||
let source = source.inner_source().clone();
|
||||
let audio_track = match source {
|
||||
audio_frame::AudioSource::Native(native_source) => LocalAudioTrack::create_audio_track(
|
||||
&create.name,
|
||||
create.options.unwrap_or_default().into(),
|
||||
native_source,
|
||||
),
|
||||
};
|
||||
let audio_track = LocalAudioTrack::create_audio_track(&create.name, source);
|
||||
|
||||
let handle_id = self.next_id() as FfiHandleId;
|
||||
let track_info = proto::TrackInfo::from_local_audio_track(handle_id, &audio_track);
|
||||
|
||||
self.ffi_handles
|
||||
.insert(handle_id, Box::new(LocalTrack::Audio(audio_track)));
|
||||
.insert(handle_id, Box::new(Track::LocalAudio(audio_track)));
|
||||
|
||||
Ok(proto::CreateAudioTrackResponse {
|
||||
track: Some(track_info),
|
||||
@@ -714,6 +751,9 @@ impl FfiServer {
|
||||
proto::ffi_request::Message::UnpublishTrack(unpublish) => {
|
||||
proto::ffi_response::Message::UnpublishTrack(self.on_unpublish_track(unpublish)?)
|
||||
}
|
||||
proto::ffi_request::Message::PublishData(publish) => {
|
||||
proto::ffi_response::Message::PublishData(self.on_publish_data(publish)?)
|
||||
}
|
||||
proto::ffi_request::Message::CreateVideoTrack(create) => {
|
||||
proto::ffi_response::Message::CreateVideoTrack(self.on_create_video_track(create)?)
|
||||
}
|
||||
|
||||
+165
-22
@@ -1,14 +1,24 @@
|
||||
use crate::server::FfiServer;
|
||||
use crate::{proto, FfiHandleId, FfiResult};
|
||||
use crate::{proto, FfiAsyncId, FfiError, FfiHandleId, FfiResult};
|
||||
use livekit::prelude::*;
|
||||
use std::slice;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
use tokio::sync::{broadcast, mpsc};
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
struct DataPacket {
|
||||
data: Vec<u8>,
|
||||
kind: DataPacketKind,
|
||||
destination_sids: Vec<String>,
|
||||
async_id: FfiAsyncId,
|
||||
}
|
||||
|
||||
pub struct FfiRoom {
|
||||
room: Arc<Room>,
|
||||
handle: JoinHandle<()>,
|
||||
close_tx: oneshot::Sender<()>,
|
||||
event_handle: JoinHandle<()>,
|
||||
data_handle: JoinHandle<()>,
|
||||
close_tx: broadcast::Sender<()>,
|
||||
data_tx: mpsc::UnboundedSender<DataPacket>,
|
||||
}
|
||||
|
||||
impl FfiRoom {
|
||||
@@ -16,33 +26,77 @@ impl FfiRoom {
|
||||
server: &'static FfiServer,
|
||||
connect: proto::ConnectRequest,
|
||||
) -> FfiResult<proto::RoomInfo> {
|
||||
let (room, events) = Room::connect(&connect.url, &connect.token).await?;
|
||||
let (room, events) = Room::connect(
|
||||
&connect.url,
|
||||
&connect.token,
|
||||
connect.options.map(Into::into).unwrap_or_default(),
|
||||
)
|
||||
.await?;
|
||||
let room = Arc::new(room);
|
||||
let (close_tx, close_rx) = oneshot::channel();
|
||||
let next_id = server.next_id() as FfiHandleId;
|
||||
let (close_tx, close_rx) = broadcast::channel(1);
|
||||
let (data_tx, data_rx) = mpsc::unbounded_channel();
|
||||
|
||||
let handle =
|
||||
let next_id = server.next_id() as FfiHandleId;
|
||||
let event_handle = server.async_runtime.spawn(room_task(
|
||||
server,
|
||||
room.clone(),
|
||||
next_id,
|
||||
events,
|
||||
close_rx.resubscribe(),
|
||||
));
|
||||
let data_handle =
|
||||
server
|
||||
.async_runtime
|
||||
.spawn(room_task(server, room.clone(), next_id, events, close_rx));
|
||||
let room_info = proto::RoomInfo::from_room(next_id, &room);
|
||||
.spawn(data_task(server, room.clone(), data_rx, close_rx));
|
||||
|
||||
let ffi_room = Self {
|
||||
room: room.clone(),
|
||||
handle,
|
||||
event_handle,
|
||||
data_handle,
|
||||
close_tx,
|
||||
data_tx,
|
||||
};
|
||||
|
||||
server.ffi_handles().insert(next_id, Box::new(ffi_room));
|
||||
server.rooms().lock().insert(room.sid(), next_id);
|
||||
|
||||
let room_info = proto::RoomInfo::from_room(next_id, &room);
|
||||
Ok(room_info)
|
||||
}
|
||||
|
||||
pub fn publish_data(
|
||||
&self,
|
||||
server: &'static FfiServer,
|
||||
publish: proto::PublishDataRequest,
|
||||
) -> FfiResult<proto::PublishDataResponse> {
|
||||
let data = unsafe {
|
||||
slice::from_raw_parts(publish.data_ptr as *const u8, publish.data_size as usize)
|
||||
};
|
||||
let kind = proto::DataPacketKind::from_i32(publish.kind).unwrap();
|
||||
let destination_sids: Vec<String> = publish.destination_sids;
|
||||
let async_id = server.next_id() as FfiAsyncId;
|
||||
|
||||
let packet = DataPacket {
|
||||
data: data.to_vec(), // Avoid copy?
|
||||
kind: kind.into(),
|
||||
destination_sids,
|
||||
async_id,
|
||||
};
|
||||
|
||||
self.data_tx
|
||||
.send(packet)
|
||||
.map_err(|_| FfiError::InvalidRequest("failed to send data packet"))?;
|
||||
|
||||
Ok(proto::PublishDataResponse {
|
||||
async_id: Some(async_id.into()),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn close(self) {
|
||||
let _ = self.room.close().await;
|
||||
let _ = self.close_tx.send(());
|
||||
let _ = self.handle.await;
|
||||
let _ = self.event_handle.await;
|
||||
let _ = self.data_handle.await;
|
||||
}
|
||||
|
||||
pub fn room(&self) -> &Arc<Room> {
|
||||
@@ -50,12 +104,41 @@ impl FfiRoom {
|
||||
}
|
||||
}
|
||||
|
||||
async fn data_task(
|
||||
server: &'static FfiServer,
|
||||
room: Arc<Room>,
|
||||
mut data_rx: mpsc::UnboundedReceiver<DataPacket>,
|
||||
mut close_rx: broadcast::Receiver<()>,
|
||||
) {
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(event) = data_rx.recv() => {
|
||||
let res = room.local_participant().publish_data(
|
||||
event.data,
|
||||
event.kind,
|
||||
event.destination_sids,
|
||||
).await;
|
||||
|
||||
let cb = proto::PublishDataCallback {
|
||||
async_id: Some(event.async_id.into()),
|
||||
error: res.err().map(|e| e.to_string()),
|
||||
};
|
||||
|
||||
let _ = server.send_event(proto::ffi_event::Message::PublishData(cb));
|
||||
},
|
||||
_ = close_rx.recv() => {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn room_task(
|
||||
server: &'static FfiServer,
|
||||
room: Arc<Room>,
|
||||
room_handle: FfiHandleId,
|
||||
mut events: mpsc::UnboundedReceiver<livekit::RoomEvent>,
|
||||
mut close_rx: oneshot::Receiver<()>,
|
||||
mut close_rx: broadcast::Receiver<()>,
|
||||
) {
|
||||
server
|
||||
.async_runtime
|
||||
@@ -66,18 +149,78 @@ async fn room_task(
|
||||
loop {
|
||||
tokio::select! {
|
||||
Some(event) = events.recv() => {
|
||||
if let Some(event) = proto::RoomEvent::from(room_handle, event.clone()) {
|
||||
let _ = server.send_event(proto::ffi_event::Message::RoomEvent(event));
|
||||
let message = match event {
|
||||
RoomEvent::ParticipantConnected(participant) => {
|
||||
server.async_runtime.spawn(participant_task(Participant::Remote(participant.clone())));
|
||||
Some(proto::room_event::Message::ParticipantConnected(
|
||||
proto::ParticipantConnected {
|
||||
info: Some(proto::ParticipantInfo::from(&participant)),
|
||||
}
|
||||
))
|
||||
},
|
||||
RoomEvent::ParticipantDisconnected(participant) => {
|
||||
Some(proto::room_event::Message::ParticipantDisconnected(
|
||||
proto::ParticipantDisconnected {
|
||||
info: Some(proto::ParticipantInfo::from(&participant)),
|
||||
},
|
||||
))
|
||||
}
|
||||
RoomEvent::TrackPublished {
|
||||
publication,
|
||||
participant,
|
||||
} => Some(proto::room_event::Message::TrackPublished(
|
||||
proto::TrackPublished {
|
||||
participant_sid: participant.sid().to_string(),
|
||||
publication: Some(proto::TrackPublicationInfo::from(&publication))
|
||||
},
|
||||
)),
|
||||
RoomEvent::TrackUnpublished {
|
||||
publication,
|
||||
participant,
|
||||
} => Some(proto::room_event::Message::TrackUnpublished(
|
||||
proto::TrackUnpublished {
|
||||
participant_sid: participant.sid().to_string(),
|
||||
publication_sid: publication.sid().into(),
|
||||
},
|
||||
)),
|
||||
RoomEvent::TrackSubscribed {
|
||||
track,
|
||||
publication: _,
|
||||
participant,
|
||||
} => {
|
||||
let handle_id = server.next_id() as FfiHandleId;
|
||||
let track_info = proto::TrackInfo::from_remote_track(handle_id, &track);
|
||||
server.ffi_handles().insert(handle_id, Box::new(Track::from(track)));
|
||||
|
||||
Some(proto::room_event::Message::TrackSubscribed(
|
||||
proto::TrackSubscribed {
|
||||
participant_sid: participant.sid().to_string(),
|
||||
track: Some(track_info),
|
||||
},
|
||||
))
|
||||
},
|
||||
RoomEvent::TrackUnsubscribed {
|
||||
track,
|
||||
publication: _,
|
||||
participant,
|
||||
} => Some(proto::room_event::Message::TrackUnsubscribed(
|
||||
proto::TrackUnsubscribed {
|
||||
participant_sid: participant.sid().to_string(),
|
||||
track_sid: track.sid().to_string(),
|
||||
},
|
||||
)),
|
||||
_ => None
|
||||
};
|
||||
|
||||
if message.is_some() {
|
||||
let _ = server.send_event(proto::ffi_event::Message::RoomEvent(proto::RoomEvent{
|
||||
room_handle: Some(room_handle.into()),
|
||||
message
|
||||
}));
|
||||
}
|
||||
|
||||
match event {
|
||||
RoomEvent::ParticipantConnected(p) => {
|
||||
server.async_runtime.spawn(participant_task(Participant::Remote(p)));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
_ = &mut close_rx => {
|
||||
_ = close_rx.recv() => {
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -212,18 +212,22 @@ fn publish_video_track() {
|
||||
client::FfiHandle(connect.room.unwrap().handle.unwrap().id as FfiHandleId);
|
||||
|
||||
// Create a new VideoSource
|
||||
const VIDEO_WIDTH: u32 = 640;
|
||||
const VIDEO_HEIGHT: u32 = 480;
|
||||
const VIDEO_FPS: f64 = 8.0;
|
||||
|
||||
let res = client.send_request(proto::FfiRequest {
|
||||
message: Some(proto::ffi_request::Message::NewVideoSource(
|
||||
proto::NewVideoSourceRequest {
|
||||
r#type: proto::VideoSourceType::VideoSourceNative as i32,
|
||||
resolution: Some(proto::VideoSourceResolution {
|
||||
width: VIDEO_WIDTH,
|
||||
height: VIDEO_HEIGHT,
|
||||
}),
|
||||
},
|
||||
)),
|
||||
});
|
||||
|
||||
const VIDEO_WIDTH: u32 = 640;
|
||||
const VIDEO_HEIGHT: u32 = 480;
|
||||
const VIDEO_FPS: f64 = 8.0;
|
||||
|
||||
let proto::ffi_response::Message::NewVideoSource(new_video_source) =
|
||||
res.message.unwrap() else {
|
||||
panic!("unexpected response");
|
||||
@@ -241,13 +245,6 @@ fn publish_video_track() {
|
||||
source_handle: Some(proto::FfiHandleId {
|
||||
id: source_handle.0 as u64,
|
||||
}),
|
||||
options: Some(proto::VideoCaptureOptions {
|
||||
resolution: Some(proto::VideoResolution {
|
||||
width: VIDEO_WIDTH,
|
||||
height: VIDEO_HEIGHT,
|
||||
frame_rate: VIDEO_FPS,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
)),
|
||||
});
|
||||
@@ -258,11 +255,10 @@ fn publish_video_track() {
|
||||
};
|
||||
|
||||
let track_handle = client::FfiHandle(
|
||||
create_video_track.track.unwrap().opt_handle.unwrap().id as FfiHandleId,
|
||||
create_video_track.track.unwrap().handle.unwrap().id as FfiHandleId,
|
||||
);
|
||||
|
||||
let publish_options = proto::TrackPublishOptions {
|
||||
name: "video_test".to_string(),
|
||||
video_codec: proto::VideoCodec::H264 as i32,
|
||||
source: proto::TrackSource::SourceCamera as i32,
|
||||
..Default::default()
|
||||
@@ -320,7 +316,7 @@ fn publish_video_track() {
|
||||
id: buffer_handle.0 as u64,
|
||||
}),
|
||||
frame: Some(proto::VideoFrameInfo {
|
||||
timestamp: 0, // TODO
|
||||
timestamp_us: 0,
|
||||
rotation: proto::VideoRotation::VideoRotation0 as i32,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
use crate::{server, FfiError, FfiHandleId, FfiResult};
|
||||
use livekit::prelude::*;
|
||||
|
||||
pub fn find_remote_track(
|
||||
server: &'static server::FfiServer,
|
||||
track_sid: &TrackSid,
|
||||
participant_sid: &ParticipantSid,
|
||||
room_handle: FfiHandleId,
|
||||
) -> FfiResult<RemoteTrack> {
|
||||
let ffi_room = server
|
||||
.ffi_handles()
|
||||
.get(&room_handle)
|
||||
.ok_or(FfiError::InvalidRequest("room not found"))?;
|
||||
|
||||
let ffi_room = ffi_room
|
||||
.downcast_ref::<server::room::FfiRoom>()
|
||||
.ok_or(FfiError::InvalidRequest("room is not ffi room"))?;
|
||||
|
||||
let room = ffi_room.room();
|
||||
let participants = room.participants();
|
||||
let participant = participants
|
||||
.get(participant_sid)
|
||||
.ok_or(FfiError::InvalidRequest("participant not found"))?;
|
||||
|
||||
let track = participant
|
||||
.get_track_publication(track_sid)
|
||||
.ok_or(FfiError::InvalidRequest("publication not found"))?
|
||||
.track()
|
||||
.ok_or(FfiError::InvalidRequest("track not found/subscribed"))?;
|
||||
|
||||
Ok(track)
|
||||
}
|
||||
@@ -3,10 +3,8 @@ use futures_util::StreamExt;
|
||||
use livekit::prelude::*;
|
||||
use livekit::webrtc::prelude::*;
|
||||
use livekit::webrtc::video_frame::{BoxVideoFrameBuffer, VideoFrame};
|
||||
use livekit::webrtc::video_source::native::NativeVideoSource;
|
||||
use livekit::webrtc::video_stream::native::NativeVideoStream;
|
||||
use log::warn;
|
||||
use server::utils;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
// ===== FFIVideoStream =====
|
||||
@@ -14,7 +12,6 @@ use tokio::sync::oneshot;
|
||||
pub struct FfiVideoStream {
|
||||
handle_id: FfiHandleId,
|
||||
stream_type: proto::VideoStreamType,
|
||||
track_sid: TrackSid,
|
||||
|
||||
#[allow(dead_code)]
|
||||
close_tx: oneshot::Sender<()>, // Close the stream on drop
|
||||
@@ -35,42 +32,43 @@ impl FfiVideoStream {
|
||||
) -> FfiResult<proto::VideoStreamInfo> {
|
||||
let (close_tx, close_rx) = oneshot::channel();
|
||||
let stream_type = proto::VideoStreamType::from_i32(new_stream.r#type).unwrap();
|
||||
let track_sid: TrackSid = new_stream.track_sid.into();
|
||||
|
||||
let room_handle = new_stream
|
||||
.room_handle
|
||||
.ok_or(FfiError::InvalidRequest("room_handle is empty"))?
|
||||
let handle_id = new_stream
|
||||
.track_handle
|
||||
.ok_or(FfiError::InvalidRequest("track_handle is empty"))?
|
||||
.id as FfiHandleId;
|
||||
|
||||
let track = utils::find_remote_track(
|
||||
server,
|
||||
&track_sid,
|
||||
&new_stream.participant_sid.into(),
|
||||
room_handle,
|
||||
)?
|
||||
.rtc_track();
|
||||
let track = server
|
||||
.ffi_handles()
|
||||
.get(&handle_id)
|
||||
.ok_or(FfiError::InvalidRequest("track not found"))?;
|
||||
|
||||
let MediaStreamTrack::Video(track) = track else {
|
||||
let track = track
|
||||
.downcast_ref::<Track>()
|
||||
.ok_or(FfiError::InvalidRequest("handle is not a Track"))?;
|
||||
|
||||
let rtc_track = track.rtc_track();
|
||||
|
||||
let MediaStreamTrack::Video(rtc_track) = rtc_track else {
|
||||
return Err(FfiError::InvalidRequest("not a video track"));
|
||||
};
|
||||
|
||||
let stream = match stream_type {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
proto::VideoStreamType::VideoStreamNative => {
|
||||
let video_stream = Self {
|
||||
handle_id: server.next_id(),
|
||||
close_tx,
|
||||
stream_type,
|
||||
track_sid,
|
||||
};
|
||||
server.async_runtime.spawn(Self::native_video_stream_task(
|
||||
server,
|
||||
video_stream.handle_id,
|
||||
NativeVideoStream::new(track),
|
||||
NativeVideoStream::new(rtc_track),
|
||||
close_rx,
|
||||
));
|
||||
Ok::<FfiVideoStream, FfiError>(video_stream)
|
||||
}
|
||||
// TODO(theomonnom): Support other stream types
|
||||
_ => return Err(FfiError::InvalidRequest("unsupported video stream type")),
|
||||
}?;
|
||||
|
||||
@@ -91,10 +89,6 @@ impl FfiVideoStream {
|
||||
self.stream_type
|
||||
}
|
||||
|
||||
pub fn track_sid(&self) -> &TrackSid {
|
||||
&self.track_sid
|
||||
}
|
||||
|
||||
async fn native_video_stream_task(
|
||||
server: &'static server::FfiServer,
|
||||
stream_handle_id: FfiHandleId,
|
||||
@@ -143,12 +137,7 @@ impl FfiVideoStream {
|
||||
pub struct FfiVideoSource {
|
||||
handle_id: FfiHandleId,
|
||||
source_type: proto::VideoSourceType,
|
||||
source: VideoSource,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum VideoSource {
|
||||
Native(NativeVideoSource),
|
||||
source: RtcVideoSource,
|
||||
}
|
||||
|
||||
impl FfiVideoSource {
|
||||
@@ -157,11 +146,17 @@ impl FfiVideoSource {
|
||||
new_source: proto::NewVideoSourceRequest,
|
||||
) -> FfiResult<proto::VideoSourceInfo> {
|
||||
let source_type = proto::VideoSourceType::from_i32(new_source.r#type).unwrap();
|
||||
#[allow(unreachable_patterns)]
|
||||
let source_inner = match source_type {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
proto::VideoSourceType::VideoSourceNative => {
|
||||
let video_source = NativeVideoSource::default();
|
||||
VideoSource::Native(video_source)
|
||||
use livekit::webrtc::video_source::native::NativeVideoSource;
|
||||
let video_source = NativeVideoSource::new(
|
||||
new_source.resolution.map(Into::into).unwrap_or_default(),
|
||||
);
|
||||
RtcVideoSource::Native(video_source)
|
||||
}
|
||||
_ => return Err(FfiError::InvalidRequest("unsupported video source type")),
|
||||
};
|
||||
|
||||
let video_source = Self {
|
||||
@@ -184,7 +179,8 @@ impl FfiVideoSource {
|
||||
capture: proto::CaptureVideoFrameRequest,
|
||||
) -> FfiResult<()> {
|
||||
match self.source {
|
||||
VideoSource::Native(ref source) => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
RtcVideoSource::Native(ref source) => {
|
||||
let frame_info = capture
|
||||
.frame
|
||||
.ok_or(FfiError::InvalidRequest("frame is empty"))?;
|
||||
@@ -206,12 +202,13 @@ impl FfiVideoSource {
|
||||
let rotation = proto::VideoRotation::from_i32(frame_info.rotation).unwrap();
|
||||
let frame = VideoFrame {
|
||||
rotation: rotation.into(),
|
||||
timestamp: frame_info.timestamp,
|
||||
timestamp_us: frame_info.timestamp_us,
|
||||
buffer,
|
||||
};
|
||||
|
||||
source.capture_frame(&frame);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -224,7 +221,7 @@ impl FfiVideoSource {
|
||||
self.source_type
|
||||
}
|
||||
|
||||
pub fn inner_source(&self) -> &VideoSource {
|
||||
pub fn inner_source(&self) -> &RtcVideoSource {
|
||||
&self.source
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user