python语言视频-Python语言之Python3 实现简易局域网视频聊天工具

本文主要向大家介绍了Python语言之Python3 实现简易局域网视频聊天工具,通过具体的内容向大家展示,希望对大家学习Python语言有所帮助。

操作系统为 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。

通过在某高校校园网内验证,程序可以保证长时间顺畅通话,偶尔会出现网络质量较差导致的短暂卡顿,不影响实际视频通话效果。

本文由职坐标整理并发布,希望对同学们学习Python有所帮助,更多内容请关注职坐标编程语言Python频道!

你可能感兴趣的:(python语言视频-Python语言之Python3 实现简易局域网视频聊天工具)