智慧家居------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,发送图片数据,我们可以看到主窗口和浏览器中的画面,都同步显示摄像头的图片了,而且这个图片是动态实时更新的,形成了视频播放的效果了。
任务完成,完美手工,后面再战。哈哈哈