ケルベロスさんのプログラミング / けるぷろ

プログラマのけるさんことケルベロスです

Androidで動画プレイヤーアプリを作る

Androidで動画プレイヤーアプリを作る

VideoViewというクラスを使えば簡単な動画プレイヤーを作ることはできるのですが
今回は勉強のためにあえてVideoViewの内部で使われているSurfaceViewとMediaPlayerを使用して動画プレイヤーを作っていきます

主に使用するパッケージやクラス

開発環境

Android 5.0 ~
Android Studio 2.3.3

動画再生までの流れ

  • SurfaceViewを有効にする
  • MediaPlayerを初期化する
  • SurfaceViewにPlayerを紐づける
  • 再生中の時間表示等のためのスレッドを開始させる
  • そのスレッドでPlayerの状態を監視して画面表示を更新していく

準備

Manifestに追加する

<uses-permission android:name="android.permission.INTERNET"/>

MoviePlayerFragmentの実装

Fragmentに動画URIを渡す

MoviePlayerFragmentにイニシャライズメソッドを定義してFragment生成時に動画のURIを渡す処理をする

.. 省略

private String mPlayingUri;
private static final String PLAYING_URI_KEY = "PLAYING_URI";

.. 省略

public static MoviePlayerFragment newInstance(String uri) {

    Bundle args = new Bundle();

    MoviePlayerFragment fragment = new MoviePlayerFragment();
    args.putString(PLAYING_URI_KEY,uri);
    fragment.setArguments(args);
    return fragment;
}

.. 省略

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
                         Bundle savedInstanceState) {
    View view = inflater.inflate(R.layout.fragment_movie_player, container, false);

    mPlayingUri = getArguments().getString(PLAYING_URI_KEY);

.. 省略

SurfaceViewの準備をする

動画を描画するView

.. 省略

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_movie_player, container, false);

        mPlayingUri = getArguments().getString(PLAYING_URI_KEY);

        mSurfaceView = (SurfaceView) view.findViewById(R.id.surface_view);
        mSurfaceHolder = mSurfaceView.getHolder();                             // ->(1)
        mSurfaceHolder.addCallback(mSurfaceCallback);

.. 省略

    SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() {
        @Override
        public void surfaceCreated(SurfaceHolder surfaceHolder) {              // ->(2)
            mPlayPauseButton.setAlpha(1);                                      // ->(3)
            mLoopButton.setAlpha(1);
            mSeekBar.setAlpha(1);
        }

        @Override
        public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) { // ->(4)
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder surfaceHolder) {                 // ->(5)
        }
    };

.. 省略

(1) SurfaceHolderを取得してコールバックを登録
(2) SurfaceViewの準備が完了したタイミング
(3) 再生コントロールのUIを使用可能にする
(4) SurfaceViewの状態が変更された
(5) SurfaceViewが破棄された

MediaPlayerの準備をする

.. 省略

mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() { // ->(6)
    @Override
    public void onVideoSizeChanged(MediaPlayer mediaPlayer, int w, int h) {
        int winWidth = getView().getWidth();
        int winHeight = getView().getHeight();
        if (w > h || (w == h && winWidth < winHeight)) {
            int width = winWidth;
            float p = (float) h / (float) w;
            int height = (int) ((float) width * p);
            mSurfaceHolder.setFixedSize(width, height);
        } else  {
            int height = winHeight;
            float p = (float) w / (float) h;
            int width = (int) ((float) height * p);
            mSurfaceHolder.setFixedSize(width, height);
        }
    }
});
mMediaPlayer.setDataSource(mPlayingUri);
mMediaPlayer.setDisplay(mSurfaceHolder);                                                // ->(7)
mMediaPlayer.prepare();                                                                 // ->(8)
mMediaPlayer.start();
mSeekBar.setMax(mMediaPlayer.getDuration());                                            // ->(9)

.. 省略

(6) 動画のサイズに応じてSurfaceViewのサイズを変更させるためのコールバック追加
動画のサイズの倍率に合わせてFragment内で最大化する
(7) SurfaceViewと紐づける
(8) 再生準備 開始
(9) シークバーの最大値を動画のサイズに設定する

再生中動画の状態を取得してUIを更新していく処理を実装する

別スレッドを立てて監視をする

.. 省略

public class MoviePlayerFragment extends Fragment implements Runnable {  // ->(10)

.. 省略

private boolean mRunning;
private Thread mPlayerThread;
private final DecimalFormat mFormatterMin = new DecimalFormat("#");
private final DecimalFormat mFormatterSec = new DecimalFormat("00");agment.java

.. 省略

    mRunning = true;                                                         // ->(11)
    mPlayerThread = new Thread(this);
    mPlayerThread.start();

.. 省略

public void run() {                                                         
    while(mRunning) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        mHandler.sendMessage(Message.obtain());                            
    }
}

public void stopRunning() {
    mRunning = false;
}

private Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        if (mMediaPlayer == null) {
            return;
        }
        mSeekBar.setProgress(mMediaPlayer.getCurrentPosition());          // ->(12)

        String playButtonText = mMediaPlayer.isPlaying() ? "停止" : "再生"; // ->(13)
        mPlayPauseButton.setText(playButtonText);

        String loopButtonText = mMediaPlayer.isLooping() ? "連続" : "一回";
        mLoopButton.setText(loopButtonText);

        int leftTime = mMediaPlayer.getCurrentPosition()/1000;            // ->(14)
        String leftMinText = mFormatterMin.format((int)(leftTime / 60));
        String leftSecText = mFormatterSec.format((int)(leftTime % 60));
        int rightTime = mMediaPlayer.getDuration()/1000 - leftTime;
        String rightMinText = mFormatterMin.format((int)(rightTime / 60));
        String rightSecText = mFormatterSec.format((int)(rightTime % 60));
        mLeftTimeText.setText(leftMinText +":"+ leftSecText);
        mRightTimeText.setText("-"+rightMinText +":"+ rightSecText);
    }
};


.. 省略

(10) Runnableをimplementしておく
(11) 別スレッドをスタートさせる
(12) シークバーの表示更新
(13) 再生ボタンの表示更新
(14) 再生時間の表示更新

シークバー操作の実装

.. 省略

mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
    @Override
    public void onProgressChanged(SeekBar seekBar, int i, boolean b) {  // ->(15)
        if (null != mMediaPlayer) {
            if (!mMediaPlayer.isPlaying()) {
                mMediaPlayer.seekTo(i);
            }
        }
    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {              // ->(16)
        if (null != mMediaPlayer) {
            mMediaPlayer.pause();
        }
    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {               // ->(17)
        if (null != mMediaPlayer) {
            mMediaPlayer.start();
        }
    }
});

.. 省略

ラッキング開始でMediaPlayerを一時停止してシークを開始して、トラッキングから指を話した時点で再生を再開する
一時停止中にトラッキングをすると再生開始してしまうがフラグ等で状態を管理する実装をするつもり

(15) プログレスが変更されたときの通知
(16) トラッキング開始時
(17) トラッキング終了時

サンプルコード

上記アプリ全体のサンブルコードはgithubにありますので参考にしていただければと思います。
https://github.com/rdme/MoviePlayerSample