Python实现简易局域网视频聊天工具

         基于实验楼限免课程:

Python实现简易局域网视频聊天工具

         试验操作系统为Ubuntu14.04,OpenCV版本为2.4.13.1,源代码可以在https://github.com/Forec/lan-ichat上获取。软件初衷是为了在IPV6协议网络中节省聊天流量。

         内容简介:

                   1.实现简易视频通信工具,并能实现语音通话 2.通信质量 ,如画质、停顿等参数可以调节  3.支持IPV6

         知识点:

                   1.Python基于Opencv对摄像头信息的捕获和压缩

                   2.Python关于线程和socket通信的基础技巧(数据的压缩处理为重点)

                   3.Python基于Pyaudio对语音信息的捕获和压缩

期间遇到的问题:

         1.执行wgethttps://labfile.oss.aliyuncs.com/courses/672/2.4.13-binary.tar.gz操作时,遇到证书问题

         2.进入到python后回不去了(小白笨笨哒),http://blog.csdn.net/langzi7758521/article/details/51163009在这里找到了解法

即quit()/exit()/ctrl+d退出。。之前一直尝试cd ../cd ~无果

         3.import cv2时出现问题:failed to initialize libdc1394,百度后http://stackoverflow.com/questions/12689304/ctypes-error-libdc1394-error-failed-to-initialize-libdc1394中找到解法。

         4.最后仍是无法查看CV2的版本,原因是无法打出更长的下划线。。无语无奈

无法下载链接中的文件,于是通过实验楼中的火狐浏览器直接下载后,复制到Opencv文件夹下,进行下一步操作成功。

 

过程理解:

         1.更新实验楼中一些基本库

         2.新建OpenCV文件夹,下载作者已编译好的包,完成环境配置的准备工作

 

 

Python实现简易局域网视频聊天工具

一、课程介绍

1.课程来源

课程使用的操作系统为 Ubuntu 14.04OpenCV版本为OpenCV 2.4.13.1,你可以在这里查看该版本 OpenCV 的文档。

你可以在我的 Github 上找到 Windows 系统和 Linux 系统对应的源代码,此教程对应的版本是 v0.2。目前我正在开发的版本是 v0.3,新版本将允许使用不同IP协议的主机通信,并且范围不再局限于局域网内。这个工具最初是为了通过IPv6节省聊天工具使用的流量而开发的。

2.内容简介

  • 课程实验实现简易的视频通信工具
  • 在视频通信的基础上加入语音
  • 用户可以选择通信的质量,即画质、停顿等参数
  • 支持IPv6

3.课程知识点

本课程项目完成过程中将学习:

  • Python 基于 OpenCV 对摄像头信息的捕获和压缩
  • Python 关于 线程 和 socket 通信的一些基础技巧
  • Python 基于 PyAudio 对语音信息的捕获和压缩

其中将重点介绍 socket 传输过程中对数据的压缩和处理。

二、实验环境

  • 本实验需要先在实验平台安装 OpenCV ,需下载依赖的库、源代码并编译安装。安装过程建议按照教程给出的步骤,或者你可以参考官方文档中 Linux 环境下的安装步骤,但 有些选项需要变更。安装过程所需时间会比较长,这期间你可以先阅读接下来的教程,在大致了解代码原理后再亲自编写尝试。
  • 我提供了一个编译好的2.4.13-binary.tar.gz包,你可以通过下面的命令下载并安装,节省了编译的时间,但仍需要安装依赖库的过程,整个环境配置过程大约20~30分钟,根据当前机器速度决定。

·        $ sudo apt-get update

·        $ sudo apt-get install build-essentiallibgtk2.0-dev libjpeg-dev libtiff5-dev libjasper-dev libopenexr-dev cmakepython-dev python-numpy python-tk libtbb-dev libeigen2-dev yasm libfaac-devlibopencore-amrnb-dev libopencore-amrwb-dev libtheora-dev libvorbis-devlibxvidcore-dev libx264-dev libqt4-dev libqt4-opengl-dev sphinx-commontexlive-latex-extra libv4l-dev libdc1394-22-dev libavcodec-dev libavformat-devlibswscale-dev python-pyaudio

