Skip to content

Commit d09c2be

Browse files
authored
feat: Picture in picture support for Android (#614)
* Picture in picture support for Android * tweak * renamed pip method * vale fix
1 parent 065f3ce commit d09c2be

File tree

15 files changed

+299
-2
lines changed

15 files changed

+299
-2
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
---
2+
id: picture_in_picture
3+
sidebar_position: 4
4+
title: Picture in Picture (PiP)
5+
---
6+
7+
Picture in picture (PIP) keeps the call running and visible while you navigate to other apps.
8+
9+
:::info
10+
At the moment Picture in Picture is only supported on Android.
11+
:::
12+
13+
### Enable Picture-in-Picture
14+
You can enable Picture in Picture by setting the `enablePictureInPicture` property to `true` in the `StreamCallContainer` or `StreamCallContent` widget.
15+
16+
```dart
17+
StreamCallContainer(
18+
call: widget.call,
19+
enablePictureInPicture: true,
20+
)
21+
```
22+
23+
You can customize the widget rendered while app is in Picture-in-Picture mode by providing `callPictureInPictureBuilder` to `StreamCallContent`.
24+
25+
```dart
26+
StreamCallContainer(
27+
call: widget.call,
28+
callContentBuilder: (
29+
BuildContext context,
30+
Call call,
31+
CallState callState,
32+
) {
33+
return StreamCallContent(
34+
call: call,
35+
callState: callState,
36+
enablePictureInPicture: true,
37+
callPictureInPictureBuilder: (context, call, callState) => // YOUR CUSTOM WIDGET
38+
})
39+
```
40+
41+
### Android Configuration
42+
To enable Picture in Picture on Android, you need to add the following configuration to your `AndroidManifest.xml` file.
43+
44+
```xml
45+
<activity android:name="VideoActivity"
46+
android:supportsPictureInPicture="true"
47+
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
48+
..
49+
/>
50+
```
51+
52+
Then you need to add this code to your `MainActivity` class. It will enter Picture in Picture mode when the user leaves the app but only if the call is active.
53+
54+
```kotlin
55+
import io.flutter.embedding.android.FlutterActivity
56+
import io.getstream.video.flutter.stream_video_flutter.service.PictureInPictureHelper
57+
58+
class MainActivity: FlutterActivity() {
59+
override fun onUserLeaveHint() {
60+
super.onUserLeaveHint()
61+
PictureInPictureHelper.enterPictureInPictureIfInCall(this)
62+
}
63+
}
64+
```
65+
66+
Done. Now after leaving the app, you'll see that the call will be still alive in the background like the one below:
67+
68+
![Picture in Picture example](../assets/advanced_assets/pip_example.png)
1.38 MB
Loading

dogfooding/android/app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
android:icon="@mipmap/ic_launcher">
2020
<activity
2121
android:name=".MainActivity"
22+
android:supportsPictureInPicture="true"
2223
android:exported="true"
2324
android:launchMode="singleTask"
2425
android:theme="@style/LaunchTheme"
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package io.getstream.video.flutter.dogfooding
22

33
import io.flutter.embedding.android.FlutterActivity
4+
import io.getstream.video.flutter.stream_video_flutter.service.PictureInPictureHelper
45

56
class MainActivity: FlutterActivity() {
7+
override fun onUserLeaveHint() {
8+
super.onUserLeaveHint()
9+
PictureInPictureHelper.enterPictureInPictureIfInCall(this)
10+
}
611
}

dogfooding/lib/screens/call_screen.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ class _CallScreenState extends State<CallScreen> {
143143
call: call,
144144
callState: callState,
145145
layoutMode: _currentLayoutMode,
146+
enablePictureInPicture: true,
146147
callParticipantsBuilder: (context, call, callState) {
147148
return Stack(
148149
children: [

packages/stream_video_flutter/android/src/main/kotlin/io/getstream/video/flutter/stream_video_flutter/MethodCallHandlerImpl.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package io.getstream.video.flutter.stream_video_flutter
22

33
import android.Manifest
44
import android.app.Activity
5+
import android.app.PictureInPictureParams
56
import android.content.Context
67
import android.content.Intent
78
import android.content.pm.PackageManager
89
import android.os.Build
10+
import android.util.Rational
911
import androidx.core.app.ActivityCompat
1012
import androidx.core.content.ContextCompat
1113
import io.flutter.embedding.android.FlutterFlags
@@ -15,12 +17,14 @@ import io.flutter.plugin.common.MethodCall
1517
import io.flutter.plugin.common.MethodChannel
1618
import io.flutter.plugin.common.PluginRegistry
1719
import io.getstream.log.taggedLogger
20+
import io.getstream.video.flutter.stream_video_flutter.service.PictureInPictureHelper
1821
import io.getstream.video.flutter.stream_video_flutter.service.ServiceManager
1922
import io.getstream.video.flutter.stream_video_flutter.service.ServiceManagerImpl
2023
import io.getstream.video.flutter.stream_video_flutter.service.ServiceType
2124
import io.getstream.video.flutter.stream_video_flutter.service.StreamCallService
2225
import io.getstream.video.flutter.stream_video_flutter.service.StreamScreenShareService
2326
import io.getstream.video.flutter.stream_video_flutter.service.notification.NotificationPayload
27+
import io.getstream.video.flutter.stream_video_flutter.service.utils.putBoolean
2428

2529
class MethodCallHandlerImpl(
2630
appContext: Context,
@@ -64,6 +68,15 @@ class MethodCallHandlerImpl(
6468
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
6569
logger.d { "[onMethodCall] method: ${call.method}" }
6670
when (call.method) {
71+
"enablePictureInPictureMode" -> {
72+
val activity = getActivity()
73+
putBoolean(activity, PictureInPictureHelper.PIP_ENABLED_PREF_KEY, true)
74+
}
75+
"disablePictureInPictureMode" -> {
76+
val activity = getActivity()
77+
putBoolean(activity, PictureInPictureHelper.PIP_ENABLED_PREF_KEY, false)
78+
PictureInPictureHelper.disablePictureInPicture(activity!!)
79+
}
6780
"isBackgroundServiceRunning" -> {
6881
val statusString = call.argument<String>("type")
6982
val serviceType = ServiceType.valueOf(statusString ?: "call")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package io.getstream.video.flutter.stream_video_flutter.service
2+
3+
import android.app.Activity
4+
import android.app.PictureInPictureParams
5+
import android.content.pm.ActivityInfo
6+
import android.content.pm.PackageManager
7+
import android.os.Build
8+
import android.util.Rational
9+
import io.getstream.video.flutter.stream_video_flutter.service.utils.getBoolean
10+
11+
class PictureInPictureHelper {
12+
companion object {
13+
const val PIP_ENABLED_PREF_KEY = "pip_enabled"
14+
15+
fun disablePictureInPicture(activity: Activity) {
16+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
17+
val params = PictureInPictureParams.Builder()
18+
params.setAutoEnterEnabled(false)
19+
activity.setPictureInPictureParams(params.build())
20+
}
21+
}
22+
23+
fun enterPictureInPictureIfInCall(activity: Activity) {
24+
val pipEnabled = getBoolean(activity, PIP_ENABLED_PREF_KEY)
25+
if (!pipEnabled) return
26+
27+
if (activity.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
28+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
29+
val currentOrientation = activity.resources.configuration.orientation
30+
31+
val aspect =
32+
if (currentOrientation == ActivityInfo.SCREEN_ORIENTATION_PORTRAIT) {
33+
Rational(9, 16)
34+
} else {
35+
Rational(16, 9)
36+
}
37+
38+
val params = PictureInPictureParams.Builder()
39+
params.setAspectRatio(aspect).apply {
40+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
41+
setAutoEnterEnabled(true)
42+
}
43+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
44+
setTitle("Video Player")
45+
setSeamlessResizeEnabled(true)
46+
}
47+
}
48+
49+
activity.enterPictureInPictureMode(params.build())
50+
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
51+
activity.enterPictureInPictureMode()
52+
}
53+
}
54+
}
55+
}
56+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.getstream.video.flutter.stream_video_flutter.service.utils
2+
3+
import android.content.Context
4+
import android.content.SharedPreferences
5+
6+
private const val PREFERENCES_FILE_NAME = "stream_video_flutter"
7+
private var prefs: SharedPreferences? = null
8+
private var editor: SharedPreferences.Editor? = null
9+
10+
private fun initInstance(context: Context) {
11+
prefs = context.getSharedPreferences(PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)
12+
editor = prefs?.edit()
13+
}
14+
15+
fun putBoolean(context: Context?, key: String, value: Boolean) {
16+
if (context == null) return
17+
initInstance(context)
18+
editor?.putBoolean(key, value)
19+
editor?.commit()
20+
}
21+
22+
fun getBoolean(context: Context?, key: String, defaultValue: Boolean = false): Boolean {
23+
if (context == null) return defaultValue;
24+
initInstance(context)
25+
return prefs?.getBoolean(key, defaultValue) ?: defaultValue
26+
}
27+
28+
fun remove(context: Context?, key: String) {
29+
if (context == null) return
30+
initInstance(context)
31+
editor?.remove(key)
32+
editor?.commit()
33+
}

packages/stream_video_flutter/lib/src/call_participants/call_participants.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,14 @@ class _StreamCallParticipantsState extends State<StreamCallParticipants> {
158158

159159
@override
160160
Widget build(BuildContext context) {
161+
if (widget.layoutMode == ParticipantLayoutMode.pictureInPicture) {
162+
return widget.callParticipantBuilder(
163+
context,
164+
widget.call,
165+
_participants.first,
166+
);
167+
}
168+
161169
if (_screenShareParticipant != null) {
162170
return ScreenShareCallParticipantsContent(
163171
call: widget.call,

packages/stream_video_flutter/lib/src/call_participants/layout/participant_layout_mode.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ enum ParticipantLayoutMode {
55
grid,
66

77
/// The layout mode is set to spotlight view.
8-
spotlight;
8+
spotlight,
9+
10+
pictureInPicture;
911
}
1012

1113
extension SortingExtension on ParticipantLayoutMode {
@@ -15,6 +17,8 @@ extension SortingExtension on ParticipantLayoutMode {
1517
return CallParticipantSortingPresets.regular;
1618
case ParticipantLayoutMode.spotlight:
1719
return CallParticipantSortingPresets.speaker;
20+
case ParticipantLayoutMode.pictureInPicture:
21+
return CallParticipantSortingPresets.speaker;
1822
}
1923
}
2024
}

0 commit comments

Comments
 (0)