RTP音声再生時のプチプチノイズ

昨日のRTPでの音楽再生時のプチプチ音の理由が大体分かりました。
マイクからの入力をスピーカで流す場合、マイクの音声をデジタルサンプリングする際に自動的にサンプリング周期にしたがってパケット化されます。さらに受信側ではそのパケットを受信してサンプリングと同様の周期で再生します。ネットワーク経由の場合、たまにパケットの到着の遅延やジッタなどが発生しても送信側と再生側の周期が同じなので長期的には安定します。
一方で送信側が既に録音されたファイルの場合、その再生スピードはアプリ上で任意に決定できます。もちろん録音されたサンプリング周期を考慮してそのスピードを決めますが、意外にこのスピードは難しいです。
8Khzx8bitなので8Kbytes/sec をRTPのpayload size 160bytesで割ると50パケット、周期は20msecになります。
そこで20msecに1パケットずつ送る仕組みを作らなければなりません。パケットの処理時間があるので単に20msecのsleepを入れるわけにはいきません。
そこでロジックとしては20msecごとのパケットを送る時刻を決めてしまい、一つのパケットを送信するごとに次の時刻との差分だけsleepを入れています。とりあえず、送信側としてはこれで20msecごとに送信可能です。
一方受信側ですが、G711 u-lawで受信してそれを通常の8bitx8khzのlinearのPCMに変換しなければなりません。これはJavaのcode上ではこんな感じです。

    byteArrayInputStream = new ByteArrayInputStream(packet.getData(),12,160);
    
    ulawStream = new AudioInputStream(byteArrayInputStream,ulaw,160);
    // G.711 u-law からリニアPCM 16bit 8000Hzへ変換
    linearStream = AudioSystem.getAudioInputStream(linear,ulawStream);
    linearStream.read(voice,0,voice.length);

RTPのPayloadを一つ受信するごとにulawのStreamをlinerのStreamに変換して音声パケットを受信する。そこからバッファ分を読み出して、その後、スピーカ側に書き込むわけですが、RTPのパケットを受信する部分はblockでパケットが受信できなければそこはプログラムは「待つ」訳です。しかし、スピーカ側の再生側ではリアルタイムで再生しているので「待て」ません。もし、RTPパケットの受信が少しでも遅くなればスピーカ側がおそらく「無音」を再生するしかありません。そして、遅れてきたRTPパケットを処理するので二つのRTPパケットの再生の間に「無音」が入ってしまいます。ここがプチプチ音の原因のようです。
とりあえず、応急処置として送信側のパケットの送信間隔を20msecではなく、19msecにしてみました。するとプチプチ音は消えました。もちろん送信側のパケットの送信が早すぎるのでどこかで受信側のバッファがオーバーフローしてパケットが破棄させてしまうとは思います。
試験する前にはあまり考えていませんでしたが、音楽やビデオなどのデータをストリーミングする際にバッファリングすることの重要性を理解しました。
一方で電話ソフトの場合、この問題は発生すると思うのですが問題が露呈しなかったのはなぜでしょうか?私の推測ですが、電話のような会話の場合、無音部分がかなり多いのでパケット到着の遅延で「無音」が再生されてもそれが認識できるタイミングで発生するケースは少ないのだと思います。
実際にやってみると色々学ぶことが多いです。
良いお年を!

Linux serverのJava Audio System

ご無沙汰してしまいました。
ラジオプロジェクトのパーツとして、WAVファイルを再生してG711 U-lawに変換してRTPで送信するClassを作っていますが、PC上ではとりあえず動作するのですがAWSのLinuxサーバ上で起動すると以下のようなエラーが出てしまいます。
java.lang.IllegalArgumentException: No line matching interface TargetDataLine supporting format PCM_SIGNED 8000.0 Hz, 16 bit, mono, 2 bytes/frame, little-endian is supported.
        at javax.sound.sampled.AudioSystem.getLine(AudioSystem.java:476)
        at SendRtp.main(SendRtp.java:30)
