はじめに
カメラなどで使用されている RTSP ストリーミングは、そのままではブラウザで再生するできません。
そこで、できるだけシンプル・簡単に RTSP のストリーミング再生をブラウザで行えるようにする
WebSocket と HTML Canvas を使って実現する方法を以下の記事でご紹介しました。
こちらもCHECK
-
-
ブラウザでRTSPストリーミング再生
カメラなどで使用されているRTSPストリーミングは、そのままではブラウザで再生することはできません。そこで、シンプル・簡単に RTSP のストリーミング再生をブラウザで行えるようにするWebSocketとHTML Canvasを使って実現する方法をご紹介いたします。
続きを見る
前回は WebSocket サーバ、つまり画像を送信する側のコードを Python で記述しましたが、
今回は C++ 版で実装する場合のコードを掲載いたします。
全体の構成
本記事では、以下のフォルダ構成を元に解説しています。
Source Tree
/
|- inc/
| |- Base64.h
| |- Config.h
| |- JsonWrapper.h
| |- picojson.h
|- src/
| |- Main.cpp
|- build.sh
|- video.mp4
ユーティリティの準備
C++ 言語の標準ライブラリでは、Base64 変換や JSON を扱うことができません。
そのため、それらを行うためのユーティリティを準備してあげる必要があります。
Base64 変換
|
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 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
#pragma once #include <string> #include <vector> namespace algorithm { bool encode_base64(const std::vector<unsigned char>& src, std::string& dst) { const std::string table("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); std::string cdst; for (std::size_t i = 0; i < src.size(); ++i) { switch (i % 3) { case 0: cdst.push_back(table[(src[i] & 0xFC) >> 2]); if (i + 1 == src.size()) { cdst.push_back(table[(src[i] & 0x03) << 4]); cdst.push_back('='); cdst.push_back('='); } break; case 1: cdst.push_back(table[((src[i - 1] & 0x03) << 4) | ((src[i + 0] & 0xF0) >> 4)]); if (i + 1 == src.size()) { cdst.push_back(table[(src[i] & 0x0F) << 2]); cdst.push_back('='); } break; case 2: cdst.push_back(table[((src[i - 1] & 0x0F) << 2) | ((src[i + 0] & 0xC0) >> 6)]); cdst.push_back(table[src[i] & 0x3F]); break; } } dst.swap(cdst); return true; } bool decode_base64(const std::string& src, std::vector<unsigned char>& dst) { if (src.size() & 0x00000003) return false; const std::string table("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"); std::vector<unsigned char> cdst; for (std::size_t i = 0; i < src.size(); i += 4) { if (src[i + 0] == '=') return false; else if (src[i + 1] == '=') return false; else if (src[i + 2] == '=') { const std::string::size_type s1 = table.find(src[i + 0]); const std::string::size_type s2 = table.find(src[i + 1]); if (s1 == std::string::npos || s2 == std::string::npos) return false; cdst.push_back(static_cast<unsigned char>(((s1 & 0x3F) << 2) | ((s2 & 0x30) >> 4))); break; } else if (src[i + 3] == '=') { const std::string::size_type s1 = table.find(src[i + 0]); const std::string::size_type s2 = table.find(src[i + 1]); const std::string::size_type s3 = table.find(src[i + 2]); if (s1 == std::string::npos || s2 == std::string::npos || s3 == std::string::npos) return false; cdst.push_back(static_cast<unsigned char>(((s1 & 0x3F) << 2) | ((s2 & 0x30) >> 4))); cdst.push_back(static_cast<unsigned char>(((s2 & 0x0F) << 4) | ((s3 & 0x3C) >> 2))); break; } else { const std::string::size_type s1 = table.find(src[i + 0]); const std::string::size_type s2 = table.find(src[i + 1]); const std::string::size_type s3 = table.find(src[i + 2]); const std::string::size_type s4 = table.find(src[i + 3]); if (s1 == std::string::npos || s2 == std::string::npos || s3 == std::string::npos || s4 == std::string::npos) return false; cdst.push_back(static_cast<unsigned char>(((s1 & 0x3F) << 2) | ((s2 & 0x30) >> 4))); cdst.push_back(static_cast<unsigned char>(((s2 & 0x0F) << 4) | ((s3 & 0x3C) >> 2))); cdst.push_back(static_cast<unsigned char>(((s3 & 0x03) << 6) | ((s4 & 0x3F) >> 0))); } } dst.swap(cdst); return true; } }; // namespace algorithm |
JSON フォーマット
おおもとのライブラリには、BSD ライセンスで最も軽量な picojson を利用しています。
ただ、そのままだと C++ では扱いにくいため、ラッパー関数を用意しました。
|
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 |
#pragma once #include <functional> #include "picojson.h" // https://takap-tech.com/entry/2019/10/24/235423 class JsonWrapper { picojson::object obj; public: operator picojson::object() { return this->obj; } operator picojson::value() { return picojson::value(this->obj); } picojson::object getObject() { return *this; } picojson::value toValue() { return *this; } inline void add(const std::string& key, const int value) { JsonWrapper::add(this->obj, key, (double)value); } inline void add(const std::string& key, const unsigned int value) { JsonWrapper::add(this->obj, key, (double)value); } inline void add(const std::string& key, const float value) { JsonWrapper::add(this->obj, key, (double)value); } inline void add(const std::string& key, const bool value) { this->obj.insert(std::make_pair(key.c_str(), picojson::value(value))); } inline void addStr(const std::string& key, const std::string& value) { this->obj.insert(std::make_pair(key.c_str(), picojson::value(value))); } inline void add(const std::string& key, JsonWrapper child) { this->obj.insert(std::make_pair(key.c_str(), child.toValue())); } template<class T> void add(const std::string& key, T value, const std::function<JsonWrapper(T)>& convert) { this->obj.insert(std::make_pair(key.c_str(), convert(value).getValue())); } template<class T> void add(const std::string& key, const std::vector<T>& list, const std::function<JsonWrapper(T)>& convert) { picojson::array jsonList; for (const auto& item : list) { jsonList.push_back(convert(item).toValue()); } obj.insert(std::make_pair(key.c_str(), picojson::value(jsonList))); } std::string serialize() { return this->toValue().serialize(); } inline static void add(picojson::object& obj, const std::string& key, double value) { obj.insert(std::make_pair(key.c_str(), value)); } }; |
その他
定数の宣言、及びビルドスクリプトです。
|
1 2 3 4 5 6 7 8 9 |
#pragma once #define WSS_PORT (60000) #define IMAGE_WIDTH (640) #define IMAGE_HEIGHT (480) #define VIDEO_PATH "video.mp4" |
|
1 2 3 4 5 6 |
CFLAGS=`pkg-config --cflags opencv4` LIBS=`pkg-config --libs opencv4` echo g++ -o test -Wall -O2 -g -I./inc ${CFLAGS} src/Main.cpp ${LIBS} -lpthread g++ -o test -Wall -O2 -g -I./inc ${CFLAGS} src/Main.cpp ${LIBS} -lpthread |
WebSocket サーバの実装
WebSocket のライブラリは、libwebsocketpp を使用しました。
ヘッダーだけで記述されたライブラリのため、コンパイルに少し時間がかかりますが軽量です。
以下のコマンドでインストールします。
command
$ sudo apt install libwebsocketpp-dev
お待ちかねのコードです。
Python 版の websocket_server.py と等価となります。
|
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 |
#include <iostream> #include <fstream> #include <string> #include <opencv2/core/utils/logger.hpp> #include <opencv2/opencv.hpp> #include <websocketpp/config/asio_no_tls.hpp> #include <websocketpp/server.hpp> #include "Base64.h" #include "Config.h" #include "JsonWrapper.h" typedef websocketpp::server<websocketpp::config::asio> WebSocketServer; static void on_message(WebSocketServer* s, websocketpp::connection_hdl hd, WebSocketServer::message_ptr msg) { cv::VideoCapture cap; cap.open(VIDEO_PATH); if (!cap.isOpened()) { std::cerr << "file not opened." << std::endl; return; } cv::Mat frame; cv::Mat resized_frame; while (1) { cap >> frame; if (frame.empty()) break; cv::resize(frame, resized_frame, cv::Size(IMAGE_WIDTH, IMAGE_HEIGHT), cv::INTER_LINEAR); std::vector<unsigned char> buf; std::vector<int> params(2); params[0] = cv::IMWRITE_JPEG_QUALITY; params[1] = 80; cv::imencode(".jpg", resized_frame, buf, params); std::string b64; algorithm::encode_base64(buf, b64); s->send(hd, b64, websocketpp::frame::opcode::text); } std::cout << "DONE." << std::endl; } int main() { WebSocketServer wss; using websocketpp::lib::placeholders::_1; using websocketpp::lib::placeholders::_2; wss.set_message_handler(bind(&on_message, &wss, _1, _2)); wss.set_access_channels(websocketpp::log::alevel::none); wss.set_error_channels(websocketpp::log::elevel::all); wss.init_asio(); wss.listen(WSS_PORT); wss.start_accept(); wss.run(); return 0; } |
注意事項
Python 版と同じように、WebSocket を使った本手法で FPS が最大になるよう実装しています。
本来は、on_message 内で受信したデータの処理をしたら、return する必要があります。
libwebsocketpp の接続管理などは、こちらの OutputWebSocket クラスを参照ください。
おわりに
いかがでしたでしょうか。
こちらですと、Ubuntu 20.04 LTS の VM 環境で 120 FPS を出すことができました。
VM 環境ではなく Native 環境にし、Base64 変換を最適化すれば、
もう少し早くなると思います。