智慧家居——Flask网页视频服务器

智慧家居------Flask网页视频服务器

智慧家居是一个比较热门的物联网应用,而摄像头视频服务则是智能家居中的重头戏。

ESP32cam是一块带摄像头的开发板,受到很多人都喜欢。不管是Arduino IDE官方提供的C语言的程序,还是使用microdot驱动库的Micropython程序,一般的都是把ESP32cam做成服务器,然后用户使用PC或手机的浏览器访问ESP32cam,获取摄像头的视频流。

但是这种把ESP32cam做成服务器的运行模式也有一定的缺陷,或许我们更需要把它做成客服端的运行模式。

我们的解决方案是采用瘦客户机(一台低配的PC电脑)作为服务器,把各种居家设备接入到服务器中,并在服务器中安装一个Flask服务器,作为各种设备的协调和数据共享平台。

如图所示,这个Flask服务器平台可以实现多个设备的视频服务。可以把若干个ESP32cam摄像头作为客户端接入,成为视频的数据的采集输入设备;WebServer网页服务是视频数据的浏览播放窗口,用户可以通过浏览器查看每个摄像头的视频,成为视频数据的输出。这样我们只要通过端口映射,就可以把家里的所有设备推到互联网中,也就是实现设备上云端,从而实现真正复杂、系统的物联网智能家居服务。

一 制作一个Flask网页服务器

我们先从简单的开始,用Python做一个Flask服务器程序,做一个静态图片浏览的网页服务。程序包含两个部分:程序开始时,读取本地文件中的一张图片,并显示在程序的主窗口中(为了简化程序,主窗口中仅呈现一个图片显示框的内容);程序同时开启网页服务,用户可以使用浏览器访问这个Flask服务器,查看到这张图片(为了简化程序,网页中也仅仅出现一个图片显示控件)。

我们先准备一张图片,保存在电脑的文件夹中,命名为image.jpg。

接着我们打开IDLE(python 3.X),这是Python的编程环境,在这里创建一个Flask网页服务器的程序。程序开始时,会在main()主函数中首先读取这张图片,成为全局的共享数据image_Dtat,供主程序和网页服务这两个功能块调用。

复制代码
from flask import Flask, Response, request  # 提供网页服务
import io                                   # 提供内存操作
import tkinter as tk                        # 创建程序主窗口
from PIL import Image, ImageTk
import threading                            # 创建子进程操作
import time

fapp = Flask(__name__)        # 网页服务实例
server_thread = None          # 网页服务子进程 
image_Data = bytearray()      # 图片的全局共享数据



class create_window:
    def __init__(self):  # 创建主窗口 在主窗口中显示图片
        self.root = tk.Tk()
        self.root.title("视频服务")
        self.root.geometry("400x300")

        self.image_label = tk.Label(self.root)
        self.image_label.pack(pady=20)

        global image_Data   # 读取图片的全局共享数据,更新主窗口的图片显示
        image_stream = io.BytesIO(image_Data)           # 把字节数组转为字节流
        pil_image = Image.open(image_stream)            # 把字节流转为image对象
        tk_image = ImageTk.PhotoImage(pil_image)        # 把image对象转为photo对象
        self.image_label.image = tk_image               # 把photo对象赋值显示出来
        self.image_label.config(image=tk_image)
        


    def run(self): 
        self.root.mainloop() # 让主窗口运行起来

        # 主窗口在这里进入一个无限循环,用于响应用户的所有操作,独占程序的主进程
        

    

