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:

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();
  }
}

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,
      ),
    );
  }
}
© Getstream.io, Inc. All Rights Reserved.