如何构建一个不会被服务提供商阻止的Windows Phone 7 VoIP应用程序
它描述了如何开发一个 WP7 VoIP 客户端应用程序,该应用程序通过 TCP/UDP 协议与服务器通信
介绍
VoIP 技术是当今增长最快的技术之一。它用于通过互联网进行高质量语音通信,并且由于其广泛的应用,许多设备都支持它。然而,许多移动服务提供商会阻止使用 SIP 协议所用的 5060 端口。(SIP 协议用于 VoIP 通信)。这意味着许多移动服务提供商会阻止在您的手机上使用 VoIP。
我找到了一个解决这个问题的方法。它是一个 WP7 客户端应用程序,通过 TCP/UDP 协议与服务器通信,而服务器通过 SIP 协议与 PBX 通信。这样,移动服务提供商就不会阻止 VoIP 通信,因为他们只会看到客户端和服务器之间的 TCP/UDP 通信。
背景
我使用了 Windows Phone SDK 7.1 以及 Ozeki 提供的 VoIP SIP SDK,它提供了出色且易于管理的 VoIP 通信工具,以及 Microsoft Visual Studio 2010。对于 Visual Studio,至少需要 .NET Framework 3.5 SP1 或更高版本。我使用 C# 编程语言编写了此应用程序。
开始构建客户端
首先,我在 Visual Studio 中创建了一个新的 Windows Phone 应用程序项目,并将 WPClientSDK.dll 作为我的项目的引用,因为我的应用程序需要它。要获取此 .dll,我必须下载 Ozeki VoIP SIP SDK,因为它是其中的一部分。
在 Microsoft Visual Studio 中创建新的 Windows Phone 应用程序项目后,我开始编辑应用程序的图形界面,可以在客户端项目的 MainPage.xaml 文件中进行操作。您可以在下面看到我创建的界面的代码。
<!--LayoutRoot is the root grid where all page content is placed-->
<Grid x:Name="LayoutRoot" Background="Transparent">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!--TitlePanel contains the name of the application and page title-->
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
<TextBlock x:Name="ApplicationTitle" Text="Ozeki VoIP SIP SDK" Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBlock x:Name="PageTitle" Text="Mobile2Web" Margin="9,-7,0,0" Style="{StaticResource PhoneTextTitle1Style}"/>
</StackPanel>
<!--ContentPanel - place additional content here-->
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<Button Content="Call" Height="70" x:Name="btnCall" VerticalAlignment="Top" Click="btnCall_Click" Margin="57,156,223,0" IsEnabled="False" />
<TextBlock x:Name="txtboxInfo" TextWrapping="Wrap" Text="Offline" TextAlignment="Center" VerticalAlignment="Top" FontSize="24" Margin="0,86,0,0"/>
<Button Content="Stop Call" Height="70" Margin="215,156,66,0" Name="btnStopCall" VerticalAlignment="Top" Click="btnStopCall_Click" IsEnabled="False" />
<TextBlock Height="23" HorizontalAlignment="Center" Margin="0,27,0,0" Name="txtBDebug" Text="DebugLine" VerticalAlignment="Top" Visibility="Collapsed" />
<TextBlock Height="23" HorizontalAlignment="Right" Margin="0,27,18,0" Name="txtBClientID" Text="ClientID" VerticalAlignment="Top" FontSize="21.333" />
<TextBox x:Name="txtLog" Height="348" HorizontalAlignment="Left" Margin="0,253,0,0" Text="" VerticalAlignment="Top" Width="450" IsEnabled="False" TextWrapping="Wrap" VerticalScrollBarVisibility="Visible" />
</Grid>
</Grid>
代码示例 1: 图形用户界面的代码
图 1:图形用户界面
如您所见,该应用程序具有语音通信所需的按钮,例如:“呼叫”和“停止呼叫”按钮。您还可以在文本框中看到有关连接和呼叫的一些基本信息。
为了使客户端应用程序按我期望的方式工作,我不得不使用一些 Windows Phone 7 和 VoIP 支持包。我在客户端的 MainPage.xaml.cs 文件中将它们设置为预编译包,因此我可以在没有命名空间标签的情况下使用它们。您可以在下面看到它们。using System;
using System.Diagnostics;
using System.Windows;
using Microsoft.Phone.Controls;
using Ozeki.MediaGateway;
using WPClientSDK;
using WPClientSDK.Log;
代码示例 2:必要的包
我不得不使用一些基本的 VoIP 通信工具,即流媒体处理程序、音频播放器和麦克风处理程序类。您可以在下面的代码中看到它们。
public partial class MainPage : PhoneApplicationPage
{
private MediaConnection connection;
private MediaStreamSender streamSender;
private MediaStreamReceiver streamReceiver;
private AudioPlayer audioPlayer;
private Microphone microphone;
private string clientID;
private string IncomingCallOwner;
private bool callProcess;
代码示例 3:VIP 通信工具
当客户端应用程序启动时,第一个调用的事件是 MainPage_Loaded 事件。我在 MainPage_Loaded 事件的处理程序中设置了媒体连接和麦克风设置。您可以在 MediaConnection 事件中看到我的专用 IP 地址。如果您要为自己构建此应用程序,则必须更改为您的地址。
void MainPage_Loaded(object sender, RoutedEventArgs e)
{
Logger.Instance.LogMessageReceived += new EventHandler<GenericEventArgs<string>>(Instance_LogMessageReceived);
connection = new MediaConnection("192.168.115.181:6888");
connection.ConnectionStateChanged += new EventHandler<GenericEventArgs<ConnectionState>>(connection_ConnectionStateChanged);
connection.Client = this;
connection.Connect();
microphone = Microphone.GetMicrophone();
}
代码示例 4:连接到服务器并访问麦克风
必须初始化电话应用程序,这在下面的代码片段中完成。
public void OnSetReadyStatus(bool isReady, string name)
{
InvokeGUIThread(()=>
{
clientID = name;
btnStopCall.IsEnabled = isReady;
btnCall.IsEnabled = isReady;
txtBClientID.Text = name;
txtboxInfo.Text = isReady ? "Ready to call." : "Waiting for other client.";
});
}
代码示例 5:客户端初始化
现在应用程序已准备好接受和拨打电话,让我们看看负责这些的代码。
private void btnCall_Click(object sender, RoutedEventArgs e)
{
if (Microphone.GetPermissionToMicrophone())
{
ReleaseStreams();
callProcess = true;
if (!string.IsNullOrEmpty(IncomingCallOwner))
{
connection.InvokeOnConnection("ChangeToIncall");
IncomingCallOwner = "";
btnCall.IsEnabled = false;
}
else
{
connection.InvokeOnConnection("Call", clientID);
txtboxInfo.Text = "Outgoing call progress.";
btnCall.IsEnabled = false;
}
}
else
{
txtboxInfo.Text = "Please, add permission to access microphone.";
}
}
代码示例 6:接受或开始呼叫的按钮
正如您所见,此代码首先检查麦克风是否可用,如果可用,则可以接受来电或开始去电。否则,它会告诉您提供麦克风访问权限。
当您通话时,自然需要能够挂断电话,无论您是被呼叫还是主动呼叫。所以您可以在下面看到挂断电话的代码。
private void btnStopCall_Click(object sender, RoutedEventArgs e)
{
if (callProcess)
{
txtboxInfo.Text = "Call stop, ready to call.";
connection.InvokeOnConnection("CallStop");
ReleaseStreams();
btnCall.IsEnabled = true;
}
}
代码示例 7:停止呼叫的按钮
该应用程序会显示您何时接到呼叫,并显示来电者是谁。我已经通过以下代码实现了这一点。
public void OnCallRequest(string remotpartyId)
{
callProcess = true;
IncomingCallOwner = remotpartyId;
InvokeGUIThread(() => { txtboxInfo.Text = "Call received from " + remotpartyId; });
}
代码示例 8:接收呼叫的方法
图 2:从 client0 接收到呼叫
当呼叫建立后,下面的方法,称为 OnInCall,将在客户端应用程序上调用。它设置呼叫的媒体发送者并将麦克风设置为语音捕获设备。
public void OnInCall()
{
InvokeGUIThread(()=>txtboxInfo.Text = "Incall");
streamSender = new MediaStreamSender(connection);
try
{
streamSender.AttachMicrophone(microphone);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
streamSender.StreamStateChanged += new EventHandler<GenericEventArgs<StreamState>>(streamSender_StreamStateChanged);
streamSender.Publish(clientID);
}
代码示例 9:将客户端状态更改为 Incall
图 3:接听呼叫后处于 Incall 状态的客户端
在 VoIP 通信中,媒体在各方之间流式传输,为此我实现了一个流接收器和一个播放器对象。您可以在下面看到它们的代码。
public void OnPlayRemoteStream(string remoteparty)
{
streamReceiver = new MediaStreamReceiver(connection);
streamReceiver.StreamStateChanged += new EventHandler<GenericEventArgs<StreamState>>(streamReceiver_StreamStateChanged);
audioPlayer.AttachMediaStreamReceiver(streamReceiver);
streamReceiver.Play(remoteparty);
}
代码示例 10:播放客户端之间的音频
流接收器和发送器对象需要调试功能,我通过订阅 StreamStateChanged 事件和两个事件处理方法来实现这一点。
void streamReceiver_StreamStateChanged(object sender, GenericEventArgs<StreamState> e)
{
Debug.WriteLine("receiver play {0}", e.Item);
InvokeGUIThread(() => { txtBDebug.Text = e.Item.ToString(); });
}
void streamSender_StreamStateChanged(object sender, GenericEventArgs<StreamState> e)
{
InvokeGUIThread(() => { txtBDebug.Text = e.Item.ToString(); });
switch (e.Item)
{
case StreamState.PublishingSuccess:
connection.InvokeOnConnection("PlayRemoteStream");
break;
default:
Debug.WriteLine(e.Item);
break;
}
}
代码示例 11:调试流接收器和发送器对象
当呼叫停止时,媒体流必须被释放,GUI 将恢复到新手机的状态。这意味着它将准备好拨打电话,或者在收到将来电接听。
public void OnCallStop()
{
InvokeGUIThread(()=>
{
txtboxInfo.Text = "Remoteparty finished the call.";
btnCall.IsEnabled = true;
} );
ReleaseStreams();
}
代码示例 12:呼叫停止后允许拨打或接听新电话的方法
图 4:另一客户端挂断呼叫(左)和此客户端挂断呼叫(右)
ReleaseStreams() 方法在呼叫开始或停止时释放 MediaStreamSender 或 MediaStreamReceiver 对象,因为在这两种情况下都必须停止流式传输的媒体。在开始呼叫时,ReleaseStreams() 用作验证,以确保从给定客户端没有媒体正在流式传输。
private void ReleaseStreams()
{
if (streamSender != null)
{
streamSender.StreamStateChanged -= streamSender_StreamStateChanged;
streamSender.Close();
}
if (streamReceiver != null)
{
streamReceiver.StreamStateChanged -= streamReceiver_StreamStateChanged;
streamReceiver.Close();
}
}
代码示例 13:释放流的方法
构建服务器
应用程序的服务器端是一个可以处理多种客户端类型的控制台应用程序。您可以在 App.config 文件中设置要与服务器一起使用的客户端类型。在此示例中,我使用 windowsphone,因此下面的代码不言自明。
<add type="windowsphone" serviceName="Web2WebServer" listenedPort="6888" policyServiceEnabled="true"/>
代码示例 14:客户端类型定义
服务器基本上是一个 Ozeki MediaGateway 类,您可以看到它的定义以及必要的属性。
class Mobile2WebGateway : MediaGateway
{
private Dictionary<IClient,MyClient> Clients;
private int clientCounter;
private int busyClients;
代码示例 15:将 Mobile2WebGateway 定义为 MediaGateway 类
当您启动服务器应用程序时,它会调用 Mobile2WebGateway() 和 OnStart() 方法,这些方法按如下代码所示工作。
public Mobile2WebGateway()
{
Console.WriteLine("Mobile2Web Gateway starting...");
}
public override void OnStart()
{
Clients = new Dictionary<IClient, MyClient>();
Console.WriteLine("Mobile2Web Gateway started.");
}
代码示例 16:启动服务器
图 5:启动后的服务器控制台
服务器通过 OnClientConnect() 方法处理客户端连接,该方法将连接到服务器的客户端的 IP 地址写入控制台,并通知其他客户端新的已连接客户端。
public override void OnClientConnect(IClient client, object[] parameters)
{
Console.WriteLine( "{0} client connected to the server.",client.RemoteAddress);
if (!Clients.ContainsKey(client))
{
Clients.Add(client, new MyClient(client, string.Format("client{0}", clientCounter++)));
NotifyClientsAboutTheirCallStatus();
}
}
代码示例 17:客户端连接
图 6:第一个客户端已连接
服务器还通过 OnClientDisconnect() 方法处理客户端断开连接,该方法的工作方式与连接方法相同。它将已断开连接客户端的 IP 地址写入控制台,并通知其他客户端已断开连接的客户端。
public override void OnClientDisconnect(IClient client)
{
if (Clients.ContainsKey(client))
{
MyClient disconnectedClient = Clients[client];
if (disconnectedClient.IsBusy && disconnectedClient.RemoteParty!=null)
disconnectedClient.RemoteParty.OnCallStop();
Console.WriteLine("{0}, {1} disconnected from the server.", client.RemoteAddress,disconnectedClient.Name);
Clients.Remove(client);
NotifyClientsAboutTheirCallStatus();
return;
}
Console.WriteLine("{0} client disconnected from the server.", client.RemoteAddress);
}
代码示例 18:客户端断开连接
图 7:第一个客户端已断开连接
当两个客户端都连接到服务器后,它们就可以互相通话了。服务器可以通过 call() 方法处理呼叫,该方法在两个客户端之间工作,这两个客户端将被设置为对方。
public void Call(IClient invokerClient, string requestOwner)
{
foreach (KeyValuePair<IClient, MyClient> keyValuePair in Clients)
{
//Searchs the first not busy connected client and sets theirs remote party.
if (keyValuePair.Key != invokerClient && !keyValuePair.Value.IsBusy)
{
MyClient invoker = Clients[invokerClient];
MyClient callee = keyValuePair.Value;
invoker.RemoteParty = callee;
callee.RemoteParty = invoker;
callee.OnCallRequest(requestOwner);
return;
}
}
}
代码示例 19:呼叫方法
如果两个客户端之间的呼叫已建立,我们就必须将它们的状态设置为 InCall,并通知其他客户端它们的状态。
public void ChangeToIncall(IClient client)
{
Clients[client].OnInCall();
foreach (MyClient c in Clients.Values)
{
NotifyClientsAboutTheirCallStatus();
}
}
代码示例 20:将客户端状态更改为 Incall
在呼叫建立后,客户端开始发布它们的流,服务器必须处理它们。它通过以下方法完成。
public override void OnStreamPublishStart(IClient client, IMediaStream mediaStream)
{
Console.WriteLine("client : {0} publish his stream : {1}",client.RemoteAddress,mediaStream.Name);
base.OnStreamPublishStart(client, mediaStream);
}
代码示例 21:客户端已开始发布它们的流
服务器必须开始播放客户端的对方的媒体流,这样它们才能互相听到。它通过以下代码完成。
public void PlayRemoteStream(IClient client)
{
//foreach (MyClient c in Clients.Values)
{
Clients[client].OnPlayRemoteStream();
}
}
代码示例 22:服务器为客户端播放流
图 8: 建立呼叫后,服务器为客户端播放它们的流
当客户端停止呼叫时,服务器必须停止它们的媒体流,并将其状态从 InCall 再次更改为可用。它通过调用它们的 OnCallStop() 方法来完成,您可以在下面的代码中看到。
public void CallStop(IClient invokerClient)
{
if (Clients.ContainsKey(invokerClient))
{
MyClient invoker = Clients[invokerClient];
invoker.RemoteParty.OnCallStop();
}
}
代码示例 23:服务器通过调用客户端的 OnCallStop 方法来停止呼叫
您可以看到,每当客户端开始呼叫或接到呼入呼叫时,其状态都会更改,并且所有其他客户端都会收到有关此更改的通知。您可以在下面看到负责此方法的代码。
private void NotifyClientsAboutTheirCallStatus()
{
busyClients = 0;
foreach (MyClient c in Clients.Values)
{
if (c.IsBusy)
busyClients++;
}
bool isReady = Clients.Count > 1 && Clients.Count-busyClients > 1;
lock (Clients)
{
foreach (KeyValuePair<IClient, MyClient> keyValuePair in Clients)
{
if (!keyValuePair.Value.IsBusy)
keyValuePair.Value.OnSetReadyStatus(isReady, keyValuePair.Value.Name);
}
}
}
代码示例 24:通知客户端客户端状态更改的方法
在 MyClient.cs 文件中,服务器包含一些写入控制台的方法,当服务器运行时,它们会调用实际的、适当的客户端方法。您可以在下面看到它们。
public void OnStartPlay(string remotpartyId)
{
Client.InvokeMethod("OnPlay", remotpartyId);
}
public void OnSetReadyStatus(bool isReady, string name)
{
try
{
Client.InvokeMethod("OnSetReadyStatus", isReady, name);
}
catch (Exception)
{}
}
public void OnCallRequest(string requestOwner)
{
Console.WriteLine("Call request received from {0} to {1}",requestOwner, Name);
RemoteParty.IsBusy = true;
IsBusy = true;
Client.InvokeMethod("OnCallRequest", requestOwner);
}
public void OnInCall()
{
Console.WriteLine("Sends 'start publishing' sign to the clients.");
Client.InvokeMethod("OnInCall");
RemoteParty.Client.InvokeMethod("OnInCall");
}
public void OnPlayRemoteStream()
{
Console.WriteLine("PlayRemoteStream - client Name : {0} starts to play remoteStream: {1}", RemoteParty.Name, Name);
RemoteParty.Client.InvokeMethod("OnPlayRemoteStream", Name);
}
public void OnCallStop()
{
IsBusy = false;
RemoteParty.IsBusy = false;
Client.InvokeMethod("OnCallStop");
}
代码示例 25:MyClient 类的这些方法
摘要
总而言之,我创建了一个 Windows Phone 7 应用程序,它允许通过 TCP/UDP 通信在所有移动服务提供商上使用 Windows 移动电话进行 VoIP 通信。为此,我在 Microsoft Visual Studio 中使用 Windows Phone SDK 7.1 和 Ozeki VoIP SIP SDK 创建了一个服务器和客户端应用程序。我选择 Ozeki VoIP SIP SDK 是因为我需要的该应用程序的一切都已在其中实现。我只需要使用它提供的适当方法和类,因此我推荐它给任何想要开发 VoIP 应用程序并且不希望从头开始创建一切的人。如果您想专注于开发您的应用程序,而不是网络协议和技术细节,这是一个很棒的工具。
参考
- 您可以在此网站下载 Microsoft Visual Studio 的免费试用版:http://msdn.microsoft.com/en-us/vstudio/default.aspx
- 您可以在以下页面下载必需的 .Net Framework 3.5: http://www.microsoft.com/en-us/download/details.aspx?id=21
- Windows Phone SDK 7.1 可在此处下载:http://www.microsoft.com/en-us/download/details.aspx?id=27570
- 最后,您可以在此处获取 Ozeki VoIP SIP SDK:http://www.voip-sip-sdk.com/