- Using multiple camera streams simultaneously
- Use cases for multiple camera streams
- Multiple targets per request
- Output type
- Output size
- Hardware level
- Putting all the pieces together
- Summary
- Android Camera2 API от чайника, часть 3. Media Codec и стрим видео по UDP
- Что нам для этого нужно?
- И закончим прицеплением Media Codec
- Теперь пойдём искать нужный адрес на компьютере
- Как же так?
Using multiple camera streams simultaneously
This blog post is the latest one in the current series about camera on Android; we have previously covered camera enumeration and camera capture sessions and requests.
Use cases for multiple camera streams
A camera application might want to use more than one stream of frames simultaneously, in some cases different streams even require a different frame resolution or pixel format; some typical use cases include:
- Video recording: one stream for preview, another being encoded and saved into a file
- Barcode scanning: one stream for preview, another for barcode detection
- Computational photography: one stream for preview, another for face / scene detection
As we discussed in our previous blog post, there is a non-trivial performance cost when we process frames, and the cost is multiplied when doing parallel stream / pipeline processing.
Resources like CPU, GPU and DSP might be able to take advantage of the framework’s reprocessing capabilities, but resources like memory will grow linearly.
Multiple targets per request
Multiple camera streams can be combined into a single CameraCaptureRequest by performing a somewhat bureaucratic procedure. This code snippet illustrates how to setup a camera session with one stream for camera preview and another stream for image processing:
If you configure the target surfaces correctly, this code will only produce streams that meet the minimum FPS determined by StreamComfigurationMap.GetOutputMinFrameDuration(int, Size) and StreamComfigurationMap.GetOutputStallDuration(int, Size). Actual performance will vary from device to device although, Android gives us some guarantees for supporting specific combinations depending on three variables: output type, output size and hardware level. Using an unsupported combination of parameters may work at a low frame rate; or it may not work at all, triggering one of the failure callbacks. The documentation describes in great detail what is guaranteed to work and it is strongly recommended to read it in full, but we will cover the basics here.
Output type
Output type refers to the format in which the frames are encoded. The possible values described in the documentation are PRIV, YUV, JPEG and RAW. The documentation best explains them:
PRIV refers to any target whose available sizes are found using StreamConfigurationMap.getOutputSizes(Class) with no direct application-visible format
YUV refers to a target Surface using the ImageFormat.YUV_420_888 format
JPEG refers to the ImageFormat.JPEG format
When choosing your application’s output type, if the goal is to maximize compatibility then the recommendation is to use ImageFormat.YUV_420_888 for frame analysis and ImageFormat.JPEG for still images. For preview and recording scenarios, you will likely be using a SurfaceView , TextureView , MediaRecorder , MediaCodec or RenderScript.Allocation . In those cases do not specify an image format and for compatibility purposes it will count as ImageFormat.PRIVATE (regardless of the actual format used under the hood). To query the formats supported by a device given its CameraCharacteristics, use the following code:
Output size
All available output sizes are listed when we call StreamConfigurationMap.getOutputSizes(), but as far as compatibility goes we only need to worry about two of them: PREVIEW and MAXIMUM. We can think of those sizes as upper bounds; if the documentation says something of size PREVIEW works, then anything with a size smaller than PREVIEW also works. Same applies to MAXIMUM. Here’s a relevant excerpt from the documentation:
For the maximum size column, PREVIEW refers to the best size match to the device’s screen resolution, or to 1080p (1920×1080), whichever is smaller. RECORD refers to the camera device’s maximum supported recording resolution, as determined by CamcorderProfile. And MAXIMUM refers to the camera device’s maximum output resolution for that format or target from StreamConfigurationMap.getOutputSizes(int).
Note that the available output sizes depend on the choice of format. Given the CameraCharacteristics and a format, we can query for the available output sizes like this:
In the camera preview and recording use cases, we should be using the target class to determine supported sizes since the format will be handled by the camera framework itself:
Getting MAXIMUM size is easy — just sort the output sizes by area and return the largest one:
Getting PREVIEW size requires a little more thinking. Recall that PREVIEW refers to the best size match to the device’s screen resolution, or to 1080p (1920×1080), whichever is smaller. Keep in mind that the aspect ratio may not match the screen’s aspect ratio exactly, so we may need to apply letter-boxing or cropping to the stream if we plan on displaying it in full screen mode. In order to get the right preview size, we need to compare the available output sizes with the display size while taking into account that the display may be rotated. In this code, we also define a helper class SmartSize that will make size comparisons a little easier:
Hardware level
To determine the available capabilities at runtime the most important piece of information a camera application needs is the supported hardware level. Once again, we can lean on the documentation to explain this to us:
The supported hardware level is a high-level description of the camera device’s capabilities, summarizing several capabilities into one field. Each level adds additional features to the previous one, and is always a strict superset of the previous level. The ordering is LEGACY
Putting all the pieces together
Once we understand output type, output size and hardware level we can determine which combinations of streams are valid. For instance, here’s a snapshot of the configurations supported by a CameraDevice with LEGACY hardware level. The snapshot is taken from the documentation for createCaptureSession method:
Since LEGACY is the lowest possible hardware level, we can infer from the previous table that every device that supports Camera2 (i.e. API level 21 and above) can output up to three simultaneous streams using the right configuration — that’s pretty cool! However, it may not be possible to achieve the maximum available throughput on many devices because your own code will likely incur overhead which invokes other constraints that limit performance, such as memory, CPU and even thermal.
Now that we have the knowledge necessary to set up two simultaneous streams with support guaranteed by the framework, we can dig a little deeper into the configuration of the target output buffers. For example, if we were targeting a device with LEGACY hardware level, we could setup two target output surfaces: one using ImageFormat.PRIVATE and another one using ImageFormat.YUV_420_888 . This should be a supported combination as per the table above as long as we use the PREVIEW size. Using the function defined above, getting the required preview sizes for a camera ID is now very simple:
We must wait until SurfaceView is ready using the provided callbacks, like this:
We can even force the SurfaceView to match the camera output size by calling SurfaceHolder.setFixedSize(), but it may be better in terms of UI to take an approach similar to FixedAspectSurfaceView from the HDR viewfinder sample on GitHub, which sets an absolute size taking into consideration both the aspect ratio and the available space, while automatically adjusting when activity changes are triggered.
Setting up the other surface from ImageReader with the desired format is even easier, since there are no callbacks to wait for:
When using a blocking target buffer like ImageReader , we need to discard the frames after we used them:
We should keep in mind that we are targeting the lowest common denominator — devices with LEGACY hardware level. We could add conditional branching and use RECORD size for one of the output target surfaces in devices with LIMITED hardware level or even bump that up to MAXIMUM size for devices with FULL hardware level.
Summary
In this article, we have covered:
- Using a single camera device to output multiple streams simultaneously
- The rules for combining different targets in a single capture request
- Querying and selecting the appropriate output type, output size and hardware level
- Setting up and using a Surface provided by SurfaceView and ImageReader
With this knowledge, now we can create a camera app that has the ability to display a preview stream while performing asynchronous analysis of incoming frames in a separate stream.
Источник
Android Camera2 API от чайника, часть 3. Media Codec и стрим видео по UDP
Итак, со съемками фоточек и записью видео при помощи Camera2 API мы вроде бы, разобрались. Осталось только научиться передавать видеопоток c Android устройства страждущим получателям извне. Конечной целью, как уже неоднократно ранее говорилось, является интеллектуализация роботелеги — ставим на неё смартфон и так сказать, превращаем обезьяну в человека. В этом нам поможет Media Codec. И конечно, новое Camera2 API.
Кому интересно, прошу под кат.
Детали о проекте с роботележкой можно найти здесь, а мы пока займемся непосредственно стримингом видео с неё (вернее с прицепленного к ней Android смартфона) на персональную электронно-вычислительную машину.
Что нам для этого нужно?
Для того чтобы передать видео поток с экрана смартфона куда-либо ещё, как известно, его (поток) сначала необходимо преобразовать в подходящий ужатый формат (передавать покадрово выйдет слишком толсто), поставить time-stamps (временные метки) и отправить в бинарном виде получателю. Который произведёт обратную операцию декодирования.
Как раз этими низкоуровневыми чёрными делами и занимается класс Media Codec с 2013 года, с даты выхода Аndroid 4.3.
Другое дело, что раньше подступиться к кодированию видео, в отличие от сегодняшнего дня было не так-то просто. Чтобы вытащить картинку с камеры надо было использовать тонны загадочного кода в котором, как в заклинаниях якутских шаманов, единственная неточность могла привести к полному краху приложения. Добавьте к этому ещё предыдущее Camera API, где вместо готовых коллбэков приходилось ручками самостоятельно писать разные synchronized, а это занятие, скажем так, не для слабых духом.
И главное, издалека смотришь на рабочий код, вроде бы в общих чертах всё ясно. Начинаешь переносить по частям в свой проект — сыпется непонятно почему. А скорректировать не получается, потому что в деталях уже разобраться тяжело.
Да и от сплошных deprecated как-то не по себе. Короче говоря, непорядок
К счастью, для непонятливых гуглостроители ввели волшебную концепцию поверхности Surface, работая с которой, можно избежать низкоуровневых деталей. Какой ценой и что при этом теряет разработчик, мне как неспециалисту понять сложно, но зато теперь мы чуть ли не буквально можем сказать: «Android, возьми эту Surface на которую отображается видео с камеры и ничего там не меняя, ну вот как есть, закодируй и отправь дальше». И самое удивительное, что это работает. А с новым Camera2 API программа и сама знает, когда данные отправлять, коллбэки ж новые появились!
Так что теперь закодировать видео — раз плюнуть. Чем мы сейчас и займёмся.
Берём код из первой статьи и как обычно выкидываем из него всё кроме кнопочек и инициализации камеры.
И закончим прицеплением Media Codec
В прошлом посте мы выводили на Surface изображение с камеры и с него же писали видео при помощи MediaRecorder. Для этого мы просто указывали оба компонента в списке Surface.
Здесь то же самое, только вместо mMediaRecorder указываем:
Получается, что-то типа:
Что такое mEncoderSurface? А это та самая Surface, с которой будет работать Media Codec. Только для начала надо их обоих инициализировать примерно таким образом.
Теперь остается прописать единственный коллбэк. Когда Media Codec вдруг ощутит, что очередные данные для дальнейшей трансляции готовы, он нас об этом известит именно через него:
Байтовый массив outDate — это настоящее сокровище. В нём уже готовые кусочки закодированного в формате H264 видеопотока, с которым мы теперь можем делать всё, что захотим.
Некоторые кусочки может быть великоваты для передачи по сети, ну да ничего, система, если надо их порубит ещё сама и отправит дальше получателю.
Но если сильно страшно, то покромсать можно самому, впихнув такой фрагмент
Но нам пока надо убедиться воочию, что данные в буфере это действительно видеопоток в формате H264. Поэтому, давайте мы их отправим в файл:
Пропишем в сетапе:
А в коллбэке где буфер:
Открываем приложение, жмем кнопку: «ВКЛЮЧИТЬ КАМЕРУ И СТРИМ». Начинается автоматически запись. Ждем немного и давим кнопку остановки.
Сохраненный файл штатно скорее всего не проиграется, поскольку формат не MP4, но если открыть его VLC плеером или сконвертить онлайн каким нибудь ONLINE CONVERT, то мы убедимся, что находимся на правильном пути. Правда изображение лежит на боку, но это поправимо.
Вообще, для каждого события записи, фотографирования или стрима, лучше, конечно, открывать каждый раз новую сессию, а старую закрывать. То есть, сначала мы включаем камеру и запускаем голое превью. Потом, если надо сделать снимок, превью закрываем и открываем превью, но уже с пристегнутым Image Reader. Если переходим на запись видео, то закрываем текущую сессию и запускаем сессию с превью и прицепленным к нему Media Recorder. Я этого не делал, чтобы не страдала наглядность кода, а вам решать, как удобнее самим.
А вот и сам код целиком.
И не забудьте про разрешения в манифесте.
Итак, мы убедились, что Media Codec работает. Но использовать его для записи видео в файл как-то бездуховно. С такой задачей гораздо лучше справится Media Recorder, да ещё и звук добавит. Поэтому файловую часть мы снова выкинем и добавим блок кода для стриминга видео в сеть по udp протоколу. Это тоже очень просто.
Сначала инициализируем UDP практически сервер.
А в том же коллбэке, где мы отправляли данные по готовности в поток для файла, отправим их теперь в виде дэйтаграмм в нашу домашнюю сеть (надеюсь она есть у всех?)
Казалось бы, но нет. Приложение при запуске скрашится. Видите-ли, системе не нравится, что в главном потоке мы отправляем всякие дэйтаграмм пакеты. Но для паники нет оснований. Во-первых мы хоть и в главном потоке, но работаем все равно асинхронно, то есть по срабатыванию коллбэка. Во-вторых отправка udp пакетов, такой же асинхронный процесс. Мы только говорим операционной системе, что неплохо было бы отправить пакетик, но, что мы всецело в этом деле полагаемся на неё. Поэтому, чтобы Android не бунтовал, то в начале программы добавим две строчки:
В общем и целом, получится следующая маленькая элегантная демонстрационная программка:
Не знаю, как у других, но на моем Red Note 7 даже видно, как скачут килобайты по нужному адресу
И таких udp сокетов можно наплодить множество, на сколько хватит пропускной способности сети. Главное, чтобы были адреса куда. Будет у вас широкоадресная рассылка.
Теперь пойдём искать нужный адрес на компьютере
Надо сказать, что не каждая компьютерная программа способна всосать и переварить видео поток формата H264 по единственному udp каналу без какой-либо дополнительной информации. Но некоторые могут. Это например крайне широко известный медиаплеер VLC. Это настолько крутая штука, что если начать описывать её возможности, то из статьи получится целая книга. Наверняка у вас она есть. Если нет, поставьте.
И судя по описанию команд для него, udp пакеты переварить этот плеер может.
Причём все эти source address и bind address, по идее и не нужны. Нужен только прослушиваемый порт.
И ещё, конечно, нужно не забыть про разрешение этот порт слушать (малварь же)
А вы знали, что Винда не дает сделать принтскрин с монитора ресурсов?
Или можно вообще брандмауэр отключить (не рекомендую)
Итак, преодолев эти тернии, запускаем VLC плеер с нашим адресом и наслаждаемся пустым экраном. Видео нет.
Как же так?
А вот так. У вас, наверно, стоит последняя версия VLC 3.08 Vetinari? Вот как раз, в этой версии udp объявлен deprecated и мало того выпилен нахрен.
Так-то логика разработчиков плеера понятна. Мало кому нужно использовать голый udp канал в наше время потому-что:
- Нормально работает только в домашней неперегруженной сети. Стоит вам выйти во внешний мир и ненумерованные дэйтаграммы начнут теряться и приходить не в том порядке, в котором их послали. А для видео декодера это очень неприятно.
- Незашифрован и легко компрометируется
Поэтому нормальные люди, конечно, используют протоколы более высокого уровня RTP и другие. То есть на пальцах — вы пишете сервер, который всё равно на низком уровне использует udp (для скорости), но параллельно обменивается управляющей информацией с клиентом кому он стримит видео. Какая там у него пропускная способность, не надо ли увеличить-уменьшить кэш для данных, какая детализация изображения оптимальна сейчас и так далее и тому подобное. Опять же звук тоже иногда нужен. А ему требуется, сами понимаете, синхронизация с видео.
Вон ребятам из Одноклассников даже пришлось свой протокол запилить для стриминга. Но у них задачи-то, конечно, гораздо более важные — рассылать видео с котиками десяткам миллионов домохозяек по всему миру. Там одним udp каналом не обойдёшься.
Но нам-то писать свой RTP сервер на андроиде как-то грустно. Наверное, можно найти даже готовый и даже бесплатный, но попробуем пока не усложнять сущностей. Просто возьмем версию VLC плеера, где udp стриминг ещё работал.
Устанавливаем вместо или рядом со старым (то есть новым VLC), как вам заблагорассудится.
Запускаем и снова видим пустой экран.
А все это потому, что мы явно не настроили использование кодека H264. Так-то VLC смог бы выбрать кодек автоматически, если бы имел дело с файлом (в настройках изначально, как раз и указан автоматический выбор). Но ему-то кидают байтовый поток по единственному каналу, а кодеков, которые VLC поддерживает десятки. Как ему разобраться, какой применить?
Поэтому устанавливаем кодек силой.
И вот теперь наслаждаемся трансляцией «живого» видео. Единственное, оно зачем-то лежит на боку, но это уже легко поправить в настройках видеоплеера.
А ещё можно просто запускать плеер из командной строки по такому ключу:
Источник