承接前篇文字聊天與白板共享,本篇實現語音聊天功能。語音通訊涉及即時音訊串流、編解碼、NAT 穿透等挑戰,採用 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();
}
}
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();
}
[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;
}
}
}






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