Вместе с выходом 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 */); | |
} | |
} |
Аудио декодер
Кода на пару строк больше - создаем аудио трэк, а в качестве канвы передаем 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; | |
} | |
} |
Проблема номер два - MediaExtractor
Как говорит дока - сие есть демультиплексор. Задача которого извлекать сырые данные. Если мы хотим работать по отдельности только с аудио, либо только с видео, то вопросов нет. А вот если одновременно, например, проиграть видео файл, то парочку есть. Перед чтением необходимо вызвать метод selectTrack, указать номер трэка в контейнере с которым мы собираемся работать. Допустим есть два трэда - один аудио декодер, другой видео декодер. Есть экземпляр медиа экстрактора расшаренный между этими тредами. При такой схеме у меня дико падала производительность, видео было куцее, звук прерывистый. Второй вариант - создать отдельный медиа экстрактор для каждого трэда. Показал гораздо лучшие результаты чем первый, но с точки зрения здравого смысла он не верный. Т.к. ресурс один, а соединений два(одно соединение для видео, другое - для аудио).
Выводы
Плюсы:- Pure java - не нужно париться с NDK
- Относительно простой API
- Нет примеров
No comments:
Post a Comment