2011年8月29日月曜日

MediaPlayerでStreaming的な再生 その3

前回、4つの方法を試そう、とありましたが、
最初に試した方法でなかなかな感じに仕上がりましたので
2以降は試していません。
さて、1の方法について書きます。


RandomAccessFileを使用して、ダウンロードファイルサイズのファイルを作成、書き込みを行う


一番最初に試した方法では、ファイルへの書き込み自体はうまくいってましたが、
肝心の再生が、setDataSourceを行った時点までしか再生されない結果になっていました。
ここで、考えたのが、setした時点のファイル情報から、シーク可能な範囲が決定されているのではないのか、ということです。
そのため、前のエントリでの1と4の方法を考えたわけです。

で、この方法では、RandomAccessFileクラスを利用して、あらかじめ音楽ファイルのサイズのファイルを作成して、順次書き込んでいこうという方法です。
色々とややこしいですが
以下に必要な分のソースを置いておきます。

public class StreamingPlayer implements OnCompletionListener {

    private RandomAccessFile tempFile;
    private MediaPlayer player;
    private long fileSize;
    private long bufferdSize;
    private long musicLeng;
    private ScheduledExecutorService scheduled;
    private ScheduledFuture schFuture = null;
    private PlayerState state;
    private boolean isWatching = false;

    //独自のConnectionManagerクラス
    //sendMusicObjectメソッドを使うことで、対象のサーバに接続、
    //送信が行えます。そういうものです。
    private ConnectionManager manager;

    private final int THREAD_WATCH_INTERVAL = 1;

    //現在のプレイヤーの状態
    static private enum PlayerState{
        STOP,        //停止中
        PLAYING,    //再生中
        PAUSE,        //一時停止中
        BUFFERING,    //バッファ待ち
    }

    public StreamingPlayer(ConnectionManager manager) {
        this.manager = manager;
        player = new MediaPlayer();
        player.setOnCompletionListener(this);
        scheduled = Executors.newSingleThreadScheduledExecutor();
    }

    /**
     * 再生準備
     * writeObjectを使って独自のファイル情報を持つオブジェクトを送信しています。
     * 送信後、指定のファイルのバイナリが返却されます。
     * OrigFileInfoクラスのgetNameメソッドはファイル名、
     * getExtensionメソッドは拡張子を、
     * getSizeメソッドはファイルのサイズが取得できます。
     */
    public void prepareMusic(OrigFileInfo musicFile) {
        try {
            //新規のデータソースを入れるため、
            //再生中のものを止めて、resetをかけています。
            player.stop();
            player.reset();
            //キャッシュディレクトリに新規一時ファイル作成
            tempFile = new RandomAccessFile(File.createTempFile(
                    musicFile.getName(), musicFile.getExtension(),
                    getCacheDir()), "rw");
            //ファイルサイズの設定
            tempFile.setLength(musicFile.getSize());
            tempFile.seek(0);
            
            //バッファ済みサイズの初期化
            bufferdSize = 0;
            fileSize = musicFile.getSize();
            //プレイヤー状態の更新
            state = PlayerState.STOP;
            //オブジェクトの送信
            manager.sendMusicObject(this, musicFile);
            if(isWatching){
                schFuture.cancel(true);
            }
            //ダウンロード状態監視のためのスケジューラ
            //1秒ごとに実行させる
            schFuture = scheduled.scheduleAtFixedRate(new Watcher(), 0,THREAD_WATCH_INTERVAL, TimeUnit.SECONDS);
            isWatching = true;
            return;
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }
    }

    /**
     * 一時停止操作のためのメソッド
     */
    public void pause() {
        if (state==PlayerState.PLAYING) {
            state = PlayerState.PAUSE;
            player.pause();
        }
        else if(state==PlayerState.PAUSE){
            state = PlayerState.PLAYING;
            player.start();
            if(!isWatching){
                schFuture = scheduled.scheduleAtFixedRate(new Watcher(), 0,THREAD_WATCH_INTERVAL, TimeUnit.SECONDS);
                isWatching = true;
            }
        }
    }

    /**
     * ストップ操作のためのメソッド
     */
    public void stop() {
        if (state!=PlayerState.STOP) {
            state = PlayerState.STOP;
            player.pause();
            player.seekTo(0);
        }
    }
    
    /**
     * シーク操作のためのメソッド
     */
    public void seekTo(int seekToPercent) {
        int downRate = getBufferRate();
        //ダウンロード済みよりもシークが超えないようにする
        //今回、seekのMAXは1000になってます
        if(seekToPercent < downRate) {
            int dur = player.getDuration();
            player.seekTo((int)(dur * ((double)seekToPercent / 1000)));
        }
    }

