package jp.sourceforge.nicoro;

import static jp.sourceforge.nicoro.Log.LOG_TAG;

import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.util.DisplayMetrics;

import java.nio.ByteBuffer;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;

import jp.gr.java_conf.shiseissi.commonlib.ThreadUtil;

public class FFmpegVideoDecoder extends Thread
        implements SurfaceVideoDrawer.VideoDrawBufferPool,
        FFmpegInfoCallback, FFmpegVideoDecoderInterface {
    private static final boolean DEBUG_LOGV = Release.IS_DEBUG & false;
    private static final boolean DEBUG_LOGD = Release.IS_DEBUG & true;

    private static final int VIDEO_BUFFER_CACHE_SIZE = 20;
    private static final int VIDEO_BUFFER_EMPTY_POOL_MAX = 20;

//    private static final long THREAD_JOIN_TIME = 1000L * 3;

    public static final int CODE_DECODE_FRAME_INVALID = 0x80000001;
    public static final int CODE_DECODE_FRAME_PROGRESS = 0x7ffffffe;
    public static final int CODE_DECODE_FRAME_VIDEO = 0;
    public static final int CODE_DECODE_FRAME_AUDIO = 1;
    public static final int CODE_DECODE_FRAME_VIDEO_SKIP = 2;
    public static final int CODE_DECODE_FRAME_SKIP = 3;
    public static final int CODE_DECODE_FRAME_ERROR_OR_END = -1;

    public static final int CODE_FRAME_TYPE_VIDEO = 0;
    public static final int CODE_FRAME_TYPE_AUDIO = 1;
    public static final int CODE_FRAME_TYPE_UNKNOWN = -1;
    public static final int CODE_FRAME_TYPE_ERROR_OR_END = -2;

    public static final int CODE_SKIP_VIDEO_FRAME_NONE = 0;
    public static final int CODE_SKIP_VIDEO_FRAME_NONREF = 1;
    public static final int CODE_SKIP_VIDEO_FRAME_BIDIR = 2;
    public static final int CODE_SKIP_VIDEO_FRAME_NONKEY = 3;
    public static final int CODE_SKIP_VIDEO_FRAME_SIMPLE = 4;
    public static final int CODE_SKIP_VIDEO_FRAME_UNTIL_KEY_FRAME = 5;

    public static final int CODE_RESCALE_POINT = 0;
    public static final int CODE_RESCALE_FAST_BILINEAR = 1;
    public static final int CODE_RESCALE_BILINEAR = 2;
    public static final int CODE_RESCALE_BICUBLIN = 3;
    public static final int CODE_RESCALE_BICUBIC = 4;
    public static final int CODE_RESCALE_SPLINE = 5;
    public static final int CODE_RESCALE_SINC = 6;

    public interface SeekerBase {
        void enableSeekBar();
        void seekBySecondBase(int second);
    }

    private volatile boolean mIsFinish;

    private final SurfaceVideoDrawerInterface mSurfaceVideoDrawer;
    private final StreamAudioPlayerInterface mStreamAudioPlayer;
    private final FFmpegIOCallback mIOCallback;

    private SeekerBase mSeekerBase;

    private boolean mCacheWhenChoppy;
    private boolean mFullscreen16_9;
    private boolean mUse16bitColor;

    private final Object mSyncDecode = new Object();

    private ConcurrentLinkedQueue<VideoDrawBuffer<?>> mVideoBufferPool =
        new ConcurrentLinkedQueue<VideoDrawBuffer<?>>();
    // TODO mVideoBufferEmptyPoolに戻し切れていないインスタンスがある
    private ConcurrentLinkedQueue<VideoDrawBuffer<?>> mVideoBufferEmptyPool =
        new ConcurrentLinkedQueue<VideoDrawBuffer<?>>();
    private ConcurrentLinkedQueue<VideoDrawBuffer<?>> mVideoBufferCached =
        new ConcurrentLinkedQueue<VideoDrawBuffer<?>>();
    private final Object mSyncVideoBuffer = new Object();

    private byte[] mReadBuffer = new byte[1024*32];

    /**
     * デコードした動画データの合計サイズ（フレーム数）
     */
    private int mFrameCountDecode = 0;
    /**
     * デコードした音声データの合計サイズ（バイト単位）
     */
    private int mAudioDecodedBytes = 0;

    private volatile int mFrameSkipDecode = CODE_SKIP_VIDEO_FRAME_NONE;

    private int mSkipJudgeAudioMs;
    private int mSkipJudgeAudioStrongMs;

    private volatile int mRequestSeekSecond = -1;
    private volatile boolean mRequestSeekOnDecoder = false;

    private volatile boolean mIsPause = false;
    private boolean mReadAtEnd = false;
    private volatile boolean mIsVisible = true;

    private int mVideoOriginalWidth = 0;
    private int mVideoOriginalHeight = 0;

    private DisplayMetrics mDisplayMetrics = new DisplayMetrics();
    private int mDisplayWidth = 0;
    private int mDisplayHeight = 0;
    private int mOrientation;
    private ComputeVideoDisplaySize mComputeVideoDisplaySize =
        new ComputeVideoDisplaySize();

    private final Runtime mRuntime = Runtime.getRuntime();
    private final long mMaxMemory = mRuntime.maxMemory();
//    private final long mFreeMemoryMarginSize = 6 * 1024 * 1024;
//    private final long mFreeMemoryMarginSize = mMaxMemory / 2;
    private final long mFreeMemoryMarginSize = mMaxMemory * 2 / 3;

    private boolean mUseCacheDecodeFirst = true;

    private final Rational mRationalVideoPlay = new Rational();
    private final Rational mRationalAudioPlay = new Rational();

    private CreateDrawBufferListener mListener;

    /**
     * NativeのNicoroFFmpegPlayerクラスのポインタ<br>
     *
     * TODO スレット終了待ちのタイムアウトで終了処理強行すると、ぬるぽ発生の可能性
     */
    private volatile long mNativeInstance;

    // Nativeからアクセスされるもの
    private FFmpegData mFFmpegData = new FFmpegData();

    public FFmpegVideoDecoder(SurfaceVideoDrawerInterface surfaceVideoDrawer,
            StreamAudioPlayerInterface streamAudioPlayer,
            FFmpegIOCallback ioCallback,
            Context context, int orientation,
            SeekerBase seekerBase) {
        this(surfaceVideoDrawer, streamAudioPlayer, ioCallback,
                context, orientation);
        mSeekerBase = seekerBase;
    }
    public FFmpegVideoDecoder(SurfaceVideoDrawerInterface surfaceVideoDrawer,
            StreamAudioPlayerInterface streamAudioPlayer,
            FFmpegIOCallback ioCallback,
            Context context, int orientation) {
        super("FFmpegVideoDecoder");

        mSurfaceVideoDrawer = surfaceVideoDrawer;
        mSurfaceVideoDrawer.setVideoBufferPool(this);
        mStreamAudioPlayer = streamAudioPlayer;
        mIOCallback = ioCallback;

        Resources res = context.getResources();
        SharedPreferences sharedPreferences = Util.getDefaultSharedPreferencesMultiProcess(context);
        mCacheWhenChoppy = sharedPreferences.getBoolean(
                res.getString(R.string.pref_key_ffmpeg_cache_when_choppy), false);
        mFullscreen16_9 = sharedPreferences.getBoolean(
                res.getString(R.string.pref_key_16_9_fullscreen), false);
        mUse16bitColor = sharedPreferences.getBoolean(
                res.getString(R.string.pref_key_ffmpeg_16bit_color), true);
        int drawBufferCacheSize = Integer.parseInt(
                sharedPreferences.getString(res.getString(
                        R.string.pref_key_ffmpeg_16bit_color_cache_size), "12"));
        VideoDrawBuffer.ByteBuffer16bit.setAllocateMaxSizeMB(drawBufferCacheSize);

        // layout整ってから設定
        setOrientation(orientation);
    }

    @Override
    public void run() {
        android.os.Process.setThreadPriority(
//                android.os.Process.THREAD_PRIORITY_DISPLAY);
                android.os.Process.THREAD_PRIORITY_AUDIO);

        mFFmpegData.mAudioBufferSize = 0;

        if (mUseCacheDecodeFirst) {
            // キャッシュ
            cacheDecodeFirst();
            if (mIsFinish) {
                return;
            }

            // キャッシュぶんの音声再生開始
            mStreamAudioPlayer.flushBufferToAudioTrack();
            mStreamAudioPlayer.flushAudioTrack();
        }

        // 再生開始
        mSurfaceVideoDrawer.start();

        // TODO: とりあえずAudioTrack開始までポーリング
        mStreamAudioPlayer.waitRealStartAtStart();

        mSurfaceVideoDrawer.updatePlayStartTime();
        sendVideoCacheToDrawer();
        decodeMain();
    }

    @Override
    public boolean isNull() {
        return false;
    }

    @Override
    public void quit() {
        mIsFinish = true;
        synchronized (mSyncDecode) {
            mSyncDecode.notifyAll();
        }
        synchronized (mSyncVideoBuffer) {
            mSyncVideoBuffer.notifyAll();
        }
        // TODO CountDownLatch使った方がいい？
        // スレッド完了していないと予期せぬ例外発生の可能性があるので、タイムアウト無しで
        try {
            join();
        } catch (InterruptedException e) {
            Log.d(LOG_TAG, e.toString(), e);
        }
//        synchronized (this) {
            destroyNativeInstance();
//        }
    }

    @Override
    public void quitAsync(ExecutorService executorService,
            final CountDownLatch latch) {
        if (mIsFinish) {
            latch.countDown();
        } else {
            mIsFinish = true;
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    quit();
                    latch.countDown();
                }
            });
        }
    }

    private void cacheDecodeFirst() {
        boolean getDrawBufferFailed = false;
        DECODE_LOOP : while (!mIsFinish) {
            if (mStreamAudioPlayer.hasEnoughCache()) {
                // キャッシュ終了
                if (DEBUG_LOGD) {
                    Log.d(LOG_TAG, "cache complete");
                }
                break;
            }

            // Videoデータの残りサイズチェック
            VideoDrawBuffer<?> videoBuffer = mVideoBufferPool.poll();
            if (videoBuffer == null) {
                videoBuffer = mVideoBufferEmptyPool.poll();
                if (videoBuffer == null) {
                    videoBuffer = createVideoDrawBuffer();
                }
            }
            Object drawBuffer = videoBuffer.strengthenBuffer();
            if (drawBuffer == null && !getDrawBufferFailed
                    /*&& mVideoBufferPool.size() + mSurfaceVideoDrawer.getStandbyBufferSize() < VIDEO_BUFFER_CACHE_SIZE*/) {
                try {
//                    drawBuffer = createDrawBuffer();
                    drawBuffer = videoBuffer.createDrawBuffer(
                            mFFmpegData.mWidth, mFFmpegData.mHeight);
                    if (drawBuffer == null) {
                        getDrawBufferFailed = true;
//                      // キャッシュ終了
//                      break;
//                    } else {
//                        videoBuffer.resetBuffer(drawBuffer);
                    }
                } catch (OutOfMemoryError e) {
                    getDrawBufferFailed = true;
//                  // キャッシュ終了
//                  break;
                }
            }

            int frameType = readFrame(
                    mNativeInstance, mReadBuffer, mIOCallback);
            assert videoBuffer != null;
            switch (frameType) {
            case CODE_FRAME_TYPE_VIDEO:
                int skipVideoFrame;
                if (drawBuffer == null) {
                    skipVideoFrame = CODE_SKIP_VIDEO_FRAME_NONREF;
                } else {
                    skipVideoFrame = CODE_SKIP_VIDEO_FRAME_NONE;
                }
                int decodeFrame = decodeVideoWrapper(drawBuffer, skipVideoFrame);
                if (decodeFrame == CODE_DECODE_FRAME_VIDEO) {
                    assert videoBuffer.hasBufferRef();
                    videoBuffer.frame = mFrameCountDecode;
                    mVideoBufferCached.add(videoBuffer);
                    ++mFrameCountDecode;
                } else if (decodeFrame == CODE_DECODE_FRAME_VIDEO_SKIP) {
//                    videoBuffer.buffer = null;
//                    videoBuffer.frame = mFrameCountDecode;
//                    mVideoBufferCached.add(videoBuffer);

                    // 描画送らずに戻す
                    if (!videoBuffer.hasBufferRef()) {
                        addToVideoBufferEmptyPool(videoBuffer); // 空としてpool
                    } else {
                        videoBuffer.weakenBuffer();
                        mVideoBufferPool.add(videoBuffer);  // 戻す
                    }

                    ++mFrameCountDecode;
                } else {
                    if (DEBUG_LOGD) {
                        Log.d(LOG_TAG, Log.buf().append("decodeVideo failed=")
                                .append(decodeFrame).toString());
                    }
                    if (!videoBuffer.hasBufferRef()) {
                        addToVideoBufferEmptyPool(videoBuffer); // 空としてpool
                    } else {
                        videoBuffer.weakenBuffer();
                        mVideoBufferPool.add(videoBuffer);  // 戻す
                    }
                }
                break;
            case CODE_FRAME_TYPE_AUDIO:
                if (!videoBuffer.hasBufferRef()) {
                    addToVideoBufferEmptyPool(videoBuffer); // 空としてpool
                } else {
                    videoBuffer.weakenBuffer();
                    mVideoBufferPool.add(videoBuffer);  // 戻す
                }
                if (decodeAudio(mNativeInstance, mFFmpegData)) {
                    if (DEBUG_LOGD) {
                        Log.d(LOG_TAG,
                                Log.buf().append("AudioCache: second buffer=")
                                .append(mStreamAudioPlayer.getCachedSize())
                                .append(" mFFmpegData.mAudioBufferSize=")
                                .append(mFFmpegData.mAudioBufferSize).toString());
                    }
                    mAudioDecodedBytes += mFFmpegData.mAudioBufferSize;
                    mStreamAudioPlayer.writeBuffer(mFFmpegData.mAudioBuffer, mFFmpegData.mAudioBufferSize / 2);
                } else {
                    if (DEBUG_LOGD) {
                        Log.d(LOG_TAG, "decodeAudio failed");
                    }
                }
                break;
            case CODE_FRAME_TYPE_ERROR_OR_END:
                if (DEBUG_LOGD) {
                    Log.d(LOG_TAG, "readFrame return CODE_FRAME_TYPE_ERROR_OR_END, cacheDecodeFirst cancel");
                }
                onReadAtEnd();
                break DECODE_LOOP;
            default:    // some error
                if (DEBUG_LOGD) {
                    Log.d(LOG_TAG, Log.buf().append("readFrame return value=")
                            .append(frameType).toString());
                }
                if (!videoBuffer.hasBufferRef()) {
                    addToVideoBufferEmptyPool(videoBuffer); // 空としてpool
                } else {
                    videoBuffer.weakenBuffer();
                    mVideoBufferPool.add(videoBuffer);  // 戻す
                }
                break;
            }
        }

    }

    private void decodeMain() {
        while (!mIsFinish) {
            if (mRequestSeekOnDecoder) {
                decodeOnSeek();
                clearReadAtEnd();
                continue;
            }

            if (mReadAtEnd) {
                synchronized (mSyncDecode) {
                    try {
                        mSyncDecode.wait(1000L);
                    } catch (InterruptedException e) {
                    }
                }
                continue;
            }

            if (mIsPause) {
                synchronized (mSyncDecode) {
                    // 無駄な同期避けるため二重チェック
                    if (mIsPause) {
                        try {
                            mSyncDecode.wait(1000L);
                        } catch (InterruptedException e) {
                        }
                    }
                }
                continue;
            }

            if (mCacheWhenChoppy) {
                if (mStreamAudioPlayer.getRemainDataSizeOnDeviceMs() == 0) {
                    if (DEBUG_LOGD) {
                        Log.d(LOG_TAG, "audio choppy: start cache");
                    }
                    // TODO 音声のない動画への対応
    //              mStreamAudioPlayer.pause();
//                  mIsCaching = true;
//                    int lastFrameCountDecode = mFrameCountDecode;
                    cacheDecodeFirst();
//                  mIsCaching = false;
                    if (mIsFinish) {
                        break;
                    }
    //              mStreamAudioPlayer.restart();
//                  writeBufferToAudioTrack();
                    mStreamAudioPlayer.flushBufferToAudioTrack();
                    mStreamAudioPlayer.flushAudioTrack();
                    // 仕切り直し
//                    mSurfaceVideoDrawer.updatePlayStartTime(lastFrameCountDecode);
//                    mSurfaceVideoDrawer.updatePlayStartTime(mFrameCountDecode);
                    mSurfaceVideoDrawer.updatePlayStartTime();
                    sendVideoCacheToDrawer();
                }
            }

            int frameType = readFrame(
                    mNativeInstance, mReadBuffer, mIOCallback);
            switch (frameType) {
            case CODE_FRAME_TYPE_VIDEO:
                while (!mIsFinish) {
                    VideoDrawBuffer<?> videoBuffer = mVideoBufferPool.poll();
                    if (videoBuffer == null) {
                        videoBuffer = mVideoBufferEmptyPool.poll();
                        if (videoBuffer == null) {
                            videoBuffer = createVideoDrawBuffer();
                        }
                    }
                    Object drawBuffer = videoBuffer.strengthenBuffer();
                    assert videoBuffer != null;
                    judgeFrameSkipAtDecoderThread();
                    if (drawBuffer == null) {
                        if (mFrameSkipDecode == CODE_SKIP_VIDEO_FRAME_NONE) {
                            try {
//                                if (mVideoBufferPool.size() + mSurfaceVideoDrawer.getStandbyBufferSize() < VIDEO_BUFFER_CACHE_SIZE) {
//                                    drawBuffer = createDrawBuffer();
                                    drawBuffer = videoBuffer.createDrawBuffer(
                                            mFFmpegData.mWidth, mFFmpegData.mHeight);
//                                }
                                if (drawBuffer == null) {
                                    addToVideoBufferEmptyPool(videoBuffer);
                                    // 処理が速すぎても問題が出る
//                                    mSurfaceVideoDrawer.drawAtOnce();
                                    // バッファが来るまで待つ
                                    if (DEBUG_LOGV) {
                                        Log.v(LOG_TAG, "createDrawBuffer failed: wait");
                                    }
                                    synchronized (mSyncVideoBuffer) {
                                        try {
                                            mSyncVideoBuffer.wait(10L);
                                        } catch (InterruptedException e) {
                                        }
                                    }
                                    continue;
//                                } else {
//                                    videoBuffer.resetBuffer(drawBuffer);
                                }
                            } catch (OutOfMemoryError e) {
                                addToVideoBufferEmptyPool(videoBuffer);
                                mSurfaceVideoDrawer.drawAtOnce();
                                // バッファが来るまで待つ
                                if (DEBUG_LOGV) {
                                    Log.v(LOG_TAG, "createDrawBuffer OutOfMemoryError: wait");
                                }
                                synchronized (mSyncVideoBuffer) {
                                    try {
                                        mSyncVideoBuffer.wait(10L);
                                    } catch (InterruptedException e1) {
                                    }
                                }
                                continue;
                            }
                        }
                    }
                    assert (drawBuffer == null) ? (mFrameSkipDecode != CODE_SKIP_VIDEO_FRAME_NONE) : true;
                    int decodeFrame = decodeVideoWrapper(drawBuffer,
                            mFrameSkipDecode);
                    if (decodeFrame == CODE_DECODE_FRAME_VIDEO) {
                        assert videoBuffer.hasBufferRef();
                        if (DEBUG_LOGV) {
                            Log.v(LOG_TAG, Log.buf().append("Decode time Video: ")
                                    .append(mFFmpegData.mTimeNumVideo).append("/")
                                    .append(mFFmpegData.mTimeDenVideo).append(" = ")
                                    .append(((mFFmpegData.mTimeDenVideo != 0)
                                            ? ((float)mFFmpegData.mTimeNumVideo / (float)mFFmpegData.mTimeDenVideo)
                                                    : "NAN")).toString());
                        }
                        videoBuffer.frame = mFrameCountDecode;
                        assert videoBuffer.isStrengthBuffer();
                        // 音声再生との差が大きくなりすぎたら調整
                        mSurfaceVideoDrawer.getCurrentPosition(mRationalVideoPlay);
                        mStreamAudioPlayer.getCurrentPosition(mRationalAudioPlay);
                        if (mRationalVideoPlay.getMs() - mRationalAudioPlay.getMs()
                                > 1000) {
                            if (DEBUG_LOGD) {
                                Log.d(LOG_TAG, "Fix video time by audio");
                            }
                            mSurfaceVideoDrawer.fixPlayStartTime(1000);
                        }
                        mSurfaceVideoDrawer.postDraw(videoBuffer);
                        ++mFrameCountDecode;
                    } else if (decodeFrame == CODE_DECODE_FRAME_VIDEO_SKIP) {
                        if (drawBuffer == null) {
//                          videoBuffer.buffer = null;
//                          videoBuffer.frame = mFrameCountDecode;
//                          mVideoBufferCached.add(videoBuffer);

                            // 描画送らずに戻す
                            addToVideoBufferEmptyPool(videoBuffer);

                            ++mFrameCountDecode;
                        } else {
                            videoBuffer.weakenBuffer();
                            mVideoBufferPool.add(videoBuffer);  // 戻す

                            ++mFrameCountDecode;
                        }
                    } else {
                        if (DEBUG_LOGD) {
                            Log.d(LOG_TAG, Log.buf().append("decodeVideo failed=")
                                    .append(decodeFrame).toString());
                        }
                        if (drawBuffer == null) {
                            addToVideoBufferEmptyPool(videoBuffer);
                        } else {
                            videoBuffer.weakenBuffer();
                            mVideoBufferPool.add(videoBuffer);  // 戻す
                        }
                    }
                    break;
                }
                break;
            case CODE_FRAME_TYPE_AUDIO:
//                final int audioBufferByteLength = mFFmpegData.mAudioBuffer.length * 2;
                final int audioBufferByteLength = mFFmpegData.mAudioBuffer.capacity();
                if (mStreamAudioPlayer.getRemainBufferByteSize() < audioBufferByteLength) {
                    mStreamAudioPlayer.flushBufferToAudioTrack();
                    while (!mIsFinish) {
                        if (mStreamAudioPlayer.getRemainBufferByteSize() < audioBufferByteLength) {
                            // バッファが来るまで待つ
                            SystemClock.sleep(1L);
                            mStreamAudioPlayer.flushBufferToAudioTrack();
                            continue;
                        } else {
                            break;
                        }
                    }
                }

                if (decodeAudio(mNativeInstance, mFFmpegData)) {
                    if (DEBUG_LOGV) {
                        Log.v(LOG_TAG, Log.buf().append("Decode time Audio: ")
                                .append(mFFmpegData.mTimeNumAudio).append("/")
                                .append(mFFmpegData.mTimeDenAudio).append(" = ")
                                .append(((mFFmpegData.mTimeDenAudio != 0)
                                        ? ((float)mFFmpegData.mTimeNumAudio / (float)mFFmpegData.mTimeDenAudio)
                                                : "NAN")).toString());
                    }
                    writeBufferToAudioTrack();
                } else {
                    if (DEBUG_LOGD) {
                        Log.d(LOG_TAG, "decodeAudio failed");
                    }
                }
                break;
            case CODE_FRAME_TYPE_ERROR_OR_END:
                if (DEBUG_LOGD) {
                    Log.d(LOG_TAG, "readFrame return CODE_FRAME_TYPE_ERROR_OR_END");
                }
                onReadAtEnd();
                break;
            default:    // some error
                if (DEBUG_LOGD) {
                    Log.d(LOG_TAG, Log.buf().append("readFrame return value=")
                            .append(frameType).toString());
                }
                break;
            }
        }
    }

    private void decodeOnSeek() {
        assert mRequestSeekSecond >= 0;
        mSurfaceVideoDrawer.startSeek();
        // 描画スレッドが止まるまでちょっと待つ（コメント操作でconflict起こすため）
        // TODO タイムアウトで抜けるとやっぱりconflict起こす
        mSurfaceVideoDrawer.waitIdlePlay();
        if (mIsFinish) {
            return;
        }

        if (mSeekerBase != null) {
            mSeekerBase.seekBySecondBase(mRequestSeekSecond);
        }

        // キャッシュにたまっているバッファをクリア
        while (true) {
            VideoDrawBuffer<?> videoBuffer = mVideoBufferCached.poll();
            if (videoBuffer == null) {
                break;
            }
            if (!videoBuffer.hasBufferRef()) {
                addToVideoBufferEmptyPool(videoBuffer);
            } else {
                videoBuffer.weakenBuffer();
                mVideoBufferPool.add(videoBuffer);
            }
        }
        mFFmpegData.mAudioBufferSize = 0;
//      mStreamAudioPlayer.clearBuffer();
        mStreamAudioPlayer.startSeek();

        // 切り上げ
        int den = mFFmpegData.mFrameRateNumVideo;
        long num = (long) mFrameCountDecode * mFFmpegData.mFrameRateDenVideo + den - 1;
        int currentSecond = (int) (num / den);
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("seek current=").append(currentSecond)
                    .append(" to=").append(mRequestSeekSecond).toString());
        }
        if (mRequestSeekSecond < currentSecond) {
            // 過去へのseekなら最初からやり直してスキップ

            // TODO flvだとseek効果無し
            final int newFrameCount = seekFrameBySecond(
                    mNativeInstance, 0);
            if (newFrameCount < 0) {
                if (DEBUG_LOGD) {
                    Log.d(LOG_TAG, Log.buf()
                            .append("seekFrameBySecond return=").append(newFrameCount)
                            .toString());
                }
                // seek失敗の場合は最初から
                reopenInputStream(
                        mNativeInstance, mReadBuffer, mIOCallback);
                mFrameCountDecode = 0;
            } else {
                mFrameCountDecode = 0;
            }

            // TODO 排他制御

            mAudioDecodedBytes = 0;

            mFFmpegData.mTimeNumVideo = 0;
            mFFmpegData.mTimeNumAudio = 0;
        }

