package com.o3dr.android.client.utils.video;
import android.annotation.TargetApi;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.Build;
import android.os.Handler;
import android.util.Log;
import android.view.Surface;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* Decodes video stream bytes for playing back in a Surface.
* Created by Fredia Huya-Kouadio on 2/19/15.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2)
public class MediaCodecManager {
public interface NaluChunkListener {
void onNaluChunkUpdated(NaluChunk parametersSet, NaluChunk dataChunk);
}
private static final String TAG = MediaCodecManager.class.getSimpleName();
private static final String MIME_TYPE = "video/avc";
public static final int DEFAULT_VIDEO_WIDTH = 1920;
public static final int DEFAULT_VIDEO_HEIGHT = 1080;
private final Runnable stopSafely = new Runnable() {
@Override
public void run() {
processInputData.set(false);
sendCompletionFlag.set(false);
naluChunkAssembler.reset();
if (dequeueRunner != null && dequeueRunner.isAlive()) {
Log.d(TAG, "Interrupting dequeue runner thread.");
dequeueRunner.interrupt();
}
dequeueRunner = null;
final MediaCodec mediaCodec = mediaCodecRef.get();
if (mediaCodec != null) {
try {
mediaCodec.stop();
}catch(IllegalStateException e){
Log.e(TAG, "Error while stopping media codec.", e);
}
mediaCodec.release();
mediaCodecRef.set(null);
}
surfaceRef.set(null);
isDecoding.set(false);
handler.post(decodingEndedNotification);
}
};
private final Runnable decodingStartedNotification = new Runnable() {
@Override
public void run() {
final DecoderListener listener = decoderListenerRef.get();
if (listener != null)
listener.onDecodingStarted();
}
};
private final Runnable decodingErrorNotification = new Runnable() {
@Override
public void run() {
final DecoderListener listener = decoderListenerRef.get();
if (listener != null)
listener.onDecodingError();
}
};
private final Runnable decodingEndedNotification = new Runnable() {
@Override
public void run() {
final DecoderListener listener = decoderListenerRef.get();
if (listener != null)
listener.onDecodingEnded();
}
};
private final AtomicBoolean decodedFirstFrame = new AtomicBoolean(false);
private final AtomicBoolean isDecoding = new AtomicBoolean(false);
private final AtomicBoolean processInputData = new AtomicBoolean(false);
private final AtomicBoolean sendCompletionFlag = new AtomicBoolean(false);
private final AtomicReference<Surface> surfaceRef = new AtomicReference<>();
private final AtomicReference<MediaCodec> mediaCodecRef = new AtomicReference<>();
private final AtomicReference<DecoderListener> decoderListenerRef = new AtomicReference<>();
private final NaluChunkAssembler naluChunkAssembler;
private final Handler handler;
private final AtomicReference<NaluChunkListener> naluChunkListenerRef = new AtomicReference<>();
private DequeueCodec dequeueRunner;
public MediaCodecManager(Handler handler) {
this.handler = handler;
this.naluChunkAssembler = new NaluChunkAssembler();
}
public void setNaluChunkListener(NaluChunkListener naluChunkListener) {
this.naluChunkListenerRef.set(naluChunkListener);
}
public Surface getSurface(){
return surfaceRef.get();
}
public void startDecoding(Surface surface, DecoderListener listener) throws IOException {
if (surface == null)
throw new IllegalStateException("Surface argument must be non-null.");
if (isDecoding.compareAndSet(false, true)) {
Log.i(TAG, "Starting decoding...");
this.naluChunkAssembler.reset();
this.decoderListenerRef.set(listener);
final MediaFormat mediaFormat = MediaFormat.createVideoFormat(MIME_TYPE, DEFAULT_VIDEO_WIDTH, DEFAULT_VIDEO_HEIGHT);
final MediaCodec mediaCodec = MediaCodec.createDecoderByType(MIME_TYPE);
mediaCodec.configure(mediaFormat, surface, null, 0);
mediaCodec.start();
surfaceRef.set(surface);
mediaCodecRef.set(mediaCodec);
processInputData.set(true);
dequeueRunner = new DequeueCodec();
dequeueRunner.start();
}
}
public void stopDecoding(DecoderListener listener) {
Log.i(TAG, "Stopping input data processing...");
this.decoderListenerRef.set(listener);
if(!isDecoding.get()) {
if (listener != null) {
notifyDecodingEnded();
}
}
else {
if(decodedFirstFrame.get()) {
if (processInputData.compareAndSet(true, false)) {
sendCompletionFlag.set(!processNALUChunk(naluChunkAssembler.getEndOfStream()));
}
}
else{
handler.post(stopSafely);
}
}
}
public void onInputDataReceived(byte[] data, int dataSize) {
if (isDecoding.get()) {
if (processInputData.get()) {
//Process the received buffer
NaluChunk naluChunk = naluChunkAssembler.assembleNALUChunk(data, dataSize);
if (naluChunk != null)
processNALUChunk(naluChunk);
} else {
if (sendCompletionFlag.get()) {
Log.d(TAG, "Sending end of stream data.");
sendCompletionFlag.set(!processNALUChunk(naluChunkAssembler.getEndOfStream()));
}
}
}
}
private boolean processNALUChunk(NaluChunk naluChunk) {
if (naluChunk == null)
return false;
final MediaCodec mediaCodec = mediaCodecRef.get();
if (mediaCodec == null)
return false;
try {
final int index = mediaCodec.dequeueInputBuffer(-1);
if (index >= 0) {
ByteBuffer inputBuffer;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
inputBuffer = mediaCodec.getInputBuffer(index);
} else {
inputBuffer = mediaCodec.getInputBuffers()[index];
}
if (inputBuffer == null)
return false;
inputBuffer.clear();
int totalLength = 0;
int payloadCount = naluChunk.payloads.length;
for (int i = 0; i < payloadCount; i++) {
ByteBuffer payload = naluChunk.payloads[i];
if (payload.capacity() == 0)
continue;
inputBuffer.order(payload.order());
final int dataLength = payload.position();
byte[] payloadData = payload.array();
inputBuffer.put(payloadData, 0, dataLength);
totalLength += dataLength;
}
NaluChunkListener naluChunkListener = naluChunkListenerRef.get();
if(naluChunkListener != null){
naluChunkListener.onNaluChunkUpdated(naluChunkAssembler.getParametersSet(), naluChunk);
}
mediaCodec.queueInputBuffer(index, 0, totalLength, naluChunk.presentationTime, naluChunk.flags);
}
} catch (IllegalStateException e) {
Log.e(TAG, e.getMessage(), e);
return false;
}
return true;
}
private void notifyDecodingStarted() {
handler.post(decodingStartedNotification);
}
private void notifyDecodingError() {
handler.post(decodingErrorNotification);
}
private void notifyDecodingEnded() {
handler.post(stopSafely);
}
private class DequeueCodec extends Thread {
@Override
public void run() {
final MediaCodec mediaCodec = mediaCodecRef.get();
if (mediaCodec == null)
throw new IllegalStateException("Start decoding hasn't been called yet.");
Log.i(TAG, "Starting dequeue codec runner.");
final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
decodedFirstFrame.set(false);
boolean doRender;
boolean continueDequeue = true;
try {
while (continueDequeue) {
final int index = mediaCodec.dequeueOutputBuffer(info, -1);
if (index >= 0) {
doRender = info.size != 0;
mediaCodec.releaseOutputBuffer(index, doRender);
if (decodedFirstFrame.compareAndSet(false, true)) {
notifyDecodingStarted();
Log.i(TAG, "Received first decoded frame of size " + info.size);
}
continueDequeue = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) == 0;
if (!continueDequeue) {
Log.i(TAG, "Received end of stream flag.");
}
}
}
} catch (IllegalStateException e) {
if(!isInterrupted()) {
Log.e(TAG, "Decoding error!", e);
notifyDecodingError();
}
} finally {
if (!isInterrupted())
notifyDecodingEnded();
Log.i(TAG, "Stopping dequeue codec runner.");
}
}
}
}