[譯] 用 Flask 輸出視頻流

相信你已經知道,我和 O'Reilly Media 合做出版了 講解 Flask 的書籍和一些視頻。儘管這些書籍和視頻對 Flask 的講解已經足夠詳細了,但因爲某些緣由一小部分特性講的不夠多,所以我以爲把他們寫在這篇文章中是個好主意。html

本文專一於,一個有意思的特性,它讓 Flask 應用可以以分割成小塊的形式提供超大的響應,這可能要花一段較長的時間。爲了闡明這個主題,你將會看到如何構建一個實時視頻流服務器。前端

注意:如今有一篇關於本文的後續文章,Flask Video Streaming Revisited,我在後續文章中講了關於本文介紹的流服務器的一些改進。python

什麼是流?

流是一種讓服務器在響應請求時將響應數據分塊的技術。我能想到好多可能頗有用的理由:android

  • 超級巨大的響應數據。對於超大的響應數據來講,先把響應數據裝載到內存中,再返回給客戶端是很是低效的。另外一種方法是將響應數據寫入到磁盤中,而後用 flask.send_file() 將文件返回給客戶端,但這樣將會增長 I/O 操做。若是響應數據較小,這就是個好得多的方法,由於數據可以按塊進行存儲。
  • 實時數據。對於某些應用來講,也許須要向某個請求返回來自實時數據源的數據。一個很貼切的例子是實時視頻或音頻傳送。不少安全攝像頭用該技術將視頻以流的形式發送到服務器。

用 Flask 實現流

Flask 經過使用 生成器(generator functions) 原生支持流式響應。生成器是一個特殊的函數,能夠被停止或繼續運行。看看下面的函數:ios

def gen():
    yield 1
    yield 2
    yield 3
複製代碼

這是一個分三步運行的函數,每一步都返回一個值。生成器的實現超出了本文的範圍,若是你對此很感興趣的話,下面的 shell session 會讓你知道怎麼使用生成器:git

>>> x = gen()
>>> x
<generator object gen at 0x7f06f3059c30>
>>> x.next()
1
>>> x.next()
2
>>> x.next()
3
>>> x.next()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration
複製代碼

能夠看到,在這個簡單的例子中,一個生成器能夠依次返回多個值。Flask 使用生成器的該特性實現了流。github

下面的例子展現瞭如何在不把整個表都裝配到內存的狀況下,使用流生成巨型數據表:web

from flask import Response, render_template
from app.models import Stock

def generate_stock_table():
    yield render_template('stock_header.html')
    for stock in Stock.query.all():
        yield render_template('stock_row.html', stock=stock)
    yield render_template('stock_footer.html')

@app.route('/stock-table')
def stock_table():
    return Response(generate_stock_table())
複製代碼

在這個例子中你能夠看到 Flask 是如何使用生成器的。某個返回流式響應的路由須要返回一個入參爲生成器的 Response 對象。Flask 將會負責調用生成器,並把全部部分的結果以塊的形式發送給客戶端。shell

譯者注:python3 中,訪問 /stock-table 路由時,若是在 Debug 模式下看到 AttributeError: 'NoneType' object has no attribute 'app',則須要將 Response 的入參用 stream_with_context() 預處理。導入該函數:from flask import stream_with_context,路由的返回值:return Response( stream_with_context( generate_stock_table() ) )數據庫

對於這個特殊的例子,假設 Stock.query.all() 返回的是可迭代的數據庫查詢結果,那麼你能夠按每次一行的速度生成一個巨大的表,所以不管查詢結果中的元素數量有多少,該 Python 進程的內存佔用不會由於裝配巨大的響應字符串而變得愈來愈大。

分部響應

上述的表格示例生成小部分傳統頁面,再把全部部分銜接成最終的文檔。這是如何生成巨大響應的很好的示例,但更讓人興奮的事情是操做實時數據。

一種有趣的流的用法是讓每個數據塊取代頁面中的前一塊,這樣流就可以在瀏覽器窗口中進行「播放」或者動畫。使用該技術你可以用圖片做爲流的每一部分,這將帶來一個很酷的在瀏覽器中運行的視頻播放器。

實現原地更新的祕訣在於使用 multipart(分部) 響應。分部響應的內容是一個包含分部內容類型的頭部,後面的是用 boundary(分界線) 標記分割的部分,每一部分有各自的特定內容類型。

有若干個分部內容類型用於不一樣的用途。爲了達到讓流中的每部分可以替代前一部分的目的,內容類型必須用 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(Motion JPEG),這種方法正被一些 IP 安全攝像頭使用。這種方法的延遲低,可是質量並非最好,由於對於移動視頻來講,JPEG 的壓縮並不高效。

下面你將看到一個特別簡單但又十分完善的 web 應用,能夠提供移動的 JPEG 流:

#!/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)
複製代碼

這個應用導入了 Camera 類,該類負責提供幀序列。當前情形將攝像頭控制部分放在單獨的模塊中是很好的主意,這樣 web 應用就能保持代碼的整潔、簡單和通用性。

該應用有兩個路由。路由 / 提供定義在 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 頁面,只有一個 heading 和一個圖片標籤。注意圖片標籤的 src 屬性指向的是該應用的第二個路由,而這正是奇妙的地方。

路由 /video_feed 返回的是流式響應。由於流返回的是能夠顯示在網頁中的圖片,到該路由的 URL 就放在圖片標籤的 src 屬性中。瀏覽器會自動顯示流中的 JPEG 圖片,從而保持更新圖片元素,因爲分部響應受大多數(甚至全部)瀏覽器的支持(若是你找到一款瀏覽器沒有這種功能,請務必告訴我)。

