Monday, October 1, 2012

Android Media API

Вместе с выходом Android 4.1 был представлен новый Меdia API Поначалу данное событие мною воспринялось как многообещающее, но после попыток раскурить его, энтузиазма поубавилось. И так попорядку.

Видео

Автор раскрывает некоторые возможности, делится общими принципами того, как это хозяйство использовать. Т.к с предметной областью мне приходилось пересекаться, то возникло кипучее желание все испытать, покрутить, убедиться и т.д.

На первых парах API выглядит достаточно простым и стройным. Полистав слайды, кажется, что демо можно накидать за пару часов. Это почти правда. Как выяснилось потом, на слайдах нет самых интересных кусков кода. Поэтому к некоторым вещам приходилось идти империческим путем т.е. методом проб и ошибок. Загуглив, я лишь нашел javadoc и один юнит тест :). Негусто. Качнул исходники Android 4.1 и "грепнул" их - толку тоже мало. В общем самым полезным из всего найденного оказался юнит тест.

Абстрактный декодер

Напишим три класса: абстрактный декодер и два наследника. Один для видео другой для аудио. В наших примерах мы будем играть либо видео, либо аудио. Позже я объясню почему. А сейчас код:
import java.nio.ByteBuffer;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.util.Log;
import android.view.Surface;
public abstract class AbstractTrackDecoder extends Thread {
private static final String TAG = AbstractTrackDecoder.class.getSimpleName();
protected final long TIMEOUT_US = 5000;
protected boolean hasInputData = true;
protected boolean hasOutputData = true;
protected MediaExtractor extractor;
protected MediaFormat format;
protected String mime;
protected int trackNum;
protected MediaCodec codec;
protected ByteBuffer[] inputBuffers;
protected ByteBuffer[] outputBuffers;
protected MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
/**
* Index of the current buffer(decoded data), which is the first in the
* buffers queue.
*/
protected int outputBufferIndex = -1;
protected AbstractTrackDecoder(String path, String prefix, Surface surface) {
this.extractor = new MediaExtractor();
this.extractor.setDataSource(path);
initializeCodec(extractor, prefix, surface);
}
private void initializeCodec(MediaExtractor extractor, String prefix, Surface surface) {
int trackCount = extractor.getTrackCount();
for (int i = 0; i < trackCount; i++) {
format = extractor.getTrackFormat(i);
mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith(prefix)) {
trackNum = i;
codec = MediaCodec.createDecoderByType(mime);
codec.configure(format, surface, null /* crypto */, 0 /* flags */);
codec.start();
inputBuffers = codec.getInputBuffers();
outputBuffers = codec.getOutputBuffers();
Log.d(TAG, "Found: " + mime);
break;
}
}
}
public boolean isHasRawData() {
return hasInputData;
}
public boolean isHasDecodedData() {
return hasOutputData;
}
public void cleanup() {
if (codec == null) {
return;
}
codec.stop();
codec.release();
codec = null;
}
public void dequeueInput() throws InterruptedException {
int inputBufferIndex = codec.dequeueInputBuffer(TIMEOUT_US);
if (inputBufferIndex >= 0) {
ByteBuffer dst = inputBuffers[inputBufferIndex];
extractor.selectTrack(trackNum);
int sampleSize = extractor.readSampleData(dst, 0 /* offset */);
if (sampleSize >= 0) {
long pts = extractor.getSampleTime();
codec.queueInputBuffer(inputBufferIndex, 0 /* offset */, sampleSize, pts, 0);
extractor.advance();
} else {
codec.queueInputBuffer(inputBufferIndex, 0 /* offset */, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
hasInputData = false;
Log.d(TAG, "Raw data EOS.");
}
} else {
// no buffer is available
synchronized (this) {
wait();
}
}
}
public void dequeueOutput() {
outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US);
if (outputBufferIndex >= 0) {
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
Log.d(TAG, "Decoded data EOS.");
hasOutputData = false;
}
}
}
public void systemTime(long us) {
if (bufferInfo.presentationTimeUs / 1000 <= us) {
processDecodedData();
dequeueOutput();
synchronized (this) {
notify();
}
}
}
protected abstract void processDecodedData();
@Override
public void run() {
while (true) {
if (hasInputData) {
try {
dequeueInput();
} catch (Exception e) {
Log.e(TAG, "reading data", e);
break;
}
}
if (!hasOutputData) {
break;
}
}
}
}

Класс представляет собой тред, который "молотит" до момента пока в очереди есть декодированные данные(иначе говоря есть что отрисовывать/проигрывать). Важный момент - это инициализации входного/выходного буфера, и их заполнение/вычитка - методы dequeueInput()/dequeueOutput(). На начальном этапе трудности и вопросы возникает именно здесь.

Видео декодер

