局域网音频实时传输、屏幕单播及广播
程序源代码及可执行程序链接: https://gitee.com/azhezhezhezhe/DesktopSharing/tree/master/
设计思路
教师端
主界面加载时,会调用方法对与本机ip网络地址段相同的一系列地址进行ping操作,能收到返回信息的说明此ip地址活跃于本局域网,这些ip地址会被添加到单选模式的ip选择下拉框和多播模式的ip复选框中。
模式选择完毕后,点击开始共享,当当前模式为多播或广播时将选中的ip地址以a字符为间隔连接,并使用udpclient广播,当为单播模式时,广播的信息为单个0字符,这样操作的目的时使学生端根据收到的信息判断是否需要接收教师端发送的音频信息。与此同时使用定时器每隔固定时间调用获取屏幕图像的方法,将图像转换成数据流,并使用udpclient发送给指定ip或多播组ip,由于数据量过大,需要分包,按每1400字节分割数据,并在首部加两位的头,第一位取值A、B、C,分别代表数据流开始、中间、结束,第二位取值为分包的个数。
开始共享后,点击开启声音,使用NAudio库,开始采集麦克风的声音信息,不断存入缓存,同时读指针不断读取缓存,当达到指定大小后,触发事件,执行委托,将读取的缓存数据使用udpclient发送到指定多播组ip。
学生端
主界面加载时,点击开始,首先进行接收来自教师端的广播信息,若接收信息为0字符,则为单播模式,若接收信息为IP字符串,则将其中IP与本端IP依次对比,若存在相同,则将本端接收图像数据的udpclient加入多播组,接收图像数据时,依次查询数据包的头部,从开始标识数据包开始接收,直到结尾标识数据包,然后整合数据,将其复原成图片,显示到端界面。
当成功接收图像信息后才可点击开启声音,用于接收声音数据的udpclient加入多播组,接收教师端发送的数据,存入待播放缓存,播放声音。
主要步骤代码
获取局域网IP
private List<String> ipItems = new List<String>();//存通过ping探测到的ip
private List<String> PingIp()//通过ping的方式找到局域网中的活动ip
{
var hostName = Dns.GetHostName();//得到本机主机名
var ipEntry = Dns.GetHostEntry(hostName);//根据主机名得到IPHostEntry实例
var ips = ipEntry.AddressList;//获得本机的ip地址列表
string sIP = null;
foreach (var ipa in ips)
{
if (ipa.AddressFamily == AddressFamily.InterNetwork)
{
sIP = ipa.ToString();//得到列表中最后一个ip,转字符串
}
}
if (sIP != null)
{
for (int i = 1; i < 255; i++)
{
var ip = sIP.Substring(0, sIP.LastIndexOf(".")) + "." + i;//截取出网络地址段+i
if (ip == sIP)//跳过本机ip地址
{
continue;
}
var ping = new Ping();
ping.PingCompleted += ping_completed;//事件触发执行委托
ping.SendAsync(ip, 1000, null);//分别ping这些ip地址
}
Thread.Sleep(1000);
}
return ipItems;
}
发送与接收模式信息
public void sendScreen()//发送窗口截图
{
IPEndPoint iep1 = null;
IPEndPoint iep2 = null;
byte[] ipSelected = null;//被选中的目的主机ip
string ipstr = null;
UdpClient udpClient1 = new UdpClient(2001);//用于发送模式信息
UdpClient udpClient2 = new UdpClient(2002);//用于发送图片
byte[] data = screenData();//执行窗口截图函数
if (form2.radioButton1.Checked == true)//单播被选中
{
if (form2.comboBox1.SelectedItem != null)//已有选中ip
{
iep1 = new IPEndPoint(IPAddress.Parse(form2.comboBox1.SelectedItem.ToString()), 2001);
iep2 = new IPEndPoint(IPAddress.Parse(form2.comboBox1.SelectedItem.ToString()), 2002);
ipSelected = Encoding.Default.GetBytes("0");
udpClient1.Send(ipSelected, ipSelected.Length, iep1);//给目的ip发送长度为1的数据包//以告知发送端当前为单播模式
}
}
else if (form2.radioButton2.Checked == true)//多播被选中
{
iep1 = new IPEndPoint(IPAddress.Parse("192.168.1.255"), 2001);//直接广播地址及端口
iep2 = new IPEndPoint(IPAddress.Parse("224.1.2.3"), 2002); //多播地址及端口
udpClient1.EnableBroadcast = true;//允许发送广播
//将两个多选框中被选中项转换为字符串,以a为间隔
for (int j = 0; j < form2.checkedListBox1.SelectedItems.Count; j++)
{
ipstr += form2.checkedListBox1.SelectedItems[j].ToString() + "a";
}
for (int j = 0; j < form2.checkedListBox2.SelectedItems.Count; j++)
{
ipstr += form2.checkedListBox2.SelectedItems[j].ToString() + "a";
}
ipSelected = Encoding.Default.GetBytes(ipstr);//字符串转byte数组
udpClient1.Send(ipSelected, ipSelected.Length, iep1);//广播存有被选ip的数据包//以告知发送端为多播模式
}
else if (form2.radioButton3.Checked == true)//广播被选中
{
iep1 = new IPEndPoint(IPAddress.Parse("192.168.1.255"), 2001);//直接广播地址及端口
iep2 = new IPEndPoint(IPAddress.Parse("224.1.2.3"), 2002); //多播地址及端口
udpClient1.EnableBroadcast = true;//允许发送广播
for (int i = 0; i < ipItems.Count; i++)//将所有ip转为字符串,a为间隔
{
ipstr += ipItems[i] + "a";
}
ipSelected = Encoding.Default.GetBytes(ipstr);//字符串转byte数组
udpClient1.Send(ipSelected, ipSelected.Length, iep1);//广播存有被选ip的数据包//以告知发送端为多播模式
}
public void Listen1()//监听模式信息//单播或多播及ip
{
udpClient1 = new UdpClient(2001);
iep1 = new IPEndPoint(IPAddress.Any, 2001);
//获得本机ip
var hostname = Dns.GetHostName();
var ipEntry = Dns.GetHostEntry(hostname);
var ips = ipEntry.AddressList;
string sIP = null;
foreach (var ipa in ips)
{
if (ipa.AddressFamily == AddressFamily.InterNetwork)
{
sIP = ipa.ToString();
}
}
bool flag = true;
bool flag2 = false;//TRUE为多播//FALSE为单播
while (flag)
{
byte[] receive = udpClient1.Receive(ref iep1);
int count = receive.Length;
if (count > 0)
{
if (count == 1)//收到字节数为1时意味着单播
{
break;
}
string[] str = Encoding.Default.GetString(receive).Split('a');//分割收到的ip数据
for (int i = 0; i < str.Length; i++)//依次与本机ip地址对比
{
if (str[i].Equals(sIP))
{
flag2 = true;
break;
}
}
flag = false;
}
receive = null;
}
Listen2(flag2);
}
获取屏幕图像
private byte[] screenData()//截取窗口
{
MemoryStream ms = new MemoryStream();
try
{
byte[] by;
Bitmap bmp = new Bitmap(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height);
Graphics gp = Graphics.FromImage(bmp);
//将屏幕图像复制到bmp中
gp.CopyFromScreen(0, 0, 0, 0, new Size(Screen.PrimaryScreen.Bounds.Width, Screen.PrimaryScreen.Bounds.Height));
bmp.Save(ms, ImageFormat.Jpeg);
by = ms.GetBuffer();
pictureBox1.Image = bmp.GetThumbnailImage(320, 256, null, IntPtr.Zero);//返回缩略图
ms.Close();
ms.Dispose();
return by;
}
catch
{
ms.Close();
ms.Dispose();
return null;
}
}
发送与接收图像
public void sendScreen()//发送窗口截图
{
IPEndPoint iep1 = null;
IPEndPoint iep2 = null;
byte[] ipSelected = null;//被选中的目的主机ip
string ipstr = null;
UdpClient udpClient1 = new UdpClient(2001);//用于发送模式信息
UdpClient udpClient2 = new UdpClient(2002);//用于发送图片
byte[] data = screenData();//执行窗口截图函数
if (form2.radioButton1.Checked == true)//单播被选中
{
if (form2.comboBox1.SelectedItem != null)//已有选中ip
{
iep1 = new IPEndPoint(IPAddress.Parse(form2.comboBox1.SelectedItem.ToString()), 2001);
iep2 = new IPEndPoint(IPAddress.Parse(form2.comboBox1.SelectedItem.ToString()), 2002);
ipSelected = Encoding.Default.GetBytes("0");
udpClient1.Send(ipSelected, ipSelected.Length, iep1);//给目的ip发送长度为1的数据包//以告知发送端当前为单播模式
}
}
else if (form2.radioButton2.Checked == true)//多播被选中
{
iep1 = new IPEndPoint(IPAddress.Parse("192.168.1.255"), 2001);//直接广播地址及端口
iep2 = new IPEndPoint(IPAddress.Parse("224.1.2.3"), 2002); //多播地址及端口
udpClient1.EnableBroadcast = true;//允许发送广播
//将两个多选框中被选中项转换为字符串,以a为间隔
for (int j = 0; j < form2.checkedListBox1.SelectedItems.Count; j++)
{
ipstr += form2.checkedListBox1.SelectedItems[j].ToString() + "a";
}
for (int j = 0; j < form2.checkedListBox2.SelectedItems.Count; j++)
{
ipstr += form2.checkedListBox2.SelectedItems[j].ToString() + "a";
}
ipSelected = Encoding.Default.GetBytes(ipstr);//字符串转byte数组
udpClient1.Send(ipSelected, ipSelected.Length, iep1);//广播存有被选ip的数据包//以告知发送端为多播模式
}
else if (form2.radioButton3.Checked == true)//广播被选中
{
iep1 = new IPEndPoint(IPAddress.Parse("192.168.1.255"), 2001);//直接广播地址及端口
iep2 = new IPEndPoint(IPAddress.Parse("224.1.2.3"), 2002); //多播地址及端口
udpClient1.EnableBroadcast = true;//允许发送广播
for (int i = 0; i < ipItems.Count; i++)//将所有ip转为字符串,a为间隔
{
ipstr += ipItems[i] + "a";
}
ipSelected = Encoding.Default.GetBytes(ipstr);//字符串转byte数组
udpClient1.Send(ipSelected, ipSelected.Length, iep1);//广播存有被选ip的数据包//以告知发送端为多播模式
}
udpClient2.Client.SendBufferSize = 1024 * 1024 * 5;//发送端缓存大小
int size = 0;
int count = 1399;
int length = (int)Math.Ceiling((double)data.Length / 1400);//分包数量
while (size < data.Length)
{
byte[] temp = null;
//数据分包//给数据添加特殊头//分别表示属于哪部分(开始、中间、结束)、分包数
if (size == 0)//数据开始部分
{
temp = SetFirst(data.Skip(size).Take(count).ToArray(), length);
}
else if (size + count > data.Length)//数据中间部分
{
temp = SetLast(data.Skip(size).Take(count).ToArray(), length);
}
else//数据结尾部分
{
temp = SetMiddle(data.Skip(size).Take(count).ToArray(), length);
}
if (iep2!=null)
{
udpClient2.Send(temp, temp.Length, iep2);//发送数据
}
size += count;//取缓存的位置移动
}
udpClient1.Close();//释放资源
udpClient2.Close();
}
private static byte[] SetFirst(byte[] arr, int length)
{
byte[] res = new byte[arr.Length + 2];
arr.CopyTo(res, 2);//原本数据后移2位
res[0] = (byte)'A';//数据属于哪部分
res[1] = (byte)length;//分包数
return res;
}
private static byte[] SetMiddle(byte[] arr, int length)
{
byte[] res = new byte[arr.Length + 2];
arr.CopyTo(res, 2);
res[0] = (byte)'B';
res[1] = (byte)length;
return res;
}
private static byte[] SetLast(byte[] arr, int length)
{
byte[] res = new byte[arr.Length + 2];
arr.CopyTo(res, 2);
res[0] = (byte)'C';
res[1] = (byte)length;
return res;
}
public void Listen2(bool flag)
{
if (udpClient2 == null)
{
udpClient2 = new UdpClient(2002);
iep2 = new IPEndPoint(IPAddress.Any, 2002);
if (flag)
{
udpClient2.JoinMulticastGroup(IPAddress.Parse("224.1.2.3"), 0);
}
ReceiveThread = new Thread(new ThreadStart(ListenThread));
ReceiveThread.IsBackground = true;
ReceiveThread.Start();
}
}
private void ListenThread()
{
try
{
bool flag = false;
int length = 0;
while (true)
{
byte[] receive = udpClient2.Receive(ref iep2);
int count = receive.Length;
if (receive[0] == 'A')//数据开始
{
flag = true;
}
if (count > 0 && flag)
{
if (receive[0] == 'C')//数据结尾
{
length++;
if (length != receive[1])//分包书与接收数不符
{
TempData.Clear();
}
else
{
TempData.Add(receive.Take(count).ToArray());
var res = GetAllData(TempData);
MemoryStream ms = new MemoryStream(res);
pictureBox1.Image = Image.FromStream(ms);
Action action = () => { button3.Enabled = true; };
Invoke(action);
ms.Close();
TempData.Clear();
length = 0;
}
}
else
{
length++;
TempData.Add(receive);
}
}
}
}
catch (Exception)
{
TempData.Clear();
}
}
private static byte[] GetAllData(List<byte[]> list)//将头部之外的数据整合
{
List<byte> data = new List<byte>();
for (int i = 0; i < list.Count; i++)
{
for (int j = 2; j < list[i].Length; j++)
{
data.Add(list[i][j]);
}
}
return data.ToArray();
}
发送与接收声音
使用了Naudio开源库,在VS2022项目右键-管理NuGet程序包里搜索安装,就可以在代码里Using了,NAudio库的链接:https://github.com/naudio/NAudio
private WaveIn waveIn;
private UdpClient udpClient3;//用于发送声音数据
private void toolStripMenuItem3_Click(object sender, EventArgs e)//开启关闭声音
{
if (toolStripMenuItem3.Text.Equals("开启声音"))
{
toolStripMenuItem3.Text = "关闭声音";
udpClient3 = new UdpClient(2003);
waveIn = new WaveIn();//生成音频输入设备
waveIn.WaveFormat = new WaveFormat(16000, 16, 1); //设置录音格式
//绑定声音输入处理事件
waveIn.DataAvailable += new EventHandler<WaveInEventArgs>(DataAvailable);
waveIn.StartRecording();//开始采集声音
}
else
{
toolStripMenuItem3.Text = "开启声音";
waveIn.StopRecording();//停止采集声音
CloseUdpCliend();//关闭组播
}
}
private void DataAvailable(object sender, WaveInEventArgs e)//处理事件的方法
{
SendMessage(e.Buffer, 2003);
}
public async void SendMessage(byte[] message, int destport)//发送声音数据
{
if (udpClient3 == null) return;
byte[] buffer = message;
int len = 0;
try
{
len = await udpClient3.SendAsync(buffer, buffer.Length, new IPEndPoint(IPAddress.Parse("224.1.2.4"), destport));
}
catch (Exception)
{
len = 0;
}
if (len <= 0)
{
Thread.Sleep(100);
}
}
public void CloseUdpCliend()
{
if (udpClient3 == null)
{ throw new ArgumentNullException("udpClient cant not null"); }
try
{
udpClient3.Client.Shutdown(SocketShutdown.Both);
}
catch (Exception)
{
}
udpClient3.Close();
udpClient3 = null;
}
private WaveOut waveOut;//音频输出
private BufferedWaveProvider buffered;//接收声音缓冲
private UdpClient udpClient3;//接收
private void button3_Click(object sender, EventArgs e)
{
if (button3.Text.Equals("开启声音"))
{
//接收udp组播数据用
udpClient3 = new UdpClient(2003);
//生成默认声音播放对象
waveOut = new WaveOut();
//设置声音格式
WaveFormat wf = new WaveFormat(16000, 16, 1);
//初始化待播放声音缓存对象
buffered = new BufferedWaveProvider(wf);
//初始化播放设备
waveOut.Init(buffered);
Start();//开始接收数据
button3.Text = "关闭声音";
waveOut.Play();//开始播放声音
}
else
{
button3.Text = "开启声音";
waveOut.Stop();//停止播放声音
CloseUdpCliend();//关闭接收
}
}
public void Start()
{
while (true)
{
try
{
udpClient3.JoinMulticastGroup(IPAddress.Parse("224.1.2.4"));
ReceiveMessage();
break;
}
catch (Exception)
{
Thread.Sleep(100);
}
}
}
private async void ReceiveMessage()//接收声音数据
{
while (true)
{
if (udpClient3 == null)return;
try
{
UdpReceiveResult udpReceiveResult = await udpClient3.ReceiveAsync();
byte[] message = udpReceiveResult.Buffer;
buffered.AddSamples(message, 0, message.Length);//添加到待播放缓存中
}
catch (Exception)
{
}
}
}
public void CloseUdpCliend()
{
if (udpClient3 == null)
throw new ArgumentNullException("udpClient cant not null");
try
{
udpClient3.Client.Shutdown(SocketShutdown.Both);
}
catch (Exception)
{
}
udpClient3.Close();
udpClient3 = null;
}
运行截图
发送端
接收端
存在的明显缺陷
1.传输图像中没有鼠标
2.图像成功传输二十几秒后就出问题了,推测与发送端或接收端缓存有关,还未解决