Jitsi 是一款允许用户进行视频和语音通话、共享桌面以及交换文件和消息的应用程序。更重要的是,它允许用户通过多种不同的协议进行这些操作,从标准化的 XMPP(可扩展消息处理和出席协议)和 SIP(会话发起协议)到 Yahoo! 和 Windows Live Messenger(MSN)等专有协议。它可以在 Microsoft Windows、Apple Mac OS X、Linux 和 FreeBSD 上运行。它主要用 Java 编写,但也包含部分用原生代码编写的部分。在本章中,我们将了解 Jitsi 基于 OSGi 的架构,了解它如何实现和管理协议,并回顾我们在构建它中学到的知识。1
在设计 Jitsi(当时称为 SIP Communicator)时,我们必须牢记三个最重要的约束条件:多协议支持、跨平台操作和开发者友好性。
从开发者的角度来看,多协议意味着对所有协议都拥有一个通用的接口。换句话说,当用户发送消息时,无论当前选择的协议实际使用的是 sendXmppMessage
还是 sendSipMsg
方法,我们的图形用户界面都需要始终调用相同的 sendMessage
方法。
我们的大部分代码是用 Java 编写的,这在很大程度上满足了我们的第二个约束:跨平台操作。尽管如此,Java 运行时环境 (JRE) 仍有一些不支持或无法按照我们想要的方式执行的操作,例如从网络摄像头捕获视频。因此,我们需要在 Windows 上使用 DirectShow,在 Mac OS X 上使用 QTKit,在 Linux 上使用 Video for Linux 2。就像协议一样,控制视频通话的代码部分无需考虑这些细节(本身已经足够复杂了)。
最后,开发者友好意味着应该易于添加新功能。如今,数百万人以数千种不同的方式使用 VoIP;各种服务提供商和服务器供应商提出了不同的用例和关于新功能的想法。我们必须确保他们可以轻松地按照自己的方式使用 Jitsi。需要添加新内容的人员只需要阅读和理解他们正在修改或扩展的项目部分。同样,一个人的更改对其他人的工作应该产生尽可能小的影响。
总而言之,我们需要一个代码的不同部分相对独立的环境。必须能够根据操作系统轻松替换某些部分;让其他部分(如协议)并行运行,但行为相同;并且必须能够完全重写任何一个部分,而代码的其余部分无需任何更改即可工作。最后,我们希望能够轻松地启用和禁用部分功能,以及能够从 Internet 下载插件到我们的列表中。
我们简要考虑过编写自己的框架,但很快放弃了这个想法。我们渴望尽快开始编写 VoIP 和 IM 代码,而花几个月的时间开发插件框架似乎并不那么令人兴奋。有人建议使用 OSGi,它似乎非常适合。
人们已经写了整本书来介绍 OSGi,所以我们不会介绍该框架代表的所有内容。相反,我们只解释它为我们提供了什么以及我们在 Jitsi 中如何使用它。
最重要的是,OSGi 与模块有关。OSGi 应用程序中的功能被分成多个捆绑包。OSGi 捆绑包只不过是一个常规的 JAR 文件,类似于用于分发 Java 库和应用程序的文件。Jitsi 是此类捆绑包的集合。一个负责连接到 Windows Live Messenger,另一个负责 XMPP,另一个负责处理 GUI,等等。所有这些捆绑包都在一个环境中一起运行,在本例中由 Apache Felix(一个开源的 OSGi 实现)提供。
所有这些模块都需要协同工作。GUI 捆绑包需要通过协议捆绑包发送消息,而协议捆绑包又需要通过处理消息历史记录的捆绑包存储这些消息。这就是 OSGi 服务的用途:它们代表捆绑包中对其他所有人可见的部分。OSGi 服务通常是一组 Java 接口,允许使用特定功能,例如日志记录、通过网络发送消息或检索最近通话列表。实际实现该功能的类称为服务实现。它们大多数都带有其实现的服务接口的名称,并在末尾添加“Impl”后缀(例如,ConfigurationServiceImpl
)。OSGi 框架允许开发人员隐藏服务实现,并确保它们永远不会在其所在的捆绑包外部可见。这样,其他捆绑包只能通过服务接口使用它们。
大多数捆绑包还具有激活器。激活器是定义 start
和 stop
方法的简单接口。每次 Felix 在 Jitsi 中加载或删除捆绑包时,它都会调用这些方法,以便捆绑包可以准备运行或关闭。调用这些方法时,Felix 会将名为 BundleContext 的参数传递给它们。BundleContext 为捆绑包提供了一种连接到 OSGi 环境的方法。这样,它们就可以发现需要使用的任何 OSGi 服务,或自己注册服务(图 10.1)。
图 10.1:OSGi 捆绑包激活
那么让我们看看它是如何工作的。假设一项服务可以持久地存储和检索属性。在 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(), // service name new ConfigurationServiceImpl(), // service implementation null); } }
ConfigurationServiceImpl
类在 BundleContext
中注册后,其他捆绑包就可以开始使用它。以下示例显示了一些随机捆绑包如何使用我们的配置服务
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); // And that's all! We have a reference to the service implementation // and we are ready to start saving properties: configService.setProperty("propertyName", "propertyValue"); } }
再次注意包。在 net.java.sip.communicator.plugin
中,我们保留了使用其他服务定义的服务但既不导出也不实现任何服务的捆绑包。配置表单就是此类插件的一个很好的例子:它们是 Jitsi 用户界面的补充,允许用户配置应用程序的某些方面。当用户更改首选项时,配置表单会与 ConfigurationService
或直接与负责某个功能的捆绑包交互。但是,其他任何捆绑包都不需要以任何方式与它们交互(图 10.2)。
图 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 文件,并将 service
和 impl
层次结构中的配置包添加到其中。现在,我们唯一需要做的就是让 Felix 加载它。
我们已经提到,Jitsi 仅仅是 OSGi 捆绑包的集合。当用户执行应用程序时,他们实际上启动 Felix 并提供其需要加载的捆绑包列表。您可以在我们的 lib
目录中找到该列表,位于名为 felix.client.run.properties
的文件中。Felix 按启动级别定义的顺序启动捆绑包:保证在特定级别内的所有捆绑包都完成,然后才开始加载后续级别的捆绑包。虽然在上面的示例代码中您看不到这一点,但我们的配置服务将属性存储在文件中,因此它需要使用我们的 FileAccessService(在 fileaccess.jar
文件中提供)。因此,我们将确保 ConfigurationService 在 FileAccessService 之后启动
⋮ ⋮ ⋮ 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 部分的包,Jitsi 开发人员很少需要向其中添加新包;在大多数情况下,包由捆绑包提供。
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 是另一个很好的例子:曾经它们都不提供向离线用户发送消息的功能,而其他所有协议都提供。 (这种情况现已改变。)
底线是我们的 ProtocolProviderService
需要有一种方法来处理这些差异,以便其他捆绑包(如 GUI)相应地做出反应;如果无法实际拨打电话,则没有必要为 AIM 联系人添加呼叫按钮。
OperationSets 来救援(图 10.3)。不出所料,它们是一组操作,并提供 Jitsi 捆绑包用来控制协议实现的接口。您在操作集接口中找到的方法都与特定功能相关。例如,OperationSetBasicInstantMessaging 包含创建和发送即时消息的方法,以及注册允许 Jitsi 检索收到的消息的侦听器。另一个例子,OperationSetPresence,具有查询联系人列表中联系人状态和设置自身状态的方法。因此,当 GUI 更新它显示的联系人状态或向联系人发送消息时,它首先能够询问相应的提供程序他们是否支持状态和消息传递。ProtocolProviderService
为此目的定义的方法是
public Map<String, OperationSet> getSupportedOperationSets(); public <T extends OperationSet> T getOperationSet(Class<T> opsetClass);
必须设计 OperationSets,以使我们添加的新协议不太可能仅支持 OperationSet 中定义的一些操作。例如,某些协议不支持服务器存储的联系人列表,即使它们允许用户查询彼此的状态。因此,我们没有将状态管理和好友列表检索功能组合在 OperationSetPresence
中,而是还定义了一个 OperationSetPersistentPresence
,它仅与能够在线存储联系人的协议一起使用。另一方面,我们还没有遇到仅允许发送消息而不接收任何消息的协议,这就是发送和接收消息可以安全地组合在一起的原因。
图 10.3:操作集
ProtocolProviderService
的一个重要特征是,一个实例对应一个协议帐户。因此,在任何给定时间,您在 BundleContext
中拥有的服务实现数量与用户注册的帐户数量相同。
此时您可能想知道谁创建并注册了协议提供程序。涉及两个不同的实体。首先是 ProtocolProviderFactory
。这是允许其他捆绑包实例化提供程序并将其注册为服务的服务。每个协议都有一个工厂,每个工厂负责为该特定协议创建提供程序。工厂实现与协议内部的其他部分一起存储。例如,对于 SIP,我们有 net.java.sip.communicator.impl.protocol.sip.ProtocolProviderFactorySipImpl
。
参与帐户创建的第二个实体是协议向导。与工厂不同,向导与协议实现的其余部分分离,因为它们涉及图形用户界面。例如,允许用户创建 SIP 帐户的向导可以在 net.java.sip.communicator.plugin.sipaccregwizz
中找到。
在处理基于 IP 的实时通信时,有一件重要的事情需要理解:像 SIP 和 XMPP 这样的协议,虽然被许多人认为是最常见的 VoIP 协议,但它们并不是真正通过互联网传输语音和视频的协议。这项任务由实时传输协议 (RTP) 处理。SIP 和 XMPP 只负责准备 RTP 需要的一切,例如确定需要发送 RTP 数据包的地址并协商音频和视频需要编码的格式(即编解码器)等。它们还负责诸如查找用户、维护其状态、使电话响铃以及许多其他事情。这就是为什么 SIP 和 XMPP 等协议通常被称为信令协议的原因。
在 Jitsi 的上下文中这意味着什么?好吧,首先这意味着您不会在 sip 或 jabber jitsi 包中找到任何操作音频或视频流的代码。这种代码存在于我们的 MediaService 中。MediaService 及其实现位于 net.java.sip.communicator.service.neomedia
和 net.java.sip.communicator.impl.neomedia
中。
为什么是“neomedia”?
neomedia 包名称中的“neo”表示它替换了我们最初使用的类似包,然后我们不得不完全重写该包。这实际上是我们想出的经验法则之一:花费大量时间设计应用程序以使其 100% 面向未来几乎是不值得的。根本无法考虑所有因素,因此您无论如何都必须以后进行更改。此外,精心设计的阶段很可能会引入您永远不需要的复杂性,因为您准备的场景从未发生过。
除了 MediaService 本身之外,还有另外两个特别重要的接口:MediaDevice 和 MediaStream。
MediaDevices 代表我们在通话期间使用的捕获和播放设备(图 10.4)。您的麦克风和扬声器、您的耳机和您的网络摄像头都是此类 MediaDevices 的示例,但它们并非唯一示例。Jitsi 中的桌面流式传输和共享通话从您的桌面上捕获视频,而电话会议使用 AudioMixer 设备来混合我们从活动参与者那里接收到的音频。在所有情况下,MediaDevices 仅表示单个 MediaType。也就是说,它们只能是音频或视频,而不能同时是两者。这意味着,例如,如果您有一个带有集成麦克风的网络摄像头,Jitsi 会将其视为两个设备:一个只能捕获视频,另一个只能捕获声音。
但是,仅靠设备不足以进行电话或视频通话。除了播放和捕获媒体外,还必须能够将其发送到网络上。这就是 MediaStreams 的用武之地。MediaStream 接口是连接 MediaDevice 和您的对话者的桥梁。它表示您在通话过程中与他们交换的传入和传出数据包。
与设备一样,一个流只能负责一个 MediaType。这意味着在音频/视频通话的情况下,Jitsi 必须创建两个单独的媒体流,然后将每个流连接到相应的音频或视频 MediaDevice。
图 10.4:不同设备的媒体流
媒体流式传输中的另一个重要概念是 MediaFormats,也称为编解码器。默认情况下,大多数操作系统允许您以 48KHz PCM 或类似格式捕获音频。这就是我们通常所说的“原始音频”,也是您在 WAV 文件中获得的音频类型:质量极佳,大小巨大。尝试以 PCM 格式通过互联网传输音频是相当不切实际的。
这就是编解码器的作用:它们允许您以各种不同的方式呈现和传输音频或视频。一些音频编解码器,如 iLBC、8KHz Speex 或 G.729,具有低带宽要求,但声音有些闷。其他一些编解码器,如宽带 Speex 和 G.722,可以提供极佳的音频质量,但也需要更大的带宽。还有一些编解码器试图在保持带宽要求在合理水平的同时提供良好的质量。流行的视频编解码器 H.264 就是一个很好的例子。这里的权衡是转换期间所需的计算量。如果您使用 Jitsi 进行 H.264 视频通话,您会看到高质量的图像,并且您的带宽要求相当合理,但您的 CPU 会以最大速度运行。
所有这些都是一种过于简化的说法,但其思想是编解码器的选择完全是权衡取舍。您可以牺牲带宽、质量、CPU 强度或这些因素的某种组合。使用 VoIP 的人员很少需要了解有关编解码器的更多信息。
Jitsi 中当前具有音频/视频支持的协议都以完全相同的方式使用我们的 MediaServices。首先,它们询问 MediaService 系统上可用的设备
public List<MediaDevice> getDevices(MediaType mediaType, MediaUseCase useCase);
MediaType 指示我们是否对音频或视频设备感兴趣。MediaUseCase 参数当前仅在视频设备的情况下才考虑。它告诉媒体服务我们是否希望获取可用于常规通话的设备(MediaUseCase.CALL),在这种情况下,它会返回可用网络摄像头的列表,或者桌面共享会话(MediaUseCase.DESKTOP),在这种情况下,它会返回对用户桌面的引用。
下一步是获取特定设备可用的格式列表。我们通过 MediaDevice.getSupportedFormats
方法来实现这一点
public List<MediaFormat> getSupportedFormats();
获得此列表后,协议实现将其发送到远程方,远程方会以其子集进行响应,以指示其支持哪些格式。这种交换也称为“Offer/Answer 模型”,它通常使用会话描述协议或其某种形式。
交换格式以及一些端口号和 IP 地址后,VoIP 协议将创建、配置并启动 MediaStreams。粗略地说,此初始化过程如下所示
// first create a stream connector telling the media service what sockets // to use when transport media with RTP and flow control and statistics // messages with RTCP StreamConnector connector = new DefaultStreamConnector(rtpSocket, rtcpSocket); MediaStream stream = mediaService.createMediaStream(connector, device, control); // A MediaStreamTarget indicates the address and ports where our // interlocutor is expecting media. Different VoIP protocols have their // own ways of exchanging this information stream.setTarget(target); // The MediaDirection parameter tells the stream whether it is going to be // incoming, outgoing or both stream.setDirection(direction); // Then we set the stream format. We use the one that came // first in the list returned in the session negotiation answer. stream.setFormat(format); // Finally, we are ready to actually start grabbing media from our // media device and streaming it over the Internet stream.start();
现在您可以对着网络摄像头挥手,拿起麦克风并说:“Hello world!”
到目前为止,我们已经介绍了 Jitsi 处理协议、发送和接收消息以及拨打电话的部分内容。然而,最重要的是,Jitsi 是一个供实际人员使用的应用程序,因此,其最重要的方面之一是其用户界面。大多数情况下,用户界面使用 Jitsi 中所有其他捆绑包公开的服务。但是,在某些情况下,事情会反过来。
插件是我们想到的第一个例子。Jitsi 中的插件通常需要能够与用户交互。这意味着它们必须打开、关闭、移动或向用户界面中现有的窗口和面板添加组件。这就是我们的 UIService 发挥作用的地方。它允许对 Jitsi 中的主窗口进行基本控制,这就是我们在 Mac OS X Dock 和 Windows 通知区域中的图标如何让用户控制应用程序。
除了简单地使用联系人列表之外,插件还可以扩展它。在 Jitsi 中实现聊天加密 (OTR) 支持的插件就是一个很好的例子。我们的 OTR 捆绑包需要在用户界面的各个部分注册多个 GUI 组件。它在聊天窗口中添加了一个挂锁按钮,并在所有联系人的右键菜单中添加了一个子部分。
好消息是,它只需几个方法调用即可完成所有这些操作。OTR 捆绑包的 OSGi 激活器 OtrActivator 包含以下几行
Hashtable<String, String> filter = new Hashtable<String, String>(); // Register the right-click menu item. filter(Container.CONTAINER_ID, Container.CONTAINER_CONTACT_RIGHT_BUTTON_MENU.getID()); bundleContext.registerService(PluginComponent.class.getName(), new OtrMetaContactMenu(Container.CONTAINER_CONTACT_RIGHT_BUTTON_MENU), filter); // Register the chat window menu bar item. 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 包含以下几行
// Search for plugin components registered through the OSGI bundle context. ServiceReference[] serRefs = null; String osgiFilter = "(" + Container.CONTAINER_ID + "="+Container.CONTAINER_CONTACT_RIGHT_BUTTON_MENU.getID()+")"; serRefs = GuiActivator.bundleContext.getServiceReferences( PluginComponent.class.getName(), osgiFilter); // Go through all the plugins we found and add them to the menu. 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 很慢”的迷思甚至被潜在用户作为他们坚持使用 Skype 而不是尝试 Jitsi 的理由。但我们在项目中学习到的第一课是,与 C++ 或其他原生替代方案相比,Java 的效率并没有更令人担忧。
我们不会假装选择 Java 的决定是经过对所有可能选项的严格分析的结果。我们只是想要一种简单的方法来构建可在 Windows 和 Linux 上运行的东西,而 Java 和 Java Media Framework 似乎提供了一种相对简单的方法。
多年来,我们没有多少理由后悔这个决定。恰恰相反:尽管它并没有使其完全透明,但 Java 确实有助于可移植性,并且 SIP Communicator 中 90% 的代码在不同的操作系统之间没有变化。这包括所有协议栈实现(例如,SIP、XMPP、RTP 等),这些实现本身就足够复杂了。在代码的这些部分中不必担心操作系统的细节已被证明非常有用。
此外,Java 的流行在构建我们的社区时变得非常重要。贡献者本来就是稀缺资源。人们需要喜欢应用程序的特性,他们需要抽出时间和动力——所有这些都很难做到。因此,不需要他们学习一门新的语言是一个优势。
与大多数预期相反,Java 假设的运行速度不足很少成为转向原生语言的理由。大多数时候,使用原生语言的决定是由操作系统集成以及 Java 提供给我们的操作系统特定实用程序的访问权限驱动的。下面我们将讨论 Java 存在不足的三个最重要的方面。
Java Sound 是 Java 用于捕获和播放音频的默认 API。它是运行时环境的一部分,因此可以在 Java 虚拟机提供的任何平台上运行。在 SIP Communicator 的最初几年,Jitsi 专门使用 JavaSound,这给我们带来了不少不便。
首先,该 API 没有给我们选择使用哪个音频设备的选项。这是一个大问题。当使用他们的电脑进行音频和视频通话时,用户经常使用高级 USB 耳机或其他音频设备以获得最佳质量。当计算机上存在多个设备时,JavaSound 会通过操作系统认为是默认的设备路由所有音频,这在很多情况下都不够好。许多用户希望将所有其他应用程序保留在其默认声卡上,以便例如他们可以通过扬声器继续收听音乐。更重要的是,在许多情况下,SIP Communicator 最好将音频通知发送到一个设备,而将实际通话音频发送到另一个设备,允许用户即使不在电脑前也能通过扬声器听到来电提示,然后在接听电话后,开始使用耳机。
Java Sound 无法做到这一点。此外,Linux 实现使用 OSS,而 OSS 在大多数当今的 Linux 发行版中已弃用。
我们决定使用替代音频系统。我们不想牺牲我们的跨平台特性,如果可能的话,我们希望避免自己处理所有事情。这就是 PortAudio2 派上用场的地方。
当 Java 本身无法让你做某件事时,跨平台的开源项目是次佳选择。切换到 PortAudio 使我们能够实现对细粒度可配置音频渲染和捕获的支持,就像我们上面描述的那样。它还在 Windows、Linux、Mac OS X、FreeBSD 和其他我们还没有时间提供软件包的平台上运行。
视频对我们来说与音频一样重要。然而,Java 的创建者似乎并不这么认为,因为 JRE 中没有允许捕获或渲染视频的默认 API。有一段时间,Java Media Framework 似乎注定要成为这样一个 API,直到 Sun 停止维护它。
自然地,我们开始寻找类似 PortAudio 的视频替代方案,但这次我们没有那么幸运。起初,我们决定使用 Ken Larson3 的 LTI-CIVIL 框架。这是一个很棒的项目,我们使用它有一段时间了4。然而,当在实时通信环境中使用时,它被证明不是最佳选择。
因此,我们得出结论,为 Jitsi 提供完美的视频通信的唯一方法是我们自己实现原生抓取器和渲染器。这不是一个容易的决定,因为它意味着为项目增加了很多复杂性和大量的维护负担,但我们别无选择:我们真的希望能够进行高质量的视频通话。现在我们做到了!
我们的原生抓取器和渲染器分别在 Linux、Mac OS X 和 Windows 上直接使用 Video4Linux 2、QTKit 和 DirectShow/Direct3D。
SIP Communicator,以及 Jitsi,从一开始就支持视频通话。这是因为 Java Media Framework 允许使用 H.263 编解码器和 176x144(CIF)格式对视频进行编码。那些知道 H.263 CIF 长什么样的人现在可能正在微笑;如果视频聊天应用程序只有这些功能,我们中很少有人会在今天使用它。
为了提供良好的质量,我们不得不使用其他库,如 FFmpeg。视频编码实际上是 Java 在性能方面显示其限制的少数几个地方之一。其他语言也是如此,FFmpeg 开发人员实际上在许多地方使用汇编语言来以尽可能高效的方式处理视频这一事实证明了这一点。
还有许多其他地方,我们决定为了获得更好的结果而需要使用原生代码。Mac OS X 上使用 Growl 和 Linux 上使用 libnotify 的系统托盘通知就是一个例子。其他例子包括查询 Microsoft Outlook 和 Apple Address Book 中的联系人数据库、根据目标确定源 IP 地址、使用 Speex 和 G.722 的现有编解码器实现、捕获桌面屏幕截图以及将字符转换为键码。
重要的是,每当我们需要选择原生解决方案时,我们都可以,而且确实这么做了。这引出了我们的观点:自从我们开始 Jitsi 以来,我们已经修复、添加甚至完全重写了它的各个部分,因为我们希望它们看起来、感觉或运行得更好。但是,我们从未后悔我们第一次没有做对的事情。在有疑问的情况下,我们只需选择一个可用的选项并继续使用它。我们本可以等到我们更好地了解自己在做什么,但如果我们那样做了,今天就不会有 Jitsi 了。
非常感谢 Yana Stamcheva 为本章创建所有图表。
http://jitsi.org/source
下载它。如果您使用的是 Eclipse 或 NetBeans,您可以访问 http://jitsi.org/eclipse
或 http://jitsi.org/netbeans
以获取有关如何配置它们的说明。http://portaudio.com/
http://lti-civil.org/