//        int audioDummyData = mStreamAudioPlayer.writeDummyDataAtSeek();

        DECODE_LOOP : while (!mIsFinish) {
            // 切り上げ
            den = mFFmpegData.mFrameRateNumVideo;
            num = (long) mFrameCountDecode * mFFmpegData.mFrameRateDenVideo + den - 1;
            currentSecond = (int) (num /den);
//          long currentMs = num * 1000L / den;
            if (DEBUG_LOGV) {
                Log.v(LOG_TAG, Log.buf().append("seek current=").append(currentSecond)
                        .append(" to=").append(mRequestSeekSecond).toString());
            }
            if (mRequestSeekSecond <= currentSecond) {
                break;
            }

            // TODO かなり無駄だがある程度デコード処理やらないと計算合わなくなる？
            // TODO frameだけ読むならkeyframeのタイミングで次の描画再開を行わないと画面化けする
            // →ちゃんとやるなら数秒前からkeyframeを探し出して数秒後までの間に見つけたポイントが便宜上のシーク位置
            int frameType = readFrame(
                    mNativeInstance, mReadBuffer, mIOCallback);
            switch (frameType) {
            case CODE_FRAME_TYPE_VIDEO:
                if (mRequestSeekSecond - currentSecond <= 2) {
                    int decodeFrame = decodeVideo(
                            mNativeInstance, null, mFFmpegData, CODE_SKIP_VIDEO_FRAME_NONREF);
                    if (decodeFrame != CODE_DECODE_FRAME_VIDEO_SKIP) {
                        if (DEBUG_LOGD) {
                            Log.d(LOG_TAG, Log.buf().append("decodeVideo unexpected result=")
                                    .append(decodeFrame).toString());
                        }
                    }

//                      VideoBuffer videoBuffer = mVideoBufferEmptyPool.poll();
//                      if (videoBuffer == null) {
//                          videoBuffer = new VideoBuffer();
//                      }
//                      videoBuffer.buffer = null;
//                      videoBuffer.frame = mFrameCountDecode;
//                      mVideoBufferCached.add(videoBuffer);
                }

                // TODO 排他制御

                ++mFrameCountDecode;
                break;
            case CODE_FRAME_TYPE_AUDIO:
                // TODO シーク時の微妙な音ずれおかしいから要調査
                // →シーク時間が長くて再生が途切れると発生？
                // 動画内の画像と音声の配置構成にも影響されるかも

////                if (mRequestSeekSecond * 1000L - currentMs <= 500L) {
//                if (mRequestSeekSecond - currentSecond <= 2) {
//                    if (decodeAudio(mNativeInstance, mFFmpegData)) {
//                        if (DEBUG_LOGD) {
//                            Log.d(LOG_TAG,
//                                    Log.buf().append("AudioSeek: second buffer=")
//                                    .append(mStreamAudioPlayer.getCachedSize())
//                                    .append(" mFFmpegData.mAudioBufferSize=")
//                                    .append(mFFmpegData.mAudioBufferSize).toString());
//                        }
//                        mAudioDecodedBytes += mFFmpegData.mAudioBufferSize;
//                        // TODO バッファ溢れどうしよう
////                        mStreamAudioPlayer.writeBuffer(mFFmpegData.mAudioBuffer, mFFmpegData.mAudioBufferSize / 2);
//                        mStreamAudioPlayer.writeBufferOnlyPool(mFFmpegData.mAudioBuffer, mFFmpegData.mAudioBufferSize / 2);
//                    } else {
//                        if (DEBUG_LOGD) {
//                            Log.d(LOG_TAG, "decodeAudio failed");
//                        }
//                    }
//                }
                // TODO 一時停止中のシークがうまく動作せず
//                mStreamAudioPlayer.writeDummyDataAtSeekForAlive();
                break;
            case CODE_FRAME_TYPE_ERROR_OR_END:
                if (DEBUG_LOGD) {
                    Log.d(LOG_TAG, "readFrame return CODE_FRAME_TYPE_ERROR_OR_END, seek cancel");
                }
                onReadAtEnd();
                break DECODE_LOOP;
            default:    // some error
                if (DEBUG_LOGD) {
                    Log.d(LOG_TAG, Log.buf().append("readFrame return value=")
                            .append(frameType).toString());
                }
                break;
            }
        }
        if (mIsFinish) {
            return;
        }

