| /* |
| * Copyright (C) 2022 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.common.cts; |
| |
| import static org.junit.Assert.assertEquals; |
| import static org.junit.Assert.assertTrue; |
| |
| import android.graphics.ImageFormat; |
| import android.graphics.Rect; |
| import android.media.AudioFormat; |
| import android.media.Image; |
| import android.media.MediaCodec; |
| |
| import java.nio.ByteBuffer; |
| import java.nio.ByteOrder; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.zip.CRC32; |
| |
| /** |
| * Class to store the output received from mediacodec components. The dequeueOutput() call sends |
| * compressed/decoded bytes of data and their corresponding timestamp information. This is stored |
| * in memory and outPtsList fields of this class. For video decoders, the decoded information can |
| * be overwhelming as it is uncompressed YUV. For them we compute the CRC32 checksum of the |
| * output image and buffer and store it instead. |
| */ |
| public class OutputManager { |
| private static final String LOG_TAG = OutputManager.class.getSimpleName(); |
| private byte[] mMemory; |
| private int mMemIndex; |
| private final CRC32 mCrc32UsingImage; |
| private final CRC32 mCrc32UsingBuffer; |
| private final ArrayList<Long> mInpPtsList; |
| private final ArrayList<Long> mOutPtsList; |
| private final StringBuilder mErrorLogs; |
| private final StringBuilder mSharedErrorLogs; |
| |
| public OutputManager() { |
| this(new StringBuilder()); |
| } |
| |
| public OutputManager(StringBuilder sharedErrorLogs) { |
| mMemory = new byte[1024]; |
| mMemIndex = 0; |
| mCrc32UsingImage = new CRC32(); |
| mCrc32UsingBuffer = new CRC32(); |
| mInpPtsList = new ArrayList<>(); |
| mOutPtsList = new ArrayList<>(); |
| mErrorLogs = new StringBuilder( |
| "################## Error Details ####################\n"); |
| mSharedErrorLogs = sharedErrorLogs; |
| } |
| |
| public void saveInPTS(long pts) { |
| // Add only unique timeStamp, discarding any duplicate frame / non-display frame |
| if (!mInpPtsList.contains(pts)) { |
| mInpPtsList.add(pts); |
| } |
| } |
| |
| public void saveOutPTS(long pts) { |
| mOutPtsList.add(pts); |
| } |
| |
| public boolean isPtsStrictlyIncreasing(long lastPts) { |
| boolean res = true; |
| for (int i = 0; i < mOutPtsList.size(); i++) { |
| if (lastPts < mOutPtsList.get(i)) { |
| lastPts = mOutPtsList.get(i); |
| } else { |
| mErrorLogs.append("Timestamp values are not strictly increasing. \n"); |
| mErrorLogs.append("Frame indices around which timestamp values decreased :- \n"); |
| for (int j = Math.max(0, i - 3); j < Math.min(mOutPtsList.size(), i + 3); j++) { |
| if (j == 0) { |
| mErrorLogs.append(String.format("pts of frame idx -1 is %d \n", lastPts)); |
| } |
| mErrorLogs.append(String.format("pts of frame idx %d is %d \n", j, |
| mOutPtsList.get(j))); |
| } |
| res = false; |
| break; |
| } |
| } |
| return res; |
| } |
| |
| static boolean arePtsListsIdentical(ArrayList<Long> refList, ArrayList<Long> testList, |
| StringBuilder msg) { |
| boolean res = true; |
| if (refList.size() != testList.size()) { |
| msg.append("Reference and test timestamps list sizes are not identical \n"); |
| msg.append(String.format("reference pts list size is %d \n", refList.size())); |
| msg.append(String.format("test pts list size is %d \n", testList.size())); |
| res = false; |
| } |
| if (!res || !refList.equals(testList)) { |
| res = false; |
| ArrayList<Long> refCopyList = new ArrayList<>(refList); |
| ArrayList<Long> testCopyList = new ArrayList<>(testList); |
| refCopyList.removeAll(testList); |
| testCopyList.removeAll(refList); |
| if (refCopyList.size() != 0) { |
| msg.append("Some of the frame/access-units present in ref list are not present " |
| + "in test list. Possibly due to frame drops. \n"); |
| msg.append("List of timestamps that are dropped by the component :- \n"); |
| msg.append("pts :- [[ "); |
| for (int i = 0; i < refCopyList.size(); i++) { |
| msg.append(String.format("{ %d us }, ", refCopyList.get(i))); |
| } |
| msg.append(" ]]\n"); |
| } |
| if (testCopyList.size() != 0) { |
| msg.append("Test list contains frame/access-units that are not present in" |
| + " ref list, Possible due to duplicate transmissions. \n"); |
| msg.append("List of timestamps that are additionally present in test list" |
| + " are :- \n"); |
| msg.append("pts :- [[ "); |
| for (int i = 0; i < testCopyList.size(); i++) { |
| msg.append(String.format("{ %d us }, ", testCopyList.get(i))); |
| } |
| msg.append(" ]]\n"); |
| } |
| } |
| return res; |
| } |
| |
| public boolean isOutPtsListIdenticalToInpPtsList(boolean requireSorting) { |
| Collections.sort(mInpPtsList); |
| if (requireSorting) { |
| Collections.sort(mOutPtsList); |
| } |
| return arePtsListsIdentical(mInpPtsList, mOutPtsList, mErrorLogs); |
| } |
| |
| public int getOutStreamSize() { |
| return mMemIndex; |
| } |
| |
| public void checksum(ByteBuffer buf, int size) { |
| checksum(buf, size, 0, 0, 0, 0); |
| } |
| |
| public void checksum(ByteBuffer buf, int size, int width, int height, int stride, |
| int bytesPerSample) { |
| int cap = buf.capacity(); |
| assertTrue("checksum() params are invalid: size = " + size + " cap = " + cap, |
| size > 0 && size <= cap); |
| if (buf.hasArray()) { |
| if (width > 0 && height > 0 && stride > 0 && bytesPerSample > 0) { |
| int offset = buf.position() + buf.arrayOffset(); |
| byte[] bb = new byte[width * height * bytesPerSample]; |
| for (int i = 0; i < height; ++i) { |
| System.arraycopy(buf.array(), offset, bb, i * width * bytesPerSample, |
| width * bytesPerSample); |
| offset += stride; |
| } |
| mCrc32UsingBuffer.update(bb, 0, width * height * bytesPerSample); |
| } else { |
| mCrc32UsingBuffer.update(buf.array(), buf.position() + buf.arrayOffset(), size); |
| } |
| } else if (width > 0 && height > 0 && stride > 0 && bytesPerSample > 0) { |
| // Checksum only the Y plane |
| int pos = buf.position(); |
| int offset = pos; |
| byte[] bb = new byte[width * height * bytesPerSample]; |
| for (int i = 0; i < height; ++i) { |
| buf.position(offset); |
| buf.get(bb, i * width * bytesPerSample, width * bytesPerSample); |
| offset += stride; |
| } |
| mCrc32UsingBuffer.update(bb, 0, width * height * bytesPerSample); |
| buf.position(pos); |
| } 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); |
| mCrc32UsingBuffer.update(bb, 0, chk); |
| } |
| buf.position(pos); |
| } |
| } |
| |
| public void checksum(Image image) { |
| int format = image.getFormat(); |
| assertTrue("unexpected image format", |
| format == ImageFormat.YUV_420_888 || format == ImageFormat.YCBCR_P010); |
| int bytesPerSample = (ImageFormat.getBitsPerPixel(format) * 2) / (8 * 3); // YUV420 |
| |
| Rect cropRect = image.getCropRect(); |
| int imageWidth = cropRect.width(); |
| int imageHeight = cropRect.height(); |
| assertTrue("unexpected image dimensions", imageWidth > 0 && imageHeight > 0); |
| |
| int imageLeft = cropRect.left; |
| int imageTop = cropRect.top; |
| 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, left, top; |
| rowStride = planes[i].getRowStride(); |
| pixelStride = planes[i].getPixelStride(); |
| if (i == 0) { |
| assertEquals(bytesPerSample, pixelStride); |
| width = imageWidth; |
| height = imageHeight; |
| left = imageLeft; |
| top = imageTop; |
| } else { |
| width = imageWidth / 2; |
| height = imageHeight / 2; |
| left = imageLeft / 2; |
| top = imageTop / 2; |
| } |
| int cropOffset = (left * pixelStride) + top * rowStride; |
| // local contiguous pixel buffer |
| byte[] bb = new byte[width * height * bytesPerSample]; |
| |
| if (buf.hasArray()) { |
| byte[] b = buf.array(); |
| int offs = buf.arrayOffset() + cropOffset; |
| if (pixelStride == bytesPerSample) { |
| for (y = 0; y < height; ++y) { |
| System.arraycopy(b, offs + y * rowStride, bb, y * width * bytesPerSample, |
| width * bytesPerSample); |
| } |
| } else { |
| // do it pixel-by-pixel |
| for (y = 0; y < height; ++y) { |
| int lineOffset = offs + y * rowStride; |
| for (x = 0; x < width; ++x) { |
| for (int bytePos = 0; bytePos < bytesPerSample; ++bytePos) { |
| bb[y * width * bytesPerSample + x * bytesPerSample + bytePos] = |
| b[lineOffset + x * pixelStride + bytePos]; |
| } |
| } |
| } |
| } |
| } else { // almost always ends up here due to direct buffers |
| int base = buf.position(); |
| int pos = base + cropOffset; |
| if (pixelStride == bytesPerSample) { |
| for (y = 0; y < height; ++y) { |
| buf.position(pos + y * rowStride); |
| buf.get(bb, y * width * bytesPerSample, width * bytesPerSample); |
| } |
| } 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) + |
| // bytesPerSample bytes |
| buf.get(lb, 0, pixelStride * (width - 1) + bytesPerSample); |
| for (x = 0; x < width; ++x) { |
| for (int bytePos = 0; bytePos < bytesPerSample; ++bytePos) { |
| bb[y * width * bytesPerSample + x * bytesPerSample + bytePos] = |
| lb[x * pixelStride + bytePos]; |
| } |
| } |
| } |
| } |
| buf.position(base); |
| } |
| mCrc32UsingImage.update(bb, 0, width * height * bytesPerSample); |
| } |
| } |
| |
| public void saveToMemory(ByteBuffer buf, MediaCodec.BufferInfo info) { |
| 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; |
| } |
| |
| void position(int index) { |
| if (index < 0 || index >= mMemory.length) index = 0; |
| mMemIndex = index; |
| } |
| |
| public ByteBuffer getBuffer() { |
| return ByteBuffer.wrap(mMemory); |
| } |
| |
| public StringBuilder getSharedErrorLogs() { |
| return mSharedErrorLogs; |
| } |
| |
| public void reset() { |
| position(0); |
| mCrc32UsingImage.reset(); |
| mCrc32UsingBuffer.reset(); |
| mInpPtsList.clear(); |
| mOutPtsList.clear(); |
| mSharedErrorLogs.setLength(0); |
| mErrorLogs.setLength(0); |
| mErrorLogs.append("################## Error Details ####################\n"); |
| } |
| |
| public float getRmsError(Object refObject, int audioFormat) { |
| double totalErrorSquared = 0; |
| double avgErrorSquared; |
| int bytesPerSample = AudioFormat.getBytesPerSample(audioFormat); |
| if (refObject instanceof float[]) { |
| if (audioFormat != AudioFormat.ENCODING_PCM_FLOAT) return Float.MAX_VALUE; |
| float[] refData = (float[]) refObject; |
| if (refData.length != mMemIndex / bytesPerSample) return Float.MAX_VALUE; |
| float[] floatData = new float[refData.length]; |
| ByteBuffer.wrap(mMemory, 0, mMemIndex).order(ByteOrder.LITTLE_ENDIAN).asFloatBuffer() |
| .get(floatData); |
| for (int i = 0; i < refData.length; i++) { |
| float d = floatData[i] - refData[i]; |
| totalErrorSquared += d * d; |
| } |
| avgErrorSquared = (totalErrorSquared / refData.length); |
| } else if (refObject instanceof int[]) { |
| int[] refData = (int[]) refObject; |
| int[] intData; |
| if (audioFormat == AudioFormat.ENCODING_PCM_24BIT_PACKED) { |
| if (refData.length != (mMemIndex / bytesPerSample)) return Float.MAX_VALUE; |
| intData = new int[refData.length]; |
| for (int i = 0, j = 0; i < mMemIndex; i += 3, j++) { |
| intData[j] = mMemory[j] | (mMemory[j + 1] << 8) | (mMemory[j + 2] << 16); |
| } |
| } else if (audioFormat == AudioFormat.ENCODING_PCM_32BIT) { |
| if (refData.length != mMemIndex / bytesPerSample) return Float.MAX_VALUE; |
| intData = new int[refData.length]; |
| ByteBuffer.wrap(mMemory, 0, mMemIndex).order(ByteOrder.LITTLE_ENDIAN).asIntBuffer() |
| .get(intData); |
| } else { |
| return Float.MAX_VALUE; |
| } |
| for (int i = 0; i < intData.length; i++) { |
| float d = intData[i] - refData[i]; |
| totalErrorSquared += d * d; |
| } |
| avgErrorSquared = (totalErrorSquared / refData.length); |
| } else if (refObject instanceof short[]) { |
| short[] refData = (short[]) refObject; |
| if (refData.length != mMemIndex / bytesPerSample) return Float.MAX_VALUE; |
| if (audioFormat != AudioFormat.ENCODING_PCM_16BIT) return Float.MAX_VALUE; |
| short[] shortData = new short[refData.length]; |
| ByteBuffer.wrap(mMemory, 0, mMemIndex).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer() |
| .get(shortData); |
| for (int i = 0; i < shortData.length; i++) { |
| float d = shortData[i] - refData[i]; |
| totalErrorSquared += d * d; |
| } |
| avgErrorSquared = (totalErrorSquared / refData.length); |
| } else if (refObject instanceof byte[]) { |
| byte[] refData = (byte[]) refObject; |
| if (refData.length != mMemIndex / bytesPerSample) return Float.MAX_VALUE; |
| if (audioFormat != AudioFormat.ENCODING_PCM_8BIT) return Float.MAX_VALUE; |
| byte[] byteData = new byte[refData.length]; |
| ByteBuffer.wrap(mMemory, 0, mMemIndex).get(byteData); |
| for (int i = 0; i < byteData.length; i++) { |
| float d = byteData[i] - refData[i]; |
| totalErrorSquared += d * d; |
| } |
| avgErrorSquared = (totalErrorSquared / refData.length); |
| } else { |
| return Float.MAX_VALUE; |
| } |
| return (float) Math.sqrt(avgErrorSquared); |
| } |
| |
| public long getCheckSumImage() { |
| return mCrc32UsingImage.getValue(); |
| } |
| |
| public long getCheckSumBuffer() { |
| return mCrc32UsingBuffer.getValue(); |
| } |
| |
| @Override |
| public boolean equals(Object o) { |
| if (this == o) return true; |
| if (o == null || getClass() != o.getClass()) return false; |
| OutputManager that = (OutputManager) o; |
| |
| if (!this.equalsInterlaced(o)) return false; |
| return arePtsListsIdentical(mOutPtsList, that.mOutPtsList, mSharedErrorLogs); |
| } |
| |
| // TODO: Timestamps for deinterlaced content are under review. (E.g. can decoders |
| // produce multiple progressive frames?) For now, do not verify timestamps. |
| public boolean equalsInterlaced(Object o) { |
| if (this == o) return true; |
| if (o == null || getClass() != o.getClass()) return false; |
| OutputManager that = (OutputManager) o; |
| boolean isEqual = true; |
| if (mCrc32UsingImage.getValue() != that.mCrc32UsingImage.getValue()) { |
| isEqual = false; |
| mSharedErrorLogs.append("CRC32 checksums computed for image buffers received from " |
| + "getOutputImage() do not match between ref and test runs. \n"); |
| mSharedErrorLogs.append(String.format("Ref CRC32 checksum value is %d \n", |
| mCrc32UsingImage.getValue())); |
| mSharedErrorLogs.append(String.format("Test CRC32 checksum value is %d \n", |
| that.mCrc32UsingImage.getValue())); |
| } |
| if (mCrc32UsingBuffer.getValue() != that.mCrc32UsingBuffer.getValue()) { |
| isEqual = false; |
| mSharedErrorLogs.append("CRC32 checksums computed for byte buffers received from " |
| + "getOutputBuffer() do not match between ref and test runs. \n"); |
| mSharedErrorLogs.append(String.format("Ref CRC32 checksum value is %d \n", |
| mCrc32UsingBuffer.getValue())); |
| mSharedErrorLogs.append(String.format("Test CRC32 checksum value is %d \n", |
| that.mCrc32UsingBuffer.getValue())); |
| if (mMemIndex == that.mMemIndex) { |
| int count = 0; |
| StringBuilder msg = new StringBuilder(); |
| for (int i = 0; i < mMemIndex; i++) { |
| if (mMemory[i] != that.mMemory[i]) { |
| count++; |
| msg.append(String.format("At offset %d, ref buffer val is %x and test " |
| + "buffer val is %x \n", i, mMemory[i], that.mMemory[i])); |
| if (count == 20) { |
| msg.append("stopping after 20 mismatches, ...\n"); |
| break; |
| } |
| } |
| } |
| if (count != 0) { |
| mSharedErrorLogs.append("Ref and Test outputs are not identical \n"); |
| mSharedErrorLogs.append(msg); |
| } |
| } else { |
| mSharedErrorLogs.append("CRC32 byte buffer checksums are different because ref and" |
| + " test output sizes are not identical \n"); |
| mSharedErrorLogs.append(String.format("Ref output buffer size %d \n", mMemIndex)); |
| mSharedErrorLogs.append(String.format("Test output buffer size %d \n", |
| that.mMemIndex)); |
| } |
| } |
| return isEqual; |
| } |
| |
| public String getErrMsg() { |
| return (mErrorLogs.toString() + mSharedErrorLogs.toString()); |
| } |
| } |