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