blob: e2fe17734f8f623c948d8e5564d1368413463692 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.media.samplevideoencoder;
import android.content.Context;
import android.content.res.AssetFileDescriptor;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.os.Build;
import android.util.Log;
import android.util.Pair;
import android.view.Surface;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import static com.android.media.samplevideoencoder.MainActivity.FRAME_TYPE_B;
import static com.android.media.samplevideoencoder.MainActivity.FRAME_TYPE_I;
import static com.android.media.samplevideoencoder.MainActivity.FRAME_TYPE_P;
public class MediaCodecSurfaceEncoder {
private static final String TAG = MediaCodecSurfaceEncoder.class.getSimpleName();
private static final boolean DEBUG = false;
private static final int VIDEO_BITRATE = 8000000 /*8 Mbps*/;
private static final int VIDEO_FRAMERATE = 30;
private final Context mActivityContext;
private final int mResID;
private final int mMaxBFrames;
private final String mMime;
private final String mOutputPath;
private int mTrackID = -1;
private int mFrameNum = 0;
private int[] mFrameTypeOccurrences = {0, 0, 0};
private Surface mSurface;
private MediaExtractor mExtractor;
private MediaCodec mDecoder;
private MediaCodec mEncoder;
private MediaMuxer mMuxer;
private final boolean mIsCodecSoftware;
private boolean mSawDecInputEOS;
private boolean mSawDecOutputEOS;
private boolean mSawEncOutputEOS;
private int mDecOutputCount;
private int mEncOutputCount;
private final CodecAsyncHandler mAsyncHandleEncoder = new CodecAsyncHandler();
private final CodecAsyncHandler mAsyncHandleDecoder = new CodecAsyncHandler();
public MediaCodecSurfaceEncoder(Context context, int resId, String mime, boolean isSoftware,
String outputPath, int maxBFrames) {
mActivityContext = context;
mResID = resId;
mMime = mime;
mIsCodecSoftware = isSoftware;
mOutputPath = outputPath;
mMaxBFrames = maxBFrames;
}
public MediaCodecSurfaceEncoder(Context context, int resId, String mime, boolean isSoftware,
String outputPath) {
// Default value of MediaFormat.KEY_MAX_B_FRAMES is set to 1, if not passed as a parameter.
this(context, resId, mime, isSoftware, outputPath, 1);
}
public int startEncodingSurface() throws IOException, InterruptedException {
MediaFormat decoderFormat = setUpSource();
if (decoderFormat == null) {
return -1;
}
String decoderMime = decoderFormat.getString(MediaFormat.KEY_MIME);
ArrayList<String> decoders =
MediaCodecBase.selectCodecs(decoderMime, null, null, false, mIsCodecSoftware);
if (decoders.isEmpty()) {
Log.e(TAG, "No suitable decoder found for mime: " + decoderMime);
return -1;
}
mDecoder = MediaCodec.createByCodecName(decoders.get(0));
MediaFormat encoderFormat = setUpEncoderFormat(decoderFormat);
ArrayList<String> listOfEncoders =
MediaCodecBase.selectCodecs(mMime, null, null, true, mIsCodecSoftware);
if (listOfEncoders.isEmpty()) {
Log.e(TAG, "No suitable encoder found for mime: " + mMime);
return -1;
}
boolean muxOutput = true;
for (String encoder : listOfEncoders) {
mEncoder = MediaCodec.createByCodecName(encoder);
mExtractor.seekTo(0, MediaExtractor.SEEK_TO_CLOSEST_SYNC);
if (muxOutput) {
int muxerFormat = MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4;
mMuxer = new MediaMuxer(mOutputPath, muxerFormat);
}
configureCodec(decoderFormat, encoderFormat);
mEncoder.start();
mDecoder.start();
doWork(Integer.MAX_VALUE);
queueEOS();
waitForAllEncoderOutputs();
if (muxOutput) {
if (mTrackID != -1) {
mMuxer.stop();
mTrackID = -1;
}
if (mMuxer != null) {
mMuxer.release();
mMuxer = null;
}
}
mDecoder.reset();
mEncoder.reset();
mSurface.release();
mSurface = null;
Log.i(TAG, "Number of I-frames = " + mFrameTypeOccurrences[FRAME_TYPE_I]);
Log.i(TAG, "Number of P-frames = " + mFrameTypeOccurrences[FRAME_TYPE_P]);
Log.i(TAG, "Number of B-frames = " + mFrameTypeOccurrences[FRAME_TYPE_B]);
}
mEncoder.release();
mDecoder.release();
mExtractor.release();
return 0;
}
private MediaFormat setUpSource() throws IOException {
mExtractor = new MediaExtractor();
AssetFileDescriptor fd = mActivityContext.getResources().openRawResourceFd(mResID);
mExtractor.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength());
for (int trackID = 0; trackID < mExtractor.getTrackCount(); trackID++) {
MediaFormat format = mExtractor.getTrackFormat(trackID);
String mime = format.getString(MediaFormat.KEY_MIME);
if (mime.startsWith("video/")) {
mExtractor.selectTrack(trackID);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible);
return format;
}
}
mExtractor.release();
return null;
}
private MediaFormat setUpEncoderFormat(MediaFormat decoderFormat) {
MediaFormat encoderFormat = new MediaFormat();
encoderFormat.setString(MediaFormat.KEY_MIME, mMime);
encoderFormat
.setInteger(MediaFormat.KEY_WIDTH, decoderFormat.getInteger(MediaFormat.KEY_WIDTH));
encoderFormat.setInteger(MediaFormat.KEY_HEIGHT,
decoderFormat.getInteger(MediaFormat.KEY_HEIGHT));
encoderFormat.setInteger(MediaFormat.KEY_FRAME_RATE, VIDEO_FRAMERATE);
encoderFormat.setInteger(MediaFormat.KEY_BIT_RATE, VIDEO_BITRATE);
encoderFormat.setFloat(MediaFormat.KEY_I_FRAME_INTERVAL, 1.0f);
encoderFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
if (mMime.equals(MediaFormat.MIMETYPE_VIDEO_HEVC)) {
encoderFormat.setInteger(MediaFormat.KEY_PROFILE,
MediaCodecInfo.CodecProfileLevel.HEVCProfileMain);
encoderFormat.setInteger(MediaFormat.KEY_LEVEL,
MediaCodecInfo.CodecProfileLevel.HEVCMainTierLevel4);
} else {
encoderFormat.setInteger(MediaFormat.KEY_PROFILE,
MediaCodecInfo.CodecProfileLevel.AVCProfileMain);
encoderFormat
.setInteger(MediaFormat.KEY_LEVEL, MediaCodecInfo.CodecProfileLevel.AVCLevel4);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
encoderFormat.setInteger(MediaFormat.KEY_MAX_B_FRAMES, mMaxBFrames);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
encoderFormat.setInteger(MediaFormat.KEY_LATENCY, 1);
}
return encoderFormat;
}
private void resetContext() {
mAsyncHandleDecoder.resetContext();
mAsyncHandleEncoder.resetContext();
mSawDecInputEOS = false;
mSawDecOutputEOS = false;
mSawEncOutputEOS = false;
mDecOutputCount = 0;
mEncOutputCount = 0;
mFrameNum = 0;
Arrays.fill(mFrameTypeOccurrences, 0);
}
private void configureCodec(MediaFormat decFormat, MediaFormat encFormat) {
resetContext();
mAsyncHandleEncoder.setCallBack(mEncoder, true);
mEncoder.configure(encFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
mSurface = mEncoder.createInputSurface();
if (!mSurface.isValid()) {
Log.e(TAG, "Surface is not valid");
return;
}
mAsyncHandleDecoder.setCallBack(mDecoder, true);
mDecoder.configure(decFormat, mSurface, null, 0);
Log.d(TAG, "Codec configured");
if (DEBUG) {
Log.d(TAG, "Encoder Output format: " + mEncoder.getOutputFormat());
}
}
private void dequeueDecoderOutput(int bufferIndex, MediaCodec.BufferInfo info) {
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
mSawDecOutputEOS = true;
}
if (DEBUG) {
Log.d(TAG,
"output: id: " + bufferIndex + " flags: " + info.flags + " size: " + info.size +
" timestamp: " + info.presentationTimeUs);
}
if (info.size > 0 && (info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
mDecOutputCount++;
}
mDecoder.releaseOutputBuffer(bufferIndex, mSurface != null);
}
private void enqueueDecoderInput(int bufferIndex) {
ByteBuffer inputBuffer = mDecoder.getInputBuffer(bufferIndex);
int size = mExtractor.readSampleData(inputBuffer, 0);
if (size < 0) {
enqueueDecoderEOS(bufferIndex);
} else {
long pts = mExtractor.getSampleTime();
int extractorFlags = mExtractor.getSampleFlags();
int codecFlags = 0;
if ((extractorFlags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
codecFlags |= MediaCodec.BUFFER_FLAG_KEY_FRAME;
}
if ((extractorFlags & MediaExtractor.SAMPLE_FLAG_PARTIAL_FRAME) != 0) {
codecFlags |= MediaCodec.BUFFER_FLAG_PARTIAL_FRAME;
}
if (!mExtractor.advance()) {
codecFlags |= MediaCodec.BUFFER_FLAG_END_OF_STREAM;
mSawDecInputEOS = true;
}
if (DEBUG) {
Log.d(TAG, "input: id: " + bufferIndex + " size: " + size + " pts: " + pts +
" flags: " + codecFlags);
}
mDecoder.queueInputBuffer(bufferIndex, 0, size, pts, codecFlags);
}
}
private void doWork(int frameLimit) throws InterruptedException {
int frameCount = 0;
while (!hasSeenError() && !mSawDecInputEOS && frameCount < frameLimit) {
Pair<Integer, MediaCodec.BufferInfo> element = mAsyncHandleDecoder.getWork();
if (element != null) {
int bufferID = element.first;
MediaCodec.BufferInfo info = element.second;
if (info != null) {
// <id, info> corresponds to output callback.
dequeueDecoderOutput(bufferID, info);
} else {
// <id, null> corresponds to input callback.
enqueueDecoderInput(bufferID);
frameCount++;
}
}
// check decoder EOS
if (mSawDecOutputEOS) mEncoder.signalEndOfInputStream();
// encoder output
if (mDecOutputCount - mEncOutputCount > mMaxBFrames) {
tryEncoderOutput();
}
}
}
private void queueEOS() throws InterruptedException {
while (!mAsyncHandleDecoder.hasSeenError() && !mSawDecInputEOS) {
Pair<Integer, MediaCodec.BufferInfo> element = mAsyncHandleDecoder.getWork();
if (element != null) {
int bufferID = element.first;
MediaCodec.BufferInfo info = element.second;
if (info != null) {
dequeueDecoderOutput(bufferID, info);
} else {
enqueueDecoderEOS(element.first);
}
}
}
while (!hasSeenError() && !mSawDecOutputEOS) {
Pair<Integer, MediaCodec.BufferInfo> decOp = mAsyncHandleDecoder.getOutput();
if (decOp != null) dequeueDecoderOutput(decOp.first, decOp.second);
if (mSawDecOutputEOS) mEncoder.signalEndOfInputStream();
if (mDecOutputCount - mEncOutputCount > mMaxBFrames) {
tryEncoderOutput();
}
}
}
private void tryEncoderOutput() throws InterruptedException {
if (!hasSeenError() && !mSawEncOutputEOS) {
Pair<Integer, MediaCodec.BufferInfo> element = mAsyncHandleEncoder.getOutput();
if (element != null) {
dequeueEncoderOutput(element.first, element.second);
}
}
}
private void waitForAllEncoderOutputs() throws InterruptedException {
while (!hasSeenError() && !mSawEncOutputEOS) {
tryEncoderOutput();
}
}
private void enqueueDecoderEOS(int bufferIndex) {
if (!mSawDecInputEOS) {
mDecoder.queueInputBuffer(bufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
mSawDecInputEOS = true;
Log.d(TAG, "Queued End of Stream");
}
}
private void dequeueEncoderOutput(int bufferIndex, MediaCodec.BufferInfo info) {
if (DEBUG) {
Log.d(TAG, "encoder output: id: " + bufferIndex + " flags: " + info.flags + " size: " +
info.size + " timestamp: " + info.presentationTimeUs);
}
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
mSawEncOutputEOS = true;
}
if (info.size > 0) {
ByteBuffer buf = mEncoder.getOutputBuffer(bufferIndex);
// Parse the buffer to get the frame type
if (DEBUG) Log.d(TAG, "[ Frame : " + (mFrameNum++) + " ]");
int frameTypeResult = -1;
if (mMime == MediaFormat.MIMETYPE_VIDEO_AVC) {
frameTypeResult = NalUnitUtil.getStandardizedFrameTypesFromAVC(buf);
} else if (mMime == MediaFormat.MIMETYPE_VIDEO_HEVC){
frameTypeResult = NalUnitUtil.getStandardizedFrameTypesFromHEVC(buf);
} else {
Log.e(TAG, "Mime type " + mMime + " is not supported.");
return;
}
if (frameTypeResult != -1) {
mFrameTypeOccurrences[frameTypeResult]++;
}
if (mMuxer != null) {
if (mTrackID == -1) {
mTrackID = mMuxer.addTrack(mEncoder.getOutputFormat());
mMuxer.start();
}
mMuxer.writeSampleData(mTrackID, buf, info);
}
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
mEncOutputCount++;
}
}
mEncoder.releaseOutputBuffer(bufferIndex, false);
}
private boolean hasSeenError() {
return mAsyncHandleDecoder.hasSeenError() || mAsyncHandleEncoder.hasSeenError();
}
public int[] getFrameTypes() {
return mFrameTypeOccurrences;
}
}