1 /*
2  * Copyright 2023 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 android.media.misc.cts;
18 
19 import android.app.Activity;
20 import android.media.MediaCodec;
21 import android.media.MediaCodecInfo;
22 import android.media.MediaCodecInfo.CodecCapabilities;
23 import android.media.MediaCodecInfo.VideoCapabilities;
24 import android.media.MediaCodecList;
25 import android.media.MediaFormat;
26 import android.os.Bundle;
27 import android.util.Log;
28 
29 import java.io.IOException;
30 import java.util.ArrayList;
31 
32 public class ResourceManagerCodecActivity extends Activity {
33     private static final String TAG = "ResourceManagerCodecActivity";
34     private static final int MAX_INSTANCES = 32;
35     private static final int FRAME_RATE = 30;
36     private static final int IFRAME_INTERVAL = 10;
37     private boolean mHighResolution = false;
38     private volatile boolean mGotReclaimedException = false;
39     private int mWidth = 0;
40     private int mHeight = 0;
41     private int mBitrate = 0;
42     private String mMime = MediaFormat.MIMETYPE_VIDEO_AVC;
43     private ArrayList<MediaCodec> mCodecs = new ArrayList<MediaCodec>();
44     private Thread mWorkerThread;
45 
46     @Override
onCreate(Bundle savedInstanceState)47     protected void onCreate(Bundle savedInstanceState) {
48         Log.d(TAG, "onCreate called.");
49         super.onCreate(savedInstanceState);
50         // Making this as a background Activity
51         // so that high priority Activities can reclaim codec from this.
52         moveTaskToBack(true);
53 
54         Bundle extras = getIntent().getExtras();
55         if (extras != null) {
56             mHighResolution = extras.getBoolean("high-resolution", mHighResolution);
57             mMime = extras.getString("mime", mMime);
58         }
59 
60         if (allocateCodecs(MAX_INSTANCES) == MAX_INSTANCES) {
61             // As we haven't reached the limit with MAX_INSTANCES,
62             // no need to wait for reclaim exception.
63             Log.d(TAG, "We may not get reclaim event");
64         }
65 
66         useCodecs();
67     }
68 
69     @Override
onDestroy()70     protected void onDestroy() {
71         Log.d(TAG, "onDestroy called.");
72         super.onDestroy();
73     }
74 
75     // MediaCodec callback
76     private static class TestCodecCallback extends MediaCodec.Callback {
77         @Override
onInputBufferAvailable(MediaCodec codec, int index)78         public void onInputBufferAvailable(MediaCodec codec, int index) {
79         }
80 
81         @Override
onOutputBufferAvailable( MediaCodec codec, int index, MediaCodec.BufferInfo info)82         public void onOutputBufferAvailable(
83                 MediaCodec codec, int index, MediaCodec.BufferInfo info) {
84         }
85 
86         @Override
onError(MediaCodec codec, MediaCodec.CodecException e)87         public void onError(MediaCodec codec, MediaCodec.CodecException e) {
88             Log.d(TAG, "onError " + codec.toString() + " errorCode " + e.getErrorCode());
89         }
90 
91         @Override
onOutputFormatChanged(MediaCodec codec, MediaFormat format)92         public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
93             Log.d(TAG, "onOutputFormatChanged " + codec.toString());
94         }
95     }
96 
97     private MediaCodec.Callback mCallback = new TestCodecCallback();
98 
99     // Get a HW Codec info for a given mime (mMime, which is either AVC or HEVC)
getCodecInfo(boolean lookForDecoder)100     private MediaCodecInfo getCodecInfo(boolean lookForDecoder) {
101         MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
102         for (MediaCodecInfo info : mcl.getCodecInfos()) {
103             if (info.isSoftwareOnly()) {
104                 // not testing the sw codecs for now as currently there are't
105                 // any limit on how many concurrent sw codecs can be created.
106                 // Allowing too many codecs may lead system into low memory
107                 // situation and lmkd will kill the test activity and eventually
108                 // failing the test case.
109                 continue;
110             }
111             boolean isEncoder = info.isEncoder();
112             if (lookForDecoder && isEncoder) {
113                 // Looking for a decoder, but found an encoder.
114                 // Skip it
115                 continue;
116             }
117             if (!lookForDecoder && !isEncoder) {
118                 // Looking for an encoder, but found a decoder.
119                 // Skip it
120                 continue;
121             }
122 
123             // Return the first codec capable of the specified MIME type
124             String[] types = info.getSupportedTypes();
125             for (int j = 0; j < types.length; j++) {
126                 if (types[j].equalsIgnoreCase(mMime)) {
127                     return info;
128                 }
129             }
130         }
131 
132         // no matching codec, return null.
133         return null;
134     }
135 
createVideoFormat(MediaCodecInfo info)136     private MediaFormat createVideoFormat(MediaCodecInfo info) {
137         CodecCapabilities caps = info.getCapabilitiesForType(mMime);
138         VideoCapabilities vcaps = caps.getVideoCapabilities();
139 
140         if (mHighResolution) {
141             mWidth = vcaps.getSupportedWidths().getUpper();
142             mHeight = vcaps.getSupportedHeightsFor(mWidth).getUpper();
143             mBitrate = vcaps.getBitrateRange().getUpper();
144         } else {
145             mWidth = vcaps.getSupportedWidths().getLower();
146             mHeight = vcaps.getSupportedHeightsFor(mWidth).getLower();
147             mBitrate = vcaps.getBitrateRange().getLower();
148         }
149 
150         Log.d(TAG, "Mime: " + mMime + " Resolution: " + mWidth + "x" + mHeight
151                  + " Bitrate: " + mBitrate);
152         MediaFormat format = MediaFormat.createVideoFormat(mMime, mWidth, mHeight);
153         format.setInteger(MediaFormat.KEY_COLOR_FORMAT, caps.colorFormats[0]);
154         format.setInteger(MediaFormat.KEY_BIT_RATE, mBitrate);
155         format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
156         format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
157 
158         return format;
159     }
160 
createVideoEncoderFormat(MediaCodecInfo info)161     private MediaFormat createVideoEncoderFormat(MediaCodecInfo info) {
162         MediaFormat format = createVideoFormat(info);
163         format.setInteger("profile", 1);
164         format.setInteger("level", 1);
165         format.setInteger("priority", 1);
166         format.setInteger("color-format", CodecCapabilities.COLOR_FormatYUV420Flexible);
167         format.setInteger("bitrate-mode", 0);
168         format.setInteger("quality", 1);
169 
170         return format;
171     }
172 
173     // Allocates at most max number of codecs
allocateCodecs(int max)174     protected int allocateCodecs(int max) {
175         boolean shouldSkip = false;
176         MediaCodecInfo info = getCodecInfo(true);
177         if (info != null) {
178             // Try allocating max number of decoders first.
179             String name = info.getName();
180             MediaFormat decoderFormat = createVideoFormat(info);
181             allocateCodecs(max, name, decoderFormat, true);
182 
183             // Try allocating max number of encoder next.
184             info = getCodecInfo(false);
185             if (info != null) {
186                 name = info.getName();
187                 MediaFormat encoderFormat = createVideoEncoderFormat(info);
188                 allocateCodecs(max, name, encoderFormat, false);
189             }
190         } else {
191             shouldSkip = true;
192         }
193 
194         if (shouldSkip) {
195             Log.d(TAG, "test skipped as there's no supported codec.");
196             finishWithResult(ResourceManagerStubActivity.RESULT_CODE_NO_DECODER);
197         }
198 
199         Log.d(TAG, "allocateCodecs returned " + mCodecs.size());
200         return mCodecs.size();
201     }
202 
203 
allocateCodecs(int max, String name, MediaFormat format, boolean decoder)204     protected void allocateCodecs(int max, String name, MediaFormat format, boolean decoder) {
205         MediaCodec codec = null;
206         max += mCodecs.size();
207         int flag = decoder ? 0 : MediaCodec.CONFIGURE_FLAG_ENCODE;
208 
209         for (int i = mCodecs.size(); i < max; ++i) {
210             try {
211                 Log.d(TAG, "Create codec " + name + " #" + i);
212                 codec = MediaCodec.createByCodecName(name);
213                 codec.setCallback(mCallback);
214                 Log.d(TAG, "Configure Codec: " + format);
215                 codec.configure(format, null, null, flag);
216                 Log.d(TAG, "Start codec ");
217                 codec.start();
218                 mCodecs.add(codec);
219                 codec = null;
220             } catch (IllegalArgumentException e) {
221                 Log.d(TAG, "IllegalArgumentException " + e.getMessage());
222                 break;
223             } catch (IOException e) {
224                 Log.d(TAG, "IOException " + e.getMessage());
225                 break;
226             } catch (MediaCodec.CodecException e) {
227                 Log.d(TAG, "CodecException 0x" + Integer.toHexString(e.getErrorCode()));
228                 break;
229             } finally {
230                 if (codec != null) {
231                     Log.d(TAG, "release codec");
232                     codec.release();
233                     codec = null;
234                 }
235             }
236         }
237     }
238 
finishWithResult(int result)239     protected void finishWithResult(int result) {
240         for (int i = 0; i < mCodecs.size(); ++i) {
241             Log.d(TAG, "release codec #" + i);
242             mCodecs.get(i).release();
243         }
244         mCodecs.clear();
245         setResult(result);
246         finish();
247         Log.d(TAG, "Activity finished with: " + result);
248     }
249 
doUseCodecs()250     private boolean doUseCodecs() {
251         int current = 0;
252         try {
253             for (current = 0; current < mCodecs.size(); ++current) {
254                 mCodecs.get(current).getName();
255             }
256         } catch (MediaCodec.CodecException e) {
257             Log.d(TAG, "useCodecs got CodecException 0x" + Integer.toHexString(e.getErrorCode()));
258             if (e.getErrorCode() == MediaCodec.CodecException.ERROR_RECLAIMED) {
259                 Log.d(TAG, "Remove codec " + current + " from the list");
260                 mCodecs.get(current).release();
261                 mCodecs.remove(current);
262                 mGotReclaimedException = true;
263             }
264             return false;
265         }
266         return true;
267     }
268 
useCodecs()269     protected void useCodecs() {
270         mWorkerThread = new Thread(new Runnable() {
271             @Override
272             public void run() {
273                 Log.i(TAG, "Started the thread");
274                 long start = System.currentTimeMillis();
275                 long timeSinceStartedMs = 0;
276                 boolean success = true;
277                 while (success && (timeSinceStartedMs < 15000)) {  // timeout in 15s
278                     success = doUseCodecs();
279                     try {
280                         // wait for 50ms before calling doUseCodecs again.
281                         Thread.sleep(50 /* millis */);
282                     } catch (InterruptedException e) { }
283                     timeSinceStartedMs = System.currentTimeMillis() - start;
284                 }
285                 if (mGotReclaimedException) {
286                     Log.d(TAG, "Got expected reclaim exception.");
287                     // As expected a Codec was reclaimed from this (background) Activity.
288                     // So, finish with success.
289                     finishWithResult(RESULT_OK);
290                 } else if (success) {
291                     Log.d(TAG, "No codec reclaim exception, but codec operations successful.");
292                     // Though we were expecting reclaim event, it could be possible that
293                     // oem was able to allocate another codec (had enough resources) for the
294                     // foreground app. In those case, we need to pass this.
295                     finishWithResult(RESULT_OK);
296                 } else {
297                     Log.d(TAG, "Stopped with an unexpected codec exception.");
298                     // We were expecting reclaim event OR codec operations to be successful.
299                     // In neither of the case, some unexpected error happened.
300                     // So, fail the case.
301                     finishWithResult(RESULT_CANCELED);
302                 }
303             }
304         });
305         mWorkerThread.start();
306     }
307 }
308