1 /*
2  * Copyright 2015 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 ResourceManagerTestActivityBase extends Activity {
33     public static final int TYPE_NONSECURE = 0;
34     public static final int TYPE_SECURE = 1;
35     public static final int TYPE_MIX = 2;
36     private static final int FRAME_RATE = 10;
37     // 10 seconds between I-frames
38     private static final int IFRAME_INTERVAL = 10;
39     protected static final int MAX_INSTANCES = 32;
40     // Less important codec of value 100.
41     private static final int CODEC_IMPORTANCE_100 = 100;
42     private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
43 
44     private boolean mIsEncoder = false;
45     protected boolean mChangingCodecImportance = false;
46     private boolean mUseCodecImportanceAtConfig = false;
47     private boolean mUseCodecImportanceLater = false;
48     private int mWidth = 0;
49     private int mHeight = 0;
50     protected String TAG;
51     private String mMime = MediaFormat.MIMETYPE_VIDEO_AVC;
52     private String mCodecName = "none";
53 
54     private ArrayList<MediaCodec> mCodecs = new ArrayList<MediaCodec>();
55     private MediaCodec mFirstMediaCodec;
56 
57     private class TestCodecCallback extends MediaCodec.Callback {
58         @Override
onInputBufferAvailable(MediaCodec codec, int index)59         public void onInputBufferAvailable(MediaCodec codec, int index) {
60             Log.v(TAG, "onInputBufferAvailable " + codec.toString());
61         }
62 
63         @Override
onOutputBufferAvailable( MediaCodec codec, int index, MediaCodec.BufferInfo info)64         public void onOutputBufferAvailable(
65                 MediaCodec codec, int index, MediaCodec.BufferInfo info) {
66             Log.v(TAG, "onOutputBufferAvailable " + codec.toString());
67         }
68 
69         @Override
onError(MediaCodec codec, MediaCodec.CodecException e)70         public void onError(MediaCodec codec, MediaCodec.CodecException e) {
71             int error = e.getErrorCode();
72             Log.e(TAG, "onError " + codec.toString() + " errorCode " + error);
73             if (mChangingCodecImportance && error == MediaCodec.CodecException.ERROR_RECLAIMED) {
74                 if (mFirstMediaCodec == codec) {
75                     mGotReclaimedException = true;
76                     Log.d(TAG, "Codec " + codec + " Was expected to be Reclaimed");
77                 }
78                 codec.release();
79             }
80         }
81 
82         @Override
onOutputFormatChanged(MediaCodec codec, MediaFormat format)83         public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
84             Log.v(TAG, "onOutputFormatChanged " + codec.toString());
85         }
86     }
87 
88     private MediaCodec.Callback mCallback = new TestCodecCallback();
89 
getTestFormat(CodecCapabilities caps, boolean securePlayback, boolean highResolution)90     private MediaFormat getTestFormat(CodecCapabilities caps, boolean securePlayback,
91             boolean highResolution) {
92         VideoCapabilities vcaps = caps.getVideoCapabilities();
93         int bitrate = 0;
94 
95         if (highResolution) {
96             if (mWidth == 0 || mHeight == 0) {
97                 mWidth = vcaps.getSupportedWidths().getUpper();
98                 mHeight = vcaps.getSupportedHeightsFor(mWidth).getUpper();
99             }
100             bitrate = vcaps.getBitrateRange().getUpper();
101         } else {
102             if (mWidth == 0 || mHeight == 0) {
103                 mWidth = vcaps.getSupportedWidths().getLower();
104                 mHeight = vcaps.getSupportedHeightsFor(mWidth).getLower();
105             }
106             bitrate = vcaps.getBitrateRange().getLower();
107         }
108 
109         MediaFormat format = MediaFormat.createVideoFormat(mMime, mWidth, mHeight);
110         format.setInteger(MediaFormat.KEY_COLOR_FORMAT, caps.colorFormats[0]);
111         format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate);
112         format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE);
113         format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, IFRAME_INTERVAL);
114         format.setFeatureEnabled(CodecCapabilities.FEATURE_SecurePlayback, securePlayback);
115 
116         if (mIsEncoder) {
117             // TODO: Facilitate the verification of reclaim when the codec is configured
118             // in realtime and non-realtime priorities.
119             // format.setInteger(MediaFormat.KEY_PRIORITY, 1);
120             // format.setInteger(MediaFormat.KEY_PRIORITY, 0);
121             // TODO: Make sure this color format is supported by the encoder
122             // If not, pick one that is supported.
123             format.setInteger(MediaFormat.KEY_COLOR_FORMAT,
124                     CodecCapabilities.COLOR_FormatYUV420Flexible);
125         }
126         return format;
127     }
128 
getCodecInfo(boolean securePlayback)129     private MediaCodecInfo getCodecInfo(boolean securePlayback) {
130         if (mCodecName.equals("none")) {
131             // We don't know the codec name yet, so look for a decoder
132             // that supports the mime type.
133             return getDecoderInfo(securePlayback);
134         }
135 
136         // We already know the codec name, so return the info directly.
137         for (MediaCodecInfo info : sMCL.getCodecInfos()) {
138             if (info.getName().equals(mCodecName)) {
139                 mIsEncoder = info.isEncoder();
140                 return info;
141             }
142         }
143 
144         return null;
145     }
146 
getDecoderInfo(boolean securePlayback)147     private MediaCodecInfo getDecoderInfo(boolean securePlayback) {
148         MediaCodecInfo fallbackInfo = null;
149 
150         for (MediaCodecInfo info : sMCL.getCodecInfos()) {
151             if (info.isEncoder()) {
152                 // Skip through encoders.
153                 continue;
154             }
155             CodecCapabilities caps;
156             try {
157                 caps = info.getCapabilitiesForType(mMime);
158                 boolean securePlaybackSupported =
159                         caps.isFeatureSupported(CodecCapabilities.FEATURE_SecurePlayback);
160                 boolean securePlaybackRequired =
161                         caps.isFeatureRequired(CodecCapabilities.FEATURE_SecurePlayback);
162                 if ((securePlayback && securePlaybackSupported) ||
163                         (!securePlayback && !securePlaybackRequired) ) {
164                     Log.d(TAG, "securePlayback " + securePlayback + " will use " + info.getName());
165                 } else {
166                     Log.d(TAG, "securePlayback " + securePlayback + " skip " + info.getName());
167                     // If in case there is no secure decoder, use the first
168                     // one as fallback.
169                     if (fallbackInfo == null) {
170                         fallbackInfo = info;
171                     }
172                     continue;
173                 }
174             } catch (IllegalArgumentException e) {
175                 // mime is not supported
176                 continue;
177             }
178             return info;
179         }
180 
181         return fallbackInfo;
182     }
183 
allocateCodecs(int max)184     protected int allocateCodecs(int max) {
185         Bundle extras = getIntent().getExtras();
186         int type = TYPE_NONSECURE;
187         boolean highResolution = false;
188         boolean isResolutionSet = true;
189         if (extras != null) {
190             type = extras.getInt("test-type", type);
191             // Check if codec name has been passed.
192             mCodecName = extras.getString("name", mCodecName);
193             // Check if mime has been passed.
194             mMime = extras.getString("mime", mMime);
195             // Check if resolution has been passed.
196             mWidth = extras.getInt("width");
197             mHeight = extras.getInt("height");
198             if (mWidth == 0 || mHeight == 0) {
199                 // Either no resolution has been passed or its invalid.
200                 isResolutionSet = false;
201                 // See if the high-resolution flag has been set.
202                 highResolution = extras.getBoolean("high-resolution", highResolution);
203             } else if (mHeight >= 1080) {
204                 highResolution = true;
205             }
206 
207             // See if we need to set codec-importance during config.
208             mUseCodecImportanceAtConfig = extras.getBoolean("codec-importance-at-config", false);
209             if (!mUseCodecImportanceAtConfig) {
210                 // See if we need to set codec-importance later (using setParameters)
211                 mUseCodecImportanceLater = extras.getBoolean("codec-importance-later", false);
212             }
213             // Setting this flag to track that we get an expected reclaim on expected codec.
214             mChangingCodecImportance = mUseCodecImportanceAtConfig || mUseCodecImportanceLater;
215         }
216 
217         boolean shouldSkip = false;
218         boolean securePlayback;
219         if (type == TYPE_NONSECURE || type == TYPE_MIX) {
220             securePlayback = false;
221             MediaCodecInfo info = getCodecInfo(securePlayback);
222             if (info != null) {
223                 allocateCodecs(max, info, securePlayback, highResolution);
224             } else {
225                 shouldSkip = true;
226             }
227         }
228 
229         if (!shouldSkip) {
230             if (type == TYPE_SECURE || type == TYPE_MIX) {
231                 if (!isResolutionSet && type == TYPE_MIX) {
232                     // clear previously set resolutions by the unsecure codecs,
233                     // so that we read the secure codec resolutions again.
234                     mWidth = 0;
235                     mHeight = 0;
236                 }
237                 securePlayback = true;
238                 MediaCodecInfo info = getCodecInfo(securePlayback);
239                 if (info != null) {
240                     allocateCodecs(max, info, securePlayback, highResolution);
241                 } else {
242                     shouldSkip = true;
243                 }
244             }
245         }
246 
247         if (shouldSkip) {
248             Log.d(TAG, "test skipped as there's no supported codec.");
249             finishWithResult(ResourceManagerStubActivity.RESULT_CODE_NO_DECODER);
250         }
251 
252         Log.d(TAG, "allocateCodecs(" +  mCodecName + ":" + mMime + ":" + mWidth
253                 + "x" + mHeight + ") returned " + mCodecs.size());
254         return mCodecs.size();
255     }
256 
changeCodecImportance(MediaCodec codec, int importance)257     private void changeCodecImportance(MediaCodec codec, int importance) {
258         final Bundle params = new Bundle();
259         params.putInt(MediaFormat.KEY_IMPORTANCE, importance);
260         codec.setParameters(params);
261     }
262 
allocateCodecs(int max, MediaCodecInfo info, boolean securePlayback, boolean highResolution)263     protected void allocateCodecs(int max, MediaCodecInfo info, boolean securePlayback,
264             boolean highResolution) {
265         mCodecName = info.getName();
266         int flag = mIsEncoder ? MediaCodec.CONFIGURE_FLAG_ENCODE : 0;
267         CodecCapabilities caps = info.getCapabilitiesForType(mMime);
268         MediaFormat format = getTestFormat(caps, securePlayback, highResolution);
269         MediaCodec codec = null;
270         boolean firstCodec = true;
271         boolean loweredFirstCodecImportance = false;
272         boolean lastCodecAttemptWithImportance = false;
273         boolean loweredLastCodecImportance = false;
274         for (int i = mCodecs.size(); i < max; ++i) {
275             try {
276                 Log.d(TAG, "Create codec " + mCodecName + " #" + i);
277                 codec = MediaCodec.createByCodecName(mCodecName);
278                 codec.setCallback(mCallback);
279                 Log.d(TAG, "Configure codec " + format);
280 
281                 // if it's the first codec and if we are to set the importance while configuring,
282                 // then set the same through MediaFormat with codec-importance as a lesser value.
283                 if (firstCodec && mUseCodecImportanceAtConfig) {
284                     format.setInteger(MediaFormat.KEY_IMPORTANCE, CODEC_IMPORTANCE_100);
285                     mFirstMediaCodec = codec;
286                 }
287                 // The last codec creation failed because of insufficient resources.
288                 // So, lets attempt to create one last codec with lesser importance (100)
289                 // and expect it to fail as well.
290                 if (lastCodecAttemptWithImportance) {
291                     format.setInteger(MediaFormat.KEY_IMPORTANCE, CODEC_IMPORTANCE_100);
292                     loweredLastCodecImportance = true;
293                 }
294                 codec.configure(format, null, null, flag);
295 
296                 // We don't want to lower the importance for other codecs.
297                 // So, remove it from the format, if it were set above.
298                 // The remaining codecs will have the default codec-importance as highest (0).
299                 if (firstCodec && mUseCodecImportanceAtConfig) {
300                     format.removeKey(MediaFormat.KEY_IMPORTANCE);
301                     firstCodec = false;
302                 }
303                 Log.d(TAG, "Start codec " + format);
304                 codec.start();
305                 mCodecs.add(codec);
306                 codec = null;
307             } catch (IllegalArgumentException e) {
308                 Log.d(TAG, "IllegalArgumentException " + e.getMessage());
309                 break;
310             } catch (IOException e) {
311                 Log.d(TAG, "IOException " + e.getMessage());
312                 break;
313             } catch (MediaCodec.CodecException e) {
314                 int error = e.getErrorCode();
315                 Log.d(TAG, "CodecException 0x" + Integer.toHexString(error));
316                 if (mUseCodecImportanceLater && !loweredFirstCodecImportance
317                         && error == MediaCodec.CodecException.ERROR_INSUFFICIENT_RESOURCE) {
318                     // Making sure we have at least one codec started.
319                     Log.d(TAG, "Make Codec 0 less important so that it will be reclaimed");
320                     if (i > 0) {
321                         mFirstMediaCodec = mCodecs.get(0);
322                         changeCodecImportance(mFirstMediaCodec, CODEC_IMPORTANCE_100);
323                         // We are doing it only once.
324                         loweredFirstCodecImportance = true;
325                         continue;
326                     } else {
327                         // We don't have any codecs to lower the importance.
328                         break;
329                     }
330                 } else if (!loweredLastCodecImportance && mChangingCodecImportance) {
331                     // The last codec start failed because of insufficient resources.
332                     // So, lets attempt to create one last codec with lesser importance (100)
333                     // and expect it to fail as well.
334                     Log.d(TAG, "Attempt creating less important codec and expect it to fail");
335                     lastCodecAttemptWithImportance = true;
336                     continue;
337                 } else {
338                     break;
339                 }
340             } finally {
341                 if (codec != null) {
342                     Log.d(TAG, "release codec");
343                     codec.release();
344                     codec = null;
345                 }
346             }
347         }
348     }
349 
finishWithResult(int result)350     protected void finishWithResult(int result) {
351         for (int i = 0; i < mCodecs.size(); ++i) {
352             Log.d(TAG, "release codec #" + i + " : " + mCodecs.get(i).toString());
353             mCodecs.get(i).release();
354         }
355         mCodecs.clear();
356         setResult(result);
357         finish();
358         Log.d(TAG, "activity finished with: " + result);
359     }
360 
doUseCodecs()361     private void doUseCodecs() {
362         int current = 0;
363         try {
364             for (current = 0; current < mCodecs.size(); ++current) {
365                 mCodecs.get(current).getName();
366             }
367         } catch (MediaCodec.CodecException e) {
368             Log.d(TAG, "useCodecs got CodecException 0x" + Integer.toHexString(e.getErrorCode()));
369             if (e.getErrorCode() == MediaCodec.CodecException.ERROR_RECLAIMED) {
370                 Log.d(TAG, "Remove codec " + current + " from the list");
371                 mCodecs.get(current).release();
372                 mCodecs.remove(current);
373                 mGotReclaimedException = true;
374                 mUseCodecs = false;
375             }
376             return;
377         }
378     }
379 
380     protected boolean mWaitForReclaim = true;
381     private Thread mWorkerThread;
382     private volatile boolean mUseCodecs = true;
383     private volatile boolean mGotReclaimedException = false;
useCodecs()384     protected void useCodecs() {
385         mWorkerThread = new Thread(new Runnable() {
386             @Override
387             public void run() {
388                 long start = System.currentTimeMillis();
389                 long timeSinceStartedMs = 0;
390                 while (mUseCodecs && (timeSinceStartedMs < 15000)) {  // timeout in 15s
391                     doUseCodecs();
392                     try {
393                         Thread.sleep(50 /* millis */);
394                     } catch (InterruptedException e) {}
395                     timeSinceStartedMs = System.currentTimeMillis() - start;
396                 }
397                 if (mGotReclaimedException) {
398                     Log.d(TAG, "Got expected reclaim exception.");
399                     finishWithResult(RESULT_OK);
400                 } else {
401                     Log.d(TAG, "Stopped without getting reclaim exception.");
402                     // if the test is supposed to wait for reclaim event then this is a failure,
403                     // otherwise this is a pass.
404                     finishWithResult(mWaitForReclaim ? RESULT_CANCELED : RESULT_OK);
405                 }
406             }
407         });
408         mWorkerThread.start();
409     }
410 
411     @Override
onDestroy()412     protected void onDestroy() {
413         Log.d(TAG, "onDestroy called.");
414         super.onDestroy();
415     }
416 }
417