まだ、確認はしていないのですが、もしかしたら、Linux server上にはAudio Systemが存在しないのかなと思い始めています。単に音声処理のパッケージなどを入れればいいのかも知れませんが、サーバですので、通常マイクもスピーカも存在しないのでそのようなパッケージも基本存在しないのでしょう。
というわけで、とりあえず、音声ファイルの変換ツールをネットから手に入れて音源をG711u-lawフォーマットに変換。このファイルを単に読み出してRTPのパケットのPayloadに入れて送るだけのClassを作ってみました。音楽で実験してみたのですが、音質の悪いAMラジオのようになってしまいました。音質が悪いのは仕方が無いとしてなぜかプチプチというノイズが入りました。きっとどこか私のプログラムが悪いのでしょう。
それをおいても、ラジオを意識するのであればG711では音質がきつそうです。もう少し広い帯域で実現するほうがよさそうです。それからサーバ側で音声変換ができないのであれば送り元のPC側で実施するというのも一つの方法かも知れません。サーバを電波を出す放送所、送り手のPCを放送スタジオとして、スタジオ側で音質を決めてサーバに送信、サーバ側は音声パケットに関しては単に再放送するだけの仕組みでどうでしょう。

妄想プロジェクト 番外編 年賀状アプリ

2012年も年の瀬がせまってきました。年賀状を書かれている方も多いかと思います。
メールがこれだけ普及してもまだ、年賀状を送る人は多いようです。
もちろん、直筆の年賀状や手紙は確かに心が心がこもっていてもらって嬉しいものですが、実際には多くの年賀状が印刷されたものです。
それでもなぜ、メールではなく、手紙が使われるのか考えてみるとやはり、二つのポイントがあるのかなと思います。一つはリアリティ、紙とはいえやはり手で触れるリアリティは電子的なものとは違います。もう一つははがきというフォーマット。メールでどんなに手の込んだ趣向を凝らすよりも、今までの年賀状というフォーマットに対する愛着とか慣れがあるのではと思います。人間の行動や判断を考えると今までと同じ…というのは色々な物事を決定するときの大きなファクターになっています。
そこで思うのですが、メールではなく、年賀状用のアプリを開発したらどうかなということです。ターゲットとしてはもちろんスマホとかタブレッド。単にアプリ上ではリアルの年賀状と同じ様に見えるものを作成したり、送信できるアプリを作る。もちろん送信しても配達されるのは1月1日になってから。スマホやタブレットのタッチ機能やたとえばペン入力をサポートすれば年賀状の手書きの文字にも対応できると思います。
実際の年賀状でも多くの場合、宛名までも印刷されているわけす。しかも個人の場合、実はPCで作成した画像を印刷していたりします。それをそのままこの年賀状アプリの入力にすればあとはあて先を指定して送信するだけです。リアルな「紙」の部分を除けば年賀状と変わりません。
むしろ、正月に旅行中でも年賀状を見れたり、12月31日に送信しても相手に元旦に届けることが可能です。実際のデータの転送に関しては専用のプロトコルを使っても良いし、ユーザに見えない範囲でメールをベースにしても実現は簡単と思います。
メールが普及し始めたときに、年賀状なんて直ぐになくなると思ったのですが、実際には年賀状はなくなりませんでした。メールは便利ゆえに、年賀状のように相手に挨拶するものとしては質が低い、あるいは失礼なイメージがあるのかも知れません。また、1人1人にメールで年賀状を書く場合には全て同じ文言では余りにも間抜けに見えるように思うのかも知れません。いずれにせよ、人間は経験や習慣の奴隷みたいなところがあるので、一気にジャンプするのは難しいのだと思います。この年賀状アプリによって少しだけ年賀状からメールへのジャンプの橋渡しができるかも知れません。

 翌日の追記:
その後、世の中に年賀状アプリなるものが存在することを理解しました。私の理解ではWebやあるいはダウンロードしたソフトで年賀状をオーサリングする。そしてそれをネット上、おそらくソーシャルメディアで相手に送る。受け取った人は自分の住所を入力するとそのリアルの年賀状が受け取れる…というもののようです。
この年賀状アプリのポイントとして、相手の住所やアドレスを知らなくても年賀状が出せるということのようです。
私の感覚ではなぜ、そこまでしてリアルの年賀状にこだわるのかなということです。このシステムでは既に送り手側は100%デジタル、Virtual化されているのに受け手側だけがリアルのカードが残ります。確かに、カードゲームのレアカードのように特定の人から受け取った年賀状を人生の記念にとっておくのは分からないでもありませんが、送り手側は既にリアスではないわけですし、デジタルで受信して満足していれば良いのにと思わないでもありません。

妄想 電話プロジェクト 番外編 ラジオ