//      final int newFrameCount = seekFrameBySecond(mNativeInstance, mRequestSeekSecond);
//      if (DEBUG_LOGD) {
//          Log.d(LOG_TAG, Log.buf()
//                  .append("seekFrameBySecond return=").append(newFrameCount)
//                  .append(" second=").append(mRequestSeekSecond).toString());
//      }
//      if (newFrameCount >= 0) {
//          mFrameCountDecode = newFrameCount;
//      }

        int lastFrameCountDecode = mFrameCountDecode;
//        cacheDecodeFirst();
//        if (mIsFinish) {
//            break;
//        }

////        writeBufferToAudioTrack();
//        mStreamAudioPlayer.flushBufferToAudioTrack();
//        mStreamAudioPlayer.flushAudioTrack();
//
//        // TODO: とりあえずAudioTrack再開までポーリング
////        mStreamAudioPlayer.waitRealStart();
//        mStreamAudioPlayer.waitPlayForward();

////        int audioDummyData = mStreamAudioPlayer.writeDummyDataAtSeek();
//      if (audioDummyData >= 0) {
//          mStreamAudioPlayer.waitRealStartAtSeek();
//      }

        den = mFFmpegData.mFrameRateNumVideo;
        num = (long) mFrameCountDecode * mFFmpegData.mFrameRateDenVideo + den - 1;
        int currentMilliSecond = (int) (num * 1000L / den);
        mStreamAudioPlayer.endSeek(currentMilliSecond);

        mSurfaceVideoDrawer.updatePlayStartTime(lastFrameCountDecode);
