com.serenegiant.media.TLMediaEncoder.java Source code

Java tutorial

Introduction

Here is the source code for com.serenegiant.media.TLMediaEncoder.java

Source

package com.serenegiant.media;

/*
 * TimeLapseRecordingSample
 * Sample project to capture audio and video periodically from internal mic/camera
 * and save as time lapsed MPEG4 file.
 *
 * Copyright (c) 2015 saki t_saki@serenegiant.com
 *
 * File name: TLMediaEncoder.java
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 * All files in the folder are under this Apache License, Version 2.0.
*/

import android.content.Context;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.os.Environment;
import android.text.TextUtils;
import android.util.Log;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.util.concurrent.LinkedBlockingDeque;

/**
 * abstract class to audio/video frames into intermediate file
 * using MediaCodec encoder so that pause / resume feature is available.
 */
public abstract class TLMediaEncoder {
    private static final boolean DEBUG = false;
    private static final String TAG_STATIC = "TLMediaEncoder";
    private final String TAG = getClass().getSimpleName();

    protected static final int TIMEOUT_USEC = 10000; // 10[msec]   

    private static final int STATE_RELEASE = 0;
    private static final int STATE_INITIALIZED = 1;
    private static final int STATE_PREPARING = 2;
    private static final int STATE_PREPARED = 3;
    private static final int STATE_PAUSING = 4;
    private static final int STATE_PAUSED = 5;
    private static final int STATE_RESUMING = 6;
    private static final int STATE_RUNNING = 7;

    private static final int REQUEST_NON = 0;
    private static final int REQUEST_PREPARE = 1;
    private static final int REQUEST_RESUME = 2;
    private static final int REQUEST_STOP = 3;
    private static final int REQUEST_PAUSE = 4;
    private static final int REQUEST_DRAIN = 5;

    static final int TYPE_VIDEO = 0;
    static final int TYPE_AUDIO = 1;

    /**
     * callback listener
     */
    public interface MediaEncoderListener {
        /**
         * called when encoder finished preparing
         * @param encoder
         */
        public void onPrepared(TLMediaEncoder encoder);

        /**
         * called when encoder stopped
         * @param encoder
         */
        public void onStopped(TLMediaEncoder encoder);

        /**
         * called when resuming
         * @param encoder
         */
        public void onResume(TLMediaEncoder encoder);

        /**
         * called when pausing
         * @param encoder
         */
        public void onPause(TLMediaEncoder encoder);
    }

    private final Object mSync = new Object();
    private final LinkedBlockingDeque<Integer> mRequestQueue = new LinkedBlockingDeque<Integer>();
    protected volatile boolean mIsRunning;
    private boolean mIsEOS;
    private MediaCodec mMediaCodec; // API >= 16(Android4.1.2)
    private MediaFormat mConfigFormat;
    private ByteBuffer[] encoderOutputBuffers;
    private ByteBuffer[] encoderInputBuffers;
    private MediaCodec.BufferInfo mBufferInfo; // API >= 16(Android4.1.2)
    private final MediaEncoderListener mListener;

    private final File mBaseDir;
    private final int mType;
    private Exception mCurrentException;
    private int mState = STATE_RELEASE;
    private DataOutputStream mCurrentOutputStream;
    private int mSequence;
    private int mNumFrames = -1;
    private int mFrameCounts;

    /**
     * constructor
     * @param movie_name this values is used as a directory name for intermediate files
     * @param listener
     */
    public TLMediaEncoder(final Context context, final String movie_name, final int type,
            final MediaEncoderListener listener) {
        if (DEBUG)
            Log.v(TAG, "TLMediaEncoder");
        if (TextUtils.isEmpty(movie_name))
            throw new IllegalArgumentException("movie_name should not be null");
        mBaseDir = new File(context.getExternalFilesDir(Environment.DIRECTORY_MOVIES), movie_name);
        mBaseDir.mkdirs();
        mType = type;
        mListener = listener;
        mBufferInfo = new MediaCodec.BufferInfo();
        new Thread(mEncoderTask, getClass().getSimpleName()).start();
        synchronized (mSync) {
            try {
                mSync.wait();
            } catch (InterruptedException e) {
                // ignore
            }
        }
    }

    /*
     * prepare encoder. This method will be called once.
     * @throws IOException
     */
    public final void prepare() throws Exception {
        if (DEBUG)
            Log.v(TAG, "prepare");
        synchronized (mSync) {
            if (!mIsRunning || (mState != STATE_INITIALIZED))
                throw new IllegalStateException("not ready/already released:" + mState);
        }
        setRequestAndWait(REQUEST_PREPARE);
    }

    /**
     * start encoder
     */
    public final void start() throws IOException {
        start(false);
    }

    /**
     * start encoder with specific sequence
     * @param pauseAfterStarted
     * @throws IOException
     */
    public void start(boolean pauseAfterStarted) throws IOException {
        if (DEBUG)
            Log.v(TAG, "start");
        synchronized (mSync) {
            if (!mIsRunning || ((mState != STATE_PREPARING) && (mState != STATE_PREPARED)))
                throw new IllegalStateException("not prepare/already released:" + mState);
            if (pauseAfterStarted) {
                setRequest(REQUEST_PAUSE);
            } else {
                resume(-1);
            }
        }
    }

