blob: 715c7761361401ca938b23a4ff8198d276ad949e [file] [log] [blame]
/*
* Copyright (C) 2023 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 android.videocodec.cts;
import static android.media.MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface;
import static android.media.MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR;
import static android.media.MediaFormat.KEY_ALLOW_FRAME_DROP;
import static android.mediav2.common.cts.CodecEncoderTestBase.getMuxerFormatForMediaType;
import static android.mediav2.common.cts.CodecEncoderTestBase.getTempFilePath;
import static android.mediav2.common.cts.CodecEncoderTestBase.muxOutput;
import static android.mediav2.common.cts.CodecTestBase.ComponentClass.HARDWARE;
import static android.mediav2.common.cts.CodecTestBase.Q_DEQ_TIMEOUT_US;
import static android.mediav2.common.cts.VideoErrorManager.computeFrameVariance;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;
import android.media.MediaCodec;
import android.media.MediaExtractor;
import android.media.MediaFormat;
import android.mediav2.common.cts.CodecTestBase;
import android.mediav2.common.cts.CompareStreams;
import android.mediav2.common.cts.DecodeStreamToYuv;
import android.mediav2.common.cts.EncoderConfigParams;
import android.mediav2.common.cts.InputSurface;
import android.mediav2.common.cts.OutputSurface;
import android.mediav2.common.cts.RawResource;
import android.util.Log;
import android.util.Pair;
import androidx.annotation.NonNull;
import com.android.compatibility.common.util.ApiTest;
import com.android.compatibility.common.util.Preconditions;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
/**
* This test is similar to {@link android.media.codec.cts.DecodeEditEncodeTest}, except for the
* edit part. The DecodeEditEncodeTest does swapping of color planes during editing, this test
* performs smoothening and sharpening. Besides this every thing is almost same.
* This test additionally validates the output of encoder. As smoothening and sharpening filters
* are applied on input, the test compares block level (32x32) between smoothened clip and
* sharpened clip and checks if they are as expected.
*/
@RunWith(Parameterized.class)
public class VideoDecodeEditEncodeTest {
private static final String LOG_TAG = VideoDecodeEditEncodeTest.class.getSimpleName();
private static final boolean WORK_AROUND_BUGS = false; // avoid fatal codec bugs
private static final boolean VERBOSE = false;
private static final CodecTestBase.ComponentClass SELECT_SWITCH = HARDWARE;
private static final String MEDIA_DIR = WorkDir.getMediaDirString();
private static final String RES_CLIP =
MEDIA_DIR + "AVICON-MOBILE-BirthdayHalfway-SI17-CRUW03-L-420-8bit-SDR-1080p-30fps.mp4";
private static final String RES_MEDIA_TYPE = MediaFormat.MIMETYPE_VIDEO_AVC;
private static final int TARGET_WIDTH = 1280;
private static final int TARGET_HEIGHT = 720;
private static final int TARGET_BITRATE = 10000000;
private static final float AVG_ACCEPTABLE_QUALITY = 25.0f; // dB
private static final String[] KERNELS = {
// 5x5 Gaussian smoothing filter
"float kernel[KERNEL_SIZE];\n"
+ "kernel[0] = 1.0 / 273.0;\n"
+ "kernel[1] = 4.0 / 273.0;\n"
+ "kernel[2] = 7.0 / 273.0;\n"
+ "kernel[3] = 4.0 / 273.0;\n"
+ "kernel[4] = 1.0 / 273.0;\n"
+ "kernel[5] = 4.0 / 273.0;\n"
+ "kernel[6] = 16.0 / 273.0;\n"
+ "kernel[7] = 26.0 / 273.0;\n"
+ "kernel[8] = 16.0 / 273.0;\n"
+ "kernel[9] = 4.0 / 273.0;\n"
+ "kernel[10] = 7.0 / 273.0;\n"
+ "kernel[11] = 26.0 / 273.0;\n"
+ "kernel[12] = 41.0 / 273.0;\n"
+ "kernel[13] = 26.0 / 273.0;\n"
+ "kernel[14] = 7.0 / 273.0;\n"
+ "kernel[15] = 4.0 / 273.0;\n"
+ "kernel[16] = 16.0 / 273.0;\n"
+ "kernel[17] = 26.0 / 273.0;\n"
+ "kernel[18] = 16.0 / 273.0;\n"
+ "kernel[19] = 4.0 / 273.0;\n"
+ "kernel[20] = 1.0 / 273.0;\n"
+ "kernel[21] = 4.0 / 273.0;\n"
+ "kernel[22] = 7.0 / 273.0;\n"
+ "kernel[23] = 4.0 / 273.0;\n"
+ "kernel[24] = 1.0 / 273.0;\n",
// 5x5 Sharpening filter
// Sharpening Kernel = 2 * Identity matrix - Gaussian smoothing kernel
"float kernel[KERNEL_SIZE];\n"
+ "kernel[0] = -1.0 / 273.0;\n"
+ "kernel[1] = -4.0 / 273.0;\n"
+ "kernel[2] = -7.0 / 273.0;\n"
+ "kernel[3] = -4.0 / 273.0;\n"
+ "kernel[4] = -1.0 / 273.0;\n"
+ "kernel[5] = -4.0 / 273.0;\n"
+ "kernel[6] = -16.0 / 273.0;\n"
+ "kernel[7] = -26.0 / 273.0;\n"
+ "kernel[8] = -16.0 / 273.0;\n"
+ "kernel[9] = -4.0 / 273.0;\n"
+ "kernel[10] = -7.0 / 273.0;\n"
+ "kernel[11] = -26.0 / 273.0;\n"
+ "kernel[12] = 505.0 / 273.0;\n"
+ "kernel[13] = -26.0 / 273.0;\n"
+ "kernel[14] = -7.0 / 273.0;\n"
+ "kernel[15] = -4.0 / 273.0;\n"
+ "kernel[16] = -16.0 / 273.0;\n"
+ "kernel[17] = -26.0 / 273.0;\n"
+ "kernel[18] = -16.0 / 273.0;\n"
+ "kernel[19] = -4.0 / 273.0;\n"
+ "kernel[20] = -1.0 / 273.0;\n"
+ "kernel[21] = -4.0 / 273.0;\n"
+ "kernel[22] = -7.0 / 273.0;\n"
+ "kernel[23] = -4.0 / 273.0;\n"
+ "kernel[24] = -1.0 / 273.0;\n"
};
private final String mEncoderName;
private final String mMediaType;
private final ArrayList<String> mTmpFiles = new ArrayList<>();
public VideoDecodeEditEncodeTest(String encoderName, String mediaType,
@SuppressWarnings("unused") String allTestParams) {
mEncoderName = encoderName;
mMediaType = mediaType;
}
@Parameterized.Parameters(name = "{index}_{0}_{1}")
public static Collection<Object[]> input() {
final boolean isEncoder = true;
final boolean needAudio = false;
final boolean needVideo = true;
// mediaType
final List<Object[]> exhaustiveArgsList = Arrays.asList(new Object[][]{
{MediaFormat.MIMETYPE_VIDEO_AVC},
{MediaFormat.MIMETYPE_VIDEO_HEVC},
});
return CodecTestBase.prepareParamList(exhaustiveArgsList, isEncoder, needAudio, needVideo,
false, SELECT_SWITCH);
}
@After
public void tearDown() {
for (String tmpFile : mTmpFiles) {
File tmp = new File(tmpFile);
if (tmp.exists()) assertTrue("unable to delete file " + tmpFile, tmp.delete());
}
mTmpFiles.clear();
}
private static String getShader(String width, String height, String kernel) {
return "#extension GL_OES_EGL_image_external : require\n"
+ "#define KERNEL_SIZE 25\n"
+ "precision mediump float;\n"
+ "varying vec2 vTextureCoord;\n"
+ "uniform samplerExternalOES sTexture;\n"
+ width
+ height
+ "const float step_w = 1.0/width;\n"
+ "const float step_h = 1.0/height;\n"
+ "void main() {\n"
+ kernel
+ " vec2 offset[KERNEL_SIZE];"
+ " offset[0] = vec2(-2.0 * step_w, -2.0 * step_h);\n" // [-2, -2] // row -2
+ " offset[1] = vec2(-step_w, -2.0 * step_h);\n" // [-1, -2]
+ " offset[2] = vec2(0.0, -2.0 * step_h);\n" // [0, -2]
+ " offset[3] = vec2(step_w, -2.0 * step_h);\n" // [1, -2]
+ " offset[4] = vec2(2.0 * step_w, -2.0 * step_h);\n" // [2, -2]
+ " offset[5] = vec2(-2.0 * step_w, -step_h);\n" // [-2, -1] // row -1
+ " offset[6] = vec2(-step_w, -step_h);\n" // [-1, -1]
+ " offset[7] = vec2(0.0, -step_h);\n" // [0, -1]
+ " offset[8] = vec2(step_w, -step_h);\n" // [1, -1]
+ " offset[9] = vec2(2.0 * step_w, -step_h);\n" // [2, -1]
+ " offset[10] = vec2(-2.0 * step_w, 0.0);\n" // [-2, 0] // curr row
+ " offset[11] = vec2(-step_w, 0.0);\n" // [-1, 0]
+ " offset[12] = vec2(0.0, 0.0);\n" // [0, 0]
+ " offset[13] = vec2(step_w, 0.0);\n" // [1, 0]
+ " offset[14] = vec2(2.0 * step_w, 0.0);\n" // [2, 0]
+ " offset[15] = vec2(-2.0 * step_w, step_h);\n" // [-2, 1] // row +1
+ " offset[16] = vec2(-step_w, step_h);\n" // [-1, 1]
+ " offset[17] = vec2(0.0, step_h);\n" // [0, 1]
+ " offset[18] = vec2(step_w, step_h);\n" // [1, 1]
+ " offset[19] = vec2(2.0 * step_w, step_h);\n" // [2, 1]
+ " offset[20] = vec2(-2.0 * step_w, 2.0 * step_h);\n" // [-2, 2] // row +2
+ " offset[21] = vec2(-step_w, 2.0 * step_h);\n" // [-1, 2]
+ " offset[22] = vec2(0.0, 2.0 * step_h);\n" // [-0, 2]
+ " offset[23] = vec2(step_w, 2.0 * step_h);\n" // [1, 2]
+ " offset[24] = vec2(2.0 * step_w, 2.0 * step_h);\n" // [2, 2]
+ " vec4 sum = vec4(0.0);\n"
+ " vec4 sample;\n"
+ " for (int i=0; i<KERNEL_SIZE; i++) {\n"
+ " sample = texture2D(sTexture, vTextureCoord + offset[i]).rgba;\n"
+ " sum = sum + sample * kernel[i];\n"
+ " }\n"
+ " gl_FragColor = sum;\n"
+ "}\n";
}
/**
* The elementary stream coming out of the encoder needs to be fed back into
* the decoder one chunk at a time. If we just wrote the data to a file, we would lose
* the information about chunk boundaries. This class stores the encoded data in memory,
* retaining the chunk organization.
*/
private static class VideoChunks {
private MediaFormat mMediaFormat;
private byte[] mMemory = new byte[1024];
private int mMemIndex = 0;
private final ArrayList<MediaCodec.BufferInfo> mBufferInfo = new ArrayList<>();
private void splitMediaToMuxerParameters(@NonNull String srcPath, @NonNull String mediaType,
int frameLimit) throws IOException {
// Set up MediaExtractor to read from the source.
MediaExtractor extractor = new MediaExtractor();
Preconditions.assertTestFileExists(srcPath);
extractor.setDataSource(srcPath);
// Set up MediaFormat
for (int trackID = 0; trackID < extractor.getTrackCount(); trackID++) {
extractor.selectTrack(trackID);
MediaFormat format = extractor.getTrackFormat(trackID);
if (mediaType.equals(format.getString(MediaFormat.KEY_MIME))) {
mMediaFormat = format;
break;
} else {
extractor.unselectTrack(trackID);
}
}
if (null == mMediaFormat) {
extractor.release();
throw new IllegalArgumentException(
"could not find usable track in file " + srcPath);
}
// Set up location for elementary stream
File file = new File(srcPath);
int bufferSize = (int) file.length();
bufferSize = ((bufferSize + 127) >> 7) << 7;
// Ideally, Sum of return values of extractor.readSampleData(...) should not exceed
// source file size. But in case of Vorbis, aosp extractor appends an additional 4
// bytes to the data at every readSampleData() call. bufferSize <<= 1 empirically
// large enough to hold the excess 4 bytes per read call
bufferSize <<= 1;
mMemory = new byte[bufferSize];
ByteBuffer byteBuffer = ByteBuffer.allocate(bufferSize);
mMemIndex = 0;
mBufferInfo.clear();
// Let MediaExtractor do its thing
boolean sawEOS = false;
int offset = 0;
int frameCount = 0;
while (!sawEOS && frameCount < frameLimit) {
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
bufferInfo.offset = offset;
bufferInfo.size = extractor.readSampleData(byteBuffer, offset);
if (bufferInfo.size < 0) {
sawEOS = true;
} else {
bufferInfo.presentationTimeUs = extractor.getSampleTime();
int flags = extractor.getSampleFlags();
bufferInfo.flags = 0;
if ((flags & MediaExtractor.SAMPLE_FLAG_SYNC) != 0) {
bufferInfo.flags |= MediaCodec.BUFFER_FLAG_KEY_FRAME;
}
mBufferInfo.add(bufferInfo);
extractor.advance();
}
offset += bufferInfo.size;
frameCount++;
}
byteBuffer.rewind();
byteBuffer.get(mMemory, 0, byteBuffer.limit());
mMemIndex = byteBuffer.limit();
extractor.release();
}
public void addChunkData(ByteBuffer buf, MediaCodec.BufferInfo info) {
MediaCodec.BufferInfo copy = new MediaCodec.BufferInfo();
copy.set(mMemIndex, info.size, info.presentationTimeUs, info.flags);
mBufferInfo.add(copy);
if (mMemIndex + info.size >= mMemory.length) {
mMemory = Arrays.copyOf(mMemory, mMemIndex + info.size);
}
buf.position(info.offset);
buf.get(mMemory, mMemIndex, info.size);
mMemIndex += info.size;
}
/**
* Sets the MediaFormat, for the benefit of a future decoder.
*/
public void setMediaFormat(MediaFormat format) {
mMediaFormat = format;
}
/**
* Gets the MediaFormat that was used by the encoder.
*/
public MediaFormat getMediaFormat() {
return new MediaFormat(mMediaFormat);
}
/**
* Returns the number of chunks currently held.
*/
public int getNumChunks() {
return mBufferInfo.size();
}
/**
* Copies the data from chunk N into "dest". Advances dest.position.
*/
public void getChunkData(int chunk, ByteBuffer dest) {
int offset = mBufferInfo.get(chunk).offset;
int size = mBufferInfo.get(chunk).size;
dest.put(mMemory, offset, size);
}
/**
* Returns the flags associated with chunk N.
*/
public int getChunkFlags(int chunk) {
return mBufferInfo.get(chunk).flags;
}
/**
* Returns the timestamp associated with chunk N.
*/
public long getChunkTime(int chunk) {
return mBufferInfo.get(chunk).presentationTimeUs;
}
public ArrayList<MediaCodec.BufferInfo> getChunkInfos() {
return mBufferInfo;
}
public ByteBuffer getBuffer() {
return ByteBuffer.wrap(mMemory);
}
public void dumpBuffer() throws IOException {
File dump = File.createTempFile(LOG_TAG + "OUT", ".bin");
Log.d(LOG_TAG, "dump file name is " + dump.getAbsolutePath());
try (FileOutputStream outputStream = new FileOutputStream(dump)) {
outputStream.write(mMemory, 0, mMemIndex);
}
}
}
/**
* Edits a video file, saving the contents to a new file. This involves decoding and
* re-encoding, not to mention conversions between YUV and RGB, and so may be lossy.
* <p>
* If we recognize the decoded format we can do this in Java code using the ByteBuffer[]
* output, but it's not practical to support all OEM formats. By using a SurfaceTexture
* for output and a Surface for input, we can avoid issues with obscure formats and can
* use a fragment shader to do transformations.
*/
private VideoChunks editVideoFile(VideoChunks inputData, String kernel) throws IOException {
VideoChunks outputData = new VideoChunks();
MediaCodec decoder = null;
MediaCodec encoder = null;
InputSurface inputSurface = null;
OutputSurface outputSurface = null;
try {
// find decoder for the test clip
MediaFormat decoderFormat = inputData.getMediaFormat();
decoderFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT, COLOR_FormatSurface);
ArrayList<MediaFormat> formats = new ArrayList<>();
formats.add(decoderFormat);
ArrayList<String> decoders = CodecTestBase.selectCodecs(RES_MEDIA_TYPE, formats, null,
false, SELECT_SWITCH);
assumeTrue("Could not find decoder for format : " + decoderFormat, decoders.size() > 0);
String decoderName = decoders.get(0);
// build encoder format and check if it is supported by the current component
EncoderConfigParams.Builder foreman =
new EncoderConfigParams.Builder(mMediaType)
.setWidth(TARGET_WIDTH)
.setHeight(TARGET_HEIGHT)
.setColorFormat(COLOR_FormatSurface)
.setInputBitDepth(8)
.setFrameRate(30)
.setBitRate(TARGET_BITRATE)
.setBitRateMode(BITRATE_MODE_VBR);
MediaFormat encoderFormat = foreman.build().getFormat();
formats.clear();
formats.add(encoderFormat);
assumeTrue("Encoder: " + mEncoderName + " doesn't support format: " + encoderFormat,
CodecTestBase.areFormatsSupported(mEncoderName, mMediaType, formats));
// configure
encoder = MediaCodec.createByCodecName(mEncoderName);
encoder.configure(encoderFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
inputSurface = new InputSurface(encoder.createInputSurface(), false);
if (inputSurface.getWidth() != TARGET_WIDTH
|| inputSurface.getHeight() != TARGET_HEIGHT) {
inputSurface.updateSize(TARGET_WIDTH, TARGET_HEIGHT);
}
inputSurface.makeCurrent();
encoder.start();
// OutputSurface uses the EGL context created by InputSurface.
decoder = MediaCodec.createByCodecName(decoderName);
outputSurface = new OutputSurface();
outputSurface.changeFragmentShader(getShader(
"const float width = " + (float) TARGET_WIDTH + ";\n",
"const float height = " + (float) TARGET_HEIGHT + ";\n", kernel));
// do not allow frame drops
decoderFormat.setInteger(KEY_ALLOW_FRAME_DROP, 0);
decoder.configure(decoderFormat, outputSurface.getSurface(), null, 0);
decoder.start();
// verify that we are not dropping frames
MediaFormat format = decoder.getInputFormat();
assertEquals("Could not prevent frame dropping", 0,
format.getInteger(KEY_ALLOW_FRAME_DROP));
editVideoData(inputData, decoder, outputSurface, inputSurface, encoder, outputData);
} finally {
if (VERBOSE) {
Log.d(LOG_TAG, "shutting down encoder, decoder");
}
if (outputSurface != null) {
outputSurface.release();
}
if (inputSurface != null) {
inputSurface.release();
}
if (encoder != null) {
encoder.stop();
encoder.release();
}
if (decoder != null) {
decoder.stop();
decoder.release();
}
}
return outputData;
}
/**
* Edits a stream of video data.
*/
private void editVideoData(VideoChunks inputData, MediaCodec decoder,
OutputSurface outputSurface, InputSurface inputSurface, MediaCodec encoder,
VideoChunks outputData) {
ByteBuffer[] decoderInputBuffers = decoder.getInputBuffers();
ByteBuffer[] encoderOutputBuffers = encoder.getOutputBuffers();
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
int inputChunk = 0;
int outputCount = 0;
boolean outputDone = false;
boolean inputDone = false;
boolean decoderDone = false;
while (!outputDone) {
if (VERBOSE) {
Log.d(LOG_TAG, "edit loop");
}
// Feed more data to the decoder.
if (!inputDone) {
int inputBufIndex = decoder.dequeueInputBuffer(Q_DEQ_TIMEOUT_US);
if (inputBufIndex >= 0) {
if (inputChunk == inputData.getNumChunks()) {
// End of stream -- send empty frame with EOS flag set.
decoder.queueInputBuffer(inputBufIndex, 0, 0, 0L,
MediaCodec.BUFFER_FLAG_END_OF_STREAM);
inputDone = true;
if (VERBOSE) {
Log.d(LOG_TAG, "sent input EOS (with zero-length frame)");
}
} else {
// Copy a chunk of input to the decoder. The first chunk should have
// the BUFFER_FLAG_CODEC_CONFIG flag set.
ByteBuffer inputBuf = decoderInputBuffers[inputBufIndex];
inputBuf.clear();
inputData.getChunkData(inputChunk, inputBuf);
int flags = inputData.getChunkFlags(inputChunk);
long time = inputData.getChunkTime(inputChunk);
decoder.queueInputBuffer(inputBufIndex, 0, inputBuf.position(),
time, flags);
if (VERBOSE) {
Log.d(LOG_TAG, "submitted frame " + inputChunk + " to dec, size="
+ inputBuf.position() + " flags=" + flags);
}
inputChunk++;
}
} else {
if (VERBOSE) {
Log.d(LOG_TAG, "input buffer not available");
}
}
}
// Assume output is available. Loop until both assumptions are false.
boolean decoderOutputAvailable = !decoderDone;
boolean encoderOutputAvailable = true;
while (decoderOutputAvailable || encoderOutputAvailable) {
// Start by draining any pending output from the encoder. It's important to
// do this before we try to stuff any more data in.
int encoderStatus = encoder.dequeueOutputBuffer(info, Q_DEQ_TIMEOUT_US);
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet
if (VERBOSE) {
Log.d(LOG_TAG, "no output from encoder available");
}
encoderOutputAvailable = false;
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
encoderOutputBuffers = encoder.getOutputBuffers();
if (VERBOSE) {
Log.d(LOG_TAG, "encoder output buffers changed");
}
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
MediaFormat newFormat = encoder.getOutputFormat();
outputData.setMediaFormat(newFormat);
if (VERBOSE) {
Log.d(LOG_TAG, "encoder output format changed: " + newFormat);
}
} else if (encoderStatus < 0) {
fail("unexpected result from encoder.dequeueOutputBuffer: " + encoderStatus);
} else { // encoderStatus >= 0
ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
if (encodedData == null) {
fail("encoderOutputBuffer " + encoderStatus + " was null");
}
// Write the data to the output "file".
if (info.size != 0) {
encodedData.position(info.offset);
encodedData.limit(info.offset + info.size);
outputData.addChunkData(encodedData, info);
if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) outputCount++;
if (VERBOSE) {
Log.d(LOG_TAG, "encoder output " + info.size + " bytes");
}
}
outputDone = (info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
encoder.releaseOutputBuffer(encoderStatus, false);
}
if (encoderStatus != MediaCodec.INFO_TRY_AGAIN_LATER) {
// Continue attempts to drain output.
continue;
}
// Encoder is drained, check to see if we've got a new frame of output from
// the decoder. (The output is going to a Surface, rather than a ByteBuffer,
// but we still get information through BufferInfo.)
if (!decoderDone) {
int decoderStatus = decoder.dequeueOutputBuffer(info, Q_DEQ_TIMEOUT_US);
if (decoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
// no output available yet
if (VERBOSE) {
Log.d(LOG_TAG, "no output from decoder available");
}
decoderOutputAvailable = false;
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
//decoderOutputBuffers = decoder.getOutputBuffers();
if (VERBOSE) {
Log.d(LOG_TAG, "decoder output buffers changed (we don't care)");
}
} else if (decoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
// expected before first buffer of data
MediaFormat newFormat = decoder.getOutputFormat();
if (VERBOSE) {
Log.d(LOG_TAG, "decoder output format changed: " + newFormat);
}
} else if (decoderStatus < 0) {
fail("unexpected result from decoder.dequeueOutputBuffer: "
+ decoderStatus);
} else { // decoderStatus >= 0
if (VERBOSE) {
Log.d(LOG_TAG, "surface decoder given buffer " + decoderStatus
+ " (size=" + info.size + ")");
}
// The ByteBuffers are null references, but we still get a nonzero
// size for the decoded data.
boolean doRender = (info.size != 0);
// As soon as we call releaseOutputBuffer, the buffer will be forwarded
// to SurfaceTexture to convert to a texture. The API doesn't
// guarantee that the texture will be available before the call
// returns, so we need to wait for the onFrameAvailable callback to
// fire. If we don't wait, we risk rendering from the previous frame.
decoder.releaseOutputBuffer(decoderStatus, doRender);
if (doRender) {
// This waits for the image and renders it after it arrives.
if (VERBOSE) {
Log.d(LOG_TAG, "awaiting frame");
}
outputSurface.awaitNewImage();
outputSurface.drawImage();
// Send it to the encoder.
inputSurface.setPresentationTime(info.presentationTimeUs * 1000);
if (VERBOSE) {
Log.d(LOG_TAG, "swapBuffers");
}
inputSurface.swapBuffers();
}
if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
// forward decoder EOS to encoder
if (VERBOSE) {
Log.d(LOG_TAG, "signaling input EOS");
}
if (WORK_AROUND_BUGS) {
// Bail early, possibly dropping a frame.
return;
} else {
encoder.signalEndOfInputStream();
}
}
}
}
}
}
if (inputChunk != outputCount) {
throw new RuntimeException("frame lost: " + inputChunk + " in, " + outputCount
+ " out");
}
}
private double computeVariance(RawResource yuv) throws IOException {
Preconditions.assertTestFileExists(yuv.mFileName);
assertEquals("has support for 8 bit clips only", 1, yuv.mBytesPerSample);
double variance = 0;
int blocks = 0;
try (RandomAccessFile refStream = new RandomAccessFile(new File(yuv.mFileName), "r")) {
int ySize = yuv.mWidth * yuv.mHeight;
int uvSize = ySize >> 1;
byte[] luma = new byte[ySize];
while (true) {
int bytesReadRef = refStream.read(luma);
if (bytesReadRef == -1) break;
assertEquals("bad, reading unaligned frame size", bytesReadRef, ySize);
refStream.skipBytes(uvSize);
Pair<Double, Integer> var = computeFrameVariance(yuv.mWidth, yuv.mHeight, luma);
variance += var.first;
blocks += var.second;
}
return variance / blocks;
}
}
/**
* Extract, Decode, Edit, Encode and Validate. Check description of class
* {@link VideoDecodeEditEncodeTest}
*/
@ApiTest(apis = {"android.opengl.GLES20#GL_FRAGMENT_SHADER",
"android.media.format.MediaFormat#KEY_ALLOW_FRAME_DROP",
"MediaCodecInfo.CodecCapabilities#COLOR_FormatSurface"})
@Test
public void testVideoEdit() throws IOException, InterruptedException {
VideoChunks sourceChunks = new VideoChunks();
sourceChunks.splitMediaToMuxerParameters(RES_CLIP, RES_MEDIA_TYPE, 90);
VideoChunks[] outputData = new VideoChunks[2];
for (int i = 0; i < 2; i++) {
outputData[i] = editVideoFile(sourceChunks, KERNELS[i]);
}
String tmpPathA = getTempFilePath("");
mTmpFiles.add(tmpPathA);
String tmpPathB = getTempFilePath("");
mTmpFiles.add(tmpPathB);
int muxerFormat = getMuxerFormatForMediaType(mMediaType);
muxOutput(tmpPathA, muxerFormat, outputData[0].getMediaFormat(), outputData[0].getBuffer(),
outputData[0].getChunkInfos());
muxOutput(tmpPathB, muxerFormat, outputData[1].getMediaFormat(), outputData[1].getBuffer(),
outputData[1].getChunkInfos());
CompareStreams cs = null;
try {
cs = new CompareStreams(mMediaType, tmpPathA, mMediaType, tmpPathB, false, false);
double[] avgPSNR = cs.getAvgPSNR();
final double weightedAvgPSNR = (4 * avgPSNR[0] + avgPSNR[1] + avgPSNR[2]) / 6;
if (weightedAvgPSNR < AVG_ACCEPTABLE_QUALITY) {
fail(String.format("Average PSNR of the sequence: %f is < threshold : %f\n",
weightedAvgPSNR, AVG_ACCEPTABLE_QUALITY));
}
} finally {
if (cs != null) cs.cleanUp();
}
DecodeStreamToYuv yuvRes = new DecodeStreamToYuv(mMediaType, tmpPathA, Integer.MAX_VALUE,
LOG_TAG);
RawResource yuv = yuvRes.getDecodedYuv();
mTmpFiles.add(yuv.mFileName);
double varA = computeVariance(yuv);
yuvRes = new DecodeStreamToYuv(mMediaType, tmpPathB, Integer.MAX_VALUE, LOG_TAG);
yuv = yuvRes.getDecodedYuv();
mTmpFiles.add(yuv.mFileName);
double varB = computeVariance(yuv);
Log.d(LOG_TAG, "variance is " + varA + " " + varB);
assertTrue(String.format("Blurred clip variance is not less than sharpened clip. Variance"
+ " of blurred clip is %f, variance of sharpened clip is %f", varA, varB),
varA < varB);
}
}