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