/video_feed 路由中用到的生成器函數叫作 gen(),它接收 Camera 類的實例做爲參數。mimetype 參數的設置和上面同樣,是 multipart/x-mixed-replace 類型,邊界字符串設置爲 frame

gen() 函數進入循環,從而持續地將攝像頭中獲取的幀數據做爲響應塊返回。該函數經過調用 camera.get_frame() 方法從攝像頭中獲取一幀數據,而後它將這一幀之內容類型爲 image/jpeg 的響應塊形式產出(yield),如上所述。

從視頻攝像頭中獲取幀

如今剩下要作的只有實現 Camera 類了,它要可以鏈接到攝像機硬件,並從硬件中下載實時視頻幀。將應用的硬件依賴部分封裝到類中的好處是,這個類能夠針對不一樣人羣有不一樣的實現,但應用的其它部分保持不變。你能夠把這個類想象成設備驅動,不管實際使用的是什麼硬件,它都能提供統一的實現。

Camera 類從應用中分離出來的另外一個優點是很容易讓應用誤覺得相機是存在的,而實際狀況是相機並不存在,由於相機類能夠被實現成沒有真實硬件的模擬相機。實際上,在我製做這個應用的時候,對我來講最簡單的測試流的方式就是模擬相機,在我跑通其它部分前,不用考慮硬件問題。接下來你將看到我所使用的簡單模擬相機的實現:

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]
複製代碼

這種實現是從硬盤中讀取三張分別叫作 1.jpg2.jpg3.jpg 的圖片,而後以每秒一幀的速度循環返回它們。get_frame() 方法使用當前時間的秒數來決定當前應該返回三張圖片中的哪一張。很是簡單,不是嗎?

要運行這個模擬相機,我須要建立三個幀。我使用 gimp 作出了下面的圖片:

Frame 1
Frame 2
Frame 3

由於相機模擬出來了,這個應用能夠運行在任何環境中,所以你能夠當即運行它!我把這個應用的全部東西都準備好了,放在 GitHub 上。若是你熟悉 git,你能夠用下面的命令克隆這個倉庫:

$ git clone https://github.com/miguelgrinberg/flask-video-streaming.git
複製代碼

若是你要下載該應用,你能夠從 這兒 獲取一個 zip 壓縮文件。

裝好應用後,建立一個虛擬環境並安裝好 Flask。而後你能夠運行命令:

$ python app.py
複製代碼

當你開啓應用後,在瀏覽器中輸入 http://localhost:5000,你就能看到模擬的視頻流,不斷播放着圖片 一、二、3。是否是很酷?

當我作好了這些事情後,我用相機模塊啓動了樹莓派,並實現了一個新的 Camera 類,這個類將樹莓派轉換成一個視頻流服務器,使用 picamera 包來控制硬件。這裏不會涉及到相應的相機實現,但你能夠在文件 camera_pi.py 中找到相應的源代碼。

若是你有一個樹莓派和相機模塊,你能夠編輯 app.py 文件,從這個模塊中引入 Camera 類,而後你就能夠流式直播樹莓派的相機,就像在下面的截圖中我所作的那樣:

Frame 1

若是你想讓這個流式應用和不一樣的相機一塊兒使用,那麼你要作的就是改寫 Camera 類的實現。若是你實現了這樣的一個相機類,並將它貢獻到個人 GitHub 項目中,我將不勝感激。

流的侷限

Flask 應用在服務常規請求時,請求的週期短。web worker 接收到請求,調用處理函數,最終返回響應。一旦響應返回給了客戶端,worker 就處於空閒狀態,等待着接收下一次請求。

當接收到使用流的請求時,在流的持續時間內 worker 一直留存在客戶端中。在處理永不結束的、長的流時,好比從攝像機發來的一個視頻流,worker 將會對客戶端保持鎖定狀態,直到客戶端斷開鏈接。這也就意味着除非採用特殊的方法,不然有多少客戶端,應用就要爲多少 web workers 提供服務。在 debug 模式下運行 Flask 應用意味着只有一個線程,所以你沒法打開另外一個瀏覽器窗口,在兩個地方同時觀看流。

有不少方法能夠解決這個關鍵的限制。我認爲最好的方案是使用基於協程的 web 服務器,好比 Flask 支持很好的 gevent。gevent 經過使用協程可以在一個工做線程中處理多個客戶端,由於 gevent 修改了 Python I/O 函數,在必要時處理上下文的切換。

結論

若是你跳過了上面的內容,能夠在 GitHub 倉庫上看到本文相應的代碼:github.com/miguelgrinb…。你能從中找到不須要相機的視頻流通用實現,也能夠看到樹莓派相機模塊的實現。這篇 後續文章 講述了本文最開始發佈後我所作的一些改進。

我但願本文可以爲流這一話題帶來一些啓發。我專一於視頻流,由於我在這一領域中有些經驗,但流的應用不只限於視頻。好比,這個技術能夠用來保持服務器與客戶端的鏈接長時間有效,容許服務器在有信息時發送新信息。最近 Web Socket 協議能夠更高效的實現這個目的,可是 Web Socket 至關新穎,只能在現代瀏覽器中使用,而流卻能在很是多的瀏覽器中使用。

若是你有任何問題,請將它們寫在下方。我打算爲不爲大衆所知的 Flask 專題繼續撰寫文章,因此但願你能以某種方式聯繫我,以便知道更多文章發佈的時間,下篇文章中再見。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索