首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

WPF + WCF 打造音訊聊天室(三):語音聊天核心實作

     編輯:關於.NET

承接前篇文字聊天與白板共享,本篇實現語音聊天功能。語音通訊涉及即時音訊串流、編解碼、NAT 穿透等挑戰,採用 UDP 協定降低延遲。

整體流程

  1. 發起者向 WCF 服務發起語音邀請;
  2. 服務透過雙工回調通知被邀請者;
  3. 被邀請者接受/拒絕;
  4. 雙方建立 UDP 連線,啟動錄音/播放執行緒進行即時語音傳輸。

客戶端音訊管理類:CallManager

封裝開源 Wave 庫(LumiSoft)實現錄音與播放:


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 already started");
        
        int waveInDevice = (int)Application.UserAppDataRegistry.GetValue("WaveIn", 0);
        int waveOutDevice = (int)Application.UserAppDataRegistry.GetValue("WaveOut", 0);
        
        _socket = new UdpClient(0);
        _waveIn = new WaveIn(WaveIn.Devices[waveInDevice], 8000, 16, 1, 400);
        _waveIn.BufferFull += (buffer) => 
        {
            lock (_socket) _socket.Send(buffer, buffer.Length, _serverEndPoint);
        };
        _waveIn.Start();
        
        _waveOut = new WaveOut(WaveOut.Devices[waveOutDevice], 8000, 16, 1);
        _playSound = new Thread(PlaySound) { IsBackground = true };
        _playSound.Start();
    }

    private void PlaySound()
    {
        try
        {
            while (true)
            {
                lock (_socket)
                {
                    if (_socket.Available > 0)
                    {
                        IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0);
                        byte[] received = _socket.Receive(ref ep);
                        _waveOut.Play(received, 0, received.Length);
                    }
                }
                Thread.Sleep(1);
            }
        }
        catch (ThreadAbortException) { }
        catch { Stop(); }
    }

    public void Stop()
    {
        _waveIn?.Dispose();
        _waveOut?.Dispose();
        if (_playSound?.IsAlive == true) _playSound.Abort();
        _socket?.Close();
    }
}

服務端 UDP 轉發器:UdpServer


public class UdpServer
{
    private Thread _listenerThread;
    private List<IPEndPoint> _users = new List<IPEndPoint>();
    private UdpClient _udpSender = new UdpClient();
    public UdpClient UdpListener { get; set; }
    public IPAddress ServerAddress { get; set; } = IPAddress.Parse("127.0.0.1");

    public void Start()
    {
        UdpListener = new UdpClient(0);
        _listenerThread = new Thread(Listen) { 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 (var ep in _users.Where(e => !e.Equals(sender)))
                _udpSender.Send(received, received.Length, ep);
        }
    }

    public void EndCall() => _listenerThread?.Abort();
}

WCF 服務合約與實作


[ServiceContract(CallbackContract = typeof(IZqlChartServiceCallback))]
public interface IZqlChartService
{
    [OperationContract] bool InitiateCall(string username);
    [OperationContract(IsOneWay = true)] void EndCall();
    // ... 其他文字/白板合約
}

public class ZqlChartService : IZqlChartService
{
    public static Dictionary<IZqlChartServiceCallback, ClientCallBack> s_dictCallbackToUser = ...;

    public bool InitiateCall(string username)
    {
        var caller = s_dictCallbackToUser[OperationContext.Current.GetCallbackChannel<IZqlChartServiceCallback>()];
        var callee = s_dictCallbackToUser.Values.First(p => p.JoinChatUser.NickName == username);
        
        if (caller.Callee != null || callee.Callee != null || caller == callee) return false;
        
        if (callee.Client.AcceptCall(caller.JoinChatUser.NickName))
        {
            caller.Callee = callee.Client;
            callee.Callee = caller.Client;
            
            caller.UdpCallServer = new UdpServer();
            caller.UdpCallServer.Start();
            
            // 非同步通知雙方 UDP 端點資訊
            Task.Run(() => 
            {
                var ep = new IPEndPoint(caller.UdpCallServer.ServerAddress, 
                    ((IPEndPoint)caller.UdpCallServer.UdpListener.Client.LocalEndPoint).Port);
                callee.Client.CallDetailes(ep, caller.JoinChatUser.NickName, username);
                caller.Client.CallDetailes(ep, caller.JoinChatUser.NickName, username);
                BroadcastSystemMessage($"{caller.JoinChatUser.NickName} 與 {username} 開始通話");
            });
            return true;
        }
        return false;
    }

    public void EndCall()
    {
        var caller = s_dictCallbackToUser[OperationContext.Current.GetCallbackChannel<IZqlChartServiceCallback>()];
        var callee = s_dictCallbackToUser[caller.Callee];
        
        caller.UdpCallServer?.EndCall();
        callee.UdpCallServer?.EndCall();
        
        if (caller.Callee != null)
        {
            BroadcastSystemMessage($"{caller.JoinChatUser.NickName} 與 {callee.JoinChatUser.NickName} 結束通話");
            caller.Callee.EndCallClient();
            caller.Callee = null;
        }
        if (callee.Callee != null)
        {
            callee.Callee.EndCallClient();
            callee.Callee = null;
        }
    }
}

執行畫面

  1. 雙用戶登入:
  2. 發起語音邀請:
  3. 接收邀請確認:
  4. 通話中視窗:
  5. 結束通話廣播:

單機測試使用單一耳麥,語音品質良好。程式碼下載:Zql_src.rar。下一篇將實現視訊會話功能。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved