Skip to content
This repository was archived by the owner on Feb 10, 2026. It is now read-only.

Commit c9acb6a

Browse files
FCE-321: iOS camera in background; FCE-1727: Add PiP support to mobile-sdk (#505)
## Description - Picture in Picture. Displayes main and secondary tracks if available, secondary track is determined by VAD. - On iOS support for background camera while in PiP This is still a bit of an ongoing effort. UI and track switch should be improved in next PRs. This PR introduces the main concept which works. ## Motivation and Context - Customers need PiP when dealing with calls in background ## Documentation impact - [x] Documentation update required - [ ] Documentation updated [in another PR](_) - [ ] No documentation update required ## Types of changes - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) --------- Co-authored-by: Anna Olak <anna.olak@swmansion.com>
1 parent 76d05a6 commit c9acb6a

29 files changed

Lines changed: 1548 additions & 26 deletions

internal/fishjam-chat/app.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,11 +62,13 @@
6262
"@fishjam-cloud/react-native-client",
6363
{
6464
"android": {
65-
"enableForegroundService": true
65+
"enableForegroundService": true,
66+
"supportsPictureInPicture": true
6667
},
6768
"ios": {
6869
"enableScreensharing": true,
69-
"enableVoIPBackgroundMode": true
70+
"enableVoIPBackgroundMode": true,
71+
"supportsPictureInPicture": true
7072
}
7173
}
7274
],

internal/fishjam-chat/components/VideosGrid/index.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { PeerMetadata } from '../../types/metadata';
66
import NoCameraView from '../NoCameraView';
77
import { GridTrack, GridTrackItem } from './GridTrackItem';
88
import { parsePeersToTracks } from './parsePeersToTracks';
9+
import { PipContainerView } from '@fishjam-cloud/react-native-client';
910

1011
const ListFooterComponent = () => <View style={{ height: 60 }} />;
1112

@@ -39,14 +40,19 @@ export default function VideosGrid({
3940
);
4041

4142
return (
42-
<FlatList<GridTrack>
43-
data={videoTracks}
44-
keyExtractor={keyExtractor}
45-
renderItem={renderItem}
46-
contentContainerStyle={styles.contentContainerStyle}
47-
ListFooterComponent={ListFooterComponent}
48-
ListEmptyComponent={ListEmptyComponent}
49-
/>
43+
<PipContainerView
44+
allowsCameraInBackground
45+
startAutomatically
46+
stopAutomatically>
47+
<FlatList<GridTrack>
48+
data={videoTracks}
49+
keyExtractor={keyExtractor}
50+
renderItem={renderItem}
51+
contentContainerStyle={styles.contentContainerStyle}
52+
ListFooterComponent={ListFooterComponent}
53+
ListEmptyComponent={ListEmptyComponent}
54+
/>
55+
</PipContainerView>
5056
);
5157
}
5258

packages/android-client/FishjamClient/src/main/java/com/fishjamcloud/client/ReconnectionManager.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.fishjamcloud.client
33
import com.fishjamcloud.client.models.ReconnectConfig
44
import timber.log.Timber
55
import java.util.Timer
6+
import java.util.concurrent.CopyOnWriteArrayList
67
import kotlin.concurrent.schedule
78

