流媒体原理基本介绍

流媒体是一种技术,其中,服务器以数据块的形式响应请求。

非常大的响应 。对于非常大的响应而言,内存中收集的响应只返回给客户端,这是很低效的。另一种方法是将响应写入磁盘,然后使用flask.send_file()返回文件,但是这增加了I/O的组合。假设数据可以分块生成,以小块数据的方式给请求提供响应是一种更好的解决方案。

流媒体服务器的原理介绍:
在这里插入图片描述
实时数据 。对于一些应用,需要请求返回的数据来自实时数据源。在这个方面一个非常好的例子就是提供一个实时视频或音频。很多安全摄像机使用这种技术将视频数据流传输给Web浏览器。

现实生活中,会使用物理设备直播采集进行直播分享:
在这里插入图片描述

yield 生成器函数

Flask 通过使用生成器函数对流式响应提供本机支持。生成器是一个特别的函数,它可以中断和恢复。

首先,如果你还没有对 yield 有个初步分认识,那么你先把 yield 看做 “return”,这个是直观的,它首先是个 return,普通的 return 是什么意思,就是在程序中返回某个值,返回之后程序就不再往下运行了。

看做 return 之后再把它看做一个是生成器(generator)的一部分(带 yield 的函数才是真正的迭代器),好了,如果你对这些不明白的话,那先把 yield 看做 return,然后直接看下面的程序,你就会明白 yield 的全部意思了:

def foo():
    print("starting...")
    while True:
        res = yield 4
        print("res:",res)
g = foo()
print(next(g))
print("*"*20)
print(next(g))

就这么简单的几行代码就让你明白什么是 yield,代码的输出这个:

starting...
4
********************
res: None
4

直接解释代码运行顺序,相当于代码单步调试:

  1. 程序开始执行以后,因为 foo 函数中有 yield 关键字,所以 foo 函数并不会真的执行,而是先得到一个生成器 g (相当于一个对象)。

  2. 直到调用 next 方法,foo 函数正式开始执行,先执行 foo 函数中的 print 方法,然后进入 while 循环。

  3. 程序遇到 yield 关键字,然后把 yield 想成return,return了一个 4 之后,程序停止,并没有执行赋值给 res 操作,此时 next(g) 语句执行完成,所以输出的前两行(第一个是while上面的print的结果,第二个是return出的结果)是执行- print(next(g)) 的结果。

  4. 程序执行 print("*"*20),输出20个*。

  5. 又开始执行下面的 print(next(g)),这个时候和上面那个差不多,不过不同的是,这个时候是从刚才那个 next 程序停止的地方开始执行的,也就是要执行 res 的赋值操作,这时候要注意,这个时候赋值操作的右边是没有值的(因为刚才那个是 return 出去了,并没有给赋值操作的左边传参数),所以这个时候 res 赋值是 None,所以接着下面的输出就是 res:None。

  6. 程序会继续在 while 里执行,又一次碰到yield,这个时候同样 return 出 4,然后程序停止,print 函数输出的 4 就是这次 return 出的 4。

到这里你可能就明白 yield 和 return 的关系和区别了,带 yield 的函数是一个生成器,而不是一个函数了,这个生成器有一个函数就是 next 函数,next 就相当于“下一步”生成哪个数,这一次的 next 开始的地方是接着上一次的 next 停止的地方执行的,所以调用 next 的时候,生成器并不会从 foo 函数的开始执行,只是接着上一步停止的地方开始,然后遇到 yield 后,return 出要生成的数,此步就结束。

使用 Flask 实现流式传输

使用流式传输的一个有趣的应用是使用每个块来替换原来页面中的地方,这能使流在浏览器窗口中形成动画。利用这种技术,你可以让流中每个数据块成为一个图像,这给你提供了一个运行在浏览器中的很酷的视频输入信号!

实现就地更新的秘密是使用多部分响应。多部分响应由一个报头(header)和很多部分(parts)组成。报头包括多部分中的一种内容类型,后面的部分由边界标记分隔,每个部分中含有自身部分中的特定内容类型。

对于不同的需求,这里有一些多部分内容类型。对于具有流式传输的,每个部分替换先前部分必须使用multipart/x-mixed-replace内容类型。为了帮助你了解它到底是什么样子的,这里有一个多部分视频流传输的响应结构:

HTTP/1.1 200 OK
Content-Type: multipart/x-mixed-replace; boundary=frame
 
--frame
Content-Type: image/jpeg
 
<jpeg data here>
--frame
Content-Type: image/jpeg
 
<jpeg data here>
...