//      sendVideoCacheToDrawer();

        mRequestSeekSecond = -1;
        mRequestSeekOnDecoder = false;

        // シークバー再有効化
        mSeekerBase.enableSeekBar();
    }

    private void sendVideoCacheToDrawer() {
        for (VideoDrawBuffer<?> videoBuffer : mVideoBufferCached) {
            mSurfaceVideoDrawer.postDraw(videoBuffer);
        }
        mVideoBufferCached.clear();
    }

    private boolean hasEnoughFreeMemory(long size) {
        final Runtime runtime = mRuntime;
        final long freeSize = mMaxMemory - runtime.totalMemory() + runtime.freeMemory();
        final boolean ret = (freeSize > size + mFreeMemoryMarginSize);
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("hasEnoughFreeMemory() maxMemory=")
                    .append(mMaxMemory).append(" freeSize=")
                    .append(freeSize).append(" return=")
                    .append(ret)
                    .toString());
        }
        return ret;
    }

////    private int[] createDrawBuffer() {
//    private Object createDrawBuffer() {
//        assert (mFFmpegData.mWidth > 0 && mFFmpegData.mHeight > 0);
//        final int area = mFFmpegData.mWidth * mFFmpegData.mHeight;
//        if (mUse16bitColor) {
//            // TODO directメモリに関する計測方法
////            if (hasEnoughFreeMemory(area * 2)) {
//                return ByteBuffer.allocateDirect(area * 2);
////            } else {
////                return null;
////            }
//        } else {
//            if (hasEnoughFreeMemory(area * 4)) {
//                return new int[area];
//            } else {
//                return null;
//            }
//        }
//    }

    private void addToVideoBufferEmptyPool(VideoDrawBuffer<?> videoBuffer) {
        if (mVideoBufferEmptyPool.size() < VIDEO_BUFFER_EMPTY_POOL_MAX) {
            mVideoBufferEmptyPool.add(videoBuffer);
        }
    }

    @Override
    public void addToFill(VideoDrawBuffer<?> videoBuffer) {
        mVideoBufferPool.add(videoBuffer);
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, Log.buf().append("addToFill() mVideoBufferPool.size=")
                    .append(mVideoBufferPool.size()).toString());
        }
        synchronized (mSyncVideoBuffer) {
            mSyncVideoBuffer.notifyAll();
        }
    }
    @Override
    public void addToEmpty(VideoDrawBuffer<?> videoBuffer) {
        addToVideoBufferEmptyPool(videoBuffer);
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, Log.buf().append("addToEmpty() mVideoBufferEmptyPool.size=")
                    .append(mVideoBufferEmptyPool.size()).toString());
        }
        synchronized (mSyncVideoBuffer) {
            mSyncVideoBuffer.notifyAll();
        }
    }

    @Override
    public void seekBySecond(int second) {
        mRequestSeekSecond = second;
        mRequestSeekOnDecoder = true;
        synchronized (mSyncDecode) {
            mSyncDecode.notifyAll();
        }
    }

    @Override
    public void pause() {
        synchronized (mSyncDecode) {
            mIsPause = true;
            mSyncDecode.notifyAll();
        }
    }
    @Override
    public void restart() {
        synchronized (mSyncDecode) {
            mIsPause = false;
            mSyncDecode.notifyAll();
        }
    }

    private void judgeFrameSkipAtDecoderThread() {
        // 音声基準
        // FIXME 音声のない動画への対応
        final int audioRemainMs = mStreamAudioPlayer.getRemainDataSizeOnDeviceMs();
        final int frameDurationMs = getFrameDurationMs();

//      final boolean hasLongFrameskip = mSurfaceVideoDrawer.hasLongFrameskip();

        int frameSkipDecode;
        if (mIsVisible) {
            frameSkipDecode = CODE_SKIP_VIDEO_FRAME_NONE;
        } else {
            frameSkipDecode = CODE_SKIP_VIDEO_FRAME_SIMPLE;
        }
        // TODO パラメータ調整
        if (audioRemainMs == 0) {
            if (!mCacheWhenChoppy) {
                // 処理落ちが大きくなったら仕切り直し
                mSurfaceVideoDrawer.updatePlayStartTime();
            }
        } else if (audioRemainMs < mSkipJudgeAudioStrongMs) {
            frameSkipDecode = CODE_SKIP_VIDEO_FRAME_UNTIL_KEY_FRAME;
        } else if (audioRemainMs < mSkipJudgeAudioMs) {
//            frameSkipDecode = CODE_SKIP_VIDEO_FRAME_NONREF;
//            frameSkipDecode = CODE_SKIP_VIDEO_FRAME_NONKEY;
            frameSkipDecode = CODE_SKIP_VIDEO_FRAME_SIMPLE;
//            frameSkipDecode = CODE_SKIP_VIDEO_FRAME_UNTIL_KEY_FRAME;
        }
//      if (frameSkipDecode == CODE_SKIP_VIDEO_FRAME_NONREF) {
//          if (hasLongFrameskip) {
//                frameSkipDecode = CODE_SKIP_VIDEO_FRAME_NONE;
//          }
//      }
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, Log.buf().append("judgeFrameSkipAtDecoderThread() frameSkipDecode=")
                    .append(frameSkipDecode)
                    .append(" frameDurationMs=").append(frameDurationMs)
                    .append(" audioRemainMs=").append(audioRemainMs)
//                  .append(" hasLongFrameskip=").append(hasLongFrameskip)
                    .toString());
        }
        mFrameSkipDecode = frameSkipDecode;
    }

    private int getFrameDurationMs() {
        return 1 * 1000 * mFFmpegData.mFrameRateDenVideo / mFFmpegData.mFrameRateNumVideo;
    }

    private void writeBufferToAudioTrack() {
        if (DEBUG_LOGV) {
            Log.v(LOG_TAG, Log.buf().append("writeBufferToAudioTrack: mAudioBufferSize=")
                    .append(mFFmpegData.mAudioBufferSize).toString());
        }
        if (!mStreamAudioPlayer.canPlay()) {
            Log.d(LOG_TAG, "mAudioTrack is null");
            return;
        }

        mAudioDecodedBytes += mFFmpegData.mAudioBufferSize;
        mStreamAudioPlayer.writeBuffer(mFFmpegData.mAudioBuffer, mFFmpegData.mAudioBufferSize / 2);

//      if (mAudioBufferSecondOffset > 44100 * 2 / 10) {
//      if (mAudioBufferSecondOffset > mAudioBufferSecond.length / 2) {
            mStreamAudioPlayer.flushBufferToAudioTrack();
//          mAudioTrack.flush();
//      }
    }

    @Override
    public boolean prepareFFmpeg(boolean isOpened) {
        destroyNativeInstance();
        for (int i = 0; i < 3; ++i) {
            mNativeInstance = createNative();
            if (mNativeInstance != 0) {
                break;
            }
            Util.gcStrong();
            SystemClock.sleep(10);
        }
        if (mNativeInstance == 0) {
            // メモリ不足でnew失敗
            throw new OutOfMemoryError("failed to create FFmpegVideoDecoder native instance");
        }

        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append(getClass().getName())
                    .append("prepareFFmpeg: loadStream start").toString());
        }
        loadStream(mNativeInstance, mReadBuffer, isOpened,
                mIOCallback, mFFmpegData);
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append(getClass().getSimpleName())
                    .append("#prepareFFmpeg: loadStream end, FrameRate=")
                    .append(mFFmpegData.mFrameRateNumVideo).append("/")
                    .append(mFFmpegData.mFrameRateDenVideo).toString());
        }

        if (isOpened && (mFFmpegData.mFrameRateDenVideo == 0 || mFFmpegData.mFrameRateNumVideo == 0)) {
            // 動画読み込み完了後にもう１回チャレンジ
            return false;
        }

        if (ThreadUtil.isInMainThread()) {
            prepareFFmpegOnMainThread();
        } else {
            final boolean[] sync = new boolean[] { false };
            final IllegalArgumentException[] exception = new IllegalArgumentException[1];
            synchronized (sync) {
                Handler handler = mSurfaceVideoDrawer.getViewHandler();
                if (handler == null) {
                    assert false;
                    handler = new Handler(Looper.getMainLooper());
                }
                handler.post(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            prepareFFmpegOnMainThread();
                        } catch (IllegalArgumentException e) {
                            exception[0] = e;
                        } finally {
                            synchronized (sync) {
                                sync[0] = true;
                                sync.notifyAll();
                            }
                        }
                    }
                });
                while (!sync[0]) {
                    try {
                        sync.wait();
                    } catch (InterruptedException e) {
                        Log.d(LOG_TAG, e.toString(), e);
                    }
                }
            }
            if (exception[0] != null) {
                throw exception[0];
            }
        }
        return true;
    }

    private void prepareFFmpegOnMainThread() {
        mSurfaceVideoDrawer.setFrameDurationMs(getFrameDurationMs());
        mSurfaceVideoDrawer.setFrameRate(mFFmpegData.mFrameRateNumVideo,
                mFFmpegData.mFrameRateDenVideo);

        createAudioTrack(mNativeInstance, this);
        createDrawBuffer(mNativeInstance, this);

        final int frameDurationMs = getFrameDurationMs();
        // TODO スキップパラメータ調整
//        mSkipJudgeAudioMs = Math.min(frameDurationMs*4, 250);
        mSkipJudgeAudioMs = Math.min(frameDurationMs*4, 166);
//        mSkipJudgeAudioMs = Math.min(frameDurationMs*2, 250);
        mSkipJudgeAudioStrongMs = Math.min(frameDurationMs*1, 16);
    }

    @Override
    public void prepareDecode() {
        int audioBufferByteSize = mStreamAudioPlayer.getAudioBufferByteSize();
        for (int i = 0; i < 3; ++i) {
            try {
//                mFFmpegData.mAudioBuffer = new short[audioBufferByteSize/2];
                mFFmpegData.mAudioBuffer = ByteBuffer.allocateDirect(audioBufferByteSize);
                break;
            } catch (OutOfMemoryError e) {
                Log.e(LOG_TAG, e.toString(), e);
            }
        }
        mFFmpegData.mAudioBufferSize = 0;
    }

    @Override
    public void destroyNativeInstance() {
        long nativeInstance = mNativeInstance;
        mNativeInstance = 0L;
        destroyNative(nativeInstance);
    }

    @Override
    public void getCurrentPositionVideoDecode(Rational rational) {
        rational.num = mFFmpegData.mTimeNumVideo;
        rational.den = mFFmpegData.mTimeDenVideo;
    }
    @Override
    public void getCurrentPositionAudioDecode(Rational rational) {
        // TODO シークすると破綻
        rational.num = mAudioDecodedBytes;
        rational.den = mStreamAudioPlayer.getSampleRate() * mStreamAudioPlayer.getAudioSampleByteSize();
    }

    // Nativeからアクセスされるもの

    @Override
    public void createAudioTrackFromNativeCallback(int sample_rate, int channels, int sample_fmt) {
        try {
            if (DEBUG_LOGD) {
                Log.d(LOG_TAG, Log.buf().append("createAudioTrackFromNativeCallback: sample_rate=")
                        .append(sample_rate).append(" channels=").append(channels)
                        .append(" sample_fmt=").append(sample_fmt).toString());
            }
            mStreamAudioPlayer.startByFFmpeg(sample_rate, channels, sample_fmt);
        } catch (IllegalArgumentException e) {
            // TODO AudioTrackのパラメータエラーはそのまま通知
            // 本来は非対応サンプルレートの音声の場合は変換して再生するべき
            throw e;
//        } catch (Throwable e) { // やっぱり上まで通知
//            Log.e(LOG_TAG, e.toString(), e);
        }
    }

    @Override
    public void createDrawBufferFromNativeCallback(int width, int height) {
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("createDrawBufferFromNativeCallback: width=")
                    .append(width).append(" height=").append(height).toString());
        }
        mVideoOriginalWidth = width;
        mVideoOriginalHeight = height;

