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.AudioTrack;
     19 import android.media.MediaCodec;
     20 import android.media.MediaExtractor;
     21 import android.media.MediaFormat;
     22 import android.util.Log;
     23 
     24 import java.nio.ByteBuffer;
     25 import java.util.LinkedList;
     26 
     27 /**
     28  * Class for directly managing both audio and video playback by
     29  * using {@link MediaCodec} and {@link AudioTrack}.
     30  */
     31 public class CodecState {
     32     private static final String TAG = CodecState.class.getSimpleName();
     33 
     34     private boolean mSawInputEOS, mSawOutputEOS;
     35     private boolean mLimitQueueDepth;
     36     private boolean mTunneled;
     37     private boolean mIsAudio;
     38     private int mAudioSessionId;
     39     private ByteBuffer[] mCodecInputBuffers;
     40     private ByteBuffer[] mCodecOutputBuffers;
     41     private int mTrackIndex;
     42     private LinkedList<Integer> mAvailableInputBufferIndices;
     43     private LinkedList<Integer> mAvailableOutputBufferIndices;
     44     private LinkedList<MediaCodec.BufferInfo> mAvailableOutputBufferInfos;
     45     private long mPresentationTimeUs;
     46     private long mSampleBaseTimeUs;
     47     private MediaCodec mCodec;
     48     private MediaTimeProvider mMediaTimeProvider;
     49     private MediaExtractor mExtractor;
     50     private MediaFormat mFormat;
     51     private MediaFormat mOutputFormat;
     52     private NonBlockingAudioTrack mAudioTrack;
     53 
     54     /**
     55      * Manages audio and video playback using MediaCodec and AudioTrack.
     56      */
     57     public CodecState(
     58             MediaTimeProvider mediaTimeProvider,
     59             MediaExtractor extractor,
     60             int trackIndex,
     61             MediaFormat format,
     62             MediaCodec codec,
     63             boolean limitQueueDepth,
     64             boolean tunneled,
     65             int audioSessionId) {
     66         mMediaTimeProvider = mediaTimeProvider;
     67         mExtractor = extractor;
     68         mTrackIndex = trackIndex;
     69         mFormat = format;
     70         mSawInputEOS = mSawOutputEOS = false;
     71         mLimitQueueDepth = limitQueueDepth;
     72         mTunneled = tunneled;
     73         mAudioSessionId = audioSessionId;
     74         mSampleBaseTimeUs = -1;
     75 
     76         mCodec = codec;
     77 
     78         mAvailableInputBufferIndices = new LinkedList<Integer>();
     79         mAvailableOutputBufferIndices = new LinkedList<Integer>();
     80         mAvailableOutputBufferInfos = new LinkedList<MediaCodec.BufferInfo>();
     81 
     82         mPresentationTimeUs = 0;
     83 
     84         String mime = mFormat.getString(MediaFormat.KEY_MIME);
     85         Log.d(TAG, "CodecState::onOutputFormatChanged " + mime);
     86         mIsAudio = mime.startsWith("audio/");
     87     }
     88 
     89     public void release() {
     90         mCodec.stop();
     91         mCodecInputBuffers = null;
     92         mCodecOutputBuffers = null;
     93         mOutputFormat = null;
     94 
     95         mAvailableInputBufferIndices.clear();
     96         mAvailableOutputBufferIndices.clear();
     97         mAvailableOutputBufferInfos.clear();
     98 
     99         mAvailableInputBufferIndices = null;
    100         mAvailableOutputBufferIndices = null;
    101         mAvailableOutputBufferInfos = null;
    102 
    103         mCodec.release();
    104         mCodec = null;
    105 
    106         if (mAudioTrack != null) {
    107             mAudioTrack.release();
    108             mAudioTrack = null;
    109         }
    110     }
    111 
    112     public void start() {
    113         mCodec.start();
    114         mCodecInputBuffers = mCodec.getInputBuffers();
    115         if (!mTunneled || mIsAudio) {
    116             mCodecOutputBuffers = mCodec.getOutputBuffers();
    117         }
    118 
    119         if (mAudioTrack != null) {
    120             mAudioTrack.play();
    121         }
    122     }
    123 
    124     public void pause() {
    125         if (mAudioTrack != null) {
    126             mAudioTrack.pause();
    127         }
    128     }
    129 
    130     public long getCurrentPositionUs() {
    131         return mPresentationTimeUs;
    132     }
    133 
    134     public void flush() {
    135         mAvailableInputBufferIndices.clear();
    136         if (!mTunneled || mIsAudio) {
    137             mAvailableOutputBufferIndices.clear();
    138             mAvailableOutputBufferInfos.clear();
    139         }
    140 
    141         mSawInputEOS = false;
    142         mSawOutputEOS = false;
    143 
    144         if (mAudioTrack != null
    145                 && mAudioTrack.getPlayState() != AudioTrack.PLAYSTATE_PLAYING) {
    146             mAudioTrack.flush();
    147         }
    148 
    149         mCodec.flush();
    150     }
    151 
    152     public boolean isEnded() {
    153         return mSawInputEOS && mSawOutputEOS;
    154     }
    155 
    156     /**
    157      * doSomeWork() is the worker function that does all buffer handling and decoding works.
    158      * It first reads data from {@link MediaExtractor} and pushes it into {@link MediaCodec};
    159      * it then dequeues buffer from {@link MediaCodec}, consumes it and pushes back to its own
    160      * buffer queue for next round reading data from {@link MediaExtractor}.
    161      */
    162     public void doSomeWork() {
    163         int indexInput = mCodec.dequeueInputBuffer(0 /* timeoutUs */);
    164 
    165         if (indexInput != MediaCodec.INFO_TRY_AGAIN_LATER) {
    166             mAvailableInputBufferIndices.add(indexInput);
    167         }
    168 
    169         while (feedInputBuffer()) {
    170         }
    171 
    172         if (mIsAudio || !mTunneled) {
    173             MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
    174             int indexOutput = mCodec.dequeueOutputBuffer(info, 0 /* timeoutUs */);
    175 
    176             if (indexOutput == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
    177                 mOutputFormat = mCodec.getOutputFormat();
    178                 onOutputFormatChanged();
    179             } else if (indexOutput == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
    180                 mCodecOutputBuffers = mCodec.getOutputBuffers();
    181             } else if (indexOutput != MediaCodec.INFO_TRY_AGAIN_LATER) {
    182                 mAvailableOutputBufferIndices.add(indexOutput);
    183                 mAvailableOutputBufferInfos.add(info);
    184             }
    185 
    186             while (drainOutputBuffer()) {
    187             }
    188         }
    189     }
    190 
    191     /** Returns true if more input data could be fed. */
    192     private boolean feedInputBuffer() throws MediaCodec.CryptoException, IllegalStateException {
    193         if (mSawInputEOS || mAvailableInputBufferIndices.isEmpty()) {
    194             return false;
    195         }
    196 
    197         // stalls read if audio queue is larger than 2MB full so we will not occupy too much heap
    198         if (mLimitQueueDepth && mAudioTrack != null &&
    199                 mAudioTrack.getNumBytesQueued() > 2 * 1024 * 1024) {
    200             return false;
    201         }
    202 
    203         int index = mAvailableInputBufferIndices.peekFirst().intValue();
    204 
    205         ByteBuffer codecData = mCodecInputBuffers[index];
    206 
    207         int trackIndex = mExtractor.getSampleTrackIndex();
    208 
    209         if (trackIndex == mTrackIndex) {
    210             int sampleSize =
    211                 mExtractor.readSampleData(codecData, 0 /* offset */);
    212 
    213             long sampleTime = mExtractor.getSampleTime();
    214 
    215             int sampleFlags = mExtractor.getSampleFlags();
    216 
    217             if (sampleSize <= 0) {
    218                 Log.d(TAG, "sampleSize: " + sampleSize + " trackIndex:" + trackIndex +
    219                         " sampleTime:" + sampleTime + " sampleFlags:" + sampleFlags);
    220                 mSawInputEOS = true;
    221                 // FIX-ME: in tunneled mode we currently use input EOS as output EOS indicator
    222                 // we should use stream duration
    223                 if (mTunneled && !mIsAudio) {
    224                     mSawOutputEOS = true;
    225                 }
    226                 return false;
    227             }
    228 
    229             if (mTunneled && !mIsAudio) {
    230                 if (mSampleBaseTimeUs == -1) {
    231                     mSampleBaseTimeUs = sampleTime;
    232                 }
    233                 sampleTime -= mSampleBaseTimeUs;
    234                 // FIX-ME: in tunneled mode we currently use input buffer time
    235                 // as video presentation time. This is not accurate and should be fixed
    236                 mPresentationTimeUs = sampleTime;
    237             }
    238 
    239             if ((sampleFlags & MediaExtractor.SAMPLE_FLAG_ENCRYPTED) != 0) {
    240                 MediaCodec.CryptoInfo info = new MediaCodec.CryptoInfo();
    241                 mExtractor.getSampleCryptoInfo(info);
    242 
    243                 mCodec.queueSecureInputBuffer(
    244                         index, 0 /* offset */, info, sampleTime, 0 /* flags */);
    245             } else {
    246                 mCodec.queueInputBuffer(
    247                         index, 0 /* offset */, sampleSize, sampleTime, 0 /* flags */);
    248             }
    249 
    250             mAvailableInputBufferIndices.removeFirst();
    251             mExtractor.advance();
    252 
    253             return true;
    254         } else if (trackIndex < 0) {
    255             Log.d(TAG, "saw input EOS on track " + mTrackIndex);
    256 
    257             mSawInputEOS = true;
    258             // FIX-ME: in tunneled mode we currently use input EOS as output EOS indicator
    259             // we should use stream duration
    260             if (mTunneled && !mIsAudio) {
    261                 mSawOutputEOS = true;
    262             }
    263 
    264             mCodec.queueInputBuffer(
    265                     index, 0 /* offset */, 0 /* sampleSize */,
    266                     0 /* sampleTime */, MediaCodec.BUFFER_FLAG_END_OF_STREAM);
    267 
    268             mAvailableInputBufferIndices.removeFirst();
    269         }
    270 
    271         return false;
    272     }
    273 
    274     private void onOutputFormatChanged() {
    275         String mime = mOutputFormat.getString(MediaFormat.KEY_MIME);
    276         // b/9250789
    277         Log.d(TAG, "CodecState::onOutputFormatChanged " + mime);
    278 
    279         mIsAudio = false;
    280         if (mime.startsWith("audio/")) {
    281             mIsAudio = true;
    282             int sampleRate =
    283                 mOutputFormat.getInteger(MediaFormat.KEY_SAMPLE_RATE);
    284 
    285             int channelCount =
    286                 mOutputFormat.getInteger(MediaFormat.KEY_CHANNEL_COUNT);
    287 
    288             Log.d(TAG, "CodecState::onOutputFormatChanged Audio" +
    289                     " sampleRate:" + sampleRate + " channels:" + channelCount);
    290             // We do sanity check here after we receive data from MediaExtractor and before
    291             // we pass them down to AudioTrack. If MediaExtractor works properly, this
    292             // sanity-check is not necessary, however, in our tests, we found that there
    293             // are a few cases where ch=0 and samplerate=0 were returned by MediaExtractor.
    294             if (channelCount < 1 || channelCount > 8 ||
    295                     sampleRate < 8000 || sampleRate > 128000) {
    296                 return;
    297             }
    298             mAudioTrack = new NonBlockingAudioTrack(sampleRate, channelCount,
    299                                     mTunneled, mAudioSessionId);
    300             mAudioTrack.play();
    301         }
    302 
    303         if (mime.startsWith("video/")) {
    304             int width = mOutputFormat.getInteger(MediaFormat.KEY_WIDTH);
    305             int height = mOutputFormat.getInteger(MediaFormat.KEY_HEIGHT);
    306             Log.d(TAG, "CodecState::onOutputFormatChanged Video" +
    307                     " width:" + width + " height:" + height);
    308         }
    309     }
    310 
    311     /** Returns true if more output data could be drained. */
    312     private boolean drainOutputBuffer() {
    313         if (mSawOutputEOS || mAvailableOutputBufferIndices.isEmpty()) {
    314             return false;
    315         }
    316 
    317         int index = mAvailableOutputBufferIndices.peekFirst().intValue();
    318         MediaCodec.BufferInfo info = mAvailableOutputBufferInfos.peekFirst();
    319 
    320         if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
    321             Log.d(TAG, "saw output EOS on track " + mTrackIndex);
    322 
    323             mSawOutputEOS = true;
    324 
    325             // We need to stop the audio track so that all audio frames are played
    326             // and the video codec can consume all of its data.
    327             // After audio track stop, getAudioTimeUs will return 0.
    328             if (mAudioTrack != null) {
    329                 mAudioTrack.stop();
    330             }
    331             return false;
    332         }
    333 
    334         long realTimeUs =
    335             mMediaTimeProvider.getRealTimeUsForMediaTime(info.presentationTimeUs);
    336 
    337         long nowUs = mMediaTimeProvider.getNowUs();
    338 
    339         long lateUs = nowUs - realTimeUs;
    340 
    341         if (mAudioTrack != null) {
    342             ByteBuffer buffer = mCodecOutputBuffers[index];
    343             buffer.clear();
    344             ByteBuffer audioBuffer = ByteBuffer.allocate(buffer.remaining());
    345             audioBuffer.put(buffer);
    346             audioBuffer.clear();
    347 
    348             mAudioTrack.write(audioBuffer, info.size, info.presentationTimeUs*1000);
    349 
    350             mCodec.releaseOutputBuffer(index, false /* render */);
    351 
    352             mPresentationTimeUs = info.presentationTimeUs;
    353 
    354             mAvailableOutputBufferIndices.removeFirst();
    355             mAvailableOutputBufferInfos.removeFirst();
    356             return true;
    357         } else {
    358             // video
    359             boolean render;
    360 
    361             if (lateUs < -45000) {
    362                 // too early;
    363                 return false;
    364             } else if (lateUs > 30000) {
    365                 Log.d(TAG, "video late by " + lateUs + " us.");
    366                 render = false;
    367             } else {
    368                 render = true;
    369                 mPresentationTimeUs = info.presentationTimeUs;
    370             }
    371 
    372             mCodec.releaseOutputBuffer(index, render);
    373 
    374             mAvailableOutputBufferIndices.removeFirst();
    375             mAvailableOutputBufferInfos.removeFirst();
    376             return true;
    377         }
    378     }
    379 
    380     public long getAudioTimeUs() {
    381         if (mAudioTrack == null) {
    382             return 0;
    383         }
    384 
    385         return mAudioTrack.getAudioTimeUs();
    386     }
    387 
    388     public void process() {
    389         if (mAudioTrack != null) {
    390             mAudioTrack.process();
    391         }
    392     }
    393 }
    394