def start_server():          # 在子线程中创建网页服务  在网页中显示图片
    
    def generate_frames():   
        global image_Data
        while True:         # 图片文件比较大,需要利用yield把图片数据分段发送给用户端
            yield b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + image_Data + b'\r\n'


    @fapp.route('/')    # 这个是网页服务的主页,用户流量网页服务器的网址时,会显示一张图片
    def index():
        return '''<html><head><title>视频流演示</title></head>
     <body><img src="/video_feed" width="320" height="240" /></body></html>'''

    @fapp.route('/video_feed')  # 这个就是嵌套在主页中的图片,图片数据时采用分段发送的
    def video_feed():
        return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')


    fapp.run(host='0.0.0.0', port=5000, threaded=True)

    # 开启flask网页服务器,用户可以浏览访问这个网页服务器,网站是本地计算机内网IP + 端口号

    # 比如 http://192.168.X.X:5000 ,这时候访问的手机和运行服务器的PC必须在同一网络,也就是连接同一个路由器

    # 这个flask网页服务器一经启动 run,会独占一个无限循环,所以必须运行在一个独立的子进程中


    

def main():                  # 这里是整个程序的主函数
    global image_Data        # 指向全局变量,用于存储图片数据,提供给全局的所有功能块共享
    with open('D:\\daling\\image.jpg', 'rb') as file:
        image_Data = file.read()  #从本地文件中读取一张图片的数据

    global server_thread  # 创建一个网页服务的子线程
    server_thread = threading.Thread(target=start_server, daemon=True)
    server_thread.start()

    app = create_window()  # 显示程序主窗口
    app.run()              # 把程序的主进程赋予主窗口

if __name__ == '__main__':
    main()

这里需要说明的是,这个程序采用多线程的处理方式,程序的主线程用于主窗口,主窗口运行在PC计算机中。这条主线程运行在self.root.mainloop() 的无限循环中,以后我们还可以往主窗口正增加一些功能按钮、数据显示标签、设备列表等内容,方便与用户实现交互操作,处理协调更多的设备、数据、运算。

我们又开启一条新的线程,用于实现Flask服务器,这个服务器的主网页是一个<img>图片标签,这个图片标签指向一个图片的路径"/video_feed",而这个图片路径又再次链接到一个包含yield构造器的frame。yield是python提供的自动处理数据流的驱动库,我们可以从程序运行时反馈回来的消息看出,这张image.jpg的图片数据共享数据image_Data,被自动切割成了若干个数据块,然后依次传递给客户端的浏览器。这样的切片分割发送,在处理视频流、大文件是非常便捷和高效的。

(避坑提醒:需要再次注意的是,在while循环中,每次运行yield的时候,都需要一个小的延时time_sleep(0.03),这样程序就可以在这个延时中释放计算机资源,用于处理其他的代码。我在后面做视频服务的时候,开始是没有加这个延时的,结果视频画面非常卡顿,差不多2秒才更新一帧图片;后来增加了这个延时释放后,差不多每秒可以更新2---3帧的视频了,效率提升了五六倍,视频画面也流畅多了)

二.制作ESP32cam视频采集端

我们准备一块ESP32cam摄像头开发板,用CH340的串口烧写器连接到电脑(注意不要使用ESP32cam的底座,我发现很多很多的底座,在arduino IDE中可以使用,但是在Micropython中都无法正常使用的),如图所示,点击Thonny的右下角选择串口设备,窗口中出现这样的信息,这表示设备连接成功,这样就可以把程序代码写入到ESP32cam开发板中了。

复制代码
import camera
import time
import network
import urequests as requests
import gc

def wifiConnect():
    wlan = network.WLAN(network.STA_IF)
    wlan.active(True)
     
    if not wlan.isconnected():
        print('connecting to network...')
        wlan.connect("TP-LINK_8080","123456789")
        while not wlan.isconnected():
            pass
    print('网络配置:', wlan.ifconfig())

# 初始化摄像头
def init_camera():
    try:
        # 配置摄像头参数
        camera.init(0, format=camera.JPEG, framesize=camera.FRAME_HQVGA)
        # 设置其他参数
        camera.quality(10)  # 质量 0-63 数值越小质量越高
        camera.contrast(2)  # 对比度
        camera.brightness(2)  # 亮度
        camera.saturation(2)  # 饱和度
        camera.flip(0)  # 翻转
        camera.mirror(1)  # 镜像
        print("摄像头初始化成功")
        return True
    except Exception as e:
        print(f"摄像头初始化失败: {e}")
        return False


