Add AssetRegistry to call state, expose addAsset()
This commit is contained in:
parent
0db47267d0
commit
e3c46a7cba
@ -337,6 +337,44 @@ public class CallManager {
|
||||
ringrtcSetSelfUuid(nativeCallManager, Util.getBytesFromUuid(uuid));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Adds an asset to the asset manager by file path.
|
||||
*
|
||||
* @param assetGroup The asset identifier
|
||||
* @param filePath Path to the asset file on disk
|
||||
*
|
||||
* @throws CallException for native code failures
|
||||
*
|
||||
*/
|
||||
public void addAsset(@NonNull String assetGroup, @NonNull String filePath)
|
||||
throws CallException
|
||||
{
|
||||
checkCallManagerExists();
|
||||
|
||||
Log.i(TAG, "addAsset(): by filePath");
|
||||
ringrtcAddAsset(nativeCallManager, assetGroup, filePath, null);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Adds an asset to the asset manager by byte content.
|
||||
*
|
||||
* @param assetGroup The asset identifier
|
||||
* @param content The raw asset bytes
|
||||
*
|
||||
* @throws CallException for native code failures
|
||||
*
|
||||
*/
|
||||
public void addAsset(@NonNull String assetGroup, @NonNull byte[] content)
|
||||
throws CallException
|
||||
{
|
||||
checkCallManagerExists();
|
||||
|
||||
Log.i(TAG, "addAsset(): by content");
|
||||
ringrtcAddAsset(nativeCallManager, assetGroup, null, content);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Indication from application to start a new outgoing call
|
||||
@ -2445,6 +2483,10 @@ public class CallManager {
|
||||
void ringrtcSetSelfUuid(long nativeCallManager, byte[] uuid)
|
||||
throws CallException;
|
||||
|
||||
private native
|
||||
void ringrtcAddAsset(long nativeCallManager, String assetGroup, String filePath, byte[] content)
|
||||
throws CallException;
|
||||
|
||||
private native
|
||||
long ringrtcCreatePeerConnection(long nativePeerConnectionFactory,
|
||||
long nativeConnection,
|
||||
|
||||
@ -448,6 +448,42 @@ public class CallManager<CallType, CallManagerDelegateType>: CallManagerInterfac
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func addAsset(assetGroup: String, filePath: String) throws {
|
||||
Logger.debug("addAsset by filePath")
|
||||
|
||||
let assetGroupSlice = allocatedAppByteSliceFromString(maybe_string: assetGroup)
|
||||
let filePathSlice = allocatedAppByteSliceFromString(maybe_string: filePath)
|
||||
let emptySlice = AppByteSlice(bytes: nil, len: 0)
|
||||
defer {
|
||||
assetGroupSlice.bytes?.deallocate()
|
||||
filePathSlice.bytes?.deallocate()
|
||||
}
|
||||
|
||||
let retPtr = ringrtcAddAsset(ringRtcCallManager, assetGroupSlice, filePathSlice, emptySlice)
|
||||
if retPtr == nil {
|
||||
throw CallManagerError.apiFailed(description: "addAsset() by filepath function failure")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func addAsset(assetGroup: String, content: Data) throws {
|
||||
Logger.debug("addAsset by content")
|
||||
|
||||
let assetGroupSlice = allocatedAppByteSliceFromString(maybe_string: assetGroup)
|
||||
let emptySlice = AppByteSlice(bytes: nil, len: 0)
|
||||
let contentSlice = allocatedAppByteSliceFromData(maybe_data: content)
|
||||
defer {
|
||||
assetGroupSlice.bytes?.deallocate()
|
||||
contentSlice.bytes?.deallocate()
|
||||
}
|
||||
|
||||
let retPtr = ringrtcAddAsset(ringRtcCallManager, assetGroupSlice, emptySlice, contentSlice)
|
||||
if retPtr == nil {
|
||||
throw CallManagerError.apiFailed(description: "addAsset() by content function failure")
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Close the RingRTC Call Manager.
|
||||
let retPtr = ringrtcClose(self.ringRtcCallManager)
|
||||
|
||||
@ -79,6 +79,7 @@ class NativeCallManager {
|
||||
// Mirror methods onto NativeCallManager.
|
||||
// This is done through direct assignment rather than wrapper methods to avoid indirection.
|
||||
(NativeCallManager.prototype as any).setSelfUuid = Native.cm_setSelfUuid;
|
||||
(NativeCallManager.prototype as any).addAsset = Native.cm_addAsset;
|
||||
(NativeCallManager.prototype as any).createOutgoingCall =
|
||||
Native.cm_createOutgoingCall;
|
||||
(NativeCallManager.prototype as any).proceed = Native.cm_proceed;
|
||||
@ -494,6 +495,16 @@ export class RingRTCType {
|
||||
this.callManager.setSelfUuid(uuid);
|
||||
}
|
||||
|
||||
// Called by UX
|
||||
addAsset(
|
||||
assetGroup: string,
|
||||
asset: { filePath: string } | { content: Uint8Array }
|
||||
): void {
|
||||
const filePath = 'filePath' in asset ? asset.filePath : null;
|
||||
const content = 'content' in asset ? asset.content : null;
|
||||
this.callManager.addAsset(assetGroup, filePath, content);
|
||||
}
|
||||
|
||||
// Called by UX
|
||||
startOutgoingCall(
|
||||
remoteUserId: UserId,
|
||||
@ -2940,6 +2951,11 @@ export enum RingCancelReason {
|
||||
export interface CallManager {
|
||||
setConfig(config: Config): void;
|
||||
setSelfUuid(uuid: Uint8Array): void;
|
||||
addAsset(
|
||||
assetGroup: string,
|
||||
filePath: string | null,
|
||||
content: Uint8Array | null
|
||||
): void;
|
||||
createOutgoingCall(
|
||||
remoteUserId: UserId,
|
||||
isVideoCall: boolean,
|
||||
|
||||
@ -111,6 +111,30 @@ pub unsafe extern "C" fn Java_org_signal_ringrtc_CallManager_ringrtcSetSelfUuid(
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe extern "C" fn Java_org_signal_ringrtc_CallManager_ringrtcAddAsset(
|
||||
mut env: JNIEnv,
|
||||
_object: JObject,
|
||||
call_manager: jlong,
|
||||
asset_group: JString,
|
||||
file_path: JString,
|
||||
content: JByteArray,
|
||||
) {
|
||||
match call_manager::add_asset(
|
||||
&mut env,
|
||||
call_manager as *mut AndroidCallManager,
|
||||
asset_group,
|
||||
file_path,
|
||||
content,
|
||||
) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error::throw_error(&mut env, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
#[allow(non_snake_case)]
|
||||
pub unsafe extern "C" fn Java_org_signal_ringrtc_CallManager_ringrtcCall(
|
||||
|
||||
@ -169,6 +169,33 @@ pub fn set_self_uuid(
|
||||
call_manager.set_self_uuid(env.convert_byte_array(uuid)?)
|
||||
}
|
||||
|
||||
/// Adds an asset to the asset manager.
|
||||
pub fn add_asset(
|
||||
env: &mut JNIEnv,
|
||||
call_manager: *mut AndroidCallManager,
|
||||
asset_group: JString,
|
||||
file_path: JString,
|
||||
content: JByteArray,
|
||||
) -> Result<()> {
|
||||
use crate::core::assets::AssetHandle;
|
||||
|
||||
let asset_group: String = env.get_string(&asset_group)?.into();
|
||||
|
||||
let handle = if !content.is_null() {
|
||||
AssetHandle::Content(env.convert_byte_array(content)?)
|
||||
} else if !file_path.is_null() {
|
||||
let path: String = env.get_string(&file_path)?.into();
|
||||
AssetHandle::FilePath(path)
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"addAsset requires either a filePath or content"
|
||||
));
|
||||
};
|
||||
|
||||
let call_manager = unsafe { ptr_as_mut(call_manager)? };
|
||||
call_manager.add_asset(&asset_group, handle)
|
||||
}
|
||||
|
||||
/// Application notification to start a new call
|
||||
pub fn call(
|
||||
env: &JNIEnv,
|
||||
|
||||
@ -14,6 +14,7 @@ use log::info;
|
||||
use ringrtc::{
|
||||
common::{CallEndReason, units::DataRate},
|
||||
core::{
|
||||
assets::AssetRegistry,
|
||||
call_mutex::CallMutex,
|
||||
call_summary::CallSummary,
|
||||
endorsements::EndorsementUpdateResultRef,
|
||||
@ -26,8 +27,7 @@ use ringrtc::{
|
||||
http::sim as sim_http,
|
||||
sfu::{DemuxId, MemberMap, ObfuscatedResolver, PeekInfo, UserId},
|
||||
},
|
||||
protobuf,
|
||||
protobuf::signaling::CallMessage,
|
||||
protobuf::{self, signaling::CallMessage},
|
||||
webrtc::{
|
||||
media::{VideoFrame, VideoFrameMetadata, VideoPixelFormat, VideoSink, VideoTrack},
|
||||
peer_connection::{AudioLevel, ReceivedAudioLevel, SendRates},
|
||||
@ -302,6 +302,7 @@ fn main() {
|
||||
ring_id: None,
|
||||
audio_levels_interval: None,
|
||||
group_send_endorsement_cache: None,
|
||||
asset_registry: AssetRegistry::default(),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ use crate::{
|
||||
actor::{Actor, Stopper},
|
||||
},
|
||||
core::{
|
||||
assets::AssetRegistry,
|
||||
call_fsm::{CallEvent, CallStateMachine},
|
||||
call_manager::CallManager,
|
||||
call_mutex::CallMutex,
|
||||
@ -110,6 +111,8 @@ where
|
||||
forking: Arc<CallMutex<Option<ForkingState<T>>>>,
|
||||
/// Captures call statistics for reporting.
|
||||
call_summary: DirectCallSummary,
|
||||
/// Registry for accessing large, pre-loaded assets
|
||||
asset_registry: AssetRegistry,
|
||||
}
|
||||
|
||||
impl<T> fmt::Display for Call<T>
|
||||
@ -192,6 +195,7 @@ where
|
||||
),
|
||||
forking: Arc::clone(&self.forking),
|
||||
call_summary: self.call_summary.clone(),
|
||||
asset_registry: self.asset_registry.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -210,6 +214,7 @@ where
|
||||
call_manager: CallManager<T>,
|
||||
) -> Result<Self> {
|
||||
info!("new(): call_id: {}", call_id);
|
||||
let asset_registry = call_manager.asset_registry()?;
|
||||
|
||||
// create a FSM worker for this connection
|
||||
let (fsm_sender, fsm_receiver) = std::sync::mpsc::sync_channel(256);
|
||||
@ -237,6 +242,7 @@ where
|
||||
did_notify_application_of_remote_ringing: Arc::new(AtomicBool::new(false)),
|
||||
forking: Arc::new(CallMutex::new(None, "forking")),
|
||||
call_summary: DirectCallSummary::default(),
|
||||
asset_registry,
|
||||
};
|
||||
|
||||
Ok(call)
|
||||
|
||||
@ -28,8 +28,10 @@ use crate::{
|
||||
actor::{Actor, Stopper},
|
||||
},
|
||||
core::{
|
||||
assets::{self, AssetManager, AssetRegistry},
|
||||
call::Call,
|
||||
call_mutex::CallMutex,
|
||||
call_rwlock::CallRwLock,
|
||||
call_summary::CallSummary,
|
||||
connection::{Connection, ConnectionType},
|
||||
endorsements::{EndorsementUpdateResultRef, EndorsementsCache},
|
||||
@ -417,6 +419,12 @@ where
|
||||
message_queue: Arc<CallMutex<SignalingMessageQueue<T>>>,
|
||||
/// How to make HTTP requests to the SFU for group calls.
|
||||
http_client: http::DelegatingClient,
|
||||
/// Asset manager that holds a copy of assets in memory
|
||||
/// Assets are initialized before the start of a call by
|
||||
/// the application using the platform addAsset() function
|
||||
/// Manager can be used to inject a registry handle into
|
||||
/// call state.
|
||||
asset_manager: Arc<CallRwLock<AssetManager>>,
|
||||
}
|
||||
|
||||
impl<T> fmt::Display for CallManager<T>
|
||||
@ -482,6 +490,7 @@ where
|
||||
worker: self.worker.clone(),
|
||||
message_queue: Arc::clone(&self.message_queue),
|
||||
http_client: self.http_client.clone(),
|
||||
asset_manager: self.asset_manager.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -529,6 +538,10 @@ where
|
||||
"message_queue",
|
||||
)),
|
||||
http_client,
|
||||
asset_manager: Arc::new(CallRwLock::new(
|
||||
AssetManager::new(assets::manifest::supported_assets()),
|
||||
"asset-manager",
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
@ -536,6 +549,19 @@ where
|
||||
&self.http_client
|
||||
}
|
||||
|
||||
pub fn asset_registry(&self) -> Result<AssetRegistry> {
|
||||
Ok(self.asset_manager.read()?.get_registry())
|
||||
}
|
||||
|
||||
/// Adds an asset to the asset manager, verifying it against the supported assets manifest.
|
||||
pub fn add_asset(&self, asset_group: &str, handle: assets::AssetHandle) -> Result<()> {
|
||||
info!("Adding asset for asset group {asset_group}");
|
||||
self.asset_manager
|
||||
.write()?
|
||||
.add_asset_for_feature(asset_group, handle)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the current user's UUID.
|
||||
pub fn set_self_uuid(&mut self, uuid: UserId) -> Result<()> {
|
||||
info!("set_self_uuid():");
|
||||
@ -3041,6 +3067,8 @@ where
|
||||
let obfuscated_resolver =
|
||||
ObfuscatedResolver::new(Arc::new(MemberMap::new(&[])), None, None);
|
||||
|
||||
let asset_registry = { self.asset_manager.read()?.get_registry() };
|
||||
|
||||
let client = Client::start(ClientStartParams {
|
||||
group_id,
|
||||
client_id,
|
||||
@ -3050,6 +3078,7 @@ where
|
||||
obfuscated_resolver,
|
||||
busy: self.busy.clone(),
|
||||
self_uuid: self.self_uuid.clone(),
|
||||
asset_registry,
|
||||
peer_connection_factory,
|
||||
outgoing_audio_track,
|
||||
outgoing_video_track: Some(outgoing_video_track),
|
||||
@ -3144,6 +3173,8 @@ where
|
||||
Some(endorsements_public_key),
|
||||
);
|
||||
|
||||
let asset_registry = { self.asset_manager.read()?.get_registry() };
|
||||
|
||||
let client = Client::start(ClientStartParams {
|
||||
group_id: room_id,
|
||||
client_id,
|
||||
@ -3153,6 +3184,7 @@ where
|
||||
obfuscated_resolver,
|
||||
busy: self.busy.clone(),
|
||||
self_uuid: self.self_uuid.clone(),
|
||||
asset_registry,
|
||||
peer_connection_factory,
|
||||
outgoing_audio_track,
|
||||
outgoing_video_track: Some(outgoing_video_track),
|
||||
|
||||
@ -35,6 +35,7 @@ use crate::{
|
||||
units::DataRate,
|
||||
},
|
||||
core::{
|
||||
assets::AssetRegistry,
|
||||
call_mutex::CallMutex,
|
||||
call_summary::{CallSummary, GroupCallSummary},
|
||||
crypto::{self as frame_crypto, DecryptionErrorStats},
|
||||
@ -1014,6 +1015,9 @@ struct State {
|
||||
// Shared state with the CallManager that might change
|
||||
busy: Arc<CallMutex<bool>>,
|
||||
self_uuid: Arc<CallMutex<Option<UserId>>>,
|
||||
#[allow(dead_code)]
|
||||
/// Registry of large, preloaded assets
|
||||
asset_registry: AssetRegistry,
|
||||
|
||||
// State that changes regularly and is sent to the observer
|
||||
connection_state: ConnectionState,
|
||||
@ -1234,6 +1238,7 @@ pub struct ClientStartParams {
|
||||
pub observer: Box<dyn Observer + Send>,
|
||||
pub busy: Arc<CallMutex<bool>>,
|
||||
pub self_uuid: Arc<CallMutex<Option<UserId>>>,
|
||||
pub asset_registry: AssetRegistry,
|
||||
pub peer_connection_factory: Option<PeerConnectionFactory>,
|
||||
pub outgoing_audio_track: AudioTrack,
|
||||
pub outgoing_video_track: Option<VideoTrack>,
|
||||
@ -1254,6 +1259,7 @@ impl Client {
|
||||
observer,
|
||||
busy,
|
||||
self_uuid,
|
||||
asset_registry,
|
||||
peer_connection_factory,
|
||||
outgoing_audio_track,
|
||||
outgoing_video_track,
|
||||
@ -1342,6 +1348,7 @@ impl Client {
|
||||
observer,
|
||||
busy,
|
||||
self_uuid,
|
||||
asset_registry,
|
||||
local_ice_ufrag,
|
||||
local_ice_pwd,
|
||||
|
||||
@ -5326,7 +5333,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
common::time::saturating_epoch_time,
|
||||
core::endorsements::EndorsementUpdateResult,
|
||||
core::{assets::AssetRegistry, endorsements::EndorsementUpdateResult},
|
||||
lite::{
|
||||
call_links::{CallLinkMemberResolver, CallLinkRootKey},
|
||||
sfu::{MemberResolver, PeekDeviceInfo},
|
||||
@ -6105,6 +6112,7 @@ mod tests {
|
||||
let observer = FakeObserver::new(user_id.clone());
|
||||
let fake_busy = Arc::new(CallMutex::new(false, "fake_busy"));
|
||||
let fake_self_uuid = Arc::new(CallMutex::new(Some(user_id.clone()), "fake_self_uuid"));
|
||||
let fake_asset_registry = AssetRegistry::default();
|
||||
let fake_audio_track = AudioTrack::new(
|
||||
webrtc::Arc::from_owned(unsafe {
|
||||
webrtc::ptr::OwnedRc::from_ptr(&FAKE_AUDIO_TRACK as *const u32)
|
||||
@ -6127,6 +6135,7 @@ mod tests {
|
||||
observer: Box::new(observer.clone()),
|
||||
busy: fake_busy,
|
||||
self_uuid: fake_self_uuid,
|
||||
asset_registry: fake_asset_registry,
|
||||
peer_connection_factory: None,
|
||||
outgoing_audio_track: fake_audio_track,
|
||||
outgoing_video_track: None,
|
||||
|
||||
@ -26,6 +26,7 @@ use strum::IntoDiscriminant;
|
||||
use crate::{
|
||||
common::{CallConfig, CallId, CallMediaType, DataMode, DeviceId, Result},
|
||||
core::{
|
||||
assets::AssetHandle,
|
||||
call_manager::CallManager,
|
||||
call_summary::{CallSummary, MediaQualityStats, QualityStats},
|
||||
group_call::{self, GroupId, SignalingMessageUrgency},
|
||||
@ -856,6 +857,33 @@ fn setSelfUuid(mut cx: FunctionContext) -> JsResult<JsValue> {
|
||||
Ok(cx.undefined().upcast())
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn addAsset(mut cx: FunctionContext) -> JsResult<JsValue> {
|
||||
debug!("JsCallManager.addAsset()");
|
||||
|
||||
let asset_group = cx.argument::<JsString>(0)?.value(&mut cx);
|
||||
let file_path = cx
|
||||
.argument_opt(1)
|
||||
.and_then(|v| v.downcast::<JsString, _>(&mut cx).ok())
|
||||
.map(|s| s.value(&mut cx));
|
||||
let content = cx
|
||||
.argument_opt(2)
|
||||
.and_then(|v| v.downcast::<JsUint8Array, _>(&mut cx).ok())
|
||||
.map(|buf| buf.as_slice(&cx).to_vec());
|
||||
|
||||
let handle = match (file_path, content) {
|
||||
(_, Some(bytes)) => AssetHandle::Content(bytes),
|
||||
(Some(path), _) => AssetHandle::FilePath(path),
|
||||
_ => return cx.throw_error("addAsset requires either a filePath or content"),
|
||||
};
|
||||
|
||||
with_call_endpoint(&mut cx, |endpoint| {
|
||||
endpoint.call_manager.add_asset(&asset_group, handle)
|
||||
})
|
||||
.or_else(|err: anyhow::Error| cx.throw_error(format!("{}", err)))?;
|
||||
Ok(cx.undefined().upcast())
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn createOutgoingCall(mut cx: FunctionContext) -> JsResult<JsValue> {
|
||||
let peer_id = cx.argument::<JsString>(0)?.value(&mut cx) as PeerId;
|
||||
@ -3165,6 +3193,7 @@ fn register(mut cx: ModuleContext) -> NeonResult<()> {
|
||||
cx.export_value("callEndpointPropertyKey", js_property_key)?;
|
||||
|
||||
cx.export_function("cm_setSelfUuid", setSelfUuid)?;
|
||||
cx.export_function("cm_addAsset", addAsset)?;
|
||||
cx.export_function("cm_createOutgoingCall", createOutgoingCall)?;
|
||||
cx.export_function("cm_cancelGroupRing", cancelGroupRing)?;
|
||||
cx.export_function("cm_proceed", proceed)?;
|
||||
|
||||
@ -611,6 +611,39 @@ pub extern "C" fn ringrtcSetSelfUuid(callManager: *mut c_void, uuid: AppByteSlic
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
#[allow(non_snake_case)]
|
||||
pub extern "C" fn ringrtcAddAsset(
|
||||
callManager: *mut c_void,
|
||||
assetGroup: AppByteSlice,
|
||||
filePath: AppByteSlice,
|
||||
content: AppByteSlice,
|
||||
) -> *mut c_void {
|
||||
use crate::core::assets::AssetHandle;
|
||||
|
||||
let asset_group = match string_from_app_slice(&assetGroup) {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
error!("Missing assetGroup");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
|
||||
let handle = if let Some(path) = string_from_app_slice(&filePath) {
|
||||
AssetHandle::FilePath(path)
|
||||
} else if let Some(bytes) = byte_vec_from_app_slice(&content) {
|
||||
AssetHandle::Content(bytes)
|
||||
} else {
|
||||
error!("addAsset requires either a filePath or content");
|
||||
return ptr::null_mut();
|
||||
};
|
||||
|
||||
match call_manager::add_asset(callManager as *mut IosCallManager, asset_group, handle) {
|
||||
Ok(_) => callManager,
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[unsafe(no_mangle)]
|
||||
#[allow(non_snake_case)]
|
||||
pub extern "C" fn ringrtcCall(
|
||||
|
||||
@ -51,6 +51,16 @@ pub fn set_self_uuid(call_manager: *mut IosCallManager, uuid: UserId) -> Result<
|
||||
call_manager.set_self_uuid(uuid)
|
||||
}
|
||||
|
||||
/// Adds an asset to the asset manager.
|
||||
pub fn add_asset(
|
||||
call_manager: *mut IosCallManager,
|
||||
asset_group: String,
|
||||
handle: crate::core::assets::AssetHandle,
|
||||
) -> Result<()> {
|
||||
let call_manager = unsafe { ptr_as_mut(call_manager)? };
|
||||
call_manager.add_asset(&asset_group, handle)
|
||||
}
|
||||
|
||||
/// Application notification to start a new call.
|
||||
pub fn call(
|
||||
call_manager: *mut IosCallManager,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user