·        $ cd ~

·        $ mkdir OpenCV && cd OpenCV

·        $ wgethttps://labfile.oss.aliyuncs.com/courses/672/2.4.13-binary.tar.gz

·        $ tar -zxvf 2.4.13-binary.tar.gz

·        $ cdopencv-2.4.13

·        $ cd build

·        $ sudo make install

  • 如果你想体验编译的整个过程,我也提供了一个一键安装的脚本文件,你可以通过下面的命令尝试。这个过程会非常漫长,期间可能还需要你做一定的交互确认工作。

·        $ cd ~

·        $ sudo apt-get update

·        $ wgethttps://labfile.oss.aliyuncs.com/courses/672/opencv.sh

·        $ sudo chmod 777 opencv.sh

·        $ ./opencv.sh

  • 如果你觉得有必要亲自尝试一下安装的每一步,可以按照下面的命令逐条输入执行,在实验楼的环境中大概需要两个小时。

·        $ sudo apt-get update

·        $ sudo apt-get install build-essentiallibgtk2.0-dev libjpeg-dev libtiff5-dev libjasper-dev libopenexr-dev cmakepython-dev python-numpy python-tk libtbb-dev libeigen2-dev yasm libfaac-devlibopencore-amrnb-dev libopencore-amrwb-dev libtheora-dev libvorbis-dev libxvidcore-devlibx264-dev libqt4-dev libqt4-opengl-dev sphinx-common texlive-latex-extralibv4l-dev libdc1394-22-dev libavcodec-dev libavformat-dev libswscale-devpython-pyaudio

·        $ wgethttps://github.com/Itseez/opencv/archive/2.4.13.zip

·        $ unzip 2.4.13.zip

·        $ cd 2.4.13

·        $ mkdir release && cd release

·        $ cmake -D WITH_TBB=ON -DBUILD_NEW_PYTHON_SUPPORT=ON -D WITH_V4L=ON -D INSTALL_C_EXAMPLES=ON -DINSTALL_PYTHON_EXAMPLES=ON -D BUILD_EXAMPLES=ON -D WITH_QT=ON -D WITH_GTK=ON -DWITH_OPENGL=ON ..

·        $ sudo make

·        $ sudo make install

·        $ sudo gedit/etc/ld.so.conf.d/opencv.conf  

·        $ 输入 /usr/local/lib,按 Ctrl + X 退出,退出时询问是否保存,按 Y 确认。

·        $ sudo ldconfig -v

·        $ sudo gedit /etc/bash.bashrc

·        $ 在文件末尾加入

·        $ PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig

·        export PKG_CONFIG_PATH

·         Ctrl + X 退出,按 Y 确认保存。

  • 检验配置是否成功。

·        $ python

·        >>> import cv2

·        >>> cv2.__version__

·        '2.4.13'

三、实验原理

实验实现了简易的视频通信工具,基于 OpenCV PyAudio,使用 TCP 协议通信,通信双方建立双向 CS 连接,双方均维护一个客户端和一个服务器端。在捕获视频信息后,根据用户指定的参数对画面做压缩并传输。

四、实验步骤

通过以下命令可下载项目源码,作为参照对比完成下面详细步骤的学习。

wgethttps://labfile.oss.aliyuncs.com/courses/672/ichat.zip

unzip ichat.zip

1.实现双向 C/S 连接

先为双方的通信设计 Server 类和 Client类,两个类均继承threading.Thread,只需要分别实现 __init____del__run方法,之后对象调用.start()方法即可在独立线程中执行run方法中的内容。首先Client类需要存储远端的IP地址和端口,而Server类需要存储本地服务器监听的端口号。用户还应当可以指定通信双方使用的协议版本,即基于IPv4 还是IPv6 TCP连接。因此Server类的初始化需要传入两个参数(端口、版本),Client类的初始化需要三个参数(远端IP、端口、版本)。新建文件vchat.py,在其中定义基础的两个类如下。

from socket import *

import threading

classVideo_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()

        # TODO

   defrun(self):

        print("server starts...")

        self.sock.bind(self.ADDR)

        self.sock.listen(1)

        conn, addr = self.sock.accept()

        print("remote client success connected...")

        # TODO

 

