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