電話のことを考えていましたが、同じような仕組みでラジオ局も開設できるなと思いました。
既にインターネットラジオがあるじゃんという突っ込みもあるとは思いますが、とりあえず、妄想を続けます。
現在のインターネットラジオはTCPによるストリーミングを使っていると理解してます。一方通行の音声ですので、遅延を気にしなくていいのでこれはデザインとしては全く正しい考え方です。適当なバッファを確保することによってパケットの到達の遅延や揺らぎを吸収できます。しかし、場合によっては始めのバッファリングでスイッチオンで音声再生とはいきません。また、結局回線の品質が悪くなるとTCP自体が切れてしまって肝心の番組を聴くことができません。そして、再度新規の接続ということになります。
一方で本来のラジオをかんがてみるとたとえば電波が悪くなれば聞こえなくなるのは当然ですし、内容にもよりますが、ちょっと位聞こえなくてもまあ、全体の流れで番組を楽しんでいる面があります。結局、ストリーミングを使うのであればファイルをダウンロードして再生するのと変わらないような気がします。

というわけでUDPベースでのラジオサービスを考えて見ます。
まず、基本的な音声部分は簡単です。マイク入力でもかまいませんし、テープの入力をただ決められたUDPのポートに送り出す仕組みを作りこみます。クライアント側でのバッファリングを考えるとRTPの方が好ましそうですが、単にサンプリングしたデータを送り出すだけでもいいかも知れません。
さて、ラジオ局は音声を送る方法は確立していますが、どのアドレスのどのポートにパケットを送れば良いかが分かりません。もちろん、それはClientからのリクエストによってそのアドレスとポートにRTPなりの音声パケットを送ることになります。
ここで電話プロジェクトで出てきたNATの話が出てきます。もし、Clientが自分のアドレスとポート番号をHTTPなどの方法でサーバに伝えたとしてもそれはClient側のローカルの情報の可能性があります。しかし、ここでClient側がUDPのしかも音声を送ってもらいたいポート番号からリクエストをサーバ側に送ります。すると以下のような変換が行われてサーバに到着します。
Client —-UDP —————–> FW (NAT)———UDP——-> Server
 Target Address:Server IP                     TargetAddress:Server IP 
 Target Port:Server Port                        TargetAddress:Server Port  
Source Address:Client IP(private)         SourceAddress:NAT address(global) 
Source Address:Client Port                    SourceAddress:NAT Port
このようにUDP自体でリクエストを送ることによってサーバ側は単にその受信したパケットのアドレス+ポートに音声を送り始めればいいわけです。
さらに、Clientは番組を聴いている間はサーバ側の同じポート番号に定期的にKeep Aliveのパケットを送り続けることができます。サーバ側でのこのKeep Aliveが到着している限り番組を送り続け、Keep Aliveが切れたときに送信を終了すれば切断側の仕組みも組み込むことができます。

簡単にまとめると
①Client側は事前に自分の聴きたい番組のIP AddressとPort番号を確認する。
②Client側は自分が受信可能のUDPポートをソースとしてサーバのアドレスにパケットを
送り始める
③サーバ側はUDPポートにパケットを受信したら、そのパケットの送信元(Addess+Port)に対して番組を流し始める。
④さらにサーバは受信するUDPパケットの情報から現在の聴取者のリストを保持し、最新のUDPパケットの受信によりリストを更新する。
⑤Clientは番組の聴取を終了する場合はUDPのパケットの送信を止める。実際にはこれはアプリケーションの終了で自動的に達成することも可能。

今、まとめながら気がつきましたが、アプリ側にサーバ側のIPアドレスやポート番号を組み込んでしまえば、単にClient側では単にアプリを起動するだけで、特定の番組と接続して聴取することも簡単です。
これはAKBのようなアイドルタレントのファンクラブや宗教団体、学校、あるいは特別なイベントプロモーションでもつかえるのでは思います。

妄想 電話プロジェクト④ JavaによるRTPの実装

さて、脱線ついでに先日ネットで見つけたRTPによる音声通信のコードを使って試験をしてみる。基本的なクラスのほかにu-lawで録音してauファイルに書き込むサンプルプログラムなどが付随していてコードを書いた人のJavaでの音声処理に精通していることが分かります。
サンプルコードには8bitx8Khz u-lawのRTPによる音声通話用のサンプルがありました。
使ってみたのですが、音声繋がりませんでした。
一体何が悪いのかWireSharkでトレースを取ってみると、本来20msec毎に1パケットを送信するはずが、大体120msec間隔でRTPパケットが6個バースト上に送信されていました。おそらく、受信側ではこのバーストパケットをバッファできないので1/6程度のパケットしか再生できないため、音声が聞こえなかったのだと思います。
なぜ、このようになってしまうかですが、おそらくマイクからの16bitx8khzの音声ストリームを8bitx8khのu-lawのストリームに変換する部分で期待するタイミングで変換が行われていないからだと考えています。オリジナルのコードでは以下の流れで8bitx8hhz u-lawのデータを取得しています。
   AudioInputStream linearStream = new AudioInputStream(targetDataLine);
   // リニアPCM 16bit 8000Hz から G.711 u-lawへ変換
   AudioInputStream ulawStream = AudioSystem.getAudioInputStream(ulawFormat,linearStream);

