Home | History | Annotate | Download | only in cts
      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