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 に音声ストリームが送信されていくことになります。

このプログラムには INTERNETRECORD_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プロトコルの使い方もまとめたいと思います。