求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
  
 
 
 
WPF+WCF一步一步打造音频聊天室(二)
 

2011-4-18 来源:网络

 

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。

添加一个窗体,在这个窗体上使用这个控件,布局如下。

注意:左边是本机的视频,右边是对方的视频。修改窗体的构造函数;

private IPEndPoint _serverEndPoint;
        private UdpClient _socket;
        public WebcamPlayerForm(IPEndPoint serverEndpoint, string caller, string callee)
        {
        }

与语音聊天一样,数据传递我使用了UdpClient,我感觉UdpClient简单好用。_serverEndPoint是WCF服务的地址,_socket用于视频数据传递。在客户端我使用了两个System.Windows.Threading.DispatcherTimer,本来打算直接使用两个线程,发现一些莫名奇妙的线程问题。两个DispatcherTimer,一个用来启动接受来自WCF服务的视频数据,一个用来将自己的视频数据发送到WCF服务。代码如下:

System.Windows.Threading.DispatcherTimer myDispatcherTimer = new System.Windows.Threading.DispatcherTimer();
            myDispatcherTimer.Interval = new TimeSpan(0, 0, 0, 0, 1000);
            myDispatcherTimer.Tick += new EventHandler(captureVideo);
            myDispatcherTimer.Start();

            System.Windows.Threading.DispatcherTimer myDispatcherTimer2 = new System.Windows.Threading.DispatcherTimer();
            myDispatcherTimer2.Interval = new TimeSpan(0, 0, 0, 0, 1000);
            myDispatcherTimer2.Tick += new EventHandler(playVideo);
            myDispatcherTimer2.Start();
   captureVideo用于从摄像头捕获数据通过UdpClient发送到WCF服务中,代码如下。
        private void captureVideo(object sender, EventArgs e)
        {
            try
            {
                    byte[] bytes;
                    if (webcamPlayer.CurrentBitmap != null)
                    {
                        bytes = ConvertImageSourceToByteArray(webcamPlayer.CurrentBitmap);//webcamPlayer.CurrentBitmap
                        _socket.Send(bytes, bytes.Length, _serverEndPoint);
                    }
       
            }
            catch (Exception) { }
        }

上面的captureVideo方法将视频数据先转发到WCF服务,在由WCF服务转发给对方,在WCF服务中有一个UdpClient接受数据,方法是listen(),它的代码如下:

        private void listen()
        {
            try
            {
                while (true)
                {
                    
                    IPEndPoint sender = new IPEndPoint(IPAddress.Any, 0);
                    //if (sender.ToString() != "0.0.0.0: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);
                            }
                        }
                    //}
                }
            }
            catch (Exception e)
            {
                     
           }
            }

WCF服务中的listen()方法将接收到的数据发送给对方的客户端,在客户端有playVideo方法来接收和并播放来自WCF的视频数据,代码如下:

        private void playVideo(object sender, EventArgs e)
        {
       
            try
            {
                    lock (_socket)
                    {
                        if (_socket.Available != 0)
                        {
                            IPEndPoint endpoint = new IPEndPoint(IPAddress.Any, 0);
                            byte[] received = _socket.Receive(ref endpoint);
                            image1.Source = ConvertByteArrayToImageSource(received);

                        }
                }
            }
            catch
            {
               
            }
        }
  

由于从摄像头获取的数据格式是ImageSource,我们需要将它转换成byte[]传输,转换的代码如下

        /// <summary>
        /// Converts an <see cref="ImageSource"/> to an array of bytes.
        /// </summary>
        /// <param name="image"><see cref="ImageSource"/> to convert.</param>
        /// <returns>Array of bytes.</returns>
        public  byte[] ConvertImageSourceToByteArray( ImageSource image)
        {
            // Declare variables
            byte[] result = null;

            // Use a memory stream to convert
            using (MemoryStream memoryStream = new MemoryStream())
            {
                // Get right encoder
                JpegBitmapEncoder encoder = new JpegBitmapEncoder();

                // Get right frame
                if (image is BitmapSource)
                {
                    encoder.Frames.Add(BitmapFrame.Create((BitmapSource)image));
                }

                // Now set some encoder values
                encoder.QualityLevel = 100;
                encoder.Save(memoryStream);

                // Now convert
                result = memoryStream.ToArray();
            }

            // Return result
            return result;
        }

    从WCF服务收到到的数据是byte[] 格式,我们需要将其转换成ImageSource,代码如下:

        /// <summary>
        /// Converts an array of bytes to a <see cref="ImageSource"/>.
        /// </summary>
        /// <param name="bytes">Bytes to convert.</param>
        /// <returns><see cref="ImageSource"/>.</returns>
        public ImageSource ConvertByteArrayToImageSource(byte[] bytes)
        {
            // Declare variables
            ImageSource result = null;

            // Validate input
            if (bytes.Length == 0) return null;

            // Create memory stream - it seems that if you clean up or dispose 
            // the memory stream, you cannot display the image any longer
            MemoryStream memoryStream = new MemoryStream(bytes);

            // Assign to bitmap image
            BitmapImage bitmapImage = new BitmapImage();
            bitmapImage.BeginInit();
            bitmapImage.StreamSource = memoryStream;
            bitmapImage.EndInit();

            // Assign bitmap to image source
            result = bitmapImage;

            // Return result
            return result;
        }

在前面文章的的基础上完成这些操作,我们就可以实现视频会话的功能。

效果:

1、选择跟小花视频:

2、小花接受到请求:

3、视频中:

上图是我在一台电脑上演示的,所以只有一边显示数据。但是,我用两台笔记本测试过,效果也还不错。

总结:

主要用到的技术有;WCF、WPF、UDPClient。还使用了一个开源的控件WebCam control for WPF。这个程序调试了我一天的时间。



使用decj简化Web前端开发
Web开发框架形成之旅
更有效率的使用Visual Studio
MVP+WCF+三层结构搭建框架
ASP.NET运行机制浅析【图解】
编写更好的C#代码
10个Visual Studio开发调试技巧
更多...   


.NET框架与分布式应用架构设计
.NET & WPF & WCF应用开发
UML&.Net架构设计
COM组件开发
.Net应用开发
InstallShield


日照港 .NET Framework & WCF应用开发
神华信息 .NET单元测试
北京 .Net应用软件系统架构
台达电子 .NET程序设计与开发
赛门铁克 C#与.NET架构设计
广东核电 .Net应用系统架构
更多...