89
interface ReconnectionManagerListener {
@@ -31,7 +32,7 @@ internal class ReconnectionManager(
3132
private var reconnectConfig: ReconnectConfig = ReconnectConfig(),
3233
private val connect: () -> Unit
3334
) {
34-
private val listeners = mutableListOf<ReconnectionManagerListener>()
35+
private val listeners = CopyOnWriteArrayList<ReconnectionManagerListener>()
3536
private var reconnectAttempts = 0
3637
private var reconnectionStatus = ReconnectionStatus.Idle
3738

packages/android-client/FishjamClient/src/main/java/com/fishjamcloud/client/webrtc/PeerConnectionManager.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,12 @@ import kotlinx.coroutines.sync.withLock
2424
import org.webrtc.*
2525
import timber.log.Timber
2626
import java.util.*
27+
import java.util.concurrent.CopyOnWriteArrayList
2728

2829
internal class PeerConnectionManager(
2930
private val peerConnectionFactory: PeerConnectionFactoryWrapper
3031
) : PeerConnection.Observer {
31-
private val listeners = mutableListOf<PeerConnectionListener>()
32+
private val listeners = CopyOnWriteArrayList<PeerConnectionListener>()
3233

3334
fun addListener(listener: PeerConnectionListener) {
3435
listeners.add(listener)

packages/android-client/FishjamClient/src/main/java/com/fishjamcloud/client/webrtc/RTCEngineCommunication.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import com.fishjamcloud.client.utils.gson
77
import com.fishjamcloud.client.utils.serializeToMap
88
import fishjam.media_events.Shared
99
import timber.log.Timber
10+
import java.util.concurrent.CopyOnWriteArrayList
1011

1112
internal class RTCEngineCommunication {
12-
private val listeners = mutableListOf<RTCEngineListener>()
13+
private val listeners = CopyOnWriteArrayList<RTCEngineListener>()
1314

1415
fun addListener(listener: RTCEngineListener) {
1516
listeners.add(listener)

packages/ios-client/Sources/FishjamClient/FishjamClientInternal.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -896,7 +896,7 @@ extension FishjamClientInternal: RTCEngineListener {
896896
}
897897

898898
func onTrackUpdated(endpointId: String, trackId: String, metadata: Metadata) {
899-
guard let track = getTrack(trackId: trackId) else {
899+
guard let track = getTrackWithRtcEngineId(trackId: trackId) else {
900900
sdkLogger.error("Failed to process TrackUpdated event: Track context not found: \(trackId)")
901901
return
902902
}

packages/ios-client/Sources/FishjamClient/media/Capturers/CameraCapturer.swift

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public protocol CameraCapturerDeviceChangedListener: AnyObject {
88
class CameraCapturer: VideoCapturer {
99
private let videoParameters: VideoParameters
1010
private let capturer: RTCCameraVideoCapturer
11+
private let captureSession: AVCaptureSession
1112
internal var isFront: Bool = true
1213
private(set) var device: AVCaptureDevice? {
1314
didSet {
@@ -19,7 +20,8 @@ class CameraCapturer: VideoCapturer {
1920

2021
init(videoParameters: VideoParameters, delegate: RTCVideoCapturerDelegate, deviceId: String? = nil) {
2122
self.videoParameters = videoParameters
22-
self.capturer = RTCCameraVideoCapturer(delegate: delegate, captureSession: AVCaptureSession())
23+
self.captureSession = AVCaptureSession()
24+
self.capturer = RTCCameraVideoCapturer(delegate: delegate, captureSession: captureSession)
2325
let devices = RTCCameraVideoCapturer.captureDevices()
2426

2527
if let newDevice = devices.first(where: { $0.uniqueID == deviceId }) {
@@ -113,4 +115,29 @@ class CameraCapturer: VideoCapturer {
113115
}
114116
)
115117
}
118+
119+
public func isMultitaskingCameraAccessSupported() -> Bool {
120+
if #available(iOS 16.0, *) {
121+
return captureSession.isMultitaskingCameraAccessSupported
122+
}
123+
return false
124+
}
125+
126+
public func setMultitaskingCameraAccessEnabled(_ enabled: Bool) -> Bool {
127+
if #available(iOS 16.0, *) {
128+
guard captureSession.isMultitaskingCameraAccessSupported else {
129+
return false
130+
}
131+
captureSession.isMultitaskingCameraAccessEnabled = enabled
132+
return true
133+
}
134+
return false
135+
}
136+
137+
public func isMultitaskingCameraAccessEnabled() -> Bool {
138+
if #available(iOS 16.0, *) {
139+
return captureSession.isMultitaskingCameraAccessEnabled
140+
}
141+
return false
142+
}
116143
}

packages/ios-client/Sources/FishjamClient/media/Tracks/LocalCameraTrack.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,4 +72,16 @@ public class LocalCameraTrack: LocalVideoTrack, LocalTrack {
7272
public static func getCaptureDevices() -> [AVCaptureDevice] {
7373
return RTCCameraVideoCapturer.captureDevices()
7474
}
75+
76+
public func isMultitaskingCameraAccessSupported() -> Bool {
77+
return capturer.isMultitaskingCameraAccessSupported()
78+
}
79+
80+
public func setMultitaskingCameraAccessEnabled(_ enabled: Bool) -> Bool {
81+
return capturer.setMultitaskingCameraAccessEnabled(enabled)
82+
}
83+
84+
public func isMultitaskingCameraAccessEnabled() -> Bool {
85+
return capturer.isMultitaskingCameraAccessEnabled()
86+
}
7587
}

packages/ios-client/Sources/FishjamClient/media/Tracks/Track.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import WebRTC
22

33
open class Track: Equatable {
4-
internal var mediaTrack: RTCMediaStreamTrack?
4+
public internal(set) var mediaTrack: RTCMediaStreamTrack?
55
internal var sendEncodings: [RTCRtpEncodingParameters] = []
66
let endpointId: String
77
internal var rtcEngineId: String?

0 commit comments

Comments
 (0)