前一篇文章中实现了文字聊天和共享白板的功能,这篇文章中,我将在前一篇文章的基础上实现语音聊天的功能。语音聊天要比文字聊天和共享白板难度要大一点。
实现的大概的流程为:
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,非常的粗糙。最近会很忙,期待将来的某一天能空去完善。如果实现了视频的功能我会写第四篇的。
本文转自麒麟博客园博客,原文链接:http://www.cnblogs.com/zhuqil/archive/2010/06/08/ZqlChart-3.html,如需转载请自行联系原作者