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