さらにこのulawStreamからループの中で160byteを取得するごとに送信しています。
while(true){
     ulawStream.read(voicePacket,0,voicePacket.length);
     // RTPヘッダーを付ける
     rtpPacket = this.addRtpHeader(voicePacket);
     packet = new DatagramPacket(rtpPacket,rtpPacket.length,address);
     // 相手へ送信
     this.socket.send(packet);
}
私のマイクからのサンプリングされたデータを直接送信するコードもこのロジックで処理しています。マイクからの単純な16bit x 8khzであれば殆ど大きな遅延もなく、スムーズにデータが変換されているのだと思いますが、その出力をu-lawに変換する際にはJavaのモジュール内のバッファなどの関係で20msecx6 120msec程度のデータの塊としてしか処理できないのだと思います。
本来、処理遅延に関しては出力データをさらにバッファに入れてそのバッファから正確な間隔で読み出すのが正しいRTPの実装になりますが、結局は受信側の音声再生の時点で細かい時間の変動は処理されてしまうと思いますので、とりあえずはループにdelayを入れることで問題を回避することができました。単にsleepを1行入れただけです。
while(true){
     ulawStream.read(voicePacket,0,voicePacket.length);
     // RTPヘッダーを付ける
     rtpPacket = this.addRtpHeader(voicePacket);
     packet = new DatagramPacket(rtpPacket,rtpPacket.length,address);
     // 相手へ送信
     this.socket.send(packet);
  Thread.sleep(18);
}

やはり、この手の処理のちょっとした複雑さや遅延の増加を考えるとRTPを使うよりも単なる音声のストリームを相互に流し合うほうがシステム的には優れているような気がします。もちろん、現在のIPネットワークの品質が前提の話です。

妄想 電話プロジェクト ③RTP

プロジェクト的にはちょっと脱線。
今回の自分のアプリケーション的には音声は16bitx8khzのサンプリングでヘッダー無しでのパケットで通信しようと思っている。しかし、世の中の常識を考えると音声をIPで伝えるにはRTPプロトコルを使い、A-law/U-lawでの8bitx8khzのPCMあるいはG.711と呼ばれる企画が主流となっている。
私も電話業界にいるので、その常識も理解しているし、なぜ、そうなっているかの理由も理解しているつもり。これは長い電話の歴史に基づいていて全てに理由がある規格と考えて良い。たとえば8bit サンプリングは人間の聴覚を考えると大体3Khz程度の高音が高いほうの限界。だから、それ以上の高い音声を再生しても意味がない。アナログデータをサンプリングする場合、必要なデータの最高周波数の2倍のサンプリングレートでデジタル化すればそれをアナログ化するときに元のデータに戻せる。あるいはサンプリングレートの半分の周波数以下の情報だけがデジタルデータとして処理できるといこと。この人間の聴覚に関して言えば昔の古い電話を使おうがPCとかスマホとつかっても同じなのでこれからも若干の変更あったとしてもこれからも同じでしょう。
一方で何ビットでサンプリングするか..これは実はかつてのアナログ電話と同じ程度の音質を確保しつつ、デジタルデータとして使いやすいように8bitx8khzの64Kbpsが決定されている。単純な8bitのサンプリングだと、音声の伝送、特に、小さい声に関してここ得にくいのでそこを補正しているのがA-lawとかU-lawというもの。8bitに収めつつ、小さい声でも聞こえやすいように工夫されている。
これらの仕組みを作り上げた先輩は本当に偉いと思うし、規格全体の調和感も美しく感じる。しかし、時代は変わってしまったんですよね。かつての古い電話機のマイクやスピーカに比べればエントリーレベルのPCのマイクやスピーカでも遥かに性能はいいんだよね。単に会話を楽しむだけでなく、雰囲気なんかを伝えられる余裕もあると思う。するとやはり8bitではなく、16bitの方が良いと思う。
また、RTPパケットはヘッダ情報によってデータの内容やパケットの順番などを受け手に伝えている。受けてはその番号をみて遅延や受信タイミングのずれを調整したりしている。IPネットワークでのパケットの逆順、遅延、ロスなどを考慮している。
でも、現在のIPネットワークの現状を考えてみると、遅延やパケットロスは殆ど存在しないのが普通出し、品質の悪い状態で使う必要もないような気がする。パケットロスや遅延も、受信側はそのとき受信した最新のパケットを再生すればいいし、もし、パケットがなければ無音でも再生しておけば良い。高品質のネットワークが常識になっている環境ではあえてそのようなオーバーヘッドをつけるよりもただ、データを送り続けるほうが少なくとも閉じたアプリケーションでは有利と思う。

