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.MediaExtractor; 22 import android.media.MediaFormat; 23 import android.net.Uri; 24 import android.util.Log; 25 import android.view.SurfaceHolder; 26 27 import java.io.IOException; 28 import java.util.Arrays; 29 import java.util.HashMap; 30 import java.util.Map; 31 import java.util.UUID; 32 33 /** 34 * JB(API 21) introduces {@link MediaCodec} tunneled mode API. It allows apps 35 * to use MediaCodec to delegate their Audio/Video rendering to a vendor provided 36 * Codec component. 37 */ 38 public class MediaCodecTunneledPlayer implements MediaTimeProvider { 39 private static final String TAG = MediaCodecTunneledPlayer.class.getSimpleName(); 40 41 private static final int STATE_IDLE = 1; 42 private static final int STATE_PREPARING = 2; 43 private static final int STATE_PLAYING = 3; 44 private static final int STATE_PAUSED = 4; 45 46 private Boolean mThreadStarted = false; 47 private byte[] mSessionId; 48 private CodecState mAudioTrackState; 49 private int mMediaFormatHeight; 50 private int mMediaFormatWidth; 51 private Integer mState; 52 private long mDeltaTimeUs; 53 private long mDurationUs; 54 private Map<Integer, CodecState> mAudioCodecStates; 55 private Map<Integer, CodecState> mVideoCodecStates; 56 private Map<String, String> mAudioHeaders; 57 private Map<String, String> mVideoHeaders; 58 private MediaExtractor mAudioExtractor; 59 private MediaExtractor mVideoExtractor; 60 private SurfaceHolder mSurfaceHolder; 61 private Thread mThread; 62 private Uri mAudioUri; 63 private Uri mVideoUri; 64 private boolean mTunneled; 65 private int mAudioSessionId; 66 67 /* 68 * Media player class to playback video using tunneled MediaCodec. 69 */ 70 public MediaCodecTunneledPlayer(SurfaceHolder holder, boolean tunneled, int AudioSessionId) { 71 mSurfaceHolder = holder; 72 mTunneled = tunneled; 73 mAudioTrackState = null; 74 mState = STATE_IDLE; 75 mAudioSessionId = AudioSessionId; 76 mThread = new Thread(new Runnable() { 77 @Override 78 public void run() { 79 while (true) { 80 synchronized (mThreadStarted) { 81 if (mThreadStarted == false) { 82 break; 83 } 84 } 85 synchronized (mState) { 86 if (mState == STATE_PLAYING) { 87 doSomeWork(); 88 if (mAudioTrackState != null) { 89 mAudioTrackState.process(); 90 } 91 } 92 } 93 try { 94 Thread.sleep(5); 95 } catch (InterruptedException ex) { 96 Log.d(TAG, "Thread interrupted"); 97 } 98 } 99 } 100 }); 101 } 102 103 public void setAudioDataSource(Uri uri, Map<String, String> headers) { 104 mAudioUri = uri; 105 mAudioHeaders = headers; 106 } 107 108 public void setVideoDataSource(Uri uri, Map<String, String> headers) { 109 mVideoUri = uri; 110 mVideoHeaders = headers; 111 } 112 113 public final int getMediaFormatHeight() { 114 return mMediaFormatHeight; 115 } 116 117 public final int getMediaFormatWidth() { 118 return mMediaFormatWidth; 119 } 120 121 private boolean prepareAudio() throws IOException { 122 for (int i = mAudioExtractor.getTrackCount(); i-- > 0;) { 123 MediaFormat format = mAudioExtractor.getTrackFormat(i); 124 String mime = format.getString(MediaFormat.KEY_MIME); 125 126 if (!mime.startsWith("audio/")) { 127 continue; 128 } 129 130 Log.d(TAG, "audio track #" + i + " " + format + " " + mime + 131 " Is ADTS:" + getMediaFormatInteger(format, MediaFormat.KEY_IS_ADTS) + 132 " Sample rate:" + getMediaFormatInteger(format, MediaFormat.KEY_SAMPLE_RATE) + 133 " Channel count:" + 134 getMediaFormatInteger(format, MediaFormat.KEY_CHANNEL_COUNT)); 135 136 mAudioExtractor.selectTrack(i); 137 if (!addTrack(i, format)) { 138 Log.e(TAG, "prepareAudio - addTrack() failed!"); 139 return false; 140 } 141 142 if (format.containsKey(MediaFormat.KEY_DURATION)) { 143 long durationUs = format.getLong(MediaFormat.KEY_DURATION); 144 145 if (durationUs > mDurationUs) { 146 mDurationUs = durationUs; 147 } 148 Log.d(TAG, "audio track format #" + i + 149 " Duration:" + mDurationUs + " microseconds"); 150 } 151 } 152 return true; 153 } 154 155 private boolean prepareVideo() throws IOException { 156 for (int i = mVideoExtractor.getTrackCount(); i-- > 0;) { 157 MediaFormat format = mVideoExtractor.getTrackFormat(i); 158 String mime = format.getString(MediaFormat.KEY_MIME); 159 160 if (!mime.startsWith("video/")) { 161 continue; 162 } 163 164 mMediaFormatHeight = getMediaFormatInteger(format, MediaFormat.KEY_HEIGHT); 165 mMediaFormatWidth = getMediaFormatInteger(format, MediaFormat.KEY_WIDTH); 166 Log.d(TAG, "video track #" + i + " " + format + " " + mime + 167 " Width:" + mMediaFormatWidth + ", Height:" + mMediaFormatHeight); 168 169 mVideoExtractor.selectTrack(i); 170 if (!addTrack(i, format)) { 171 Log.e(TAG, "prepareVideo - addTrack() failed!"); 172 return false; 173 } 174 175 if (format.containsKey(MediaFormat.KEY_DURATION)) { 176 long durationUs = format.getLong(MediaFormat.KEY_DURATION); 177 178 if (durationUs > mDurationUs) { 179 mDurationUs = durationUs; 180 } 181 Log.d(TAG, "track format #" + i + " Duration:" + 182 mDurationUs + " microseconds"); 183 } 184 } 185 return true; 186 } 187 188 public boolean prepare() throws IOException { 189 if (null == mAudioExtractor) { 190 mAudioExtractor = new MediaExtractor(); 191 if (null == mAudioExtractor) { 192 Log.e(TAG, "prepare - Cannot create Audio extractor."); 193 return false; 194 } 195 } 196 197 if (null == mVideoExtractor){ 198 mVideoExtractor = new MediaExtractor(); 199 if (null == mVideoExtractor) { 200 Log.e(TAG, "prepare - Cannot create Video extractor."); 201 return false; 202 } 203 } 204 205 mAudioExtractor.setDataSource(mAudioUri.toString(), mAudioHeaders); 206 mVideoExtractor.setDataSource(mVideoUri.toString(), mVideoHeaders); 207 208 if (null == mVideoCodecStates) { 209 mVideoCodecStates = new HashMap<Integer, CodecState>(); 210 } else { 211 mVideoCodecStates.clear(); 212 } 213 214 if (null == mAudioCodecStates) { 215 mAudioCodecStates = new HashMap<Integer, CodecState>(); 216 } else { 217 mAudioCodecStates.clear(); 218 } 219 220 if (!prepareAudio()) { 221 Log.e(TAG,"prepare - prepareAudio() failed!"); 222 return false; 223 } 224 if (!prepareVideo()) { 225 Log.e(TAG,"prepare - prepareVideo() failed!"); 226 return false; 227 } 228 229 synchronized (mState) { 230 mState = STATE_PAUSED; 231 } 232 return true; 233 } 234 235 private boolean addTrack(int trackIndex, MediaFormat format) throws IOException { 236 String mime = format.getString(MediaFormat.KEY_MIME); 237 boolean isVideo = mime.startsWith("video/"); 238 boolean isAudio = mime.startsWith("audio/"); 239 MediaCodec codec; 240 241 // setup tunneled video codec if needed 242 if (isVideo && mTunneled) { 243 format.setFeatureEnabled(MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback, 244 true); 245 MediaCodecList mcl = new MediaCodecList(MediaCodecList.ALL_CODECS); 246 String codecName = mcl.findDecoderForFormat(format); 247 if (codecName == null) { 248 Log.e(TAG,"addTrack - Could not find Tunneled playback codec for "+mime+ 249 " format!"); 250 return false; 251 } 252 253 codec = MediaCodec.createByCodecName(codecName); 254 if (codec == null) { 255 Log.e(TAG, "addTrack - Could not create Tunneled playback codec "+ 256 codecName+"!"); 257 return false; 258 } 259 260 if (mAudioTrackState != null) { 261 format.setInteger(MediaFormat.KEY_AUDIO_SESSION_ID, mAudioSessionId); 262 } 263 } 264 else { 265 codec = MediaCodec.createDecoderByType(mime); 266 if (codec == null) { 267 Log.e(TAG, "addTrack - Could not create regular playback codec for mime "+ 268 mime+"!"); 269 return false; 270 } 271 } 272 codec.configure( 273 format, 274 isVideo ? mSurfaceHolder.getSurface() : null, null, 0); 275 276 CodecState state; 277 if (isVideo) { 278 state = new CodecState((MediaTimeProvider)this, mVideoExtractor, 279 trackIndex, format, codec, true, mTunneled, mAudioSessionId); 280 mVideoCodecStates.put(Integer.valueOf(trackIndex), state); 281 } else { 282 state = new CodecState((MediaTimeProvider)this, mAudioExtractor, 283 trackIndex, format, codec, true, mTunneled, mAudioSessionId); 284 mAudioCodecStates.put(Integer.valueOf(trackIndex), state); 285 } 286 287 if (isAudio) { 288 mAudioTrackState = state; 289 } 290 291 return true; 292 } 293 294 protected int getMediaFormatInteger(MediaFormat format, String key) { 295 return format.containsKey(key) ? format.getInteger(key) : 0; 296 } 297 298 public boolean start() { 299 Log.d(TAG, "start"); 300 301 synchronized (mState) { 302 if (mState == STATE_PLAYING || mState == STATE_PREPARING) { 303 return true; 304 } else if (mState == STATE_IDLE) { 305 mState = STATE_PREPARING; 306 return true; 307 } else if (mState != STATE_PAUSED) { 308 throw new IllegalStateException(); 309 } 310 311 for (CodecState state : mVideoCodecStates.values()) { 312 state.start(); 313 } 314 315 for (CodecState state : mAudioCodecStates.values()) { 316 state.start(); 317 } 318 319 mDeltaTimeUs = -1; 320 mState = STATE_PLAYING; 321 } 322 return false; 323 } 324 325 public void startWork() throws IOException, Exception { 326 try { 327 // Just change state from STATE_IDLE to STATE_PREPARING. 328 start(); 329 // Extract media information from uri asset, and change state to STATE_PAUSED. 330 prepare(); 331 // Start CodecState, and change from STATE_PAUSED to STATE_PLAYING. 332 start(); 333 } catch (IOException e) { 334 throw e; 335 } 336 337 synchronized (mThreadStarted) { 338 mThreadStarted = true; 339 mThread.start(); 340 } 341 } 342 343 public void startThread() { 344 start(); 345 synchronized (mThreadStarted) { 346 mThreadStarted = true; 347 mThread.start(); 348 } 349 } 350 351 public void pause() { 352 Log.d(TAG, "pause"); 353 354 synchronized (mState) { 355 if (mState == STATE_PAUSED) { 356 return; 357 } else if (mState != STATE_PLAYING) { 358 throw new IllegalStateException(); 359 } 360 361 for (CodecState state : mVideoCodecStates.values()) { 362 state.pause(); 363 } 364 365 for (CodecState state : mAudioCodecStates.values()) { 366 state.pause(); 367 } 368 369 mState = STATE_PAUSED; 370 } 371 } 372 373 public void flush() { 374 Log.d(TAG, "flush"); 375 376 synchronized (mState) { 377 if (mState == STATE_PLAYING || mState == STATE_PREPARING) { 378 return; 379 } 380 381 for (CodecState state : mAudioCodecStates.values()) { 382 state.flush(); 383 } 384 385 for (CodecState state : mVideoCodecStates.values()) { 386 state.flush(); 387 } 388 } 389 } 390 391 public void reset() { 392 synchronized (mState) { 393 if (mState == STATE_PLAYING) { 394 pause(); 395 } 396 if (mVideoCodecStates != null) { 397 for (CodecState state : mVideoCodecStates.values()) { 398 state.release(); 399 } 400 mVideoCodecStates = null; 401 } 402 403 if (mAudioCodecStates != null) { 404 for (CodecState state : mAudioCodecStates.values()) { 405 state.release(); 406 } 407 mAudioCodecStates = null; 408 } 409 410 if (mAudioExtractor != null) { 411 mAudioExtractor.release(); 412 mAudioExtractor = null; 413 } 414 415 if (mVideoExtractor != null) { 416 mVideoExtractor.release(); 417 mVideoExtractor = null; 418 } 419 420 mDurationUs = -1; 421 mState = STATE_IDLE; 422 } 423 synchronized (mThreadStarted) { 424 mThreadStarted = false; 425 } 426 try { 427 mThread.join(); 428 } catch (InterruptedException ex) { 429 Log.d(TAG, "mThread.join " + ex); 430 } 431 } 432 433 public boolean isEnded() { 434 for (CodecState state : mVideoCodecStates.values()) { 435 if (!state.isEnded()) { 436 return false; 437 } 438 } 439 440 for (CodecState state : mAudioCodecStates.values()) { 441 if (!state.isEnded()) { 442 return false; 443 } 444 } 445 446 return true; 447 } 448 449 private void doSomeWork() { 450 try { 451 for (CodecState state : mVideoCodecStates.values()) { 452 state.doSomeWork(); 453 } 454 } catch (IllegalStateException e) { 455 throw new Error("Video CodecState.doSomeWork" + e); 456 } 457 458 try { 459 for (CodecState state : mAudioCodecStates.values()) { 460 state.doSomeWork(); 461 } 462 } catch (IllegalStateException e) { 463 throw new Error("Audio CodecState.doSomeWork" + e); 464 } 465 466 } 467 468 public long getNowUs() { 469 if (mAudioTrackState == null) { 470 return System.currentTimeMillis() * 1000; 471 } 472 473 return mAudioTrackState.getAudioTimeUs(); 474 } 475 476 public long getRealTimeUsForMediaTime(long mediaTimeUs) { 477 if (mDeltaTimeUs == -1) { 478 long nowUs = getNowUs(); 479 mDeltaTimeUs = nowUs - mediaTimeUs; 480 } 481 482 return mDeltaTimeUs + mediaTimeUs; 483 } 484 485 public int getDuration() { 486 return (int)((mDurationUs + 500) / 1000); 487 } 488 489 public int getCurrentPosition() { 490 if (mVideoCodecStates == null) { 491 return 0; 492 } 493 494 long positionUs = 0; 495 496 for (CodecState state : mVideoCodecStates.values()) { 497 long trackPositionUs = state.getCurrentPositionUs(); 498 499 if (trackPositionUs > positionUs) { 500 positionUs = trackPositionUs; 501 } 502 } 503 return (int)((positionUs + 500) / 1000); 504 } 505 506 } 507