Flask 是一個 Python 實現的 Web 開發微框架。這篇文章是一個講述如何用它實現傳送視訊資料流的詳細教程。
我敢肯定,現在你已經知道我在O’Reilly Media上釋出了有關Flask的一本書和一些視訊資料。在這些上面,Flask框架介紹的覆蓋面是相當完整的,出於某種原因,也有一小部分的功能沒有太多的提到,因此我認為在這裡寫一篇介紹它們的文章是一個好主意。
這篇文章是專門介紹流媒體的,這個有趣的功能讓Flask應用擁有這樣一種能力,以分割成小資料塊的方式,高效地為大型請求提供資料,這可能要花費較長的時間。為了說明這個主題,我將告訴你如何構建一個實時視訊流媒體伺服器!
什麼是流媒體?
流媒體是一種技術,其中,伺服器以資料塊的形式響應請求。我能想到一個原因來解釋為什麼這個技術可能是有用的:
- 非常大的響應 。對於非常大的響應而言,記憶體中收集的響應只返回給客戶端,這是很低效的。另一種方法是將響應寫入磁碟,然後使用
flask.send_file()
返回檔案,但是這增加了I/O的組合。假設資料可以分塊生成,以小塊資料的方式給請求提供響應是一種更好的解決方案。 - 實時資料 。對於一些應用,需要請求返回的資料來自實時資料來源。在這個方面一個非常好的例子就是提供一個實時視訊或音訊。很多安全攝像機使用這種技術將視訊資料流傳輸給Web瀏覽器。
使用Flask實現流式傳輸
Flask通過使用生成器函式對流式響應提供本機支援。生成器是一個特別的函式,它可以中斷和恢復。考慮一下下面的函式:
1 2 3 4 |
def gen(): yield 1 yield 2 yield 3 |
這是一個執行三步的函式,其中每步返回一個值。描述生成器如何實現超出了本文的範圍,但如果你有點好奇,下面的shell會話將給你說明生成器是如何被使用的:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
; html-script: false ]>>> 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使用生成器 函式這一特性來實現流式傳輸。
下面的例子說明了如何使用流式傳輸能夠產生大的資料表,而不必將整個表放入記憶體中:
1 2 3 4 5 6 7 8 9 10 11 12 |
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和生成器函式是如何一起工作的。返回流式響應的路由(route)需要返回一個由生成器函式初始化的Response
物件。Flask然後採取呼叫生成器,並以分塊的方式吧結果傳送給客戶端。
對於這個特殊的例子,如果你假設Stock.query.all()
返回的資料庫查詢結果是一個迭代器,那麼你能一次生成一個潛在大表的一行,因此無論查詢中的字元數量有多少,Python過程中的記憶體消耗不會因為較大的響應字串而越來越大。
多部分響應
上文提到了表的例子以小塊的形式生成一個傳統網頁,各個的部分連線成最後的結果。對於如何生成較大的響應這是一個很好的例子,但更令人激動的事情是處理實時資料。
使用流式傳輸的一個有趣的應用是使用每個塊來替換原來頁面中的地方,這能使流在瀏覽器視窗中形成動畫。利用這種技術,你可以讓流中每個資料塊成為一個影象,這給你提供了一個執行在瀏覽器中的很酷的視訊輸入訊號!
實現就地更新的祕密是使用多部分響應。多部分響應由一個報頭(header)和很多部分(parts)組成。報頭包括多部分中的一種內容型別,後面的部分由邊界標記分隔,每個部分中含有自身部分中的特定內容型別。
對於不同的需求,這裡有一些多部分內容型別。對於具有流式傳輸的,每個部分替換先前部分必須使用multipart/x-mixed-replace
內容型別。為了幫助你瞭解它到底是什麼樣子的,這裡有一個多部分視訊流傳輸的響應結構:
1 2 3 4 5 6 7 8 9 10 11 12 |
; html-script: false ]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
頭,但至少對影象瀏覽器而言,能夠處理沒有長度的流。
建立一個實時視訊流媒體伺服器
這篇文章中已經有足夠的理論,現在是時候來建立一個將實時視訊流式傳輸到Web瀏覽器的完整應用。
這裡有很多方法將視訊流式傳輸到瀏覽器,並且每個方法都有其優點和缺點。與Flask流特徵協同工作的一個好方法是流式傳輸獨立的JPEG圖片序列。這就是動態JPEG。這被用於許多IP監控攝像機。這種方法具有較短的延遲時間,但傳輸質量並不是最好的,因為對於動態影像而言,JPEG壓縮不是非常有效。
下面你可以看到一個非常簡單但完整的Web應用。它可以提供一個動態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'--framern' b'Content-Type: image/jpegrnrn' + frame + b'rn') @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
類來負責提供幀序列。在這個例子中,將camera控制部分放入一個單獨的模組是一個很好的主意。這樣,Web應用會保持乾淨、簡單和通用。
該應用有兩個路由(route)。/
路由為主頁服務,被定義在index.html
模板中。下面你能看到這個模板檔案中的內容:
1 2 3 4 5 6 7 8 9 |
; html-script: false ]<html> <head> <title>Video Streaming Demonstration</title> </head> <body> <h1>Video Streaming Demonstration</h1> <img src="{{ url_for('video_feed') }}"> </body> </html> |
這是一個簡單的HTML頁面,只含有一個標題和影象標籤。注意這個影象標籤的src
屬性指向這個應用的第二個路由,這就是魔法發生的地方。
/video_feed
路由返回流式響應。因為這個流返回要被展示在web頁面上的影象,在影象標籤的src
屬性中,URL指向這個路由。因為大多數/所有瀏覽器支援多部分響應(如果你找到一個不支援這個的瀏覽器,請告訴我),瀏覽器會通過顯示JPEG影象流自動保持影象元素的更新。
在/video_feed
路由中使用的生成器函式叫gen()
,將Camera
類的一個例項作為其引數。mimetype
引數設定如上所示,並具有multipart/x-mixed-replace
的內容型別和設為"frame"
的邊界字串。
gen()
函式進入一個迴圈,其中連續的從camera返回幀作為響應塊。如上所示,這個函式通過呼叫camera.get_frame()
方法要求camera提供幀,然後生成幀,使用image/jpeg
內容型別將該幀格式化為響應塊。
從攝像機獲取幀
現在,所有剩下的就是實現Camera
類,這必須連線攝像機硬體並從中下載實時視訊幀。將這個應用硬體相關部分封裝在一個類中的好處是,對於不同的人這個類可以有不同的實現,而應用的其他部分保持不變。你可以把這個類當做一個裝置驅動,不管實際使用中的硬體裝置而提供一個統一的實現。
從應用的其餘部分分離出Camera
類的另一個優點是,當實際上沒有攝像機時,很容易能騙過應用程式,讓它認為這裡有攝像機,因為camera類能被實現為模擬攝像機而無需真實硬體。事實上,當我執行這個應用時,最簡單的方式是測試流能做那些,而不需擔心硬體,直到我已經使其他部分都正確執行。下面,你可以看到我使用的簡單模擬攝像機實現:
1 2 3 4 5 6 7 8 |
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.jpg
、2.jpg
、3.jpg
,然後以每秒一幀的速率重複的依次返回。get_frame()
函式使用當前時間,以秒來確定在給定的時刻返回哪三個幀。很簡單吧?
要執行這個模擬攝像機,我需要建立三個幀。我使用gimp做了下面的影象:
因為攝像機是模擬的,你能在任何環境在執行這個應用!我將這個應用的所有檔案放在了GitHub。如果你熟悉git
,你可以使用下面的命令克隆它:
1 |
$ git clone https://github.com/miguelgrinberg/flask-video-streaming.git |
如果你喜歡下載它,你可以在這裡得到一個zip檔案。
你安裝好這個應用後,建立一個虛擬環境並在裡面安裝Flask。然後你就可以使用下面的命令執行這個應用:
1 |
$ python app.py |
當你在你的Web瀏覽器中輸入http://localhost:5000
啟動這個應用時,你會看到模擬視訊流一遍遍地播放影象1、2、3。很酷吧?
有一次,應用中的所有都在執行,我啟動了樹莓派及其攝像機模組,並實現了一個新的Camera
類來將樹莓派變成一個視訊流媒體伺服器,使用picamera
包來控制硬體。我不會在這裡討論這個camera類的實現,但你可以在原始碼中的camera_pi.py
檔案中找到。
如果你有一個樹莓派和一個攝像機模組,你可以編輯app.py
檔案從這個模組中匯入Camera
類,然後你就可以利用樹莓派實時傳輸視訊流,就像我在下面的截圖中所做的:
如果你想要讓這個流傳輸應用適用於不同的攝像機,那麼你要做的就是實現不同的Camera
類。如果你最終能寫一個並提供給我的Github上的專案,我將不勝感激。
流的限制
當Flask應用伺服器提供常規請求時,請求週期短。工作執行緒(web worker)接收請求,呼叫處理函式並最終返回響應。一旦響應被髮送回客戶端,工作執行緒是空閒的,並準備執行下一個請求。
當接收到一個使用流式傳輸的請求時,工作執行緒在整個流式傳輸的持續時間內繫結在一個客戶端上。當處理時間長而無止境的流時,比如來自攝像機的視訊流,工作執行緒將鎖定在一個客戶端直到該客戶端連線斷開。這實際上意味著,除非採取特殊手段,否則應用程式能服務的客戶端數量和工作執行緒是一樣的。當使用Flask應用的debug模式時,這意味著只有一個工作執行緒,因此你將無法同時連線兩個瀏覽器視窗來同時檢視來自兩個不同地方的資料流。
這裡有辦法克服這一重要的限制。在我看來,最好的解決方案是使用基於協程的Web伺服器,如gevent,Flask完全支援它。通過使用協程gevent能夠在一個工作執行緒上處理多個客戶端,因為gevent修改Python I/O函式來進行必要的上下文切換。
結論
如果你錯過了上面的內容,這篇文章中所包含的程式碼放在了這個GitHub庫中:https://github.com/miguelgrinberg/flask-video-streaming。在這裡,你可以找到一個通用的視訊流傳輸實現而不需要一個攝像機,並且還有一個樹莓派攝像頭模組實現。
我希望這篇文章闡述了一些有關流技術的話題。我關注於視訊流傳輸,因為這是一個我已有一些經驗的領域,但除了流媒體視訊之外,流傳輸技術還有很多其他的用途。例如,這種技術可以用來保持客戶端與伺服器之間較長時間的連線,允許伺服器推送新的資訊。這些日子,網路套接字協議是實現這個更有效的方式,但網路套接字是相當新的,只在現代瀏覽器中有效,而流傳輸技術能在你能想到的任何瀏覽器中運用。
如果你有任何問題,請隨時在下面寫出來。我會不斷記錄很多不為人所熟知的Flask話題,因此當有更多文章釋出時,我希望你在某種程度上能跟上我。我希望下一個看到你!
Miguel