| /* |
| * Copyright (C) 2013 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.media.cts; |
| |
| import android.media.MediaCodec; |
| import android.media.MediaCodecInfo; |
| import android.media.MediaCodecList; |
| import android.media.MediaFormat; |
| import android.util.Log; |
| import com.android.cts.media.R; |
| |
| import java.io.File; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| |
| /** |
| * Verification test for vp8 encoder and decoder. |
| * |
| * A raw yv12 stream is encoded at various settings and written to an IVF |
| * file. Encoded stream bitrate and key frame interval are checked against target values. |
| * The stream is later decoded by vp8 decoder to verify frames are decodable and to |
| * calculate PSNR values for various bitrates. |
| */ |
| public class Vp8EncoderTest extends Vp8CodecTestBase { |
| |
| private static final String ENCODED_IVF_BASE = "football"; |
| private static final String INPUT_YUV = null; |
| private static final String OUTPUT_YUV = SDCARD_DIR + File.separator + |
| ENCODED_IVF_BASE + "_out.yuv"; |
| |
| // YUV stream properties. |
| private static final int WIDTH = 320; |
| private static final int HEIGHT = 240; |
| private static final int FPS = 30; |
| // Default encoding bitrate. |
| private static final int BITRATE = 400000; |
| // Default encoding bitrate mode |
| private static final int BITRATE_MODE = VIDEO_ControlRateVariable; |
| // List of bitrates used in quality and basic bitrate tests. |
| private static final int[] TEST_BITRATES_SET = { 300000, 500000, 700000, 900000 }; |
| // Maximum allowed bitrate variation from the target value. |
| private static final double MAX_BITRATE_VARIATION = 0.2; |
| // Average PSNR values for reference Google VP8 codec for the above bitrates. |
| private static final double[] REFERENCE_AVERAGE_PSNR = { 33.1, 35.2, 36.6, 37.8 }; |
| // Minimum PSNR values for reference Google VP8 codec for the above bitrates. |
| private static final double[] REFERENCE_MINIMUM_PSNR = { 25.9, 27.5, 28.4, 30.3 }; |
| // Maximum allowed average PSNR difference of encoder comparing to reference Google encoder. |
| private static final double MAX_AVERAGE_PSNR_DIFFERENCE = 2; |
| // Maximum allowed minimum PSNR difference of encoder comparing to reference Google encoder. |
| private static final double MAX_MINIMUM_PSNR_DIFFERENCE = 4; |
| // Maximum allowed average PSNR difference of the encoder running in a looper thread with 0 ms |
| // buffer dequeue timeout comparing to the encoder running in a callee's thread with 100 ms |
| // buffer dequeue timeout. |
| private static final double MAX_ASYNC_AVERAGE_PSNR_DIFFERENCE = 0.5; |
| // Maximum allowed minimum PSNR difference of the encoder running in a looper thread |
| // comparing to the encoder running in a callee's thread. |
| private static final double MAX_ASYNC_MINIMUM_PSNR_DIFFERENCE = 2; |
| // Maximum allowed average key frame interval variation from the target value. |
| private static final int MAX_AVERAGE_KEYFRAME_INTERVAL_VARIATION = 1; |
| // Maximum allowed key frame interval variation from the target value. |
| private static final int MAX_KEYFRAME_INTERVAL_VARIATION = 3; |
| |
| /** |
| * A basic test for VP8 encoder. |
| * |
| * Encodes 9 seconds of raw stream with default configuration options, |
| * and then decodes it to verify the bitstream. |
| * Also checks the average bitrate is within MAX_BITRATE_VARIATION of the target value. |
| */ |
| public void testBasic() throws Exception { |
| int encodeSeconds = 9; |
| boolean skipped = true; |
| |
| for (int targetBitrate : TEST_BITRATES_SET) { |
| EncoderOutputStreamParameters params = getDefaultEncodingParameters( |
| INPUT_YUV, |
| ENCODED_IVF_BASE, |
| encodeSeconds, |
| WIDTH, |
| HEIGHT, |
| FPS, |
| BITRATE_MODE, |
| targetBitrate, |
| true); |
| ArrayList<MediaCodec.BufferInfo> bufInfo = encode(params); |
| if (bufInfo == null) { |
| continue; |
| } |
| skipped = false; |
| |
| Vp8EncodingStatistics statistics = computeEncodingStatistics(bufInfo); |
| |
| assertEquals("Stream bitrate " + statistics.mAverageBitrate + |
| " is different from the target " + targetBitrate, |
| targetBitrate, statistics.mAverageBitrate, |
| MAX_BITRATE_VARIATION * targetBitrate); |
| |
| decode(params.outputIvfFilename, null, FPS, params.forceGoogleEncoder); |
| } |
| |
| if (skipped) { |
| Log.i(TAG, "SKIPPING testBasic(): codec is not supported"); |
| } |
| } |
| |
| /** |
| * Asynchronous encoding test for VP8 encoder. |
| * |
| * Encodes 9 seconds of raw stream using synchronous and asynchronous calls. |
| * Checks the PSNR difference between the encoded and decoded output and reference yuv input |
| * does not change much for two different ways of the encoder call. |
| */ |
| public void testAsyncEncoding() throws Exception { |
| int encodeSeconds = 9; |
| |
| // First test the encoder running in a looper thread with buffer callbacks enabled. |
| boolean syncEncoding = false; |
| EncoderOutputStreamParameters params = getDefaultEncodingParameters( |
| INPUT_YUV, |
| ENCODED_IVF_BASE, |
| encodeSeconds, |
| WIDTH, |
| HEIGHT, |
| FPS, |
| BITRATE_MODE, |
| BITRATE, |
| syncEncoding); |
| ArrayList<MediaCodec.BufferInfo> bufInfos = encodeAsync(params); |
| if (bufInfos == null) { |
| Log.i(TAG, "SKIPPING testAsyncEncoding(): no suitable encoder found"); |
| return; |
| } |
| computeEncodingStatistics(bufInfos); |
| decode(params.outputIvfFilename, OUTPUT_YUV, FPS, params.forceGoogleEncoder); |
| Vp8DecodingStatistics statisticsAsync = computeDecodingStatistics( |
| params.inputYuvFilename, R.raw.football_qvga, OUTPUT_YUV, |
| params.frameWidth, params.frameHeight); |
| |
| |
| // Test the encoder running in a callee's thread. |
| syncEncoding = true; |
| params = getDefaultEncodingParameters( |
| INPUT_YUV, |
| ENCODED_IVF_BASE, |
| encodeSeconds, |
| WIDTH, |
| HEIGHT, |
| FPS, |
| BITRATE_MODE, |
| BITRATE, |
| syncEncoding); |
| bufInfos = encode(params); |
| if (bufInfos == null) { |
| Log.i(TAG, "SKIPPING testAsyncEncoding(): no suitable encoder found"); |
| return; |
| } |
| computeEncodingStatistics(bufInfos); |
| decode(params.outputIvfFilename, OUTPUT_YUV, FPS, params.forceGoogleEncoder); |
| Vp8DecodingStatistics statisticsSync = computeDecodingStatistics( |
| params.inputYuvFilename, R.raw.football_qvga, OUTPUT_YUV, |
| params.frameWidth, params.frameHeight); |
| |
| // Check PSNR difference. |
| Log.d(TAG, "PSNR Average: Async: " + statisticsAsync.mAveragePSNR + |
| ". Sync: " + statisticsSync.mAveragePSNR); |
| Log.d(TAG, "PSNR Minimum: Async: " + statisticsAsync.mMinimumPSNR + |
| ". Sync: " + statisticsSync.mMinimumPSNR); |
| if ((Math.abs(statisticsAsync.mAveragePSNR - statisticsSync.mAveragePSNR) > |
| MAX_ASYNC_AVERAGE_PSNR_DIFFERENCE) || |
| (Math.abs(statisticsAsync.mMinimumPSNR - statisticsSync.mMinimumPSNR) > |
| MAX_ASYNC_MINIMUM_PSNR_DIFFERENCE)) { |
| throw new RuntimeException("Difference between PSNRs for async and sync encoders"); |
| } |
| } |
| |
| /** |
| * Check if MediaCodec.PARAMETER_KEY_REQUEST_SYNC_FRAME is honored. |
| * |
| * Encodes 9 seconds of raw stream and requests a sync frame every second (30 frames). |
| * The test does not verify the output stream. |
| */ |
| public void testSyncFrame() throws Exception { |
| int encodeSeconds = 9; |
| |
| EncoderOutputStreamParameters params = getDefaultEncodingParameters( |
| INPUT_YUV, |
| ENCODED_IVF_BASE, |
| encodeSeconds, |
| WIDTH, |
| HEIGHT, |
| FPS, |
| BITRATE_MODE, |
| BITRATE, |
| true); |
| params.syncFrameInterval = encodeSeconds * FPS; |
| params.syncForceFrameInterval = FPS; |
| ArrayList<MediaCodec.BufferInfo> bufInfo = encode(params); |
| if (bufInfo == null) { |
| Log.i(TAG, "SKIPPING testSyncFrame(): no suitable encoder found"); |
| return; |
| } |
| |
| Vp8EncodingStatistics statistics = computeEncodingStatistics(bufInfo); |
| |
| // First check if we got expected number of key frames. |
| int actualKeyFrames = statistics.mKeyFrames.size(); |
| if (actualKeyFrames != encodeSeconds) { |
| throw new RuntimeException("Number of key frames " + actualKeyFrames + |
| " is different from the expected " + encodeSeconds); |
| } |
| |
| // Check key frame intervals: |
| // Average value should be within +/- 1 frame of the target value, |
| // maximum value should not be greater than target value + 3, |
| // and minimum value should not be less that target value - 3. |
| if (Math.abs(statistics.mAverageKeyFrameInterval - FPS) > |
| MAX_AVERAGE_KEYFRAME_INTERVAL_VARIATION || |
| (statistics.mMaximumKeyFrameInterval - FPS > MAX_KEYFRAME_INTERVAL_VARIATION) || |
| (FPS - statistics.mMinimumKeyFrameInterval > MAX_KEYFRAME_INTERVAL_VARIATION)) { |
| throw new RuntimeException( |
| "Key frame intervals are different from the expected " + FPS); |
| } |
| } |
| |
| /** |
| * Check if MediaCodec.PARAMETER_KEY_VIDEO_BITRATE is honored. |
| * |
| * Run the the encoder for 12 seconds. Request changes to the |
| * bitrate after 6 seconds and ensure the encoder responds. |
| */ |
| public void testDynamicBitrateChange() throws Exception { |
| int encodeSeconds = 12; // Encoding sequence duration in seconds. |
| int[] bitrateTargetValues = { 400000, 800000 }; // List of bitrates to test. |
| |
| EncoderOutputStreamParameters params = getDefaultEncodingParameters( |
| INPUT_YUV, |
| ENCODED_IVF_BASE, |
| encodeSeconds, |
| WIDTH, |
| HEIGHT, |
| FPS, |
| BITRATE_MODE, |
| bitrateTargetValues[0], |
| true); |
| |
| // Number of seconds for each bitrate |
| int stepSeconds = encodeSeconds / bitrateTargetValues.length; |
| // Fill the bitrates values. |
| params.bitrateSet = new int[encodeSeconds * FPS]; |
| for (int i = 0; i < bitrateTargetValues.length ; i++) { |
| Arrays.fill(params.bitrateSet, |
| i * encodeSeconds * FPS / bitrateTargetValues.length, |
| (i + 1) * encodeSeconds * FPS / bitrateTargetValues.length, |
| bitrateTargetValues[i]); |
| } |
| |
| ArrayList<MediaCodec.BufferInfo> bufInfo = encode(params); |
| if (bufInfo == null) { |
| Log.i(TAG, "SKIPPING testDynamicBitrateChange(): no suitable encoder found"); |
| return; |
| } |
| |
| Vp8EncodingStatistics statistics = computeEncodingStatistics(bufInfo); |
| |
| // Calculate actual average bitrates for every [stepSeconds] second. |
| int[] bitrateActualValues = new int[bitrateTargetValues.length]; |
| for (int i = 0; i < bitrateTargetValues.length ; i++) { |
| bitrateActualValues[i] = 0; |
| for (int j = i * stepSeconds; j < (i + 1) * stepSeconds; j++) { |
| bitrateActualValues[i] += statistics.mBitrates.get(j); |
| } |
| bitrateActualValues[i] /= stepSeconds; |
| Log.d(TAG, "Actual bitrate for interval #" + i + " : " + bitrateActualValues[i] + |
| ". Target: " + bitrateTargetValues[i]); |
| |
| // Compare actual bitrate values to make sure at least same increasing/decreasing |
| // order as the target bitrate values. |
| for (int j = 0; j < i; j++) { |
| long differenceTarget = bitrateTargetValues[i] - bitrateTargetValues[j]; |
| long differenceActual = bitrateActualValues[i] - bitrateActualValues[j]; |
| if (differenceTarget * differenceActual < 0) { |
| throw new RuntimeException("Target bitrates: " + |
| bitrateTargetValues[j] + " , " + bitrateTargetValues[i] + |
| ". Actual bitrates: " |
| + bitrateActualValues[j] + " , " + bitrateActualValues[i]); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Check if encoder and decoder can run simultaneously on different threads. |
| * |
| * Encodes and decodes 9 seconds of raw stream sequentially in CBR mode, |
| * and then run parallel encoding and decoding of the same streams. |
| * Compares average bitrate and PSNR for sequential and parallel runs. |
| */ |
| public void testParallelEncodingAndDecoding() throws Exception { |
| // check for encoder up front, as by the time we detect lack of |
| // encoder support, we may have already started decoding. |
| MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); |
| MediaFormat format = MediaFormat.createVideoFormat(VP8_MIME, WIDTH, HEIGHT); |
| if (mcl.findEncoderForFormat(format) == null) { |
| Log.i(TAG, "SKIPPING testParallelEncodingAndDecoding(): no suitable encoder found"); |
| return; |
| } |
| |
| int encodeSeconds = 9; |
| final int[] bitrate = new int[1]; |
| final double[] psnr = new double[1]; |
| final Exception[] exceptionEncoder = new Exception[1]; |
| final Exception[] exceptionDecoder = new Exception[1]; |
| final EncoderOutputStreamParameters params = getDefaultEncodingParameters( |
| INPUT_YUV, |
| ENCODED_IVF_BASE, |
| encodeSeconds, |
| WIDTH, |
| HEIGHT, |
| FPS, |
| VIDEO_ControlRateConstant, |
| BITRATE, |
| true); |
| final String inputIvfFilename = params.outputIvfFilename; |
| |
| Runnable runEncoder = new Runnable() { |
| public void run() { |
| try { |
| ArrayList<MediaCodec.BufferInfo> bufInfo = encode(params); |
| Vp8EncodingStatistics statistics = computeEncodingStatistics(bufInfo); |
| bitrate[0] = statistics.mAverageBitrate; |
| } catch (Exception e) { |
| Log.e(TAG, "Encoder error: " + e.toString()); |
| exceptionEncoder[0] = e; |
| } |
| } |
| }; |
| Runnable runDecoder = new Runnable() { |
| public void run() { |
| try { |
| decode(inputIvfFilename, OUTPUT_YUV, FPS, params.forceGoogleEncoder); |
| Vp8DecodingStatistics statistics = computeDecodingStatistics( |
| params.inputYuvFilename, R.raw.football_qvga, OUTPUT_YUV, |
| params.frameWidth, params.frameHeight); |
| psnr[0] = statistics.mAveragePSNR; |
| } catch (Exception e) { |
| Log.e(TAG, "Decoder error: " + e.toString()); |
| exceptionDecoder[0] = e; |
| } |
| } |
| }; |
| |
| // Sequential encoding and decoding. |
| runEncoder.run(); |
| if (exceptionEncoder[0] != null) { |
| throw exceptionEncoder[0]; |
| } |
| int referenceBitrate = bitrate[0]; |
| runDecoder.run(); |
| if (exceptionDecoder[0] != null) { |
| throw exceptionDecoder[0]; |
| } |
| double referencePsnr = psnr[0]; |
| |
| // Parallel encoding and decoding. |
| params.outputIvfFilename = SDCARD_DIR + File.separator + ENCODED_IVF_BASE + "_copy.ivf"; |
| Thread threadEncoder = new Thread(runEncoder); |
| Thread threadDecoder = new Thread(runDecoder); |
| threadEncoder.start(); |
| threadDecoder.start(); |
| threadEncoder.join(); |
| threadDecoder.join(); |
| if (exceptionEncoder[0] != null) { |
| throw exceptionEncoder[0]; |
| } |
| if (exceptionDecoder[0] != null) { |
| throw exceptionDecoder[0]; |
| } |
| |
| // Compare bitrates and PSNRs for sequential and parallel cases. |
| Log.d(TAG, "Sequential bitrate: " + referenceBitrate + ". PSNR: " + referencePsnr); |
| Log.d(TAG, "Parallel bitrate: " + bitrate[0] + ". PSNR: " + psnr[0]); |
| assertEquals("Bitrate for sequenatial encoding" + referenceBitrate + |
| " is different from parallel encoding " + bitrate[0], |
| referenceBitrate, bitrate[0], MAX_BITRATE_VARIATION * referenceBitrate); |
| assertEquals("PSNR for sequenatial encoding" + referencePsnr + |
| " is different from parallel encoding " + psnr[0], |
| referencePsnr, psnr[0], MAX_ASYNC_AVERAGE_PSNR_DIFFERENCE); |
| } |
| |
| |
| /** |
| * Check the encoder quality for various bitrates by calculating PSNR |
| * |
| * Run the the encoder for 9 seconds for each bitrate and calculate PSNR |
| * for each encoded stream. |
| * Video streams with higher bitrates should have higher PSNRs. |
| * Also compares average and minimum PSNR of codec with PSNR values of reference Google codec. |
| */ |
| public void testEncoderQuality() throws Exception { |
| int encodeSeconds = 9; // Encoding sequence duration in seconds for each bitrate. |
| double[] psnrPlatformCodecAverage = new double[TEST_BITRATES_SET.length]; |
| double[] psnrPlatformCodecMin = new double[TEST_BITRATES_SET.length]; |
| boolean[] completed = new boolean[TEST_BITRATES_SET.length]; |
| boolean skipped = true; |
| |
| // Run platform specific encoder for different bitrates |
| // and compare PSNR of codec with PSNR of reference Google codec. |
| for (int i = 0; i < TEST_BITRATES_SET.length; i++) { |
| EncoderOutputStreamParameters params = getDefaultEncodingParameters( |
| INPUT_YUV, |
| ENCODED_IVF_BASE, |
| encodeSeconds, |
| WIDTH, |
| HEIGHT, |
| FPS, |
| BITRATE_MODE, |
| TEST_BITRATES_SET[i], |
| true); |
| if (encode(params) == null) { |
| // parameters not supported, try other bitrates |
| completed[i] = false; |
| continue; |
| } |
| completed[i] = true; |
| skipped = false; |
| |
| decode(params.outputIvfFilename, OUTPUT_YUV, FPS, params.forceGoogleEncoder); |
| Vp8DecodingStatistics statistics = computeDecodingStatistics( |
| params.inputYuvFilename, R.raw.football_qvga, OUTPUT_YUV, |
| params.frameWidth, params.frameHeight); |
| psnrPlatformCodecAverage[i] = statistics.mAveragePSNR; |
| psnrPlatformCodecMin[i] = statistics.mMinimumPSNR; |
| } |
| |
| if (skipped) { |
| Log.i(TAG, "SKIPPING testEncoderQuality(): no bitrates supported"); |
| return; |
| } |
| |
| // First do a sanity check - higher bitrates should results in higher PSNR. |
| for (int i = 1; i < TEST_BITRATES_SET.length ; i++) { |
| if (!completed[i]) { |
| continue; |
| } |
| for (int j = 0; j < i; j++) { |
| if (!completed[j]) { |
| continue; |
| } |
| double differenceBitrate = TEST_BITRATES_SET[i] - TEST_BITRATES_SET[j]; |
| double differencePSNR = psnrPlatformCodecAverage[i] - psnrPlatformCodecAverage[j]; |
| if (differenceBitrate * differencePSNR < 0) { |
| throw new RuntimeException("Target bitrates: " + |
| TEST_BITRATES_SET[j] + ", " + TEST_BITRATES_SET[i] + |
| ". Actual PSNRs: " |
| + psnrPlatformCodecAverage[j] + ", " + psnrPlatformCodecAverage[i]); |
| } |
| } |
| } |
| |
| // Then compare average and minimum PSNR of platform codec with reference Google codec - |
| // average PSNR for platform codec should be no more than 2 dB less than reference PSNR |
| // and minumum PSNR - no more than 4 dB less than reference minimum PSNR. |
| // These PSNR difference numbers are arbitrary for now, will need further estimation |
| // when more devices with HW VP8 codec will appear. |
| for (int i = 0; i < TEST_BITRATES_SET.length ; i++) { |
| if (!completed[i]) { |
| continue; |
| } |
| |
| Log.d(TAG, "Bitrate " + TEST_BITRATES_SET[i]); |
| Log.d(TAG, "Reference: Average: " + REFERENCE_AVERAGE_PSNR[i] + ". Minimum: " + |
| REFERENCE_MINIMUM_PSNR[i]); |
| Log.d(TAG, "Platform: Average: " + psnrPlatformCodecAverage[i] + ". Minimum: " + |
| psnrPlatformCodecMin[i]); |
| if (psnrPlatformCodecAverage[i] < REFERENCE_AVERAGE_PSNR[i] - |
| MAX_AVERAGE_PSNR_DIFFERENCE) { |
| throw new RuntimeException("Low average PSNR " + psnrPlatformCodecAverage[i] + |
| " comparing to reference PSNR " + REFERENCE_AVERAGE_PSNR[i] + |
| " for bitrate " + TEST_BITRATES_SET[i]); |
| } |
| if (psnrPlatformCodecMin[i] < REFERENCE_MINIMUM_PSNR[i] - |
| MAX_MINIMUM_PSNR_DIFFERENCE) { |
| throw new RuntimeException("Low minimum PSNR " + psnrPlatformCodecMin[i] + |
| " comparing to reference PSNR " + REFERENCE_MINIMUM_PSNR[i] + |
| " for bitrate " + TEST_BITRATES_SET[i]); |
| } |
| } |
| } |
| } |
| |