2014年7月12日土曜日

MediaPlayerでoggファイルがループ再生されてしまう問題

MediaPlayerでANDROID_LOOP=trueメタデータ付きのoggファイルを再生すると、setLooping(false)にもかかわらずループしてしまいます。この問題(というか仕様)への対処方法を検索してもいい方法が見つからなかったため、自分で調べてみました。

対策としては、ANDROID_LOOP=trueのoggファイルを再生するときには、getDuration()で得た再生時間(ms)経過後にMediaPlayerの再生を停止するしかないようです。

問題はどうやってANDROID_LOOP=trueのoggファイルか判定するかです。

まずANDROID_LOOP=trueメタデータ付きのoggファイルをバイナリエディタで開いてみました。すると文字列で"ANDROID_LOOP=true"を確認することができます。



ちょっと強引な方法ですが、oggファイルを読み込んでその中に文字列"ANDROID_LOOP=true"が含まれているか確認すれば判定できそうです。

いくつかのoggファイルを確認したところ、"ANDROID_LOOP=true"が存在する場所はファイルによって異なっていたため、読み込むバイト数はあらかじめきめられません。ただしいずれのoggファイルも先頭付近にありましたので、とりあえず256バイト読み込んで判定してみます。

static public boolean isAndroidLoop(InputStream stream) {
    try {
        byte[] data = new byte[256];
        stream.read(data);
        String text = new String(data);
        return text.contains("ANDROID_LOOP=true");
    } catch (IOException e) {
        e.printStackTrace();
        return false;
    }
}

手持ちのoggファイルをいくつか試してみましたが、これで一応、ANDROID_LOOP=trueかどうか判定することができました。でも本当にこれでいいんでしょうか。
  1. ANDROID_LOOP=trueが256バイト以降にあったら正しく判定できない
  2. oggファイルのサイズが256バイト以下の小さいファイルであったら正しく判定できない
  3. そもそもこのやり方って正しいの?
まじめにやるには、oggファイルのフォーマットを理解して、フォーマットに従ってファイルを読み込む必要があります。フォーマットの詳細についてすべて理解する必要はありませんが(私も理解していません)、簡単に説明すると
  • oggファイルは音声専用のファイルフォーマットではなく、movやaviのように動画や音声など複数のデータをまとめて格納するためのコンテナファイル
  • oggファイルはページという単位でデータを格納する
そこでフォーマットにしたがってoggファイルを読み込んでみます。フォーマットは以下のサイトを参考にしました
本家のogg仕様
Mitsugu Oyama のソフトウェア倉庫

まず1ページ分のbyte配列を読み込む関数を作成します。
public static byte[] readPageBytes(InputStream stream) {
    try {
        DataInputStream dis = new DataInputStream(stream);

        // 読み込めなかったらデータの終わりと判断する
        byte[] capture_pattern = new byte[4];
        if (dis.read(capture_pattern) == -1) return new byte[0];
        if (capture_pattern[0]!=0x4f || capture_pattern[1]!=0x67 || capture_pattern[2]!=0x67 || capture_pattern[3]!=0x53) return null;

        byte[] header = new byte[22];
        dis.read(header);

        byte page_segments = dis.readByte();

        int segment_data_bytes = 0;
        byte[] segment_table = new byte[page_segments];
        int segment_table_int[] = new int[page_segments];
        for(int i=0; i<page_segments; i++) {
            segment_table[i] = dis.readByte();
            segment_table_int[i] = segment_table[i] & 0xFF; //JavaではByteは符号付き
            segment_data_bytes += segment_table_int[i];
        }

        byte[][] segment_data = new byte[page_segments][];
        for(int i=0; i<page_segments; i++) {
            segment_data[i] = new byte[segment_table_int[i]];
            dis.read(segment_data[i]);
        }

        int result_index = 0;
        int result_bytes = 4 + 22 + 1 + page_segments + segment_data_bytes;
        byte[] result = new byte[result_bytes];
        System.arraycopy(capture_pattern, 0, result, result_index, 4); result_index += 4;
        System.arraycopy(header, 0, result, result_index, 22); result_index += 22;
        result[result_index] = page_segments; result_index += 1;
        System.arraycopy(segment_table, 0, result, result_index, page_segments); result_index += page_segments;
        for(int i = 0; i<page_segments; i++) {
            System.arraycopy(segment_data[i], 0, result, result_index, segment_table_int[i]); result_index += segment_table_int[i];
        }                    
                
        return result;
    } catch (IOException e) {
        e.printStackTrace();
        return null;
    }
}

この関数を以下のように 呼び出します。
static public boolean isAndroidLoop(InputStream stream) {
    while(true) {
        byte[] raw_page = readPageBytes(stream);
        if (raw_page == null) break;
        if (raw_page.length == 0) break;
        String text = new String(raw_page);
        if (text.contains("ANDROID_LOOP=true")) {
            return true;
        }
    }
    return false;
}

0 件のコメント:

コメントを投稿