1 /* 2 * Copyright 2014 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 package android.cts.util; 17 18 import android.content.Context; 19 import android.content.res.AssetFileDescriptor; 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.MediaExtractor; 26 import android.media.MediaFormat; 27 import android.net.Uri; 28 import android.util.Log; 29 import android.util.Range; 30 31 import com.android.compatibility.common.util.DeviceReportLog; 32 import com.android.compatibility.common.util.ResultType; 33 import com.android.compatibility.common.util.ResultUnit; 34 35 import java.lang.reflect.Method; 36 import static java.lang.reflect.Modifier.isPublic; 37 import static java.lang.reflect.Modifier.isStatic; 38 import java.util.ArrayList; 39 import java.util.Arrays; 40 import java.util.Map; 41 42 import static junit.framework.Assert.assertTrue; 43 44 import java.io.IOException; 45 46 public class MediaUtils { 47 private static final String TAG = "MediaUtils"; 48 49 /* 50 * ----------------------- HELPER METHODS FOR SKIPPING TESTS ----------------------- 51 */ 52 private static final int ALL_AV_TRACKS = -1; 53 54 private static final MediaCodecList sMCL = new MediaCodecList(MediaCodecList.REGULAR_CODECS); 55 56 /** 57 * Returns the test name (heuristically). 58 * 59 * Since it uses heuristics, this method has only been verified for media 60 * tests. This centralizes the way to signal errors during a test. 61 */ getTestName()62 public static String getTestName() { 63 return getTestName(false /* withClass */); 64 } 65 66 /** 67 * Returns the test name with the full class (heuristically). 68 * 69 * Since it uses heuristics, this method has only been verified for media 70 * tests. This centralizes the way to signal errors during a test. 71 */ getTestNameWithClass()72 public static String getTestNameWithClass() { 73 return getTestName(true /* withClass */); 74 } 75 getTestName(boolean withClass)76 private static String getTestName(boolean withClass) { 77 int bestScore = -1; 78 String testName = "test???"; 79 Map<Thread, StackTraceElement[]> traces = Thread.getAllStackTraces(); 80 for (Map.Entry<Thread, StackTraceElement[]> entry : traces.entrySet()) { 81 StackTraceElement[] stack = entry.getValue(); 82 for (int index = 0; index < stack.length; ++index) { 83 // method name must start with "test" 84 String methodName = stack[index].getMethodName(); 85 if (!methodName.startsWith("test")) { 86 continue; 87 } 88 89 int score = 0; 90 // see if there is a public non-static void method that takes no argument 91 Class<?> clazz; 92 try { 93 clazz = Class.forName(stack[index].getClassName()); 94 ++score; 95 for (final Method method : clazz.getDeclaredMethods()) { 96 if (method.getName().equals(methodName) 97 && isPublic(method.getModifiers()) 98 && !isStatic(method.getModifiers()) 99 && method.getParameterTypes().length == 0 100 && method.getReturnType().equals(Void.TYPE)) { 101 ++score; 102 break; 103 } 104 } 105 if (score == 1) { 106 // if we could read the class, but method is not public void, it is 107 // not a candidate 108 continue; 109 } 110 } catch (ClassNotFoundException e) { 111 } 112 113 // even if we cannot verify the method signature, there are signals in the stack 114 115 // usually test method is invoked by reflection 116 int depth = 1; 117 while (index + depth < stack.length 118 && stack[index + depth].getMethodName().equals("invoke") 119 && stack[index + depth].getClassName().equals( 120 "java.lang.reflect.Method")) { 121 ++depth; 122 } 123 if (depth > 1) { 124 ++score; 125 // and usually test method is run by runMethod method in android.test package 126 if (index + depth < stack.length) { 127 if (stack[index + depth].getClassName().startsWith("android.test.")) { 128 ++score; 129 } 130 if (stack[index + depth].getMethodName().equals("runMethod")) { 131 ++score; 132 } 133 } 134 } 135 136 if (score > bestScore) { 137 bestScore = score; 138 testName = methodName; 139 if (withClass) { 140 testName = stack[index].getClassName() + "." + testName; 141 } 142 } 143 } 144 } 145 return testName; 146 } 147 148 /** 149 * Finds test name (heuristically) and prints out standard skip message. 150 * 151 * Since it uses heuristics, this method has only been verified for media 152 * tests. This centralizes the way to signal a skipped test. 153 */ skipTest(String tag, String reason)154 public static void skipTest(String tag, String reason) { 155 Log.i(tag, "SKIPPING " + getTestName() + "(): " + reason); 156 DeviceReportLog log = new DeviceReportLog("CtsMediaSkippedTests", "test_skipped"); 157 log.addValue("reason", reason, ResultType.NEUTRAL, ResultUnit.NONE); 158 log.addValue( 159 "test", getTestNameWithClass(), ResultType.NEUTRAL, ResultUnit.NONE); 160 // TODO: replace with submit() when it is added to DeviceReportLog 161 try { 162 log.submit(null); 163 } catch (NullPointerException e) { } 164 } 165 166 /** 167 * Finds test name (heuristically) and prints out standard skip message. 168 * 169 * Since it uses heuristics, this method has only been verified for media 170 * tests. This centralizes the way to signal a skipped test. 171 */ skipTest(String reason)172 public static void skipTest(String reason) { 173 skipTest(TAG, reason); 174 } 175 check(boolean result, String message)176 public static boolean check(boolean result, String message) { 177 if (!result) { 178 skipTest(message); 179 } 180 return result; 181 } 182 183 /* 184 * ------------------- HELPER METHODS FOR CHECKING CODEC SUPPORT ------------------- 185 */ 186 187 // returns the list of codecs that support any one of the formats getCodecNames( boolean isEncoder, Boolean isGoog, MediaFormat... formats)188 private static String[] getCodecNames( 189 boolean isEncoder, Boolean isGoog, MediaFormat... formats) { 190 MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); 191 ArrayList<String> result = new ArrayList<>(); 192 for (MediaCodecInfo info : mcl.getCodecInfos()) { 193 if (info.isEncoder() != isEncoder) { 194 continue; 195 } 196 if (isGoog != null 197 && info.getName().toLowerCase().startsWith("omx.google.") != isGoog) { 198 continue; 199 } 200 201 for (MediaFormat format : formats) { 202 String mime = format.getString(MediaFormat.KEY_MIME); 203 204 CodecCapabilities caps = null; 205 try { 206 caps = info.getCapabilitiesForType(mime); 207 } catch (IllegalArgumentException e) { // mime is not supported 208 continue; 209 } 210 if (caps.isFormatSupported(format)) { 211 result.add(info.getName()); 212 break; 213 } 214 } 215 } 216 return result.toArray(new String[result.size()]); 217 } 218 219 /* Use isGoog = null to query all decoders */ getDecoderNames( Boolean isGoog, MediaFormat... formats)220 public static String[] getDecoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) { 221 return getCodecNames(false /* isEncoder */, isGoog, formats); 222 } 223 getDecoderNames(MediaFormat... formats)224 public static String[] getDecoderNames(MediaFormat... formats) { 225 return getCodecNames(false /* isEncoder */, null /* isGoog */, formats); 226 } 227 228 /* Use isGoog = null to query all decoders */ getEncoderNames( Boolean isGoog, MediaFormat... formats)229 public static String[] getEncoderNames(/* Nullable */ Boolean isGoog, MediaFormat... formats) { 230 return getCodecNames(true /* isEncoder */, isGoog, formats); 231 } 232 getEncoderNames(MediaFormat... formats)233 public static String[] getEncoderNames(MediaFormat... formats) { 234 return getCodecNames(true /* isEncoder */, null /* isGoog */, formats); 235 } 236 verifyNumCodecs( int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats)237 public static void verifyNumCodecs( 238 int count, boolean isEncoder, Boolean isGoog, MediaFormat... formats) { 239 String desc = (isEncoder ? "encoders" : "decoders") + " for " 240 + (formats.length == 1 ? formats[0].toString() : Arrays.toString(formats)); 241 if (isGoog != null) { 242 desc = (isGoog ? "Google " : "non-Google ") + desc; 243 } 244 245 String[] codecs = getCodecNames(isEncoder, isGoog, formats); 246 assertTrue("test can only verify " + count + " " + desc + "; found " + codecs.length + ": " 247 + Arrays.toString(codecs), codecs.length <= count); 248 } 249 getDecoder(MediaFormat format)250 public static MediaCodec getDecoder(MediaFormat format) { 251 String decoder = sMCL.findDecoderForFormat(format); 252 if (decoder != null) { 253 try { 254 return MediaCodec.createByCodecName(decoder); 255 } catch (IOException e) { 256 } 257 } 258 return null; 259 } 260 canEncode(MediaFormat format)261 public static boolean canEncode(MediaFormat format) { 262 if (sMCL.findEncoderForFormat(format) == null) { 263 Log.i(TAG, "no encoder for " + format); 264 return false; 265 } 266 return true; 267 } 268 canDecode(MediaFormat format)269 public static boolean canDecode(MediaFormat format) { 270 if (sMCL.findDecoderForFormat(format) == null) { 271 Log.i(TAG, "no decoder for " + format); 272 return false; 273 } 274 return true; 275 } 276 supports(String codecName, String mime, int w, int h)277 public static boolean supports(String codecName, String mime, int w, int h) { 278 // While this could be simply written as such, give more graceful feedback. 279 // MediaFormat format = MediaFormat.createVideoFormat(mime, w, h); 280 // return supports(codecName, format); 281 282 VideoCapabilities vidCap = getVideoCapabilities(codecName, mime); 283 if (vidCap == null) { 284 return false; 285 } else if (vidCap.isSizeSupported(w, h)) { 286 return true; 287 } 288 289 Log.w(TAG, "unsupported size " + w + "x" + h); 290 return false; 291 } 292 supports(String codecName, MediaFormat format)293 public static boolean supports(String codecName, MediaFormat format) { 294 MediaCodec codec; 295 try { 296 codec = MediaCodec.createByCodecName(codecName); 297 } catch (IOException e) { 298 Log.w(TAG, "codec not found: " + codecName); 299 return false; 300 } 301 302 String mime = format.getString(MediaFormat.KEY_MIME); 303 CodecCapabilities cap = null; 304 try { 305 cap = codec.getCodecInfo().getCapabilitiesForType(mime); 306 } catch (IllegalArgumentException e) { 307 Log.w(TAG, "not supported mime: " + mime); 308 codec.release(); 309 return false; 310 } 311 312 return cap.isFormatSupported(format); 313 } 314 hasCodecForTrack(MediaExtractor ex, int track)315 public static boolean hasCodecForTrack(MediaExtractor ex, int track) { 316 int count = ex.getTrackCount(); 317 if (track < 0 || track >= count) { 318 throw new IndexOutOfBoundsException(track + " not in [0.." + (count - 1) + "]"); 319 } 320 return canDecode(ex.getTrackFormat(track)); 321 } 322 323 /** 324 * return true iff all audio and video tracks are supported 325 */ hasCodecsForMedia(MediaExtractor ex)326 public static boolean hasCodecsForMedia(MediaExtractor ex) { 327 for (int i = 0; i < ex.getTrackCount(); ++i) { 328 MediaFormat format = ex.getTrackFormat(i); 329 // only check for audio and video codecs 330 String mime = format.getString(MediaFormat.KEY_MIME).toLowerCase(); 331 if (!mime.startsWith("audio/") && !mime.startsWith("video/")) { 332 continue; 333 } 334 if (!canDecode(format)) { 335 return false; 336 } 337 } 338 return true; 339 } 340 341 /** 342 * return true iff any track starting with mimePrefix is supported 343 */ hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix)344 public static boolean hasCodecForMediaAndDomain(MediaExtractor ex, String mimePrefix) { 345 mimePrefix = mimePrefix.toLowerCase(); 346 for (int i = 0; i < ex.getTrackCount(); ++i) { 347 MediaFormat format = ex.getTrackFormat(i); 348 String mime = format.getString(MediaFormat.KEY_MIME); 349 if (mime.toLowerCase().startsWith(mimePrefix)) { 350 if (canDecode(format)) { 351 return true; 352 } 353 Log.i(TAG, "no decoder for " + format); 354 } 355 } 356 return false; 357 } 358 hasCodecsForResourceCombo( Context context, int resourceId, int track, String mimePrefix)359 private static boolean hasCodecsForResourceCombo( 360 Context context, int resourceId, int track, String mimePrefix) { 361 try { 362 AssetFileDescriptor afd = null; 363 MediaExtractor ex = null; 364 try { 365 afd = context.getResources().openRawResourceFd(resourceId); 366 ex = new MediaExtractor(); 367 ex.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); 368 if (mimePrefix != null) { 369 return hasCodecForMediaAndDomain(ex, mimePrefix); 370 } else if (track == ALL_AV_TRACKS) { 371 return hasCodecsForMedia(ex); 372 } else { 373 return hasCodecForTrack(ex, track); 374 } 375 } finally { 376 if (ex != null) { 377 ex.release(); 378 } 379 if (afd != null) { 380 afd.close(); 381 } 382 } 383 } catch (IOException e) { 384 Log.i(TAG, "could not open resource"); 385 } 386 return false; 387 } 388 389 /** 390 * return true iff all audio and video tracks are supported 391 */ hasCodecsForResource(Context context, int resourceId)392 public static boolean hasCodecsForResource(Context context, int resourceId) { 393 return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, null /* mimePrefix */); 394 } 395 checkCodecsForResource(Context context, int resourceId)396 public static boolean checkCodecsForResource(Context context, int resourceId) { 397 return check(hasCodecsForResource(context, resourceId), "no decoder found"); 398 } 399 400 /** 401 * return true iff track is supported. 402 */ hasCodecForResource(Context context, int resourceId, int track)403 public static boolean hasCodecForResource(Context context, int resourceId, int track) { 404 return hasCodecsForResourceCombo(context, resourceId, track, null /* mimePrefix */); 405 } 406 checkCodecForResource(Context context, int resourceId, int track)407 public static boolean checkCodecForResource(Context context, int resourceId, int track) { 408 return check(hasCodecForResource(context, resourceId, track), "no decoder found"); 409 } 410 411 /** 412 * return true iff any track starting with mimePrefix is supported 413 */ hasCodecForResourceAndDomain( Context context, int resourceId, String mimePrefix)414 public static boolean hasCodecForResourceAndDomain( 415 Context context, int resourceId, String mimePrefix) { 416 return hasCodecsForResourceCombo(context, resourceId, ALL_AV_TRACKS, mimePrefix); 417 } 418 419 /** 420 * return true iff all audio and video tracks are supported 421 */ hasCodecsForPath(Context context, String path)422 public static boolean hasCodecsForPath(Context context, String path) { 423 MediaExtractor ex = null; 424 try { 425 ex = new MediaExtractor(); 426 Uri uri = Uri.parse(path); 427 String scheme = uri.getScheme(); 428 if (scheme == null) { // file 429 ex.setDataSource(path); 430 } else if (scheme.equalsIgnoreCase("file")) { 431 ex.setDataSource(uri.getPath()); 432 } else { 433 ex.setDataSource(context, uri, null); 434 } 435 return hasCodecsForMedia(ex); 436 } catch (IOException e) { 437 Log.i(TAG, "could not open path " + path); 438 } finally { 439 if (ex != null) { 440 ex.release(); 441 } 442 } 443 return true; 444 } 445 checkCodecsForPath(Context context, String path)446 public static boolean checkCodecsForPath(Context context, String path) { 447 return check(hasCodecsForPath(context, path), "no decoder found"); 448 } 449 hasCodecForDomain(boolean encoder, String domain)450 public static boolean hasCodecForDomain(boolean encoder, String domain) { 451 for (MediaCodecInfo info : sMCL.getCodecInfos()) { 452 if (encoder != info.isEncoder()) { 453 continue; 454 } 455 456 for (String type : info.getSupportedTypes()) { 457 if (type.toLowerCase().startsWith(domain.toLowerCase() + "/")) { 458 Log.i(TAG, "found codec " + info.getName() + " for mime " + type); 459 return true; 460 } 461 } 462 } 463 return false; 464 } 465 checkCodecForDomain(boolean encoder, String domain)466 public static boolean checkCodecForDomain(boolean encoder, String domain) { 467 return check(hasCodecForDomain(encoder, domain), 468 "no " + domain + (encoder ? " encoder" : " decoder") + " found"); 469 } 470 hasCodecForMime(boolean encoder, String mime)471 private static boolean hasCodecForMime(boolean encoder, String mime) { 472 for (MediaCodecInfo info : sMCL.getCodecInfos()) { 473 if (encoder != info.isEncoder()) { 474 continue; 475 } 476 477 for (String type : info.getSupportedTypes()) { 478 if (type.equalsIgnoreCase(mime)) { 479 Log.i(TAG, "found codec " + info.getName() + " for mime " + mime); 480 return true; 481 } 482 } 483 } 484 return false; 485 } 486 hasCodecForMimes(boolean encoder, String[] mimes)487 private static boolean hasCodecForMimes(boolean encoder, String[] mimes) { 488 for (String mime : mimes) { 489 if (!hasCodecForMime(encoder, mime)) { 490 Log.i(TAG, "no " + (encoder ? "encoder" : "decoder") + " for mime " + mime); 491 return false; 492 } 493 } 494 return true; 495 } 496 497 hasEncoder(String... mimes)498 public static boolean hasEncoder(String... mimes) { 499 return hasCodecForMimes(true /* encoder */, mimes); 500 } 501 hasDecoder(String... mimes)502 public static boolean hasDecoder(String... mimes) { 503 return hasCodecForMimes(false /* encoder */, mimes); 504 } 505 checkDecoder(String... mimes)506 public static boolean checkDecoder(String... mimes) { 507 return check(hasCodecForMimes(false /* encoder */, mimes), "no decoder found"); 508 } 509 checkEncoder(String... mimes)510 public static boolean checkEncoder(String... mimes) { 511 return check(hasCodecForMimes(true /* encoder */, mimes), "no encoder found"); 512 } 513 canDecodeVideo(String mime, int width, int height, float rate)514 public static boolean canDecodeVideo(String mime, int width, int height, float rate) { 515 MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); 516 format.setFloat(MediaFormat.KEY_FRAME_RATE, rate); 517 return canDecode(format); 518 } 519 canDecodeVideo( String mime, int width, int height, float rate, Integer profile, Integer level, Integer bitrate)520 public static boolean canDecodeVideo( 521 String mime, int width, int height, float rate, 522 Integer profile, Integer level, Integer bitrate) { 523 MediaFormat format = MediaFormat.createVideoFormat(mime, width, height); 524 format.setFloat(MediaFormat.KEY_FRAME_RATE, rate); 525 if (profile != null) { 526 format.setInteger(MediaFormat.KEY_PROFILE, profile); 527 if (level != null) { 528 format.setInteger(MediaFormat.KEY_LEVEL, level); 529 } 530 } 531 if (bitrate != null) { 532 format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); 533 } 534 return canDecode(format); 535 } 536 checkEncoderForFormat(MediaFormat format)537 public static boolean checkEncoderForFormat(MediaFormat format) { 538 return check(canEncode(format), "no encoder for " + format); 539 } 540 checkDecoderForFormat(MediaFormat format)541 public static boolean checkDecoderForFormat(MediaFormat format) { 542 return check(canDecode(format), "no decoder for " + format); 543 } 544 545 /* 546 * ----------------------- HELPER METHODS FOR MEDIA HANDLING ----------------------- 547 */ 548 getVideoCapabilities(String codecName, String mime)549 public static VideoCapabilities getVideoCapabilities(String codecName, String mime) { 550 for (MediaCodecInfo info : sMCL.getCodecInfos()) { 551 if (!info.getName().equalsIgnoreCase(codecName)) { 552 continue; 553 } 554 CodecCapabilities caps; 555 try { 556 caps = info.getCapabilitiesForType(mime); 557 } catch (IllegalArgumentException e) { 558 // mime is not supported 559 Log.w(TAG, "not supported mime: " + mime); 560 return null; 561 } 562 VideoCapabilities vidCaps = caps.getVideoCapabilities(); 563 if (vidCaps == null) { 564 Log.w(TAG, "not a video codec: " + codecName); 565 } 566 return vidCaps; 567 } 568 Log.w(TAG, "codec not found: " + codecName); 569 return null; 570 } 571 getTrackFormatForResource( Context context, int resourceId, String mimeTypePrefix)572 public static MediaFormat getTrackFormatForResource( 573 Context context, int resourceId, String mimeTypePrefix) 574 throws IOException { 575 MediaFormat format = null; 576 MediaExtractor extractor = new MediaExtractor(); 577 AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId); 578 try { 579 extractor.setDataSource( 580 afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); 581 } finally { 582 afd.close(); 583 } 584 int trackIndex; 585 for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) { 586 MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex); 587 if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) { 588 format = trackMediaFormat; 589 break; 590 } 591 } 592 extractor.release(); 593 afd.close(); 594 if (format == null) { 595 throw new RuntimeException("couldn't get a track for " + mimeTypePrefix); 596 } 597 598 return format; 599 } 600 createMediaExtractorForMimeType( Context context, int resourceId, String mimeTypePrefix)601 public static MediaExtractor createMediaExtractorForMimeType( 602 Context context, int resourceId, String mimeTypePrefix) 603 throws IOException { 604 MediaExtractor extractor = new MediaExtractor(); 605 AssetFileDescriptor afd = context.getResources().openRawResourceFd(resourceId); 606 try { 607 extractor.setDataSource( 608 afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); 609 } finally { 610 afd.close(); 611 } 612 int trackIndex; 613 for (trackIndex = 0; trackIndex < extractor.getTrackCount(); trackIndex++) { 614 MediaFormat trackMediaFormat = extractor.getTrackFormat(trackIndex); 615 if (trackMediaFormat.getString(MediaFormat.KEY_MIME).startsWith(mimeTypePrefix)) { 616 extractor.selectTrack(trackIndex); 617 break; 618 } 619 } 620 if (trackIndex == extractor.getTrackCount()) { 621 extractor.release(); 622 throw new IllegalStateException("couldn't get a track for " + mimeTypePrefix); 623 } 624 625 return extractor; 626 } 627 628 /* 629 * ---------------------- HELPER METHODS FOR CODEC CONFIGURATION 630 */ 631 632 /** Format must contain mime, width and height. 633 * Throws Exception if encoder does not support this width and height */ setMaxEncoderFrameAndBitrates( MediaCodec encoder, MediaFormat format, int maxFps)634 public static void setMaxEncoderFrameAndBitrates( 635 MediaCodec encoder, MediaFormat format, int maxFps) { 636 String mime = format.getString(MediaFormat.KEY_MIME); 637 638 VideoCapabilities vidCaps = 639 encoder.getCodecInfo().getCapabilitiesForType(mime).getVideoCapabilities(); 640 setMaxEncoderFrameAndBitrates(vidCaps, format, maxFps); 641 } 642 setMaxEncoderFrameAndBitrates( VideoCapabilities vidCaps, MediaFormat format, int maxFps)643 public static void setMaxEncoderFrameAndBitrates( 644 VideoCapabilities vidCaps, MediaFormat format, int maxFps) { 645 int width = format.getInteger(MediaFormat.KEY_WIDTH); 646 int height = format.getInteger(MediaFormat.KEY_HEIGHT); 647 648 int maxWidth = vidCaps.getSupportedWidths().getUpper(); 649 int maxHeight = vidCaps.getSupportedHeightsFor(maxWidth).getUpper(); 650 int frameRate = Math.min( 651 maxFps, vidCaps.getSupportedFrameRatesFor(width, height).getUpper().intValue()); 652 format.setInteger(MediaFormat.KEY_FRAME_RATE, frameRate); 653 654 int bitrate = vidCaps.getBitrateRange().clamp( 655 (int)(vidCaps.getBitrateRange().getUpper() / 656 Math.sqrt((double)maxWidth * maxHeight / width / height))); 657 format.setInteger(MediaFormat.KEY_BIT_RATE, bitrate); 658 } 659 660 /* 661 * ------------------ HELPER METHODS FOR STATISTICS AND REPORTING ------------------ 662 */ 663 664 // TODO: migrate this into com.android.compatibility.common.util.Stat 665 public static class Stats { 666 /** does not support NaN or Inf in |data| */ Stats(double[] data)667 public Stats(double[] data) { 668 mData = data; 669 if (mData != null) { 670 mNum = mData.length; 671 } 672 } 673 getNum()674 public int getNum() { 675 return mNum; 676 } 677 678 /** calculate mSumX and mSumXX */ analyze()679 private void analyze() { 680 if (mAnalyzed) { 681 return; 682 } 683 684 if (mData != null) { 685 for (double x : mData) { 686 if (!(x >= mMinX)) { // mMinX may be NaN 687 mMinX = x; 688 } 689 if (!(x <= mMaxX)) { // mMaxX may be NaN 690 mMaxX = x; 691 } 692 mSumX += x; 693 mSumXX += x * x; 694 } 695 } 696 mAnalyzed = true; 697 } 698 699 /** returns the maximum or NaN if it does not exist */ getMin()700 public double getMin() { 701 analyze(); 702 return mMinX; 703 } 704 705 /** returns the minimum or NaN if it does not exist */ getMax()706 public double getMax() { 707 analyze(); 708 return mMaxX; 709 } 710 711 /** returns the average or NaN if it does not exist. */ getAverage()712 public double getAverage() { 713 analyze(); 714 if (mNum == 0) { 715 return Double.NaN; 716 } else { 717 return mSumX / mNum; 718 } 719 } 720 721 /** returns the standard deviation or NaN if it does not exist. */ getStdev()722 public double getStdev() { 723 analyze(); 724 if (mNum == 0) { 725 return Double.NaN; 726 } else { 727 double average = mSumX / mNum; 728 return Math.sqrt(mSumXX / mNum - average * average); 729 } 730 } 731 732 /** returns the statistics for the moving average over n values */ movingAverage(int n)733 public Stats movingAverage(int n) { 734 if (n < 1 || mNum < n) { 735 return new Stats(null); 736 } else if (n == 1) { 737 return this; 738 } 739 740 double[] avgs = new double[mNum - n + 1]; 741 double sum = 0; 742 for (int i = 0; i < mNum; ++i) { 743 sum += mData[i]; 744 if (i >= n - 1) { 745 avgs[i - n + 1] = sum / n; 746 sum -= mData[i - n + 1]; 747 } 748 } 749 return new Stats(avgs); 750 } 751 752 /** returns the statistics for the moving average over a window over the 753 * cumulative sum. Basically, moves a window from: [0, window] to 754 * [sum - window, sum] over the cumulative sum, over ((sum - window) / average) 755 * steps, and returns the average value over each window. 756 * This method is used to average time-diff data over a window of a constant time. 757 */ movingAverageOverSum(double window)758 public Stats movingAverageOverSum(double window) { 759 if (window <= 0 || mNum < 1) { 760 return new Stats(null); 761 } 762 763 analyze(); 764 double average = mSumX / mNum; 765 if (window >= mSumX) { 766 return new Stats(new double[] { average }); 767 } 768 int samples = (int)Math.ceil((mSumX - window) / average); 769 double[] avgs = new double[samples]; 770 771 // A somewhat brute force approach to calculating the moving average. 772 // TODO: add support for weights in Stats, so we can do a more refined approach. 773 double sum = 0; // sum of elements in the window 774 int num = 0; // number of elements in the moving window 775 int bi = 0; // index of the first element in the moving window 776 int ei = 0; // index of the last element in the moving window 777 double space = window; // space at the end of the window 778 double foot = 0; // space at the beginning of the window 779 780 // invariants: foot + sum + space == window 781 // bi + num == ei 782 // 783 // window: |-------------------------------| 784 // | <-----sum------> | 785 // <foot> <---space--> 786 // | | 787 // intervals: |-----------|-------|-------|--------------------|--------| 788 // ^bi ^ei 789 790 int ix = 0; // index in the result 791 while (ix < samples) { 792 // add intervals while there is space in the window 793 while (ei < mData.length && mData[ei] <= space) { 794 space -= mData[ei]; 795 sum += mData[ei]; 796 num++; 797 ei++; 798 } 799 800 // calculate average over window and deal with odds and ends (e.g. if there are no 801 // intervals in the current window: pick whichever element overlaps the window 802 // most. 803 if (num > 0) { 804 avgs[ix++] = sum / num; 805 } else if (bi > 0 && foot > space) { 806 // consider previous 807 avgs[ix++] = mData[bi - 1]; 808 } else if (ei == mData.length) { 809 break; 810 } else { 811 avgs[ix++] = mData[ei]; 812 } 813 814 // move the window to the next position 815 foot -= average; 816 space += average; 817 818 // remove intervals that are now partially or wholly outside of the window 819 while (bi < ei && foot < 0) { 820 foot += mData[bi]; 821 sum -= mData[bi]; 822 num--; 823 bi++; 824 } 825 } 826 return new Stats(Arrays.copyOf(avgs, ix)); 827 } 828 829 /** calculate mSortedData */ sort()830 private void sort() { 831 if (mSorted || mNum == 0) { 832 return; 833 } 834 mSortedData = Arrays.copyOf(mData, mNum); 835 Arrays.sort(mSortedData); 836 mSorted = true; 837 } 838 839 /** returns an array of percentiles for the points using nearest rank */ getPercentiles(double... points)840 public double[] getPercentiles(double... points) { 841 sort(); 842 double[] res = new double[points.length]; 843 for (int i = 0; i < points.length; ++i) { 844 if (mNum < 1 || points[i] < 0 || points[i] > 100) { 845 res[i] = Double.NaN; 846 } else { 847 res[i] = mSortedData[(int)Math.round(points[i] / 100 * (mNum - 1))]; 848 } 849 } 850 return res; 851 } 852 853 @Override equals(Object o)854 public boolean equals(Object o) { 855 if (o instanceof Stats) { 856 Stats other = (Stats)o; 857 if (other.mNum != mNum) { 858 return false; 859 } else if (mNum == 0) { 860 return true; 861 } 862 return Arrays.equals(mData, other.mData); 863 } 864 return false; 865 } 866 867 private double[] mData; 868 private double mSumX = 0; 869 private double mSumXX = 0; 870 private double mMinX = Double.NaN; 871 private double mMaxX = Double.NaN; 872 private int mNum = 0; 873 private boolean mAnalyzed = false; 874 private double[] mSortedData; 875 private boolean mSorted = false; 876 } 877 878 /* 879 * -------------------------------------- END -------------------------------------- 880 */ 881 } 882