| /* |
| * Copyright (C) 2019 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.mediav2.cts; |
| |
| import android.graphics.ImageFormat; |
| import android.media.Image; |
| import android.media.MediaCodec; |
| import android.media.MediaCodecInfo; |
| import android.media.MediaCodecList; |
| import android.media.MediaFormat; |
| import android.os.Build; |
| import android.util.Log; |
| import android.util.Pair; |
| |
| import androidx.annotation.NonNull; |
| |
| import java.io.IOException; |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.LinkedList; |
| import java.util.concurrent.locks.Condition; |
| import java.util.concurrent.locks.Lock; |
| import java.util.concurrent.locks.ReentrantLock; |
| import java.util.zip.CRC32; |
| |
| import static org.junit.Assert.assertTrue; |
| |
| class CodecAsyncHandler extends MediaCodec.Callback { |
| private static final String LOG_TAG = CodecAsyncHandler.class.getSimpleName(); |
| private final Lock mLock = new ReentrantLock(); |
| private final Condition mCondition = mLock.newCondition(); |
| private final LinkedList<Pair<Integer, MediaCodec.BufferInfo>> mCbInputQueue; |
| private final LinkedList<Pair<Integer, MediaCodec.BufferInfo>> mCbOutputQueue; |
| private MediaFormat mOutFormat; |
| private boolean mSignalledOutFormatChanged; |
| private volatile boolean mSignalledError; |
| |
| CodecAsyncHandler() { |
| mCbInputQueue = new LinkedList<>(); |
| mCbOutputQueue = new LinkedList<>(); |
| mSignalledError = false; |
| mSignalledOutFormatChanged = false; |
| } |
| |
| void clearQueues() { |
| mLock.lock(); |
| mCbInputQueue.clear(); |
| mCbOutputQueue.clear(); |
| mLock.unlock(); |
| } |
| |
| void resetContext() { |
| clearQueues(); |
| mOutFormat = null; |
| mSignalledOutFormatChanged = false; |
| mSignalledError = false; |
| } |
| |
| @Override |
| public void onInputBufferAvailable(@NonNull MediaCodec codec, int bufferIndex) { |
| assertTrue(bufferIndex >= 0); |
| mLock.lock(); |
| mCbInputQueue.add(new Pair<>(bufferIndex, (MediaCodec.BufferInfo) null)); |
| mCondition.signalAll(); |
| mLock.unlock(); |
| } |
| |
| @Override |
| public void onOutputBufferAvailable(@NonNull MediaCodec codec, int bufferIndex, |
| @NonNull MediaCodec.BufferInfo info) { |
| assertTrue(bufferIndex >= 0); |
| mLock.lock(); |
| mCbOutputQueue.add(new Pair<>(bufferIndex, info)); |
| mCondition.signalAll(); |
| mLock.unlock(); |
| } |
| |
| @Override |
| public void onError(@NonNull MediaCodec codec, MediaCodec.CodecException e) { |
| mLock.lock(); |
| mSignalledError = true; |
| mCondition.signalAll(); |
| mLock.unlock(); |
| Log.e(LOG_TAG, "received media codec error : " + e.getMessage()); |
| } |
| |
| @Override |
| public void onOutputFormatChanged(@NonNull MediaCodec codec, @NonNull MediaFormat format) { |
| mOutFormat = format; |
| mSignalledOutFormatChanged = true; |
| Log.i(LOG_TAG, "Output format changed: " + format.toString()); |
| } |
| |
| void setCallBack(MediaCodec codec, boolean isCodecInAsyncMode) { |
| if (isCodecInAsyncMode) { |
| codec.setCallback(this); |
| } else { |
| codec.setCallback(null); |
| } |
| } |
| |
| Pair<Integer, MediaCodec.BufferInfo> getInput() throws InterruptedException { |
| Pair<Integer, MediaCodec.BufferInfo> element = null; |
| mLock.lock(); |
| while (!mSignalledError) { |
| if (mCbInputQueue.isEmpty()) { |
| mCondition.await(); |
| } else { |
| element = mCbInputQueue.remove(0); |
| break; |
| } |
| } |
| mLock.unlock(); |
| return element; |
| } |
| |
| Pair<Integer, MediaCodec.BufferInfo> getOutput() throws InterruptedException { |
| Pair<Integer, MediaCodec.BufferInfo> element = null; |
| mLock.lock(); |
| while (!mSignalledError) { |
| if (mCbOutputQueue.isEmpty()) { |
| mCondition.await(); |
| } else { |
| element = mCbOutputQueue.remove(0); |
| break; |
| } |
| } |
| mLock.unlock(); |
| return element; |
| } |
| |
| Pair<Integer, MediaCodec.BufferInfo> getWork() throws InterruptedException { |
| Pair<Integer, MediaCodec.BufferInfo> element = null; |
| mLock.lock(); |
| while (!mSignalledError) { |
| if (mCbInputQueue.isEmpty() && mCbOutputQueue.isEmpty()) { |
| mCondition.await(); |
| } else { |
| if (!mCbOutputQueue.isEmpty()) { |
| element = mCbOutputQueue.remove(0); |
| break; |
| } |
| if (!mCbInputQueue.isEmpty()) { |
| element = mCbInputQueue.remove(0); |
| break; |
| } |
| } |
| } |
| mLock.unlock(); |
| return element; |
| } |
| |
| boolean hasSeenError() { |
| return mSignalledError; |
| } |
| |
| boolean hasOutputFormatChanged() { |
| return mSignalledOutFormatChanged; |
| } |
| |
| MediaFormat getOutputFormat() { |
| return mOutFormat; |
| } |
| } |
| |
| class OutputManager { |
| private static final String LOG_TAG = OutputManager.class.getSimpleName(); |
| private byte[] memory; |
| private int memIndex; |
| private ArrayList<Long> crc32List; |
| |
| OutputManager() { |
| memory = new byte[1024]; |
| memIndex = 0; |
| crc32List = new ArrayList<>(); |
| } |
| |
| void checksum(ByteBuffer buf, int size) { |
| int cap = buf.capacity(); |
| assertTrue("checksum() params are invalid: size = " + size + " cap = " + cap, |
| size > 0 && size <= cap); |
| CRC32 crc = new CRC32(); |
| if (buf.hasArray()) { |
| crc.update(buf.array(), buf.position() + buf.arrayOffset(), size); |
| } else { |
| int pos = buf.position(); |
| final int rdsize = Math.min(4096, size); |
| byte[] bb = new byte[rdsize]; |
| int chk; |
| for (int i = 0; i < size; i += chk) { |
| chk = Math.min(rdsize, size - i); |
| buf.get(bb, 0, chk); |
| crc.update(bb, 0, chk); |
| } |
| buf.position(pos); |
| } |
| crc32List.add(crc.getValue()); |
| } |
| |
| void checksum(Image image) { |
| int format = image.getFormat(); |
| if (format != ImageFormat.YUV_420_888) { |
| crc32List.add(-1L); |
| return; |
| } |
| CRC32 crc = new CRC32(); |
| int imageWidth = image.getWidth(); |
| int imageHeight = image.getHeight(); |
| Image.Plane[] planes = image.getPlanes(); |
| for (int i = 0; i < planes.length; ++i) { |
| ByteBuffer buf = planes[i].getBuffer(); |
| int width, height, rowStride, pixelStride, x, y; |
| rowStride = planes[i].getRowStride(); |
| pixelStride = planes[i].getPixelStride(); |
| if (i == 0) { |
| width = imageWidth; |
| height = imageHeight; |
| } else { |
| width = imageWidth / 2; |
| height = imageHeight / 2; |
| } |
| // local contiguous pixel buffer |
| byte[] bb = new byte[width * height]; |
| if (buf.hasArray()) { |
| byte[] b = buf.array(); |
| int offs = buf.arrayOffset(); |
| if (pixelStride == 1) { |
| for (y = 0; y < height; ++y) { |
| System.arraycopy(bb, y * width, b, y * rowStride + offs, width); |
| } |
| } else { |
| // do it pixel-by-pixel |
| for (y = 0; y < height; ++y) { |
| int lineOffset = offs + y * rowStride; |
| for (x = 0; x < width; ++x) { |
| bb[y * width + x] = b[lineOffset + x * pixelStride]; |
| } |
| } |
| } |
| } else { // almost always ends up here due to direct buffers |
| int pos = buf.position(); |
| if (pixelStride == 1) { |
| for (y = 0; y < height; ++y) { |
| buf.position(pos + y * rowStride); |
| buf.get(bb, y * width, width); |
| } |
| } else { |
| // local line buffer |
| byte[] lb = new byte[rowStride]; |
| // do it pixel-by-pixel |
| for (y = 0; y < height; ++y) { |
| buf.position(pos + y * rowStride); |
| // we're only guaranteed to have pixelStride * (width - 1) + 1 bytes |
| buf.get(lb, 0, pixelStride * (width - 1) + 1); |
| for (x = 0; x < width; ++x) { |
| bb[y * width + x] = lb[x * pixelStride]; |
| } |
| } |
| } |
| buf.position(pos); |
| } |
| crc.update(bb, 0, width * height); |
| } |
| crc32List.add(crc.getValue()); |
| } |
| |
| void saveToMemory(ByteBuffer buf, MediaCodec.BufferInfo info) { |
| if (memIndex + info.size >= memory.length) { |
| memory = Arrays.copyOf(memory, memIndex + info.size); |
| } |
| buf.position(info.offset); |
| buf.get(memory, memIndex, info.size); |
| memIndex += info.size; |
| } |
| |
| void position(int index) { |
| if (index < 0 || index >= memory.length) index = 0; |
| memIndex = index; |
| } |
| |
| void reset() { |
| position(0); |
| crc32List.clear(); |
| } |
| |
| float getRmsError(short[] refData) { |
| long totalErrorSquared = 0; |
| assertTrue(0 == (memory.length & 1)); |
| short[] shortData = new short[memory.length / 2]; |
| ByteBuffer.wrap(memory).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(shortData); |
| if (refData.length != shortData.length) return Float.MAX_VALUE; |
| for (int i = 0; i < shortData.length; i++) { |
| int d = shortData[i] - refData[i]; |
| totalErrorSquared += d * d; |
| } |
| long avgErrorSquared = (totalErrorSquared / shortData.length); |
| return (float) Math.sqrt(avgErrorSquared); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (o == null || getClass() != o.getClass()) return false; |
| OutputManager that = (OutputManager) o; |
| boolean isEqual = true; |
| if (!crc32List.equals(that.crc32List)) { |
| isEqual = false; |
| Log.e(LOG_TAG, "ref and test crc32 checksums mismatch"); |
| } |
| if (memIndex == that.memIndex) { |
| int count = 0; |
| for (int i = 0; i < memIndex; i++) { |
| if (memory[i] != that.memory[i]) { |
| count++; |
| if (count < 20) { |
| Log.d(LOG_TAG, "sample at offset " + i + " exp/got:: " + memory[i] + '/' + |
| that.memory[i]); |
| } |
| } |
| } |
| if (count != 0) { |
| isEqual = false; |
| Log.e(LOG_TAG, "ref and test o/p samples mismatch " + count); |
| } |
| } else { |
| isEqual = false; |
| Log.e(LOG_TAG, "ref and test o/p sizes mismatch " + memIndex + '/' + that.memIndex); |
| } |
| return isEqual; |
| } |
| } |
| |
| abstract class CodecTestBase { |
| private static final String LOG_TAG = CodecTestBase.class.getSimpleName(); |
| static final boolean ENABLE_LOGS = false; |
| static final int PER_TEST_TIMEOUT_LARGE_TEST_MS = 300000; |
| static final int PER_TEST_TIMEOUT_SMALL_TEST_MS = 60000; |
| static final long Q_DEQ_TIMEOUT_US = 500; |
| static final String mInpPrefix = WorkDir.getMediaDirString(); |
| |
| CodecAsyncHandler mAsyncHandle; |
| boolean mIsCodecInAsyncMode; |
| boolean mSawInputEOS; |
| boolean mSawOutputEOS; |
| boolean mSignalEOSWithLastFrame; |
| int mInputCount; |
| int mOutputCount; |
| long mPrevOutputPts; |
| boolean mSignalledOutFormatChanged; |
| MediaFormat mOutFormat; |
| boolean mIsAudio; |
| |
| boolean mSaveToMem; |
| OutputManager mOutputBuff; |
| |
| MediaCodec mCodec; |
| |
| abstract void enqueueInput(int bufferIndex) throws IOException; |
| |
| abstract void dequeueOutput(int bufferIndex, MediaCodec.BufferInfo info); |
| |
| void configureCodec(MediaFormat format, boolean isAsync, boolean signalEOSWithLastFrame, |
| boolean isEncoder) { |
| resetContext(isAsync, signalEOSWithLastFrame); |
| mAsyncHandle.setCallBack(mCodec, isAsync); |
| mCodec.configure(format, null, null, isEncoder ? MediaCodec.CONFIGURE_FLAG_ENCODE : 0); |
| if (ENABLE_LOGS) { |
| Log.v(LOG_TAG, "codec configured"); |
| } |
| } |
| |
| void flushCodec() { |
| mCodec.flush(); |
| mAsyncHandle.clearQueues(); |
| mSawInputEOS = false; |
| mSawOutputEOS = false; |
| mInputCount = 0; |
| mOutputCount = 0; |
| mPrevOutputPts = Long.MIN_VALUE; |
| if (ENABLE_LOGS) { |
| Log.v(LOG_TAG, "codec flushed"); |
| } |
| } |
| |
| void reConfigureCodec(MediaFormat format, boolean isAsync, boolean signalEOSWithLastFrame, |
| boolean isEncoder) { |
| /* TODO(b/147348711) */ |
| if (false) mCodec.stop(); |
| else mCodec.reset(); |
| configureCodec(format, isAsync, signalEOSWithLastFrame, isEncoder); |
| } |
| |
| void resetContext(boolean isAsync, boolean signalEOSWithLastFrame) { |
| mAsyncHandle.resetContext(); |
| mIsCodecInAsyncMode = isAsync; |
| mSawInputEOS = false; |
| mSawOutputEOS = false; |
| mSignalEOSWithLastFrame = signalEOSWithLastFrame; |
| mInputCount = 0; |
| mOutputCount = 0; |
| mPrevOutputPts = Long.MIN_VALUE; |
| mSignalledOutFormatChanged = false; |
| } |
| |
| void enqueueEOS(int bufferIndex) { |
| if (!mSawInputEOS) { |
| mCodec.queueInputBuffer(bufferIndex, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); |
| mSawInputEOS = true; |
| if (ENABLE_LOGS) { |
| Log.v(LOG_TAG, "Queued End of Stream"); |
| } |
| } |
| } |
| |
| void doWork(int frameLimit) throws InterruptedException, IOException { |
| int frameCount = 0; |
| if (mIsCodecInAsyncMode) { |
| // dequeue output after inputEOS is expected to be done in waitForAllOutputs() |
| while (!mAsyncHandle.hasSeenError() && !mSawInputEOS && frameCount < frameLimit) { |
| Pair<Integer, MediaCodec.BufferInfo> element = mAsyncHandle.getWork(); |
| if (element != null) { |
| int bufferID = element.first; |
| MediaCodec.BufferInfo info = element.second; |
| if (info != null) { |
| // <id, info> corresponds to output callback. Handle it accordingly |
| dequeueOutput(bufferID, info); |
| } else { |
| // <id, null> corresponds to input callback. Handle it accordingly |
| enqueueInput(bufferID); |
| frameCount++; |
| } |
| } |
| } |
| } else { |
| MediaCodec.BufferInfo outInfo = new MediaCodec.BufferInfo(); |
| // dequeue output after inputEOS is expected to be done in waitForAllOutputs() |
| while (!mSawInputEOS && frameCount < frameLimit) { |
| int outputBufferId = mCodec.dequeueOutputBuffer(outInfo, Q_DEQ_TIMEOUT_US); |
| if (outputBufferId >= 0) { |
| dequeueOutput(outputBufferId, outInfo); |
| } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { |
| mOutFormat = mCodec.getOutputFormat(); |
| mSignalledOutFormatChanged = true; |
| } |
| int inputBufferId = mCodec.dequeueInputBuffer(Q_DEQ_TIMEOUT_US); |
| if (inputBufferId != -1) { |
| enqueueInput(inputBufferId); |
| frameCount++; |
| } |
| } |
| } |
| } |
| |
| void queueEOS() throws InterruptedException { |
| if (!mSawInputEOS) { |
| if (mIsCodecInAsyncMode) { |
| Pair<Integer, MediaCodec.BufferInfo> element = mAsyncHandle.getInput(); |
| if (element != null) { |
| enqueueEOS(element.first); |
| } |
| } else { |
| enqueueEOS(mCodec.dequeueInputBuffer(-1)); |
| } |
| } |
| } |
| |
| void waitForAllOutputs() throws InterruptedException { |
| if (mIsCodecInAsyncMode) { |
| while (!mAsyncHandle.hasSeenError() && !mSawOutputEOS) { |
| Pair<Integer, MediaCodec.BufferInfo> element = mAsyncHandle.getOutput(); |
| if (element != null) { |
| dequeueOutput(element.first, element.second); |
| } |
| } |
| } else { |
| MediaCodec.BufferInfo outInfo = new MediaCodec.BufferInfo(); |
| while (!mSawOutputEOS) { |
| int outputBufferId = mCodec.dequeueOutputBuffer(outInfo, Q_DEQ_TIMEOUT_US); |
| if (outputBufferId >= 0) { |
| dequeueOutput(outputBufferId, outInfo); |
| } else if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { |
| mOutFormat = mCodec.getOutputFormat(); |
| mSignalledOutFormatChanged = true; |
| } |
| } |
| } |
| } |
| |
| static ArrayList<String> selectCodecs(String mime, ArrayList<MediaFormat> formats, |
| String[] features, boolean isEncoder) { |
| MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS); |
| MediaCodecInfo[] codecInfos = codecList.getCodecInfos(); |
| ArrayList<String> listOfDecoders = new ArrayList<>(); |
| for (MediaCodecInfo codecInfo : codecInfos) { |
| if (codecInfo.isEncoder() != isEncoder) continue; |
| if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && codecInfo.isAlias()) continue; |
| String[] types = codecInfo.getSupportedTypes(); |
| for (String type : types) { |
| if (type.equalsIgnoreCase(mime)) { |
| boolean isOk = true; |
| MediaCodecInfo.CodecCapabilities codecCapabilities = |
| codecInfo.getCapabilitiesForType(type); |
| if (formats != null) { |
| for (MediaFormat format : formats) { |
| if (!codecCapabilities.isFormatSupported(format)) { |
| isOk = false; |
| break; |
| } |
| } |
| } |
| if (features != null) { |
| for (String feature : features) { |
| if (!codecCapabilities.isFeatureSupported(feature)) { |
| isOk = false; |
| break; |
| } |
| } |
| } |
| if (isOk) listOfDecoders.add(codecInfo.getName()); |
| } |
| } |
| } |
| return listOfDecoders; |
| } |
| |
| static int getWidth(MediaFormat format) { |
| int width = format.getInteger(MediaFormat.KEY_WIDTH); |
| if (format.containsKey("crop-left") && format.containsKey("crop-right")) { |
| width = format.getInteger("crop-right") + 1 - format.getInteger("crop-left"); |
| } |
| return width; |
| } |
| |
| static int getHeight(MediaFormat format) { |
| int height = format.getInteger(MediaFormat.KEY_HEIGHT); |
| if (format.containsKey("crop-top") && format.containsKey("crop-bottom")) { |
| height = format.getInteger("crop-bottom") + 1 - format.getInteger("crop-top"); |
| } |
| return height; |
| } |
| |
| static boolean isFormatSimilar(MediaFormat inpFormat, MediaFormat outFormat) { |
| if (inpFormat == null || outFormat == null) return false; |
| String inpMime = inpFormat.getString(MediaFormat.KEY_MIME); |
| String outMime = inpFormat.getString(MediaFormat.KEY_MIME); |
| if (outMime.startsWith("audio/")) { |
| return inpFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) == |
| outFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT) && |
| inpFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE) == |
| outFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE) && |
| inpMime.startsWith("audio/"); |
| } else if (outMime.startsWith("video/")) { |
| return getWidth(inpFormat) == getWidth(outFormat) && |
| getHeight(inpFormat) == getHeight(outFormat) && inpMime.startsWith("video/"); |
| } |
| return true; |
| } |
| } |