    /**
     * request stop encoder
     * current implementation is same as release and don't re-use again.
     */
    public void stop() {
        if (DEBUG)
            Log.v(TAG, "stop");
        if (mState > STATE_INITIALIZED) {
            removeRequest(REQUEST_DRAIN);
            try {
                setRequestAndWait(REQUEST_STOP);
            } catch (Exception e) {
                Log.w(TAG, "stop:", e);
            }
        }
    }

    /**
     * request resume encoder
     * @throws IOException
     */
    public void resume() throws IOException {
        resume(-1);
    }

    /**
     * request resume encoder. after obtaining more than specific frames, automatically become pause state.
     * @param num_frames if num_frames is negative value, automatic pausing is disabled.
     * @throws IOException
     */
    public void resume(final int num_frames) throws IOException {
        if (DEBUG)
            Log.v(TAG, "resume");
        synchronized (mSync) {
            if (!mIsRunning || ((mState != STATE_PREPARING) && (mState != STATE_PREPARED)
                    && (mState != STATE_PAUSING) && (mState != STATE_PAUSED)))
                throw new IllegalStateException("not ready to resume:" + mState);
            mNumFrames = num_frames;
        }
        setRequest(REQUEST_RESUME);
    }

    /**
     * request pause encoder
     */
    public void pause() throws Exception {
        if (DEBUG)
            Log.v(TAG, "pause");
        removeRequest(REQUEST_DRAIN);
        setRequestFirst(REQUEST_PAUSE);
    }

    /**
     * get whether this encoder is pause state
     * @return
     */
    public boolean isPaused() {
        synchronized (mSync) {
            return (mState == STATE_PAUSING) || (mState == STATE_PAUSED);
        }
    }

    /**
      * calling this method notify encoder that the input data is already available or will be available soon
      * @return return tur if this encoder can accept input data
      */
    public boolean frameAvailableSoon() {
        //       if (DEBUG) Log.v(TAG, "frameAvailableSoon");
        synchronized (mSync) {
            if (mState != STATE_RUNNING) {
                return false;
            }
        }
        removeRequest(REQUEST_DRAIN);
        setRequest(REQUEST_DRAIN);
        return true;
    }

    public void release() {
        removeRequest(REQUEST_DRAIN);
        setRequestFirst(REQUEST_STOP);
    }

    //********************************************************************************
    //********************************************************************************
    /**
     * prepare MediaFormat instance for this encoder.
     * If there are previous intermediate files exist in current movie directory,
     * this method may not be called.
     * @return
     * @throws IOException
     */
    protected abstract MediaFormat internal_prepare() throws IOException;

    /**
     * execute MediaCodec#configure.
     * this method will be called every resuming
     * @param previous_codec
     * @param format
     * @return
     * @throws IOException
     */
    protected abstract MediaCodec internal_configure(MediaCodec previous_codec, MediaFormat format)
            throws IOException;

    protected void callOnPrepared() {
        if (mListener != null) {
            try {
                mListener.onPrepared(TLMediaEncoder.this);
            } catch (Exception e) {
                Log.e(TAG, "callOnPrepared:", e);
            }
        }
    }

    protected void callOnResume() {
        if (mListener != null) {
            try {
                mListener.onResume(this);
            } catch (Exception e) {
                Log.e(TAG, "callOnResume:", e);
            }
        }
    }

    protected void callOnPause() {
        if (mListener != null) {
            try {
                mListener.onPause(this);
            } catch (Exception e) {
                Log.e(TAG, "callOnPause:", e);
            }
        }
    }

    protected void callOnStopped() {
        if (mListener != null) {
            try {
                mListener.onStopped(this);
            } catch (Exception e) {
                Log.e(TAG, "callOnStopped:", e);
            }
        }
    }

    //********************************************************************************
    //********************************************************************************
    private final void setState(final int state, final Exception e) {
        synchronized (mSync) {
            mState = state;
            mCurrentException = e;
            mSync.notifyAll();
        }
    }

    private final void setRequest(final int request) {
        mRequestQueue.offer(Integer.valueOf(request));
    }

    private final void setRequestFirst(final int request) {
        mRequestQueue.offerFirst(Integer.valueOf(request));
    }

    private final void removeRequest(final int request) {
        for (; mRequestQueue.remove(Integer.valueOf(request));)
            ;
    }

    private final void setRequestAndWait(final int request) throws Exception {
        synchronized (mSync) {
            mRequestQueue.offer(Integer.valueOf(request));
            try {
                mSync.wait();
                if (mCurrentException != null)
                    throw mCurrentException;
            } catch (InterruptedException e) {
            }
        }
    }

    /**
     * wait request
     * @return
     */
    private final int waitRequest() {
        //      if (DEBUG) Log.v(TAG, "waitRequest:");
        Integer request = null;
        try {
            request = mRequestQueue.take();
        } catch (InterruptedException e) {
        }
        return request != null ? request : REQUEST_NON;
    }

