使用Flask搭建一個流媒體服務器

摘要

收到前不久訂閱的PythonWeekly發過來的一個郵件通知,由Miguel寫的一篇介紹如何使用Flask搭建一個流媒體服務器的文章,思路很新穎也很有意思。你可以點擊這裏閱讀英文原文。或者跟隨本文跟我一起體驗一把搭建一個流媒體服務器的過程吧。

理論基礎

流媒體有兩大特點,一是數據量大。二是有實時性要求。針對這兩個特點,我們必須把應答數據分塊傳輸給客戶端來實現流媒體服務器。這裏我們用到了兩個關鍵技術來實現流媒體服務器,我們使用生成器函數來把數據分塊傳送,Flask的Response類本身對生成器函數有良好的支持。接着,我們使用Multipart來組裝一個HTTP應答。

生成器函數

生成器函數是可被打斷和恢復的函數。其關鍵字是yield,來看一個例子:

def gen():
    yield 1
    yield 2
    yield 3

上面的代碼我們就定義了一個生成器函數,當生成器函數被調用時,它返回一個生成器迭代器,或直接叫生成器。通過不斷地調用生成器的next()方法來執行生成器函數體的代碼,直到遇到異常爲止。

>>> g = gen()
>>> g
<generator object gen at 0xb72330a4>
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
>>> g.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

從上面的例子可以看到一個生成器函數可以返回多個結果。每當程序執行到yield語句時,函數現場會被保留,同時返回一個值。Flask就是利用這個特性把應答數據通過生成器分塊發送給客戶端。

Multipart應答

Multipart應答包含一個multipart媒體類型,後面跟着多塊獨立的數據,每塊數據有自己的Content-Type,每塊數據之間通過boundary分隔。下面是一個例子:

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>
...

Multipart有多種不同的類型,針對流媒體,我們使用multipart/x-mixed-replace。瀏覽器處理這種Multipart類型時,會使用當前的塊數據替換之前的塊數據。這剛好就是我們想要的流媒體的效果。我們可以把媒體的一幀數據打包爲一個數據塊,每塊數據有自己的Content-Type和可選的Content-Length。瀏覽器逐幀替換,就實現了視頻的播放功能。RFC1341對Multipart媒體類型進行了詳細的描述,有興趣的朋友可移步參考。

實現流媒體服務器

上面介紹了實現流媒體服務器的理論知識。接下來我們使用這些知識來用Flask搭建一個流媒體服務器。

有多種方法可以在瀏覽器裏實現流媒體播放,和Flask配合較好的是使用Motion JPEG的方法。簡單地講,就是把視頻畫面通過JPEG圖片的方式,一幀一幀地發送給瀏覽器。這也是很多IP Camera使用的流媒體播放方式,它實時性很好,但視頻效果不是很理想。因爲Motion JPEG對視頻的壓縮效率太低了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python
from flask import Flask, render_template, Response
from camera import Camera

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\n'
               b'Content-Type: image/jpeg\r\n\r\n' + frame + b'\r\n')

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

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

這個Flask應用程序導入了一個Camera類,這個類是爲了持續不斷地提供視頻的幀數據的類。這個程序提供了兩個服務路徑,/路徑由index.html模板提供服務,下面是它的內容:

<html>
  <head>
    <title>Video Streaming Demonstration</title>
  </head>
  <body>
    <h1>Video Streaming Demonstration</h1>
    <img src="{{ url_for('video_feed') }}">
  </body>
</html>

這是一個非常簡單的HTML網頁。其中關鍵的是img這個標籤,它定義了一張圖片元素,其URL是/video_feed。從Flask應用程序代碼的Line17-20可以知道,/video_feed是由一個video_feed()方法提供服務的,它返回的是一個multipart應答。這個應答的內容是由生成器函數gen()提供的。而gen()函數就是不停地從camera裏獲取一幀一幀的圖片,並通過生成器返回給客戶端。客戶端瀏覽器在收到這個流媒體時,會在img標籤定義的圖片裏,逐幀地顯示圖片,這樣一個視頻就播放出來的。目前市面上絕大部分瀏覽器都支持這個功能。

模擬視頻幀數據

現在只要實現Camera類,並提供源源不斷的視頻幀數據即可運行上面的程序了。由於連接攝像頭涉及到硬件,我們使用一個簡單的模擬器來源源不斷地返回數據:

1
2
3
4
5
6
7
8
9
#!/usr/bin/env python
from time import time

class Camera(object):
    def __init__(self):
        self.frames = [open(f + '.jpg', 'rb').read() for f in ['1', '2', '3']]

    def get_frame(self):
        return self.frames[int(time()) % 3]

這個代碼很簡單,它從本地讀取三個圖片,並根據當前時間,每秒返回不同的圖片來模擬提供源源不斷的視頻幀數據。

大家可以從原作者的GitHub上下載程序的代碼來運行。

$ git clone https://github.com/miguelgrinberg/flask-video-streaming.git

或者直接下載ZIP包來運行。

下載完代碼,進入代碼根目錄,執行python app.py。然後在瀏覽器裏打開http://localhost:5000即可以看到模擬的視頻了。

安裝Flask

要運行上述代碼,需要先安裝Flask。官網上有教程,簡單易懂。

連接硬件攝像頭

下載代碼的同學應該可以看到代碼裏還有一個camera_pi.py的文件,這個是用來實現真正的連接硬件攝像頭的代碼。原文作者使用的攝像頭是Raspberry Pi,這是個類似Arduino的開源的硬件項目。

一些限制

當客戶端瀏覽器打開上述流媒體服務的網址時,它就獨佔了這個線程。在把Flask應用Deploy到Nginx+uwsgi服務器上時,它能服務的最大客戶端數目爲應用程序的線程數,一般就是幾個到幾十個。而如果是在本機使用python app.py運行的測試服務器,則只能服務一個客戶端。

針對這個問題,原文作者提供了一個解決方案。使用gevent來解決。

gevent is a coroutine-based Python networking library that uses greenlet to provide a high-level synchronous API on top of the libev event loop.

有興趣的同學可以在原代碼的基礎上,引入gevent來支持多客戶端。


發佈了29 篇原創文章 · 獲贊 8 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章