• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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