    private final Runnable mEncoderTask = new Runnable() {
        @Override
        public void run() {
            int request = REQUEST_NON;
            if (DEBUG)
                Log.v(TAG, "#run");
            mIsRunning = true;
            setState(STATE_INITIALIZED, null);
            for (; mIsRunning;) {
                if (request == REQUEST_NON) { // if there is no handling request
                    request = waitRequest(); // wait for next request
                }
                if (request == REQUEST_STOP) {
                    handlePauseRecording();
                    mIsRunning = false;
                    break;
                }
                if (mState == STATE_RUNNING) {
                    request = handleRunning(request);
                } else {
                    if (request == REQUEST_DRAIN) {
                        request = REQUEST_NON; // just clear request
                        removeRequest(REQUEST_DRAIN);
                        continue;
                    }
                    switch (mState) {
                    case STATE_RELEASE:
                        setState(STATE_RELEASE,
                                new IllegalStateException("state=" + mState + ",request=" + request));
                        mIsRunning = false;
                        continue;
                    case STATE_INITIALIZED:
                        request = handleInitialized(request);
                        break;
                    case STATE_PREPARING:
                        request = handlePreparing(request);
                        break;
                    case STATE_PREPARED:
                        request = handlePrepared(request);
                        break;
                    case STATE_PAUSING:
                        request = handlePausing(request);
                        break;
                    case STATE_PAUSED:
                        request = handlePaused(request);
                        break;
                    case STATE_RESUMING:
                        request = handleResuming(request);
                        break;
                    default:
                    } // end of switch (mState)
                }
            } // end of for mIsRunning
            if (DEBUG)
                Log.v(TAG, "#run:finished");
            setState(STATE_RELEASE, null);
            // internal_release all related objects
            internal_release();
        }
    };

    private final int handleRunning(int request) {
        if (DEBUG)
            Log.v(TAG, "STATE_RUNNING");
        switch (request) {
        case REQUEST_RESUME:
            request = REQUEST_NON; // just clear request
            break;
        case REQUEST_PAUSE:
            setState(STATE_PAUSING, null);
            break;
        case REQUEST_DRAIN:
            request = REQUEST_NON;
            drain();
            break;
        default:
            setState(STATE_INITIALIZED, new IllegalStateException("state=" + mState + ",request=" + request));
            request = REQUEST_NON;
        }
        return request;
    }

    private final int handleInitialized(int request) {
        if (DEBUG)
            Log.v(TAG, "STATE_INITIALIZED");
        switch (request) {
        case REQUEST_PREPARE:
            setState(STATE_PREPARING, null);
            break;
        default:
            setState(STATE_INITIALIZED, new IllegalStateException("state=" + mState + ",request=" + request));
            request = REQUEST_NON;
        }
        return request;
    }

    private final int handlePreparing(int request) {
        if (DEBUG)
            Log.v(TAG, "STATE_PREPARING");
        request = REQUEST_NON;
        try {
            checkLastSequence();
            if (mConfigFormat == null)
                mConfigFormat = internal_prepare();
            if (mConfigFormat != null) {
                setState(STATE_PREPARED, null);
            } else {
                setState(STATE_INITIALIZED, new IllegalArgumentException());
            }
            callOnPrepared();
        } catch (IOException e) {
            setState(STATE_INITIALIZED, e);
        }
        return request;
    }

    private final int handlePrepared(int request) {
        if (DEBUG)
            Log.v(TAG, "STATE_PREPARED");
        switch (request) {
        case REQUEST_PREPARE:
            request = REQUEST_NON; // just clear request
            break;
        case REQUEST_RESUME:
            setState(STATE_RESUMING, null);
            break;
        case REQUEST_PAUSE:
            setState(STATE_PAUSING, null);
            break;
        default:
            setState(STATE_INITIALIZED, new IllegalStateException("state=" + mState + ",request=" + request));
            request = REQUEST_NON;
        }
        return request;
    }

    private final int handlePausing(int request) {
        if (DEBUG)
            Log.v(TAG, "STATE_PAUSING");
        request = REQUEST_NON;
        handlePauseRecording();
        setState(STATE_PAUSED, null);
        callOnPause();
        return request;
    }

    private final int handlePaused(int request) {
        if (DEBUG)
            Log.v(TAG, "STATE_PAUSED");
        switch (request) {
        case REQUEST_RESUME:
            setState(STATE_RESUMING, null);
            break;
        case REQUEST_PAUSE:
            request = REQUEST_NON; // just clear request
            break;
        default:
            setState(STATE_INITIALIZED, new IllegalStateException("state=" + mState + ",request=" + request));
            request = REQUEST_NON;
        }
        return request;
    }

    private final int handleResuming(int request) {
        if (DEBUG)
            Log.v(TAG, "STATE_RESUMING");
        request = REQUEST_NON;
        try {
            mIsEOS = false;
            mMediaCodec = internal_configure(mMediaCodec, mConfigFormat);
            mCurrentOutputStream = openOutputStream(); // changeOutputStream();
            mMediaCodec.start();
            encoderOutputBuffers = mMediaCodec.getOutputBuffers();
            encoderInputBuffers = mMediaCodec.getInputBuffers();
            mFrameCounts = -1;
            setState(STATE_RUNNING, null);
            callOnResume();
        } catch (IOException e) {
            setState(STATE_INITIALIZED, e);
        }
        return request;
    }

