diff --git a/Cargo.lock b/Cargo.lock index 2cac8d4..1773477 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -667,6 +667,17 @@ dependencies = [ "xmpp-parsers-gst-meet", ] +[[package]] +name = "lib-gst-meet-c" +version = "0.1.0" +dependencies = [ + "anyhow", + "glib", + "gstreamer", + "lib-gst-meet", + "tokio", +] + [[package]] name = "libc" version = "0.2.99" diff --git a/Cargo.toml b/Cargo.toml index e0e50f9..47ae19e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "gst-meet", "lib-gst-meet", + "lib-gst-meet-c", "nice-gst-meet", "nice-gst-meet-sys", -] \ No newline at end of file +] diff --git a/lib-gst-meet-c/Cargo.toml b/lib-gst-meet-c/Cargo.toml new file mode 100644 index 0000000..1c20f4e --- /dev/null +++ b/lib-gst-meet-c/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "lib-gst-meet-c" +description = "Connect GStreamer pipelines to Jitsi Meet conferences (C bindings)" +version = "0.1.0" +edition = "2018" +license = "MIT/Apache-2.0" +authors = ["Jasper Hugo "] + +[dependencies] +anyhow = { version = "1", default-features = false } +glib = { version = "0.14", default-features = false } +gstreamer = { version = "0.17", default-features = false } +lib-gst-meet = { version = "0.1", path = "../lib-gst-meet", default-features = false } +tokio = { version = "1", default-features = false, features = ["rt-multi-thread"] } + +[lib] +name = "gstmeet" +crate_type = ["staticlib", "cdylib"] \ No newline at end of file diff --git a/lib-gst-meet-c/cbindgen.toml b/lib-gst-meet-c/cbindgen.toml new file mode 100644 index 0000000..3f6060e --- /dev/null +++ b/lib-gst-meet-c/cbindgen.toml @@ -0,0 +1,3 @@ +language = "C" +include_guard = "gstmeet_h" +autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" diff --git a/lib-gst-meet-c/gstmeet.h b/lib-gst-meet-c/gstmeet.h new file mode 100644 index 0000000..eb17739 --- /dev/null +++ b/lib-gst-meet-c/gstmeet.h @@ -0,0 +1,65 @@ +#ifndef gstmeet_h +#define gstmeet_h + +/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */ + +#include +#include +#include +#include + +typedef struct Context Context; + +typedef struct ConferenceConfig { + const char *muc; + const char *focus; + const char *nick; + const char *region; + const char *video_codec; +} ConferenceConfig; + +typedef struct Participant { + const char *jid; + const char *muc_jid; + const char *nick; +} Participant; + +struct Context *gstmeet_init(void); + +void gstmeet_deinit(struct Context *context); + +JitsiConnection *gstmeet_connection_new(struct Context *context, + const char *websocket_url, + const char *xmpp_domain); + +void gstmeet_connection_free(JitsiConnection *connection); + +bool gstmeet_connection_connect(struct Context *context, JitsiConnection *connection); + +JitsiConference *gstmeet_connection_join_conference(struct Context *context, + JitsiConnection *connection, + GMainContext *glib_main_context, + const struct ConferenceConfig *config); + +bool gstmeet_conference_connected(struct Context *context, JitsiConference *conference); + +bool gstmeet_conference_leave(struct Context *context, JitsiConference *conference); + +bool gstmeet_conference_set_muted(struct Context *context, + JitsiConference *conference, + MediaType media_type, + bool muted); + +GstPipeline *gstmeet_conference_pipeline(struct Context *context, JitsiConference *conference); + +GstElement *gstmeet_conference_audio_sink_element(struct Context *context, + JitsiConference *conference); + +GstElement *gstmeet_conference_video_sink_element(struct Context *context, + JitsiConference *conference); + +void gstmeet_conference_on_participant(struct Context *context, + JitsiConference *conference, + GstBin *(*f)(struct Participant)); + +#endif /* gstmeet_h */ diff --git a/lib-gst-meet-c/src/lib.rs b/lib-gst-meet-c/src/lib.rs new file mode 100644 index 0000000..38f35f9 --- /dev/null +++ b/lib-gst-meet-c/src/lib.rs @@ -0,0 +1,211 @@ +use std::{ + ffi::{CStr, CString}, + os::raw::c_char, + ptr, +}; + +use anyhow::Result; +use glib::{ffi::GMainContext, translate::{from_glib_full, ToGlibPtr}}; +pub use lib_gst_meet::{JitsiConference, JitsiConnection, MediaType}; +use lib_gst_meet::JitsiConferenceConfig; +use tokio::runtime::Runtime; + +pub struct Context { + runtime: Runtime, +} + +#[repr(C)] +pub struct ConferenceConfig { + pub muc: *const c_char, + pub focus: *const c_char, + pub nick: *const c_char, + pub region: *const c_char, + pub video_codec: *const c_char, +} + +#[repr(C)] +pub struct Participant { + pub jid: *const c_char, + pub muc_jid: *const c_char, + pub nick: *const c_char, +} + +trait ResultExt { + fn ok_raw_or_log(self) -> *mut T; +} + +impl ResultExt for Result { + fn ok_raw_or_log(self) -> *mut T { + match self { + Ok(o) => Box::into_raw(Box::new(o)), + Err(e) => { + eprintln!("lib-gst-meet: {:?}", e); + ptr::null_mut() + } + } + } +} + +#[no_mangle] +pub extern "C" fn gstmeet_init() -> *mut Context { + Runtime::new() + .map(|runtime| Context { runtime }) + .map_err(|e| e.into()) + .ok_raw_or_log() +} + +#[no_mangle] +pub unsafe extern "C" fn gstmeet_deinit(context: *mut Context) { + Box::from_raw(context); +} + +#[no_mangle] +pub unsafe extern "C" fn gstmeet_connection_new( + context: *mut Context, + websocket_url: *const c_char, + xmpp_domain: *const c_char, +) -> *mut JitsiConnection { + let websocket_url = CStr::from_ptr(websocket_url); + let xmpp_domain = CStr::from_ptr(xmpp_domain); + (*context) + .runtime + .block_on(JitsiConnection::new(&websocket_url.to_string_lossy(), &xmpp_domain.to_string_lossy())) + .map(|(connection, background)| { + (*context).runtime.spawn(background); + connection + }) + .ok_raw_or_log() +} + +#[no_mangle] +pub unsafe extern "C" fn gstmeet_connection_free(connection: *mut JitsiConnection) { + Box::from_raw(connection); +} + +#[no_mangle] +pub unsafe extern "C" fn gstmeet_connection_connect(context: *mut Context, connection: *mut JitsiConnection) -> bool { + (*context) + .runtime + .block_on((*connection).connect()) + .map_err(|e| eprintln!("lib-gst-meet: {:?}", e)) + .is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn gstmeet_connection_join_conference( + context: *mut Context, + connection: *mut JitsiConnection, + glib_main_context: *mut GMainContext, + config: *const ConferenceConfig, +) -> *mut JitsiConference { + let muc = match CStr::from_ptr((*config).muc).to_string_lossy().parse() { + Ok(jid) => jid, + Err(e) => { + eprintln!("lib-gst-meet: invalid MUC JID: {:?}", e); + return ptr::null_mut(); + }, + }; + let focus = match CStr::from_ptr((*config).focus).to_string_lossy().parse() { + Ok(jid) => jid, + Err(e) => { + eprintln!("lib-gst-meet: invalid focus JID: {:?}", e); + return ptr::null_mut(); + }, + }; + let config = JitsiConferenceConfig { + muc, + focus, + nick: CStr::from_ptr((*config).nick).to_string_lossy().to_string(), + region: CStr::from_ptr((*config).region).to_string_lossy().to_string(), + video_codec: CStr::from_ptr((*config).video_codec).to_string_lossy().to_string(), + }; + (*context) + .runtime + .block_on((*connection).join_conference(from_glib_full(glib_main_context), config)) + .ok_raw_or_log() +} + +#[no_mangle] +pub unsafe extern "C" fn gstmeet_conference_connected(context: *mut Context, conference: *mut JitsiConference) -> bool { + (*context) + .runtime + .block_on((*conference).connected()) + .map_err(|e| eprintln!("lib-gst-meet: {:?}", e)) + .is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn gstmeet_conference_leave(context: *mut Context, conference: *mut JitsiConference) -> bool { + (*context) + .runtime + .block_on(Box::from_raw(conference).connected()) + .map_err(|e| eprintln!("lib-gst-meet: {:?}", e)) + .is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn gstmeet_conference_set_muted(context: *mut Context, conference: *mut JitsiConference, media_type: MediaType, muted: bool) -> bool { + (*context) + .runtime + .block_on((*conference).set_muted(media_type, muted)) + .map_err(|e| eprintln!("lib-gst-meet: {:?}", e)) + .is_ok() +} + +#[no_mangle] +pub unsafe extern "C" fn gstmeet_conference_pipeline(context: *mut Context, conference: *mut JitsiConference) -> *mut gstreamer::ffi::GstPipeline { + (*context) + .runtime + .block_on((*conference).pipeline()) + .map(|pipeline| pipeline.to_glib_full()) + .map_err(|e| eprintln!("lib-gst-meet: {:?}", e)) + .unwrap_or(ptr::null_mut()) +} + +#[no_mangle] +pub unsafe extern "C" fn gstmeet_conference_audio_sink_element(context: *mut Context, conference: *mut JitsiConference) -> *mut gstreamer::ffi::GstElement { + (*context) + .runtime + .block_on((*conference).audio_sink_element()) + .map(|pipeline| pipeline.to_glib_full()) + .map_err(|e| eprintln!("lib-gst-meet: {:?}", e)) + .unwrap_or(ptr::null_mut()) +} + +#[no_mangle] +pub unsafe extern "C" fn gstmeet_conference_video_sink_element(context: *mut Context, conference: *mut JitsiConference) -> *mut gstreamer::ffi::GstElement { + (*context) + .runtime + .block_on((*conference).video_sink_element()) + .map(|pipeline| pipeline.to_glib_full()) + .map_err(|e| eprintln!("lib-gst-meet: {:?}", e)) + .unwrap_or(ptr::null_mut()) +} + +#[no_mangle] +pub unsafe extern "C" fn gstmeet_conference_on_participant( + context: *mut Context, + conference: *mut JitsiConference, + f: unsafe extern "C" fn(Participant) -> *mut gstreamer::ffi::GstBin, +) { + (*context) + .runtime + .block_on((*conference).on_participant(move |participant| Box::pin(async move { + let participant = Participant { + jid: CString::new(participant.jid.to_string())?.into_raw() as *const _, + muc_jid: CString::new(participant.muc_jid.to_string())?.into_raw() as *const _, + nick: participant + .nick + .map(|nick| Ok::<_, anyhow::Error>(CString::new(nick)?.into_raw() as *const _)) + .transpose()? + .unwrap_or_else(ptr::null), + }; + let maybe_bin = f(participant); + if maybe_bin.is_null() { + Ok(None) + } + else { + Ok(Some(from_glib_full(maybe_bin))) + } + }))); +} \ No newline at end of file diff --git a/lib-gst-meet/src/source.rs b/lib-gst-meet/src/source.rs index 86ffba0..ac67277 100644 --- a/lib-gst-meet/src/source.rs +++ b/lib-gst-meet/src/source.rs @@ -6,6 +6,7 @@ pub struct Source { } #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] +#[repr(C)] pub enum MediaType { Video, Audio, diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..d2d4085 --- /dev/null +++ b/shell.nix @@ -0,0 +1,9 @@ +{ pkgs ? import {} }: + +pkgs.mkShell { + name = "gst-meet"; + buildInputs = with pkgs; [ + glib + pkg-config + ]; +}