WPF+WCF一步一步打造音频聊天室(3):语音聊天
前一篇文章中实现了文字聊天和共享白板的功能,这篇文章中,我将在前一篇文章的基础上实现语音聊天的功能。语音聊天要比文字聊天和共享白板难度要大一点。
实现的大概的流程为:
1、一个聊天室成员向另外一个成员发起语音聊天请求
2、这个请求将被送至WCF服务端,WCF的双工通知被邀请人。
3、被邀请人接到通知,他可以选择接受或者拒绝语音聊天的请求。
4、如果拒绝,将通知请求者拒绝语音聊天
5、如果同意,邀请者和被邀请者的客户端将进行语音聊天,此时客户端会开启一个播放声音和接受声音的线程。这里用到了一个开源的wave类库,在http://www.lumisoft.ee/lswww/download/downloads/Examples/可以下载。声音的通信使用到了UDPClient
类。这个类使用 UDP 与网络服务通讯。UDP 的优点是简单易用,并且能够同时向多个地址广播消息。UdpClient
类提供了一些简单的方法,用于在阻止同步模式下发送和接收无连接 UDP 数据报。因为 UDP 是无连接传输协议,所以不需要在发送和接收数据前建立远程主机连接。但您可以选择使用下面两种方法之一来建立默认远程主机:
使用远程主机名和端口号作为参数创建 UdpClient 类的实例。
创建 UdpClient 类的实例,然后调用 Connect 方法。
可以使用在UdpClient 中提供的任何一种发送方法将数据发送到远程设备。使用 Receive 方法可以从远程主机接收数据。
这篇文章使用了Receive 方法从客户端接受数据。然后通过WCF中存储的IP地址,通过Send方法将其发送给客户端。
下面我将在前一篇文章的基础上实现这个语音聊天的功能。首先在客户端添加声音管理的类CallManager,这个类使用到了开源的wave类库,代码如下:
public class
CallManager
{
private
WaveIn _waveIn;
private
WaveOut _waveOut;
private
IPEndPoint _serverEndPoint;
private
Thread _playSound;
private
UdpClient _socket;
public
CallManager(IPEndPoint serverEndpoint)
{
_serverEndPoint = serverEndpoint;
}
public
void Start()
{
if (_waveIn !=
null || _waveOut != null)
{
throw new
Exception("Call is allready started");
}
int waveInDevice =
(Int32)Application.UserAppDataRegistry.GetValue("WaveIn",
0);
int waveOutDevice =
(Int32)Application.UserAppDataRegistry.GetValue("WaveOut",
0);
_socket =
new UdpClient(0);
// opens a random available port on all interfaces
_waveIn =
new WaveIn(WaveIn.Devices[waveInDevice],
8000, 16, 1, 400);
_waveIn.BufferFull += new
BufferFullHandler(_waveIn_BufferFull);
_waveIn.Start();
_waveOut =
new WaveOut(WaveOut.Devices[waveOutDevice],
8000, 16, 1);
_playSound =
new Thread(new
ThreadStart(playSound));
_playSound.IsBackground = true;
_playSound.Start();
}
private
void playSound()
{
try
{
while (true)
{
lock (_socket)
{
if (_socket.Available != 0)
{
IPEndPoint endpoint = new
IPEndPoint(IPAddress.Any, 0);
byte[] received = _socket.Receive(ref
endpoint);
// todo: add codec
_waveOut.Play(received, 0, received.Length);
}
}
Thread.Sleep(1);
}
}
catch (ThreadAbortException)
{
}
catch
{
this.Stop();
}
}
void
_waveIn_BufferFull(byte[] buffer)
{
lock (_socket)
{
//todo: add codec
_socket.Send(buffer, buffer.Length, _serverEndPoint);
}
}
public
void Stop()
{
if (_waveIn !=
null)
{
_waveIn.Dispose();
}
if (_waveOut !=
null)
{
_waveOut.Dispose();
}
if (_playSound.IsAlive)
{
_playSound.Abort();
}
if (_socket !=
null)
{
_socket.Close();
_socket = null;
}
}
}
在服务端添加将接受到的声音,发送给接受者的类,使用到了UDPClient类:
public class
UdpServer
{
private
Thread _listenerThread;
private
List<IPEndPoint> _users = new
List<IPEndPoint>();
private
UdpClient _udpSender = new UdpClient();
public
IPAddress ServerAddress
{
get;
set;
}
public
UdpClient UdpListener
{
get;
set;
}
public
UdpServer()
{
try
{
ServerAddress = IPAddress.Parse("127.0.0.1");
}
catch
{
throw new
Exception("Configuration not set propperly. View
original source code");
}
}
public
void Start()
{
UdpListener =
new System.Net.Sockets.UdpClient(0);
_listenerThread = new Thread(new
ThreadStart(listen));
_listenerThread.IsBackground = true;
_listenerThread.Start();
}
private
void listen()
{
while (true)
{
IPEndPoint sender = new
IPEndPoint(IPAddress.Any, 0);
byte[] received = UdpListener.Receive(ref
sender);
if (!_users.Contains(sender))
{
_users.Add(sender);
}
foreach (IPEndPoint endpoint
in _users)
{
if (!endpoint.Equals(sender))
{
_udpSender.Send(received, received.Length, endpoint);
}
}
}
}
public
void EndCall()
{
_listenerThread.Abort();
}
}
在WCF服务中添加两个方法:初始化语音通信和结束语音通信。
[OperationContract(IsOneWay = false)]
bool
InitiateCall(string username);
[OperationContract(IsOneWay =
true)]
void
EndCall();
具体是实现代码:
public
bool InitiateCall(string
username)
{
ClientCallBack clientCaller = s_dictCallbackToUser[OperationContext.Current.GetCallbackChannel<IZqlChartServiceCallback>()];
ClientCallBack clientCalee = s_dictCallbackToUser.Values.Where(p
=> p.JoinChatUser.NickName == username).First();
if (clientCaller.Callee
!= null || clientCalee.Callee
!= null) //
callee or caller is in another call
{
return false;
}
if (clientCaller
== clientCalee)
{
return false;
}
if (clientCalee.Client.AcceptCall(clientCaller.JoinChatUser.NickName))
{
clientCaller.Callee = clientCalee.Client;
clientCalee.Callee = clientCaller.Client;
clientCaller.UdpCallServer = new
UdpServer();
clientCaller.UdpCallServer.Start();
EmtpyDelegate separateThread = delegate()
{
IPEndPoint endpoint = new
IPEndPoint(clientCaller.UdpCallServer.ServerAddress,
((IPEndPoint)clientCaller.UdpCallServer.UdpListener.Client.LocalEndPoint).Port);
clientCalee.Client.CallDetailes(endpoint, clientCaller.JoinChatUser.NickName,
username);
clientCaller.Client.CallDetailes(endpoint, clientCaller.JoinChatUser.NickName,
username);
foreach (var callback
in s_dictCallbackToUser.Keys)
{
callback.NotifyMessage(String.Format
("System:User \"{0}\"
and user \"{1}\" have started a call",clientCaller.JoinChatUser.NickName,
username));
}
};
separateThread.BeginInvoke(null,
null);
return true;
}
else
{
return false;
}
}
public
void EndCall()
{
ClientCallBack clientCaller = s_dictCallbackToUser[OperationContext.Current.
GetCallbackChannel<IZqlChartServiceCallback>()];
ClientCallBack ClientCalee = s_dictCallbackToUser[clientCaller.Callee];
if (clientCaller.UdpCallServer
!= null)
{
clientCaller.UdpCallServer.EndCall();
}
if (ClientCalee.UdpCallServer
!= null)
{
ClientCalee.UdpCallServer.EndCall();
}
if (clientCaller.Callee
!= null)
{
foreach (var callback
in s_dictCallbackToUser.Keys)
{
callback.NotifyMessage(String.Format
("System:User \"{0}\"
and user \"{1}\" have ended the call",
clientCaller.JoinChatUser.NickName,
ClientCalee.JoinChatUser.NickName));
}
clientCaller.Callee.EndCallClient();
clientCaller.Callee = null;
}
if (ClientCalee.Callee
!= null)
{
ClientCalee.Callee.EndCallClient();
ClientCalee.Callee = null;
}
}
下面看下演示的截图:
1、两个用户登录:
2、选择小花,点击按钮。麒麟向小花同学发起语音聊天:
3、小花同学接受通知,选择是:
4、弹出通话中的窗体,双发都可以选择结束通话:
5、结束通话之后,消息框中会广告消息
总结: 这个聊天程序主要都是用到了WCF的双工通信。没有用两台机子测试,我在我的笔记本上开了一个服务端和一个客户端,用了一个带耳麦的耳机,声音效果良好。其实这个东西做完整还有很多细活,这个只是Demo,非常的粗糙。最近会很忙,期待将来的某一天能空去完善。
WPF+WCF一步一步打造音频聊天室(4):视频会话
前面文章中,我实现了音频聊天室的部分功能,包括:文字聊天,共享白板,语音聊天。这篇文章我将叙述一下视频会话实现的技术要点。
在Silerlight4中已经集成了摄像和采集声音的功能,但是在WPF4中却没有直接可以用的的控件,由此也可以看出,由桌面程序走向web程序的大趋势。如果你想用Silverlight实现类似的音频聊天室,下面我列出一些资料供你参考。
1、 Your First Step to the Silverlight Voice/Video
Chatting Client/Server
2、Accessing Web Camera and Microphone
3、Record The Audio Into A Wave File
4、Playback The Wave File in Silverlight
5、Using the G.711 Codec
6、convert encode and decode silverlight
上面是Silverlight实现的方案和资料。这篇文章是用WPF+WCF去实现的。列出Silerlight是方便大家有个对照。
视频会话实现的方式和语音通话实现的方式是一样的。他们之间不一样的地方在于,一个是通过麦克风获取数据,一个是通过摄像头获取数据。下面我用WF4画了一个流程图(这个流程图只是为了说明问题,没有用到程序里面)。
实现
前面说到了,WPF中没有像Silerlight一样集成了摄像的功能,在WPF中又如何去实现摄像呢?这也是首先要解决的问题,我经过一番google,在Codeplex上找到了一个开源WPF的Webcam控件。地址是:WebCam
control for WPF。
添加一个窗体,在这个窗体上使用这个控件,布局如下。
注意:左边是本机的视频,右边是对方的视频。修改窗体的构造函数;
与语音聊天一样,数据传递我使用了UdpClient,我感觉UdpClient简单好用。_serverEndPoint是WCF服务的地址,_socket用于视频数据传递。在客户端我使用了两个System.Windows.Threading.DispatcherTimer,本来打算直接使用两个线程,发现一些莫名奇妙的线程问题。两个DispatcherTimer,一个用来启动接受来自WCF服务的视频数据,一个用来将自己的视频数据发送到WCF服务。代码如下:
在前面文章的的基础上完成这些操作,我们就可以实现视频会话的功能。
效果:
1、选择跟小花视频:
2、小花接受到请求:
3、视频中:
上图是我在一台电脑上演示的,所以只有一边显示数据。但是,我用两台笔记本测试过,效果也还不错。
总结:
主要用到的技术有;WCF、WPF、UDPClient。还使用了一个开源的控件WebCam control
for WPF。这个程序调试了我一天的时间。
|