classVideo_Client(threading.Thread):

   def__init__(self ,ip, port, version):

        threading.Thread.__init__(self)

        self.setDaemon(True)

        self.ADDR = (ip, 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()

        # TODO

   defrun(self):

        print("client starts...")

        whileTrue:

            try:

                self.sock.connect(self.ADDR)

                break

            except:

                time.sleep(3)

                continue

        print("client connected...")

        # TODO

2.实现摄像头数据流捕获

OpenCV Python 提供的接口非常简单并且易于理解。捕获视频流的任务应当由Client类完成,下面完善Clientrun函数。在下面的代码中,我们为类添加了一个成员变量cap,它用来捕获默认摄像头的输出。

classVideo_Client(threading.Thread):

   def__init__(self ,ip, port, version):

        threading.Thread.__init__(self)

        self.setDaemon(True)

        self.ADDR = (ip, port)

        if version == 4:

            self.sock = socket(AF_INET,SOCK_STREAM)

        else:

            self.sock = socket(AF_INET6,SOCK_STREAM)

        self.cap = cv2.VideoCapture(0)

   def__del__(self) :

        self.sock.close()

        self.cap.release()

   defrun(self):

        print("client starts...")

        whileTrue:

            try:

                self.sock.connect(self.ADDR)

                break

            except:

                time.sleep(3)

                continue

        print("client connected...")

        whileself.cap.isOpened():

            ret, frame = self.cap.read()

        # TODO

3.发送捕获到的数据到服务器

已经捕获到数据,接下来要发送字节流。首先我们继续编写Client,为其添加发送数据功能的实现。这里只改动了run方法。在捕获到帧后,我们使用pickle.dumps方法对其打包,并用sock.sendall方法发送。注意发送过程中我们用struct.pack方法为每批数据加了一个头,用于接收方确认接受数据的长度。

   defrun(self):

        whileTrue:

            try:

                self.sock.connect(self.ADDR)

                break

            except:

                time.sleep(3)

                continue

        print("client connected...")

        whileself.cap.isOpened():

            ret, frame = self.cap.read()

            data = pickle.dumps(frame)

            try:

                self.sock.sendall(struct.pack("L", len(data))+ data)

            except:

                break

下面编写Server,在服务器端连接成功后,应当创建一个窗口用于显示接收到的视频。因为连接不一定创建成功,因此cv.destroyAllWindows()被放在一个try..catch块中防止出现错误。在接收数据过程中,我们使用payload_size记录当前从缓冲区读入的数据长度,这个长度通过struct.calcsize('L')来读取。使用该变量的意义在于缓冲区中读出的数据可能不足一个帧,也可能由多个帧构成。为了准确提取每一帧,我们用payload_size区分帧的边界。在从缓冲区读出的数据流长度超过payload_size时,剩余部分和下一次读出的数据流合并,不足payload_size时将合并下一次读取的数据流到当前帧中。在接收完完整的一帧后,显示在创建的窗口中。同时我们为窗口创建一个键盘响应,当按下Esc  q键时退出程序。

classVideo_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

   defrun(self):

        print("server starts...")

        self.sock.bind(self.ADDR)

        self.sock.listen(1)

        conn, addr = self.sock.accept()

        print("remote client successconnected...")

        data = "".encode("utf-8")

        payload_size = struct.calcsize("L")

        cv2.namedWindow('Remote', cv2.WINDOW_NORMAL)

        whileTrue:

            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.视频缩放和数据压缩

现在的服务器和客户端已经可以运行,你可以在代码中创建一个Client类实例和一个Server类实例,并将IP地址设为127.0.0.1,端口设为任意合法的(0-65535)且不冲突的值,版本设为IPv4。执行代码等同于自己和自己通信。如果网络状况不好,你也许会发现自己和自己的通信也有卡顿现象。为了使画面质量、延迟能够和现实网络状况相匹配,我们需要允许用户指定通信中画面的质量,同时我们的代码应当本身具有压缩数据的能力,以尽可能利用带宽。

当用户指定使用低画质通信,我们应当对原始数据做变换,最简单的方式即将捕获的每一帧按比例缩放,同时降低传输的帧速,在代码中体现为resize,该函数的第二个参数为缩放中心,后两个参数为缩放比例,并且根据用户指定的等级,不再传输捕获的每一帧,而是间隔几帧传输一帧。为了防止用户指定的画质过差,代码中限制了最坏情况下的缩放比例为0.3,最大帧间隔为3。此外,我们在发送每一帧的数据前使用zlib.compress对其压缩,尽量降低带宽负担。

classVideo_Client(threading.Thread):

   def__init__(self ,ip, port, level, version):

        threading.Thread.__init__(self)

        self.setDaemon(True)

        self.ADDR = (ip, port)

        if level <= 3:

            self.interval = level

        else:

            self.interval = 3

        self.fx = 1 /(self.interval + 1)

        if self.fx <0.3:

            self.fx = 0.3

        if version == 4:

            self.sock = socket(AF_INET,SOCK_STREAM)

        else:

            self.sock = socket(AF_INET6,SOCK_STREAM)

        self.cap = cv2.VideoCapture(0)

   def__del__(self) :

        self.sock.close()

        self.cap.release()

   defrun(self):

        print("VEDIO client starts...")

        whileTrue:

            try:

                self.sock.connect(self.ADDR)

                break

            except:

                time.sleep(3)

                continue

        print("VEDIO client connected...")

        whileself.cap.isOpened():

            ret, frame = self.cap.read()

            sframe = cv2.resize(frame, (0,0),fx=self.fx, fy=self.fx)

            data = pickle.dumps(sframe)

            zdata = zlib.compress(data,zlib.Z_BEST_COMPRESSION)

            try:

                self.sock.sendall(struct.pack("L", len(zdata))+ zdata)

            except:

                break

            for i inrange(self.interval):

                self.cap.read()

服务器端最终代码如下,增加了对接收到数据的解压缩处理。

classVideo_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

   defrun(self):

        print("VEDIO server starts...")

        self.sock.bind(self.ADDR)

        self.sock.listen(1)

        conn, addr = self.sock.accept()

        print("remote VEDIO client successconnected...")

        data = "".encode("utf-8")

        payload_size = struct.calcsize("L")

        cv2.namedWindow('Remote',cv2.WINDOW_NORMAL)

        whileTrue:

            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

5.加入音频的捕获和传输

在完成视频通信的基础上,整体框架对于音频通信可以直接挪用,只需要修改其中捕获视频/音频的代码和服务器解码播放的部分。这里我们使用 PyAudio 库处理音频,在 Linux 下你也可以选择 sounddevice。关于sounddevice这里不做过多介绍,你可以在这里看到它最新版本的文档。将vchat.py复制一份,重命名为achat.py,简单修改几处,最终音频捕获、传输的完整代码如下。我将上面代码中的ServerClient分别加上VideoAudio前缀以区分,同时显示给用户的print输出语句也做了一定修改,对于视频加上VIDEO前缀,音频加上AUDIO前缀。如果你对代码中使用到的 PyAudio 提供的库函数有所疑问,可以在这里找到相关的入门文档及示例。

classAudio_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)

        self.p = pyaudio.PyAudio()

        self.stream = None

   def__del__(self):

        self.sock.close()

        if self.stream isnotNone:

            self.stream.stop_stream()

            self.stream.close()

        self.p.terminate()

   defrun(self):

        print("AUDIO server starts...")

        self.sock.bind(self.ADDR)

        self.sock.listen(1)

        conn, addr = self.sock.accept()

        print("remote AUDIO client success connected...")

        data = "".encode("utf-8")

        payload_size = struct.calcsize("L")

        self.stream =self.p.open(format=FORMAT,

                                 channels=CHANNELS,

                                  rate=RATE,

                                  output=True,

                                 frames_per_buffer = CHUNK

                                  )

        whileTrue:

            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)

            frame_data = data[:msg_size]

            data = data[msg_size:]

            frames = pickle.loads(frame_data)

            for frame in frames:

                self.stream.write(frame, CHUNK)

 

