This post is also available in: Английский
Функция Screen Capture, или, попросту говоря, «снятие изображения экрана», в мобильном приложении представляет собой мощный вспомогательный инструмент и может использоваться в самых разнообразных задачах. В этой статье мы подробно рассмотрим механизмы, доступные для реализации данного функционала.
С помощью снимков или записи происходящего на экране можно создавать удобные руководства, обучающие пользователей работе со сложными приложениями. Эффективность анимированных руководств, наглядно показывающих пользователю, как именно нужно использовать функционал приложения, гораздо выше в сравнении с простыми текстовыми «обучалками». Поддержка и исправление ошибок приложения может существенно упроститься, если вместе с сообщением об ошибке пользователь сможет отправлять запись своих действий, приводящих к сбою. Наконец, многие любители компьютерных игр часто используют запись с экрана для создания руководств по прохождению уровней или для демонстрации своих достижений.
Очевидно, что возможность Screen Capture в мобильных приложениях весьма полезна и потенциально применима для упрощения многих задач. Было бы наивно полагать, что Apple не предусмотрели такой возможности в iOS. И действительно, такие возможности есть. В данной статье мы постараемся охватить весь спектр возможностей Screen Capture для платформы iOS, а также расскажем о некоторых тонкостях и нюансах, связанных с таким распространённым действием, как снятие скриншота с видеоплеера.
Разрешение и запрет UIGetScreenImage
Самым простым и верным способом получить содержимое экрана iPhone была и остаётся функция UIGetScreenImage, которая делает именно то, что от неё требуется — возвращает ссылку на изображение, показывающее содержимое экрана вашего телефона в момент вызова.
У этой функции есть только один недостаток и, если бы не он, то она была бы, пожалуй, наилучшим решением для снятия скриншотов. Недостаток состоит в том, что функция является частью Private API, а значит, любое приложение, использующее её, не сможет попасть в App Store и добраться до устройств пользователей.
Интересно отметить, что в конце 2009 года компания Apple сообщила на своем разработческом форуме о разрешении к использованию данной функции в приложениях. Многие разработчики одобрили этот шаг. Однако, уже через полгода на том же форуме появилась новость о возвращении функции UIGetScreenImage в закрытый API, а следовательно, все приложения, которые её использовали на тот момент, должны были перейти на публично доступные аналоги. Таким образом, команда разработки Apple переместила данную функцию из ряда полезных способов получения изображений экрана в категорию вспомогательных функций, используемых только при разработке и тестировании приложений.
Но было бы неправильно отбирать у разработчиков полезную функцию, не предоставив ничего взамен. Поэтому вместе с объявлением о закрытии функции были указаны три возможных способа её замены в трёх различных случаях. Рассмотрим их подробнее.
Способ первый. Рендеринг слоя UIView
Первым пунктом в списке идёт метод класса CALayer для получения содержимого слоя в виде изображения — это метод
— (void)renderInContext:(CGContextRef)context.Использование этого метода в целях Screen Capture описано в Q&A QA1703 на сайте Apple. Этот метод просто сохраняет содержимое слоя в текущий графический контекст, после чего из контекста можно получить непосредственно хранящееся там изображение. Следовательно, для любого UIView можно снять изображение его слоя.
Ниже приведён пример кода, который можно использовать для получения изображения из UIView.
[gist id=4233212 bump=1]
С уверенностью можно сказать, что этот способ подойдёт в большинстве простейших случаев снятия скриншотов в приложениях с простым интерфейсом, состоящим только из элементов UIKit. Однако нюанс состоит в том, что как только в интерфейсе появляется элемент вроде плеера, играющего видео, или сцены OpenGL, в скриншоте, полученном данным методом, вместо их изображения будет показан просто черный экран (или экран любого другого цвета, который находится ниже означенных элементов в иерархии вида). Такой эффект объясняется наличием в подобных элементах так называемого Hardware Overlay — слоя, который обрабатывается не CPU, а GPU. Иными словами, сцена OpenGL, равно как и плеер, попросту находится в памяти графического процессора и не отреагируют на сообщение о передаче своего вида в контекст.
Таким образом, первое предложенное решение является наиболее общим, но не универсальным. Оно сработает для интерфейса, построенного в UIKit, но при наличии в приложении элементов с GPU, скриншот будет содержать пустые области.
Но в Apple, по всей видимости, учли этот момент. Следующие два предложенных способа Screen Capture предлагают решение проблем со снятием сцены OpenGL и видео-изображения с камеры устройства.
Cпособ второй. Получение изображения из OpenGL ES
Как уже было отмечено, содержимое сцен OpenGL находится в области памяти графического процессора. Поэтому для того, чтобы получить скриншот с окна, в котором отображается графическая сцена, созданная с помощью фреймворка OpenGL ES, необходим другой способ. Такой способ описан в Q&A QA1704. Это функция glReadPixels.
Для получения изображения сцены OpenGL необходимо проделать следующие шаги.
- Подключиться к нужному буферу рендеринга (если используется больше одного) с помощью функции glBindRenderbufferOES.
- Выделить память под изображение нужного размера, учитывая, что на каждую точку изображения отводится по 4 байта.
- Скопировать пиксели изображения в выделенную область памяти с помощью вызова функции glReadPixels.
- Объявить выделенную область «источником данных» путем указания ссылки на источник функцией CGDataProviderCreateWithData.
- Получить CGImage из источника данных с помощью функции CGImageCreate.
Код, иллюстрирущий описанную последовательность вызовов, приведён ниже.
[gist id=4233229 bump=1]
Имея CGImage, можно легко получить UIImage изображения и далее использовать его в своих целях. Необходимо помнить, что системы координат OpenGL ES и UIKit отличаются направлением оси Y. Поэтому скриншот перед использованием необходимо перевернуть.
Способ третий. Изображение с камеры устройства
Изображение с камеры iPhone также не является собственностью CPU. Для решения проблемы со снятием изображений с видео, получаемого с камеры в текущем времени, в Q&A QA1702 предлагается использовать следующий способ, основанный на использовании фреймворка AVFoundation.
Предполагается, что у нас уже есть объект AVCaptureSession, представляющий собой текущий процесс записи, и связанные с ним объекты AVCaptureDeviceInput (представляющий камеру) и AVCaptureVideoDataOutput (представляющий вывод данных). Сначала необходимо создать делегата для объекта вывода, который бы реагировал на получение очередного кадра видео вызовом метода делегата captureOutput:didOutputSampleBuffer:fromConnection:. Отметим, что в этот метод передаётся буфер, содержащий очередной кадр, в виде объекта CMSampleBufferRef. Этот объект мы можем использовать для создания объекта CVImageBufferRef, а его область памяти можно использовать для создания графического контекста с той же самой информацией внутри него, то есть изображением с камеры. Это выполняется вызовом функции CGBitmapContextCreate. Далее требуется только получить изображение в виде UIImage из текущего контекста. Пример описанного подхода приведён ниже.
[gist id=4233239 bump=3]
Подобный способ получения изображений с медиа-объектов AVFoundation является почти универсальным — его можно использовать также и для снятия скриншотов с содержимого видеоплеера, нужно лишь получить буфер, содержащий очередной кадр. К слову, именно такой способ используется в популярном фреймворке GPUImage для наложения фильтров на видео. Подробнее о снятии скриншотов с видео будет рассказано далее.
Особенных нюансов в использовании этого способа Screen Capture обнаружено не было. Упомянем только о том, что в Apple также описали процесс снятия изображения в том случае, если на экране одновременно присутствуют элементы UIKit и видео, снимаемое камерой. Например, если необходимо, чтобы в скриншот вместе с видео попали элементы интерфейса. Способ описан в Q&A QA1714, и по сути представляет собой комбинацию первого и третьего методов. Предлагается сначала отправить в графический контекст изображение, полученное с камеры, а затем добавить туда интерфейс через renderInContext. После чего в контексте будут одновременно находиться скриншот, содержащий как интерфейс, так и кадр изображения с камеры.
Что же мы имеем в итоге?
Итак, Apple предоставила замену функции UIGetScreenImage. Однако остаётся вопрос — является ли эта замена полной? Отнюдь нет. Хотя представленные выше функции и их комбинации могут быть использованы практически во всех случаях Screen Capture, по меньшей мере один вариант остался без внимания. Как быть в том случае, если необходимо снять скриншот с видео на плеере, если оно в данный момент находится на экране? Ведь в таком случае при использовании первого способа на скриншоте вместо плеера с кадром из видео будет лишь пустое место, а второй и третий способ являются слишком частными. По неизвестным причинам, в Apple не стали освещать эту область, поэтому мы провели собственное исследование этого вопроса. В следующих разделах освещаются вопросы получения скриншотов из видео.
Screen Capture в видео
Итак, вам нужно снять кадр из видео, проигрывающегося в вашем приложении. Ни один из методов, предложенных ранее, не сработает. Видеоконтент не является частью UIKit, имеет мало общего с OpenGL ES, а также не является видеопотоком с камеры телефона. Что делать в этом случае?
Существует, как минимум, два подхода к решению такой задачи. Их применение зависит от типа плеера.
Типов плеера в iOS всего два — это MPMoviePlayer из фреймворка MediaPlayer и AVPlayer из фреймворка AVFoundation. Как было описано в нашей статье «Воспроизведение видео в iOS приложениях», первый плеер представляет собой удобное готовое решение с функциями «из коробки» для проигрывания медиафайлов. Его минусом является невозможность настроить его функционал, поведение и внешний вид. Второй плеер, наоборот, предоставляет вам возможность целиком и полностью контролировать все аспекты его работы и интерфейса, однако закономерно является более сложным в использовании.
Снятие изображений с плеера MPMoviePlayer
MPMoviePlayer предоставляет удобный метод для снятия изображений с видео, предназначенный для создания ярлыков объектов, представляющих то или иное видео в интерфейсе приложения. Это метод
— (UIImage *)thumbnailImageAtTime:timeOption.Его описание можно найти в официальной документации. Он позволяет задать время, соответствующее требуемому кадру, а также определить, какой кадр вам важнее получить — ближайший к требуемому времени ключевой кадр или точный кадр по заданному времени. Вызов этого метода — все, что требуется сделать для получения скриншота.
Снятие изображений с плеера AVPlayer
С плеером MPMoviePlayer все оказалось довольно просто. AVPlayer более сложен, поэтому закономерно предположить, что и получить изображение его видеоконтента также будет труднее. Так и есть. Существует по меньшей мере два способа получить изображение с этого плеера.
Первый способ — использовать класс AVAssetImageGenerator. Он содержит специальный метод
— (CGImageRef)copyCGImageAtTime:actualTime:error: .Как описано в документации, метод возвращает ссылку на изображение по указанному времени. Однако существует одна тонкость. На самом деле, методом будет возвращён кадр, ближайший к заданному времени. Поэтому в метод и передаётся ссылка на параметр actualTime: чтобы сообщить реальное время, соответствующее кадру. Очевидно, если вас не сильно интересует точность взятого кадра, то этот метод весьма удобен в использовании.
[gist id=4233335]
Второй способ снятия скриншота из плеера чуть более сложен. Он состоит в получении объекта CMSampleBufferRef из видео и последующем извлечении изображения из него. Так что нам не придётся объяснять его целиком — вторая часть метода уже описана нами в разделе про получение изображения с камеры. Получить буфер с изображением возможно следующим путем.
- Сначала необходимо создать объект AVAsset, содержащий в себе всю информацию о воспроизводимом медиа-контенте.
- Затем необходимо получить массив треков в этом объекте (за подробной информацией об устройстве AVAsset обращайтесь к официальной документации). Из всех треков выбрать интересующий.
- Для трека нужно создать объект AVAssetReaderTrackOutput для вывода содержимого трека и добавить его к предварительно созданному объекту для чтения содержимого — AVAssetReader.
- После этого очень легко получить буфер. Достаточно вызвать метод copyNextSampleBuffer (ссылка на описание в документации класса) для объекта AVAssetReaderTrackOutput.
Пример кода, иллюстрирующий этот подход, приведен ниже.
[gist id=4249346]
Как уже было отмечено, такой способ довольно универсален и позволяет работать с любыми видео — локальными или же расположенным на сервере.
Итак, мы описали основные методы получения скриншотов с видео. В заключение, мы хотели бы лишь рассказать о весьма неприятной проблеме с проведением описанных действий с видео, передающимся по протоколу HLS.
Проблема с HTTP Live Streaming
Все описанные методы прекрасно выполняют свою функцию. Однако если вы попытаетесь применить их в случае, когда ваше приложение проигрывает видео, которое оно получает по протоколу HTTP Live Streaming, вас ждёт неприятный сюрприз. С такого видео невозможно снять скриншот. Причина такого поведения HLS-видео описана в документе AVFoundation Release Notes for iOS 4.3, в главе «Enhancements for HTTP Live Streaming».
Дело в том, что ввиду динамической природы стримингового видео реальная длительность и набор треков его файла может и будет отличаться от аналогичных показателей для этого видео в плеере. Поэтому во избежание неприятных последствий этого факта, объект AVAsset спроектирован таким образом, что массив треков для передаваемого файла будет пуст. А это значит, что ни один метод получения кадра с видео не сработает, так как кадры попросту неоткуда взять. Более того, здесь указано, что даже в случае непустого массива треков метод copyNextSampleBuffer всё равно вернёт nil для стримингового видео.
Всё это в совокупности объясняет наблюдаемую нами в ходе исследования полную невозможность каждым из описанных способов получить скриншот с видео, передаваемого по HLS.
* * *
В этой статье мы постарались наиболее полно описать способы Screen Capture, доступные для использования в iOS.
Надо отметить, что закрытый по сей день UIGetScreenImage лишён каких-либо ограничений и позволяет снять изображение экрана в любом из описанных в статье случаев, включая и случай с HLS-видео. Однако отказ в использовании этого метода в итоге заставил разработчиков искать иные подходы к решению задачи получения скриншотов. Представленные в статье методы покрывают практически все случаи снятия скриншотов. Единственная замеченная на данный момент проблема — невозможность получить скриншоты стримингового видео, получаемого по протоколу HTTP Live Streaming. Пока остаётся лишь надеяться, что в будущем Apple устранит эту досадную неприятность.
Надеемся, что вы почерпнули много полезных сведений из нашей статьи и нашли ответы на ваши вопросы касательно Screen Capture в iOS.object