//        if (false) {
//            // 強制的に画面サイズに拡大して補完処理
//            // →予想以上に重かったので没
//            if (displayWidth * height < displayHeight * width) {
//                // widthで合わせる
//                mFFmpegData.mWidth = displayWidth;
//                mFFmpegData.mHeight = height * displayWidth / width;
//            } else {
//                // heightで合わせる
//                mFFmpegData.mWidth = width * displayHeight / height;
//                mFFmpegData.mHeight = displayHeight;
//            }
//        } else {
//            if (width > displayWidth || height > displayHeight) {
//                if (displayWidth * height < displayHeight * width) {
//                    // widthで合わせる
//                    mFFmpegData.mWidth = displayWidth;
//                    mFFmpegData.mHeight = height * displayWidth / width;
//                } else {
//                    // heightで合わせる
//                    mFFmpegData.mWidth = width * displayHeight / height;
//                    mFFmpegData.mHeight = displayHeight;
//                }
//            } else {
//                mFFmpegData.mWidth = width;
//                mFFmpegData.mHeight = height;
//            }
//        }
        // 画面回転との兼ね合いもあるので、動画サイズは画面サイズ等によらずそのままで
        mFFmpegData.mWidth = width;
        mFFmpegData.mHeight = height;
        if (DEBUG_LOGD) {
            Log.d(LOG_TAG, Log.buf().append("mWidth=").append(mFFmpegData.mWidth)
                    .append(" mHeight=").append(mFFmpegData.mHeight).toString());
        }