とはいえ、業界常識のRTPのパケットに関して技術的に実装できる準備は必要です。JavaのAudioSystemから直接G711とかを取り出せるとか思ったのですが、そう甘くはありませんでした。
しかし、世の中には素晴らしい先駆者がいて、既にJavaに関して、そのあたりをまとめて下さっていました。
http://www013.upp.so-net.ne.jp/fukugawa/javadertp.html
内容を詳細には確認していませんが、これをこのままベースとすれば基本的な仕組みは作っておけそうです。

妄想 電話プロジェクト② 交換

ばたばたと忙しく、一週間のご無沙汰になってしまいました。
実作業はともかく、頭の中の整理と考えを忘れないために少しアップデート….

WinChatの加入者がサーバ側に登録されれば、サーバ側では今、どの端末が使用可能状態か分かります。あとは二つの加入者の情報を繋いでやれば音声通信が確立します。
サーバにアクセスして、GUIにて利用可能なClientを確認して二つをコネクトすればOKです。
しかし、これではずっと昔の電話交換手による交換作業と変わりません。
この部分を加入者のリクエストによって接続できるように作りこんでいきたいところです。
まずは通信相手の状態情報をClientに送る必要があります。本当にアプリを作るのであればもちろんTCPの通信によって実現するべきでしょうが、ここはhttpを使いたいと思います。FWなどの環境を考えるとport80が一番繋がるであろう事と、Registerで既にhttpを使っているので、そこに乗っけるのが実装上好ましいという理由です。
実際、RegisterとそのKeep AliveでClientは通常的にサーバにリクエストを送りますので、その返信で通信可能な他のClient情報をClientに送ることができます。とりあえず、プロトタイプでは現在の使用可能者全ての情報を返せばいいですが、規模の大きなシステムでは、各加入者の友人リストのようなDBをサーバに持たせれば、関連ユーザのみのデータを端末に戻すように改良するのは簡単です。

さて、ここでやっと、ユーザはサーバに登録すると同時に通信可能な相手のリストも見ることができるようになります。ここからは、まだ、自分でもクリアにデザインできていませんが、進めて見ましょう。

ClientAはサーバから受信しているリストからClientBへの通信をするためにサーバにリクエストを送ります。
サーバはClientAからリクエストを受信したので、そのリクエストを元にClientBとUDPのポートをつなげます。
これで両者の通信が成立します。ぱちぱち…

とうまくいくのででしょうか?
まず、この方式の場合、各ClientはRegisterしたときに既にUDPポートを決定し、音声を送り始めなければいけません。サーバ側はそのUDPポートを理解すると共に、そのポートで受信するパケットからClientへの送信アドレスを記録する必要があります。
実験的なシステムではこれでも問題ありませんが、実際のシステムで音声通信を行っていないのに、音声パケットを送り続けるのはネットワークのリソース利用方法としては好ましくありません。また、サーバ側でも音声通信を始める以前からUDPポートをリザーブしてしまうのは同じく有効なリソース管理の方法ではありません。
(とは言うものの、この方法が全く悪いかというとそうでもないかと思っています。ネットワークリソースに関しては実際の音声通信をしていないときはパケットの数を大幅に絞って、Keep Alive用として使用することも可能です。またUDPポートについても、呼の接続時にポートを確保しようとしても、既にリソースが枯渇していたり、その番号がネットワーク上でブロックされていたりする可能性も有ります。リクエストが来たときにきちんと繋げるためにはリソースを事前に確保、確認しておくのは場合によっては有効な方法になります。)

さらに、電話を掛けるほうはいいのですが、着信側にどのように呼が掛かってくるか、あるいは掛かってきたかを通知する方法を考えなければいけません。実際の電話では呼出音にあたる機能です。事前にサーバとUDPパケットの通信を始めている場合には、そのポートに対して数秒、呼び出し音を再生するといった方法が考えれれます。
しかし、UDPポートの割り当てや音声パケットの送受信を交換作業と同時に始めようとした場合、電話を掛ける側とサーバの通信は問題ないのですが、受信側とのやり取りを考えなければいけません。受信側とはKeep Aliveのパケットでやり取りがあるとはいえ、Keep Aliveはかなり時間間隔があるので、接続リクエストに対しての反応が遅くなります。やはり、電話を掛ける側がアクションをおこしたら、殆ど瞬時に受信側にも通知がいくような仕組みを作り上げたいところです。
これはサーバがクライアントに対してアクションを起こす仕組みですのでhttpでは実現しにくい部分ではあります。少し上に書いた話とは矛盾してしまいますが、UDPのポートで1秒に1回くらいのKeepAliveを保持するシステムならこの部分は解決できます。
いっそのこと、HTTPを全てやめて、UDPポートでの通信だけで、keep aliveやRegister処理など実行してしまえ…という声も聞こえてきます。