    //********************************************************************************
    //********************************************************************************
    /**
     * handle pausing request
     * this method is called from message handler of EncoderHandler
     */
    private final void handlePauseRecording() {
        if (DEBUG)
            Log.v(TAG, "handlePauseRecording");
        // process all available output data
        drain();
        // send EOS to MediaCodec encoder(request to stop)
        signalEndOfInputStream();
        // process output data again for EOS signal
        drain();
        if (mCurrentOutputStream != null)
            try {
                mCurrentOutputStream.flush();
                mCurrentOutputStream.close();
            } catch (IOException e) {
                Log.e(TAG, "handlePauseRecording:", e);
            }
        mCurrentOutputStream = null;
        encoderOutputBuffers = encoderInputBuffers = null;
        mRequestQueue.clear();
        if (mMediaCodec != null) {
            try {
                mMediaCodec.stop();
                mMediaCodec.release();
                mMediaCodec = null;
            } catch (Exception e) {
                Log.e(TAG, "failed releasing MediaCodec", e);
            }
        }
    }

    /**
     * Release all related objects
     */
    protected void internal_release() {
        if (DEBUG)
            Log.d(TAG, "internal_release:");
        callOnStopped();
        mIsRunning = false;
        if (mMediaCodec != null) {
            try {
                mMediaCodec.stop();
                mMediaCodec.release();
                mMediaCodec = null;
            } catch (Exception e) {
                Log.e(TAG, "failed releasing MediaCodec", e);
            }
        }
        mBufferInfo = null;
    }

    protected void signalEndOfInputStream() {
        if (DEBUG)
            Log.d(TAG, "sending EOS to encoder");
        // signalEndOfInputStream is only available for video encoding with surface
        // and equivalent sending a empty buffer with BUFFER_FLAG_END_OF_STREAM flag.
        //      mMediaCodec.signalEndOfInputStream();   // API >= 18
        encode(null, 0, getPTSUs());
    }

    protected boolean isRecording() {
        return mIsRunning && (mState == STATE_RUNNING) && (!mIsEOS);
    }

