Реализация picture-in-picture видеоплеера в мобильном приложении
PiP — плавающее видеоокно, которое остаётся поверх других приложений когда пользователь уходит с экрана плеера. Нативная поддержка есть на iOS 14+ и Android 8+, но правильно её подключить — не пять строк кода.
iOS: AVPictureInPictureController
Требования: AVPlayerLayer или AVPlayerViewController, фоновый режим audio-video в Info.plist, разрешение com.apple.security.application-groups при необходимости.
let pipController = AVPictureInPictureController(playerLayer: playerLayer)
pipController.delegate = self
// Проверяем поддержку перед показом кнопки
if AVPictureInPictureController.isPictureInPictureSupported() {
pipButton.isHidden = false
}
// Запуск PiP
pipController.startPictureInPicture()
Делегат AVPictureInPictureControllerDelegate уведомляет о старте, остановке и ошибках. pictureInPictureControllerWillStartPictureInPicture — хорошее место, чтобы скрыть собственные контролы в основном view.
Для SwiftUI: VideoPlayer из AVKit + модификатор .onAppear с настройкой PiP через AVPlayerViewController.allowsPictureInPicturePlayback = true.
Кастомный контент (не AVPlayer). iOS 15+ поддерживает AVPictureInPictureController с contentSource: AVPictureInPictureControllerContentSource(sampleBufferDisplayLayer:) — можно показывать любой CMSampleBuffer, включая WebRTC-поток или данные с AR-сцены.
Android: PictureInPictureParams
// В AndroidManifest.xml
// android:supportsPictureInPicture="true"
// android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
val params = PictureInPictureParams.Builder()
.setAspectRatio(Rational(16, 9))
.setActions(buildPipActions()) // кнопки play/pause/close
.build()
enterPictureInPictureMode(params)
setActions принимает список RemoteAction — это PendingIntent-кнопки в PiP-окне. Используем BroadcastReceiver для обработки: нажатие pause в PiP должно остановить плеер и обновить иконку кнопки через обновлённый PictureInPictureParams.
Отслеживаем переход в PiP через onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean) — скрываем UI-элементы, оставляем только PlayerView.
Автоматический PiP при уходе с экрана
Android 12+: setAutoEnterEnabled(true) в PictureInPictureParams — приложение само переходит в PiP при нажатии Home без вызова кода. На Android 8–11 нужно вызывать enterPictureInPictureMode() в onUserLeaveHint().
iOS: AVPictureInPictureController.canStartPictureInPictureAutomaticallyFromInline = true (iOS 14.2+) — автозапуск при уходе из приложения.
Flutter: platform channels
Нативного Flutter API для PiP нет. Реализуем через MethodChannel: при нажатии кнопки в Flutter вызываем нативный метод, который запускает PiP. Состояние (вошли/вышли из PiP) передаём обратно через EventChannel. Для video_player (pub.dev) есть форки с PiP-поддержкой, но стабильность зависит от версии плагина.
Размер и позиция PiP-окна
iOS не позволяет программно задать положение PiP-окна — пользователь перетаскивает его сам. Размер определяется системой на основе preferredContentSize контейнера и соотношения сторон видео. Нельзя сделать PiP-окно «большим» принудительно.
Android: размер PiP-окна определяется setAspectRatio(Rational) — задаём соотношение сторон, система вычисляет размер. На большинстве Android-устройств PiP-окно занимает 30–40% ширины экрана.
Что нужно тестировать
PiP-поведение сильно зависит от версии ОС и производителя устройства. На Android: Samsung One UI 5 имеет свои ограничения на setAspectRatio — квадратные соотношения (1:1) могут отображаться некорректно. MIUI (Xiaomi) по умолчанию блокирует PiP для сторонних приложений и требует явного разрешения пользователя в настройках.
На iOS: PiP недоступен на iPhone с iOS 13 и ниже, а также на iPad с iOS 13 при использовании AVPlayerViewController без дополнительной настройки. Тестируем на минимально поддерживаемой версии приложения.
Сроки
PiP на одной платформе (iOS или Android) — 2 дня включая тестирование на нескольких версиях ОС. Обе платформы + Flutter — 3–4 дня.