うーんこれでは、単なるSIPになってしまうではないですか…本当は逆で、httpに音声を乗せれないかと考えていたのに……

妄想 電話プロジェクト ①加入者登録

とりあえず、2台のPC上のChatアプリをインターネット上のサーバを経由して音声通信するシステムを作ることができました。
しかし、現状これでは、遥か昔の交換手による電話交換と同じです。できれば端末が自動的にサーバに登録して、さらにクライアント側で相手側の所在を確認して通信を開始する仕組みを考えてみます。このあたりは実際にはSIPの技術規格によって既に作られているのですが、まあ、個人の遊びでの話しになります。
まず、各Clientは音声通信をする、しないにかかわらずサーバに所在を登録します。この登録情報は将来的には他のClientにも状態が表示できることが目標になります。SIPの世界ではRegisterというパケットを使用して実現している機能ですが、ここではHTTPを使ってサーブレットをインターフェースにしたいと思います。HTTPは通常どのようなネットワークでも疎通可能なのとhttp/htmlを利用できるので非常に簡易に開発ができます。
詳しくは状態遷移図(State Machine)を作成しないといけませんが、概要はこんな感じ。
Client側:
   毎分http GETをサーバに送る。URL内のパラメータにローカルホストのIDを送る。
   サーバからのレスポンスによって自分の使用するポート番号を理解し、音声通信が必要な場合にはそのポートを使用して音声の送受信を行う。
        また、URLの内容によって、自分のRegistrationを削除もできるようにする。
Server側:
   Clientからの登録リクエストを受信して、未使用のポート番号を返信しつつ、Clientの状態を利用可能として保持する。
   Clientからの登録解除リクエストを受信して、Clientの登録を解除してリザーブしたポート番号も未使用状態に戻す。
   登録されたClientからRegisterリクエストが一定期間を超えて到着しない場合、登録情報を解除する。

一応、このような仕組みを実装すると、サーバ側ではどのClientが利用可能状態にあるのかが分かります。これをたとえば各ClientからのRegister情報の返信(GetのResponse)に入れてやれば、各ClientではどのClientがOnLineか分かるようになります。Yahoo Messengerなどのメンバーの状態表示と同じ機能になります。通話可能な相手の状態がわかればその相手に対して音声通信のリクエストを出すことも可能になります。システムが大きくなれば、加入者をグループに分けて、自分の帰属する加入者の状態だけを表示するようにもできます。そのためにはサーバ側に加入者情報を持ったDBを作る必要があります。

ここまで実現できれば次は、ClientAからの接続リクエストを受けてClientAとClientBの音声通信を開始する仕組みを作ることになります。電話の世界では交換とか信号処理になります……

Java Appletへの署名方法

Java Appletはデフォルトではローカルのリソース(ファイルやネットワーク)などにアクセスすることができません。
この制約はAppletに署名を実施することにより解除できます。実際には署名の認証なども正しく処理しないとSecurity Warningが出てしまうのですが、とりあえず試験的に実施しました。
Appletが格納されたjarファイルに対してkeytoolとjarsignerという二つのツールを使います。

C:\Test>keytool -genkey  -alias
 test -keystore teststore
キーストアのパスワードを入力してください:
新規パスワードを再入力してください:
姓名を入力してください。
  [Unknown]:  Nanashino Gonbei
組織名を入力してください。
  [Unknown]:  Nantoka7 networks
都市名または地域名を入力してください。
  [Unknown]:  Tokyo
州名または地方名を入力してください。
  [Unknown]:  Tokyo
この単位に該当する 2 文字の国番号を入力してください。
  [Unknown]:  JP
CN=Nozomu Hasegawa, OU=NA, O=Sonusnetworks, L=Tokyo, ST=Tokyo, C=JP でよろしいで
すか?
  [no]:  yes

<test> の鍵パスワードを入力してください。
        (キーストアのパスワードと同じ場合は RETURN を押してください):

ここでローカルディレクトリにteststoreというkeystoreが作成されます。
さらに、
C:/Test>jarsigner -keystore teststore chatsigned.jar test
キーストアのパスワードを入力してください:

