blob: ba811d0d23ed1b786ed67f686d8a6fbc83aadebb [file] [log] [blame]
/*
* Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
*
* Use of this source code is governed by a BSD-style license
* that can be found in the LICENSE file in the root of the source
* tree. An additional intellectual property rights grant can be found
* in the file PATENTS. All contributing project authors may
* be found in the AUTHORS file in the root of the source tree.
*/
package org.webrtc.webrtcdemo;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.media.MediaCodec;
import android.media.MediaCrypto;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
import android.view.Surface;
import android.view.SurfaceView;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.LinkedList;
class MediaCodecVideoDecoder {
public static final int DECODE = 0;
private enum CodecName { ON2_VP8, GOOGLE_VPX, EXYNOX_VP8 }
private void check(boolean value, String message) {
if (value) {
return;
}
Log.e("WEBRTC-CHECK", message);
AlertDialog alertDialog = new AlertDialog.Builder(context).create();
alertDialog.setTitle("WebRTC Error");
alertDialog.setMessage(message);
alertDialog.setButton(DialogInterface.BUTTON_POSITIVE,
"OK",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
return;
}
}
);
alertDialog.show();
}
class Frame {
public ByteBuffer buffer;
public long timestampUs;
Frame(ByteBuffer buffer, long timestampUs) {
this.buffer = buffer;
this.timestampUs = timestampUs;
}
}
// This class enables decoding being run on a separate thread.
class DecodeHandler extends Handler {
@Override
public void handleMessage(Message msg) {
// TODO(dwkang): figure out exceptions just make this thread finish.
try {
switch (msg.what) {
case DECODE:
decodePendingBuffers();
long delayMillis = 5; // Don't busy wait.
handler.sendMessageDelayed(
handler.obtainMessage(DECODE), delayMillis);
break;
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static String TAG;
private Context context;
private SurfaceView surfaceView;
private DecodeHandler handler;
private Thread looperThread;
MediaCodec codec;
MediaFormat format;
// Buffers supplied by MediaCodec for pushing encoded data to and pulling
// decoded data from.
private ByteBuffer[] codecInputBuffers;
private ByteBuffer[] codecOutputBuffers;
// Frames from the native layer.
private LinkedList<Frame> frameQueue;
// Indexes to MediaCodec buffers
private LinkedList<Integer> availableInputBufferIndices;
private LinkedList<Integer> availableOutputBufferIndices;
private LinkedList<MediaCodec.BufferInfo> availableOutputBufferInfos;
// Offset between system time and media time.
private long deltaTimeUs;
public MediaCodecVideoDecoder(Context context) {
TAG = context.getString(R.string.tag);
this.context = context;
surfaceView = new SurfaceView(context);
frameQueue = new LinkedList<Frame>();
availableInputBufferIndices = new LinkedList<Integer>();
availableOutputBufferIndices = new LinkedList<Integer>();
availableOutputBufferInfos = new LinkedList<MediaCodec.BufferInfo>();
}
public void dispose() {
codec.stop();
codec.release();
}
// Return view that is written to by MediaCodec.
public SurfaceView getView() { return surfaceView; }
// Entry point from the native layer. Called when the class should be ready
// to start receiving raw frames.
private boolean start(int width, int height) {
deltaTimeUs = -1;
if (!setCodecState(width, height, CodecName.ON2_VP8)) {
return false;
}
startLooperThread();
// The decoding must happen on |looperThread| thread.
handler.sendMessage(handler.obtainMessage(DECODE));
return true;
}
private boolean setCodecState(int width, int height, CodecName codecName) {
// TODO(henrike): enable more than ON2_VP8 codec.
format = new MediaFormat();
format.setInteger(MediaFormat.KEY_WIDTH, width);
format.setInteger(MediaFormat.KEY_HEIGHT, height);
try {
switch (codecName) {
case ON2_VP8:
format.setString(MediaFormat.KEY_MIME, "video/x-vnd.on2.vp8");
codec = MediaCodec.createDecoderByType("video/x-vnd.on2.vp8");
break;
case GOOGLE_VPX:
// SW VP8 decoder
codec = MediaCodec.createByCodecName("OMX.google.vpx.decoder");
break;
case EXYNOX_VP8:
// Nexus10 HW VP8 decoder
codec = MediaCodec.createByCodecName("OMX.Exynos.VP8.Decoder");
break;
default:
return false;
}
} catch (Exception e) {
// TODO(dwkang): replace this instanceof/throw with a narrower catch
// clause once the SDK advances.
if (e instanceof IOException) {
Log.e(TAG, "Failed to create MediaCodec for VP8.", e);
return false;
}
throw new RuntimeException(e);
}
Surface surface = surfaceView.getHolder().getSurface();
MediaCrypto crypto = null; // No crypto.
int flags = 0; // Decoder (1 for encoder)
codec.configure(format, surface, crypto, flags);
codec.start();
codecInputBuffers = codec.getInputBuffers();
codecOutputBuffers = codec.getOutputBuffers();
return true;
}
private void startLooperThread() {
looperThread = new Thread() {
@Override
public void run() {
Looper.prepare();
// Handler that is run by this thread.
handler = new DecodeHandler();
// Notify that the thread has created a handler.
synchronized(MediaCodecVideoDecoder.this) {
MediaCodecVideoDecoder.this.notify();
}
Looper.loop();
}
};
looperThread.start();
// Wait for thread to notify that Handler has been set up.
synchronized(this) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// Entry point from the native layer. It pushes the raw buffer to this class.
private void pushBuffer(ByteBuffer buffer, long renderTimeMs) {
// TODO(dwkang): figure out why exceptions just make this thread finish.
try {
final long renderTimeUs = renderTimeMs * 1000;
synchronized(frameQueue) {
frameQueue.add(new Frame(buffer, renderTimeUs));
}
} catch (Exception e) {
e.printStackTrace();
}
}
private boolean hasFrame() {
synchronized(frameQueue) {
return !frameQueue.isEmpty();
}
}
private Frame dequeueFrame() {
synchronized(frameQueue) {
return frameQueue.removeFirst();
}
}
private void flush() {
availableInputBufferIndices.clear();
availableOutputBufferIndices.clear();
availableOutputBufferInfos.clear();
codec.flush();
}
// Media time is relative to previous frame.
private long mediaTimeToSystemTime(long mediaTimeUs) {
if (deltaTimeUs == -1) {
long nowUs = System.currentTimeMillis() * 1000;
deltaTimeUs = nowUs - mediaTimeUs;
}
return deltaTimeUs + mediaTimeUs;
}
private void decodePendingBuffers() {
int timeoutUs = 0; // Don't block on dequeuing input buffer.
int index = codec.dequeueInputBuffer(timeoutUs);
if (index != MediaCodec.INFO_TRY_AGAIN_LATER) {
availableInputBufferIndices.add(index);
}
while (feedInputBuffer()) {}
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
index = codec.dequeueOutputBuffer(info, timeoutUs);
if (index > 0) {
availableOutputBufferIndices.add(index);
availableOutputBufferInfos.add(info);
}
if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
codecOutputBuffers = codec.getOutputBuffers();
}
while (drainOutputBuffer()) {}
}
// Returns true if MediaCodec is ready for more data and there was data
// available from the native layer.
private boolean feedInputBuffer() {
if (availableInputBufferIndices.isEmpty()) {
return false;
}
if (!hasFrame()) {
return false;
}
Frame frame = dequeueFrame();
ByteBuffer buffer = frame.buffer;
int index = availableInputBufferIndices.pollFirst();
ByteBuffer codecData = codecInputBuffers[index];
check(codecData.capacity() >= buffer.capacity(),
"Buffer is too small to copy a frame.");
buffer.rewind();
codecData.rewind();
codecData.put(buffer);
try {
int offset = 0;
int flags = 0;
codec.queueInputBuffer(index, offset, buffer.capacity(),
frame.timestampUs, flags);
} catch (MediaCodec.CryptoException e) {
check(false, "CryptoException w/ errorCode " + e.getErrorCode() +
", '" + e.getMessage() + "'");
}
return true;
}
// Returns true if more output data could be drained.MediaCodec has more data
// to deliver.
private boolean drainOutputBuffer() {
if (availableOutputBufferIndices.isEmpty()) {
return false;
}
int index = availableOutputBufferIndices.peekFirst();
MediaCodec.BufferInfo info = availableOutputBufferInfos.peekFirst();
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
// End of stream is unexpected with streamed video.
check(false, "Saw output end of stream.");
return false;
}
long realTimeUs = mediaTimeToSystemTime(info.presentationTimeUs);
long nowUs = System.currentTimeMillis() * 1000;
long lateUs = nowUs - realTimeUs;
if (lateUs < -10000) {
// Frame should not be presented yet.
return false;
}
// TODO(dwkang): For some extreme cases, just not doing rendering is not
// enough. Need to seek to the next key frame.
boolean render = lateUs <= 30000;
if (!render) {
Log.d(TAG, "video late by " + lateUs + " us. Skipping...");
}
// Decode and render to surface if desired.
codec.releaseOutputBuffer(index, render);
availableOutputBufferIndices.removeFirst();
availableOutputBufferInfos.removeFirst();
return true;
}
}