AudioStreamを使って音声をリアルタイムでストリーミングする

AudioStream クラスを使うと 驚くほど簡単に音声のリアルタイムストリーミングを実装することができます。今回は 2 つの Android 端末間で音声を一方向に送信するということをやってみます。

AudioStream では RTP プロトコルが使われていますが RTP プロトコルの詳細を知らなくても使えるように抽象化されています。開発者に優しい API 設計ですね。

送信側 受信側どちらも Android で作成しますが まずは送信側のコードを作成して Windows VLC プレーヤーで再生できるか確認します。

送信側
package net.osdn.cafe.example; import android.app.Activity; import android.net.rtp.AudioCodec; import android.net.rtp.AudioGroup; import android.net.rtp.AudioStream; import android.os.Bundle; import android.util.Log; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InterfaceAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.Enumeration; public class MainActivity extends Activity { private static final String TAG = "MainActivity"; private AudioGroup audioGroup; private AudioStream audioStream; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override protected void onResume() { super.onResume(); try { InetAddress localAddress = getLocalAddress(); InetAddress receiverAddress = InetAddress.getByName("192.168.10.8"); audioStream = new AudioStream(localAddress); audioStream.setCodec(AudioCodec.PCMU); audioStream.setMode(AudioStream.MODE_SEND_ONLY); audioStream.associate(receiverAddress, 12345); audioGroup = new AudioGroup(); audioGroup.setMode(AudioGroup.MODE_NORMAL); audioStream.join(audioGroup); } catch(Exception e) { Log.e(TAG, "", e); } } @Override protected void onPause() { super.onPause(); if(audioGroup != null) { audioGroup.clear(); audioGroup = null; } if(audioStream != null) { audioStream.release(); audioStream = null; } } /** 最初に見つかったIPv4ローカルアドレスを返します。 * * @return * @throws SocketException */ private static InetAddress getLocalAddress() throws SocketException { Enumeration<NetworkInterface> netifs = NetworkInterface.getNetworkInterfaces(); while(netifs.hasMoreElements()) { NetworkInterface netif = netifs.nextElement(); for(InterfaceAddress ifAddr : netif.getInterfaceAddresses()) { InetAddress a = ifAddr.getAddress(); if(a != null && !a.isLoopbackAddress() && a instanceof Inet4Address){ return a; } } } return null; } }

receiverAddress に設定している 192.168.10.8 というアドレスはストリームを受信する PC のアドレスです。12345 は送信先ポート番号です。とりあえず 適当に決めてかまいません。上記コードでは 192.168.10.8:12345 に音声ストリームが送信されていくことになります。

このプログラムには INTERNET RECORD_AUDIO のパーミッションが必要になります。Manifest.xml にパーミッションを追加しておいてください。

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

上記のプログラムをマイク付きの Android 端末で実行します。

次に PC VLC を起動します。メディアネットワークストリームを開く... を選択します。ネットワーク URL として rtp://192.168.10.8:12345 を入力して右下の 再生 を押してください。

ここで入力するアドレスは受信する PC 自身のアドレスであることに注意してください。
送信元である Android 端末のアドレスを入力するわけではありません。

分かりやすいように VLC オーディオ視覚化 から何か適当に選択しておきましょう。そして Android 端末のマイクに向かって何か話してみます。

PC のスピーカーから聞こえてくれば成功です。

ツールコーデック情報 を開くとストリームの情報を確認することができます。

統計 タブに切り替えると受信しているストリームのサイズやビットレートも分かります。

受信側も Android で作ってみる

Android 端末のマイクで拾った音声をストリーム送信できていることが確認できたので 続いて受信側のコードも Android で作ってみます。