警告:
署名者の証明書は 6 か月以内に期限切れになります。

これでこの署名されたjarファイルを対象にたとえばこんな感じでhtmlファイルを作成すればAppletが起動してローカルのUDPポートにアクセスができました。

<!DOCTYPE HTML PUBLIC “-//W3C//DTD XHTML 1.0 Strict//EN” “http://www.w3.org/TR/x
html1/DTD/xhtml1-strict.dtd”>
<html xmlns=”http://www.w3.org/1999/xhtml”><head>

<applet code=”com.nozomu.audio.WinChatApplet.class”
   archive=”chatsigned.jar”
   width=1000 height=600></Applet>

<title>WinChat</title>
</head>
<body>
<left>
メッセージ<BR>
</left>
<center><hr size=1 width=675><font size=5>
<a href=”./index.html” target=_top>Return</a> <BR>
</body>
</html>

カテゴリー: AWS

Chatパッケージ初リリース

単に私のChat用のモジュールが完成しただけです。
この先、色々作りこんでいきたいとは思っていますが、とりあえず、ひと段落ということでClassとソースの入ったjar ファイルをアップロードしておきます。
chat

このパッケージだけで、WindowsやUnixのコマンド画面でのテストやJaveのWindowプログラムでのUDP音声試験ができます。
Java Windowsプログラム:
 java -cp chat.jar com.nozomu.audio.WinChat
Command Line プログラム:
 java -cp chat.jar com.nozomu.audio.Chat targetHostname targetPort waitPort
GUIでは画面で設定できますが、コマンドラインでは引数で対向のIPアドレス、ポート番号、そして自身の受信用ポート番号を指定します。
またコマンドラインで受信パケットを送信元へそのまま戻したり、二つのコネクションを接続するプログラムもそれぞれ、ConnectorとExchange2として含まれています。

さらにこのパッケージをクラスパスに入れておけばTomcatで以下のような2つUDPポートを接続するサーブレットも動作させることができます。

import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.nozomu.audio.PortHandler;
import com.nozomu.tom.utils.MyLog;

/**
 * Servlet implementation class Exchange
 */
@WebServlet(“/Exchange”)
public class Exchange extends HttpServlet {
 private static final long serialVersionUID = 1L;
 List<Port> portList = null;
 List<Connection> connList = null;
      
    /**
     * @see HttpServlet#HttpServlet()
     */
    public Exchange() {
        super();
        // TODO Auto-generated constructor stub
        portList = new ArrayList<Port>();
        connList = new ArrayList<Connection>();
        System.out.println(“here:”+portList.size());
    }

 /**
  * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
  */
 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  // TODO Auto-generated method stub
  MyLog.logInfo(this.getClass().getName()+” doGet is called”);

  request.setCharacterEncoding(“UTF-8″);
  response.setContentType(“text/html; charset=UTF-8″);
  String add = request.getParameter(“a”);
  String del = request.getParameter(“d”);
  String mod = request.getParameter(“m”);
  String connect = request.getParameter(“c”);
  if(add==null)add=”";
  if(del==null)del=”";
  if(mod==null)mod=”";
  if(connect==null)connect=”";
  int port = 0;
  int port1 = 0;
  int port2 = 0;
  String name = request.getParameter(“n”);
  
  try {port = Integer.parseInt(request.getParameter(“p”));
  }catch (NumberFormatException e){port = 0;}
  try {port1 = Integer.parseInt(request.getParameter(“p1″));
  }catch (NumberFormatException e){port1 = 0;}
  try {port2 = Integer.parseInt(request.getParameter(“p2″));
  }catch (NumberFormatException e){port2 = 0;}
  
  String remoteHost = request.getRemoteHost();
  String remoteAddr = request.getRemoteAddr();
  PrintWriter out = response.getWriter();

  out.println(“<html>”);
        out.println(“<head><title>Exchange</title></head>”);
        out.println(“<body>”);
       
        if(add.equals(“1″)&& (port != 0)){
         boolean found = false;
         for(int i=0;i<portList.size();i++){
          Port p = portList.get(i);
          if(p.port == port){
           found = true;
           break;
          }
         }
   if(!found)portList.add(new Port(port,name,remoteHost,remoteAddr));
  }
        if(del.equals(“1″)&&(port != 0)){
         for(int i=0;i<portList.size();i++){
          Port p = portList.get(i);
          if(p.port == port){
           portList.remove(i–);
          }
         }
  }
        if(connect.equals(“1″)&&port1!=0&&port2!=0){
         boolean found = false;
         for(int i=0;i<connList.size();i++){
          Connection c = connList.get(i);
          if(c.portA == port1 || c.portB == port1 ||
            c.portA == port2 || c.portB == port2){
           found = true;
           break;
          }
         }
   if(!found){
    Connection c = new Connection(port1,port2);
    c.start();
    connList.add(c);         
   }
        }

