Как добавить скриншаринг в ваше Android-приложение [2023] + примеры кода — обложка

Скриншаринг (screen sharing — демонстрация экрана во время видеозвонка) — уже практически базовая функция в платформах для видеосвязи. Skype, WhatsApp, Telegram, Teams, Google Meet — во всех этих сервисах она есть.

Мы уже рассказывали, как реализовать скриншаринг в мобильном iOS-приложении. В этой статье объясним, как сделать это на Android. С примерами кода.

Также в других статьях нашего Android-цикла узнаете, как сделать кастомное уведомление о звонке и в целом, как мы разрабатываем WebRTC-системы для звонков.

Реализация скриншаринга

Скриншаринг можно включить сразу при создании нового видеозвонка — ещё до его начала.

Однако рассмотрим наиболее распространённую ситуацию — включение скриншаринга во время видеозвонка, через N секунд после его начала.

Для упрощения описания реализации скриншаринга допустим, что у нас уже есть готовое приложение с WebRTC звонками. Читайте про реализацию видеозвонка WebRTC подробнее.

Шаги реализации:

  1. Получение доступа к содержимому экрана
  2. Создание видеотрека с изображением с экрана
  3. Замена видеотрека с камеры на видеотрек с экрана
  4. Отображение уведомления о том, что идёт скриншот

Теперь всё по порядку:

Получение доступа к содержимому экрана

Сначала получим возможность захватывать экран и звук устройства с помощью Media Projection API:

 
 val screenSharingPermissionLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
// Handle request result
val screenSharingIntent = result.data
if (screenSharingIntent != null) {
// Success request
}
}
val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
val intent = mediaProjectionManager.createScreenCaptureIntent()
screenSharingPermissionLauncher.launch(intent)
 

При вызове screenSharingPermission Launcher.launch(intent) отобразится диалог, в котором пользователю объяснят, что media projection получит доступ ко всей информации, отображаемой на экране.

В результате успешного получения доступа к содержимому экрана мы получим screenSharingIntent.

Создание видеотрека с изображением с экрана

Создадим videoCapturer, который будет захватывать изображение с экрана:

 
val mediaProjectionCallback = object : MediaProjection.Callback() {
override fun onStop() {
// screen capture stopped
}
}
val videoCapturer = ScreenCapturerAndroid(screenSharingIntent, mediaProjectionCallback)
 

Далее создадим localVideoTrack:

 
 val surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", eglBase.eglBaseContext)

val videoSource = peerConnectionFactory.createVideoSource(/* isScreencast = */ true)

videoCapturer.initialize(surfaceTextureHelper,context, videoSource.capturerObserver)

videoCapturer.startCapture(displayWidth, displayHeight, fps)

val localVideoTrack = peerConnectionFactory.createVideoTrack(VIDEO_TRACK_ID, videoSource)

Замена видеотрека с камеры видеотреком с экрана

Для корректной замены видеотрека реализуем логику renegotiation у обоих участников звонка. При изменении локальных медиа треков WebRTC вызывает метод onRenegotiationNeeded. В нем повторяем процесс обмена sdp:

 
val peerConnectionObserver = object : PeerConnection.Observer {
...
override fun onRenegotiationNeeded() {
// Launch sdp exchange
peerConnection.createOffer(...)
}
}
val peerConnection = peerConnectionFactory.createPeerConnection(iceServers, peerConnectionObserver)
 

Приступим к замене видеотрека. Удалим из локального медиа-потока видеотрек с камеры:

 
localMediaStream.removeTrack(cameraVideoTrack)
 

Остановим захват видео с камеры:

 
cameraVideoCapturer.stopCapture()
 

Добавим видеотрек скриншаринга:

 
 localMediaStream.addTrack(screenVideoTrack)
 

Отображение уведомления о текущем скриншаринге

При запуске скриншара нужно запустить Foreground Service с уведомлением о записи экрана.

Создадим ScreencastService и добавим его в AndroidManifest.xml. Также укажем параметр foregroundServiceType:

 
 service
android:name=".ScreencastService"
android:foregroundServiceType="mediaProjection"
android:stopWithTask="true" /
 

Перед заменой видеотрека с камеры на видеотрек скриншота запустим ScreencastService:

 
val intent = Intent(this, ScreencastService::class.java)
ContextCompat.startForegroundService(this, intent)
 

Далее в ScreencastService, например в onStartCommand(), нужно вызвать метод startForeground:

  
startForeground(NOTIFICATION_ID, notification)
 

Частые проблемы при реализации

Приложение падает на устройствах с Android 10+ с ошибкой “Media projections require a foreground service of type ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION”

Эта ошибка возникает, когда скриншаринг запускается без фонового сервиса. Начиная с Android 10 система требует, чтобы во время скриншаринга работал фоновый сервис с параметром “android:foregroundServiceType="mediaProjection”».

Foreground Service гарантирует, что система не «убьёт» приложение во время скриншота. Уведомление Foreground Service сообщит пользователю о текущем скриншоте и позволит быстро вернуться в приложение.

Как решить: не забыть показать уведомление о начале скриншаринга :)

Не происходит замена видеотрека с камеры на видеотрек с экрана

Проблема замены видеотрека может возникать, если на стороне одного (или обоих) участников видеозвонка не реализована или реализована неправильно логика renegotiation.

Как решить: переопределить метод onRenegotiationNeeded в PeerConnection.Observer (название метода на других платформах может отличаться). При вызове onRenegotiationNeeded должен запускаться процесс обмена SDP.

Заключение

В этой статье мы рассмотрели реализацию скриншаринга в видеозвонке:

  • Получили доступ к содержимому экрана с помощью MediaProjection API
  • Захватили содержимое экрана с помощью ScreenCapturerAndroid
  • Создали локальный видеотрек с изображением с экрана
  • Заменили видеотрек с камеры на видеотрек со скриншота
  • Реализовали Foreground Service для отображения уведомления о скриншоте

Посмотреть готовую реализацию скриншаринга можно в нашем приложении.

  • Разработка