# 主程序
def main():
    wifiConnect()
    time.sleep(1)
    if not init_camera():
        return
    
    while True:
        time.sleep(0.1)
        try:
            buf = camera.capture()
            if buf is not None:
                r = requests.post('http://192.168.0.110:5050/updata', data = buf)
                r.close
                del buf           # 释放内存资源
                gc.collect()
                
        except Exception as e:
            print(f"拍照过程中出现错误: {e}")
            break

    
    # 释放摄像头资源
    camera.deinit()
    print("程序结束")

# 程序入口
if __name__ == "__main__":
    main()

程序代码说明,在程序的主函数main()中,首先是WiFi连接、然后是摄像头初始化、最后进入到主循环while(True)中。在主循环中,不断地读取摄像头的数据,并连接Flash服务器的"/updata"网页,把图片数据一帧一帧地发送到服务器中。

这里需要提醒的是,在每一次发送数据完成后,需要对ESP32cam内存中的图片数据进行清除,否则发送不了几张,就会出现内存溢出的运行错误。(我最开始也是没有做数据清除、内存回收的操作,开始的几帧图片运行的很顺利,然后就出现程序运行错误,程序卡死了)

三.制作Flask视频服务

Flask视频服务器包括三个功能块配合来实现的:

(1)视频上传网页"/updata"。 这个是用于负责接收来自ESP32cam摄像头的图片数据,并存入到全局共享数据image_Data中。为了让程序运行更加流畅,我有开辟了一条新的线程,专门运行这个视频上传网页,让视频接收独占一条线程。

(2)主窗口的图片自动更新。 我们在主窗口中增加一个图片更新的函数getimage(),在这个函数中,我们读取图片共享数据image_Data,并把这个数据转化为图片对象,在标签Label中显示。奇妙的是我们给这个函数增加的一个回调代码(self.root.after(100, self.getimage)),让这个函数每个100毫秒就回调一次自己,这样就形成了一个自动封闭的循环,程序不断地读取图片共享数据,不断地显示,也就是当共享数据image_Data发生变化时,这个程序能第一时间感觉到,并更新到主窗口中显示出来。

(3)网站主页中的图片更新。网站注意采用yield构造体,通过不断地调用这个构造体,也能把共享图片数据image_Data的内容,第一时间传递到用户的浏览器中了。

复制代码
from flask import Flask, Response, request  # 提供网页服务
import io                                   # 提供内存操作
import tkinter as tk                        # 创建程序主窗口
from PIL import Image, ImageTk
import threading                            # 创建子进程操作
import time

fapp = Flask(__name__)        # 网页服务实例
uapp = Flask(__name__)        # 网页服务实例
image_Data = bytearray()      # 图片的全局共享数据





class create_window:
    def __init__(self):  # 创建主窗口 在主窗口中显示图片
        self.root = tk.Tk()
        self.root.title("视频服务")
        self.root.geometry("400x300")

        self.image_label = tk.Label(self.root)
        self.image_label.pack(pady=20)

        self.getimage()   # 这是一个图片自动更新的操作函数
        
    def run(self): 
        self.root.mainloop() # 让主窗口运行起来

        # 主窗口在这里进入一个无限循环,用于响应用户的所有操作,独占程序的主进程

    def getimage(self):
        global image_Data
        try:      # 读取图片的全局共享数据,更新主窗口的图片显示
            image_stream = io.BytesIO(image_Data)           # 把字节数组转为字节流
            pil_image = Image.open(image_stream)            # 把字节流转为image对象
            tk_image = ImageTk.PhotoImage(pil_image)        # 把image对象转为photo对象
            self.image_label.image = tk_image               # 把photo对象赋值显示出来
            self.image_label.config(image=tk_image)
        
        except Exception as e:
            pass

        self.root.after(100, self.getimage)  # 主程序每隔100毫秒  就运行一次本函数

        # 这是一个巧妙的回调,当图片的全局共享数据发生变化,图片显示也会跟着变化

        # 达到了图片自动更新的功能,甚至可以实现视频播放的效果

        

    

