diff --git a/README.md b/README.md index 6557074..03375aa 100644 --- a/README.md +++ b/README.md @@ -76,3 +76,19 @@ The application reads `config.json` next to the executable at: ```text /opt/vehicle-video-service/bin/config.json ``` + +## Web Configuration + +The service includes a small built-in configuration page: + +```text +http://: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. diff --git a/config.json b/config.json index 3ee9197..eda619f 100644 --- a/config.json +++ b/config.json @@ -13,16 +13,32 @@ "root": "/opt/vehicle-video-service/srs", "record_config": "/opt/vehicle-video-service/srs/conf/record.conf", "stream_app": "camera", + "live_enabled": true, + "record_enabled": true, "live_host": "127.0.0.1", "live_rtmp_port": 1935, "live_http_api_port": 1985, + "live_http_server_port": 8080, + "live_rtc_port": 8000, "live_vhost": "live", "record_host": "127.0.0.1", "record_rtmp_port": 2935, "record_http_api_port": 2985, + "record_http_server_port": 2980, "record_vhost": "record", + "record_path": "/media/record", + "dvr_duration_sec": 60, + "retention_days": 14, + "usage_threshold": 0.9, "public_interface": "enP2p33s0" }, + "web": { + "enabled": true, + "bind": "0.0.0.0", + "port": 18080, + "username": "admin", + "password": "admin123" + }, "cameras": [ { "device": "/dev/video0", diff --git a/include/app_config.hpp b/include/app_config.hpp index 085af46..7c6f727 100644 --- a/include/app_config.hpp +++ b/include/app_config.hpp @@ -70,22 +70,41 @@ struct SRSConfig std::string root = "/opt/vehicle-video-service/srs"; std::string record_config = "/opt/vehicle-video-service/srs/conf/record.conf"; std::string stream_app = "camera"; + bool live_enabled = true; + bool record_enabled = true; std::string live_host = "127.0.0.1"; int live_rtmp_port = 1935; int live_http_api_port = 1985; + int live_http_server_port = 8080; + int live_rtc_port = 8000; std::string live_vhost = "live"; std::string record_host = "127.0.0.1"; int record_rtmp_port = 2935; int record_http_api_port = 2985; + int record_http_server_port = 2980; 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"; }; +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 { std::vector cameras; MQTTConfig mqtt; SRSConfig srs; + WebConfig web; 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.record_config = s.value("record_config", cfg.srs.record_config); 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_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_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.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_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_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); } + 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] MQTT client ID: " + cfg.mqtt.client_id); LOG_INFO("[Config] MQTT Credentials - username: " + cfg.mqtt.username + ", password: " + cfg.mqtt.password); diff --git a/include/config_server.hpp b/include/config_server.hpp new file mode 100644 index 0000000..8ad8d31 --- /dev/null +++ b/include/config_server.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +#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 running_{false}; + int server_fd_ = -1; +}; diff --git a/scripts/package_release.sh b/scripts/package_release.sh index 6f03a89..cbfce8b 100755 --- a/scripts/package_release.sh +++ b/scripts/package_release.sh @@ -5,7 +5,6 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" APP_BIN="$ROOT_DIR/bin/vehicle_video_service" DIST_DIR="${DIST_DIR:-$ROOT_DIR/dist}" PACKAGE_NAME="${PACKAGE_NAME:-vehicle-video-service-aarch64}" -STAGE_DIR="$DIST_DIR/$PACKAGE_NAME" if [[ "${SKIP_VIDEO_BUILD:-0}" != "1" ]]; then "$ROOT_DIR/scripts/build_video.sh" @@ -63,7 +62,11 @@ if [[ ! -x "$APP_BIN" ]]; then exit 1 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" \ "$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 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" diff --git a/src/config_server.cpp b/src/config_server.cpp new file mode 100644 index 0000000..6987fc1 --- /dev/null +++ b/src/config_server.cpp @@ -0,0 +1,675 @@ +#include "config_server.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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>& 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(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(vfs.f_blocks) * vfs.f_frsize; + uint64_t avail = static_cast(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::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(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( + + + + + + Vehicle Video Service + + + +

Vehicle Video Service

+
+
+
+ + + +
+
+ +
+

运行状态

+
加载中...
+
+ +
+

管理页面

+
+ + + + + +
+
+ +
+

MQTT

+
+ + + + + + + + +
+
+ +
+

SRS 与录像

+
+ + + + + + + + + + +
+
+ +
+

端口

+
+ + + + + + + +
+
+ +
+

视频通道

+ + + +
启用名称设备码率FPS
+
+
+ + + +)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(&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)); + } +} diff --git a/src/main.cpp b/src/main.cpp index 9860055..722eaf3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -6,6 +6,7 @@ #include #include "app_config.hpp" +#include "config_server.hpp" #include "logger.hpp" #include "mqtt_client_wrapper.hpp" #include "record_manager.hpp" @@ -45,8 +46,13 @@ int main() return -1; } + const std::string config_path = get_executable_dir_file_path("config.json"); + g_record_manager = std::make_shared(g_app_config.srs.record_config); + ConfigServer config_server; + config_server.start(config_path); + RTMPManager::init(); LOG_INFO("[MAIN] Starting all record streams..."); @@ -73,6 +79,8 @@ int main() if (g_record_manager) g_record_manager->stopAutoScan(); + config_server.stop(); + RTMPManager::stop_all(); if (mqtt_thread.joinable()) diff --git a/src/rtmp_manager.cpp b/src/rtmp_manager.cpp index a1c88ad..dc099ed 100644 --- a/src/rtmp_manager.cpp +++ b/src/rtmp_manager.cpp @@ -115,24 +115,25 @@ GstElement* RTMPManager::create_pipeline(const Camera& cam) " ! mpph264enc bps=" + std::to_string(bitrate) + " gop=" + std::to_string(fps) + " rc-mode=cbr " - " ! h264parse ! tee name=t " + " ! h264parse ! tee name=t "; - // ------ 分支1:live ------ - "t. ! queue max-size-buffers=5 leaky=downstream " - " ! flvmux streamable=true name=mux_live " - " ! rtmpsink location=\"" + - live_rtmp + - "\" sync=false async=false " + if (srs.live_enabled) + { + pipeline_str += "t. ! queue max-size-buffers=5 leaky=downstream " + " ! flvmux streamable=true name=mux_live " + " ! rtmpsink location=\"" + + live_rtmp + "\" sync=false async=false "; + } - // ------ 分支2:record ------ - "t. ! queue max-size-buffers=5 leaky=downstream " - " ! flvmux streamable=true name=mux_record " - " ! rtmpsink location=\"" + - record_rtmp + - "\" sync=false async=false " + if (srs.record_enabled) + { + pipeline_str += "t. ! queue max-size-buffers=5 leaky=downstream " + " ! flvmux streamable=true name=mux_record " + " ! rtmpsink location=\"" + + record_rtmp + "\" sync=false async=false "; + } - // ------ 分支3:AI ------ - "t. ! queue ! fakesink sync=false"; + pipeline_str += "t. ! queue ! fakesink sync=false"; GError* error = nullptr; GstElement* pipeline = gst_parse_launch(pipeline_str.c_str(), &error);