添加Web配置管理页面
This commit is contained in:
parent
929728f032
commit
a6ee43d6af
16
README.md
16
README.md
@ -76,3 +76,19 @@ The application reads `config.json` next to the executable at:
|
|||||||
```text
|
```text
|
||||||
/opt/vehicle-video-service/bin/config.json
|
/opt/vehicle-video-service/bin/config.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Web Configuration
|
||||||
|
|
||||||
|
The service includes a small built-in configuration page:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://<device-ip>:18080
|
||||||
|
```
|
||||||
|
|
||||||
|
Default login:
|
||||||
|
|
||||||
|
```text
|
||||||
|
admin / admin123
|
||||||
|
```
|
||||||
|
|
||||||
|
The page can edit camera channels, live/record switches, SRS ports, record path, MQTT settings, and the management page port. Saving writes `config.json` and regenerates SRS `live.conf` / `record.conf` under the configured SRS root. Restart the related services after saving for runtime changes to take effect.
|
||||||
|
|||||||
16
config.json
16
config.json
@ -13,16 +13,32 @@
|
|||||||
"root": "/opt/vehicle-video-service/srs",
|
"root": "/opt/vehicle-video-service/srs",
|
||||||
"record_config": "/opt/vehicle-video-service/srs/conf/record.conf",
|
"record_config": "/opt/vehicle-video-service/srs/conf/record.conf",
|
||||||
"stream_app": "camera",
|
"stream_app": "camera",
|
||||||
|
"live_enabled": true,
|
||||||
|
"record_enabled": true,
|
||||||
"live_host": "127.0.0.1",
|
"live_host": "127.0.0.1",
|
||||||
"live_rtmp_port": 1935,
|
"live_rtmp_port": 1935,
|
||||||
"live_http_api_port": 1985,
|
"live_http_api_port": 1985,
|
||||||
|
"live_http_server_port": 8080,
|
||||||
|
"live_rtc_port": 8000,
|
||||||
"live_vhost": "live",
|
"live_vhost": "live",
|
||||||
"record_host": "127.0.0.1",
|
"record_host": "127.0.0.1",
|
||||||
"record_rtmp_port": 2935,
|
"record_rtmp_port": 2935,
|
||||||
"record_http_api_port": 2985,
|
"record_http_api_port": 2985,
|
||||||
|
"record_http_server_port": 2980,
|
||||||
"record_vhost": "record",
|
"record_vhost": "record",
|
||||||
|
"record_path": "/media/record",
|
||||||
|
"dvr_duration_sec": 60,
|
||||||
|
"retention_days": 14,
|
||||||
|
"usage_threshold": 0.9,
|
||||||
"public_interface": "enP2p33s0"
|
"public_interface": "enP2p33s0"
|
||||||
},
|
},
|
||||||
|
"web": {
|
||||||
|
"enabled": true,
|
||||||
|
"bind": "0.0.0.0",
|
||||||
|
"port": 18080,
|
||||||
|
"username": "admin",
|
||||||
|
"password": "admin123"
|
||||||
|
},
|
||||||
"cameras": [
|
"cameras": [
|
||||||
{
|
{
|
||||||
"device": "/dev/video0",
|
"device": "/dev/video0",
|
||||||
|
|||||||
@ -70,22 +70,41 @@ struct SRSConfig
|
|||||||
std::string root = "/opt/vehicle-video-service/srs";
|
std::string root = "/opt/vehicle-video-service/srs";
|
||||||
std::string record_config = "/opt/vehicle-video-service/srs/conf/record.conf";
|
std::string record_config = "/opt/vehicle-video-service/srs/conf/record.conf";
|
||||||
std::string stream_app = "camera";
|
std::string stream_app = "camera";
|
||||||
|
bool live_enabled = true;
|
||||||
|
bool record_enabled = true;
|
||||||
std::string live_host = "127.0.0.1";
|
std::string live_host = "127.0.0.1";
|
||||||
int live_rtmp_port = 1935;
|
int live_rtmp_port = 1935;
|
||||||
int live_http_api_port = 1985;
|
int live_http_api_port = 1985;
|
||||||
|
int live_http_server_port = 8080;
|
||||||
|
int live_rtc_port = 8000;
|
||||||
std::string live_vhost = "live";
|
std::string live_vhost = "live";
|
||||||
std::string record_host = "127.0.0.1";
|
std::string record_host = "127.0.0.1";
|
||||||
int record_rtmp_port = 2935;
|
int record_rtmp_port = 2935;
|
||||||
int record_http_api_port = 2985;
|
int record_http_api_port = 2985;
|
||||||
|
int record_http_server_port = 2980;
|
||||||
std::string record_vhost = "record";
|
std::string record_vhost = "record";
|
||||||
|
std::string record_path = "/media/record";
|
||||||
|
int dvr_duration_sec = 60;
|
||||||
|
int retention_days = 14;
|
||||||
|
double usage_threshold = 0.90;
|
||||||
std::string public_interface = "enP2p33s0";
|
std::string public_interface = "enP2p33s0";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct WebConfig
|
||||||
|
{
|
||||||
|
bool enabled = true;
|
||||||
|
std::string bind = "0.0.0.0";
|
||||||
|
int port = 18080;
|
||||||
|
std::string username = "admin";
|
||||||
|
std::string password = "admin123";
|
||||||
|
};
|
||||||
|
|
||||||
struct AppConfig
|
struct AppConfig
|
||||||
{
|
{
|
||||||
std::vector<Camera> cameras;
|
std::vector<Camera> cameras;
|
||||||
MQTTConfig mqtt;
|
MQTTConfig mqtt;
|
||||||
SRSConfig srs;
|
SRSConfig srs;
|
||||||
|
WebConfig web;
|
||||||
|
|
||||||
static AppConfig load_from_file(const std::string& filepath)
|
static AppConfig load_from_file(const std::string& filepath)
|
||||||
{
|
{
|
||||||
@ -142,17 +161,36 @@ struct AppConfig
|
|||||||
cfg.srs.root = s.value("root", cfg.srs.root);
|
cfg.srs.root = s.value("root", cfg.srs.root);
|
||||||
cfg.srs.record_config = s.value("record_config", cfg.srs.record_config);
|
cfg.srs.record_config = s.value("record_config", cfg.srs.record_config);
|
||||||
cfg.srs.stream_app = s.value("stream_app", cfg.srs.stream_app);
|
cfg.srs.stream_app = s.value("stream_app", cfg.srs.stream_app);
|
||||||
|
cfg.srs.live_enabled = s.value("live_enabled", cfg.srs.live_enabled);
|
||||||
|
cfg.srs.record_enabled = s.value("record_enabled", cfg.srs.record_enabled);
|
||||||
cfg.srs.live_host = s.value("live_host", cfg.srs.live_host);
|
cfg.srs.live_host = s.value("live_host", cfg.srs.live_host);
|
||||||
cfg.srs.live_rtmp_port = s.value("live_rtmp_port", cfg.srs.live_rtmp_port);
|
cfg.srs.live_rtmp_port = s.value("live_rtmp_port", cfg.srs.live_rtmp_port);
|
||||||
cfg.srs.live_http_api_port = s.value("live_http_api_port", cfg.srs.live_http_api_port);
|
cfg.srs.live_http_api_port = s.value("live_http_api_port", cfg.srs.live_http_api_port);
|
||||||
|
cfg.srs.live_http_server_port = s.value("live_http_server_port", cfg.srs.live_http_server_port);
|
||||||
|
cfg.srs.live_rtc_port = s.value("live_rtc_port", cfg.srs.live_rtc_port);
|
||||||
cfg.srs.live_vhost = s.value("live_vhost", cfg.srs.live_vhost);
|
cfg.srs.live_vhost = s.value("live_vhost", cfg.srs.live_vhost);
|
||||||
cfg.srs.record_host = s.value("record_host", cfg.srs.record_host);
|
cfg.srs.record_host = s.value("record_host", cfg.srs.record_host);
|
||||||
cfg.srs.record_rtmp_port = s.value("record_rtmp_port", cfg.srs.record_rtmp_port);
|
cfg.srs.record_rtmp_port = s.value("record_rtmp_port", cfg.srs.record_rtmp_port);
|
||||||
cfg.srs.record_http_api_port = s.value("record_http_api_port", cfg.srs.record_http_api_port);
|
cfg.srs.record_http_api_port = s.value("record_http_api_port", cfg.srs.record_http_api_port);
|
||||||
|
cfg.srs.record_http_server_port = s.value("record_http_server_port", cfg.srs.record_http_server_port);
|
||||||
cfg.srs.record_vhost = s.value("record_vhost", cfg.srs.record_vhost);
|
cfg.srs.record_vhost = s.value("record_vhost", cfg.srs.record_vhost);
|
||||||
|
cfg.srs.record_path = s.value("record_path", cfg.srs.record_path);
|
||||||
|
cfg.srs.dvr_duration_sec = s.value("dvr_duration_sec", cfg.srs.dvr_duration_sec);
|
||||||
|
cfg.srs.retention_days = s.value("retention_days", cfg.srs.retention_days);
|
||||||
|
cfg.srs.usage_threshold = s.value("usage_threshold", cfg.srs.usage_threshold);
|
||||||
cfg.srs.public_interface = s.value("public_interface", cfg.srs.public_interface);
|
cfg.srs.public_interface = s.value("public_interface", cfg.srs.public_interface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (j.contains("web"))
|
||||||
|
{
|
||||||
|
auto& w = j["web"];
|
||||||
|
cfg.web.enabled = w.value("enabled", cfg.web.enabled);
|
||||||
|
cfg.web.bind = w.value("bind", cfg.web.bind);
|
||||||
|
cfg.web.port = w.value("port", cfg.web.port);
|
||||||
|
cfg.web.username = w.value("username", cfg.web.username);
|
||||||
|
cfg.web.password = w.value("password", cfg.web.password);
|
||||||
|
}
|
||||||
|
|
||||||
LOG_INFO("[Config] Loaded MQTT server: " + cfg.mqtt.server_ip + ":" + std::to_string(cfg.mqtt.server_port));
|
LOG_INFO("[Config] Loaded MQTT server: " + cfg.mqtt.server_ip + ":" + std::to_string(cfg.mqtt.server_port));
|
||||||
LOG_INFO("[Config] MQTT client ID: " + cfg.mqtt.client_id);
|
LOG_INFO("[Config] MQTT client ID: " + cfg.mqtt.client_id);
|
||||||
LOG_INFO("[Config] MQTT Credentials - username: " + cfg.mqtt.username + ", password: " + cfg.mqtt.password);
|
LOG_INFO("[Config] MQTT Credentials - username: " + cfg.mqtt.username + ", password: " + cfg.mqtt.password);
|
||||||
|
|||||||
26
include/config_server.hpp
Normal file
26
include/config_server.hpp
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include "app_config.hpp"
|
||||||
|
|
||||||
|
class ConfigServer
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ConfigServer() = default;
|
||||||
|
~ConfigServer();
|
||||||
|
|
||||||
|
bool start(const std::string& config_path);
|
||||||
|
void stop();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void run();
|
||||||
|
void handle_client(int client_fd);
|
||||||
|
|
||||||
|
std::string config_path_;
|
||||||
|
std::thread thread_;
|
||||||
|
std::atomic<bool> running_{false};
|
||||||
|
int server_fd_ = -1;
|
||||||
|
};
|
||||||
@ -5,7 +5,6 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|||||||
APP_BIN="$ROOT_DIR/bin/vehicle_video_service"
|
APP_BIN="$ROOT_DIR/bin/vehicle_video_service"
|
||||||
DIST_DIR="${DIST_DIR:-$ROOT_DIR/dist}"
|
DIST_DIR="${DIST_DIR:-$ROOT_DIR/dist}"
|
||||||
PACKAGE_NAME="${PACKAGE_NAME:-vehicle-video-service-aarch64}"
|
PACKAGE_NAME="${PACKAGE_NAME:-vehicle-video-service-aarch64}"
|
||||||
STAGE_DIR="$DIST_DIR/$PACKAGE_NAME"
|
|
||||||
|
|
||||||
if [[ "${SKIP_VIDEO_BUILD:-0}" != "1" ]]; then
|
if [[ "${SKIP_VIDEO_BUILD:-0}" != "1" ]]; then
|
||||||
"$ROOT_DIR/scripts/build_video.sh"
|
"$ROOT_DIR/scripts/build_video.sh"
|
||||||
@ -63,7 +62,11 @@ if [[ ! -x "$APP_BIN" ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rm -rf "$STAGE_DIR"
|
mkdir -p "$DIST_DIR"
|
||||||
|
STAGE_PARENT="$(mktemp -d "$DIST_DIR/.package.XXXXXX")"
|
||||||
|
STAGE_DIR="$STAGE_PARENT/$PACKAGE_NAME"
|
||||||
|
trap 'rm -rf "$STAGE_PARENT"' EXIT
|
||||||
|
|
||||||
mkdir -p "$STAGE_DIR/bin" "$STAGE_DIR/config" "$STAGE_DIR/srs/bin" "$STAGE_DIR/srs/conf" \
|
mkdir -p "$STAGE_DIR/bin" "$STAGE_DIR/config" "$STAGE_DIR/srs/bin" "$STAGE_DIR/srs/conf" \
|
||||||
"$STAGE_DIR/srs/html" "$STAGE_DIR/srs/log" "$STAGE_DIR/srs/run" "$STAGE_DIR/systemd"
|
"$STAGE_DIR/srs/html" "$STAGE_DIR/srs/log" "$STAGE_DIR/srs/run" "$STAGE_DIR/systemd"
|
||||||
|
|
||||||
@ -78,6 +81,6 @@ install -m 0755 "$ROOT_DIR/deploy/install.sh" "$STAGE_DIR/install.sh"
|
|||||||
install -m 0755 "$ROOT_DIR/deploy/uninstall.sh" "$STAGE_DIR/uninstall.sh"
|
install -m 0755 "$ROOT_DIR/deploy/uninstall.sh" "$STAGE_DIR/uninstall.sh"
|
||||||
install -m 0644 "$ROOT_DIR/README.md" "$STAGE_DIR/README.md"
|
install -m 0644 "$ROOT_DIR/README.md" "$STAGE_DIR/README.md"
|
||||||
|
|
||||||
tar -C "$DIST_DIR" -czf "$DIST_DIR/$PACKAGE_NAME.tar.gz" "$PACKAGE_NAME"
|
tar -C "$STAGE_PARENT" -czf "$DIST_DIR/$PACKAGE_NAME.tar.gz" "$PACKAGE_NAME"
|
||||||
|
|
||||||
echo "Packaged $DIST_DIR/$PACKAGE_NAME.tar.gz"
|
echo "Packaged $DIST_DIR/$PACKAGE_NAME.tar.gz"
|
||||||
|
|||||||
675
src/config_server.cpp
Normal file
675
src/config_server.cpp
Normal file
@ -0,0 +1,675 @@
|
|||||||
|
#include "config_server.hpp"
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/statvfs.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <cerrno>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include "logger.hpp"
|
||||||
|
#include "rtmp_manager.hpp"
|
||||||
|
|
||||||
|
extern AppConfig g_app_config;
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
using json = nlohmann::json;
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
std::string read_file(const std::string& path)
|
||||||
|
{
|
||||||
|
std::ifstream ifs(path);
|
||||||
|
if (!ifs.is_open()) return "";
|
||||||
|
std::ostringstream ss;
|
||||||
|
ss << ifs.rdbuf();
|
||||||
|
return ss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool write_file_atomic(const std::string& path, const std::string& content)
|
||||||
|
{
|
||||||
|
std::string tmp = path + ".tmp";
|
||||||
|
{
|
||||||
|
std::ofstream ofs(tmp, std::ios::trunc);
|
||||||
|
if (!ofs.is_open()) return false;
|
||||||
|
ofs << content;
|
||||||
|
if (!ofs.good()) return false;
|
||||||
|
}
|
||||||
|
return ::rename(tmp.c_str(), path.c_str()) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string http_reason(int status)
|
||||||
|
{
|
||||||
|
switch (status)
|
||||||
|
{
|
||||||
|
case 200:
|
||||||
|
return "OK";
|
||||||
|
case 400:
|
||||||
|
return "Bad Request";
|
||||||
|
case 401:
|
||||||
|
return "Unauthorized";
|
||||||
|
case 404:
|
||||||
|
return "Not Found";
|
||||||
|
case 405:
|
||||||
|
return "Method Not Allowed";
|
||||||
|
case 500:
|
||||||
|
return "Internal Server Error";
|
||||||
|
default:
|
||||||
|
return "OK";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void send_response(int fd, int status, const std::string& content_type, const std::string& body,
|
||||||
|
const std::vector<std::pair<std::string, std::string>>& extra_headers = {})
|
||||||
|
{
|
||||||
|
std::ostringstream resp;
|
||||||
|
resp << "HTTP/1.1 " << status << " " << http_reason(status) << "\r\n";
|
||||||
|
resp << "Content-Type: " << content_type << "\r\n";
|
||||||
|
resp << "Content-Length: " << body.size() << "\r\n";
|
||||||
|
resp << "Connection: close\r\n";
|
||||||
|
for (const auto& h : extra_headers) resp << h.first << ": " << h.second << "\r\n";
|
||||||
|
resp << "\r\n";
|
||||||
|
resp << body;
|
||||||
|
std::string raw = resp.str();
|
||||||
|
::send(fd, raw.data(), raw.size(), MSG_NOSIGNAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string url_decode(const std::string& s)
|
||||||
|
{
|
||||||
|
std::string out;
|
||||||
|
for (size_t i = 0; i < s.size(); ++i)
|
||||||
|
{
|
||||||
|
if (s[i] == '%' && i + 2 < s.size())
|
||||||
|
{
|
||||||
|
char hex[3] = {s[i + 1], s[i + 2], 0};
|
||||||
|
out.push_back(static_cast<char>(strtol(hex, nullptr, 16)));
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
else if (s[i] == '+')
|
||||||
|
{
|
||||||
|
out.push_back(' ');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
out.push_back(s[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string base64_decode(const std::string& in)
|
||||||
|
{
|
||||||
|
static const std::string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
||||||
|
std::string out;
|
||||||
|
int val = 0;
|
||||||
|
int valb = -8;
|
||||||
|
for (unsigned char c : in)
|
||||||
|
{
|
||||||
|
if (std::isspace(c)) continue;
|
||||||
|
if (c == '=') break;
|
||||||
|
int idx = chars.find(c);
|
||||||
|
if (idx == (int)std::string::npos) return "";
|
||||||
|
val = (val << 6) + idx;
|
||||||
|
valb += 6;
|
||||||
|
if (valb >= 0)
|
||||||
|
{
|
||||||
|
out.push_back(char((val >> valb) & 0xFF));
|
||||||
|
valb -= 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool authorized(const std::string& headers)
|
||||||
|
{
|
||||||
|
if (g_app_config.web.username.empty()) return true;
|
||||||
|
std::string needle = "Authorization: Basic ";
|
||||||
|
auto pos = headers.find(needle);
|
||||||
|
if (pos == std::string::npos) return false;
|
||||||
|
pos += needle.size();
|
||||||
|
auto end = headers.find("\r\n", pos);
|
||||||
|
std::string token = headers.substr(pos, end - pos);
|
||||||
|
return base64_decode(token) == g_app_config.web.username + ":" + g_app_config.web.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string make_live_conf(const json& cfg)
|
||||||
|
{
|
||||||
|
const auto& s = cfg.at("srs");
|
||||||
|
std::ostringstream os;
|
||||||
|
os << "listen " << s.value("live_rtmp_port", 1935) << ";\n";
|
||||||
|
os << "max_connections 2000;\n";
|
||||||
|
os << "daemon off;\n";
|
||||||
|
os << "pid ./run/srs_live.pid;\n";
|
||||||
|
os << "srs_log_tank file;\n";
|
||||||
|
os << "srs_log_file ./log/srs_live.log;\n\n";
|
||||||
|
os << "http_server {\n enabled on;\n listen " << s.value("live_http_server_port", 8080)
|
||||||
|
<< ";\n dir ./html;\n}\n\n";
|
||||||
|
os << "http_api {\n enabled on;\n listen " << s.value("live_http_api_port", 1985)
|
||||||
|
<< ";\n}\n\n";
|
||||||
|
os << "stats {\n network 0;\n}\n\n";
|
||||||
|
os << "rtc_server {\n enabled on;\n listen " << s.value("live_rtc_port", 8000)
|
||||||
|
<< ";\n candidate $CANDIDATE;\n}\n\n";
|
||||||
|
os << "vhost " << s.value("live_vhost", "live") << " {\n";
|
||||||
|
os << " rtc {\n enabled on;\n rtmp_to_rtc on;\n rtc_to_rtmp off;\n }\n\n";
|
||||||
|
os << " play {\n mw_latency 100;\n queue_length 10;\n }\n\n";
|
||||||
|
os << " http_remux {\n enabled on;\n mount [vhost]/[app]/[stream].flv;\n }\n\n";
|
||||||
|
os << " dvr {\n enabled off;\n }\n hls {\n enabled off;\n }\n forward {\n enabled off;\n }\n}\n";
|
||||||
|
return os.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string make_record_conf(const json& cfg)
|
||||||
|
{
|
||||||
|
const auto& s = cfg.at("srs");
|
||||||
|
std::string record_path = s.value("record_path", "/media/record");
|
||||||
|
if (!record_path.empty() && record_path.back() == '/') record_path.pop_back();
|
||||||
|
std::ostringstream os;
|
||||||
|
os << "listen " << s.value("record_rtmp_port", 2935) << ";\n";
|
||||||
|
os << "pid ./run/srs_record.pid;\n";
|
||||||
|
os << "srs_log_tank file;\n";
|
||||||
|
os << "srs_log_file ./log/srs_record.log;\n";
|
||||||
|
os << "max_connections 2000;\n";
|
||||||
|
os << "daemon off;\n\n";
|
||||||
|
os << "http_server {\n enabled on;\n listen " << s.value("record_http_server_port", 2980)
|
||||||
|
<< ";\n dir ./html;\n}\n\n";
|
||||||
|
os << "http_api {\n enabled on;\n listen " << s.value("record_http_api_port", 2985)
|
||||||
|
<< ";\n}\n\n";
|
||||||
|
os << "stats {\n network 0;\n}\n\n";
|
||||||
|
os << "vhost " << s.value("record_vhost", "record") << " {\n";
|
||||||
|
os << " dvr {\n enabled " << (s.value("record_enabled", true) ? "on" : "off")
|
||||||
|
<< ";\n dvr_plan segment;\n dvr_duration " << s.value("dvr_duration_sec", 60)
|
||||||
|
<< ";\n dvr_apply all;\n\n";
|
||||||
|
os << " dvr_path " << record_path
|
||||||
|
<< "/[stream]/[2006]-[01]-[02]/[15]/[15]-[04]-[05].mp4;\n\n time_jitter full;\n }\n\n";
|
||||||
|
os << " http_remux {\n enabled on;\n mount [vhost]/[app]/[stream].flv;\n }\n\n";
|
||||||
|
os << " rtc {\n enabled off;\n }\n}\n";
|
||||||
|
return os.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
json disk_status(const std::string& path)
|
||||||
|
{
|
||||||
|
json d;
|
||||||
|
d["path"] = path;
|
||||||
|
struct statvfs vfs{};
|
||||||
|
if (statvfs(path.c_str(), &vfs) != 0)
|
||||||
|
{
|
||||||
|
d["ok"] = false;
|
||||||
|
d["error"] = strerror(errno);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
uint64_t total = static_cast<uint64_t>(vfs.f_blocks) * vfs.f_frsize;
|
||||||
|
uint64_t avail = static_cast<uint64_t>(vfs.f_bavail) * vfs.f_frsize;
|
||||||
|
d["ok"] = true;
|
||||||
|
d["total_bytes"] = total;
|
||||||
|
d["available_bytes"] = avail;
|
||||||
|
d["used_percent"] = total == 0 ? 0 : (100.0 * (double)(total - avail) / (double)total);
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
|
||||||
|
json service_active(const std::string& name)
|
||||||
|
{
|
||||||
|
json s;
|
||||||
|
s["name"] = name;
|
||||||
|
std::string cmd = "systemctl is-active " + name + " 2>/dev/null";
|
||||||
|
FILE* fp = popen(cmd.c_str(), "r");
|
||||||
|
if (!fp)
|
||||||
|
{
|
||||||
|
s["active"] = "unknown";
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
char buf[64] = {0};
|
||||||
|
std::string out;
|
||||||
|
if (fgets(buf, sizeof(buf), fp)) out = buf;
|
||||||
|
pclose(fp);
|
||||||
|
while (!out.empty() && (out.back() == '\n' || out.back() == '\r')) out.pop_back();
|
||||||
|
s["active"] = out.empty() ? "unknown" : out;
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
json status_json()
|
||||||
|
{
|
||||||
|
json st;
|
||||||
|
st["time_ms"] = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::system_clock::now().time_since_epoch())
|
||||||
|
.count();
|
||||||
|
st["public_ip"] = get_ip_address(g_app_config.srs.public_interface);
|
||||||
|
st["disk"] = disk_status(g_app_config.srs.record_path);
|
||||||
|
st["services"] = json::array({service_active("vehicle-video-srs-live.service"),
|
||||||
|
service_active("vehicle-video-srs-record.service"),
|
||||||
|
service_active("vehicle-video-service.service")});
|
||||||
|
st["channels"] = json::array();
|
||||||
|
auto channels = RTMPManager::get_all_channels_status();
|
||||||
|
for (size_t i = 0; i < g_app_config.cameras.size(); ++i)
|
||||||
|
{
|
||||||
|
const auto& cam = g_app_config.cameras[i];
|
||||||
|
json c;
|
||||||
|
c["loc"] = static_cast<int>(i);
|
||||||
|
c["name"] = cam.name;
|
||||||
|
c["device"] = cam.device;
|
||||||
|
c["enabled"] = cam.enabled;
|
||||||
|
c["device_exists"] = access(cam.device.c_str(), F_OK) == 0;
|
||||||
|
if (i < channels.size())
|
||||||
|
{
|
||||||
|
c["running"] = channels[i].running;
|
||||||
|
c["url"] = channels[i].url;
|
||||||
|
c["reason"] = channels[i].reason;
|
||||||
|
}
|
||||||
|
st["channels"].push_back(c);
|
||||||
|
}
|
||||||
|
return st;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool write_srs_configs(const json& cfg, std::string& message)
|
||||||
|
{
|
||||||
|
if (!cfg.contains("srs")) return true;
|
||||||
|
std::string root = cfg["srs"].value("root", "/opt/vehicle-video-service/srs");
|
||||||
|
fs::path conf_dir = fs::path(root) / "conf";
|
||||||
|
std::error_code ec;
|
||||||
|
fs::create_directories(conf_dir, ec);
|
||||||
|
if (ec)
|
||||||
|
{
|
||||||
|
message = "config saved, but failed to create SRS conf dir: " + ec.message();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bool ok1 = write_file_atomic((conf_dir / "live.conf").string(), make_live_conf(cfg));
|
||||||
|
bool ok2 = write_file_atomic((conf_dir / "record.conf").string(), make_record_conf(cfg));
|
||||||
|
if (!ok1 || !ok2)
|
||||||
|
{
|
||||||
|
message = "config saved, but failed to write SRS conf files";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* index_html()
|
||||||
|
{
|
||||||
|
return R"HTML(
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Vehicle Video Service</title>
|
||||||
|
<style>
|
||||||
|
:root { color-scheme: light; font-family: Arial, "Microsoft YaHei", sans-serif; background:#f4f6f8; color:#17202a; }
|
||||||
|
body { margin:0; }
|
||||||
|
header { background:#12343b; color:white; padding:18px 24px; }
|
||||||
|
h1 { margin:0; font-size:22px; font-weight:700; }
|
||||||
|
main { max-width:1180px; margin:0 auto; padding:20px; }
|
||||||
|
section { background:white; border:1px solid #d9e0e6; border-radius:8px; margin-bottom:16px; padding:16px; }
|
||||||
|
h2 { margin:0 0 14px; font-size:17px; }
|
||||||
|
.grid { display:grid; grid-template-columns:repeat(4,minmax(0,1fr)); gap:12px; }
|
||||||
|
.grid.two { grid-template-columns:repeat(2,minmax(0,1fr)); }
|
||||||
|
label { display:flex; flex-direction:column; gap:6px; font-size:13px; color:#52606d; }
|
||||||
|
input, select { box-sizing:border-box; width:100%; padding:9px 10px; border:1px solid #c8d1da; border-radius:6px; font-size:14px; }
|
||||||
|
input[type=checkbox] { width:auto; }
|
||||||
|
table { width:100%; border-collapse:collapse; font-size:14px; }
|
||||||
|
th, td { border-bottom:1px solid #e6ebef; padding:8px; text-align:left; vertical-align:middle; }
|
||||||
|
th { color:#52606d; font-weight:600; }
|
||||||
|
button { border:0; border-radius:6px; padding:10px 14px; background:#0f766e; color:white; font-weight:700; cursor:pointer; }
|
||||||
|
button.secondary { background:#405261; }
|
||||||
|
.actions { display:flex; gap:10px; align-items:center; flex-wrap:wrap; }
|
||||||
|
.pill { display:inline-block; padding:3px 7px; border-radius:999px; background:#e8f2ff; color:#174ea6; font-size:12px; }
|
||||||
|
.warn { color:#9a3412; }
|
||||||
|
.ok { color:#166534; }
|
||||||
|
.muted { color:#667785; }
|
||||||
|
#toast { min-height:24px; margin-left:6px; }
|
||||||
|
@media (max-width: 860px) { .grid, .grid.two { grid-template-columns:1fr; } table { display:block; overflow-x:auto; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header><h1>Vehicle Video Service</h1></header>
|
||||||
|
<main>
|
||||||
|
<section>
|
||||||
|
<div class="actions">
|
||||||
|
<button onclick="saveConfig()">保存配置</button>
|
||||||
|
<button class="secondary" onclick="loadAll()">刷新</button>
|
||||||
|
<span id="toast" class="muted"></span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>运行状态</h2>
|
||||||
|
<div id="status" class="muted">加载中...</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>管理页面</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<label>启用<input id="web_enabled" type="checkbox"></label>
|
||||||
|
<label>监听地址<input id="web_bind"></label>
|
||||||
|
<label>端口<input id="web_port" type="number"></label>
|
||||||
|
<label>用户名<input id="web_username"></label>
|
||||||
|
<label>密码<input id="web_password" type="password"></label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>MQTT</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<label>车辆ID<input id="veh_id" type="number"></label>
|
||||||
|
<label>服务器地址<input id="mqtt_addr"></label>
|
||||||
|
<label>端口<input id="mqtt_port" type="number"></label>
|
||||||
|
<label>心跳间隔(ms)<input id="mqtt_keepalive" type="number"></label>
|
||||||
|
<label>Client ID<input id="mqtt_client"></label>
|
||||||
|
<label>用户名<input id="mqtt_user"></label>
|
||||||
|
<label>密码<input id="mqtt_pass" type="password"></label>
|
||||||
|
<label>使用账号密码<input id="mqtt_need_pwd" type="checkbox"></label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>SRS 与录像</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<label>SRS根目录<input id="srs_root"></label>
|
||||||
|
<label>录像配置文件<input id="record_config"></label>
|
||||||
|
<label>对外网卡<input id="public_interface"></label>
|
||||||
|
<label>应用名<input id="stream_app"></label>
|
||||||
|
<label>实时流启用<input id="live_enabled" type="checkbox"></label>
|
||||||
|
<label>录像启用<input id="record_enabled" type="checkbox"></label>
|
||||||
|
<label>录像目录<input id="record_path"></label>
|
||||||
|
<label>切片秒数<input id="dvr_duration" type="number"></label>
|
||||||
|
<label>保留天数<input id="retention_days" type="number"></label>
|
||||||
|
<label>磁盘阈值(0-1)<input id="usage_threshold" type="number" step="0.01"></label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>端口</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<label>Live RTMP<input id="live_rtmp_port" type="number"></label>
|
||||||
|
<label>Live HTTP API<input id="live_http_api_port" type="number"></label>
|
||||||
|
<label>Live HTTP Server<input id="live_http_server_port" type="number"></label>
|
||||||
|
<label>Live RTC UDP<input id="live_rtc_port" type="number"></label>
|
||||||
|
<label>Record RTMP<input id="record_rtmp_port" type="number"></label>
|
||||||
|
<label>Record HTTP API<input id="record_http_api_port" type="number"></label>
|
||||||
|
<label>Record HTTP Server<input id="record_http_server_port" type="number"></label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>视频通道</h2>
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>启用</th><th>名称</th><th>设备</th><th>码率</th><th>宽</th><th>高</th><th>FPS</th></tr></thead>
|
||||||
|
<tbody id="camera_rows"></tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
<script>
|
||||||
|
let config = {};
|
||||||
|
const $ = id => document.getElementById(id);
|
||||||
|
const num = id => Number($(id).value);
|
||||||
|
|
||||||
|
async function api(path, options) {
|
||||||
|
const res = await fetch(path, options);
|
||||||
|
if (!res.ok) throw new Error(await res.text());
|
||||||
|
return await res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function setToast(text, cls='muted') { const t=$('toast'); t.className=cls; t.textContent=text; }
|
||||||
|
|
||||||
|
function fillConfig() {
|
||||||
|
const m = config.mqtt_server || {};
|
||||||
|
const s = config.srs || {};
|
||||||
|
const w = config.web || {};
|
||||||
|
$('veh_id').value = m.veh_id ?? 0;
|
||||||
|
$('mqtt_addr').value = m.address ?? '';
|
||||||
|
$('mqtt_port').value = m.port ?? 1883;
|
||||||
|
$('mqtt_keepalive').value = m.mqtt_heart_threshold ?? 1000;
|
||||||
|
$('mqtt_client').value = m.client_id ?? '';
|
||||||
|
$('mqtt_user').value = m.username ?? '';
|
||||||
|
$('mqtt_pass').value = m.password ?? '';
|
||||||
|
$('mqtt_need_pwd').checked = !!m.need_username_pwd;
|
||||||
|
$('web_enabled').checked = w.enabled ?? true;
|
||||||
|
$('web_bind').value = w.bind ?? '0.0.0.0';
|
||||||
|
$('web_port').value = w.port ?? 18080;
|
||||||
|
$('web_username').value = w.username ?? 'admin';
|
||||||
|
$('web_password').value = w.password ?? '';
|
||||||
|
$('srs_root').value = s.root ?? '/opt/vehicle-video-service/srs';
|
||||||
|
$('record_config').value = s.record_config ?? '';
|
||||||
|
$('public_interface').value = s.public_interface ?? 'enP2p33s0';
|
||||||
|
$('stream_app').value = s.stream_app ?? 'camera';
|
||||||
|
$('live_enabled').checked = s.live_enabled ?? true;
|
||||||
|
$('record_enabled').checked = s.record_enabled ?? true;
|
||||||
|
$('record_path').value = s.record_path ?? '/media/record';
|
||||||
|
$('dvr_duration').value = s.dvr_duration_sec ?? 60;
|
||||||
|
$('retention_days').value = s.retention_days ?? 14;
|
||||||
|
$('usage_threshold').value = s.usage_threshold ?? 0.9;
|
||||||
|
['live_rtmp_port','live_http_api_port','live_http_server_port','live_rtc_port','record_rtmp_port','record_http_api_port','record_http_server_port'].forEach(id => $(id).value = s[id] ?? '');
|
||||||
|
renderCameras();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCameras() {
|
||||||
|
const tbody = $('camera_rows');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
(config.cameras || []).forEach((c, i) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td><input type="checkbox" data-i="${i}" data-k="enabled" ${c.enabled ? 'checked' : ''}></td>
|
||||||
|
<td><input data-i="${i}" data-k="name" value="${c.name ?? ''}"></td>
|
||||||
|
<td><input data-i="${i}" data-k="device" value="${c.device ?? ''}"></td>
|
||||||
|
<td><input type="number" data-i="${i}" data-k="bitrate" value="${c.bitrate ?? 2000000}"></td>
|
||||||
|
<td><input type="number" data-i="${i}" data-k="width" value="${c.width ?? 1280}"></td>
|
||||||
|
<td><input type="number" data-i="${i}" data-k="height" value="${c.height ?? 960}"></td>
|
||||||
|
<td><input type="number" data-i="${i}" data-k="fps" value="${c.fps ?? 30}"></td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectConfig() {
|
||||||
|
config.mqtt_server = config.mqtt_server || {};
|
||||||
|
Object.assign(config.mqtt_server, {
|
||||||
|
veh_id: num('veh_id'), address: $('mqtt_addr').value, port: num('mqtt_port'),
|
||||||
|
need_username_pwd: $('mqtt_need_pwd').checked, client_id: $('mqtt_client').value,
|
||||||
|
username: $('mqtt_user').value, password: $('mqtt_pass').value,
|
||||||
|
mqtt_heart_threshold: num('mqtt_keepalive')
|
||||||
|
});
|
||||||
|
config.web = { enabled:$('web_enabled').checked, bind:$('web_bind').value, port:num('web_port'), username:$('web_username').value, password:$('web_password').value };
|
||||||
|
config.srs = config.srs || {};
|
||||||
|
Object.assign(config.srs, {
|
||||||
|
root:$('srs_root').value, record_config:$('record_config').value, public_interface:$('public_interface').value,
|
||||||
|
stream_app:$('stream_app').value, live_enabled:$('live_enabled').checked, record_enabled:$('record_enabled').checked,
|
||||||
|
record_path:$('record_path').value, dvr_duration_sec:num('dvr_duration'), retention_days:num('retention_days'),
|
||||||
|
usage_threshold:Number($('usage_threshold').value),
|
||||||
|
live_rtmp_port:num('live_rtmp_port'), live_http_api_port:num('live_http_api_port'), live_http_server_port:num('live_http_server_port'),
|
||||||
|
live_rtc_port:num('live_rtc_port'), record_rtmp_port:num('record_rtmp_port'), record_http_api_port:num('record_http_api_port'),
|
||||||
|
record_http_server_port:num('record_http_server_port')
|
||||||
|
});
|
||||||
|
document.querySelectorAll('#camera_rows input').forEach(input => {
|
||||||
|
const i = Number(input.dataset.i), k = input.dataset.k;
|
||||||
|
if (!config.cameras[i]) return;
|
||||||
|
config.cameras[i][k] = input.type === 'checkbox' ? input.checked : (input.type === 'number' ? Number(input.value) : input.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadConfig() { config = await api('/api/config'); fillConfig(); }
|
||||||
|
async function loadStatus() {
|
||||||
|
const st = await api('/api/status');
|
||||||
|
const disk = st.disk || {};
|
||||||
|
$('status').innerHTML = `
|
||||||
|
<div>对外IP:<span class="pill">${st.public_ip || 'unknown'}</span></div>
|
||||||
|
<div>录像磁盘:${disk.ok ? `<span class="ok">${disk.used_percent.toFixed(1)}% used</span>` : `<span class="warn">${disk.error || 'unavailable'}</span>`}</div>
|
||||||
|
<div>服务:${(st.services||[]).map(s => `${s.name}: <span class="${s.active==='active'?'ok':'warn'}">${s.active}</span>`).join(' | ')}</div>
|
||||||
|
<div>通道:${(st.channels||[]).map(c => `${c.name} ${c.device_exists?'':'(无设备)'} ${c.running?'运行':'停止'}`).join(',')}</div>`;
|
||||||
|
}
|
||||||
|
async function saveConfig() {
|
||||||
|
try {
|
||||||
|
collectConfig();
|
||||||
|
const res = await api('/api/config', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(config, null, 2) });
|
||||||
|
setToast(res.message || '已保存,重启服务后生效', res.ok ? 'ok' : 'warn');
|
||||||
|
} catch (e) { setToast(e.message, 'warn'); }
|
||||||
|
}
|
||||||
|
async function loadAll() { try { await loadConfig(); await loadStatus(); setToast('已刷新'); } catch(e) { setToast(e.message, 'warn'); } }
|
||||||
|
loadAll();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
)HTML";
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ConfigServer::~ConfigServer() { stop(); }
|
||||||
|
|
||||||
|
bool ConfigServer::start(const std::string& config_path)
|
||||||
|
{
|
||||||
|
if (!g_app_config.web.enabled) return true;
|
||||||
|
config_path_ = config_path;
|
||||||
|
running_.store(true);
|
||||||
|
thread_ = std::thread([this]() { run(); });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigServer::stop()
|
||||||
|
{
|
||||||
|
running_.store(false);
|
||||||
|
if (server_fd_ >= 0)
|
||||||
|
{
|
||||||
|
::shutdown(server_fd_, SHUT_RDWR);
|
||||||
|
::close(server_fd_);
|
||||||
|
server_fd_ = -1;
|
||||||
|
}
|
||||||
|
if (thread_.joinable()) thread_.join();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigServer::run()
|
||||||
|
{
|
||||||
|
server_fd_ = ::socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
if (server_fd_ < 0)
|
||||||
|
{
|
||||||
|
LOG_ERROR("[ConfigServer] socket failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int opt = 1;
|
||||||
|
setsockopt(server_fd_, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
||||||
|
|
||||||
|
sockaddr_in addr{};
|
||||||
|
addr.sin_family = AF_INET;
|
||||||
|
addr.sin_port = htons(g_app_config.web.port);
|
||||||
|
if (inet_pton(AF_INET, g_app_config.web.bind.c_str(), &addr.sin_addr) != 1)
|
||||||
|
{
|
||||||
|
addr.sin_addr.s_addr = INADDR_ANY;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bind(server_fd_, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) != 0)
|
||||||
|
{
|
||||||
|
LOG_ERROR(std::string("[ConfigServer] bind failed: ") + strerror(errno));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (listen(server_fd_, 16) != 0)
|
||||||
|
{
|
||||||
|
LOG_ERROR(std::string("[ConfigServer] listen failed: ") + strerror(errno));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INFO("[ConfigServer] listening on " + g_app_config.web.bind + ":" + std::to_string(g_app_config.web.port));
|
||||||
|
|
||||||
|
while (running_.load())
|
||||||
|
{
|
||||||
|
int client = accept(server_fd_, nullptr, nullptr);
|
||||||
|
if (client < 0)
|
||||||
|
{
|
||||||
|
if (running_.load()) LOG_WARN(std::string("[ConfigServer] accept failed: ") + strerror(errno));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
handle_client(client);
|
||||||
|
close(client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigServer::handle_client(int client_fd)
|
||||||
|
{
|
||||||
|
std::string req;
|
||||||
|
char buf[4096];
|
||||||
|
while (req.find("\r\n\r\n") == std::string::npos)
|
||||||
|
{
|
||||||
|
ssize_t n = recv(client_fd, buf, sizeof(buf), 0);
|
||||||
|
if (n <= 0) return;
|
||||||
|
req.append(buf, n);
|
||||||
|
if (req.size() > 1024 * 1024) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto header_end = req.find("\r\n\r\n");
|
||||||
|
std::string headers = req.substr(0, header_end + 4);
|
||||||
|
std::string body = req.substr(header_end + 4);
|
||||||
|
|
||||||
|
std::istringstream first_line(headers.substr(0, headers.find("\r\n")));
|
||||||
|
std::string method, target, version;
|
||||||
|
first_line >> method >> target >> version;
|
||||||
|
std::string path = target.substr(0, target.find('?'));
|
||||||
|
path = url_decode(path);
|
||||||
|
|
||||||
|
size_t content_length = 0;
|
||||||
|
auto cl = headers.find("Content-Length:");
|
||||||
|
if (cl != std::string::npos)
|
||||||
|
{
|
||||||
|
cl += strlen("Content-Length:");
|
||||||
|
auto end = headers.find("\r\n", cl);
|
||||||
|
content_length = std::stoul(headers.substr(cl, end - cl));
|
||||||
|
}
|
||||||
|
while (body.size() < content_length)
|
||||||
|
{
|
||||||
|
ssize_t n = recv(client_fd, buf, sizeof(buf), 0);
|
||||||
|
if (n <= 0) break;
|
||||||
|
body.append(buf, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authorized(headers))
|
||||||
|
{
|
||||||
|
send_response(client_fd, 401, "text/plain; charset=utf-8", "Unauthorized",
|
||||||
|
{{"WWW-Authenticate", "Basic realm=\"Vehicle Video Service\""}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (method == "GET" && (path == "/" || path == "/index.html"))
|
||||||
|
{
|
||||||
|
send_response(client_fd, 200, "text/html; charset=utf-8", index_html());
|
||||||
|
}
|
||||||
|
else if (method == "GET" && path == "/api/config")
|
||||||
|
{
|
||||||
|
std::string content = read_file(config_path_);
|
||||||
|
if (content.empty()) send_response(client_fd, 500, "application/json", R"({"ok":false})");
|
||||||
|
else send_response(client_fd, 200, "application/json; charset=utf-8", content);
|
||||||
|
}
|
||||||
|
else if (method == "POST" && path == "/api/config")
|
||||||
|
{
|
||||||
|
json cfg = json::parse(body);
|
||||||
|
std::string dumped = cfg.dump(2);
|
||||||
|
if (!write_file_atomic(config_path_, dumped + "\n"))
|
||||||
|
{
|
||||||
|
send_response(client_fd, 500, "application/json", R"({"ok":false,"message":"failed to write config"})");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
std::string msg = "saved. restart services to apply runtime changes";
|
||||||
|
bool srs_ok = write_srs_configs(cfg, msg);
|
||||||
|
send_response(client_fd, 200, "application/json; charset=utf-8",
|
||||||
|
json({{"ok", srs_ok}, {"message", msg}, {"requires_restart", true}}).dump(2));
|
||||||
|
}
|
||||||
|
else if (method == "GET" && path == "/api/status")
|
||||||
|
{
|
||||||
|
send_response(client_fd, 200, "application/json; charset=utf-8", status_json().dump(2));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
send_response(client_fd, 404, "text/plain; charset=utf-8", "Not found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
send_response(client_fd, 400, "application/json; charset=utf-8",
|
||||||
|
json({{"ok", false}, {"message", e.what()}}).dump(2));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@
|
|||||||
#include <thread>
|
#include <thread>
|
||||||
|
|
||||||
#include "app_config.hpp"
|
#include "app_config.hpp"
|
||||||
|
#include "config_server.hpp"
|
||||||
#include "logger.hpp"
|
#include "logger.hpp"
|
||||||
#include "mqtt_client_wrapper.hpp"
|
#include "mqtt_client_wrapper.hpp"
|
||||||
#include "record_manager.hpp"
|
#include "record_manager.hpp"
|
||||||
@ -45,8 +46,13 @@ int main()
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const std::string config_path = get_executable_dir_file_path("config.json");
|
||||||
|
|
||||||
g_record_manager = std::make_shared<RecordManager>(g_app_config.srs.record_config);
|
g_record_manager = std::make_shared<RecordManager>(g_app_config.srs.record_config);
|
||||||
|
|
||||||
|
ConfigServer config_server;
|
||||||
|
config_server.start(config_path);
|
||||||
|
|
||||||
RTMPManager::init();
|
RTMPManager::init();
|
||||||
|
|
||||||
LOG_INFO("[MAIN] Starting all record streams...");
|
LOG_INFO("[MAIN] Starting all record streams...");
|
||||||
@ -73,6 +79,8 @@ int main()
|
|||||||
|
|
||||||
if (g_record_manager) g_record_manager->stopAutoScan();
|
if (g_record_manager) g_record_manager->stopAutoScan();
|
||||||
|
|
||||||
|
config_server.stop();
|
||||||
|
|
||||||
RTMPManager::stop_all();
|
RTMPManager::stop_all();
|
||||||
|
|
||||||
if (mqtt_thread.joinable())
|
if (mqtt_thread.joinable())
|
||||||
|
|||||||
@ -115,24 +115,25 @@ GstElement* RTMPManager::create_pipeline(const Camera& cam)
|
|||||||
" ! mpph264enc bps=" +
|
" ! mpph264enc bps=" +
|
||||||
std::to_string(bitrate) + " gop=" + std::to_string(fps) +
|
std::to_string(bitrate) + " gop=" + std::to_string(fps) +
|
||||||
" rc-mode=cbr "
|
" rc-mode=cbr "
|
||||||
" ! h264parse ! tee name=t "
|
" ! h264parse ! tee name=t ";
|
||||||
|
|
||||||
// ------ 分支1:live ------
|
if (srs.live_enabled)
|
||||||
"t. ! queue max-size-buffers=5 leaky=downstream "
|
{
|
||||||
|
pipeline_str += "t. ! queue max-size-buffers=5 leaky=downstream "
|
||||||
" ! flvmux streamable=true name=mux_live "
|
" ! flvmux streamable=true name=mux_live "
|
||||||
" ! rtmpsink location=\"" +
|
" ! rtmpsink location=\"" +
|
||||||
live_rtmp +
|
live_rtmp + "\" sync=false async=false ";
|
||||||
"\" sync=false async=false "
|
}
|
||||||
|
|
||||||
// ------ 分支2:record ------
|
if (srs.record_enabled)
|
||||||
"t. ! queue max-size-buffers=5 leaky=downstream "
|
{
|
||||||
|
pipeline_str += "t. ! queue max-size-buffers=5 leaky=downstream "
|
||||||
" ! flvmux streamable=true name=mux_record "
|
" ! flvmux streamable=true name=mux_record "
|
||||||
" ! rtmpsink location=\"" +
|
" ! rtmpsink location=\"" +
|
||||||
record_rtmp +
|
record_rtmp + "\" sync=false async=false ";
|
||||||
"\" sync=false async=false "
|
}
|
||||||
|
|
||||||
// ------ 分支3:AI ------
|
pipeline_str += "t. ! queue ! fakesink sync=false";
|
||||||
"t. ! queue ! fakesink sync=false";
|
|
||||||
|
|
||||||
GError* error = nullptr;
|
GError* error = nullptr;
|
||||||
GstElement* pipeline = gst_parse_launch(pipeline_str.c_str(), &error);
|
GstElement* pipeline = gst_parse_launch(pipeline_str.c_str(), &error);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user