classAudio_Client(threading.Thread):

   def__init__(self ,ip, port, version):

        threading.Thread.__init__(self)

        self.setDaemon(True)

        self.ADDR = (ip, port)

        if version == 4:

            self.sock = socket(AF_INET,SOCK_STREAM)

        else:

            self.sock = socket(AF_INET6,SOCK_STREAM)

        self.p = pyaudio.PyAudio()

        self.stream = None

   def__del__(self) :

        self.sock.close()

        if self.stream isnotNone:

            self.stream.stop_stream()

            self.stream.close()

        self.p.terminate()

   defrun(self):

       print("AUDIO client starts...")

        whileTrue:

            try:

                self.sock.connect(self.ADDR)

                break

            except:

                time.sleep(3)

                continue

        print("AUDIO client connected...")

        self.stream =self.p.open(format=FORMAT,

                             channels=CHANNELS,

                             rate=RATE,

                             input=True,

                            frames_per_buffer=CHUNK)

        whileself.stream.is_active():

            frames = []

            for i in range(0, int(RATE /CHUNK * RECORD_SECONDS)):

                data = self.stream.read(CHUNK)

                frames.append(data)

            senddata = pickle.dumps(frames)

           try:

                self.sock.sendall(struct.pack("L",len(senddata)) + senddata)

            except:

                break

