Picture-in-Picture - системная функция
iOS, которая позволяет воспроизводить видео в отдельном плавающем окне поверх других приложений. Пользователь может свернуть приложение, продолжив смотреть контент в компактном окошке. Для разработчика видеосервиса это отдельный механизм со своей инфраструктурой, жизненным циклом и ограничениями.
Что такое PiP:
Технически
PiP - это не просто маленькое видео в углу. Это сквозной системный механизм. Окно
PiP не принадлежит приложению в привычном смысле - оно управляется системой и продолжает жить, даже когда пользователь перешел на рабочий стол.
Функция появилась на
iPad в
iOS 9, но для
iPhone стала общедоступной с
iOS 14. С этого момента фоновое воспроизведение видео в маленьком окошке стало стандартом для уважающих себя видеосервисов.
Подготовка и запуск:
Основной инструмент - класс
AVPictureInPictureController. Создать его можно двумя способами: передать ссылку на
AVPlayerLayer или использовать
contentSource.
let pipController = AVPictureInPictureController(playerLayer: playerLayer)
Либо:
let contentSource = AVPictureInPictureController.ContentSource(playerLayer: playerLayer)
let pipController = AVPictureInPictureController(contentSource: contentSource)
Настройка AVAudioSession:
Чтобы система разрешила запускать видео в фоне, необходимо правильно настроить
AVAudioSession. Без этого
startPictureInPicture() просто не сработает.
do {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playback, mode: .moviePlayback)
try session.setActive(true)
} catch {
print("Failed to configure AVAudioSession: \(error)")
}
Почему PiP может не запуститься:
Есть две частые проблемы:
- Контроллер не удерживается в памяти. Если создать AVPictureInPictureController локально внутри функции, ARC удалит его сразу после завершения функции. Контроллер не доживет до отрисовки окна. Нужна сильная ссылка.
- Не настроены Background Modes. В Xcode нужно зайти в Signing & Capabilities, добавить Background Modes и поставить галочку напротив Audio, AirPlay, and Picture in Picture.
Отслеживание состояний:
Для отслеживания жизненного цикла
PiP нужно реализовать протокол
AVPictureInPictureControllerDelegate. Он включает методы
WillStart,
DidStart,
FailedToStart,
WillStop,
Restore и
DidStop.
Особый интерес представляет
Restore. Он вызывается, когда пользователь возвращается из
PiP в приложение. В этом методе нужно восстановить
UI плеера и вызвать
completionHandler(true).
func pictureInPictureController(
_ pictureInPictureController: AVPictureInPictureController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
self.playerView.isHidden = false
completionHandler(true)
}
Для автоматического запуска
PiP при свайпе на главный экран можно использовать флаг
canStartPictureInPictureAutomaticallyFromInline.
Как устроен PiP под капотом:
Если запустить приложение с
PiP и посмотреть на таймлайн в
Xcode Instruments, видно, что больше всего событий приходится на процессы
AVKit и
Pegasus.
Pegasus - системный фреймворк для работы с
Picture-in-Picture. Архитектурная цепочка выглядит так:
AVPictureInPictureController -> AVPictureInPicturePlatformAdapter -> PGPictureInPictureProxy
AVPictureInPictureController не рисует системное окно сам. Он передает команду адаптеру, который обращается к
PGPictureInPictureProxy. Именно этот класс владеет свойством
UIWindow для окна
PiP.
Отслеживание действий пользователя:
Когда пользователь ставит видео на паузу или перематывает его через системные контролы, делегат не предоставляет методов для этих событий.
В
iOS 18+ появился механизм
AVMetrics. Можно подписаться на асинхронный поток событий перемотки:
if let stream = item.metrics(forType: AVMetricPlayerItemSeekEvent.self) {
Task {
for try await event in stream {
print("Пользователь перемотал видео")
}
}
}
На более старых версиях
iOS используется
KVO или
Combine. Нужно наблюдать за свойством
timeControlStatus у
AVPlayer. Когда пользователь нажимает паузу в окне
PiP, статус меняется на
.paused.
Переключение контента:
Есть два сценария:
Если посмотреть на логи в
Instruments в момент смены
contentSource, видно, что метаданные контента меняются, а адреса объектов
PGPictureInPictureProxy и
PGPlaybackState остаются прежними. Это значит, что система переиспользует существующий прокси и состояние.
Жизненный цикл приложения:
Окно
PiP привязано к процессу приложения.
Если пользователь блокирует экран, видео продолжит играть только при корректно настроенной аудиосессии.
Если пользователь смахивает приложение из многозадачности, процесс уничтожается и окно
PiP мгновенно закрывается.
Apple не позволяет
PiP работать без хост-приложения.
iOS может принудительно приостановить фоновый плеер, если приложение долго не выводит изображение. В этом случае делегат уведомит через
DidStop, и нужно корректно очистить ресурсы.
Вывод:
Picture-in-Picture - мощный инструмент, который улучшает пользовательский опыт. Но для стабильной работы критически важно правильно настроить аудиосессию и
Background Modes, грамотно управлять памятью контроллера и использовать
contentSource для бесшовного переключения видео.
Подкапотный механизм сложнее, чем кажется.
AVKit взаимодействует с системным фреймворком
Pegasus, который управляет окном
PiP. Понимание этой архитектуры помогает избегать типичных ошибок, а работу сделать стабильной и надежной.