Python3 实现简易局域网视频聊天工具
2018-07-13 02:46:35来源:博客园 阅读 ()
操作系统为 Ubuntu 16.04
,OpenCV 版本为opencv-python 3.4.1.15
。
你可以在我的 Github 上找到 Windows 系统和 Linux 系统对应的源代码,此教程对应的版本是 v0.2。目前我正在开发的版本是 v0.3,新版本将允许使用不同IP协议的主机通信,并且范围不再局限于局域网内。这个工具最初是为了通过IPv6节省聊天工具使用的流量而开发的。
2. 内容简介
- 本实验实现简易的视频通信工具
- 在视频通信的基础上加入语音
- 用户可以选择通信的质量,即画质、停顿等参数
- 支持IPv6
3. 实验知识点
本课程项目完成过程中将学习:
- Python 基于 OpenCV 对摄像头信息的捕获和压缩
- Python 关于 线程 和 socket 通信的一些基础技巧
- Python 基于 PyAudio 对语音信息的捕获和压缩
其中将重点介绍 socket 传输过程中对数据的压缩和处理。
4.实验环境
- python 3.5
- opencv-python 3.4.1.15
- numpy 1.14.5
- PyAudio 0.2.11
二、环境搭建
通过以下命令可下载项目源码,作为参照对比完成下面详细步骤的学习。
$ cd Code $ wget https://labfile.oss.aliyuncs.com/courses/672/ichat.zip $ unzip ichat.zip
现在开始下载环境依赖的包,确保在刚在解压文件下的目录里运行。
$ cd ichat
$ sudo pip3 install numpy
$ sudo pip3 install opencv_python
这一步下载了我们需要的opencv-python和numpy两个包。
剩下的PyAudio,由于本虚拟环境的部分问题,我们单独分开下载。
$ sudo apt-get install portaudio19-dev python-all-dev python3-all-dev
$ sudo pip3 install pyaudio==0.2.11
现在,我们的实验环境就搭好了。
三、实验原理
实验实现了简易的视频通信工具,基于 OpenCV 和 PyAudio,使用 TCP 协议通信,通信双方建立双向 CS 连接,双方均维护一个客户端和一个服务器端。在捕获视频信息后,根据用户指定的参数对画面做压缩并传输。
四、实验步骤
接下来我们分步骤讲解本实验。
4.1 实现双向 C/S 连接
先为双方的通信设计 Server 类和 Client类,两个类均继承 threading.Thread
,只需要分别实现 __init__
、__del__
和run
方法,之后对象调用.start()
方法即可在独立线程中执行run
方法中的内容。首先Client
类需要存储远端的IP地址和端口,而Server
类需要存储本地服务器监听的端口号。用户还应当可以指定通信双方使用的协议版本,即基于IPv4 还是IPv6 的TCP连接。因此Server
类的初始化需要传入两个参数(端口、版本),Client
类的初始化需要三个参数(远端IP、端口、版本)。新建文件vchat.py
,在其中定义基础的两个类如下。
1 from socket import * 2 import threading 3 class Video_Server(threading.Thread): 4 def __init__(self, port, version) : 5 threading.Thread.__init__(self) 6 self.setDaemon(True) 7 self.ADDR = ('', port) 8 if version == 4: 9 self.sock = socket(AF_INET ,SOCK_STREAM) 10 else: 11 self.sock = socket(AF_INET6 ,SOCK_STREAM) 12 def __del__(self): 13 self.sock.close() 14 # TODO 15 def run(self): 16 print("server starts...") 17 self.sock.bind(self.ADDR) 18 self.sock.listen(1) 19 conn, addr = self.sock.accept() 20 print("remote client success connected...") 21 # TODO 22 23 class Video_Client(threading.Thread): 24 def __init__(self ,ip, port, version): 25 threading.Thread.__init__(self) 26 self.setDaemon(True) 27 self.ADDR = (ip, port) 28 if version == 4: 29 self.sock = socket(AF_INET, SOCK_STREAM) 30 else: 31 self.sock = socket(AF_INET6, SOCK_STREAM) 32 def __del__(self) : 33 self.sock.close() 34 # TODO 35 def run(self): 36 print("client starts...") 37 while True: 38 try: 39 self.sock.connect(self.ADDR) 40 break 41 except: 42 time.sleep(3) 43 continue 44 print("client connected...") 45 # TODO
4.2 实现摄像头数据流捕获
OpenCV 为 Python 提供的接口非常简单并且易于理解。捕获视频流的任务应当由Client
类完成,下面完善Client
的run
函数。在下面的代码中,我们为类添加了一个成员变量cap
,它用来捕获默认摄像头的输出。
1 class Video_Client(threading.Thread): 2 def __init__(self ,ip, port, version): 3 threading.Thread.__init__(self) 4 self.setDaemon(True) 5 self.ADDR = (ip, port) 6 if version == 4: 7 self.sock = socket(AF_INET, SOCK_STREAM) 8 else: 9 self.sock = socket(AF_INET6, SOCK_STREAM) 10 self.cap = cv2.VideoCapture(0) 11 def __del__(self) : 12 self.sock.close() 13 self.cap.release() 14 def run(self): 15 print("client starts...") 16 while True: 17 try: 18 self.sock.connect(self.ADDR) 19 break 20 except: 21 time.sleep(3) 22 continue 23 print("client connected...") 24 while self.cap.isOpened(): 25 ret, frame = self.cap.read() 26 # TODO
4.3 发送捕获到的数据到服务器
已经捕获到数据,接下来要发送字节流。首先我们继续编写Client
,为其添加发送数据功能的实现。这里只改动了run
方法。在捕获到帧后,我们使用pickle.dumps
方法对其打包,并用sock.sendall
方法发送。注意发送过程中我们用struct.pack
方法为每批数据加了一个头,用于接收方确认接受数据的长度。
1 def run(self): 2 while True: 3 try: 4 self.sock.connect(self.ADDR) 5 break 6 except: 7 time.sleep(3) 8 continue 9 print("client connected...") 10 while self.cap.isOpened(): 11 ret, frame = self.cap.read() 12 data = pickle.dumps(frame) 13 try: 14 self.sock.sendall(struct.pack("L", len(data)) + data) 15 except: 16 break
下面编写Server
,在服务器端连接成功后,应当创建一个窗口用于显示接收到的视频。因为连接不一定创建成功,因此cv.destroyAllWindows()
被放在一个try..catch
块中防止出现错误。在接收数据过程中,我们使用payload_size
记录当前从缓冲区读入的数据长度,这个长度通过struct.calcsize('L')
来读取。使用该变量的意义在于缓冲区中读出的数据可能不足一个帧,也可能由多个帧构成。为了准确提取每一帧,我们用payload_size
区分帧的边界。在从缓冲区读出的数据流长度超过payload_size
时,剩余部分和下一次读出的数据流合并,不足payload_size
时将合并下一次读取的数据流到当前帧中。在接收完完整的一帧后,显示在创建的窗口中。同时我们为窗口创建一个键盘响应,当按下Esc
或 q
键时退出程序。
class Video_Server(threading.Thread): def __init__(self, port, version) : threading.Thread.__init__(self) self.setDaemon(True) self.ADDR = ('', port) if version == 4: self.sock = socket(AF_INET ,SOCK_STREAM) else: self.sock = socket(AF_INET6 ,SOCK_STREAM) def __del__(self): self.sock.close() try: cv2.destroyAllWindows() except: pass def run(self): print("server starts...") self.sock.bind(self.ADDR) self.sock.listen(1) conn, addr = self.sock.accept() print("remote client success connected...") data = "".encode("utf-8") payload_size = struct.calcsize("L") cv2.namedWindow('Remote', cv2.WINDOW_NORMAL) while True: while len(data) < payload_size: data += conn.recv(81920) packed_size = data[:payload_size] data = data[payload_size:] msg_size = struct.unpack("L", packed_size)[0] while len(data) < msg_size: data += conn.recv(81920) zframe_data = data[:msg_size] data = data[msg_size:] frame_data = zlib.decompress(zframe_data) frame = pickle.loads(frame_data) cv2.imshow('Remote', frame) if cv2.waitKey(1) & 0xFF == 27: break
4.4 视频缩放和数据压缩
现在的服务器和客户端已经可以运行,你可以在代码中创建一个Client
类实例和一个Server
类实例,并将IP地址设为127.0.0.1
,端口设为任意合法的(0-65535)且不冲突的值,版本设为IPv4。执行代码等同于自己和自己通信。如果网络状况不好,你也许会发现自己和自己的通信也有卡顿现象。为了使画面质量、延迟能够和现实网络状况相匹配,我们需要允许用户指定通信中画面的质量,同时我们的代码应当本身具有压缩数据的能力,以尽可能利用带宽。
当用户指定使用低画质通信,我们应当对原始数据做变换,最简单的方式即将捕获的每一帧按比例缩放,同时降低传输的帧速,在代码中体现为resize
,该函数的第二个参数为缩放中心,后两个参数为缩放比例,并且根据用户指定的等级,不再传输捕获的每一帧,而是间隔几帧传输一帧。为了防止用户指定的画质过差,代码中限制了最坏情况下的缩放比例为0.3,最大帧间隔为3。此外,我们在发送每一帧的数据前使用zlib.compress
对其压缩,尽量降低带宽负担。
1 class Video_Client(threading.Thread): 2 def __init__(self ,ip, port, level, version): 3 threading.Thread.__init__(self) 4 self.setDaemon(True) 5 self.ADDR = (ip, port) 6 if level <= 3: 7 self.interval = level 8 else: 9 self.interval = 3 10 self.fx = 1 / (self.interval + 1) 11 if self.fx < 0.3: 12 self.fx = 0.3 13 if version == 4: 14 self.sock = socket(AF_INET, SOCK_STREAM) 15 else: 16 self.sock = socket(AF_INET6, SOCK_STREAM) 17 self.cap = cv2.VideoCapture(0) 18 def __del__(self) : 19 self.sock.close() 20 self.cap.release() 21 def run(self): 22 print("VEDIO client starts...") 23 while True: 24 try: 25 self.sock.connect(self.ADDR) 26 break 27 except: 28 time.sleep(3) 29 continue 30 print("VEDIO client connected...") 31 while self.cap.isOpened(): 32 ret, frame = self.cap.read() 33 sframe = cv2.resize(frame, (0,0), fx=self.fx, fy=self.fx) 34 data = pickle.dumps(sframe) 35 zdata = zlib.compress(data, zlib.Z_BEST_COMPRESSION) 36 try: 37 self.sock.sendall(struct.pack("L", len(zdata)) + zdata) 38 except: 39 break 40 for i in range(self.interval): 41 self.cap.read()
服务器端最终代码如下,增加了对接收到数据的解压缩处理。
1 class Video_Server(threading.Thread): 2 def __init__(self, port, version) : 3 threading.Thread.__init__(self) 4 self.setDaemon(True) 5 self.ADDR = ('', port) 6 if version == 4: 7 self.sock = socket(AF_INET ,SOCK_STREAM) 8 else: 9 self.sock = socket(AF_INET6 ,SOCK_STREAM) 10 def __del__(self): 11 self.sock.close() 12 try: 13 cv2.destroyAllWindows() 14 except: 15 pass 16 def run(self): 17 print("VEDIO server starts...") 18 self.sock.bind(self.ADDR) 19 self.sock.listen(1) 20 conn, addr = self.sock.accept() 21 print("remote VEDIO client success connected...") 22 data = "".encode("utf-8") 23 payload_size = struct.calcsize("L") 24 cv2.namedWindow('Remote', cv2.WINDOW_NORMAL) 25 while True: 26 while len(data) < payload_size: 27 data += conn.recv(81920) 28 packed_size = data[:payload_size] 29 data = data[payload_size:] 30 msg_size = struct.unpack("L", packed_size)[0] 31 while len(data) < msg_size: 32 data += conn.recv(81920) 33 zframe_data = data[:msg_size] 34 data = data[msg_size:] 35 frame_data = zlib.decompress(zframe_data) 36 frame = pickle.loads(frame_data) 37 cv2.imshow('Remote', frame) 38 if cv2.waitKey(1) & 0xFF == 27: 39 break
4.5 加入音频的捕获和传输
在完成视频通信的基础上,整体框架对于音频通信可以直接挪用,只需要修改其中捕获视频/音频的代码和服务器解码播放的部分。这里我们使用 PyAudio 库处理音频,在 Linux 下你也可以选择 sounddevice
。关于sounddevice
这里不做过多介绍,将vchat.py
复制一份,重命名为achat.py
,简单修改几处,最终音频捕获、传输的完整代码如下。我将上面代码中的Server
和Client
分别加上Video
和Audio
前缀以区分,同时显示给用户的print
输出语句也做了一定修改,对于视频加上VIDEO
前缀,音频加上AUDIO
前缀。如果你对代码中使用到的 PyAudio 提供的库函数有所疑问,
1 class Audio_Server(threading.Thread): 2 def __init__(self, port, version) : 3 threading.Thread.__init__(self) 4 self.setDaemon(True) 5 self.ADDR = ('', port) 6 if version == 4: 7 self.sock = socket(AF_INET ,SOCK_STREAM) 8 else: 9 self.sock = socket(AF_INET6 ,SOCK_STREAM) 10 self.p = pyaudio.PyAudio() 11 self.stream = None 12 def __del__(self): 13 self.sock.close() 14 if self.stream is not None: 15 self.stream.stop_stream() 16 self.stream.close() 17 self.p.terminate() 18 def run(self): 19 print("AUDIO server starts...") 20 self.sock.bind(self.ADDR) 21 self.sock.listen(1) 22 conn, addr = self.sock.accept() 23 print("remote AUDIO client success connected...") 24 data = "".encode("utf-8") 25 payload_size = struct.calcsize("L") 26 self.stream = self.p.open(format=FORMAT, 27 channels=CHANNELS, 28 rate=RATE, 29 output=True, 30 frames_per_buffer = CHUNK 31 ) 32 while True: 33 while len(data) < payload_size: 34 data += conn.recv(81920) 35 packed_size = data[:payload_size] 36 data = data[payload_size:] 37 msg_size = struct.unpack("L", packed_size)[0] 38 while len(data) < msg_size: 39 data += conn.recv(81920) 40 frame_data = data[:msg_size] 41 data = data[msg_size:] 42 frames = pickle.loads(frame_data) 43 for frame in frames: 44 self.stream.write(frame, CHUNK) 45 46 class Audio_Client(threading.Thread): 47 def __init__(self ,ip, port, version): 48 threading.Thread.__init__(self) 49 self.setDaemon(True) 50 self.ADDR = (ip, port) 51 if version == 4: 52 self.sock = socket(AF_INET, SOCK_STREAM) 53 else: 54 self.sock = socket(AF_INET6, SOCK_STREAM) 55 self.p = pyaudio.PyAudio() 56 self.stream = None 57 def __del__(self) : 58 self.sock.close() 59 if self.stream is not None: 60 self.stream.stop_stream() 61 self.stream.close() 62 self.p.terminate() 63 def run(self): 64 print("AUDIO client starts...") 65 while True: 66 try: 67 self.sock.connect(self.ADDR) 68 break 69 except: 70 time.sleep(3) 71 continue 72 print("AUDIO client connected...") 73 self.stream = self.p.open(format=FORMAT, 74 channels=CHANNELS, 75 rate=RATE, 76 input=True, 77 frames_per_buffer=CHUNK) 78 while self.stream.is_active(): 79 frames = [] 80 for i in range(0, int(RATE / CHUNK * RECORD_SECONDS)): 81 data = self.stream.read(CHUNK) 82 frames.append(data) 83 senddata = pickle.dumps(frames) 84 try: 85 self.sock.sendall(struct.pack("L", len(senddata)) + senddata) 86 except: 87 break
至此我们完成了 vchat.py 的编写。
4.6 编写程序入口 main.py
为了提供用户参数解析,代码使用了argparse
。你可能对此前几个类中初始化方法的self.setDaemon(True)
有疑惑。这个方法的调用使每个线程在主线程结束之后自动退出,保证程序不会出现崩溃且无法销毁的情况。在main.py
中,我们通过每隔1s做一次线程的保活检查,如果视频/音频中出现阻塞/故障,主线程会终止。
1 import sys 2 import time 3 import argparse 4 from vchat import Video_Server, Video_Client 5 from achat import Audio_Server, Audio_Client 6 7 parser = argparse.ArgumentParser() 8 9 parser.add_argument('--host', type=str, default='127.0.0.1') 10 parser.add_argument('--port', type=int, default=10087) 11 parser.add_argument('--level', type=int, default=1) 12 parser.add_argument('-v', '--version', type=int, default=4) 13 14 args = parser.parse_args() 15 16 IP = args.host 17 PORT = args.port 18 VERSION = args.version 19 LEVEL = args.level 20 21 if __name__ == '__main__': 22 vclient = Video_Client(IP, PORT, LEVEL, VERSION) 23 vserver = Video_Server(PORT, VERSION) 24 aclient = Audio_Client(IP, PORT+1, VERSION) 25 aserver = Audio_Server(PORT+1, VERSION) 26 vclient.start() 27 aclient.start() 28 time.sleep(1) # make delay to start server 29 vserver.start() 30 aserver.start() 31 while True: 32 time.sleep(1) 33 if not vserver.isAlive() or not vclient.isAlive(): 34 print("Video connection lost...") 35 sys.exit(0) 36 if not aserver.isAlive() or not aclient.isAlive(): 37 print("Audio connection lost...") 38 sys.exit(0)
4.7 运行情况
因为实验楼的环境没有提供摄像头,因此我们需要修改一下代码,让程序从一个本地视频文件读取,模拟摄像头的访问。将Video_Client
中self.cap = cv2.VideoCapture(0)
改为self.cap = cv2.VideoCapture('test.mp4')
,即从本地视频test.mp4
中读取。在修改完你的代码后,你可以通过以下命令下载test.mp4
(该视频文件是周杰伦《浪漫手机》的MV),并检验代码。(请确保在ichat文件夹下!)
$ wget http://labfile.oss.aliyuncs.com/courses/671/test.mp4
$ python3 main.py
和上面命令一样,在本机可以通过 python3 main.py
来实验本机和本机的视频聊天,如果你有条件在同一局域网内的两台机器上实验,则可以将程序部署在两台机器上,并相互连接观察效果。下面两张图为本机上实验截图,有些情况下 PyAudio 可能会提示一些警告,你可以忽视它的提示。用户也可以指定level
参数,level
越高,画质越差,level
为 0 为原始画面,在我们的main.py
中默认level
为 1。
通过在某高校校园网内验证,程序可以保证长时间顺畅通话,偶尔会出现网络质量较差导致的短暂卡顿,不影响实际视频通话效果。
转载来自https://hzl-fj.com/2248.html
标签:
版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有
上一篇:008days(文件操作)
- python3基础之“术语表(2)” 2019-08-13
- python3 之 字符串编码小结(Unicode、utf-8、gbk、gb2312等 2019-08-13
- Python3安装impala 2019-08-13
- python day2-爬虫实现github登录 2019-08-13
- python3 enum模块的应用 2019-08-13
IDC资讯: 主机资讯 注册资讯 托管资讯 vps资讯 网站建设
网站运营: 建站经验 策划盈利 搜索优化 网站推广 免费资源
网络编程: Asp.Net编程 Asp编程 Php编程 Xml编程 Access Mssql Mysql 其它
服务器技术: Web服务器 Ftp服务器 Mail服务器 Dns服务器 安全防护
软件技巧: 其它软件 Word Excel Powerpoint Ghost Vista QQ空间 QQ FlashGet 迅雷
网页制作: FrontPages Dreamweaver Javascript css photoshop fireworks Flash