Implement on-demand live video protocol
This commit is contained in:
parent
be68ee5180
commit
976fe61f94
14
cmake/toolchains/aarch64-linux-gnu.cmake
Normal file
14
cmake/toolchains/aarch64-linux-gnu.cmake
Normal file
@ -0,0 +1,14 @@
|
||||
set(CMAKE_SYSTEM_NAME Linux)
|
||||
set(CMAKE_SYSTEM_PROCESSOR aarch64)
|
||||
|
||||
set(CMAKE_C_COMPILER aarch64-linux-gnu-gcc)
|
||||
set(CMAKE_CXX_COMPILER aarch64-linux-gnu-g++)
|
||||
|
||||
set(CMAKE_FIND_ROOT_PATH /usr/aarch64-linux-gnu)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY BOTH)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE BOTH)
|
||||
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE BOTH)
|
||||
|
||||
set(ENV{PKG_CONFIG_PATH} "")
|
||||
set(ENV{PKG_CONFIG_LIBDIR} "/usr/lib/aarch64-linux-gnu/pkgconfig:/usr/share/pkgconfig")
|
||||
183
config.json
183
config.json
@ -1,116 +1,139 @@
|
||||
{
|
||||
"mqtt_server": {
|
||||
"veh_id": 20004,
|
||||
"address": "192.168.4.195",
|
||||
"port": 51883,
|
||||
"need_username_pwd": true,
|
||||
"client_id": "20004_vehmedia",
|
||||
"username": "20004_4A:69:BE:32:59:AE",
|
||||
"password": "Zxwl1234@",
|
||||
"mqtt_heart_threshold": 1000
|
||||
},
|
||||
"srs": {
|
||||
"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": [
|
||||
{
|
||||
"bitrate": 2000000,
|
||||
"device": "/dev/video0",
|
||||
"enabled": false,
|
||||
"fps": 30,
|
||||
"height": 960,
|
||||
"name": "AHD1",
|
||||
"enabled": true,
|
||||
"bitrate": 2000000,
|
||||
"width": 1280,
|
||||
"height": 960,
|
||||
"fps": 30
|
||||
"width": 1280
|
||||
},
|
||||
{
|
||||
"bitrate": 2000000,
|
||||
"device": "/dev/video1",
|
||||
"enabled": true,
|
||||
"fps": 30,
|
||||
"height": 960,
|
||||
"name": "AHD2",
|
||||
"enabled": true,
|
||||
"bitrate": 2000000,
|
||||
"width": 1280,
|
||||
"height": 960,
|
||||
"fps": 30
|
||||
"width": 1280
|
||||
},
|
||||
{
|
||||
"bitrate": 2000000,
|
||||
"device": "/dev/video2",
|
||||
"enabled": true,
|
||||
"fps": 30,
|
||||
"height": 960,
|
||||
"name": "AHD3",
|
||||
"enabled": true,
|
||||
"bitrate": 2000000,
|
||||
"width": 1280,
|
||||
"height": 960,
|
||||
"fps": 30
|
||||
"width": 1280
|
||||
},
|
||||
{
|
||||
"bitrate": 2000000,
|
||||
"device": "/dev/video3",
|
||||
"enabled": false,
|
||||
"fps": 30,
|
||||
"height": 960,
|
||||
"name": "AHD4",
|
||||
"enabled": true,
|
||||
"bitrate": 2000000,
|
||||
"width": 1280,
|
||||
"height": 960,
|
||||
"fps": 30
|
||||
"width": 1280
|
||||
},
|
||||
{
|
||||
"bitrate": 2000000,
|
||||
"device": "/dev/video11",
|
||||
"enabled": true,
|
||||
"fps": 30,
|
||||
"height": 960,
|
||||
"name": "AHD5",
|
||||
"enabled": true,
|
||||
"bitrate": 2000000,
|
||||
"width": 1280,
|
||||
"height": 960,
|
||||
"fps": 30
|
||||
"width": 1280
|
||||
},
|
||||
{
|
||||
"bitrate": 2000000,
|
||||
"device": "/dev/video12",
|
||||
"enabled": true,
|
||||
"fps": 30,
|
||||
"height": 960,
|
||||
"name": "AHD6",
|
||||
"enabled": true,
|
||||
"bitrate": 2000000,
|
||||
"width": 1280,
|
||||
"height": 960,
|
||||
"fps": 30
|
||||
"width": 1280
|
||||
},
|
||||
{
|
||||
"bitrate": 2000000,
|
||||
"device": "/dev/video13",
|
||||
"name": "AHD7",
|
||||
"enabled": true,
|
||||
"bitrate": 2000000,
|
||||
"width": 1280,
|
||||
"fps": 30,
|
||||
"height": 960,
|
||||
"fps": 30
|
||||
"name": "AHD7",
|
||||
"width": 1280
|
||||
},
|
||||
{
|
||||
"device": "/dev/video14",
|
||||
"name": "AHD8",
|
||||
"enabled": true,
|
||||
"bitrate": 2000000,
|
||||
"width": 1280,
|
||||
"device": "/dev/video14",
|
||||
"enabled": true,
|
||||
"fps": 30,
|
||||
"height": 960,
|
||||
"fps": 30
|
||||
"name": "AHD8",
|
||||
"width": 1280
|
||||
}
|
||||
]
|
||||
],
|
||||
"mqtt_server": {
|
||||
"address": "192.168.4.195",
|
||||
"client_id": "20004_vehmedia",
|
||||
"mqtt_heart_threshold": 1000,
|
||||
"need_username_pwd": true,
|
||||
"password": "Zxwl1234@",
|
||||
"port": 51883,
|
||||
"topic_profile": "protocol",
|
||||
"username": "20004_4A:69:BE:32:59:AE",
|
||||
"veh_id": 20004
|
||||
},
|
||||
"srs": {
|
||||
"dvr_duration_sec": 60,
|
||||
"live_enabled": true,
|
||||
"live_host": "192.168.4.194",
|
||||
"live_http_api_port": 11985,
|
||||
"live_http_server_port": 18080,
|
||||
"live_rtc_port": 8000,
|
||||
"live_rtmp_port": 1935,
|
||||
"live_vhost": "live",
|
||||
"public_interface": "enP2p33s0",
|
||||
"record_config": "/opt/vehicle-video-service/srs/conf/record.conf",
|
||||
"record_enabled": true,
|
||||
"record_host": "127.0.0.1",
|
||||
"record_http_api_port": 2985,
|
||||
"record_http_server_port": 2980,
|
||||
"record_path": "/media/record",
|
||||
"record_rtmp_port": 2935,
|
||||
"record_vhost": "record",
|
||||
"retention_days": 14,
|
||||
"root": "/opt/vehicle-video-service/srs",
|
||||
"stream_app": "camera",
|
||||
"usage_threshold": 0.9,
|
||||
"live_push_profile": "wan_194",
|
||||
"live_playback_profile": "wan_194",
|
||||
"live_profiles": {
|
||||
"lan_194": {
|
||||
"push_host": "192.168.4.194",
|
||||
"push_rtmp_port": 1935,
|
||||
"playback_scheme": "http",
|
||||
"playback_host": "192.168.4.194",
|
||||
"playback_http_api_port": 11985,
|
||||
"playback_rtc_eip": "192.168.4.194:8000",
|
||||
"vhost": "live"
|
||||
},
|
||||
"wan_194": {
|
||||
"push_host": "36.153.162.171",
|
||||
"push_rtmp_port": 19435,
|
||||
"playback_scheme": "http",
|
||||
"playback_host": "36.153.162.171",
|
||||
"playback_http_api_port": 11985,
|
||||
"playback_rtc_eip": "36.153.162.171:8000",
|
||||
"vhost": "live"
|
||||
}
|
||||
}
|
||||
},
|
||||
"web": {
|
||||
"bind": "0.0.0.0",
|
||||
"enabled": true,
|
||||
"password": "admin",
|
||||
"port": 18081,
|
||||
"username": "admin"
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,7 +75,9 @@ render_service "$PACKAGE_DIR/systemd/vehicle-video-service.service.in" \
|
||||
/etc/systemd/system/vehicle-video-service.service
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable --now vehicle-video-srs-live.service vehicle-video-srs-record.service vehicle-video-service.service
|
||||
systemctl disable --now vehicle-video-srs-live.service >/dev/null 2>&1 || true
|
||||
systemctl enable vehicle-video-srs-record.service vehicle-video-service.service
|
||||
systemctl restart vehicle-video-srs-record.service vehicle-video-service.service
|
||||
|
||||
warn_missing_gst_plugin v4l2src
|
||||
warn_missing_gst_plugin mpph264enc
|
||||
@ -84,4 +86,4 @@ warn_missing_gst_plugin flvmux
|
||||
warn_missing_gst_plugin rtmpsink
|
||||
|
||||
echo "Installed to $INSTALL_DIR"
|
||||
echo "Services: vehicle-video-srs-live, vehicle-video-srs-record, vehicle-video-service"
|
||||
echo "Services: vehicle-video-srs-record, vehicle-video-service"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
[Unit]
|
||||
Description=Vehicle Video Service
|
||||
After=network-online.target vehicle-video-srs-live.service vehicle-video-srs-record.service
|
||||
Wants=network-online.target vehicle-video-srs-live.service vehicle-video-srs-record.service
|
||||
After=network-online.target vehicle-video-srs-record.service
|
||||
Wants=network-online.target vehicle-video-srs-record.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
209
docs/视频协议.md
Normal file
209
docs/视频协议.md
Normal file
@ -0,0 +1,209 @@
|
||||
# 通用视频软件视频协议
|
||||
|
||||
本文档定义通用视频软件与平台之间的视频相关 MQTT 协议。
|
||||
|
||||
本版本仅将实时视频心跳和实时播放地址获取作为联调依据。录像查询、录像播放章节为预留章节,暂缓实现,本版本不作为联调依据。
|
||||
|
||||
## 1. MQTT 连接
|
||||
|
||||
连接参数由软件运行目录下的 `config.json` 提供:
|
||||
|
||||
| 字段 | 说明 |
|
||||
| --- | --- |
|
||||
| `mqtt_server.address` | MQTT 服务器地址 |
|
||||
| `mqtt_server.port` | MQTT 服务器端口 |
|
||||
| `mqtt_server.client_id` | MQTT client id |
|
||||
| `mqtt_server.username` | MQTT 用户名 |
|
||||
| `mqtt_server.password` | MQTT 密码 |
|
||||
| `mqtt_server.veh_id` | 设备/车辆唯一编号,协议 topic 中记为 `{vid}` |
|
||||
| `mqtt_server.topic_profile` | topic 配置,默认 `protocol`,可切换为 `legacy` |
|
||||
|
||||
## 2. MQTT Topic
|
||||
|
||||
默认 `topic_profile=protocol` 时使用协议主题:
|
||||
|
||||
| 用途 | Topic |
|
||||
| --- | --- |
|
||||
| 实时视频状态上报 | `/zxwl/sweeper/{vid}/video/status` |
|
||||
| 实时播放地址获取 | `/zxwl/sweeper/{vid}/video/query` |
|
||||
| 录像段查询,预留 | `/zxwl/sweeper/{vid}/record/query` |
|
||||
| 录像段播放,预留 | `/zxwl/sweeper/{vid}/record/play` |
|
||||
|
||||
兼容 `topic_profile=legacy` 时使用旧主题:
|
||||
|
||||
| 用途 | Topic |
|
||||
| --- | --- |
|
||||
| 实时视频状态上报 | `/kun/vehicle/video/status/{vid}` |
|
||||
| 实时播放地址获取 | `/kun/vehicle/video/request/{vid}` |
|
||||
| 录像段查询,预留 | `/kun/vehicle/video/record/query/{vid}` |
|
||||
| 录像段播放,预留 | `/kun/vehicle/video/record/play/{vid}` |
|
||||
|
||||
## 3. 实时视频状态上报
|
||||
|
||||
### 3.1 数据协议
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 协议 | MQTT + JSON |
|
||||
| 发送方 | 视频软件 |
|
||||
| 接收方 | 平台 |
|
||||
| 上报频率 | 默认 1s 一次,由 `mqtt_server.mqtt_heart_threshold` 配置 |
|
||||
| 默认 topic | `/zxwl/sweeper/{vid}/video/status` |
|
||||
|
||||
### 3.2 消息内容
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `timestamp` | long | 上报时间戳,13 位毫秒 |
|
||||
| `status` | int | `0`:未推流或全部异常;`1`:全部实时通道正常;`2`:部分实时通道异常 |
|
||||
| `channels` | array | 各通道实时推流状态 |
|
||||
|
||||
`channels` 对象结构:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `loc` | int | 摄像头位置索引,0-7 |
|
||||
| `running` | bool | 当前实时推流是否正常 |
|
||||
| `reason` | string/null | 不正常时的原因;正常时为 `null` |
|
||||
|
||||
服务启动默认不向 live SRS 外推实时流。未收到 `switch=1` 前,心跳中通道 `running=false`,`reason` 表示实时推流未开启。
|
||||
|
||||
### 3.3 示例
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": 1744247201000,
|
||||
"status": 0,
|
||||
"channels": [
|
||||
{
|
||||
"loc": 0,
|
||||
"running": false,
|
||||
"reason": "Live stream not requested"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 实时播放地址获取
|
||||
|
||||
### 4.1 数据协议
|
||||
|
||||
| 项目 | 内容 |
|
||||
| --- | --- |
|
||||
| 协议 | MQTT + JSON |
|
||||
| 请求方 | 平台 |
|
||||
| 应答方 | 视频软件 |
|
||||
| 默认 topic | `/zxwl/sweeper/{vid}/video/query` |
|
||||
|
||||
请求和应答使用同一个 topic,通过 `type` 和 `seqNo` 区分。
|
||||
|
||||
### 4.2 请求内容
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `type` | string | 固定为 `request` |
|
||||
| `seqNo` | long/string | 应答流水号,请求和应答保持一致 |
|
||||
| `data` | object | 请求内容 |
|
||||
|
||||
`data` 对象结构:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `switch` | int | `1`:开始向服务器推实时视频并返回播放 URL;`0`:停止向服务器推实时视频,避免持续消耗外网流量 |
|
||||
|
||||
### 4.3 开始实时推流示例
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "request",
|
||||
"seqNo": 1234567890,
|
||||
"data": {
|
||||
"switch": 1
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
应答:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "response",
|
||||
"seqNo": 1234567890,
|
||||
"data": [
|
||||
{
|
||||
"loc": 0,
|
||||
"url": "http://36.153.162.171:11985/rtc/v1/whep/?app=camera&stream=AHD1_main&vhost=live&eip=36.153.162.171:8000"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
说明:
|
||||
|
||||
- URL 使用当前 `srs.live_playback_profile` 生成。
|
||||
- 公网 profile 必须带 `eip=36.153.162.171:8000`。
|
||||
- `stream` 使用 `AHD*_main`,不带 `.flv`。
|
||||
- 切换实时推流状态时允许视频管线短暂重启。
|
||||
|
||||
### 4.4 停止实时推流示例
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "request",
|
||||
"seqNo": 1234567891,
|
||||
"data": {
|
||||
"switch": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
应答:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "response",
|
||||
"seqNo": 1234567891,
|
||||
"data": []
|
||||
}
|
||||
```
|
||||
|
||||
收到 `switch=0` 后,视频软件停止向 live SRS 外推实时流;服务进程继续运行,录像分支仍由 `srs.record_enabled` 配置决定。
|
||||
|
||||
## 5. 录像段查询接口,预留
|
||||
|
||||
本章节暂缓实现,本版本不作为联调依据。当前已有录像代码不主动删除,但不纳入本次协议联调目标。
|
||||
|
||||
预留 topic:
|
||||
|
||||
```text
|
||||
/zxwl/sweeper/{vid}/record/query
|
||||
```
|
||||
|
||||
预留请求字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `loc` | int | 摄像头位置索引 |
|
||||
| `startTime` | long | 查询开始时间,毫秒 |
|
||||
| `endTime` | long | 查询结束时间,毫秒 |
|
||||
|
||||
## 6. 录像段播放接口,预留
|
||||
|
||||
本章节暂缓实现,本版本不作为联调依据。当前已有录像代码不主动删除,但不纳入本次协议联调目标。
|
||||
|
||||
预留 topic:
|
||||
|
||||
```text
|
||||
/zxwl/sweeper/{vid}/record/play
|
||||
```
|
||||
|
||||
预留请求字段:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `loc` | int | 摄像头位置索引 |
|
||||
| `segmentId` | string | 录像段 ID |
|
||||
@ -4,6 +4,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#include <fstream>
|
||||
#include <map>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@ -39,12 +40,21 @@ struct VehicleMQTTTopics
|
||||
std::string record_query;
|
||||
std::string record_play;
|
||||
|
||||
void fill_with_veh_id(const std::string& vehId)
|
||||
void fill_with_veh_id(const std::string& vehId, const std::string& profile)
|
||||
{
|
||||
heartbeat_up = "/kun/vehicle/video/status/" + vehId;
|
||||
video_down = "/kun/vehicle/video/request/" + vehId;
|
||||
record_query = "/kun/vehicle/video/record/query/" + vehId;
|
||||
record_play = "/kun/vehicle/video/record/play/" + vehId;
|
||||
if (profile == "legacy")
|
||||
{
|
||||
heartbeat_up = "/kun/vehicle/video/status/" + vehId;
|
||||
video_down = "/kun/vehicle/video/request/" + vehId;
|
||||
record_query = "/kun/vehicle/video/record/query/" + vehId;
|
||||
record_play = "/kun/vehicle/video/record/play/" + vehId;
|
||||
return;
|
||||
}
|
||||
|
||||
heartbeat_up = "/zxwl/sweeper/" + vehId + "/video/status";
|
||||
video_down = "/zxwl/sweeper/" + vehId + "/video/query";
|
||||
record_query = "/zxwl/sweeper/" + vehId + "/record/query";
|
||||
record_play = "/zxwl/sweeper/" + vehId + "/record/play";
|
||||
}
|
||||
};
|
||||
|
||||
@ -57,6 +67,7 @@ struct MQTTConfig
|
||||
std::string client_id;
|
||||
std::string username;
|
||||
std::string password;
|
||||
std::string topic_profile = "protocol";
|
||||
int keep_alive = 2000;
|
||||
|
||||
int qos = 1;
|
||||
@ -67,11 +78,24 @@ struct MQTTConfig
|
||||
|
||||
struct SRSConfig
|
||||
{
|
||||
struct LiveProfile
|
||||
{
|
||||
std::string push_host;
|
||||
int push_rtmp_port = 0;
|
||||
std::string playback_scheme = "http";
|
||||
std::string playback_host;
|
||||
int playback_http_api_port = 0;
|
||||
std::string playback_rtc_eip;
|
||||
std::string vhost = "live";
|
||||
};
|
||||
|
||||
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_push_profile = "wan_194";
|
||||
std::string live_playback_profile = "wan_194";
|
||||
std::string live_host = "127.0.0.1";
|
||||
int live_rtmp_port = 1935;
|
||||
int live_http_api_port = 1985;
|
||||
@ -88,6 +112,10 @@ struct SRSConfig
|
||||
int retention_days = 14;
|
||||
double usage_threshold = 0.90;
|
||||
std::string public_interface = "enP2p33s0";
|
||||
std::map<std::string, LiveProfile> live_profiles = {
|
||||
{"lan_194", {"192.168.4.194", 1935, "http", "192.168.4.194", 11985, "192.168.4.194:8000", "live"}},
|
||||
{"wan_194", {"36.153.162.171", 19435, "http", "36.153.162.171", 11985, "36.153.162.171:8000", "live"}},
|
||||
};
|
||||
};
|
||||
|
||||
struct WebConfig
|
||||
@ -152,8 +180,9 @@ struct AppConfig
|
||||
cfg.mqtt.client_id = m.value("client_id", "");
|
||||
cfg.mqtt.username = m.value("username", "");
|
||||
cfg.mqtt.password = m.value("password", "");
|
||||
cfg.mqtt.topic_profile = m.value("topic_profile", cfg.mqtt.topic_profile);
|
||||
cfg.mqtt.keep_alive = m.value("mqtt_heart_threshold", cfg.mqtt.keep_alive);
|
||||
cfg.mqtt.topics.fill_with_veh_id(cfg.mqtt.vehicle_id);
|
||||
cfg.mqtt.topics.fill_with_veh_id(cfg.mqtt.vehicle_id, cfg.mqtt.topic_profile);
|
||||
|
||||
if (j.contains("srs"))
|
||||
{
|
||||
@ -163,6 +192,8 @@ struct AppConfig
|
||||
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_push_profile = s.value("live_push_profile", cfg.srs.live_push_profile);
|
||||
cfg.srs.live_playback_profile = s.value("live_playback_profile", cfg.srs.live_playback_profile);
|
||||
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);
|
||||
@ -179,6 +210,26 @@ struct AppConfig
|
||||
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 (s.contains("live_profiles") && s["live_profiles"].is_object())
|
||||
{
|
||||
cfg.srs.live_profiles.clear();
|
||||
for (auto it = s["live_profiles"].begin(); it != s["live_profiles"].end(); ++it)
|
||||
{
|
||||
if (!it.value().is_object()) continue;
|
||||
|
||||
SRSConfig::LiveProfile profile;
|
||||
const auto& p = it.value();
|
||||
profile.push_host = p.value("push_host", profile.push_host);
|
||||
profile.push_rtmp_port = p.value("push_rtmp_port", profile.push_rtmp_port);
|
||||
profile.playback_scheme = p.value("playback_scheme", profile.playback_scheme);
|
||||
profile.playback_host = p.value("playback_host", profile.playback_host);
|
||||
profile.playback_http_api_port =
|
||||
p.value("playback_http_api_port", profile.playback_http_api_port);
|
||||
profile.playback_rtc_eip = p.value("playback_rtc_eip", profile.playback_rtc_eip);
|
||||
profile.vhost = p.value("vhost", profile.vhost);
|
||||
cfg.srs.live_profiles[it.key()] = profile;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (j.contains("web"))
|
||||
@ -194,6 +245,7 @@ struct AppConfig
|
||||
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);
|
||||
LOG_INFO("[Config] MQTT topic profile: " + cfg.mqtt.topic_profile);
|
||||
LOG_INFO("[Config] MQTT Topics: " + cfg.mqtt.topics.heartbeat_up + ", " + cfg.mqtt.topics.video_down);
|
||||
LOG_INFO("[Config] MQTT keepAlive: " + std::to_string(cfg.mqtt.keep_alive));
|
||||
LOG_INFO("[Config] SRS root: " + cfg.srs.root);
|
||||
|
||||
@ -9,7 +9,7 @@ struct VideoPushRequest
|
||||
|
||||
struct DataItem
|
||||
{
|
||||
int switchVal; // 0=开始推流, 1=停止推流
|
||||
int switchVal; // 1=start live stream, 0=stop live stream
|
||||
std::vector<int> channels;
|
||||
int streamType; // 0=主, 1=子
|
||||
};
|
||||
|
||||
@ -28,6 +28,9 @@ class RTMPManager
|
||||
static void init();
|
||||
static void start_all();
|
||||
static void stop_all();
|
||||
static void restart_all();
|
||||
static void set_live_requested(bool requested);
|
||||
static bool live_requested();
|
||||
static bool is_streaming(const std::string& cam_name);
|
||||
static std::string get_stream_url(const std::string& cam_name);
|
||||
|
||||
@ -59,6 +62,7 @@ class RTMPManager
|
||||
|
||||
static std::unordered_map<std::string, std::unique_ptr<StreamContext>> streams;
|
||||
static std::mutex streams_mutex;
|
||||
static std::atomic<bool> live_requested_;
|
||||
|
||||
static constexpr int RETRY_BASE_DELAY_MS = 3000;
|
||||
};
|
||||
|
||||
17
scripts/build_video_aarch64_wsl.sh
Normal file
17
scripts/build_video_aarch64_wsl.sh
Normal file
@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
BUILD_DIR="${BUILD_DIR:-$ROOT_DIR/build-aarch64}"
|
||||
|
||||
GENERATOR_ARGS=()
|
||||
if command -v ninja >/dev/null 2>&1; then
|
||||
GENERATOR_ARGS=(-G Ninja)
|
||||
fi
|
||||
|
||||
cmake -S "$ROOT_DIR" -B "$BUILD_DIR" \
|
||||
-DCMAKE_TOOLCHAIN_FILE="$ROOT_DIR/cmake/toolchains/aarch64-linux-gnu.cmake" \
|
||||
"${GENERATOR_ARGS[@]}"
|
||||
cmake --build "$BUILD_DIR" --target vehicle_video_service --parallel "$(nproc 2>/dev/null || echo 4)"
|
||||
|
||||
echo "Built $ROOT_DIR/bin/vehicle_video_service"
|
||||
@ -4,7 +4,7 @@ set -euo pipefail
|
||||
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}"
|
||||
PACKAGE_NAME="${PACKAGE_NAME:-vehicle-video-service-wan-aarch64}"
|
||||
|
||||
if [[ "${SKIP_VIDEO_BUILD:-0}" != "1" ]]; then
|
||||
"$ROOT_DIR/scripts/build_video.sh"
|
||||
@ -15,6 +15,7 @@ find_srs_bin() {
|
||||
"${SRS_BIN:-}"
|
||||
"$ROOT_DIR/build/srs/objs/srs"
|
||||
"$ROOT_DIR/external/srs-server-5.0-r3/trunk/objs/srs"
|
||||
"/opt/vehicle-video-service/srs/bin/srs"
|
||||
"/home/aiec/srs-server-5.0-r3/trunk/objs/srs"
|
||||
"/home/aiec/srs/bin/srs"
|
||||
)
|
||||
@ -33,6 +34,7 @@ find_srs_html() {
|
||||
local candidates=(
|
||||
"${SRS_HTML_DIR:-}"
|
||||
"$ROOT_DIR/external/srs-server-5.0-r3/trunk/research/players"
|
||||
"/opt/vehicle-video-service/srs/html"
|
||||
"/home/aiec/srs/html"
|
||||
"$ROOT_DIR/deploy/srs/html"
|
||||
)
|
||||
|
||||
@ -245,8 +245,7 @@ json status_json()
|
||||
.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"),
|
||||
st["services"] = json::array({service_active("vehicle-video-srs-record.service"),
|
||||
service_active("vehicle-video-service.service")});
|
||||
st["channels"] = json::array();
|
||||
auto channels = RTMPManager::get_all_channels_status();
|
||||
@ -370,6 +369,7 @@ const char* index_html()
|
||||
<label>Client ID<input id="mqtt_client"></label>
|
||||
<label>用户名<input id="mqtt_user"></label>
|
||||
<label>密码<input id="mqtt_pass" type="password"></label>
|
||||
<label>Topic Profile<select id="mqtt_topic_profile"><option value="protocol">protocol</option><option value="legacy">legacy</option></select></label>
|
||||
<label>使用账号密码<input id="mqtt_need_pwd" type="checkbox"></label>
|
||||
</div>
|
||||
</section>
|
||||
@ -381,6 +381,12 @@ const char* index_html()
|
||||
<label>录像配置文件<input id="record_config"></label>
|
||||
<label>对外网卡<input id="public_interface"></label>
|
||||
<label>应用名<input id="stream_app"></label>
|
||||
<label>推流环境<select id="live_push_profile"></select></label>
|
||||
<label>播放环境<select id="live_playback_profile"></select></label>
|
||||
<label>Live Host<input id="live_host"></label>
|
||||
<label>Live VHost<input id="live_vhost"></label>
|
||||
<label>Record Host<input id="record_host"></label>
|
||||
<label>Record VHost<input id="record_vhost"></label>
|
||||
<label>实时流启用<input id="live_enabled" type="checkbox"></label>
|
||||
<label>录像启用<input id="record_enabled" type="checkbox"></label>
|
||||
<label>录像目录<input id="record_path"></label>
|
||||
@ -415,6 +421,10 @@ const char* index_html()
|
||||
let config = {};
|
||||
const $ = id => document.getElementById(id);
|
||||
const num = id => Number($(id).value);
|
||||
const defaultLiveProfiles = {
|
||||
lan_194: { push_host:'192.168.4.194', push_rtmp_port:1935, playback_scheme:'http', playback_host:'192.168.4.194', playback_http_api_port:11985, playback_rtc_eip:'192.168.4.194:8000', vhost:'live' },
|
||||
wan_194: { push_host:'36.153.162.171', push_rtmp_port:19435, playback_scheme:'http', playback_host:'36.153.162.171', playback_http_api_port:11985, playback_rtc_eip:'36.153.162.171:8000', vhost:'live' }
|
||||
};
|
||||
|
||||
async function api(path, options) {
|
||||
const res = await fetch(path, options);
|
||||
@ -438,6 +448,12 @@ function installHelp() {
|
||||
helpFor('record_config', 'video服务读取这个SRS录像配置来解析录像目录、HTTP端口和切片时长。');
|
||||
helpFor('public_interface', '用于生成对外播放地址的网卡名,比如 enP2p33s0;取不到IP会回退到127.0.0.1。');
|
||||
helpFor('stream_app', 'RTMP路径里的app名,默认 camera,对应 rtmp://host/app/stream。');
|
||||
helpFor('live_push_profile', '控制实时流推到194内网地址还是公网NAT地址。');
|
||||
helpFor('live_playback_profile', '控制页面和MQTT返回194内网WHEP地址还是公网WHEP地址。');
|
||||
helpFor('live_host', '旧配置回退用的实时流RTMP主机;配置推流环境后优先使用推流环境。');
|
||||
helpFor('live_vhost', '旧配置回退用的实时流vhost。');
|
||||
helpFor('record_host', '录像保持本地时应为127.0.0.1。');
|
||||
helpFor('record_vhost', '录像SRS的vhost。');
|
||||
helpFor('live_enabled', '控制是否向 live SRS 推实时流,关闭后不会生成实时播放分支。');
|
||||
helpFor('record_enabled', '控制是否向 record SRS 推流并由SRS写录像。');
|
||||
helpFor('record_path', 'SRS DVR保存mp4切片的根目录,目标设备上要确保可写。');
|
||||
@ -451,6 +467,16 @@ function installHelp() {
|
||||
helpFor('record_rtmp_port', 'video服务把录像流推到这个RTMP端口。');
|
||||
helpFor('record_http_api_port', 'record实例的SRS HTTP API端口。');
|
||||
helpFor('record_http_server_port', '录像文件HTTP访问端口,回放URL会走这个HTTP server。');
|
||||
helpFor('mqtt_topic_profile', 'protocol uses /zxwl/sweeper/{vid}/video/... topics; legacy uses /kun/vehicle/video/... topics.');
|
||||
}
|
||||
|
||||
function fillProfileSelect(id, selected) {
|
||||
const s = config.srs || {};
|
||||
const profiles = Object.keys(s.live_profiles || defaultLiveProfiles);
|
||||
const value = selected || profiles[0] || '';
|
||||
const options = profiles.includes(value) ? profiles : [value, ...profiles];
|
||||
$(id).innerHTML = options.map(p => `<option value="${escapeHtml(p)}">${escapeHtml(p)}</option>`).join('');
|
||||
$(id).value = value;
|
||||
}
|
||||
|
||||
function fillConfig() {
|
||||
@ -464,6 +490,7 @@ function fillConfig() {
|
||||
$('mqtt_client').value = m.client_id ?? '';
|
||||
$('mqtt_user').value = m.username ?? '';
|
||||
$('mqtt_pass').value = m.password ?? '';
|
||||
$('mqtt_topic_profile').value = m.topic_profile ?? 'protocol';
|
||||
$('mqtt_need_pwd').checked = !!m.need_username_pwd;
|
||||
$('web_enabled').checked = w.enabled ?? true;
|
||||
$('web_bind').value = w.bind ?? '0.0.0.0';
|
||||
@ -474,6 +501,12 @@ function fillConfig() {
|
||||
$('record_config').value = s.record_config ?? '';
|
||||
$('public_interface').value = s.public_interface ?? 'enP2p33s0';
|
||||
$('stream_app').value = s.stream_app ?? 'camera';
|
||||
fillProfileSelect('live_push_profile', s.live_push_profile ?? 'wan_194');
|
||||
fillProfileSelect('live_playback_profile', s.live_playback_profile ?? 'wan_194');
|
||||
$('live_host').value = s.live_host ?? '127.0.0.1';
|
||||
$('live_vhost').value = s.live_vhost ?? 'live';
|
||||
$('record_host').value = s.record_host ?? '127.0.0.1';
|
||||
$('record_vhost').value = s.record_vhost ?? 'record';
|
||||
$('live_enabled').checked = s.live_enabled ?? true;
|
||||
$('record_enabled').checked = s.record_enabled ?? true;
|
||||
$('record_path').value = s.record_path ?? '/media/record';
|
||||
@ -507,12 +540,19 @@ function collectConfig() {
|
||||
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,
|
||||
topic_profile: $('mqtt_topic_profile').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 || {};
|
||||
if (!config.srs.live_profiles || Object.keys(config.srs.live_profiles).length === 0) {
|
||||
config.srs.live_profiles = defaultLiveProfiles;
|
||||
}
|
||||
Object.assign(config.srs, {
|
||||
root:$('srs_root').value, record_config:$('record_config').value, public_interface:$('public_interface').value,
|
||||
live_push_profile:$('live_push_profile').value, live_playback_profile:$('live_playback_profile').value,
|
||||
live_host:$('live_host').value, live_vhost:$('live_vhost').value,
|
||||
record_host:$('record_host').value, record_vhost:$('record_vhost').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),
|
||||
|
||||
@ -55,7 +55,7 @@ int main()
|
||||
|
||||
RTMPManager::init();
|
||||
|
||||
LOG_INFO("[MAIN] Starting all record streams...");
|
||||
LOG_INFO("[MAIN] Starting initial video pipelines...");
|
||||
RTMPManager::start_all();
|
||||
|
||||
std::thread mqtt_thread(
|
||||
|
||||
@ -31,7 +31,7 @@ static void send_heartbeat()
|
||||
auto channels_info = RTMPManager::get_all_channels_status();
|
||||
|
||||
nlohmann::json channels = nlohmann::json::array();
|
||||
int total = static_cast<int>(channels_info.size());
|
||||
int total = 0;
|
||||
int running_count = 0;
|
||||
|
||||
for (const auto& ch : channels_info)
|
||||
@ -40,6 +40,8 @@ static void send_heartbeat()
|
||||
item["loc"] = ch.loc;
|
||||
item["running"] = ch.running;
|
||||
|
||||
if (ch.reason != "Camera disabled") total++;
|
||||
|
||||
if (ch.running)
|
||||
{
|
||||
item["reason"] = nullptr;
|
||||
@ -55,8 +57,8 @@ static void send_heartbeat()
|
||||
|
||||
nlohmann::json hb;
|
||||
hb["timestamp"] = ms;
|
||||
hb["status"] = (running_count == 0) ? 0 // 全部失败
|
||||
: (running_count == total ? 1 : 2); // 全部正常 or 部分异常
|
||||
hb["status"] = (total == 0 || running_count == 0) ? 0
|
||||
: (running_count == total ? 1 : 2);
|
||||
hb["channels"] = channels;
|
||||
|
||||
// 发布心跳
|
||||
@ -84,27 +86,48 @@ static void handle_video_down_request(const nlohmann::json& req)
|
||||
return;
|
||||
}
|
||||
|
||||
int sw = req["data"].value("switch", 0);
|
||||
if (sw != 1)
|
||||
if (!req["data"].contains("switch"))
|
||||
{
|
||||
LOG_INFO("[video_down] switch != 1, ignore");
|
||||
LOG_WARN("[video_down] Missing switch");
|
||||
return;
|
||||
}
|
||||
|
||||
// 取得当前所有 RTMP 播放地址
|
||||
auto channels_info = RTMPManager::get_all_channels_status();
|
||||
|
||||
// 构造响应
|
||||
int sw = req["data"].value("switch", -1);
|
||||
nlohmann::json resp;
|
||||
resp["type"] = "response";
|
||||
resp["seqNo"] = req.value("seqNo", "0");
|
||||
resp["seqNo"] = req.contains("seqNo") ? req["seqNo"] : nlohmann::json("0");
|
||||
resp["data"] = nlohmann::json::array();
|
||||
|
||||
int loc = 0;
|
||||
for (const auto& ch : channels_info)
|
||||
if (sw == 1)
|
||||
{
|
||||
resp["data"].push_back({{"loc", loc}, {"url", ch.url}});
|
||||
loc++;
|
||||
if (!g_app_config.srs.live_enabled)
|
||||
{
|
||||
LOG_WARN("[video_down] switch=1 ignored because srs.live_enabled=false");
|
||||
}
|
||||
else
|
||||
{
|
||||
bool restart_needed = !RTMPManager::live_requested();
|
||||
RTMPManager::set_live_requested(true);
|
||||
if (restart_needed) RTMPManager::restart_all();
|
||||
|
||||
for (size_t i = 0; i < g_app_config.cameras.size(); ++i)
|
||||
{
|
||||
const auto& cam = g_app_config.cameras[i];
|
||||
if (!cam.enabled) continue;
|
||||
resp["data"].push_back({{"loc", static_cast<int>(i)}, {"url", RTMPManager::get_stream_url(cam.name)}});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (sw == 0)
|
||||
{
|
||||
bool restart_needed = RTMPManager::live_requested();
|
||||
RTMPManager::set_live_requested(false);
|
||||
if (restart_needed) RTMPManager::restart_all();
|
||||
}
|
||||
else
|
||||
{
|
||||
LOG_WARN("[video_down] Invalid switch: " + std::to_string(sw));
|
||||
return;
|
||||
}
|
||||
|
||||
// 发布回复
|
||||
|
||||
@ -52,6 +52,7 @@ std::string get_ip_address(const std::string& ifname)
|
||||
// ========== 静态成员 ==========
|
||||
std::unordered_map<std::string, std::unique_ptr<RTMPManager::StreamContext>> RTMPManager::streams;
|
||||
std::mutex RTMPManager::streams_mutex;
|
||||
std::atomic<bool> RTMPManager::live_requested_{false};
|
||||
|
||||
// ========== 初始化 ==========
|
||||
void RTMPManager::init()
|
||||
@ -62,6 +63,54 @@ void RTMPManager::init()
|
||||
|
||||
std::string RTMPManager::make_key(const std::string& name) { return name + "_main"; }
|
||||
|
||||
static const SRSConfig::LiveProfile* find_live_profile(const std::string& name)
|
||||
{
|
||||
const auto& profiles = g_app_config.srs.live_profiles;
|
||||
auto it = profiles.find(name);
|
||||
if (it == profiles.end()) return nullptr;
|
||||
return &it->second;
|
||||
}
|
||||
|
||||
static std::string live_vhost_or_default(const SRSConfig::LiveProfile* profile)
|
||||
{
|
||||
if (profile && !profile->vhost.empty()) return profile->vhost;
|
||||
return g_app_config.srs.live_vhost;
|
||||
}
|
||||
|
||||
static std::string make_live_rtmp_url(const std::string& stream_name)
|
||||
{
|
||||
const auto& srs = g_app_config.srs;
|
||||
const auto* profile = find_live_profile(srs.live_push_profile);
|
||||
if (profile && !profile->push_host.empty() && profile->push_rtmp_port > 0)
|
||||
{
|
||||
return "rtmp://" + profile->push_host + ":" + std::to_string(profile->push_rtmp_port) + "/" +
|
||||
srs.stream_app + "/" + stream_name + "?vhost=" + live_vhost_or_default(profile);
|
||||
}
|
||||
|
||||
return "rtmp://" + srs.live_host + ":" + std::to_string(srs.live_rtmp_port) + "/" + srs.stream_app + "/" +
|
||||
stream_name + "?vhost=" + srs.live_vhost;
|
||||
}
|
||||
|
||||
static std::string make_live_whep_url(const std::string& stream_name)
|
||||
{
|
||||
const auto& srs = g_app_config.srs;
|
||||
const auto* profile = find_live_profile(srs.live_playback_profile);
|
||||
if (profile && !profile->playback_host.empty() && profile->playback_http_api_port > 0)
|
||||
{
|
||||
const std::string scheme = profile->playback_scheme.empty() ? "http" : profile->playback_scheme;
|
||||
std::string url = scheme + "://" + profile->playback_host + ":" +
|
||||
std::to_string(profile->playback_http_api_port) + "/rtc/v1/whep/?app=" + srs.stream_app +
|
||||
"&stream=" + stream_name + "&vhost=" + live_vhost_or_default(profile);
|
||||
if (!profile->playback_rtc_eip.empty()) url += "&eip=" + profile->playback_rtc_eip;
|
||||
return url;
|
||||
}
|
||||
|
||||
std::string ip = get_ip_address(srs.public_interface);
|
||||
if (ip.empty()) ip = "127.0.0.1";
|
||||
return "http://" + ip + ":" + std::to_string(srs.live_http_api_port) + "/rtc/v1/whep/?app=" + srs.stream_app +
|
||||
"&stream=" + stream_name + "&vhost=" + srs.live_vhost;
|
||||
}
|
||||
|
||||
// ========== 创建推流管线 ==========
|
||||
GstElement* RTMPManager::create_pipeline(const Camera& cam)
|
||||
{
|
||||
@ -72,8 +121,7 @@ GstElement* RTMPManager::create_pipeline(const Camera& cam)
|
||||
|
||||
const std::string stream_name = cam.name + "_main";
|
||||
const auto& srs = g_app_config.srs;
|
||||
const std::string live_rtmp = "rtmp://" + srs.live_host + ":" + std::to_string(srs.live_rtmp_port) + "/" +
|
||||
srs.stream_app + "/" + stream_name + "?vhost=" + srs.live_vhost;
|
||||
const std::string live_rtmp = make_live_rtmp_url(stream_name);
|
||||
const std::string record_rtmp = "rtmp://" + srs.record_host + ":" + std::to_string(srs.record_rtmp_port) + "/" +
|
||||
srs.stream_app + "/" + stream_name + "?vhost=" + srs.record_vhost;
|
||||
|
||||
@ -117,7 +165,7 @@ GstElement* RTMPManager::create_pipeline(const Camera& cam)
|
||||
" rc-mode=cbr "
|
||||
" ! h264parse ! tee name=t ";
|
||||
|
||||
if (srs.live_enabled)
|
||||
if (srs.live_enabled && live_requested_.load())
|
||||
{
|
||||
pipeline_str += "t. ! queue max-size-buffers=5 leaky=downstream "
|
||||
" ! flvmux streamable=true name=mux_live "
|
||||
@ -331,9 +379,17 @@ void RTMPManager::stream_loop(Camera cam, StreamContext* ctx)
|
||||
// ========== 启停与状态 ==========
|
||||
void RTMPManager::start_all()
|
||||
{
|
||||
LOG_INFO("[RTMP] Starting all record streams...");
|
||||
LOG_INFO("[RTMP] Starting all enabled streams...");
|
||||
std::lock_guard<std::mutex> lock(streams_mutex);
|
||||
|
||||
const bool need_record = g_app_config.srs.record_enabled;
|
||||
const bool need_live = g_app_config.srs.live_enabled && live_requested_.load();
|
||||
if (!need_record && !need_live)
|
||||
{
|
||||
LOG_INFO("[RTMP] Neither record nor live streaming is enabled, skip camera pipelines");
|
||||
return;
|
||||
}
|
||||
|
||||
int delay_ms = 0;
|
||||
for (auto& cam : g_app_config.cameras)
|
||||
{
|
||||
@ -374,6 +430,16 @@ void RTMPManager::stop_all()
|
||||
streams.clear();
|
||||
}
|
||||
|
||||
void RTMPManager::restart_all()
|
||||
{
|
||||
stop_all();
|
||||
start_all();
|
||||
}
|
||||
|
||||
void RTMPManager::set_live_requested(bool requested) { live_requested_.store(requested); }
|
||||
|
||||
bool RTMPManager::live_requested() { return live_requested_.load(); }
|
||||
|
||||
bool RTMPManager::is_streaming(const std::string& cam_name)
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(streams_mutex);
|
||||
@ -387,11 +453,7 @@ bool RTMPManager::is_streaming(const std::string& cam_name)
|
||||
|
||||
std::string RTMPManager::get_stream_url(const std::string& cam_name)
|
||||
{
|
||||
const auto& srs = g_app_config.srs;
|
||||
std::string ip = get_ip_address(srs.public_interface);
|
||||
if (ip.empty()) ip = "127.0.0.1";
|
||||
return "http://" + ip + ":" + std::to_string(srs.live_http_api_port) + "/rtc/v1/whep/?app=" + srs.stream_app +
|
||||
"&stream=" + cam_name + "_main&vhost=" + srs.live_vhost;
|
||||
return make_live_whep_url(cam_name + "_main");
|
||||
}
|
||||
// ========== 汇总状态 ==========
|
||||
std::vector<RTMPManager::ChannelInfo> RTMPManager::get_all_channels_status()
|
||||
@ -408,6 +470,20 @@ std::vector<RTMPManager::ChannelInfo> RTMPManager::get_all_channels_status()
|
||||
ch.loc = static_cast<int>(i);
|
||||
ch.url.clear();
|
||||
ch.running = false;
|
||||
ch.reason = cam.enabled ? "Live stream not requested" : "Camera disabled";
|
||||
|
||||
if (!cam.enabled)
|
||||
{
|
||||
result.push_back(ch);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!live_requested_.load())
|
||||
{
|
||||
result.push_back(ch);
|
||||
continue;
|
||||
}
|
||||
|
||||
ch.reason = "Not started";
|
||||
|
||||
auto it = streams.find(key);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user