1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.media.benchmark.library; 18 19 import android.media.MediaCodec; 20 import android.media.MediaCodec.CodecException; 21 import android.media.MediaFormat; 22 import android.util.Log; 23 24 import androidx.annotation.NonNull; 25 26 import java.io.FileInputStream; 27 import java.io.FileOutputStream; 28 import java.io.IOException; 29 import java.nio.ByteBuffer; 30 31 public class Encoder { 32 // Change in AUDIO_ENCODE_DEFAULT_MAX_INPUT_SIZE should also be taken to 33 // kDefaultAudioEncodeFrameSize present in BenchmarkCommon.h 34 private static final int AUDIO_ENCODE_DEFAULT_MAX_INPUT_SIZE = 4096; 35 private static final String TAG = "Encoder"; 36 private static final boolean DEBUG = false; 37 private static final int kQueueDequeueTimeoutUs = 1000; 38 39 private final Object mLock = new Object(); 40 private MediaCodec mCodec; 41 private String mMime; 42 private Stats mStats; 43 44 private int mOffset; 45 private int mFrameSize; 46 private int mNumInputFrame; 47 private int mNumFrames; 48 private int mFrameRate; 49 private int mSampleRate; 50 private long mInputBufferSize; 51 52 private boolean mSawInputEOS; 53 private boolean mSawOutputEOS; 54 private boolean mSignalledError; 55 56 private FileInputStream mInputStream; 57 private FileOutputStream mOutputStream; 58 Encoder()59 public Encoder() { 60 mStats = new Stats(); 61 mNumInputFrame = 0; 62 mSawInputEOS = false; 63 mSawOutputEOS = false; 64 mSignalledError = false; 65 } 66 67 /** 68 * Setup of encoder 69 * 70 * @param encoderOutputStream Will dump the encoder output in this stream if not null. 71 * @param fileInputStream Will read the decoded output from this stream 72 */ setupEncoder(FileOutputStream encoderOutputStream, FileInputStream fileInputStream)73 public void setupEncoder(FileOutputStream encoderOutputStream, 74 FileInputStream fileInputStream) { 75 this.mInputStream = fileInputStream; 76 this.mOutputStream = encoderOutputStream; 77 } 78 createCodec(String codecName, String mime)79 private MediaCodec createCodec(String codecName, String mime) throws IOException { 80 try { 81 MediaCodec codec; 82 if (codecName.isEmpty()) { 83 Log.i(TAG, "Mime type: " + mime); 84 if (mime != null) { 85 codec = MediaCodec.createEncoderByType(mime); 86 Log.i(TAG, "Encoder created for mime type " + mime); 87 return codec; 88 } else { 89 Log.e(TAG, "Mime type is null, please specify a mime type to create encoder"); 90 return null; 91 } 92 } else { 93 codec = MediaCodec.createByCodecName(codecName); 94 Log.i(TAG, "Encoder created with codec name: " + codecName + " and mime: " + mime); 95 return codec; 96 } 97 } catch (IllegalArgumentException ex) { 98 ex.printStackTrace(); 99 Log.e(TAG, "Failed to create encoder for " + codecName + " mime: " + mime); 100 return null; 101 } 102 } 103 104 /** 105 * Encodes the given raw input file and measures the performance of encode operation, 106 * provided a valid list of parameters are passed as inputs. 107 * 108 * @param codecName Will create the encoder with codecName 109 * @param mime For creating encode format 110 * @param encodeFormat Format of the output data 111 * @param frameSize Size of the frame 112 * @param asyncMode Will run on async implementation if true 113 * @return 0 if encode was successful , -1 for fail, -2 for encoder not created 114 * @throws IOException If the codec cannot be created. 115 */ encode(String codecName, MediaFormat encodeFormat, String mime, int frameRate, int sampleRate, int frameSize, boolean asyncMode)116 public int encode(String codecName, MediaFormat encodeFormat, String mime, int frameRate, 117 int sampleRate, int frameSize, boolean asyncMode) throws IOException { 118 mInputBufferSize = mInputStream.getChannel().size(); 119 mMime = mime; 120 mOffset = 0; 121 mFrameRate = frameRate; 122 mSampleRate = sampleRate; 123 long sTime = mStats.getCurTime(); 124 mCodec = createCodec(codecName, mime); 125 if (mCodec == null) { 126 return -2; 127 } 128 /*Configure Codec*/ 129 try { 130 mCodec.configure(encodeFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); 131 } catch (IllegalArgumentException | IllegalStateException | MediaCodec.CryptoException e) { 132 Log.e(TAG, "Failed to configure " + mCodec.getName() + " encoder."); 133 e.printStackTrace(); 134 return -2; 135 } 136 if (mMime.startsWith("video/")) { 137 mFrameSize = frameSize; 138 } else { 139 int maxInputSize = AUDIO_ENCODE_DEFAULT_MAX_INPUT_SIZE; 140 MediaFormat format = mCodec.getInputFormat(); 141 if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE)) { 142 maxInputSize = format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE); 143 } 144 mFrameSize = frameSize; 145 if (mFrameSize > maxInputSize && maxInputSize > 0) { 146 mFrameSize = maxInputSize; 147 } 148 } 149 mNumFrames = (int) ((mInputBufferSize + mFrameSize - 1) / mFrameSize); 150 if (asyncMode) { 151 mCodec.setCallback(new MediaCodec.Callback() { 152 @Override 153 public void onInputBufferAvailable(@NonNull MediaCodec mediaCodec, 154 int inputBufferId) { 155 try { 156 mStats.addInputTime(); 157 onInputAvailable(mediaCodec, inputBufferId); 158 } catch (Exception e) { 159 e.printStackTrace(); 160 Log.e(TAG, e.toString()); 161 } 162 } 163 164 @Override 165 public void onOutputBufferAvailable(@NonNull MediaCodec mediaCodec, 166 int outputBufferId, 167 @NonNull MediaCodec.BufferInfo bufferInfo) { 168 mStats.addOutputTime(); 169 onOutputAvailable(mediaCodec, outputBufferId, bufferInfo); 170 if (mSawOutputEOS) { 171 Log.i(TAG, "Saw output EOS"); 172 synchronized (mLock) { mLock.notify(); } 173 } 174 } 175 176 @Override 177 public void onError(@NonNull MediaCodec mediaCodec, @NonNull CodecException e) { 178 mSignalledError = true; 179 Log.e(TAG, "Codec Error: " + e.toString()); 180 e.printStackTrace(); 181 synchronized (mLock) { mLock.notify(); } 182 } 183 184 @Override 185 public void onOutputFormatChanged(@NonNull MediaCodec mediaCodec, 186 @NonNull MediaFormat format) { 187 Log.i(TAG, "Output format changed. Format: " + format.toString()); 188 } 189 }); 190 } 191 mCodec.start(); 192 long eTime = mStats.getCurTime(); 193 mStats.setInitTime(mStats.getTimeDiff(sTime, eTime)); 194 mStats.setStartTime(); 195 if (asyncMode) { 196 try { 197 synchronized (mLock) { mLock.wait(); } 198 if (mSignalledError) { 199 return -1; 200 } 201 } catch (InterruptedException e) { 202 e.printStackTrace(); 203 } 204 } else { 205 while (!mSawOutputEOS && !mSignalledError) { 206 /* Queue input data */ 207 if (!mSawInputEOS) { 208 int inputBufferId = mCodec.dequeueInputBuffer(kQueueDequeueTimeoutUs); 209 if (inputBufferId < 0 && inputBufferId != MediaCodec.INFO_TRY_AGAIN_LATER) { 210 Log.e(TAG, "MediaCodec.dequeueInputBuffer " + "returned invalid index : " + 211 inputBufferId); 212 return -1; 213 } 214 mStats.addInputTime(); 215 onInputAvailable(mCodec, inputBufferId); 216 } 217 /* Dequeue output data */ 218 MediaCodec.BufferInfo outputBufferInfo = new MediaCodec.BufferInfo(); 219 int outputBufferId = 220 mCodec.dequeueOutputBuffer(outputBufferInfo, kQueueDequeueTimeoutUs); 221 if (outputBufferId < 0) { 222 if (outputBufferId == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { 223 MediaFormat outFormat = mCodec.getOutputFormat(); 224 Log.i(TAG, "Output format changed. Format: " + outFormat.toString()); 225 } else if (outputBufferId != MediaCodec.INFO_TRY_AGAIN_LATER) { 226 Log.e(TAG, "MediaCodec.dequeueOutputBuffer" + " returned invalid index " + 227 outputBufferId); 228 return -1; 229 } 230 } else { 231 mStats.addOutputTime(); 232 if (DEBUG) { 233 Log.d(TAG, "Dequeue O/P buffer with BufferID " + outputBufferId); 234 } 235 onOutputAvailable(mCodec, outputBufferId, outputBufferInfo); 236 } 237 } 238 } 239 return 0; 240 } 241 onOutputAvailable(MediaCodec mediaCodec, int outputBufferId, MediaCodec.BufferInfo outputBufferInfo)242 private void onOutputAvailable(MediaCodec mediaCodec, int outputBufferId, 243 MediaCodec.BufferInfo outputBufferInfo) { 244 if (mSawOutputEOS || outputBufferId < 0) { 245 if (mSawOutputEOS) { 246 Log.i(TAG, "Saw output EOS"); 247 } 248 return; 249 } 250 ByteBuffer outputBuffer = mediaCodec.getOutputBuffer(outputBufferId); 251 if (mOutputStream != null) { 252 try { 253 254 byte[] bytesOutput = new byte[outputBuffer.remaining()]; 255 outputBuffer.get(bytesOutput); 256 mOutputStream.write(bytesOutput); 257 } catch (IOException e) { 258 e.printStackTrace(); 259 Log.d(TAG, "Error Dumping File: Exception " + e.toString()); 260 return; 261 } 262 } 263 mStats.addFrameSize(outputBuffer.remaining()); 264 mediaCodec.releaseOutputBuffer(outputBufferId, false); 265 mSawOutputEOS = (outputBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; 266 } 267 onInputAvailable(MediaCodec mediaCodec, int inputBufferId)268 private void onInputAvailable(MediaCodec mediaCodec, int inputBufferId) throws IOException { 269 if (mSawInputEOS || inputBufferId < 0) { 270 if (mSawInputEOS) { 271 Log.i(TAG, "Saw input EOS"); 272 } 273 return; 274 } 275 if (mInputBufferSize < mOffset) { 276 Log.e(TAG, "Out of bound access of input buffer"); 277 mSignalledError = true; 278 return; 279 } 280 ByteBuffer inputBuffer = mCodec.getInputBuffer(inputBufferId); 281 if (inputBuffer == null) { 282 mSignalledError = true; 283 return; 284 } 285 int bufSize = inputBuffer.capacity(); 286 int bytesToRead = mFrameSize; 287 if (mInputBufferSize - mOffset < mFrameSize) { 288 bytesToRead = (int) (mInputBufferSize - mOffset); 289 } 290 //b/148655275 - Update Frame size, as Format value may not be valid 291 if (bufSize < bytesToRead) { 292 if(mNumInputFrame == 0) { 293 mFrameSize = bufSize; 294 bytesToRead = bufSize; 295 mNumFrames = (int) ((mInputBufferSize + mFrameSize - 1) / mFrameSize); 296 } else { 297 mSignalledError = true; 298 return; 299 } 300 } 301 302 byte[] inputArray = new byte[bytesToRead]; 303 mInputStream.read(inputArray, 0, bytesToRead); 304 inputBuffer.put(inputArray); 305 int flag = 0; 306 if (mNumInputFrame >= mNumFrames - 1 || bytesToRead == 0) { 307 Log.i(TAG, "Sending EOS on input last frame"); 308 mSawInputEOS = true; 309 flag = MediaCodec.BUFFER_FLAG_END_OF_STREAM; 310 } 311 int presentationTimeUs; 312 if (mMime.startsWith("video/")) { 313 presentationTimeUs = mNumInputFrame * (1000000 / mFrameRate); 314 } else { 315 presentationTimeUs = mNumInputFrame * mFrameSize * 1000000 / mSampleRate; 316 } 317 mediaCodec.queueInputBuffer(inputBufferId, 0, bytesToRead, presentationTimeUs, flag); 318 mNumInputFrame++; 319 mOffset += bytesToRead; 320 } 321 322 /** 323 * Stops the codec and releases codec resources. 324 */ deInitEncoder()325 public void deInitEncoder() { 326 long sTime = mStats.getCurTime(); 327 if (mCodec != null) { 328 mCodec.stop(); 329 mCodec.release(); 330 mCodec = null; 331 } 332 long eTime = mStats.getCurTime(); 333 mStats.setDeInitTime(mStats.getTimeDiff(sTime, eTime)); 334 } 335 336 /** 337 * Prints out the statistics in the information log 338 * 339 * @param inputReference The operation being performed, in this case decode 340 * @param componentName Name of the component/codec 341 * @param mode The operating mode: Sync/Async 342 * @param durationUs Duration of the clip in microseconds 343 * @param statsFile The output file where the stats data is written 344 */ dumpStatistics(String inputReference, String componentName, String mode, long durationUs, String statsFile)345 public void dumpStatistics(String inputReference, String componentName, String mode, 346 long durationUs, String statsFile) throws IOException { 347 String operation = "encode"; 348 mStats.dumpStatistics( 349 inputReference, operation, componentName, mode, durationUs, statsFile); 350 } 351 352 /** 353 * Resets the stats 354 */ resetEncoder()355 public void resetEncoder() { 356 mOffset = 0; 357 mInputBufferSize = 0; 358 mNumInputFrame = 0; 359 mSawInputEOS = false; 360 mSawOutputEOS = false; 361 mSignalledError = false; 362 mStats.reset(); 363 } 364 } 365