1 /* 2 * Copyright (C) 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.media.cts; 17 18 import android.content.Context; 19 import android.content.res.Resources; 20 import android.media.AudioManager; 21 import android.media.DrmInitData; 22 import android.media.MediaCas; 23 import android.media.MediaCasException; 24 import android.media.MediaCodec; 25 import android.media.MediaCodecInfo; 26 import android.media.MediaCodecList; 27 import android.media.MediaCrypto; 28 import android.media.MediaCryptoException; 29 import android.media.MediaDescrambler; 30 import android.media.MediaExtractor; 31 import android.media.MediaFormat; 32 import android.net.Uri; 33 import android.util.Log; 34 import android.view.Surface; 35 36 import androidx.test.InstrumentationRegistry; 37 38 import com.android.compatibility.common.util.MediaUtils; 39 40 import java.io.IOException; 41 import java.util.ArrayDeque; 42 import java.util.Arrays; 43 import java.util.Deque; 44 import java.util.HashMap; 45 import java.util.List; 46 import java.util.Map; 47 import java.util.UUID; 48 49 /** 50 * JB(API 16) introduces {@link MediaCodec} API. It allows apps have more control over 51 * media playback, pushes individual frames to decoder and supports decryption via 52 * {@link MediaCrypto} API. 53 * 54 * {@link MediaDrm} can be used to obtain keys for decrypting protected media streams, 55 * in conjunction with MediaCrypto. 56 */ 57 public class MediaCodecClearKeyPlayer implements MediaTimeProvider { 58 private static final String TAG = MediaCodecClearKeyPlayer.class.getSimpleName(); 59 60 private static final String FILE_SCHEME = "file://"; 61 62 private static final int STATE_IDLE = 1; 63 private static final int STATE_PREPARING = 2; 64 private static final int STATE_PLAYING = 3; 65 private static final int STATE_PAUSED = 4; 66 67 private static final UUID CLEARKEY_SCHEME_UUID = 68 new UUID(0x1077efecc0b24d02L, 0xace33c1e52e2fb4bL); 69 70 private boolean mEncryptedAudio; 71 private boolean mEncryptedVideo; 72 private volatile boolean mThreadStarted = false; 73 private byte[] mSessionId; 74 private boolean mScrambled; 75 private CodecState mAudioTrackState; 76 private int mMediaFormatHeight; 77 private int mMediaFormatWidth; 78 private int mState; 79 private long mDeltaTimeUs; 80 private long mDurationUs; 81 private Map<Integer, CodecState> mAudioCodecStates; 82 private Map<Integer, CodecState> mVideoCodecStates; 83 private Map<String, String> mAudioHeaders; 84 private Map<String, String> mVideoHeaders; 85 private Map<UUID, byte[]> mPsshInitData; 86 private MediaCrypto mCrypto; 87 private MediaCas mMediaCas; 88 private MediaDescrambler mAudioDescrambler; 89 private MediaDescrambler mVideoDescrambler; 90 private MediaExtractor mAudioExtractor; 91 private MediaExtractor mVideoExtractor; 92 private Deque<Surface> mSurfaces; 93 private Thread mThread; 94 private Uri mAudioUri; 95 private Uri mVideoUri; 96 private Context mContext; 97 private Resources mResources; 98 private Error mErrorFromThread; 99 100 private static final byte[] PSSH = hexStringToByteArray( 101 // BMFF box header (4 bytes size + 'pssh') 102 "0000003470737368" + 103 // Full box header (version = 1 flags = 0 104 "01000000" + 105 // SystemID 106 "1077efecc0b24d02ace33c1e52e2fb4b" + 107 // Number of key ids 108 "00000001" + 109 // Key id 110 "30303030303030303030303030303030" + 111 // size of data, must be zero 112 "00000000"); 113 114 // ClearKey CAS/Descrambler test provision string 115 private static final String sProvisionStr = 116 "{ " + 117 " \"id\": 21140844, " + 118 " \"name\": \"Test Title\", " + 119 " \"lowercase_organization_name\": \"Android\", " + 120 " \"asset_key\": { " + 121 " \"encryption_key\": \"nezAr3CHFrmBR9R8Tedotw==\" " + 122 " }, " + 123 " \"cas_type\": 1, " + 124 " \"track_types\": [ ] " + 125 "} " ; 126 127 // ClearKey private data (0-bytes of length 4) 128 private static final byte[] sCasPrivateInfo = hexStringToByteArray("00000000"); 129 130 /** 131 * Convert a hex string into byte array. 132 */ hexStringToByteArray(String s)133 private static byte[] hexStringToByteArray(String s) { 134 int len = s.length(); 135 byte[] data = new byte[len / 2]; 136 for (int i = 0; i < len; i += 2) { 137 data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) 138 + Character.digit(s.charAt(i + 1), 16)); 139 } 140 return data; 141 } 142 143 /* 144 * Media player class to stream CENC content using MediaCodec class. 145 */ MediaCodecClearKeyPlayer( List<Surface> surfaces, byte[] sessionId, boolean scrambled, Context context)146 public MediaCodecClearKeyPlayer( 147 List<Surface> surfaces, byte[] sessionId, boolean scrambled, Context context) { 148 mSessionId = sessionId; 149 mScrambled = scrambled; 150 mSurfaces = new ArrayDeque<>(surfaces); 151 mContext = context; 152 mResources = context.getResources(); 153 mState = STATE_IDLE; 154 mThread = new Thread(new Runnable() { 155 @Override 156 public void run() { 157 int n = 0; 158 while (mThreadStarted == true) { 159 doSomeWork(); 160 if (mAudioTrackState != null) { 161 mAudioTrackState.processAudioTrack(); 162 } 163 try { 164 Thread.sleep(5); 165 } catch (InterruptedException ex) { 166 Log.d(TAG, "Thread interrupted"); 167 } 168 if(++n % 1000 == 0) { 169 cycleSurfaces(); 170 } 171 } 172 if (mAudioTrackState != null) { 173 mAudioTrackState.stopAudioTrack(); 174 } 175 } 176 }); 177 } 178 setAudioDataSource(Uri uri, Map<String, String> headers, boolean encrypted)179 public void setAudioDataSource(Uri uri, Map<String, String> headers, boolean encrypted) { 180 mAudioUri = uri; 181 mAudioHeaders = headers; 182 mEncryptedAudio = encrypted; 183 } 184 setVideoDataSource(Uri uri, Map<String, String> headers, boolean encrypted)185 public void setVideoDataSource(Uri uri, Map<String, String> headers, boolean encrypted) { 186 mVideoUri = uri; 187 mVideoHeaders = headers; 188 mEncryptedVideo = encrypted; 189 } 190 getMediaFormatHeight()191 public final int getMediaFormatHeight() { 192 return mMediaFormatHeight; 193 } 194 getMediaFormatWidth()195 public final int getMediaFormatWidth() { 196 return mMediaFormatWidth; 197 } 198 getDrmInitData()199 public final byte[] getDrmInitData() { 200 for (MediaExtractor ex: new MediaExtractor[] {mVideoExtractor, mAudioExtractor}) { 201 DrmInitData drmInitData = ex.getDrmInitData(); 202 if (drmInitData != null) { 203 DrmInitData.SchemeInitData schemeInitData = drmInitData.get(CLEARKEY_SCHEME_UUID); 204 if (schemeInitData != null && schemeInitData.data != null) { 205 // llama content still does not contain pssh data, return hard coded PSSH 206 return (schemeInitData.data.length > 1)? schemeInitData.data : PSSH; 207 } 208 } 209 } 210 // Should not happen after we get content that has the clear key system id. 211 return PSSH; 212 } 213 prepareAudio()214 private void prepareAudio() throws IOException, MediaCasException { 215 boolean hasAudio = false; 216 for (int i = mAudioExtractor.getTrackCount(); i-- > 0;) { 217 MediaFormat format = mAudioExtractor.getTrackFormat(i); 218 String mime = format.getString(MediaFormat.KEY_MIME); 219 if (!mime.startsWith("audio/")) { 220 continue; 221 } 222 223 Log.d(TAG, "audio track #" + i + " " + format + " " + mime + 224 " Is ADTS:" + getMediaFormatInteger(format, MediaFormat.KEY_IS_ADTS) + 225 " Sample rate:" + getMediaFormatInteger(format, MediaFormat.KEY_SAMPLE_RATE) + 226 " Channel count:" + 227 getMediaFormatInteger(format, MediaFormat.KEY_CHANNEL_COUNT)); 228 229 if (mScrambled) { 230 MediaExtractor.CasInfo casInfo = mAudioExtractor.getCasInfo(i); 231 if (casInfo != null && casInfo.getSession() != null) { 232 mAudioDescrambler = new MediaDescrambler(casInfo.getSystemId()); 233 mAudioDescrambler.setMediaCasSession(casInfo.getSession()); 234 } 235 } 236 237 if (!hasAudio) { 238 mAudioExtractor.selectTrack(i); 239 addTrack(i, format, mEncryptedAudio); 240 hasAudio = true; 241 242 if (format.containsKey(MediaFormat.KEY_DURATION)) { 243 long durationUs = format.getLong(MediaFormat.KEY_DURATION); 244 245 if (durationUs > mDurationUs) { 246 mDurationUs = durationUs; 247 } 248 Log.d(TAG, "audio track format #" + i + 249 " Duration:" + mDurationUs + " microseconds"); 250 } 251 252 if (hasAudio) { 253 break; 254 } 255 } 256 } 257 } 258 prepareVideo()259 private void prepareVideo() throws IOException, MediaCasException { 260 boolean hasVideo = false; 261 262 for (int i = mVideoExtractor.getTrackCount(); i-- > 0;) { 263 MediaFormat format = mVideoExtractor.getTrackFormat(i); 264 String mime = format.getString(MediaFormat.KEY_MIME); 265 if (!mime.startsWith("video/")) { 266 continue; 267 } 268 269 mMediaFormatHeight = getMediaFormatInteger(format, MediaFormat.KEY_HEIGHT); 270 mMediaFormatWidth = getMediaFormatInteger(format, MediaFormat.KEY_WIDTH); 271 Log.d(TAG, "video track #" + i + " " + format + " " + mime + 272 " Width:" + mMediaFormatWidth + ", Height:" + mMediaFormatHeight); 273 274 if (mScrambled) { 275 MediaExtractor.CasInfo casInfo = mVideoExtractor.getCasInfo(i); 276 if (casInfo != null && casInfo.getSession() != null) { 277 mVideoDescrambler = new MediaDescrambler(casInfo.getSystemId()); 278 mVideoDescrambler.setMediaCasSession(casInfo.getSession()); 279 } 280 } 281 282 if (!hasVideo) { 283 mVideoExtractor.selectTrack(i); 284 addTrack(i, format, mEncryptedVideo); 285 286 hasVideo = true; 287 288 if (format.containsKey(MediaFormat.KEY_DURATION)) { 289 long durationUs = format.getLong(MediaFormat.KEY_DURATION); 290 291 if (durationUs > mDurationUs) { 292 mDurationUs = durationUs; 293 } 294 Log.d(TAG, "track format #" + i + " Duration:" + 295 mDurationUs + " microseconds"); 296 } 297 298 if (hasVideo) { 299 break; 300 } 301 } 302 } 303 } 304 initCasAndDescrambler(MediaExtractor extractor)305 private boolean initCasAndDescrambler(MediaExtractor extractor) throws MediaCasException { 306 int trackCount = extractor.getTrackCount(); 307 for (int trackId = 0; trackId < trackCount; trackId++) { 308 android.media.MediaFormat format = extractor.getTrackFormat(trackId); 309 String mime = format.getString(android.media.MediaFormat.KEY_MIME); 310 Log.d(TAG, "track "+ trackId + ": " + mime); 311 if (MediaFormat.MIMETYPE_VIDEO_SCRAMBLED.equals(mime) || 312 MediaFormat.MIMETYPE_AUDIO_SCRAMBLED.equals(mime)) { 313 MediaExtractor.CasInfo casInfo = extractor.getCasInfo(trackId); 314 if (casInfo != null) { 315 if (!Arrays.equals(sCasPrivateInfo, casInfo.getPrivateData())) { 316 throw new Error("Cas private data mismatch"); 317 } 318 // Need MANAGE_USERS or CREATE_USERS permission to access 319 // ActivityManager#getCurrentUse in MediaCas, then adopt it from shell. 320 InstrumentationRegistry 321 .getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(); 322 try { 323 mMediaCas = new MediaCas(casInfo.getSystemId()); 324 } finally { 325 InstrumentationRegistry 326 .getInstrumentation().getUiAutomation().dropShellPermissionIdentity(); 327 } 328 329 mMediaCas.provision(sProvisionStr); 330 if (mMediaCas.isAidlHal()) { 331 MediaUtils.skipTest( 332 TAG, "setMediaCas is deprecated and not supported with AIDL HAL"); 333 // If AIDL CAS service is being used, then setMediaCas will not work. 334 return false; 335 } 336 extractor.setMediaCas(mMediaCas); 337 break; 338 } 339 } 340 } 341 return true; 342 } 343 prepare()344 public boolean prepare() throws IOException, MediaCryptoException, MediaCasException { 345 if (null == mCrypto && (mEncryptedVideo || mEncryptedAudio)) { 346 try { 347 byte[] initData = new byte[0]; 348 mCrypto = new MediaCrypto(CLEARKEY_SCHEME_UUID, initData); 349 } catch (MediaCryptoException e) { 350 reset(); 351 Log.e(TAG, "Failed to create MediaCrypto instance."); 352 throw e; 353 } 354 mCrypto.setMediaDrmSession(mSessionId); 355 } else { 356 reset(); 357 } 358 359 if (null == mAudioExtractor) { 360 mAudioExtractor = new MediaExtractor(); 361 if (null == mAudioExtractor) { 362 Log.e(TAG, "Cannot create Audio extractor."); 363 return false; 364 } 365 } 366 mAudioExtractor.setDataSource(mContext, mAudioUri, mAudioHeaders); 367 368 if (mScrambled) { 369 if (!initCasAndDescrambler(mAudioExtractor)) { 370 return false; 371 } 372 mVideoExtractor = mAudioExtractor; 373 } else { 374 if (null == mVideoExtractor){ 375 mVideoExtractor = new MediaExtractor(); 376 if (null == mVideoExtractor) { 377 Log.e(TAG, "Cannot create Video extractor."); 378 return false; 379 } 380 } 381 mVideoExtractor.setDataSource(mContext, mVideoUri, mVideoHeaders); 382 } 383 384 if (null == mVideoCodecStates) { 385 mVideoCodecStates = new HashMap<Integer, CodecState>(); 386 } else { 387 mVideoCodecStates.clear(); 388 } 389 390 if (null == mAudioCodecStates) { 391 mAudioCodecStates = new HashMap<Integer, CodecState>(); 392 } else { 393 mAudioCodecStates.clear(); 394 } 395 396 prepareVideo(); 397 prepareAudio(); 398 399 mState = STATE_PAUSED; 400 return true; 401 } 402 addTrack(int trackIndex, MediaFormat format, boolean encrypted)403 private void addTrack(int trackIndex, MediaFormat format, 404 boolean encrypted) throws IOException { 405 String mime = format.getString(MediaFormat.KEY_MIME); 406 boolean isVideo = mime.startsWith("video/"); 407 boolean isAudio = mime.startsWith("audio/"); 408 409 MediaCodec codec; 410 411 if (encrypted && mCrypto.requiresSecureDecoderComponent(mime)) { 412 codec = MediaCodec.createByCodecName( 413 getSecureDecoderNameForMime(mime)); 414 } else { 415 codec = MediaCodec.createDecoderByType(mime); 416 } 417 418 if (!mScrambled) { 419 codec.configure( 420 format, 421 isVideo ? mSurfaces.getFirst() : null, 422 mCrypto, 423 0); 424 } else { 425 codec.configure( 426 format, 427 isVideo ? mSurfaces.getFirst() : null, 428 0, 429 isVideo ? mVideoDescrambler : mAudioDescrambler); 430 } 431 432 CodecState state; 433 if (isVideo) { 434 state = new CodecState((MediaTimeProvider)this, mVideoExtractor, 435 trackIndex, format, codec, true, false, 436 AudioManager.AUDIO_SESSION_ID_GENERATE); 437 mVideoCodecStates.put(Integer.valueOf(trackIndex), state); 438 } else { 439 state = new CodecState((MediaTimeProvider)this, mAudioExtractor, 440 trackIndex, format, codec, true, false, 441 AudioManager.AUDIO_SESSION_ID_GENERATE); 442 mAudioCodecStates.put(Integer.valueOf(trackIndex), state); 443 } 444 445 if (isAudio) { 446 mAudioTrackState = state; 447 } 448 } 449 getMediaFormatInteger(MediaFormat format, String key)450 protected int getMediaFormatInteger(MediaFormat format, String key) { 451 return format.containsKey(key) ? format.getInteger(key) : 0; 452 } 453 454 // Find first secure decoder for media type. If none found, return 455 // the name of the first regular codec with ".secure" suffix added. 456 // If all else fails, return null. getSecureDecoderNameForMime(String mime)457 protected String getSecureDecoderNameForMime(String mime) { 458 String firstDecoderName = null; 459 int n = MediaCodecList.getCodecCount(); 460 for (int i = 0; i < n; ++i) { 461 MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); 462 463 if (info.isEncoder()) { 464 continue; 465 } 466 467 String[] supportedTypes = info.getSupportedTypes(); 468 469 for (int j = 0; j < supportedTypes.length; ++j) { 470 if (supportedTypes[j].equalsIgnoreCase(mime)) { 471 if (info.getCapabilitiesForType(mime).isFeatureSupported( 472 MediaCodecInfo.CodecCapabilities.FEATURE_AdaptivePlayback)) { 473 return info.getName(); 474 } else if (firstDecoderName == null) { 475 firstDecoderName = info.getName(); 476 } 477 } 478 } 479 } 480 if (firstDecoderName != null) { 481 return firstDecoderName + ".secure"; 482 } 483 return null; 484 } 485 start()486 public void start() { 487 Log.d(TAG, "start"); 488 489 if (mState == STATE_PLAYING || mState == STATE_PREPARING) { 490 return; 491 } else if (mState == STATE_IDLE) { 492 mState = STATE_PREPARING; 493 return; 494 } else if (mState != STATE_PAUSED) { 495 throw new IllegalStateException(); 496 } 497 498 for (CodecState state : mVideoCodecStates.values()) { 499 state.startCodec(); 500 state.play(); 501 } 502 503 for (CodecState state : mAudioCodecStates.values()) { 504 state.startCodec(); 505 state.play(); 506 } 507 508 mDeltaTimeUs = -1; 509 mState = STATE_PLAYING; 510 } 511 startWork()512 public void startWork() throws IOException, MediaCryptoException, Exception { 513 try { 514 // Just change state from STATE_IDLE to STATE_PREPARING. 515 start(); 516 // Extract media information from uri asset, and change state to STATE_PAUSED. 517 if (!prepare()) { 518 Log.d(TAG, "Could not prepare player."); 519 return; 520 } 521 // Start CodecState, and change from STATE_PAUSED to STATE_PLAYING. 522 start(); 523 } catch (IOException e) { 524 throw e; 525 } catch (MediaCryptoException e) { 526 throw e; 527 } 528 529 mThreadStarted = true; 530 mThread.start(); 531 } 532 startThread()533 public void startThread() { 534 start(); 535 mThreadStarted = true; 536 mThread.start(); 537 } 538 pause()539 public void pause() { 540 Log.d(TAG, "pause"); 541 542 if (mState == STATE_PAUSED) { 543 return; 544 } else if (mState != STATE_PLAYING) { 545 throw new IllegalStateException(); 546 } 547 548 for (CodecState state : mVideoCodecStates.values()) { 549 state.pause(); 550 } 551 552 for (CodecState state : mAudioCodecStates.values()) { 553 state.pause(); 554 } 555 556 mState = STATE_PAUSED; 557 } 558 reset()559 public void reset() { 560 if (mState == STATE_PLAYING) { 561 mThreadStarted = false; 562 563 try { 564 mThread.join(); 565 } catch (InterruptedException ex) { 566 Log.d(TAG, "mThread.join " + ex); 567 } 568 569 pause(); 570 } 571 572 if (mVideoCodecStates != null) { 573 for (CodecState state : mVideoCodecStates.values()) { 574 state.release(); 575 } 576 mVideoCodecStates = null; 577 } 578 579 if (mAudioCodecStates != null) { 580 for (CodecState state : mAudioCodecStates.values()) { 581 state.release(); 582 } 583 mAudioCodecStates = null; 584 } 585 586 if (mAudioExtractor != null) { 587 mAudioExtractor.release(); 588 mAudioExtractor = null; 589 } 590 591 if (mVideoExtractor != null) { 592 mVideoExtractor.release(); 593 mVideoExtractor = null; 594 } 595 596 if (mCrypto != null) { 597 mCrypto.release(); 598 mCrypto = null; 599 } 600 601 if (mMediaCas != null) { 602 mMediaCas.close(); 603 mMediaCas = null; 604 } 605 606 if (mAudioDescrambler != null) { 607 mAudioDescrambler.close(); 608 mAudioDescrambler = null; 609 } 610 611 if (mVideoDescrambler != null) { 612 mVideoDescrambler.close(); 613 mVideoDescrambler = null; 614 } 615 616 mDurationUs = -1; 617 mState = STATE_IDLE; 618 } 619 isEnded()620 public boolean isEnded() { 621 if (mErrorFromThread != null) { 622 throw mErrorFromThread; 623 } 624 for (CodecState state : mVideoCodecStates.values()) { 625 if (!state.isEnded()) { 626 return false; 627 } 628 } 629 630 for (CodecState state : mAudioCodecStates.values()) { 631 if (!state.isEnded()) { 632 return false; 633 } 634 } 635 636 return true; 637 } 638 doSomeWork()639 private void doSomeWork() { 640 try { 641 for (CodecState state : mVideoCodecStates.values()) { 642 state.doSomeWork(); 643 } 644 } catch (MediaCodec.CryptoException e) { 645 mErrorFromThread = new Error("Video CryptoException w/ errorCode " 646 + e.getErrorCode() + ", '" + e.getMessage() + "'"); 647 return; 648 } catch (IllegalStateException e) { 649 mErrorFromThread = 650 new Error("Video CodecState.feedInputBuffer IllegalStateException " + e); 651 return; 652 } 653 654 try { 655 for (CodecState state : mAudioCodecStates.values()) { 656 state.doSomeWork(); 657 } 658 } catch (MediaCodec.CryptoException e) { 659 mErrorFromThread = new Error("Audio CryptoException w/ errorCode " 660 + e.getErrorCode() + ", '" + e.getMessage() + "'"); 661 return; 662 } catch (IllegalStateException e) { 663 mErrorFromThread = 664 new Error("Audio CodecState.feedInputBuffer IllegalStateException " + e); 665 return; 666 } 667 } 668 cycleSurfaces()669 private void cycleSurfaces() { 670 if (mSurfaces.size() > 1) { 671 final Surface s = mSurfaces.removeFirst(); 672 mSurfaces.addLast(s); 673 for (CodecState c : mVideoCodecStates.values()) { 674 c.setOutputSurface(mSurfaces.getFirst()); 675 /* 676 * Calling InputSurface.clearSurface on an old `output` surface because after 677 * MediaCodec has rendered to the old output surface, we need `edit` 678 * (i.e. draw black on) the old output surface. 679 */ 680 InputSurface.clearSurface(s); 681 break; 682 } 683 } 684 } 685 getNowUs()686 public long getNowUs() { 687 if (mAudioTrackState == null) { 688 return System.currentTimeMillis() * 1000; 689 } 690 691 return mAudioTrackState.getAudioTimeUs(); 692 } 693 getRealTimeUsForMediaTime(long mediaTimeUs)694 public long getRealTimeUsForMediaTime(long mediaTimeUs) { 695 if (mDeltaTimeUs == -1) { 696 long nowUs = getNowUs(); 697 mDeltaTimeUs = nowUs - mediaTimeUs; 698 } 699 700 return mDeltaTimeUs + mediaTimeUs; 701 } 702 getDuration()703 public int getDuration() { 704 return (int)((mDurationUs + 500) / 1000); 705 } 706 getCurrentPosition()707 public int getCurrentPosition() { 708 if (mVideoCodecStates == null) { 709 return 0; 710 } 711 712 long positionUs = 0; 713 714 for (CodecState state : mVideoCodecStates.values()) { 715 long trackPositionUs = state.getCurrentPositionUs(); 716 717 if (trackPositionUs > positionUs) { 718 positionUs = trackPositionUs; 719 } 720 } 721 return (int)((positionUs + 500) / 1000); 722 } 723 724 } 725