• 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.media.MediaCodec;
19 import android.media.MediaCodecInfo;
20 import android.media.MediaCodecList;
21 import android.media.MediaCrypto;
22 import android.media.MediaCryptoException;
23 import android.media.MediaExtractor;
24 import android.media.MediaFormat;
25 import android.net.Uri;
26 import android.util.Log;
27 import android.view.SurfaceHolder;
28 
29 import java.io.IOException;
30 import java.util.Arrays;
31 import java.util.HashMap;
32 import java.util.Map;
33 import java.util.UUID;
34 
35 /**
36  * JB(API 16) introduces {@link MediaCodec} API.  It allows apps have more control over
37  * media playback, pushes individual frames to decoder and supports decryption via
38  * {@link MediaCrypto} API.
39  *
40  * {@link MediaDrm} can be used to obtain keys for decrypting protected media streams,
41  * in conjunction with MediaCrypto.
42  */
43 public class MediaCodecCencPlayer {
44     private static final String TAG = MediaCodecCencPlayer.class.getSimpleName();
45 
46     private static final int STATE_IDLE = 1;
47     private static final int STATE_PREPARING = 2;
48     private static final int STATE_PLAYING = 3;
49     private static final int STATE_PAUSED = 4;
50 
51     private static final UUID CLEARKEY_SCHEME_UUID =
52             new UUID(0x1077efecc0b24d02L, 0xace33c1e52e2fb4bL);
53 
54     private boolean mEncryptedAudio;
55     private boolean mEncryptedVideo;
56     private boolean mThreadStarted = false;
57     private byte[] mSessionId;
58     private CodecState mAudioTrackState;
59     private int mMediaFormatHeight;
60     private int mMediaFormatWidth;
61     private int mState;
62     private long mDeltaTimeUs;
63     private long mDurationUs;
64     private Map<Integer, CodecState> mAudioCodecStates;
65     private Map<Integer, CodecState> mVideoCodecStates;
66     private Map<String, String> mAudioHeaders;
67     private Map<String, String> mVideoHeaders;
68     private Map<UUID, byte[]> mPsshInitData;
69     private MediaCrypto mCrypto;
70     private MediaExtractor mAudioExtractor;
71     private MediaExtractor mVideoExtractor;
72     private SurfaceHolder mSurfaceHolder;
73     private Thread mThread;
74     private Uri mAudioUri;
75     private Uri mVideoUri;
76 
77     private static final byte[] PSSH = hexStringToByteArray(
78             "0000003470737368" +  // BMFF box header (4 bytes size + 'pssh')
79             "01000000" +          // Full box header (version = 1 flags = 0)
80             "1077efecc0b24d02" +  // SystemID
81             "ace33c1e52e2fb4b" +
82             "00000001" +          // Number of key ids
83             "60061e017e477e87" +  // Key id
84             "7e57d00d1ed00d1e" +
85             "00000000"            // Size of Data, must be zero
86             );
87 
88     /**
89      * Convert a hex string into byte array.
90      */
hexStringToByteArray(String s)91     private static byte[] hexStringToByteArray(String s) {
92         int len = s.length();
93         byte[] data = new byte[len / 2];
94         for (int i = 0; i < len; i += 2) {
95             data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4)
96                     + Character.digit(s.charAt(i + 1), 16));
97         }
98         return data;
99     }
100 
101     /*
102      * Media player class to stream CENC content using MediaCodec class.
103      */
MediaCodecCencPlayer(SurfaceHolder holder, byte[] sessionId)104     public MediaCodecCencPlayer(SurfaceHolder holder, byte[] sessionId) {
105         mSessionId = sessionId;
106         mSurfaceHolder = holder;
107         mState = STATE_IDLE;
108         mThread = new Thread(new Runnable() {
109             @Override
110             public void run() {
111                 while (mThreadStarted == true) {
112                     doSomeWork();
113                     if (mAudioTrackState != null) {
114                         mAudioTrackState.process();
115                     }
116                     try {
117                         Thread.sleep(5);
118                     } catch (InterruptedException ex) {
119                         Log.d(TAG, "Thread interrupted");
120                     }
121                 }
122             }
123         });
124     }
125 
setAudioDataSource(Uri uri, Map<String, String> headers, boolean encrypted)126     public void setAudioDataSource(Uri uri, Map<String, String> headers, boolean encrypted) {
127         mAudioUri = uri;
128         mAudioHeaders = headers;
129         mEncryptedAudio = encrypted;
130     }
131 
setVideoDataSource(Uri uri, Map<String, String> headers, boolean encrypted)132     public void setVideoDataSource(Uri uri, Map<String, String> headers, boolean encrypted) {
133         mVideoUri = uri;
134         mVideoHeaders = headers;
135         mEncryptedVideo = encrypted;
136     }
137 
getMediaFormatHeight()138     public final int getMediaFormatHeight() {
139         return mMediaFormatHeight;
140     }
141 
getMediaFormatWidth()142     public final int getMediaFormatWidth() {
143         return mMediaFormatWidth;
144     }
145 
getPsshInfo()146     public final Map<UUID, byte[]> getPsshInfo() {
147         // TODO (edwinwong@)
148         // Remove the if statement when we get content that has the clear key system id.
149         if (mPsshInitData == null ||
150                 (mPsshInitData != null && !mPsshInitData.containsKey(CLEARKEY_SCHEME_UUID))) {
151             mPsshInitData = new HashMap<UUID, byte[]>();
152             mPsshInitData.put(CLEARKEY_SCHEME_UUID, PSSH);
153         }
154         return mPsshInitData;
155     }
156 
prepareAudio()157     private void prepareAudio() throws IOException {
158         boolean hasAudio = false;
159         for (int i = mAudioExtractor.getTrackCount(); i-- > 0;) {
160             MediaFormat format = mAudioExtractor.getTrackFormat(i);
161             String mime = format.getString(MediaFormat.KEY_MIME);
162 
163             Log.d(TAG, "audio track #" + i + " " + format + " " + mime +
164                   " Is ADTS:" + getMediaFormatInteger(format, MediaFormat.KEY_IS_ADTS) +
165                   " Sample rate:" + getMediaFormatInteger(format, MediaFormat.KEY_SAMPLE_RATE) +
166                   " Channel count:" +
167                   getMediaFormatInteger(format, MediaFormat.KEY_CHANNEL_COUNT));
168 
169             if (!hasAudio) {
170                 mAudioExtractor.selectTrack(i);
171                 addTrack(i, format, mEncryptedAudio);
172                 hasAudio = true;
173 
174                 if (format.containsKey(MediaFormat.KEY_DURATION)) {
175                     long durationUs = format.getLong(MediaFormat.KEY_DURATION);
176 
177                     if (durationUs > mDurationUs) {
178                         mDurationUs = durationUs;
179                     }
180                     Log.d(TAG, "audio track format #" + i +
181                             " Duration:" + mDurationUs + " microseconds");
182                 }
183 
184                 if (hasAudio) {
185                     break;
186                 }
187             }
188         }
189     }
190 
prepareVideo()191     private void prepareVideo() throws IOException {
192         boolean hasVideo = false;
193 
194         for (int i = mVideoExtractor.getTrackCount(); i-- > 0;) {
195             MediaFormat format = mVideoExtractor.getTrackFormat(i);
196             String mime = format.getString(MediaFormat.KEY_MIME);
197 
198             mMediaFormatHeight = getMediaFormatInteger(format, MediaFormat.KEY_HEIGHT);
199             mMediaFormatWidth = getMediaFormatInteger(format, MediaFormat.KEY_WIDTH);
200             Log.d(TAG, "video track #" + i + " " + format + " " + mime +
201                   " Width:" + mMediaFormatWidth + ", Height:" + mMediaFormatHeight);
202 
203             if (!hasVideo) {
204                 mVideoExtractor.selectTrack(i);
205                 addTrack(i, format, mEncryptedVideo);
206 
207                 hasVideo = true;
208 
209                 if (format.containsKey(MediaFormat.KEY_DURATION)) {
210                     long durationUs = format.getLong(MediaFormat.KEY_DURATION);
211 
212                     if (durationUs > mDurationUs) {
213                         mDurationUs = durationUs;
214                     }
215                     Log.d(TAG, "track format #" + i + " Duration:" +
216                             mDurationUs + " microseconds");
217                 }
218 
219                 if (hasVideo) {
220                     break;
221                 }
222             }
223         }
224         return;
225     }
226 
prepare()227     public void prepare() throws IOException, MediaCryptoException {
228         if (null == mAudioExtractor) {
229             mAudioExtractor = new MediaExtractor();
230             if (null == mAudioExtractor) {
231                 Log.e(TAG, "Cannot create Audio extractor.");
232                 return;
233             }
234         }
235 
236         if (null == mVideoExtractor){
237             mVideoExtractor = new MediaExtractor();
238             if (null == mVideoExtractor) {
239                 Log.e(TAG, "Cannot create Video extractor.");
240                 return;
241             }
242         }
243 
244         mAudioExtractor.setDataSource(mAudioUri.toString(), mAudioHeaders);
245         mVideoExtractor.setDataSource(mVideoUri.toString(), mVideoHeaders);
246         mPsshInitData = mVideoExtractor.getPsshInfo();
247 
248         if (null == mCrypto && (mEncryptedVideo || mEncryptedAudio)) {
249             try {
250                 mCrypto = new MediaCrypto(CLEARKEY_SCHEME_UUID, mSessionId);
251             } catch (MediaCryptoException e) {
252                 reset();
253                 Log.e(TAG, "Failed to create MediaCrypto instance.");
254                 throw e;
255             }
256         } else {
257             reset();
258             mCrypto.release();
259             mCrypto = null;
260         }
261 
262         if (null == mVideoCodecStates) {
263             mVideoCodecStates = new HashMap<Integer, CodecState>();
264         } else {
265             mVideoCodecStates.clear();
266         }
267 
268         if (null == mAudioCodecStates) {
269             mAudioCodecStates = new HashMap<Integer, CodecState>();
270         } else {
271             mAudioCodecStates.clear();
272         }
273 
274         prepareVideo();
275         prepareAudio();
276 
277         mState = STATE_PAUSED;
278     }
279 
addTrack(int trackIndex, MediaFormat format, boolean encrypted)280     private void addTrack(int trackIndex, MediaFormat format,
281             boolean encrypted) throws IOException {
282         String mime = format.getString(MediaFormat.KEY_MIME);
283         boolean isVideo = mime.startsWith("video/");
284         boolean isAudio = mime.startsWith("audio/");
285 
286         MediaCodec codec;
287 
288         if (encrypted && mCrypto.requiresSecureDecoderComponent(mime)) {
289             codec = MediaCodec.createByCodecName(
290                     getSecureDecoderNameForMime(mime));
291         } else {
292             codec = MediaCodec.createDecoderByType(mime);
293         }
294 
295         codec.configure(
296                 format,
297                 isVideo ? mSurfaceHolder.getSurface() : null,
298                 mCrypto,
299                 0);
300 
301         CodecState state;
302         if (isVideo) {
303             state = new CodecState(this, mVideoExtractor, trackIndex, format, codec, true);
304             mVideoCodecStates.put(Integer.valueOf(trackIndex), state);
305         } else {
306             state = new CodecState(this, mAudioExtractor, trackIndex, format, codec, true);
307             mAudioCodecStates.put(Integer.valueOf(trackIndex), state);
308         }
309 
310         if (isAudio) {
311             mAudioTrackState = state;
312         }
313     }
314 
getMediaFormatInteger(MediaFormat format, String key)315     protected int getMediaFormatInteger(MediaFormat format, String key) {
316         return format.containsKey(key) ? format.getInteger(key) : 0;
317     }
318 
getSecureDecoderNameForMime(String mime)319     protected String getSecureDecoderNameForMime(String mime) {
320         int n = MediaCodecList.getCodecCount();
321         for (int i = 0; i < n; ++i) {
322             MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
323 
324             if (info.isEncoder()) {
325                 continue;
326             }
327 
328             String[] supportedTypes = info.getSupportedTypes();
329 
330             for (int j = 0; j < supportedTypes.length; ++j) {
331                 if (supportedTypes[j].equalsIgnoreCase(mime)) {
332                     return info.getName() + ".secure";
333                 }
334             }
335         }
336         return null;
337     }
338 
start()339     public void start() {
340         Log.d(TAG, "start");
341 
342         if (mState == STATE_PLAYING || mState == STATE_PREPARING) {
343             return;
344         } else if (mState == STATE_IDLE) {
345             mState = STATE_PREPARING;
346             return;
347         } else if (mState != STATE_PAUSED) {
348             throw new IllegalStateException();
349         }
350 
351         for (CodecState state : mVideoCodecStates.values()) {
352             state.start();
353         }
354 
355         for (CodecState state : mAudioCodecStates.values()) {
356             state.start();
357         }
358 
359         mDeltaTimeUs = -1;
360         mState = STATE_PLAYING;
361     }
362 
startWork()363     public void startWork() throws IOException, MediaCryptoException, Exception {
364         try {
365             // Just change state from STATE_IDLE to STATE_PREPARING.
366             start();
367             // Extract media information from uri asset, and change state to STATE_PAUSED.
368             prepare();
369             // Start CodecState, and change from STATE_PAUSED to STATE_PLAYING.
370             start();
371         } catch (IOException e) {
372             throw e;
373         } catch (MediaCryptoException e) {
374             throw e;
375         }
376 
377         mThreadStarted = true;
378         mThread.start();
379     }
380 
startThread()381     public void startThread() {
382         start();
383         mThreadStarted = true;
384         mThread.start();
385     }
386 
pause()387     public void pause() {
388         Log.d(TAG, "pause");
389 
390         if (mState == STATE_PAUSED) {
391             return;
392         } else if (mState != STATE_PLAYING) {
393             throw new IllegalStateException();
394         }
395 
396         for (CodecState state : mVideoCodecStates.values()) {
397             state.pause();
398         }
399 
400         for (CodecState state : mAudioCodecStates.values()) {
401             state.pause();
402         }
403 
404         mState = STATE_PAUSED;
405     }
406 
reset()407     public void reset() {
408         if (mState == STATE_PLAYING) {
409             mThreadStarted = false;
410 
411             try {
412                 mThread.join();
413             } catch (InterruptedException ex) {
414                 Log.d(TAG, "mThread.join " + ex);
415             }
416 
417             pause();
418         }
419 
420         if (mVideoCodecStates != null) {
421             for (CodecState state : mVideoCodecStates.values()) {
422                 state.release();
423             }
424             mVideoCodecStates = null;
425         }
426 
427         if (mAudioCodecStates != null) {
428             for (CodecState state : mAudioCodecStates.values()) {
429                 state.release();
430             }
431             mAudioCodecStates = null;
432         }
433 
434         if (mAudioExtractor != null) {
435             mAudioExtractor.release();
436             mAudioExtractor = null;
437         }
438 
439         if (mVideoExtractor != null) {
440             mVideoExtractor.release();
441             mVideoExtractor = null;
442         }
443 
444         if (mCrypto != null) {
445             mCrypto.release();
446             mCrypto = null;
447         }
448 
449         mDurationUs = -1;
450         mState = STATE_IDLE;
451     }
452 
isEnded()453     public boolean isEnded() {
454         for (CodecState state : mVideoCodecStates.values()) {
455           if (!state.isEnded()) {
456             return false;
457           }
458         }
459 
460         for (CodecState state : mAudioCodecStates.values()) {
461             if (!state.isEnded()) {
462               return false;
463             }
464         }
465 
466         return true;
467     }
468 
doSomeWork()469     private void doSomeWork() {
470         try {
471             for (CodecState state : mVideoCodecStates.values()) {
472                 state.doSomeWork();
473             }
474         } catch (MediaCodec.CryptoException e) {
475             throw new Error("Video CryptoException w/ errorCode "
476                     + e.getErrorCode() + ", '" + e.getMessage() + "'");
477         } catch (IllegalStateException e) {
478             throw new Error("Video CodecState.feedInputBuffer IllegalStateException " + e);
479         }
480 
481         try {
482             for (CodecState state : mAudioCodecStates.values()) {
483                 state.doSomeWork();
484             }
485         } catch (MediaCodec.CryptoException e) {
486             throw new Error("Audio CryptoException w/ errorCode "
487                     + e.getErrorCode() + ", '" + e.getMessage() + "'");
488         } catch (IllegalStateException e) {
489             throw new Error("Aduio CodecState.feedInputBuffer IllegalStateException " + e);
490         }
491 
492     }
493 
getNowUs()494     public long getNowUs() {
495         if (mAudioTrackState == null) {
496             return System.currentTimeMillis() * 1000;
497         }
498 
499         return mAudioTrackState.getAudioTimeUs();
500     }
501 
getRealTimeUsForMediaTime(long mediaTimeUs)502     public long getRealTimeUsForMediaTime(long mediaTimeUs) {
503         if (mDeltaTimeUs == -1) {
504             long nowUs = getNowUs();
505             mDeltaTimeUs = nowUs - mediaTimeUs;
506         }
507 
508         return mDeltaTimeUs + mediaTimeUs;
509     }
510 
getDuration()511     public int getDuration() {
512         return (int)((mDurationUs + 500) / 1000);
513     }
514 
getCurrentPosition()515     public int getCurrentPosition() {
516         if (mVideoCodecStates == null) {
517                 return 0;
518         }
519 
520         long positionUs = 0;
521 
522         for (CodecState state : mVideoCodecStates.values()) {
523             long trackPositionUs = state.getCurrentPositionUs();
524 
525             if (trackPositionUs > positionUs) {
526                 positionUs = trackPositionUs;
527             }
528         }
529         return (int)((positionUs + 500) / 1000);
530     }
531 
532 }
533