カメラなどで使用されているRTSPストリーミングは、そのままではブラウザで再生することはできません。
昔は Adobe FlashPlayer を使って再生していましたが、セキュリティの関係で利用されなくなり、
最近では HLS(HTML Live Streaming)や MPEG-DASH での配信が主流となっています。
そこで、できるだけシンプル・簡単に RTSP のストリーミング再生をブラウザで行えるようにする
WebSocket と HTML Canvas を使って実現する方法をご紹介いたします。
システム構成
RTSP配信機器
- RTSPでネゴシエーションし、RTPで動画ストリームを配信する機器
- 代表的なものとしては、IPカメラなど
WebSocketサーバ
- RTSP配信機器とRTSPで接続
- RTPで受信した動画フレームをWebSocketで再配信(リアルタイム)
ブラウザ
- WebSocketサーバと接続
- 受信した動画フレームをHTML Canvasに描画
動作環境
RTSP配信機器は市販の製品、ブラウザはChromeを使用しましたが特に指定なし。
ここでは、パッケージ類が必要となるWebSocketサーバの環境について記載します。
Python3 | 3.10.4 |
opencv-python | 4.6.0.66 |
pillow | 9.2.0 |
numpy | 1.23.1 |
websockets | 10.3 |
WebSocketサーバ
RTSPストリームの受像
RTSP配信機器への接続、RTPポートのネゴシエーション、受信ストリームのデコードは、
基本的に OpenCV を使って行います。OpenCV を使うと、以下のように非常にシンプルに書けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import cv2 import sys cap = cv2.VideoCapture(r'rtsp://127.0.0.1:554/mpeg4') if cap.isOpened() == False: print("cannot connect RTSP server.") sys.exit(1) while cap.isOpened(): ret, frame = cap.read() if ret == False: continue cv2.imshow('frame', frame) |
WebSocketでリアルタイム再配信
OpenCV で受像した1フレームのデータは、BGRA のフォーマットで格納されています。
このままだとブラウザで扱いにくいため、RGBA に変換するか Jpeg 画像のデータに変換します。
変換したデータを WebSocket で送信すれば、リアルタイム再配信が可能となります。
関連
OpenCV を利用する際の注意点は「画像処理の基礎知識」にまとめています。
サンプルプログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
import asyncio import cv2 import io import numpy import sys import websockets from PIL import Image class WebSocketsServer: def __init__(self, loop, address , port): self.loop = loop self.address = address self.port = port self.cap = None self.width = 640 self.height = 480 self.isJpeg = True def _cv2pil(self, image): new_image = image.copy() if new_image.ndim == 2: # mono # not supported return None elif new_image.shape[2] == 3: # RGB if self.isJpeg: img_fmt = cv2.COLOR_BGR2RGB else: img_fmt = cv2.COLOR_BGR2RGBA elif new_image.shape[2] == 4: # RGBA if self.isJpeg: img_fmt = cv2.COLOR_BGRA2RGB else: img_fmt = cv2.COLOR_BGRA2RGBA new_image = cv2.cvtColor(new_image, img_fmt) new_image = Image.fromarray(new_image) return new_image async def _handler(self, websocket, path): print("start streaming...") recv_data = await websocket.recv() self.cap = cv2.VideoCapture(r'rtsp://127.0.0.1:554/mpeg4') if self.cap.isOpened() == False: print("cannot connect RTSP server.") sys.exit(1) while self.cap.isOpened(): ret, frame = self.cap.read() if ret == False: continue image = self._cv2pil(cv2.resize(frame, (self.width, self.height))) if self.isJpeg: with io.BytesIO() as image_temp: image.save(image_temp, format="jpeg", quority=80) await websocket.send(image_temp.getvalue()) pass else: image_np = numpy.array(image) await websocket.send(image_np.tobytes()) def run(self): self._server = websockets.serve(self._handler, self.address, self.port) self.loop.run_until_complete(self._server) self.loop.run_forever() if __name__ == '__main__': loop = asyncio.get_event_loop() wss = WebSocketsServer(loop, '0.0.0.0', 60000) wss.run() |
参考
C++で実装したい場合は「ブラウザでRTSPストリーミング再生【C++版】」を参照ください。
ブラウザ(HTML/JavaScript)
WebSocket サーバと接続
もともと HTML5 の規格のため、JavaScript で WebSocket を扱うのは非常に簡単です。
以下のように記述するだけで、サーバ側と接続することができます。
1 2 3 4 5 6 |
ws = new WebSocket('ws://localhost:60000'); ws.binaryType = 'arraybuffer'; ws.onmessage = function (data) { // 受信処理 }; |
動画フレームを HTML Canvas に描画
WebSocket サーバからの送信フォーマットによって処理が少し異なります。
- Jpeg画像のバイナリデータ
受信したバイナリデータを base64 に変換して描画させます - RGBA のバイナリデータ
受信したデータを配列に格納し、HTMLCanvas.getContext('2d').putImageData() で描画します
サンプルプログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<html> <head> <script src="js/img-loader.js"></script> </head> <body onload="imgLoader.onLoad();"> <input type="button" value="START" onclick="imgLoader.onButtonClicked();" /> <br> <div> <canvas id="canvas" width="640" height="480"></canvas> </div> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
"use strict"; var imgLoader = { webSocket: null, wsSrv: 'ws://localhost:60000', width: 640, height: 480, isJpeg: true, onLoad: function() { this.webSocket = new WebSocket(this.wsSrv); this.webSocket.binaryType = 'arraybuffer'; this.webSocket.onmessage = imgLoader.onMessage.bind(this); }, onButtonClicked: function() { this.webSocket.send('START'); }, onMessage: function(data) { var img = new Uint8Array(data.data); var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); console.log("recv data..."); if (this.isJpeg) { var image = new Image(); image.src = 'data:image/jpeg;base64,' + window.btoa(String.fromCharCode.apply(null, img)); image.onload = function() { ctx.drawImage(image, 0, 0); } } else { var imageData = ctx.createImageData(this.width, this.height); for (var i = 0; i < imageData.data.length; i++) { imageData.data[i] = img[i]; } ctx.putImageData(imageData, 0, 0); } } }; |
さいごに
今回のサンプルプログラムでは、Jpeg配信とRGBA配信の両方を記述しています。
お気づきの方もおられると思いますが、動画のストリーミング風の静止画パラパラ漫画になります。
パラパラ漫画ではありますが、WebSocket+HTML Canvasは 90 FPS 以上出せる手法のため、
一般的な使用範囲であれば問題ないでしょう。