Jitsi
Jitsi はビデオ通話、ボイス通話、デスクトップの共有、ファイルやメッセージの交換を行うためのアプリケーションです。Jitsi にとってこういった機能よりも重要なのは、様々なプロトコルが利用できる点です。利用可能なプロトコルは XMPP (Extensible Messaging and Presence Protocol) や SIP (Session Initiation Protocol) といった標準化されているものから、Yahoo! Messenger や Windows Live Messenger (MSN) といったプロプライエタリなものまであります。Jitsi は Microsoft Windows、Apple Mac OS X、Linux、FreeBSD で動作します。ほとんどが Java で書かれていますが、一部にはネイティブのコードも含まれます。この章では Jitsi の OSGi を使ったアーキテクチャ、プロトコルの実装・管理方法を説明し、開発を通じて私たちが学んだことを振り返ります1。
Jitsi の設計
Jitsi (当時は SIP Communicator) を設計したときに重要だと意識していたことが三つあります。それはマルチプロトコルのサポート、クロスプラットフォームの動作、開発者にとっての親しみやすさです。
開発者の視点に立つと、全てのプロトコルに対する共通インターフェースが無ければマルチプロトコルがサポートされているとは言えません。例えばユーザーがメッセージを送るときには、グラフィカルユーザーインターフェースは常に sendMessage
メソッドを呼び出さなければならず、 sendXmppMessage
を呼ぶのか sendSipMsg
を呼ぶのかを現在のプロトコルに応じて変えてはいけません。
Jitsi のほとんどのコードが Java で書かれていることから、クロスプラットフォームでなければならないという二番目の制約の大部分は自動的に満たされます。ただしそれでも、Java 実行環境 (JRE) によってサポートされていない機能や私たちが望む形でサポートされていない (ウェブカメラからのビデオキャプチャのような) 機能がいくつかあります。そのため Windows では DirectShow を、Mac OS X では QTKit を、Linux では Video for Linux 2 を使う必要が生じます。プロトコルのコードと同様に、ビデオ通話を制御する部分のコードにはこういったライブラリの詳細が現れてはいけません (そのままでは複雑すぎます)。
最後の制約である開発者にとっての親しみやすさとは、新しい機能の追加が簡単であることを意味します。現在数百万人のユーザーが数千種類の異なる方法で VoIP を利用しており、多様なサービスプロバイダとサーバーベンダーは今までにない使い方や新しい機能のアイデアを思いつきます。そのような人々が Jitsi でやりたいことを簡単に行える必要があります。機能を拡張しようとしている人が読んで理解する必要があるコードは、プロジェクトの中で変更あるいは拡張しようとしている部分だけであることが望ましいです。同様に、一人が行った変更が他の人の動作に与える影響は最小限でなければなりません。
まとめると、コードの異なる部分が比較的独立に動作するような環境が必要だったということです。例えばオペレーティングシステムに依存する部分は簡単に置き換え可能でなければなりません。さらに並列に動作しながらも同じことを行うプロトコルのような部分もあるので、他の部分を全く変更することなくコードの一部を完全に書き直すことができる必要もあります。さらに Jitsi の一部分を簡単に有効化/無効化する機能も必要とされましたし、プラグインをインターネット経由でリストにダウンロードする機能も望まれました。
独自のフレームワークを書くというのも少しは考えましたが、このアイデアはすぐに捨てられました。VoIP と IM のコードを早く書き始めたくてうずうずしていたので、プラグインフレームワークに数か月を費やすのは面白いとは思えなかったのです。そんなとき誰かが OSGi を提案し、これがパーフェクトであるように思われました。
Jitsi と OSGi フレームワーク
OSGi についての本は既にいくつかあるので、フレームワークの詳細を全て説明することはしません。以下ではその代わりに、Jitsi が OSGi のどの機能をどのように使っているかを説明します。
OSGi で何よりも重要なのはモジュール化です。OSGi アプリケーションの機能はバンドルという単位に分割されます。バンドルは通常の Java ライブラリやアプリケーションに付いてくる JAR ファイルを少し拡張したものであり、Jitsi はバンドルの集合体です。例えば Windows Live Messenger のためのバンドルや XMPP を行うバンドル、あるいは GUI の処理するバンドルなどがあります。全てのバンドルは与えられた環境で実行され、Jitsi の場合には OSGi のオープンソース実装である Apache Felix によって実行されます。
全てのモジュールは連携して動作しなければなりません。例えば GUI バンドルはプロトコルバンドルを使ってメッセージを送信し、そのメッセージは履歴を管理するバンドルによって保存されます。バンドルを管理するときに活躍するのが OSGi サービスです。OSGi サービスはバンドルのどの部分が他のバンドルから見えるかを管理します。OSGi サービスは Java インターフェースの集合であることが多いです。各インターフェースはログ、ネットワーク越しのメッセージの送信、最近の通話リストの取得といった特定の機能を持ちます。機能を実装するクラスはサービス実装と呼ばれ、通常は名前の最後に“Impl”が付きます (例えば ConfigurationServiceImpl
)。OSGi フレームワークではサービス実装を隠蔽してバンドルの外側から見えなくすることが可能であり、こうすることで他のバンドルがバンドルを利用するときには必ずサービスインターフェースが使われるようになります。
バンドルには通常アクティベーターと呼ばれるものが付いています。アクティベーターとは start
メソッドと stop
メソッドを定義するシンプルなインターフェースです。Felix がバンドルを Jitsi に読み込んだり削除したりシャットダウンするときには、Felix がこれらのメソッドを読んで起動またはシャットダウンの準備を行わせます。これらのメソッドを呼ぶとき Felix は BundleContext
というパラメータを渡します。 BundleContext
を受け取ったバンドルは OSGi 環境にアクセスでき、バンドルが必要とする OSGi サービスを検索したり、見つからない場合には自分で登録したりできます (図 10.1)。
この処理で実際に何が起こるかを見ていきましょう。ここでは例として、プロパティをディスクに書き込んだりディスクから読み込んだりするサービスを考えます。このサービスは Jitsi で ConfigurationService
と呼ばれており、次のインターフェースを持ちます:
package net.java.sip.communicator.service.configuration;
public interface ConfigurationService
{
public void setProperty(String propertyName, Object property);
public Object getProperty(String propertyName);
}
とても単純な ConfigurationService
の実装は次のようになります:
package net.java.sip.communicator.impl.configuration;
import java.util.*;
import net.java.sip.communicator.service.configuration.*;
public class ConfigurationServiceImpl implements ConfigurationService
{
private final Properties properties = new Properties();
public Object getProperty(String name)
{
return properties.get(name);
}
public void setProperty(String name, Object value)
{
properties.setProperty(name, value.toString());
}
}
サービスは net.java.sip.communicator.service
パッケージで定義され、実装は net.java.sip.communicator.impl
パッケージで定義される点に注目してください。Jitsi における全てのサービスと実装はこの二つのパッケージで分割されます。OSGi を使うとバンドルのパッケージの一部だけを JAR ファイルの外側から見えるようにできるので、インターフェースと実装を分割したバンドルではサービスパッケージを公開しつつその実装を隠蔽するのが容易になります。
最後にバンドルを BundleContext
に登録して ConfigurationService
の実装を伝えれば、バンドルを使えるようになります。次のようにします:
package net.java.sip.communicator.impl.configuration;
import org.osgi.framework.*;
import net.java.sip.communicator.service.configuration;
public class ConfigActivator implements BundleActivator
{
public void start(BundleContext bc) throws Exception
{
bc.registerService(ConfigurationService.class.getName(), // サービスの名前
new ConfigurationServiceImpl(), // サービスの実装
null);
}
}
BundleContext
に ConfigurationServiceImpl
クラスを登録すれば、このサービスは他のバンドルから利用可能になります。適当なバンドルからプロパティ設定サービスを使う例を示します:
package net.java.sip.communicator.plugin.randombundle;
import org.osgi.framework.*;
import net.java.sip.communicator.service.configuration.*;
public class RandomBundleActivator implements BundleActivator
{
public void start(BundleContext bc) throws Exception
{
ServiceReference cRef = bc.getServiceReference(ConfigurationService.class.getName());
configService = (ConfigurationService) bc.getService(cRef);
// 設定はこれで終わり!サービス実装への参照が手に入ったので、
// プロパティを保存できる。
configService.setProperty("propertyName", "propertyValue");
}
}
もう一度パッケージの名前に注目してください。 net.java.sip.communicator.plugin
に含まれるのは、他のサービスを利用するバンドルであってサービスの公開や実装を行わないものです。設定フォームはそのようなプラグインの良い例です。設定フォームはアプリケーションを設定するための Jitsi ユーザーインターフェースを追加します。ユーザーが設定を変更すると設定フォームは ConfigurationService
あるいは変更された機能のバンドルと直接対話を行いますが、他のバンドルが設定フォームに向かって対話を行うことはありません (図 10.2)。
バンドルのビルドと実行
バンドルのコードの書き方を見たので、次はパッケージ化についてです。全てのバンドルは実行中に OSGi 環境に対して三つのことを提示します。つまり他のバンドルから利用可能な Java パッケージ (エクスポートするパッケージ)、自分が利用する他バンドルのパッケージ (インポートするパッケージ)、そして BundleActivator
クラスの名前です。これらの情報はバンドルが配置される JAR ファイルのマニフェストに書き込まれます。
上で定義した ConfigurationService
に対するマニフェストファイルの一例を次に示します:
Bundle-Activator: net.java.sip.communicator.impl.configuration.ConfigActivator
Bundle-Name: Configuration Service Implementation
Bundle-Description: A bundle that offers configuration utilities
Bundle-Vendor: jitsi.org
Bundle-Version: 0.0.1
System-Bundle: yes
Import-Package: org.osgi.framework,
Export-Package: net.java.sip.communicator.service.configuration
JAR マニフェストを作成すると、バンドルを作る準備が整います。Jitsi ではビルドに関連した作業には全て Apache Ant を使っています。Jitsi のビルドプロセスにバンドルを追加するには、プロジェクトのルートディレクトリにある build.xml
ファイルを編集する必要があります。バンドルの JAR ファイルは build.xml
の最後に bundle-xxx
というターゲットで指定され、今考えている設定サービスをビルドするには次のようにします:
<target name="bundle-configuration">
<jar destfile="${bundles.dest}/configuration.jar" manifest=
"${src}/net/java/sip/communicator/impl/configuration/conf.manifest.mf" >
<zipfileset dir="${dest}/net/java/sip/communicator/service/configuration"
prefix="net/java/sip/communicator/service/configuration"/>
<zipfileset dir="${dest}/net/java/sip/communicator/impl/configuration"
prefix="net/java/sip/communicator/impl/configuration" />
</jar>
</target>
見ればわかるとおり、この Ant ターゲットは設定マニフェストから一つの JAR ファイルを作成し、その JAR ファイルに service
および impl
の階層にある設定パッケージを追加します。ここまでくれば、後は Felix にバンドルをロードさせるだけです。
Jitsi が OSGi バンドルの集合体であるということは前に触れました。ユーザーが Jitsi アプリケーションを実行したときに実際に立ち上がるのは Felix であり、この Felix はロードすべきバンドルのリストを持っています。このリストは lib
ディレクトリの felix.client.run.properties
というファイルに含まれます。Felix がバンドルを起動するときには、スタートレベルに沿って起動していきます。つまりあるレベルにあるバンドルは次のレベルのロードが始まる前に全てロードされることが保証されるということです。上記のコードでは示していませんでしたが、例として使ってきた設定サービスは設定をファイルに書き込むので、 FileAccessService
が必要です。このサービスは fileaccess.jar
ファイルにあるので、 FileAccessService
は ConfigurationService
よりも前にロードされなければなりません:
... ... ...
felix.auto.start.30= \
reference:file:sc-bundles/fileaccess.jar
felix.auto.start.40= \
reference:file:sc-bundles/configuration.jar \
reference:file:sc-bundles/jmdnslib.jar \
reference:file:sc-bundles/provdisc.jar \
... ... ...
felix.client.run.properties
ファイルの最初には、パッケージのリストがあります:
org.osgi.framework.system.packages.extra= \
apple.awt; \
com.apple.cocoa.application; \
com.apple.cocoa.foundation; \
com.apple.eawt; \
... ... ...
このリストは Felix がシステムのクラスパスから利用可能にしなければならないパッケージを表します。つまりこのリストにあるパッケージはどのバンドルによってもエクスポートされないにもかかわらず、他のバンドルからインポートできる (マニフェストヘッダーの Import-Package に書ける) ということです。このリストに含まれるのは JRE に含まれる OS に固有なパッケージであり、Jitsi の開発者が新しいパッケージをこのリストに追加することはほとんどありません。いずれかのバンドルによって既に利用可能になっている場合が多いからです。
ProtocolProvider サービス
Jitsi の ProtocolProviderService
は全てのプロトコル実装が従うべき動作を定義します。このサービスはインターフェースであり、他のバンドル (ユーザーインターフェースなど) はメッセージの送受信、通話、Jitsi が接続するネットワーク越しのファイル交換を行うときにこのサービスを利用します。
プロトコルサービスのインターフェースは net.java.sip.communicator.service.protocol
に全て含まれます。サービスの実装はプロトコルごとに一つずつ存在し、全て net.java.sip.communicator.impl.protocol.protocol_name
にあります。
service.protocol
ディレクトリから見ていきましょう。このディレクトリで一番重要なのは ProtocolProviderService
インターフェースです。プロトコルに関するタスクを実行するバンドルは、そのプロトコルの実装を BundleContext
から検索する必要があります。サービスとその実装があることで、Jitsi はサポートされている任意のネットワークに接続し、接続状態と詳細を取得し、そして一番重要なこととして、チャットや通話といった実際の通信を行うクラスへの参照を取得できます。
オペレーションセット
前述の通り、 ProtocolProviderService
は様々な通信プロトコルを実装し、それらの間の違いを吸収する必要があります。これはメッセージの送信といった全てのプロトコルに共通の機能に対しては簡単なことですが、一部のプロトコルしかサポートしない機能に対しては自体が複雑になります。この差異がサービスそのものに由来することもあります。例えばサーバーに連絡先リストを保存する機能は多くのプロトコルでサポートされていますが、SIP サービスのほとんどはこれをサポートしません。また MSN と AIM も良い例です。オフラインのユーザーにメッセージを送る機能を持たないのがこの二つだけだったことがありました (現在では違います)。
GUI などの他のバンドルがこういった違いに応じた動作を行えるようにするのが ProtocolProviderService
の重要な役割です。例えば AIM を使って通話を行うことができないのなら、AIM を使う連絡先の欄に通話ボタンを追加してはいけません。
ここで活躍するのがオペレーションセットです。オペレーションセットとはその名の通り操作 (オペレーション) をまとめたものであり、そのインターフェースは Jitsi バンドルによってプロトコルごとの実装を制御するために使われます。オペレーションセットのインターフェースにあるメソッドはどれも特定の機能と結びついています。例えば OperationSetBasicInstantMessaging
には、インスタントメッセージを作成、送信するためのメソッドが含まれます。あるいは OperationSetPresence
には、連絡先の状態を問い合わせたり、ユーザー自身の状態を変更するためのメソッドが含まれます。よって GUI が連絡先に表示する状態を更新したり、連絡先にメッセージを送ったりするときは、最初に対応するプロバイダに状態の問い合わせやメッセージをサポートするかどうかを尋ねることができます。この仕組みのために ProtocolProviderService
が定義する関数を示します:
public Map<String, OperationSet> getSupportedOperationSets();
public <T extends OperationSet> T getOperationSet(Class<T> opsetClass);
オペレーションセットを設計するときには、新しいプロトコルがあるオペレーションセットの一部をサポートするにもかかわらずそのセットの他のオペレーションをサポートしないことが無いようにしなければなりません。例えば他の人の状態を問い合わせることが可能であってもサーバーに連絡先リストを保存できないプロトコルが存在するので、状態の問い合わせの機能と連絡先の取得機能を両方 OperationSetPresence
に入れることはできず、連絡先をオフラインに保存するプロトコルのための OperationSetPersistentPresence
というセットを定義することになります。これに対して、メッセージを送信できるにもかかわらずメッセージを受信できないプロトコルというのは目にしたことがないので、この二つの機能は一緒にしても問題ありません。
アカウント、ファクトリ、プロバイダのインスタンス
ProtocolProviderService
の重要な特徴は、一つのプロトコルのアカウントごとに一つのインスタンスが存在することです。したがってユーザーが登録したアカウントと同じ数のサービス実装が BundleContext
には常に存在することになります。
プロトコルプロバイダを誰が作成、登録するのかが気になるかもしれませんが、これには二つの要素が関係します。一つ目は ProtocolProviderFactory
です。他のバンドルはこれを使ってプロバイダをインスタンス化し、インスタンス化したプロバイダをサービスとして登録します。プロトコルごとに1つずつファクトリがあり、各ファクトリはその特定のプロトコルのプロバイダの作成を受け持ちます。ファクトリの実装はプロトコルの内部詳細と同じ場所に保存されており、例えば SIP のファクトリは net.java.sip.communicator.impl.protocol.sip.ProtocolProviderFactorySipImpl
にあります。
アカウントの作成に関わるもう一つの要素は、プロトコルウィザードです。プロトコルウィザードは GUI が関係するので、ファクトリとは異なりプロトコルの実装の他の部分とは切り離されています。例えばユーザーが SIP アカウントを作成するためのウィザードは net.java.sip.communicator.plugin.sipaccregwizz
にあります。
メディアサービス
IP 越しのリアルタイム通話を考えるときに重要なことが一つあります。それは最もよく使われるプロトコルとみなされている SIP や XMPP といった VoIP プロトコルは、インターネット越しにオーディオやビデオを転送するのに使われるプロトコルではないということです。この転送を実際に行うのは Real-time Transport Protocol (RTP) というプロトコルです。SIP や XMPP は RIP パケットの宛先の判断や、オーディオやビデオがエンコードされるべきフォーマット (“コーデック”) の調整に利用されます。SIP と XMPP はこの他にもユーザーの特定や接続の維持、呼び出し音の作成といった様々なことを行うので、こういったプロトコルはシグナリングプロトコルと呼ばれています。
これは Jitsi に対して何を意味するでしょうか?まず分かるのは、jitsi の sip
や jabber
といったパッケージにはオーディオフローやビデオフローを操作するためのコードが存在しないということです。この種のコードは MediaService に存在し、その実装は net.java.sip.communicator.service.neomedia
と net.java.sip.communicator.impl.neomedia
にあります。
neomedia パッケージの“neo”は、このパッケージが過去に書いたパッケージを置き換えるものであることを表します。私たちが最初に使っていたコードは完全に書き換えなければなりませんでした。ここから私たちは、アプリケーションを未来に 100% 対応できるよう設計するのに時間をかけても意味がないという経験則を学びました。全てを前もって考慮に入れるのは不可能であり、どうやっても後から変更することになるのです。加えて、設計フェーズで苦労して準備したシナリオが実際には起こらず、設計に伴う複雑さが全く無駄になってしまうこともよくあります。
MediaService の他に特に重要なインターフェースが二つあります。MediaDevice と MediaStream です。
キャプチャ、ストリーミング、プレイバック
MediaDevice は通話中に利用されるキャプチャデバイスおよびプレイバックデバイスを表します (図 10.4)。マイク、スピーカー、ヘッドセット、ウェブカメラは全て MediaDevice ですが、これで全てではありません。例えば Jitsi のデスクトップストリーミングや共有通話ではデスクトップからビデオをキャプチャし、会議通話ではアクティブな参加者からのオーディオをミックスするために AudioMixer デバイスを使います。どの例においても、一つの MediaDevice が表すのは一つの MediaType だけであり、担当できるのはオーディオかビデオのどちらか一方だけです。例えばマイクの付いたウェブカメラを使った場合には、Jitsi からは二つのデバイスとして見えます。一つがビデオキャプチャ用、もう一つがサウンドキャプチャ用のデバイスです。
しかしデバイスだけでは音声通話やビデオ通話を行うには不十分であり、メディアのキャプチャをしたらデータをネットワーク越しに送らなければなりません。この処理に使われるのが MediaStream です。MediaStream インターフェースは MediaDevice と通話相手を繋ぎ、通話中にやり取りされるパケットを表現します。
デバイスと同じように、ストリームも一つの MediaType につき一つだけ作られます。そのため音声とビデオを使った通話では、Jitsi が二つのメディアストリームを作成し、それぞれを対応するオーディオとビデオの MediaDevice に接続します。
コーデック
メディアのストリーミングにおけるもう一つの重要な概念が、MediaFormat (コーデック) です。多くのオペレーティングシステムではデフォルトのオーディオキャプチャが 48KHz PCM 程度となっており、この設定のことを私たちは「生のオーディオ (raw audio)」と呼んでいます。WAV ファイルとして手に入るオーディオもこの設定をしています。音質は申し分ありませんが、サイズがとても大きくなります。PCM フォーマットのオーディオをインターネット越しにやり取りするのは全く現実的ではありません。
ここで使われるのがコーデックです。コーデックを使うとオーディオやビデオを様々な方式で表現、転送できます。iLBC、8KHz Speex、G.729 といったオーディオコーデックは帯域をあまり必要としませんが、音声がいくらかくもって聞こえます。対して広帯域 Speex や G.722 といったものは音質に優れますが、帯域を多く必要とします。帯域の使用量を抑えながらも高い品質を達成しようとしているコーデックもあり、広く使われているビデオコーデック H.264 がその良い例です。ただし H.264 では変換のときの計算量がトレードオフとなります。Jitsi で H.264 を使ったビデオ通話を行うと高い画質が手に入り、必要とされる帯域もそれほど高くありませんが、CPU がフル稼働することになります。
以上の説明は単純化が過ぎますが、中心となるアイデアはコーデックの選択は折衷案の探索であるということです。帯域、品質、CPU 使用量のいずれか、またはいくつかを犠牲にすることになります。説明を端折ったのは、VoIP に取り組む人がコーデックについてこれ以上知っておく必要がほとんどないためです。
プロトコルプロバイダを使った接続
オーディオとビデオをサポートする全ての Jitsi のプロトコルは、MediaService を同じ方法で利用します。最初に行われるのは、システムで利用可能なデバイスを MediaService に尋ねる処理です:
public List<MediaDevice> getDevices(MediaType mediaType, MediaUseCase useCase);
mediaType
にはオーディオデバイスとビデオデバイスのどちらを取得するのかを指定します。 useCase
はビデオデバイスでのみ使われるパラメータであり、取得しようとするデバイスが通常の通話 (MediaUseCase.CALL
) で使うデバイスなのかそれともデスクトップ共有セッション (MediaUseCase.DESKTOP
) で使うデバイスなのかを表します。通常の通話のデバイスならば利用可能なウェブカメラのリストが返り、デスクトップ共有セッションならばユーザーデスクトップへの参照が返ります。
次に行われるのはデバイスで利用可能なフォーマットの取得です。 MediaDevice.getSupportedFormats
メソッドが使われます:
public List<MediaFormat> getSupportedFormats();
リストを手に入れたプロトコル実装はそれをリモートパーティに送り、リモートパーティはサポートされているフォーマットを表すリストの部分集合を返します。このやり取りはオファー/アンサーモデルとして知られており、Session Description Protocol などが使われます。
フォーマットに加えてポート番号と IP アドレスのやり取りが完了すると、VoIP プロトコルは設定を構築して MediaStream を開始します。大まかな初期化処理は次のようになります:
// まずストリームコネクターを作る。このコネクターは RTP を使ったメディア転送および
// RTCP を使ったフロー制御と統計メッセージのやり取りに利用するソケットを
// メディアサービスに伝える。
StreamConnector connector = new DefaultStreamConnector(rtpSocket, rtcpSocket);
MediaStream stream = mediaService.createMediaStream(connector, device, control);
// MediaStreamTarget は通話相手がメディアを受け取るアドレスとポートを指定する。
// この情報を伝える方法は VoIP プロトコルによって異なる。
stream.setTarget(target);
// MediaDirection はストリームが外向きか、内向きか、両方向かを指定する。
stream.setDirection(direction);
// ストリームのフォーマットを指定する。
// 実際にはセッションから帰ってきたリストの先頭にあるフォーマットが使われる。
stream.setFormat(format);
// メディアをデバイスからもらってインターネット越しにストリームする準備が以上で整う。
stream.start();
UI サービス
これまで見てきたのは Jitsi のうちプロトコル、メッセージの送受信、通話に関する部分です。しかし Jitsi は一般の人々が利用するアプリケーションであり、最も重要なのはユーザーインターフェースです。ユーザーインターフェースは Jitsi に含まれる他の全てのバンドルが提供するサービスを利用しますが、ときには逆のことが起こる場合もあります。これについて見ていきましょう。
この例として最初に思いつくのはプラグインです。Jitsi のプラグインはユーザーと対話を行うことが多く、ユーザーインターフェースで開かれているウィンドウやパネル内のコンポーネントを開いたり、閉じたり、動かしたりする必要があります。UIService はこのために使われるサービスであり、Jitsi のメインウィンドウに対する基本的な操作を提供します。このサービスを使うことで、Mac OS X のドックアイコンや Windows の通知からアプリケーションを操作することが可能になっています。
プラグインは連絡先をいじるだけではなく、Jitsi を拡張するのにも使えます。この良い例はチャットの暗号化 (OTR) を実装するプラグインでしょう。この OTR バンドルはいくつかの GUI コンポーネントをユーザーインターフェースの様々の部分に追加する必要があります。つまりチャットウィンドウに鍵ボタンを付けたり、連絡先の右クリックメニューにサブセクションを追加したりするということです。
ここで良いニュースは、以上のことがわずか数回のメソッドを呼び出しで可能なことです。OTR バンドルに対する OSGi アクティベーター OtrActivator
は次のようになっています:
Hashtable<String, String> filter = new Hashtable<String, String>();
// 右クリックメニューを登録する。
filter.put(Container.CONTAINER_ID,
Container.CONTAINER_CONTACT_RIGHT_BUTTON_MENU.getID());
bundleContext.registerService(PluginComponent.class.getName(),
new OtrMetaContactMenu(Container.CONTAINER_CONTACT_RIGHT_BUTTON_MENU),
filter);
// チャットウィンドウのメニューバー項目を登録する。
filter.put(Container.CONTAINER_ID,
Container.CONTAINER_CHAT_MENU_BAR.getID());
bundleContext.registerService(PluginComponent.class.getName(),
new OtrMetaContactMenu(Container.CONTAINER_CHAT_MENU_BAR),
filter);
見ればわかるとおり、グラフィカルユーザーインターフェースに要素を追加するのは OSGi サービスを登録するだけで済みます。フェンスの向こう側では私たちの書いた UIService の実装が PluginComponent インターフェースの実装を見張っており、新しく登録された実装を検出したときにはその参照が取得され、OSGi サービスのフィルタが指定するコンテナにその参照を追加します。
右クリックメニュー項目の場合に何が起こるのかを次に示します。UI バンドルの中には、右クリックメニューを表す MetaContactRightButtonMenu というクラスが次のように定義されています:
// OSGi バンドルコンテキストを通じて登録されたプラグイン要素を探索する。
ServiceReference[] serRefs = null;
String osgiFilter = "("
+ Container.CONTAINER_ID
+ "="+Container.CONTAINER_CONTACT_RIGHT_BUTTON_MENU.getID()+")";
serRefs = GuiActivator.bundleContext.getServiceReferences(
PluginComponent.class.getName(),
osgiFilter);
// 見つかったプラグインを走査し、メニューに加える。
for (int i = 0; i < serRefs.length; i ++)
{
PluginComponent component = (PluginComponent) GuiActivator
.bundleContext.getService(serRefs[i]);
component.setCurrentContact(metaContact);
if (component.getComponent() == null)
continue;
this.add((Component)component.getComponent());
}
そしてこれで全てであり、Jitsi 内のウィンドウは全て同じことをしています。つまり、バンドルコンテキストに含まれるサービスを調べ、PluginComponent インターフェースを実装しているサービスで今考えているコンテナに追加されることを表すフィルタを持っているものを取得するという処理です。プラグインは目的地が書かれたサインを掲げるヒッチハイカーのようなものであり、Jitsi のウィンドウという車に拾われます。
教訓
SIP Communicator に取り組み始めたときに一番よく受けた批判と質問は、「どうして Java を使うのか?Java が遅いと知らないのか?オーディオ通話やビデオ通話を高品質に行うなんて不可能だ!」というものでした。「Java は遅い」という神話は Jitsi を試すことなく Skype にとどまり続ける潜在的なユーザーたちによって唱え続けられています。しかしこのプロジェクトを始めて私たちが最初に学んだ教訓は、C++ などのネイティブの言語と比べたとしても Java で実行速度が問題になる頻度が増えるわけではないということです。
Java の選択が他の全ての選択肢を徹底的な調査に基づいた判断であると言うつもりはありません。私たちはただ Windows と Lunux の両方で動作するアプリケーションを簡単に作りたかったのであり、Java と Java Media Framework がそれを行う比較的簡単な方法に思えたというだけです。
長い期間の開発を経ても、この判断を後悔する理由は多くありません。むしろその逆です。Java のポータビリティは完全ではないものの役立っており、SIP のコードの 90 % は異なる OS の間で共通です。共通コードには全てのプロトコルスタック (SIP、XMPP、RTP など) の複雑な実装が含まれます。コードのそのような部分において OS について考えなくても済むのはこの上なく役立っています。
さらに、Java の人気がコミュニティを形成する上でとても重要であることも判明しました。コントリビューターはそれ自身貴重なリソースであり、プロジェクトに参加してもらうには時間とモチベーションが必要です。新しい言語を習得しなくて良いというのはこの両方を奮い立たせるためのアドバンテージとなります。
多くの人の予想に反する形で、Java の実行速度が遅いことが原因でネイティブの言語を使うことになったことはほとんどありませんでした。ほとんどの場合ネイティブの言語を使うことになった原因は、 OS とのやり取りがそもそもできないこと、あるいは Java が持っている OS 固有のユーティリティが OS へのアクセスを少ししか持たないことでした。以下では Java が上手く扱えない三つの重要な領域について議論します。
Java Sound vs. PortAudio
Java Sound はオーディオのキャプチャと再生のための Java のデフォルト API です。実行環境の一部なので、Java 仮想マシンが動作する全てのプラットフォームで実行できます。SIP Communicator は最初の一年間 Java Sound だけを使ってきましたが、いくつか不都合が生じました。
まず、Java Sound の API には利用するオーディオデバイスを選択するオプションがありませんでした。これは重大な問題です。コンピューターを使って音声通話やビデオ通話を行うユーザーは、立派な USB ヘッドセットやその他のオーディオデバイスを使って通話の品質を高めることがよくあります。コンピューターに接続されたデバイスが複数ある場合 Java Sound は OS がデフォルトで選択するデバイスに全てのオーディオを送りますが、多くのケースでこれは十分ではありません。多くのユーザーが望むのは、例えば他のアプリケーションはデフォルトのサウンドカードを使いつつも、音楽だけはスピーカーから流すといった使い方です。もっと重要なシナリオとしては、SIP Communicator が通知音と通話の音声を異なるデバイスに送るというのがあります。こうするとユーザーはコンピューターの前にいなくてもスピーカからの着信アラートを聞き取ることができ、着信に応答した後はヘッドセットを使って通話できるようになります。
以上のシナリオはどれも Java Sound を使った場合には不可能です。さらに、Java Sound の Linux 実装は現在の Linux ディストリビューションの多くで廃止されている OSS を利用しています。
これを受けて私たちは別のオーディオシステムを使う決断をしました。マルチプラットフォームという特性を手放したくはありませんでしたし、オーディオシステムを自分たちで全て書かなければならないというのは可能な限り避けたかったからです。ここで PortAudio2 が非常に助けになりました。
Java だけを使って何かを行うことができないときには、クロスプラットフォームのオープンソースプロジェクトが次善の策となります。PortAudio に乗り換えたことで上述したようなきめ細かな調整が可能なオーディオレンダリングとキャプチャが可能になりました。さらに PortAudio は Windows, Linux, Mac OS X, FreeBSD で動作し、さらに私たちがまだパッケージを用意できていない他の OS でも動作します。
ビデオのキャプチャとレンダリング
ビデオはオーディオと同じく重要なはずですが、どうやら Java の製作者はそうは思わなかったようです。JRE にはビデオのキャプチャとレンダリングのためのデフォルト API が存在しません。しばらくは Java Media Framework がデフォルトの API になるのかと思われていましたが、Sun はこのライブラリの保守をやめてしまいました。
当然の成り行きとして私たちは PortAudio のような代替案を探し始めましたが、今回は運がありませんでした。最初は Ken Larson による LTI-CIVIL フレームワーク3 に決定しました。これは素晴らしいプロジェクトであり、ずいぶん長い間使われましたが4、リアルタイムの通信にはあまり適していないことが判明しました。
そして私たちが達した結論は、申し分のない Jitsi 用のビデオ通信を手に入れるには、ネイティブのグラバーとレンダラを私たちが独自に実装するしかないというものでした。これによって複雑さと保守の負荷が大きくプロジェクトに加わるので簡単な決断ではありませんでしたが、他に選択肢が無かったのです。高画質のビデオ通話は何としても手に入れたいものでした。そしてこの決断のおかげで高画質のビデオ通話が手に入りました!
私たちの書いたネイティブのグラバーとレンダラは、Linux では Video4Linux 2 を、Mac OS X では QTKit を、Windows では DirectShow/Direct3D を使っています。
その他
より良い結果を求めてネイティブで書く必要が生じた箇所は他にもいくつかあります。Mac OS X における Growl を使ったシステムトレイ通知や Linux における libnotify がその例です。他には Microsoft Outlook や Apple Address Book に連絡先データベースを問い合わせる処理、目的地に応じてソース IP を決定する処理、Speex と G.722 に対する既存のコーデック実装を利用する処理、デスクトップのスクリーンショットを撮る処理、文字をキーコードに変換する処理があります。
ここで重要なのは、ネイティブのコードを使って問題を解決する必要が生じたときにはいつでもそれが可能であり、実際に行ってきたということです。私たちが Jitsi の開発を始めてからというもの、Jitsi は修正され、機能が追加され、様々なパーツが完全に書き直されることで、見た目、動作、パフォーマンスが改善されてきました。しかし最初に正しくできなかったことを私たちが後悔したことは一度たりともありません。迷ったときには、できそうな選択肢を選んでそれで進んでみたのです。自分たちがやっていることをよく理解するまで待つこともできたでしょうが、もしそうしていたら現在 Jitsi は存在していないでしょう。
謝辞
この章の全ての図を作成してくれた Yana Stamcheva に感謝します。
-
本文を読みながらソースコードを直接参照したい場合には、ソースコードを http://jitsi.org/source からダウンロードしてください。もし Eclipse か NetBeans を使っているのなら、http://jitsi.org/eclipse と http://jitsi.org/netbeans に設定方法が載っています。[return]
-
実際、非デフォルトのオプションとして現在も残っています。[return]