正如你上面看到的,这个结构非常简单。主要的Content-Type头被设为multipart/x-mixed-replace,同时边界标记也被定义。然后每个部分中包括,有两个短横线的前缀,及这行上的边界字符串。

每个部分有自己的Content-Type头,并且每个部分可以可选地包括一个说明所在部分有效载荷的字节长度的Content-Length头,但至少对图像浏览器而言,能够处理没有长度的流。

视频流媒体服务器

有很多方法将视频流式传输到浏览器,并且每个方法都有其优点和缺点。与Flask流特征协同工作的一个好方法是流式传输独立的JPEG图片序列。这就是动态JPEG。这被用于许多IP监控摄像机。这种方法具有较短的延迟时间,但传输质量并不是最好的,因为对于动态影像而言,JPEG压缩不是非常有效。

下面你可以看到一个非常简单但完整的Web应用。它可以提供一个动态JPEG流传输:

from flask import Flask, render_template, Response
from camera import VideoCamera

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

def gen(camera):
    while True:
        frame = camera.get_frame()
        yield (b'--frame\r\nContent-Type: image/jpeg\r\n\r\n' + frame)

@app.route('/video_feed')
def video_feed():
    return Response(gen(VideoCamera()), mimetype='multipart/x-mixed-replace; boundary=frame')

if __name__ == '__main__':
    app.run(host='192.168.43.247', debug=True, threaded=True)

这个应用导入一个Camera类来负责提供帧序列。在这个例子中,将camera控制部分放入一个单独的模块是一个很好的主意。这样,Web应用会保持干净、简单和通用。

该应用有两个路由(route):

/路由为主页服务,被定义在index.html模板中:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>唤醒手腕</title>
</head>
<body>
    <img src="{{ url_for('video_feed') }}">
    <style>
        *{padding: 0; margin: 0}
        img{ width: 100vw; height: 100vh; }
    </style>
</body>
</html>

/video_feed路由返回流式响应。因为这个流返回要被展示在web页面上的图像,在图像标签的src属性中,URL指向这个路由。

因为大多数/所有浏览器支持多部分响应(如果你找到一个不支持这个的浏览器,请告诉我),浏览器会通过显示JPEG图像流自动保持图像元素的更新。

/video_feed路由中使用的生成器函数叫gen(),将Camera类的一个实例作为其参数。mimetype参数设置如上所示,并具有multipart/x-mixed-replace的内容类型和设为"frame"的边界字符串。

gen()函数进入一个循环,其中连续的从camera返回帧作为响应块。如上所示,这个函数通过调用camera.get_frame()方法要求camera提供帧,然后生成帧,使用image/jpeg内容类型将该帧格式化为响应块。

import cv2

class VideoCamera(object):
    def __init__(self):
        # Using OpenCV to capture from device 0. If you have trouble capturing
        # from a webcam, comment the line below out and use a video file
        # instead.
        self.video = cv2.VideoCapture(0)
        # If you decide to use video.mp4, you must have this file in the folder
        # as the main.py.
        # self.video = cv2.VideoCapture('video.mp4')
    
    def __del__(self):
        self.video.release()
    
    def get_frame(self):
        success, image = self.video.read()
        image = cv2.flip(image, 1)
        # We are using Motion JPEG, but OpenCV defaults to capture raw images,
        # so we must encode it into JPEG in order to correctly display the
        # video stream.
        ret, jpeg = cv2.imencode('.jpg', image)
        return jpeg.tobytes()

实现Camera类,这必须连接摄像机硬件并从中下载实时视频帧。将这个应用硬件相关部分封装在一个类中的好处是,对于不同的人这个类可以有不同的实现,而应用的其他部分保持不变。你可以把这个类当做一个设备驱动,不管实际使用中的硬件设备而提供一个统一的实现。

然后运行项目:这边我不是采用是系统摄像头,是外设摄像头捕捉的画面
在这里插入图片描述

web 实时呈现桌面录屏

想实现录屏的功能,就是捕捉屏幕的画面:

我们控制鼠标的操作,不能盲目的进行,所以我们需要监控屏幕上的内容,从而决定要不要进行对应的操作, pyautogui 提供了一个方法screenshot(),可以返回一个Pillow的image对象;

im = pyautogui.screenshot()
im.save('屏幕截图.png')

所以既然是录屏的模式,我们就不需要Camera类了,直接在main.py进行修改,展示如下:

import pyautogui
from flask import Flask, render_template, Response
import io

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

