import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:stream_video_flutter/stream_video_flutter.dart';
class InAppPictureInPictureNotifier extends ChangeNotifier {
Call? _call;
Call? get call => _call;
void show(Call call) {
_call = call;
notifyListeners();
}
void hide() {
_call = null;
notifyListeners();
}
}
In-App Picture in Picture
This cookbook shows how to implement an in-app picture-in-picture (PiP) mode that keeps a call floating within your app while users navigate to other screens. Unlike system-level PiP, this implementation keeps the call visible within your app’s UI.
For system-level picture-in-picture that works across different apps, see our Picture in Picture guide.
Overview
In-app picture-in-picture allows users to continue their video call in a small floating window while they navigate to other parts of your application. This is useful for maintaining call visibility during tasks like browsing content, reading messages, or accessing other features.
Implementation
The implementation below demonstrates one way to add in-app PiP functionality using the Provider package. This is just a sample implementation - you can adapt it to use other state management solutions like Riverpod, Bloc, or even a simple InheritedWidget based on your app’s architecture and needs.
Step 1: Create the PiP Notifier
First, create a ChangeNotifier
to manage the PiP state:
Step 2: Create the PiP Scope Widget
Next, create a widget that provides the in-app PiP functionality:
class InAppPictureInPictureScope extends StatelessWidget {
const InAppPictureInPictureScope({
super.key,
required this.child,
this.pipViewWidth = 160,
this.pipViewHeight = 200,
});
final Widget child;
final double pipViewWidth;
final double pipViewHeight;
static void showPictureInPicture(BuildContext context, Call call) {
return context.read<InAppPictureInPictureNotifier>().show(call);
}
static void hidePictureInPicture(BuildContext context) {
return context.read<InAppPictureInPictureNotifier>().hide();
}
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => InAppPictureInPictureNotifier(),
child: FloatingViewContainer(
floatingViewWidth: pipViewWidth,
floatingViewHeight: pipViewHeight,
floatingView: Consumer<InAppPictureInPictureNotifier>(
builder: (context, callNotifier, _) {
final call = callNotifier.call;
if (callNotifier.call == null) {
return const SizedBox.shrink();
}
return SizedBox(
width: pipViewWidth,
height: pipViewHeight,
child: StreamBuilder<CallState>(
stream: call!.state.valueStream,
builder: (context, snapshot) {
final callState = snapshot.data;
if (callState == null || callState.status.isDisconnected) {
return const SizedBox.shrink();
}
return StreamCallParticipants(
call: call,
participants: callState.callParticipants,
layoutMode: ParticipantLayoutMode.pictureInPicture,
);
}),
);
},
),
child: child,
),
);
}
}
Step 3: Setup Your App
Wrap your app’s main content with the InAppPictureInPictureScope
:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: InAppPictureInPictureScope(
pipViewWidth: 160,
pipViewHeight: 200,
child: MainScreen(),
),
);
}
}
Step 4: Control PiP from Your Call Screen
Use the static methods to show and hide the picture-in-picture mode:
class CallScreen extends StatefulWidget {
final Call call;
const CallScreen({super.key, required this.call});
@override
State<CallScreen> createState() => _CallScreenState();
}
class _CallScreenState extends State<CallScreen> {
@override
void initState() {
super.initState();
// Hide PiP when user returns to the call screen
InAppPictureInPictureScope.hidePictureInPicture(context);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Video Call'),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
// Show PiP when navigating away from call screen
InAppPictureInPictureScope.showPictureInPicture(context, widget.call);
Navigator.pop(context);
},
),
),
body: StreamCallContainer(
call: widget.call,
),
floatingActionButton: FloatingActionButton(
onPressed: () {
// End the call and navigate away
widget.call.leave();
Navigator.pop(context);
},
child: Icon(Icons.call_end),
backgroundColor: Colors.red,
),
);
}
}