2011年12月6日火曜日

ネット上の音楽をキャッシュする

本エントリは、「AndroiderでAdvent Calendarやろうぜ!」という、Android Advent Calendar 2011企画中の1エントリになっております。

軽い気持ちでやりますーて言ってみたけど、わりと参加されている皆さんがガチで書いているので、これはやばい!!。と戦々恐々として今日を迎えていました。
そして、いざ自分の番がまわってきて、あぁ、、どうしようと前日にこのエントリを書いているところです。

こうなったらもう、自分が今やっていることをそのまま書くしかない!
というかですね、このエントリのためにネタを作る暇がないのです。

今やっていることで、仕事に関係ないことといったら音楽プレイヤーのJustPlayerしかありません。

というわけで、JustPlayerで扱っているネットワーク上の音楽を再生しながらダウンロードする処理の説明をします。

まず前ふり


音楽を再生するにはMediaPlayerを使います。
MediaPlayerで音楽を再生するにはsetDataSourceに音楽データを放りこみ、prepare,startと順に呼び出していきます。
ネットワーク上にある音楽ファイルも同様で、たとえば

http://hogehoge/hoge.mp3

というような音楽データを再生する場合は

mediaplayer.setDataSource(context, Uri.parse("http://hogehoge/hoge.mp3"));

とすれば鳴ります。
こうしておけば、ちゃーんとMediaPlayerがバッファリングをして3Gでも途切れることなく再生してくれます。

以上

なんて、これで十分満足するのであれば、これ以降は読む必要はありません。

本エントリでは、次の問題点を解決するための実装例を解説することを目的としています。

1 一度再生した音楽を二回目に再生したとき、再度ネットワークからダウンロードするのが勿体無い
2 トンネルなどにはいっても途切れてほしくない
3 SSLといったセキュア通信による再生をしたい
etc

色々あるだろうけども主な目的として三つあげてみました(きりがいいので)

それぞれ、既存のMediaPlayerでは実現できない問題があります。
まず1ですが、MediaPlayerにsetDataSourceしたURLはMediaPlayerが内部で処理をしちゃっているのでダウンロードして保存しながら再生するといったことができない。

次に2は、MediaPlayerが再生するために必要なデータのダウンロードより、実際のダウンロード速度は速いのでどんどん先読みしていって曲の再生よりも先にダウンロードしてあげたいけど、そのコントロールができない。

そして3つ目は、SSLなURLはsetDataSouceに対応していない、またアクセス時の認証処理もできない。


本題

それでは本題にはいります。
前振りでもあった、問題を解決するために取る方法としてリレーサーバーを使った手法を提案します。

簡単にいうと、通常setDataSourceに渡すべきURLをちょっといじって内部のサーバー経由でとってくるようにするわけです。

図に表すとこんなかんじ
(絵はいつも残念です。)

このようにLocalServerを経由することでやりたい放題できます。
じゃ具体的にみていきましょう。

まずURLをいじくります。

通常

http://hogehoge/hoge.mp3

というURLであった場合

http://127.0.0.1:6868/http://hogehoge/hoge.mp3

というURLに置換えます
そして、Androidアプリの中で
127.0.0.1 のローカルサーバーにてポートを6868でListenするわけです。
(ポートは自動生成するのが利口ですが、ここでは便宜的に6868にしました)

すると、このいじくったURLでMediaPlayerにsetDataSourceすると
騙されたMediaPlayerがLocalServerにリクエストを投げてくるので、あとはLocalServer内でリクエストされたURLへアクセスし、取得したバッファをMediaPlayerに書きこんであげればよいのです。

具体的なソースコードは次のようになります。


mPlayingUri = "http://127.0.0.1:" + mRelaySrvice.getPort() + "/" + MEDIA_PATH;
mPlayer.setDataSource(this, Uri.parse(mPlayingUri));


これが呼び出し側のコードで、ローカルサーバーの処理は次のようになります。


while (!bStop) {
try {
final Socket client = mSocket.accept();
if (client == null) {
continue;
}
Runnable r = new Runnable() {
@Override
public void run() {
procMain(client);
}
};
new Thread(r).start();
} catch (SocketTimeoutException e) {
} catch (IOException e) {
Log.e("io error", e);
}
}


この部分がLocalServerのListen処理で、騙されたMediaPlayerのSocketが入ってきます。
procMainの処理をみてみましょう。


clientInputStream = client.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(
clientInputStream));
line = reader.readLine();

if (line == null) {
Log.d("url nothing");
return;
}

StringTokenizer st = new StringTokenizer(line);
st.nextToken();
uri = st.nextToken().substring(1);


clientはSocketです。Socketを読み込むとリクエストの内容がはいっています。
この中身についてはRFC2616に詳しくかいてあります。(RFC2616

この仕様によると
1行目は GETあるいはPOSTといったリクエストタイプ
2行目はリクエストされたURLの下のパス
というわけなので、1行目を読み飛ばしてパスの部分をとります。

例)
[http://127.0.0.1:6868/][http://hogehoge/hoge.mp3]

パスの部分は二つ目の[]の部分になりますので、みごと二行目で 
http://hogehoge/hoge.mp3
をとることができました。

次にやることは、
このhttp://hogehoge/hoge.mp3を内部でHTTP接続してダウンロードするところです。
バッファリングしながらダウンロードし、ファイルとSocketに書きこんであげます。



HttpURLConnection connection = (HttpURLConnection) (new URL(uri))
.openConnection();

connection.setDoOutput(true);
connection.connect();

String status = "HTTP/1.1 " + connection.getResponseCode() + " "
+ connection.getResponseMessage() + "¥n";
client.getOutputStream().write(status.getBytes());

for (Entry> e : connection.getHeaderFields()
.entrySet()) {
if (e.getKey().equals("Accept-Ranges")) {
continue;
}
String header = e.getKey() + ": "
+ TextUtils.join(";", e.getValue()) + "¥n";
client.getOutputStream().write(header.getBytes());
}
client.getOutputStream().write("¥n".getBytes());

remoetInputStream = connection.getInputStream();
cacheOutputStream = new BufferedOutputStream(new FileOutputStream(
mCacheFile));

byte[] buf = new byte[BUF_SIZE];
int readSize;
while ((readSize = remoetInputStream.read(buf)) != -1 && !bStop) {
if (cacheOutputStream != null) {
try {
cacheOutputStream.write(buf, 0, readSize);
} catch (IOException e) {
Log.e("write cachefile", e);
try {
cacheOutputStream.close();
} catch (IOException ex) {
}
cacheOutputStream = null;
}
}
client.getOutputStream().write(buf, 0, readSize);
}


注目するところは

String status = "HTTP/1.1 " + connection.getResponseCode() + " "
+ connection.getResponseMessage() + "¥n";
client.getOutputStream().write(status.getBytes());

この部分でしょう。
MediaPlayerをHTTPサーバーだと勘違いさせるために、実際に接続したHTTPサーバーのリクエストの結果を
そのままMediaPlayerの書きこんであげます。
同様に他のHeader属性も書き込みます。
本来は、ここで注意しないといけない箇所があって、実はHTTPリクエストにはレジュームリクエストもあるのですが、
このコードではその処理をきちんとしていません。そのため本格的にリレーサーバーを実装する場合はもう少し工夫が必要です。

以上が簡単な説明でした。
具体的なサンプルコードは

MediaPlayer Sample SVN

こちらにあります。
内部でMP3ファイルのURLはの定義がありますが、そこには適当な文字をいれていますのでテストの際にには置き換えてください。

0 件のコメント:

コメントを投稿