How to implement Picture-in-Picture (PiP) in Flutter for iOS using LiveKit without a video URL?

I am building a video conferencing app using LiveKit in Flutter and want to implement Picture-in-Picture (PiP) mode on iOS. My goal is to display a view showing the speaker's initials or avatar during PiP mode. I successfully implemented this functionality on Android but am struggling to achieve it on iOS.

I am using a MethodChannel to communicate with the native iOS code. Here's the Flutter-side code:

import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

class PipController {
  static const _channel = MethodChannel('pip_channel');

  static Future<void> startPiP() async {
    try {
      await _channel.invokeMethod('enterPiP');
    } catch (e) {
      if (kDebugMode) {
        print("Error starting PiP: $e");
      }
    }
  }

  static Future<void> stopPiP() async {
    try {
      await _channel.invokeMethod('exitPiP');
    } catch (e) {
      if (kDebugMode) {
        print("Error stopping PiP: $e");
      }
    }
  }
}

On the iOS side, I am using AVPictureInPictureController. Since it requires an AVPlayerLayer, I had to include a dummy video URL to initialize the AVPlayer. However, this results in the dummy video’s audio playing in the background, but no view is displayed in PiP mode.

Here’s my iOS code:

import Flutter
import UIKit
import AVKit

@main
@objc class AppDelegate: FlutterAppDelegate {
    
    var pipController: AVPictureInPictureController?
    var playerLayer: AVPlayerLayer?
    
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        
        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
        let pipChannel = FlutterMethodChannel(name: "pip_channel", binaryMessenger: controller.binaryMessenger)

        pipChannel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in
            if call.method == "enterPiP" {
                self?.startPictureInPicture(result: result)
            } else if call.method == "exitPiP" {
                self?.stopPictureInPicture(result: result)
            } else {
                result(FlutterMethodNotImplemented)
            }
        }

        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
    
    private func startPictureInPicture(result: @escaping FlutterResult) {
        guard AVPictureInPictureController.isPictureInPictureSupported() else {
            result(FlutterError(code: "UNSUPPORTED", message: "PiP is not supported on this device.", details: nil))
            return
        }

        // Set up the AVPlayer
        let player = AVPlayer(url: URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")!)
        let playerLayer = AVPlayerLayer(player: player)
        self.playerLayer = playerLayer

        // Create a dummy view
        let dummyView = UIView(frame: CGRect(x: 0, y: 0, width: 1, height: 1))
        dummyView.isHidden = true
        window?.rootViewController?.view.addSubview(dummyView)
        dummyView.layer.addSublayer(playerLayer)
        playerLayer.frame = dummyView.bounds

        // Initialize PiP Controller
        pipController = AVPictureInPictureController(playerLayer: playerLayer)
        pipController?.delegate = self

        // Start playback and PiP
        player.play()
        pipController?.startPictureInPicture()
        print("Picture-in-Picture started")
        result(nil)
    }
    
    private func stopPictureInPicture(result: @escaping FlutterResult) {
        guard let pipController = pipController, pipController.isPictureInPictureActive else {
            result(FlutterError(code: "NOT_ACTIVE", message: "PiP is not currently active.", details: nil))
            return
        }

        pipController.stopPictureInPicture()
        playerLayer = nil
        self.pipController = nil
        result(nil)
    }
}

extension AppDelegate: AVPictureInPictureControllerDelegate {
    func pictureInPictureControllerDidStartPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("PiP started")
    }

    func pictureInPictureControllerDidStopPictureInPicture(_ pictureInPictureController: AVPictureInPictureController) {
        print("PiP stopped")
    }
}

Questions:

  1. How can I implement PiP mode on iOS without using a video URL (or AVPlayerLayer)?
  2. Is there a way to display a custom UIView (like a speaker’s initials or an avatar) in PiP mode instead of requiring a video?
  3. Why does PiP not display any view, even though the dummy video URL is playing in the background?

I am new to iOS development and would greatly appreciate any guidance or alternative approaches to achieve this functionality. Thank you!

Hopefully there are other developers of Flutter that can help.

Are you able to demonstrate the issue in a test project created from one of Xcode's templates, and using only Apple APIs? If not, you should check with the support resources provided by Flutter to get assistance with their software. Probably there have a support forums for questions about their product.

You can find tips on creating and sharing a test project in Creating a Test Project.

Albert Pascual
  Worldwide Developer Relations.

How to implement Picture-in-Picture (PiP) in Flutter for iOS using LiveKit without a video URL?
 
 
Q