def gen():
    while True:
        screenShotImg = pyautogui.screenshot()

        imgByteArr = io.BytesIO()
        screenShotImg.save(imgByteArr, format='JPEG')
        imgByteArr = imgByteArr.getvalue()
        frame = imgByteArr
        yield (b'--frame\r\n Content-Type: image/jpeg\r\n\r\n' + frame)

@app.route('/video_feed')
def video_feed():
    return Response(gen(), mimetype='multipart/x-mixed-replace; boundary=frame')

if __name__ == '__main__':
    app.run(host='192.168.43.247', debug=True, threaded=True)

那么这边的话,我通过pyautogui捕捉我主屏幕的内容,我把页面到我的扩展屏打开,展示如下所示:
在这里插入图片描述
当然用过直播软件的大家都知道,那种究极嵌套的画面,就是录屏的时候,在同一屏幕下展示录屏的画面,就会出现究极嵌套,当然这边也是一样。展示如下:
在这里插入图片描述

通过 web 远程控制桌面

想法是这样的,那么借助js我们可以获取鼠标在网页的位置,那么将网页呈现的桌面画面与实际桌面相称,进行从而远程控制。

$(document).click(
    function (event) {
        event = event || window.event;
        var x = event.offsetX || event.originalEvent.layerX;
        var y = event.offsetY || event.originalEvent.layerY;
        var x_rate = x / document.body.clientWidth;
        var y_rate = y / document.body.clientHeight;
    }
);

如上的Javascript代码就是获取鼠标点击网页页面时候,鼠标位于网页页面的位置坐标(坐标百分比表示),那么我们可以把这个坐标的值转发到服务器端,通过pyautogui进行对应的操作即可。

发送的时候,采用Ajax发送模式,就是不刷新网页就进行发送数据:

function sendPointerPosition(xrate, yrate) {
    $.ajax({
        url: "/pointer?xrate=" + xrate + "&yrate=" + yrate,
        type: "get",
        success: function (data) {
            console.log(data)
        },
        error: function (error) {
            alert(error)
        }
    })
}

客户端页面的代码完整展示:

<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="http://libs.baidu.com/jquery/2.0.0/jquery.js"></script>
    <title>唤醒手腕</title>
</head>
<body>
<img src="{{ url_for('video_feed') }}">
<style>
    * { padding: 0; margin: 0 }
    img { width: 100%; }
</style>
<script>
    $(document).click(
        function (event) {
            event = event || window.event;
            var x = event.offsetX || event.originalEvent.layerX;
            var y = event.offsetY || event.originalEvent.layerY;
            var x_rate = x / document.body.clientWidth;
            var y_rate = y / document.body.clientHeight;
            sendPointerPosition(x_rate, y_rate)
        }
    );

    function sendPointerPosition(xrate, yrate) {
        $.ajax({
            url: "/pointer?xrate=" + xrate + "&yrate=" + yrate,
            type: "get",
            success: function (data) {
                console.log(data)
            },
            error: function (error) {
                alert(error)
            }
        })
    }

</script>
</body>
</html>

服务器要对接收到的坐标数据进行执行的操作,展示如下所示:

import pyautogui
from flask import Flask, render_template, Response, request
import io

app = Flask(__name__)


@app.route('/')
def index():
    return render_template('index.html')


@app.route('/pointer')
def pointer():
    x = int(float(request.args["xrate"]) * 1920)
    y = int(float(request.args["yrate"]) * 1080)
    # 执行点击操作
    pyautogui.click(x, y)
    return "success"
	

def gen():
    while True:
        screenShotImg = pyautogui.screenshot()

        imgByteArr = io.BytesIO()
        screenShotImg.save(imgByteArr, format='JPEG')
        imgByteArr = imgByteArr.getvalue()
        frame = imgByteArr
        yield (b'--frame\r\n Content-Type: image/jpeg\r\n\r\n' + frame)


@app.route('/video_feed')
def video_feed():
    return Response(gen(), mimetype='multipart/x-mixed-replace; boundary=frame')


if __name__ == '__main__':
    app.run(host='192.168.43.247', debug=True, threaded=True)

运行测试展示如下所示:
在这里插入图片描述
如上图我们进行点击网页呈现的画面中的菜单位置:实在上就是相对于真实桌面的对应位置的比例传给服务器端,然后让服务端的pyautogui进行点击的操作。

真实桌面产生了反映,展示如下所示:
在这里插入图片描述
真实桌面的菜单栏被弹起了。

总结:我们采用的同一台电脑上测试,其实建议用另一台笔记本进行操作,这样的话效果会更加的明显。当前这仅仅是单击事件而已,如果加强功能,也可以增加双击,或者长按拖拽的事件。

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