摘要
收到前不久訂閱的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來支持多客戶端。