def start_server():          # 在子线程中创建网页服务  在网页中显示图片
    
    def generate_frames():   
        global image_Data
        while True:         # 图片文件比较大,需要利用yield把图片数据分段发送给用户端
            yield b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + image_Data + b'\r\n'
            time.sleep(0.03)


    @fapp.route('/')    # 这个是网页服务的主页,用户流量网页服务器的网址时,会显示一张图片
    def index():
        return '''<html><head><title>视频流演示</title></head>
     <body><img src="/video_feed" width="600" height="450" /></body></html>'''

    @fapp.route('/video_feed')  # 这个就是嵌套在主页中的图片,图片数据时采用分段发送的
    def video_feed():
        return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')

    fapp.run(host='0.0.0.0', port=5000, threaded=True)

    # 开启flask网页服务器,用户可以浏览访问这个网页服务器,网站是本地计算机内网IP + 端口号

    # 比如 http://192.168.X.X:5000 ,这时候访问的手机和运行服务器的PC必须在同一网络,也就是连接同一个路由器

    # 这个flask网页服务器一经启动 run,会独占一个无限循环,所以必须运行在一个独立的子进程中


def start_updata():          # 在子线程中创建接收摄像头数据的服务

    @uapp.route("/updata", methods=["POST"])
    def updata():
        buf = bytearray()
        buf = request.get_data()  #直接接收二进制
        global image_Data
        image_Data = bytearray()
        image_Data.extend(buf)
        return "over"

    uapp.run(host='0.0.0.0', port=5050, threaded=True)


    

def main():                  # 这里是整个程序的主函数
    global image_Data        # 指向全局变量,用于存储图片数据,提供给全局的所有功能块共享
    with open('D:\\daling\\image.jpg', 'rb') as file:
        image_Data = file.read()  #从本地文件中读取一张图片的数据

      # 创建一个网页服务的子线程
    server_thread = threading.Thread(target=start_server, daemon=True)
    server_thread.start()

      # 创建一个网页服务的子线程
    updata_thread = threading.Thread(target=start_updata, daemon=True)
    updata_thread.start()

    wapp = create_window()  # 显示程序主窗口
    wapp.run()              # 把程序的主进程赋予主窗口

if __name__ == '__main__':
    main()

程序运行效果:(1)在Python中运行程序,可以看到程序运行起来了,主窗口显示了一张本地图片;(2)打开电脑浏览器(或者手机浏览器),访问Flask服务器,可以看到同样一张图片;(3)打开Thonny,插入ESP32cam摄像头,连接设备,运行设备中的main.py程序;(4)摄像头开始连接WiFi,发送图片数据,我们可以看到主窗口和浏览器中的画面,都同步显示摄像头的图片了,而且这个图片是动态实时更新的,形成了视频播放的效果了。

任务完成,完美手工,后面再战。哈哈哈

相关推荐
FIT2CLOUD飞致云4 小时前
赛道第一!1Panel成功入选Gitee 2025年度开源项目
服务器·ai·开源·1panel
yanlou2334 小时前
[C++/Linux HTTP项目] HTTP服务器基于muduo高性能服务器搭载【深入详解】
运维·服务器·http·muduo库·http高性能服务器
天空属于哈夫克34 小时前
企微第三方 RPA API:非官方接口与官方接口的差异解析及选型建议
运维·服务器
niceffking4 小时前
linux 信号内核模型
linux·运维·服务器
CoLiuRs4 小时前
Image-to-3D — 让 2D 图片跃然立体*
python·3d·flask
残梦53144 小时前
Qt6.9.1起一个图片服务器(支持前端跨域请求,不支持上传,可扩展)
运维·服务器·开发语言·c++·qt
醒过来摸鱼4 小时前
Redis 服务器线程与事件循环解析
服务器·数据库·redis
hweiyu004 小时前
Linux 命令:paste
linux·运维·服务器
移动云开发者联盟5 小时前
一键部署!移动云全面上线Clawdbot
运维·服务器·负载均衡
upc8865 小时前
linux修改文件权限
linux·运维·服务器