        if(connect.equals(“0″)&&port1!=0&&port2!=0){
         for(int i=0;i<connList.size();i++){
          Connection c = connList.get(i);
          if((c.portA == port1 && c.portB == port2) ||
            (c.portA == port2 && c.portB == port1)){
           connList.get(i).end();
           connList.remove(i–);
          }
         }
        }
       
        out.println(“List of Registerd ports<br>”);
        for(int i=0;i<portList.size();i++){
   Port p = portList.get(i);
   out.println(” “+(i+1)+” : “+p.port+” : “+p.name+” : “+p.remoteHost+” : “+p.remoteAddr+”<br>”);
        }
  String button;
     button = “<form action=\”/MyProject/Exchange\” method=\”get\” accept-charset=\”UTF-8\”>”;
  button = button+”<input type=\”text\” name=\”p\” value=\”0\” /> ポート番号</p>”;
  button = button+”<input type=\”text\” name=\”n\” value=\”name\” /> 名前</p>”;
  button = button+”<input type=\”radio\” name=\”a\” value=\”1\” />追加”;
  button = button+”<input type=\”radio\” name=\”m\” value=\”1\” disabled/>変更”;
  button = button+”<input type=\”radio\” name=\”d\” value=\”1\” />削除</p>”;
  button = button+”<p><input type=\”submit\” value=\”実行\”/></p></form>”;
  out.println(button);

        out.println(“List of Connections<br>”);
        for(int i=0;i<connList.size();i++){
   Connection c = connList.get(i);
   out.println(” “+(i+1)+” : “+c.portA+” : “+c.portB+”<br>”);
        }
  button = “<form action=\”/MyProject/Exchange\” method=\”get\” accept-charset=\”UTF-8\”>”;
  button = button+”<input type=\”text\” name=\”p1\” value=\”0\” /> ポート番号1</p>”;
  button = button+”<input type=\”text\” name=\”p2\” value=\”0\” /> ポート番号2</p>”;
  button = button+”<input type=\”radio\” name=\”c\” value=\”1\” / checked>接続”;
  button = button+”<input type=\”radio\” name=\”c\” value=\”2\” disabled/>変更”;
  button = button+”<input type=\”radio\” name=\”c\” value=\”0\” />削除</p>”;
  button = button+”<p><input type=\”submit\” value=\”実行\”/></p></form>”;
  out.println(button);

  out.println(“</body>”);
  out.println(“</html>”);
 }

 /**
  * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
  */
 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  // TODO Auto-generated method stub
 }

}
class User{
 String name;
 String hostname;
 int  port;      // Assuming port number is the same on both client and server
 boolean connected;
 User    talker;
 public User(String name, String hostname, int port){
  name = this.name;
  hostname = this.hostname;
  port = this.port;
 }
}
class Port{
 int port;
 String name;
 String remoteHost;        // This is not meaningful when registered by web gui
 String remoteAddr;     //
 public Port(int port, String name, String remoteHost, String remoteAddr){
  this.name = name;
  this.port = port;
  this.remoteAddr = remoteAddr;
  this.remoteHost = remoteHost;
 }
}
class Connection extends Thread{
 int portA;
 int portB;
 PortHandler phA;
 PortHandler phB;
 boolean finish = false;
 public Connection(int portA, int portB){
  this.portA = portA;
  this.portB = portB;
  phA = new PortHandler(portA);
  phB = new PortHandler(portB);
  phA.start();
  phB.start();
 }
 public void run(){
  while(!finish){
   if(phA.fromSock!=null)phB.toSock=phA.fromSock;
   if(phA.fromIp!=null)phB.toIp=phA.fromIp;
   if(phA.fromPort!=0)phB.toPort=phA.fromPort;
   if(phB.fromSock!=null)phA.toSock=phB.fromSock;
   if(phB.fromIp!=null)phA.toIp=phB.fromIp;
   if(phB.fromPort!=0)phA.toPort=phB.fromPort;
   try{
    Thread.sleep(1000);    
   }catch(InterruptedException e){
    e.printStackTrace();
   }
  }
 
 }
 public void end(){
  finish = true;
  phA.end();
  phB.end();
  phA = null;
  phB = null;
  
 }
}