受信側
package net.osdn.cafe.example; import android.app.Activity; import android.net.rtp.AudioCodec; import android.net.rtp.AudioGroup; import android.net.rtp.AudioStream; import android.os.Bundle; import android.util.Log; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InterfaceAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.Enumeration; public class MainActivity extends Activity { private static final String TAG = "MainActivity"; private AudioGroup audioGroup; private AudioStream audioStream; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override protected void onResume() { super.onResume(); try { InetAddress localAddress = getLocalAddress(); InetAddress senderAddress = InetAddress.getByName("192.168.10.4"); // 192.168.10.4 は送信側Android端末のIPアドレスです。 AudioStream audioStream = new AudioStream(localAddress); audioStream.setCodec(AudioCodec.PCMU); audioStream.setMode(AudioStream.MODE_RECEIVE_ONLY); //1.AudioStreamに割り当てられたポート番号を送信側に伝えて、 // このポート番号に送信してもらいます。 int receiverPort = audioStream.getLocalPort(); Log.i(TAG, "#receiverPort=" + receiverPort); //4. 送信側から教えてもらったポート番号に関連付けます。 // ですが今回は双方向ではなく受信専用としているのでポート番号は適当でOKです。 int senderPort = 54321; audioStream.associate(senderAddress, senderPort); AudioGroup audioGroup = new AudioGroup(); audioGroup.setMode(AudioGroup.MODE_MUTED); audioStream.join(audioGroup); } catch(Exception e) { Log.e(TAG, "", e); } } @Override protected void onPause() { super.onPause(); if(audioGroup != null) { audioGroup.clear(); audioGroup = null; } if(audioStream != null) { audioStream.release(); audioStream = null; } } /** 最初に見つかったIPv4ローカルアドレスを返します。 * * @return * @throws SocketException */ private static InetAddress getLocalAddress() throws SocketException { Enumeration<NetworkInterface> netifs = NetworkInterface.getNetworkInterfaces(); while(netifs.hasMoreElements()) { NetworkInterface netif = netifs.nextElement(); for(InterfaceAddress ifAddr : netif.getInterfaceAddresses()) { InetAddress a = ifAddr.getAddress(); if(a != null && !a.isLoopbackAddress() && a instanceof Inet4Address){ return a; } } } return null; } }

VLC でのテスト受信と違って今回は適当にポート番号を決めることができません。なぜなら AudioStream のコンストラクタではポート番号を明示的に指定することができず 動的にポート番号が決定されてしまうためです。

受信側では AudioStream インスタンスを作成して動的に割り当てられたポート番号を送信側に伝えて そのポート番号に対して音声ストリームを送信してもらう必要があります。このポート番号を伝達するために使われるのが SIP プロトコルです。SIP ではポート番号だけでなく符号化方式など他の情報もやりとりされます

今回は SIP を省略して とても原始的な方法でポート番号を伝えます。

はじめに受信側のプログラムを Android Studio から実行します。すると logcat I/MainActivity: #receiverPort=57528 のような出力が表示されます。これが実際に割り当てられたポート番号です。

このポート番号を送信側のソースコードに書き込みます。なんて原始的なんでしょう!

送信側
package net.osdn.cafe.example; import android.app.Activity; import android.net.rtp.AudioCodec; import android.net.rtp.AudioGroup; import android.net.rtp.AudioStream; import android.os.Bundle; import android.util.Log; import java.net.Inet4Address; import java.net.InetAddress; import java.net.InterfaceAddress; import java.net.NetworkInterface; import java.net.SocketException; import java.util.Enumeration; public class MainActivity extends Activity { private static final String TAG = "MainActivity"; private AudioGroup audioGroup; private AudioStream audioStream; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } @Override protected void onResume() { super.onResume(); try { InetAddress localAddress = getLocalAddress(); InetAddress receiverAddress = InetAddress.getByName("192.168.10.5"); // 192.168.10.5 は受信側Android端末のIPアドレスです。 audioStream = new AudioStream(localAddress); audioStream.setCodec(AudioCodec.PCMU); audioStream.setMode(AudioStream.MODE_SEND_ONLY); //2.AudioStreamに割り当てられたポート番号を受信側に伝えて、 // このポート番号を関連付けてもらいます。 int senderPort = audioStream.getLocalPort(); //3.受信側から教えてもらったポート番号に音声ストリームを // 送信するように関連付けます。 int receiverPort = 57528; audioStream.associate(receiverAddress, receiverPort); audioGroup = new AudioGroup(); audioGroup.setMode(AudioGroup.MODE_NORMAL); audioStream.join(audioGroup); } catch(Exception e) { Log.e(TAG, "", e); } } @Override protected void onPause() { super.onPause(); if(audioGroup != null) { audioGroup.clear(); audioGroup = null; } if(audioStream != null) { audioStream.release(); audioStream = null; } } /** 最初に見つかったIPv4ローカルアドレスを返します。 * * @return * @throws SocketException */ private static InetAddress getLocalAddress() throws SocketException { Enumeration<NetworkInterface> netifs = NetworkInterface.getNetworkInterfaces(); while(netifs.hasMoreElements()) { NetworkInterface netif = netifs.nextElement(); for(InterfaceAddress ifAddr : netif.getInterfaceAddresses()) { InetAddress a = ifAddr.getAddress(); if(a != null && !a.isLoopbackAddress() && a instanceof Inet4Address){ return a; } } } return null; } }

受信側に割り当てられたポート番号をソースコードに書き込んだら 別の Android 端末で送信側プログラムを実行します。

送信側プログラムを実行している Android 端末のマイクに向かって 何か話しかけてみてください。

どうでしょうか? 受信側プログラムを実行している Android 端末のスピーカーから聞こえてきましたか?

また別の機会があれば SIP プロトコルの使い方もまとめたいと思います。

この記事を共有しませんか?