| /* |
| * 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); |
| } |
| } |