Add screenshare support for Android

This commit is contained in:
adel-signal 2026-04-15 13:04:47 -07:00 committed by GitHub
parent 987f011769
commit ccd8601c75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 253 additions and 13 deletions

1
.gitignore vendored
View File

@ -19,3 +19,4 @@ local.properties
!.cargo/audit.toml
target
.spr.yml
src/android/.settings

View File

@ -401,7 +401,10 @@ public class CallManager {
/**
*
* Indication from application to proceed with call
* Indication from application to proceed with call. Defaults call to:
* - videoEnabled == false
* - audioEnabled == false
* - isScreenshare == false
*
* @param callId callId for the call
* @param context Call service context
@ -832,17 +835,39 @@ public class CallManager {
* @throws CallException for native code failures
*
*/
public void setVideoEnable(boolean enable)
public void setVideoEnable(boolean enable, boolean isScreenShare)
throws CallException
{
checkCallManagerExists();
CallContext callContext = ringrtcGetActiveCallContext(nativeCallManager);
callContext.setVideoEnabled(enable);
callContext.setOutgoingVideoIsScreenShare(isScreenShare);
ringrtcSetVideoEnable(nativeCallManager, enable);
}
/**
*
* Notification from application to indicate whether the outgoing video
* is a screen share.
*
* @param isScreenShare if true, the outgoing video is a screen share
*
* @throws CallException for native code failures
*
*/
public void setOutgoingVideoIsScreenShare(boolean isScreenShare)
throws CallException
{
checkCallManagerExists();
CallContext callContext = ringrtcGetActiveCallContext(nativeCallManager);
callContext.setOutgoingVideoIsScreenShare(isScreenShare);
ringrtcSetOutgoingVideoIsScreenShare(nativeCallManager, isScreenShare);
}
/**
*
* Sets a data mode, allowing the client to limit the media bandwidth used.
@ -1243,7 +1268,10 @@ public class CallManager {
/**
*
* Creates and returns a GroupCall object.
* Creates and returns a GroupCall object. Defaults call to:
* - videoEnabled == false
* - audioEnabled == false
* - isScreenshare == false
*
* If there is any error when allocating resources for the object,
* null is returned.
@ -1289,7 +1317,11 @@ public class CallManager {
/**
*
* Creates and returns a GroupCall object for a call link call.
* Creates and returns a GroupCall object for a call link call. Defaults call to:
* - videoEnabled == false
* - audioEnabled == false
* - isScreenshare == false
*
*
* If there is any error when allocating resources for the object,
* null is returned.
@ -1883,6 +1915,14 @@ public class CallManager {
*/
static class CallContext {
public static final int CAMERA_MAX_WIDTH = 1280;
public static final int CAMERA_MAX_HEIGHT = 720;
public static final int CAMERA_MAX_FPS = 30;
public static final int SCREENSHARE_MAX_FPS = 30;
@NonNull private final String TAG = CallManager.CallContext.class.getSimpleName();
/** CallId */
@NonNull public final CallId callId;
@ -1943,6 +1983,20 @@ public class CallManager {
}
}
void setOutgoingVideoIsScreenShare(boolean isScreenShare) {
if (this.videoSource != null) {
this.videoSource.setIsScreencast(isScreenShare);
if (isScreenShare) {
this.videoSource.adaptOutputFormat(VideoSource.AspectRatio.UNDEFINED, null, VideoSource.AspectRatio.UNDEFINED, null, SCREENSHARE_MAX_FPS);
} else {
this.videoSource.adaptOutputFormat(CAMERA_MAX_WIDTH, CAMERA_MAX_HEIGHT, CAMERA_MAX_FPS);
}
} else {
Log.w(TAG, "setOutgoingVideoIsScreenShare(): tried to set isScreenshare but there is no VideoSource");
}
}
void dispose() {
Log.i(TAG, "dispose(): " + callId);
@ -2620,6 +2674,10 @@ public class CallManager {
void ringrtcSetVideoEnable(long nativeCallManager, boolean enable)
throws CallException;
private native
void ringrtcSetOutgoingVideoIsScreenShare(long nativeCallManager, boolean isScreenShare)
throws CallException;
private native
void ringrtcUpdateDataMode(long nativeCallManager, int dataMode)
throws CallException;

View File

@ -40,6 +40,14 @@ public final class GroupCall {
public static final int INVALID_CLIENT_ID = 0;
public static final int CAMERA_MAX_WIDTH = 640;
public static final int CAMERA_MAX_HEIGHT = 360;
public static final int CAMERA_MAX_FPS = 30;
public static final int SCREENSHARE_MAX_FPS = 30;
@NonNull private static final String TAG = GroupCall.class.getSimpleName();
@NonNull private Kind kind;
@ -101,7 +109,7 @@ public final class GroupCall {
this.outgoingAudioTrack.setEnabled(false);
}
this.outgoingVideoSource = factory.createVideoSource(false);
this.outgoingVideoSource = factory.createVideoSource(/* isScreencast */false);
if (this.outgoingVideoSource == null) {
return;
}
@ -114,14 +122,17 @@ public final class GroupCall {
this.outgoingVideoTrack.setEnabled(false);
}
// Define maximum output video format for group calls.
this.outgoingVideoSource.adaptOutputFormat(640, 360, 30);
// Define maximum camera video format for group calls.
this.outgoingVideoSource.adaptOutputFormat(CAMERA_MAX_WIDTH, CAMERA_MAX_HEIGHT, CAMERA_MAX_FPS);
this.incomingVideoTracks = new ArrayList<>();
}
/**
* Creates a GroupCall object.
* Creates a GroupCall object. Defaults call to:
* - videoEnabled == false
* - audioEnabled == false
* - isScreenshare == false
*
* Will return null on failure. Should only be accessed via the CallManager.createGroupCall().
*/
@ -436,17 +447,62 @@ public final class GroupCall {
* @throws CallException for native code failures
*
*/
public void setOutgoingVideoMuted(boolean muted)
public void setOutgoingVideoMuted(boolean muted, boolean isScreenShare)
throws CallException
{
Log.i(TAG, "setOutgoingVideoMuted():");
this.localDeviceState.videoMuted = muted;
this.outgoingVideoTrack.setEnabled(!this.localDeviceState.videoMuted);
this.setOutgoingVideoIsScreenShare(isScreenShare);
ringrtcSetOutgoingVideoMuted(nativeCallManager, this.clientId, muted);
}
/**
*
* Indicates whether the outgoing video is a screen share.
* This updates the local device state and sends the status to the SFU.
*
* @param isScreenShare true if the outgoing video is a screen share
*
* @throws CallException for native code failures
*
*/
public void setOutgoingVideoIsScreenShare(boolean isScreenShare)
throws CallException
{
Log.i(TAG, String.format("setOutgoingVideoIsScreenShare(): %b", isScreenShare));
this.outgoingVideoSource.setIsScreencast(isScreenShare);
if (isScreenShare) {
this.outgoingVideoSource.adaptOutputFormat(VideoSource.AspectRatio.UNDEFINED, null, VideoSource.AspectRatio.UNDEFINED, null, SCREENSHARE_MAX_FPS);
} else {
this.outgoingVideoSource.adaptOutputFormat(CAMERA_MAX_WIDTH, CAMERA_MAX_HEIGHT, CAMERA_MAX_FPS);
}
this.localDeviceState.sharingScreen = isScreenShare;
ringrtcSetOutgoingVideoIsScreenShare(nativeCallManager, this.clientId, isScreenShare);
}
/**
*
* Indicates whether the user is presenting.
* This updates the local device state and sends the status to the SFU.
*
* @param isPresenting true if the outgoing video is a screen share
*
* @throws CallException for native code failures
*
*/
public void setPresenting(boolean isPresenting)
throws CallException
{
Log.i(TAG, "setPresenting():");
this.localDeviceState.presenting = isPresenting;
ringrtcSetPresenting(nativeCallManager, this.clientId, isPresenting);
}
/**
*
* Links the camera to the outgoing video track.
@ -990,6 +1046,8 @@ public final class GroupCall {
JoinState joinState;
boolean audioMuted;
boolean videoMuted;
boolean presenting;
boolean sharingScreen;
NetworkRoute networkRoute;
int audioLevel;
@Nullable Long demuxId;
@ -999,6 +1057,8 @@ public final class GroupCall {
this.joinState = JoinState.NOT_JOINED;
this.audioMuted = true;
this.videoMuted = true;
this.presenting = false;
this.sharingScreen = false;
this.networkRoute = new NetworkRoute();
this.audioLevel = 0;
}
@ -1008,6 +1068,8 @@ public final class GroupCall {
this.joinState = localDeviceState.joinState;
this.audioMuted = localDeviceState.audioMuted;
this.videoMuted = localDeviceState.videoMuted;
this.presenting = localDeviceState.presenting;
this.sharingScreen = localDeviceState.sharingScreen;
this.networkRoute = localDeviceState.networkRoute;
this.audioLevel = localDeviceState.audioLevel;
this.demuxId = localDeviceState.demuxId;
@ -1029,6 +1091,14 @@ public final class GroupCall {
return videoMuted;
}
public boolean getPresenting() {
return presenting;
}
public boolean getSharingScreen() {
return sharingScreen;
}
public NetworkRoute getNetworkRoute() {
return networkRoute;
}
@ -1375,6 +1445,18 @@ public final class GroupCall {
boolean muted)
throws CallException;
private native
void ringrtcSetPresenting(long nativeCallManager,
long clientId,
boolean isPresenting)
throws CallException;
private native
void ringrtcSetOutgoingVideoIsScreenShare(long nativeCallManager,
long clientId,
boolean isScreenShare)
throws CallException;
private native
void ringrtcRing( long nativeCallManager,
long clientId,

View File

@ -561,6 +561,25 @@ pub unsafe extern "C" fn Java_org_signal_ringrtc_CallManager_ringrtcSetVideoEnab
}
}
#[unsafe(no_mangle)]
#[allow(non_snake_case)]
pub unsafe extern "C" fn Java_org_signal_ringrtc_CallManager_ringrtcSetOutgoingVideoIsScreenShare(
mut env: JNIEnv,
_object: JObject,
call_manager: jlong,
is_screenshare: jboolean,
) {
match call_manager::set_outgoing_video_is_screenshare(
call_manager as *mut AndroidCallManager,
is_screenshare != 0,
) {
Ok(v) => v,
Err(e) => {
error::throw_error(&mut env, e);
}
}
}
#[unsafe(no_mangle)]
#[allow(non_snake_case)]
pub extern "C" fn Java_org_signal_ringrtc_CallManager_ringrtcUpdateDataMode(
@ -1057,6 +1076,48 @@ pub unsafe extern "C" fn Java_org_signal_ringrtc_GroupCall_ringrtcSetOutgoingVid
}
}
#[unsafe(no_mangle)]
#[allow(non_snake_case)]
pub unsafe extern "C" fn Java_org_signal_ringrtc_GroupCall_ringrtcSetPresenting(
mut env: JNIEnv,
_object: JObject,
call_manager: jlong,
client_id: jlong,
presenting: jboolean,
) {
match call_manager::set_presenting(
call_manager as *mut AndroidCallManager,
client_id as group_call::ClientId,
presenting != 0,
) {
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_GroupCall_ringrtcSetOutgoingVideoIsScreenShare(
mut env: JNIEnv,
_object: JObject,
call_manager: jlong,
client_id: jlong,
is_screenshare: jboolean,
) {
match call_manager::set_outgoing_group_call_video_is_screenshare(
call_manager as *mut AndroidCallManager,
client_id as group_call::ClientId,
is_screenshare != 0,
) {
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_GroupCall_ringrtcRing(

View File

@ -558,6 +558,20 @@ pub fn set_video_enable(call_manager: *mut AndroidCallManager, enable: bool) ->
}
}
/// CMI request to set whether the outgoing video is a screen share
pub fn set_outgoing_video_is_screenshare(
call_manager: *mut AndroidCallManager,
is_screenshare: bool,
) -> Result<()> {
let call_manager = unsafe { ptr_as_mut(call_manager)? };
if let Ok(mut active_connection) = call_manager.active_connection() {
active_connection.send_is_screenshare_update(is_screenshare)
} else {
Ok(())
}
}
/// Request to update the data mode on the direct connection
pub fn update_data_mode(call_manager: *mut AndroidCallManager, data_mode: DataMode) -> Result<()> {
let call_manager = unsafe { ptr_as_mut(call_manager)? };
@ -1051,6 +1065,26 @@ pub fn set_outgoing_video_muted(
Ok(())
}
pub fn set_presenting(
call_manager: *mut AndroidCallManager,
client_id: group_call::ClientId,
presenting: bool,
) -> Result<()> {
let call_manager = unsafe { ptr_as_mut(call_manager)? };
call_manager.set_presenting(client_id, presenting);
Ok(())
}
pub fn set_outgoing_group_call_video_is_screenshare(
call_manager: *mut AndroidCallManager,
client_id: group_call::ClientId,
is_screenshare: bool,
) -> Result<()> {
let call_manager = unsafe { ptr_as_mut(call_manager)? };
call_manager.set_sharing_screen(client_id, is_screenshare);
Ok(())
}
pub fn group_ring(
env: &JNIEnv,
call_manager: *mut AndroidCallManager,

View File

@ -1606,6 +1606,13 @@ where
call.connect_incoming_media(incoming_media)
}
pub fn send_is_screenshare_update(&mut self, is_screenshare: bool) -> Result<()> {
self.update_sender_status(signaling::SenderStatus {
sharing_screen: Some(is_screenshare),
..Default::default()
})
}
/// Send a ConnectionEvent to the internal FSM.
fn inject_event(&mut self, event: ConnectionEvent) -> Result<()> {
self.fsm_sender

View File

@ -1376,10 +1376,7 @@ fn setOutgoingVideoIsScreenShare(mut cx: FunctionContext) -> JsResult<JsValue> {
.adapt_output_format(width, height, fps);
if let Ok(mut active_connection) = endpoint.call_manager.active_connection() {
active_connection.update_sender_status(signaling::SenderStatus {
sharing_screen: Some(is_screenshare),
..Default::default()
})?;
active_connection.send_is_screenshare_update(is_screenshare)?;
}
Ok(())
})