    /**
     * バッファ取得時の初回時にだけ呼ぶメソッド
     * ※少なくとも音楽ファイルのヘッダ情報を全て含むまで
     *   取得してから呼ぶ必要があります
     * @param buf 取得バッファのバイト配列
     */
    public void startMusicBuffer(byte[] buf) {
        try {
            //ファイル書き込み
            tempFile.write(buf);
            //バッファ済みサイズ更新
            bufferdSize = buf.length;
            //データソースに設定
            player.setDataSource(tempFile.getFD());
            player.prepare();
            //なくてもいいかも
            player.setAudioStreamType(AudioManager.STREAM_MUSIC);
            //音楽ファイルの長さ(ミリ秒数)取得
            musicLeng = player.getDuration();
            //プレイヤーの状態をバッファリング中に変更
            state = PlayerState.BUFFERING;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    /**
     * 2回目以降のバッファ取得時に呼ぶメソッド
     */
    public void receiveBuffer(byte[] buf) {
        try {
            //書き込み位置にシーク
            tempFile.seek(bufferdSize);
            //ファイル書き込み
            tempFile.write(buf);
            tempFile.getFD().sync();
            //バッファ済みサイズ更新
            bufferdSize += buf.length;
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 再生するために十分なファイル書き込みが行われているか判定する。
     * ファイルサイズとバッファ済みサイズ
     * MediaPlayerの再生位置と音楽ファイルの秒数から判定する
     * 計算式には自身なし。
     */
    private boolean isEnoughBuffer() {
        //全てダウンロード完了済み
        if(fileSize <= bufferdSize){
            return true;
        }
        
        int downRate = getBufferRate();
        int playRate = getPlayedRate();
        //10秒の割合を算出
        int tenSecPer = (int)(( 1000 / (double) musicLeng) * 10);
        if (1000 < tenSecPer) {
            tenSecPer = 1000;
        }
        else if (tenSecPer <= 0) {
            tenSecPer = 1;
        }

        //再生位置の割合と、ダウンロード済みサイズの割合の差が
        //閾値以下の場合、falseを返却
        if ((downRate - playRate) < tenSecPer) {
            return false;
        } else {
            return true;
        }
    }

    /**
     * ダウンロード済みのサイズとファイルの全体の長さから、割合を計算(MAX 1000)
     */
    private int getBufferRate() {
        if(fileSize==0){
            return 0;
        }
        return (int) (((double) bufferdSize / (double) fileSize) * 1000);
    }

    /**
     * 再生位置と全体の秒数から、現在の割合を計算(MAX 1000)
     */
    private int getPlayedRate() {
        if(musicLeng==0){
            return 1000;
        }
        return (int) (((double) player.getCurrentPosition() / (double) musicLeng) * 1000);
    }

    /**
     * 再生終了時
     */
    @Override
    public void onCompletion(MediaPlayer mp) {
        if(state==PlayerState.PLAYING){
            state = PlayerState.STOP;
            //監視スケジューラを停止
            schFuture.cancel(true);
            isWatching = false;
        }
        else if(isWatching) {
            //監視スケジューラを停止
            schFuture.cancel(true);
            isWatching = false;
        }
    }

    /**
     * ダウンロード進捗監視タイマ
     */
    class Watcher implements Runnable {
        public Watcher() {

        }

        @Override
        public void run() {
            if(state==PlayerState.PLAYING){
                //プレイヤーが再生中
                if (!isEnoughBuffer()) {
                    //バッファが足りないため一時停止
                    player.pause();
                    state = PlayerState.BUFFERING;
                }
            } else if(state==PlayerState.BUFFERING){
                //バッファリングで一時停止中
                if (isEnoughBuffer()){
                    //バッファが足りている場合、再開する
                    player.start();
                    state = PlayerState.PLAYING;
                }
            }
            else {
                //その他(PAUSE/STOP)
                int buffRate = getBufferRate();
                if(buffRate==1000 && isWatching){
                    //ダウンロード完了している場合、監視を終了する
                    schFuture.cancel(true);
                    isWatching = false;
                }
            }
        }
    }
}




プレイヤー部分はこんな感じ。
通信部分に関しては力尽きました省略させていただきます。
最初にprepareMusic()で再生準備を行って
定期的に溜まったbyteデータを
初回時は、startMusicBufferメソッドを、
2回目以降は、receiveBufferメソッドを使うだけです
ただし、prepareMusic()を呼ぶときに
あらかじめファイルのサイズを知る必要があります。
通信部で、intのファイルサイズを送ってから
バイナリ送信を始めるなりしてなんとかしてください。


実装したコードの一部分を引っ張ってきているため、
このままじゃ動かないかもです。

12/17 追記

android2.x系ではうまく動かない。。。 3.xや4.0では動作するみたいだけど何が悪いのかわからん。。。

0 件のコメント: