Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions examples/react-native/WhipWhepDemo/app/whip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ export default function WhipScreen() {
const [isLoading, setIsLoading] = useState(false);
const [shouldShowStreamBtn, setShouldShowStreamBtn] = useState(true);
const [streamMode, setStreamMode] = useState<StreamMode>('selection');
const [isAudioPaused, setIsAudioPaused] = useState(false);
const [isVideoPaused, setIsVideoPaused] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);

const whipClient = useRef<WhipClientViewRef | null>(null);

Expand Down Expand Up @@ -118,6 +121,12 @@ export default function WhipScreen() {
const handleDisconnectBtnClick = async () => {
try {
await whipClient.current?.disconnect();
// The client keeps its pause state across disconnects, so resume
// explicitly to start the next stream from a clean state.
await whipClient.current?.setAudioPaused(false);
await whipClient.current?.setVideoPaused(false);
setIsAudioPaused(false);
setIsVideoPaused(false);
setShouldShowStreamBtn(true);
} catch (error) {
console.error('Failed to disconnect from WHIP Client', error);
Expand Down Expand Up @@ -149,6 +158,32 @@ export default function WhipScreen() {
}
}, []);

const handleToggleAudio = useCallback(async () => {
const paused = !isAudioPaused;
try {
await whipClient.current?.setAudioPaused(paused);
setIsAudioPaused(paused);
setErrorMessage(null);
} catch (error) {
setErrorMessage(
`Failed to ${paused ? 'pause' : 'resume'} audio: ${error}`,
);
}
}, [isAudioPaused]);

const handleToggleVideo = useCallback(async () => {
const paused = !isVideoPaused;
try {
await whipClient.current?.setVideoPaused(paused);
setIsVideoPaused(paused);
setErrorMessage(null);
} catch (error) {
setErrorMessage(
`Failed to ${paused ? 'pause' : 'resume'} video: ${error}`,
);
}
}, [isVideoPaused]);

const handleSetH264VideoCodec = useCallback(async () => {
if (whipClient.current) {
try {
Expand Down Expand Up @@ -233,13 +268,26 @@ export default function WhipScreen() {
</Text>
</View>

{errorMessage && (
<Text style={styles.errorText}>{errorMessage}</Text>
)}

{streamMode === 'camera' && (
<>
<Button title="Switch Camera" onPress={handleSwitchCamera} />
<Button title="Flip Camera" onPress={handleFlipCamera} />
</>
)}

<Button
title={isAudioPaused ? 'Resume Audio' : 'Pause Audio'}
onPress={handleToggleAudio}
/>
<Button
title={isVideoPaused ? 'Resume Video' : 'Pause Video'}
onPress={handleToggleVideo}
/>

{Platform.OS === 'ios' && (
<>
<Button
Expand Down Expand Up @@ -316,6 +364,12 @@ const styles = StyleSheet.create({
fontSize: 16,
fontWeight: '500',
},
errorText: {
color: '#EF4444',
textAlign: 'center',
paddingHorizontal: 16,
marginBottom: 10,
},
selectionContainer: {
paddingVertical: 20,
paddingHorizontal: 16,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ class WhipClient(
private var isSharingScreen: Boolean = false
private val isScreenSharingMode: Boolean

private var isAudioPaused: Boolean = false
private var isVideoPaused: Boolean = false

init {
isScreenSharingMode = configOptions.isScreenSharingMode
if (!isScreenSharingMode) {
Expand Down Expand Up @@ -200,6 +203,8 @@ class WhipClient(
this.audioTrack = audioTrack
}

applyPauseState()

peerConnection?.enforceSendOnlyDirection()
}

Expand Down Expand Up @@ -407,6 +412,34 @@ class WhipClient(
}
}

/**
* Pauses or resumes the audio sent to the server.
*
* @param paused `true` to pause the audio, `false` to resume it.
*/
fun setAudioPaused(paused: Boolean) {
isAudioPaused = paused
audioTrack?.setEnabled(!paused)
}

/**
* Pauses or resumes the video sent to the server.
*
* @param paused `true` to pause the video, `false` to resume it.
*/
fun setVideoPaused(paused: Boolean) {
isVideoPaused = paused
videoTrack?.setEnabled(!paused)
}

/**
* Reapplies the stored pause state to the current tracks.
*/
private fun applyPauseState() {
audioTrack?.setEnabled(!isAudioPaused)
videoTrack?.setEnabled(!isVideoPaused)
}

/**
* Starts screen sharing using MediaProjection.
* Note: MediaProjection permission must be obtained at the Activity level before calling this.
Expand Down Expand Up @@ -476,6 +509,8 @@ class WhipClient(
Log.d(CLIENT_TAG, "Audio track created for screen share")
}

applyPauseState()

notifyTrackListeners()

Log.d(CLIENT_TAG, "Screen share setup complete")
Expand Down
34 changes: 34 additions & 0 deletions packages/ios-client/Sources/ios-client/WhipClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ public class WhipClient: ClientBase {
public var currentCameraDeviceId: String?
public var isScreenShareOn: Bool = false

private var isAudioPaused = false
private var isVideoPaused = false

/**
Initializes a `WhipClient` object.
- Parameter configurationOptions: Additional configuration options, such as a STUN server URL or authorization token.
Expand Down Expand Up @@ -245,6 +248,7 @@ public class WhipClient: ClientBase {
self.audioTrack = audioTrack
}

applyPauseState()
}

public func startCapture(_ videoDevice: AVCaptureDevice) {
Expand Down Expand Up @@ -310,6 +314,34 @@ public class WhipClient: ClientBase {
return RTCCameraVideoCapturer.captureDevices()
}

// MARK: - Pausing

/**
Pauses or resumes the audio sent to the server.

- Parameter paused: `true` to pause the audio, `false` to resume it.
*/
public func setAudioPaused(_ paused: Bool) {
isAudioPaused = paused
audioTrack?.isEnabled = !paused
}

/**
Pauses or resumes the video sent to the server.

- Parameter paused: `true` to pause the video, `false` to resume it.
*/
public func setVideoPaused(_ paused: Bool) {
isVideoPaused = paused
videoTrack?.isEnabled = !paused
}

/// Reapplies the stored pause state to the current tracks.
private func applyPauseState() {
audioTrack?.isEnabled = !isAudioPaused
videoTrack?.isEnabled = !isVideoPaused
}

// MARK: - Codec Management

/**
Expand Down Expand Up @@ -440,6 +472,8 @@ public class WhipClient: ClientBase {
videoTrack.isEnabled = true
self.videoTrack = videoTrack

applyPauseState()

logger.info("Screen sharing initialized, showing broadcast picker")

DispatchQueue.main.async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,20 @@ class ReactNativeMobileWhipClientView(
whipClient?.disconnect()
}

fun setAudioPaused(paused: Boolean) {
if (whipClient == null) {
throw IllegalStateException("WHIP client not found. Make sure it was initialized properly.")
}
whipClient?.setAudioPaused(paused)
}

fun setVideoPaused(paused: Boolean) {
if (whipClient == null) {
throw IllegalStateException("WHIP client not found. Make sure it was initialized properly.")
}
whipClient?.setVideoPaused(paused)
}

fun flipCamera() {
if (whipClient == null) {
throw IllegalStateException("WHIP client not found. Make sure it was initialized properly.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,14 @@ class ReactNativeMobileWhipClientViewModule : Module() {
view.disconnect()
}

AsyncFunction("setAudioPaused") { view: ReactNativeMobileWhipClientView, paused: Boolean ->
view.setAudioPaused(paused)
}

AsyncFunction("setVideoPaused") { view: ReactNativeMobileWhipClientView, paused: Boolean ->
view.setVideoPaused(paused)
}

AsyncFunction("flipCamera") { view: ReactNativeMobileWhipClientView ->
view.flipCamera()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,26 @@ public class ReactNativeMobileWhipClientView: ExpoView {
try await self.whipClient?.disconnect()
}

internal func setAudioPaused(_ paused: Bool) throws {
guard let client = self.whipClient else {
throw Exception(
name: "E_WHIP_CLIENT_NOT_FOUND",
description: "WHIP client not found. Make sure it was initialized properly."
)
}
client.setAudioPaused(paused)
}

internal func setVideoPaused(_ paused: Bool) throws {
guard let client = self.whipClient else {
throw Exception(
name: "E_WHIP_CLIENT_NOT_FOUND",
description: "WHIP client not found. Make sure it was initialized properly."
)
}
client.setVideoPaused(paused)
}

internal func setPreferredVideoCodecs(preferredCodecs: [String]?) {
self.whipClient?.setPreferredVideoCodecs(preferredCodecs: preferredCodecs)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ public class ReactNativeMobileWhipClientViewModule: Module {
try await view.disconnect()
}

AsyncFunction("setAudioPaused") { (view: ReactNativeMobileWhipClientView, paused: Bool) in
try view.setAudioPaused(paused)
}

AsyncFunction("setVideoPaused") { (view: ReactNativeMobileWhipClientView, paused: Bool) in
try view.setVideoPaused(paused)
}

AsyncFunction("setPreferredSenderVideoCodecs") {
(view: ReactNativeMobileWhipClientView, preferredCodecs: [String]?) in
view.setPreferredVideoCodecs(preferredCodecs: preferredCodecs)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,14 @@ export type WhipClientViewRef = {
authToken?: string;
}) => Promise<void>;
disconnect: () => Promise<void>;
/**
* Pauses or resumes the audio sent to the server.
*/
setAudioPaused: (paused: boolean) => Promise<void>;
/**
* Pauses or resumes the video sent to the server.
*/
setVideoPaused: (paused: boolean) => Promise<void>;
switchCamera: (deviceId: string) => Promise<void>;
flipCamera: () => Promise<void>;
setPreferredSenderVideoCodecs: (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,12 @@ export function WhipClientView(
disconnect: async () => {
await nativeRef.current?.disconnect();
},
setAudioPaused: async (paused: boolean) => {
await nativeRef.current?.setAudioPaused(paused);
},
setVideoPaused: async (paused: boolean) => {
await nativeRef.current?.setVideoPaused(paused);
},
switchCamera: async (deviceId: string) => {
await nativeRef.current?.switchCamera(deviceId);
},
Expand Down