Класс выглядить достаточно просто. Нам нужно передать ссылку на канву и декодер отрисует все за нас. Порадовало то, что не нужно заморачиваться со всякого рода мелочами: формат пиксела и т.д. Собственно код:
import android.view.Surface;
class VideoTrackDecoder extends AbstractTrackDecoder {
private static final String TAG = VideoTrackDecoder.class.getSimpleName();
private static final String VIDEO_PREFIX = "video/";
public VideoTrackDecoder(String path, Surface surface) {
super(path, VIDEO_PREFIX, surface);
}
@Override
protected void processDecodedData() {
if (outputBufferIndex < 0) {
return;
}
codec.releaseOutputBuffer(outputBufferIndex, true /* render */);
}
}
Второй параметр метода releaseOutputBuffer(..., true) "говорит", что мы рендерим картинку средствами декодера.

Аудио декодер

Кода на пару строк больше - создаем аудио трэк, а в качестве канвы передаем null(ибо для звука в ней нет необходимости).
import java.nio.ByteBuffer;
import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.media.MediaFormat;
import android.net.Uri;
class AudioTrackDecoder extends AbstractTrackDecoder {
private static final String TAG = AudioTrackDecoder.class.getSimpleName();
private static final String AUDIO_PREFIX = "audio/";
private static final int AUDIO_TRACK_BUFFER_SIZE = 2048;
private AudioTrack audioTrack;
public AudioTrackDecoder(String path) {
super(path, AUDIO_PREFIX, null/* surface */);
initilizeAudio();
}
private void initilizeAudio() {
int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE);
int chanelCfg = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRate,
chanelCfg /*AudioFormat.CHANNEL_CONFIGURATION_MONO*/, AudioFormat.ENCODING_PCM_16BIT,
AUDIO_TRACK_BUFFER_SIZE, AudioTrack.MODE_STREAM);
audioTrack.play();
}
@Override
protected void processDecodedData() {
if (outputBufferIndex < 0) {
return;
}
ByteBuffer buf = outputBuffers[outputBufferIndex];
final byte[] chunk = new byte[bufferInfo.size];
buf.get(chunk);
buf.clear();
if (chunk.length > 0) {
audioTrack.write(chunk, 0, chunk.length);
}
codec.releaseOutputBuffer(outputBufferIndex, false /* render */);
}
@Override
public void cleanup() {
audioTrack.stop();
audioTrack.release();
audioTrack = null;
super.cleanup();
}
}

Проблема номер один - синхронизация

Если честно, не нашел вменяемого способа, как синхронизировать аудио/видео в рамках данной задачи. Опробовав разные методы, пришел к очень примитивному, но в то же время простому - синхронизация по внешнему таймеру. Есть некий тред, который "посылает сигнал" декодеру, передавая текущую временную метку. Декодер получив уведомление, "проверяет" есть ли в очереди пакет PTS(presentation time stamp) которого равен либо меньше текущего значения метки. Если есть, то дергаем processDecodedData(). Повторюсь, способ убогий и не обеспечивает должной точности синхронизации, но он работает и для простенького демо сойдет. Пример синхронизации аудио(для видео абсолютно тоже самое за одним лишь отличием - вмето AudioTrackDecoder создать экземпляр VideoTrackDecoder с доп. параметром surface):
public class Decoder extends Thread {
private static final String TAG = Decoder.class.getSimpleName();
private AbstractTrackDecoder audioTrackDecoder;
private boolean running = true;
public Decoder(String path) {
audioTrackDecoder = new AudioTrackDecoder(path);
}
@Override
public void run() {
audioTrackDecoder.start();
long startTime = System.nanoTime();
while (running && audioTrackDecoder.hasOutputData) {
long t = (System.nanoTime() - startTime) / 1000;
audioTrackDecoder.systemTime(t);
}
audioTrackDecoder.cleanup();
}
public void terminate() {
running = false;
}
}
view raw Decoder.java hosted with ❤ by GitHub

Проблема номер два - MediaExtractor

Как говорит дока - сие есть демультиплексор. Задача которого извлекать сырые данные. Если мы хотим работать по отдельности только с аудио, либо только с видео, то вопросов нет. А вот если одновременно, например, проиграть видео файл, то парочку есть. Перед чтением необходимо вызвать метод selectTrack, указать номер трэка в контейнере с которым мы собираемся работать. Допустим есть два трэда - один аудио декодер, другой видео декодер. Есть экземпляр медиа экстрактора расшаренный между этими тредами. При такой схеме у меня дико падала производительность, видео было куцее, звук прерывистый. Второй вариант - создать отдельный медиа экстрактор для каждого трэда. Показал гораздо лучшие результаты чем первый, но с точки зрения здравого смысла он не верный. Т.к. ресурс один, а соединений два(одно соединение для видео, другое - для аудио).

Выводы

Плюсы:
  • Pure java - не нужно париться с NDK
  • Относительно простой API
Минусы:
  • Нет примеров

No comments:

Post a Comment