    /**
     * Method to set byte array to the MediaCodec encoder
    * if you use Surface to input data to encoder, you should not call this method
     * @param buffer
     * @param lengthlength of byte array, zero means EOS.
     * @param presentationTimeUs
     */
    //   protected void encode(final byte[] buffer, final int length, final long presentationTimeUs) {
    protected void encode(final ByteBuffer buffer, int length, long presentationTimeUs) {
        if (!mIsRunning || !isRecording())
            return;
        while (mIsRunning) {
            final int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC);
            if (inputBufferIndex >= 0) {
                final ByteBuffer inputBuffer = encoderInputBuffers[inputBufferIndex];
                inputBuffer.clear();
                if (buffer != null) {
                    inputBuffer.put(buffer);
                }
                if (length <= 0) {
                    // send EOS
                    mIsEOS = true;
                    if (DEBUG)
                        Log.i(TAG, "send BUFFER_FLAG_END_OF_STREAM");
                    mMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0, presentationTimeUs,
                            MediaCodec.BUFFER_FLAG_END_OF_STREAM);
                } else {
                    mMediaCodec.queueInputBuffer(inputBufferIndex, 0, length, presentationTimeUs, 0);
                }
                break;
            } else if (inputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // wait for MediaCodec encoder is ready to encode
                // nothing to do here because MediaCodec#dequeueInputBuffer(TIMEOUT_USEC)
                // will wait for maximum TIMEOUT_USEC(10msec) on each call
            }
        }
    }

    /**
     * working buffer
     */
    private byte[] writeBuffer = new byte[1024];

    /**
     * drain encoded data and write them to intermediate file
     */
    protected void drain() {
        if (mMediaCodec == null)
            return;
        int encoderStatus;
        while (mIsRunning && (mState == STATE_RUNNING)) {
            // get encoded data with maximum timeout duration of TIMEOUT_USEC(=10[msec])
            try {
                encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC);
            } catch (IllegalStateException e) {
                break;
            }
            if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) {
                // wait 5 counts(=TIMEOUT_USEC x 5 = 50msec) until data/EOS come
                if (!mIsEOS) {
                    break; // out of while
                }
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
                if (DEBUG)
                    Log.v(TAG, "INFO_OUTPUT_BUFFERS_CHANGED");
                // this should not come when encoding
                encoderOutputBuffers = mMediaCodec.getOutputBuffers();
            } else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
                if (DEBUG)
                    Log.v(TAG, "INFO_OUTPUT_FORMAT_CHANGED");
                // this status indicate the output format of codec is changed
                // this should come only once before actual encoded data
                // but this status never come on Android4.3 or less
                // and in that case, you should treat when MediaCodec.BUFFER_FLAG_CODEC_CONFIG come.
                // get output format from codec and pass them to muxer
                // getOutputFormat should be called after INFO_OUTPUT_FORMAT_CHANGED otherwise crash.
                if (mSequence == 0) { // sequence 0 is for saving MediaFormat
                    final MediaFormat format = mMediaCodec.getOutputFormat(); // API >= 16
                    try {
                        writeFormat(mCurrentOutputStream, mConfigFormat, format);
                        //                  changeOutputStream();
                    } catch (IOException e) {
                        Log.e(TAG, "drain:failed to write MediaFormat ", e);
                    }
                }
            } else if (encoderStatus < 0) {
                // unexpected status
                if (DEBUG)
                    Log.w(TAG, "drain:unexpected result from encoder#dequeueOutputBuffer: " + encoderStatus);
            } else {
                final ByteBuffer encodedData = encoderOutputBuffers[encoderStatus];
                if (encodedData == null) {
                    // this never should come...may be a MediaCodec internal error
                    throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null");
                }
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
                    // You should set output format to muxer here when you target Android4.3 or less
                    // but MediaCodec#getOutputFormat can not call here(because INFO_OUTPUT_FORMAT_CHANGED don't come yet)
                    // therefor we should expand and prepare output format from buffer data.
                    // This sample is for API>=18(>=Android 4.3), just ignore this flag here
                    if (DEBUG)
                        Log.d(TAG, "drain:BUFFER_FLAG_CODEC_CONFIG");
                    mBufferInfo.size = 0;
                }

                if (mBufferInfo.size != 0) {
                    mFrameCounts++;
                    if (mCurrentOutputStream == null) {
                        throw new RuntimeException("drain:temporary file not ready");
                    }
                    // write encoded data to muxer(need to adjust presentationTimeUs.
                    mBufferInfo.presentationTimeUs = getPTSUs();
                    try {
                        writeStream(mCurrentOutputStream, mSequence, mFrameCounts, mBufferInfo, encodedData,
                                writeBuffer);
                    } catch (IOException e) {
                        throw new RuntimeException("drain:failed to writeStream:" + e.getMessage());
                    }
                    prevOutputPTSUs = mBufferInfo.presentationTimeUs;
                }
                // return buffer to encoder
                mMediaCodec.releaseOutputBuffer(encoderStatus, false);
                if ((mNumFrames > 0) && (mFrameCounts >= mNumFrames)) {
                    setState(STATE_PAUSING, null); // request pause
                }
                if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
                    // when EOS come.
                    mIsRunning = false;
                    break; // out of while
                }
            }
        }
    }

    /**
     * time when previous encoding[micro second(s)]
     */
    private long prevOutputPTSUs = 0;
    /**
     * time when start encoding[micro seconds]
     */
    private long firstTimeUs = -1;

    /**
     * get next encoding presentationTimeUs
     * @return
     */
    protected long getPTSUs() {
        if (firstTimeUs < 0)
            firstTimeUs = System.nanoTime() / 1000L;
        long result = System.nanoTime() / 1000L - firstTimeUs;
        if (result < prevOutputPTSUs) {
            final long delta = prevOutputPTSUs - result + 8333; // add approx 1/120 sec as a bias
            result += delta;
            firstTimeUs += delta;
        }
        return result;
    }

    private void checkLastSequence() {
        if (DEBUG)
            Log.v(TAG, "checkLastSequence:");
        int sequence = -1;
        MediaFormat configFormat = null;
        try {
            final DataInputStream in = openInputStream(mBaseDir, mType, 0);
            if (in != null)
                try {
                    // read MediaFormat data for MediaCodec and for MediaMuxer
                    readHeader(in);
                    configFormat = asMediaFormat(in.readUTF()); // for MediaCodec
                    in.readUTF(); // for MediaMuxer
                    // search last sequence
                    // this is not a effective implementation for large intermediate file.
                    // ex. it may be better to split into multiple files for each sequence
                    // or split into two files; file for control block and file for raw bit stream.
                    final TLMediaFrameHeader header = new TLMediaFrameHeader();
                    for (; mIsRunning;) {
                        readHeader(in, header);
                        in.skipBytes(header.size);
                        sequence = Math.max(sequence, header.sequence);
                    }
                } finally {
                    in.close();
                }
        } catch (Exception e) {
            // ignore
        }
        mSequence = sequence;
        mConfigFormat = configFormat;
        if (sequence < 0) {
            // if intermediate files do not exist or invalid, remove them and re-create intermediate directory
            delete(mBaseDir);
            mBaseDir.mkdirs();
        }
        if (DEBUG)
            Log.v(TAG, "checkLastSequence:finished. sequence=" + sequence);
    }

    /*package*/static class TLMediaFrameHeader {
        public int sequence;
        public int frameNumber;
        public long presentationTimeUs;
        public int size;
        public int flags;

        public MediaCodec.BufferInfo asBufferInfo() {
            final MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
            info.set(0, size, presentationTimeUs, flags);
            return info;
        }

        public MediaCodec.BufferInfo asBufferInfo(final MediaCodec.BufferInfo info) {
            info.set(0, size, presentationTimeUs, flags);
            return info;
        }

        @Override
        public String toString() {
            return String.format(
                    "TLMediaFrameHeader(sequence=%d,frameNumber=%d,presentationTimeUs=%d,size=%d,flags=%d)",
                    sequence, frameNumber, presentationTimeUs, size, flags);
        }
    }

    private static String getSequenceFilePath(final File base_dir, final int type, final long sequence) {
        //      final File file = new File(base_dir, String.format("%s-%d.raw", (type == 1 ? "audio" : "video"), sequence));
        final File file = new File(base_dir, String.format("%s-0.raw", (type == 1 ? "audio" : "video")));
        return file.getAbsolutePath();
    }

    /**
     * open intermediate file for next sequence
     * @return
     * @throws IOException
     */
    private final DataOutputStream openOutputStream() throws IOException {
        if (mCurrentOutputStream != null)
            try {
                mCurrentOutputStream.flush();
                mCurrentOutputStream.close();
            } catch (IOException e) {
                Log.e(TAG, "openOutputStream: failed to flush temporary file", e);
                throw e;
            }
        mSequence++;
        final String path = getSequenceFilePath(mBaseDir, mType, mSequence);
        return new DataOutputStream(new BufferedOutputStream(new FileOutputStream(path, mSequence > 0)));
    }

    /*package*/static final DataInputStream openInputStream(final File base_dir, final int type, final int sequence)
            throws IOException {
        final String path = getSequenceFilePath(base_dir, type, sequence);
        DataInputStream in = null;
        try {
            in = new DataInputStream(new BufferedInputStream((new FileInputStream(path))));
        } catch (FileNotFoundException e) {
            //         if (DEBUG) Log.e(TAG, "openInputStream:" + path, e);
        }
        return in;
    }

    /**
     * convert ByteBuffer into String
     * @param buffer
     * @return
     */
    private static final String asString(final ByteBuffer buffer) {
        final byte[] temp = new byte[16];
        final StringBuilder sb = new StringBuilder();
        int n = (buffer != null ? buffer.limit() : 0);
        if (n > 0) {
            buffer.rewind();
            int sz = (n > 16 ? 16 : n);
            n -= sz;
            for (; sz > 0; sz = (n > 16 ? 16 : n), n -= sz) {
                buffer.get(temp, 0, sz);
                for (int i = 0; i < sz; i++) {
                    sb.append(temp[i]).append(',');
                }
            }
        }
        return sb.toString();
    }

    /**
     * convert transcribe String into ByteBuffer
     * @param str
     * @return
     */
    private static final ByteBuffer asByteBuffer(final String str) {
        final String[] hex = str.split(",");
        final int m = hex.length;
        final byte[] temp = new byte[m];
        int n = 0;
        for (int i = 0; i < m; i++) {
            if (!TextUtils.isEmpty(hex[i]))
                temp[n++] = (byte) Integer.parseInt(hex[i]);
        }
        if (n > 0)
            return ByteBuffer.wrap(temp, 0, n);
        else
            return null;
    }

    private static final String asString(final MediaFormat format) {
        final JSONObject map = new JSONObject();
        try {
            if (format.containsKey(MediaFormat.KEY_MIME))
                map.put(MediaFormat.KEY_MIME, format.getString(MediaFormat.KEY_MIME));
            if (format.containsKey(MediaFormat.KEY_WIDTH))
                map.put(MediaFormat.KEY_WIDTH, format.getInteger(MediaFormat.KEY_WIDTH));
            if (format.containsKey(MediaFormat.KEY_HEIGHT))
                map.put(MediaFormat.KEY_HEIGHT, format.getInteger(MediaFormat.KEY_HEIGHT));
            if (format.containsKey(MediaFormat.KEY_BIT_RATE))
                map.put(MediaFormat.KEY_BIT_RATE, format.getInteger(MediaFormat.KEY_BIT_RATE));
            if (format.containsKey(MediaFormat.KEY_COLOR_FORMAT))
                map.put(MediaFormat.KEY_COLOR_FORMAT, format.getInteger(MediaFormat.KEY_COLOR_FORMAT));
            if (format.containsKey(MediaFormat.KEY_FRAME_RATE))
                map.put(MediaFormat.KEY_FRAME_RATE, format.getInteger(MediaFormat.KEY_FRAME_RATE));
            if (format.containsKey(MediaFormat.KEY_I_FRAME_INTERVAL))
                map.put(MediaFormat.KEY_I_FRAME_INTERVAL, format.getInteger(MediaFormat.KEY_I_FRAME_INTERVAL));
            if (format.containsKey(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER))
                map.put(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER,
                        format.getLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER));
            if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE))
                map.put(MediaFormat.KEY_MAX_INPUT_SIZE, format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE));
            if (format.containsKey(MediaFormat.KEY_DURATION))
                map.put(MediaFormat.KEY_DURATION, format.getInteger(MediaFormat.KEY_DURATION));
            if (format.containsKey(MediaFormat.KEY_CHANNEL_COUNT))
                map.put(MediaFormat.KEY_CHANNEL_COUNT, format.getInteger(MediaFormat.KEY_CHANNEL_COUNT));
            if (format.containsKey(MediaFormat.KEY_SAMPLE_RATE))
                map.put(MediaFormat.KEY_SAMPLE_RATE, format.getInteger(MediaFormat.KEY_SAMPLE_RATE));
            if (format.containsKey(MediaFormat.KEY_CHANNEL_MASK))
                map.put(MediaFormat.KEY_CHANNEL_MASK, format.getInteger(MediaFormat.KEY_CHANNEL_MASK));
            if (format.containsKey(MediaFormat.KEY_AAC_PROFILE))
                map.put(MediaFormat.KEY_AAC_PROFILE, format.getInteger(MediaFormat.KEY_AAC_PROFILE));
            if (format.containsKey(MediaFormat.KEY_AAC_SBR_MODE))
                map.put(MediaFormat.KEY_AAC_SBR_MODE, format.getInteger(MediaFormat.KEY_AAC_SBR_MODE));
            if (format.containsKey(MediaFormat.KEY_MAX_INPUT_SIZE))
                map.put(MediaFormat.KEY_MAX_INPUT_SIZE, format.getInteger(MediaFormat.KEY_MAX_INPUT_SIZE));
            if (format.containsKey(MediaFormat.KEY_IS_ADTS))
                map.put(MediaFormat.KEY_IS_ADTS, format.getInteger(MediaFormat.KEY_IS_ADTS));
            if (format.containsKey("what"))
                map.put("what", format.getInteger("what"));
            if (format.containsKey("csd-0"))
                map.put("csd-0", asString(format.getByteBuffer("csd-0")));
            if (format.containsKey("csd-1"))
                map.put("csd-1", asString(format.getByteBuffer("csd-1")));
        } catch (JSONException e) {
            Log.e(TAG_STATIC, "writeFormat:", e);
        }

        return map.toString();
    }

    private static final MediaFormat asMediaFormat(final String format_str) {
        MediaFormat format = new MediaFormat();
        try {
            final JSONObject map = new JSONObject(format_str);
            if (map.has(MediaFormat.KEY_MIME))
                format.setString(MediaFormat.KEY_MIME, (String) map.get(MediaFormat.KEY_MIME));
            if (map.has(MediaFormat.KEY_WIDTH))
                format.setInteger(MediaFormat.KEY_WIDTH, (Integer) map.get(MediaFormat.KEY_WIDTH));
            if (map.has(MediaFormat.KEY_HEIGHT))
                format.setInteger(MediaFormat.KEY_HEIGHT, (Integer) map.get(MediaFormat.KEY_HEIGHT));
            if (map.has(MediaFormat.KEY_BIT_RATE))
                format.setInteger(MediaFormat.KEY_BIT_RATE, (Integer) map.get(MediaFormat.KEY_BIT_RATE));
            if (map.has(MediaFormat.KEY_COLOR_FORMAT))
                format.setInteger(MediaFormat.KEY_COLOR_FORMAT, (Integer) map.get(MediaFormat.KEY_COLOR_FORMAT));
            if (map.has(MediaFormat.KEY_FRAME_RATE))
                format.setInteger(MediaFormat.KEY_FRAME_RATE, (Integer) map.get(MediaFormat.KEY_FRAME_RATE));
            if (map.has(MediaFormat.KEY_I_FRAME_INTERVAL))
                format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL,
                        (Integer) map.get(MediaFormat.KEY_I_FRAME_INTERVAL));
            if (map.has(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER))
                format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER,
                        (Long) map.get(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER));
            if (map.has(MediaFormat.KEY_MAX_INPUT_SIZE))
                format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE,
                        (Integer) map.get(MediaFormat.KEY_MAX_INPUT_SIZE));
            if (map.has(MediaFormat.KEY_DURATION))
                format.setInteger(MediaFormat.KEY_DURATION, (Integer) map.get(MediaFormat.KEY_DURATION));
            if (map.has(MediaFormat.KEY_CHANNEL_COUNT))
                format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, (Integer) map.get(MediaFormat.KEY_CHANNEL_COUNT));
            if (map.has(MediaFormat.KEY_SAMPLE_RATE))
                format.setInteger(MediaFormat.KEY_SAMPLE_RATE, (Integer) map.get(MediaFormat.KEY_SAMPLE_RATE));
            if (map.has(MediaFormat.KEY_CHANNEL_MASK))
                format.setInteger(MediaFormat.KEY_CHANNEL_MASK, (Integer) map.get(MediaFormat.KEY_CHANNEL_MASK));
            if (map.has(MediaFormat.KEY_AAC_PROFILE))
                format.setInteger(MediaFormat.KEY_AAC_PROFILE, (Integer) map.get(MediaFormat.KEY_AAC_PROFILE));
            if (map.has(MediaFormat.KEY_AAC_SBR_MODE))
                format.setInteger(MediaFormat.KEY_AAC_SBR_MODE, (Integer) map.get(MediaFormat.KEY_AAC_SBR_MODE));
            if (map.has(MediaFormat.KEY_MAX_INPUT_SIZE))
                format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE,
                        (Integer) map.get(MediaFormat.KEY_MAX_INPUT_SIZE));
            if (map.has(MediaFormat.KEY_IS_ADTS))
                format.setInteger(MediaFormat.KEY_IS_ADTS, (Integer) map.get(MediaFormat.KEY_IS_ADTS));
            if (map.has("what"))
                format.setInteger("what", (Integer) map.get("what"));
            if (map.has("csd-0"))
                format.setByteBuffer("csd-0", asByteBuffer((String) map.get("csd-0")));
            if (map.has("csd-1"))
                format.setByteBuffer("csd-1", asByteBuffer((String) map.get("csd-1")));
        } catch (JSONException e) {
            Log.e(TAG_STATIC, "writeFormat:" + format_str, e);
            format = null;
        }
        return format;
    }

    private static final byte[] RESERVED = new byte[40];

    /**
     * write frame header
     * @param presentation_time_us
     * @param size
     * @throws IOException
     */
    /*package*/static void writeHeader(final DataOutputStream out, final int sequence, final int frame_number,
            final long presentation_time_us, final int size, final int flag) throws IOException {

        out.writeInt(sequence);
        out.writeInt(frame_number);
        out.writeLong(presentation_time_us);
        out.writeInt(size);
        out.writeInt(flag);
        //
        out.write(RESERVED, 0, 40);
    }

    /*package*/static TLMediaFrameHeader readHeader(final DataInputStream in, final TLMediaFrameHeader header)
            throws IOException {
        header.size = 0;
        header.sequence = in.readInt();
        header.frameNumber = in.readInt(); // frame number
        header.presentationTimeUs = in.readLong();
        header.size = in.readInt();
        header.flags = in.readInt();
        in.skipBytes(40); // long x 5
        return header;
    }

    /*package*/static TLMediaFrameHeader readHeader(final DataInputStream in) throws IOException {
        final TLMediaFrameHeader header = new TLMediaFrameHeader();
        return readHeader(in, header);
    }

    /**
     * read frame header and only returns size of frame
     * @param in
     * @return
     * @throws IOException
     */
    /*package*/static int readFrameSize(final DataInputStream in) throws IOException {
        final TLMediaFrameHeader header = readHeader(in);
        return header.size;
    }

    /**
     * write MediaFormat data into intermediate file
     * @param out
     * @param output_format
     */
    private static final void writeFormat(final DataOutputStream out, final MediaFormat codec_format,
            final MediaFormat output_format) throws IOException {
        if (DEBUG)
            Log.v(TAG_STATIC, "writeFormat:format=" + output_format);
        final String codec_format_str = asString(codec_format);
        final String output_format_str = asString(output_format);
        final int size = (TextUtils.isEmpty(codec_format_str) ? 0 : codec_format_str.length())
                + (TextUtils.isEmpty(output_format_str) ? 0 : output_format_str.length());
        try {
            writeHeader(out, 0, 0, -1, size, 0);
            out.writeUTF(codec_format_str);
            out.writeUTF(output_format_str);
        } catch (IOException e) {
            Log.e(TAG_STATIC, "writeFormat:", e);
            throw e;
        }
    }

    /*package*/static MediaFormat readFormat(final DataInputStream in) {
        MediaFormat format = null;
        try {
            readHeader(in);
            in.readUTF(); // skip MediaFormat data for configure
            format = asMediaFormat(in.readUTF());
        } catch (IOException e) {
            Log.e(TAG_STATIC, "readFormat:", e);
        }
        if (DEBUG)
            Log.v(TAG_STATIC, "readFormat:format=" + format);
        return format;
    }

    /**
     * write raw bit stream into specific intermediate file
     * @param out
     * @param sequence
     * @param frame_number
     * @param info
     * @param buffer
     * @param writeBuffer
     * @throws IOException
     */
    private static final void writeStream(final DataOutputStream out, final int sequence, final int frame_number,
            final MediaCodec.BufferInfo info, final ByteBuffer buffer, byte[] writeBuffer) throws IOException {

        if (writeBuffer.length < info.size) {
            writeBuffer = new byte[info.size];
        }
        buffer.position(info.offset);
        buffer.get(writeBuffer, 0, info.size);
        try {
            writeHeader(out, sequence, frame_number, info.presentationTimeUs, info.size, info.flags);
            out.write(writeBuffer, 0, info.size);
        } catch (IOException e) {
            if (DEBUG)
                Log.e(TAG_STATIC, "writeStream:", e);
            throw e;
        }
    }

    /**
     * read raw bit stream from specific intermediate file
     * @param in
     * @param header
     * @param buffer
     * @param readBuffer
     * @throws IOException
     * @throws BufferOverflowException
     */
    /*package*/static ByteBuffer readStream(final DataInputStream in, final TLMediaFrameHeader header,
            ByteBuffer buffer, final byte[] readBuffer) throws IOException {

        readHeader(in, header);
        if ((buffer == null) || header.size > buffer.capacity()) {
            buffer = ByteBuffer.allocateDirect(header.size);
        }
        buffer.clear();
        final int max_bytes = Math.min(readBuffer.length, header.size);
        int read_bytes;
        for (int i = header.size; i > 0; i -= read_bytes) {
            read_bytes = in.read(readBuffer, 0, Math.min(i, max_bytes));
            if (read_bytes <= 0)
                break;
            buffer.put(readBuffer, 0, read_bytes);
        }
        buffer.flip();
        return buffer;
    }

    /**
     * delete specific file/directory recursively
     * @param path
     */
    /*package*/static final void delete(final File path) {
        if (path.isDirectory()) {
            File[] files = path.listFiles();
            final int n = files != null ? files.length : 0;
            for (int i = 0; i < n; i++)
                delete(files[i]);
        }
        path.delete();
    }

}