Реалізація Screen Broadcasting (трансляція екрана) з мобільного пристрою
Трансляція екрана на iOS працює через ReplayKit — й тільки через нього. Спроби захопити UIScreen напряму через UIScreen.main.snapshotView дають UIImage один раз, а не потік кадрів. ReplayKit — єдиний публічний API Apple для screen capture у реальному часі.
Два режими ReplayKit і коли що використовувати
RPScreenRecorder (in-app recording). Захоплює тільки контент усередині додатка. Користувач не видить системний picker. Підходить для записи геймплею, захисту UI додатка. Недолік — захист припиняється при сворачиванні.
RPBroadcastSampleHandler (broadcast upload extension). Працює як системне Extension — процес живе окремо від основного додатка. Захоплює весь екран пристрою, включаючи інші додатки й сповіщення. Користувач запускає через Control Center або RPSystemBroadcastPickerView. Саме цей режим потрібен для трансляції «всього екрана».
Архітектура Broadcast Extension:
iOS System → RPBroadcastSampleHandler (Extension process)
↓
CMSampleBuffer (видео + аудіо)
↓
App Group shared container (якщо коммунікація з основним додатком)
↓
RTMP/SRT → сервер трансляції
Розширення не має UI й обмежено 50 МБ RAM. Це жорстке обмеження: все кодування й відправка повинні вміститися в цей бюджет.
RPBroadcastSampleHandler: реалізація
class BroadcastHandler: RPBroadcastSampleHandler {
private var rtmpStream: RTMPStream?
override func broadcastStarted(withSetupInfo setupInfo: [String: NSObject]?) {
// Ініціалізуємо RTMP або SRT з'єднання
// setupInfo — дані від основного додатка через Info.plist
}
override func processSampleBuffer(_ sampleBuffer: CMSampleBuffer,
with sampleBufferType: RPSampleBufferType) {
switch sampleBufferType {
case .video:
rtmpStream?.append(sampleBuffer)
case .audioApp:
rtmpStream?.append(sampleBuffer) // аудіо додатків
case .audioMic:
// аудіо мікрофона — окремий потік, вимагає явного дозволу
break
}
}
override func broadcastFinished() {
rtmpStream?.close()
}
}
Передача параметрів (stream key, endpoint) з основного додатка в Extension — через App Group UserDefaults:
let defaults = UserDefaults(suiteName: "group.com.yourapp.broadcast")
defaults?.set(streamKey, forKey: "streamKey")
Затримка й пропускна здатність
ReplayKit screen broadcast додає буферизацію ~2–5 секунд. Це не баг — Apple буферизує для захисту конфіденційності (приховування системних діалогів з паролями). Знизити неможна.
Розширення залежить від моделі пристрою: iPhone 13+ відає 1668×2388 (native scale), що надмірно для стрима. У processSampleBuffer перед кодуванням потрібно downscale через VTPixelTransferSession або CIContext:
// Масштабуємо до 1280×720 перед відправкою в енкодер
let scaledBuffer = pixelTransferSession.scale(pixelBuffer, to: CGSize(width: 1280, height: 720))
Без downscale Extension перевищить 50 МБ RAM на першому ж I-frame у 4K.
Android: MediaProjection API
На Android аналог — MediaProjection. Користувач підтверджує через системний діалог (startActivityForResult з MediaProjectionManager.createScreenCaptureIntent()). Після отримання MediaProjection створюємо VirtualDisplay й спрямовуємо його в MediaCodec через Surface:
val virtualDisplay = mediaProjection.createVirtualDisplay(
"ScreenCapture",
width, height, dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mediaCodec.createInputSurface(), null, null
)
На відміну від iOS, затримки ReplayKit немає — захист йде майже в реальному часі. Але на Android 14+ з'явилося вимога: якщо MediaProjection-сесія використовується в ForegroundService, потрібен тип FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION.
Сповіщення користувача й сторожевий таймер
На iOS Extension може працювати нескінченно, але перевищення 50 МБ — система його убуває без попередження. Для моніторингу з основного додатка — heartbeat через App Group: Extension пише timestamp кожні 5 секунд, додаток перевіряє. Якщо timestamp не оновлювався 15 секунд — вважаємо Extension упав й показуємо попередження.
Терміни
iOS Broadcast Extension з RTMP/SRT, downscaling, App Group коммунікацією: 2–3 тижні. Android MediaProjection: 1.5–2 тижні. Кросплатформа з загальним управляючим кодом: 4–5 тижнів. Вартість розраховується індивідуально.