//        final Runtime runtime = Runtime.getRuntime();
//        int videoBufferCacheSize = (int) (runtime.freeMemory()
//                / (mFFmpegData.mWidth * mFFmpegData.mHeight * 4));
//        videoBufferCacheSize = Math.min(videoBufferCacheSize, VIDEO_BUFFER_CACHE_SIZE);
//        videoBufferCacheSize = Math.max(videoBufferCacheSize, 1);
//        if (DEBUG_LOGD) {
//            Log.d(LOG_TAG, Log.buf().append("videoBufferCacheSize=")
//                    .append(videoBufferCacheSize).toString());
//        }

//        try {
//            for (int i = 0; i < videoBufferCacheSize; ++i) {
//                mVideoBufferPool.add(createDrawBuffer());
//            }
//        } catch (OutOfMemoryError e) {
//            Log.w(LOG_TAG, "OutOfMemoryError at caching video buffer");
//        }

        mSurfaceVideoDrawer.setVideoSize(mFFmpegData.mWidth, mFFmpegData.mHeight);
        updateSurfaceSize();

        if (mListener != null) {
            mListener.onCreateDrawBuffer(this);
        }
    }

    @Override
    public void updateSurfaceSize() {
        int videoWidth = mFFmpegData.mWidth;
        int videoHeight = mFFmpegData.mHeight;
        if (videoWidth <= 0 || videoHeight <= 0) {
            return;
        }

        mComputeVideoDisplaySize.compute(
                videoWidth, videoHeight,
                mDisplayWidth, mDisplayHeight,
                mOrientation, mFullscreen16_9);
        mSurfaceVideoDrawer.setViewSize(
                mComputeVideoDisplaySize.getWidth(),
                mComputeVideoDisplaySize.getHeight());
    }

    @Override
    public int getVideoOriginalWidth() {
        return mVideoOriginalWidth;
    }
    @Override
    public int getVideoOriginalHeight() {
        return mVideoOriginalHeight;
    }

    private VideoDrawBuffer<?> createVideoDrawBuffer() {
        if (mUse16bitColor) {
            return new VideoDrawBuffer.ByteBuffer16bit();
        } else {
            return new VideoDrawBuffer.IntArray32bit();
        }
    }
    private int decodeVideoWrapper(Object drawBuffer, int skipVideoFrame) {
        if (mUse16bitColor) {
            return decodeVideo16bit(mNativeInstance,
                    (ByteBuffer) drawBuffer, mFFmpegData, skipVideoFrame);
        } else {
            return decodeVideo(mNativeInstance,
                    (int[]) drawBuffer, mFFmpegData, skipVideoFrame);
        }
    }

    @Override
    public void setCacheDecodeFirst(boolean use) {
        mUseCacheDecodeFirst = use;
    }

    @Override
    public void updateDisplayMetrics(Activity activity) {
        activity.getWindowManager().getDefaultDisplay().getMetrics(mDisplayMetrics);
        mDisplayWidth = mDisplayMetrics.widthPixels;
        mDisplayHeight = mDisplayMetrics.heightPixels;
    }
    @Override
    public void updateDisplaySize(int width, int height) {
        mDisplayWidth = width;
        mDisplayHeight = height;
    }
    @Override
    public void setOrientation(int orientation) {
        mOrientation = orientation;
    }

    private void onReadAtEnd() {
        mReadAtEnd = true;
        mSurfaceVideoDrawer.setDecodeFinished(true);
    }
    private void clearReadAtEnd() {
        mReadAtEnd = false;
        mSurfaceVideoDrawer.setDecodeFinished(false);
    }

    @Override
    public void setIsVisible(boolean isVisible) {
        mIsVisible = isVisible;
    }

    @Override
    public boolean isFinish() {
        return mIsFinish;
    }

    @Override
    public void setCreateDrawBufferListener(CreateDrawBufferListener listener) {
        mListener = listener;
    }

    // Nativeメソッド

    /*private*/ native long createNative();
    /*private*/ native void destroyNative(long nativeInstance);
    /*private*/ native void loadFile(long nativeInstance, String file);
    /*private*/ native void loadStream(long nativeInstance, byte[] buffer, boolean isOpened,
            FFmpegIOCallback ioCallback, FFmpegData data);
    /*private*/ native void createAudioTrack(long nativeInstance, FFmpegInfoCallback infoCallback);
    /*private*/ native void createDrawBuffer(long nativeInstance, FFmpegInfoCallback infoCallback);
    /*private*/ native int decodeFrame(long nativeInstance,
            int[] drawBuffer, int skipVideoFrame, byte[] readBuffer, FFmpegIOCallback ioCallback, FFmpegData data);
    /*private*/ native int readFrame(long nativeInstance,
            byte[] readBuffer, FFmpegIOCallback ioCallback);
    /*private*/ native int decodeVideo(long nativeInstance,
            int[] drawBuffer, FFmpegData data, int skipVideoFrame);
    /*private*/ native int decodeVideo16bit(long nativeInstance,
            ByteBuffer drawByteBuffer, FFmpegData data, int skipVideoFrame);
    /*private*/ native boolean decodeAudio(long nativeInstance,
            FFmpegData data);
    /*private*/ native int seekFrameBySecond(long nativeInstance, int second);
    /*private*/ native void reopenInputStream(long nativeInstance, byte[] buffer, FFmpegIOCallback ioCallback);
    /*private*/ native void setRescaleFlag(long nativeInstance, int flag);
    public static native void copyDirect(ByteBuffer src, int srcPos,
            short[] dst, int dstPos, int lengthBytes);

    private static boolean sLoadedNativeLibrary;
    private static final Object mSyncLoadNativeLibrary;
    static {
        mSyncLoadNativeLibrary = new Object();
        loadNativeLibrary();
    }

    public static void loadNativeLibrary() {
        try {
            synchronized (mSyncLoadNativeLibrary) {
                if (!sLoadedNativeLibrary) {
//                    System.loadLibrary("ssl");
//                    System.loadLibrary("crypto");
                    System.loadLibrary("polarssl");
                    System.loadLibrary("rtmp");
                    System.loadLibrary("nicoro-jni");
                    sLoadedNativeLibrary = true;
                }
            }
        } catch (UnsatisfiedLinkError e) {
            Log.e(LOG_TAG, "loadNativeLibrary failed", e);
            try {
                NicoroApplication app = NicoroApplication.getInstance();
                if (app != null) {
                    NotificationController nc = NotificationController.getInstance(
                            app);
                    nc.addErrorBeforeCrash(
                            app.getString(R.string.notification_title_error_load_native_library),
                            app.getString(R.string.notification_text_error_load_native_library));
                }
            } catch (Throwable t) {
                Log.e(LOG_TAG, "Notify failed at loadNativeLibrary error", t);
            }
            throw e;
        }
    }
}