6.编写程序入口 main.py

为了提供用户参数解析,代码使用了argparse。你可能对此前几个类中初始化方法的self.setDaemon(True)有疑惑。这个方法的调用使每个线程在主线程结束之后自动退出,保证程序不会出现崩溃且无法销毁的情况。在main.py中,我们通过每隔1s做一次线程的保活检查,如果视频/音频中出现阻塞/故障,主线程会终止。

import sys

import time

import argparse

from vchat importVideo_Server, Video_Client

from achat importAudio_Server, Audio_Client

 

parser = argparse.ArgumentParser()

 

parser.add_argument('--host', type=str,default='127.0.0.1')

parser.add_argument('--port', type=int,default=10087)

parser.add_argument('--level', type=int,default=1)

parser.add_argument('-v', '--version', type=int,default=4)

 

args = parser.parse_args()

 

IP = args.host

PORT = args.port

VERSION = args.version

LEVEL = args.level

 

if __name__ == '__main__':

   vclient = Video_Client(IP, PORT, LEVEL, VERSION)

   vserver = Video_Server(PORT, VERSION)

   aclient = Audio_Client(IP, PORT+1, VERSION)

   aserver = Audio_Server(PORT+1, VERSION)

   vclient.start()

   aclient.start()

   time.sleep(1)    # make delay to start server

   vserver.start()

   aserver.start()

   whileTrue:

        time.sleep(1)

        ifnotvserver.isAlive() ornotvclient.isAlive():

            print("Video connection lost...")

            sys.exit(0)

        ifnotaserver.isAlive() ornotaclient.isAlive():

            print("Audio connection lost...")

            sys.exit(0)

7.运行情况

因为实验楼的环境没有提供摄像头,因此我们需要修改一下代码,让程序从一个本地视频文件读取,模拟摄像头的访问。将Video_Clientself.cap =cv2.VideoCapture(0)改为self.cap =cv2.VideoCapture('test.mp4'),即从本地视频test.mp4中读取。在修改完你的代码后,你可以通过以下命令下载test.mp4(该视频文件是周杰伦《浪漫手机》的MV),并检验代码。

$ wgethttp://labfile.oss.aliyuncs.com/courses/671/test.mp4

$ python2 main.py

和上面命令一样,在本机可以通过 python2 main.py 来实验本机和本机的视频聊天,如果你有条件在同一局域网内的两台机器上实验,则可以将程序部署在两台机器上,并相互连接观察效果。下面两张图为本机上实验截图,有些情况下 PyAudio 可能会提示一些警告,你可以忽视它的提示。用户也可以指定level参数,level越高,画质越差,level0 为原始画面,在我们的main.py中默认level1

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

五、代码获取

你可以在我的 Github仓库 中获取到完整的代码,里面提供了Windows 版本和 Linux版本的配置、运行方案。如果你有建议或想法,欢迎提 PR 沟通。

 

你可能感兴趣的:(Linux基础)