Commit dced7c62 authored by 957dd's avatar 957dd

优化了播报与游览器之间的竞争问题

parent 055aa314
No preview for this file type
...@@ -236,9 +236,9 @@ static const deviceconfig_t s_device_configs[] = { ...@@ -236,9 +236,9 @@ static const deviceconfig_t s_device_configs[] = {
bool get_array_length(const int* arr) { bool get_array_length(const int* arr) {
if(arr[0] != -1) { // 遇到-1停止计数 if(arr[0] != -1) { // 遇到-1停止计数
return TRUE; return true;
} }
return FALSE; return false;
} }
void device_init(int device_id) { void device_init(int device_id) {
...@@ -258,9 +258,9 @@ void device_init(int device_id) { ...@@ -258,9 +258,9 @@ void device_init(int device_id) {
} }
// 执行初始化流程 // 执行初始化流程
if(get_array_length(config->gpio_pins)==TRUE)init_gpiowpi(config->gpio_pins); // GPIO初始化 if(get_array_length(config->gpio_pins))init_gpiowpi(config->gpio_pins); // GPIO初始化
if(get_array_length(config->gpio_pwms)==TRUE)init_gpiopwm(config->gpio_pwms); // GPIOsoft_pwm初始化 if(get_array_length(config->gpio_pwms))init_gpiopwm(config->gpio_pwms); // GPIOsoft_pwm初始化
if(get_array_length(config->gpio_inputs)==TRUE) init_gpio_input(config->gpio_inputs); if(get_array_length(config->gpio_inputs)) init_gpio_input(config->gpio_inputs);
g_device_type =config->emergency_code; g_device_type =config->emergency_code;
config->device_pwm_init(); // PWM初始化 config->device_pwm_init(); // PWM初始化
config->device_control_stop(); // 速度控制初始化 config->device_control_stop(); // 速度控制初始化
......
#include "device_init.h"
#include "devcontrol_common.h"
#include "common.h"
#include "gpio_init.h"
#include "car0107_idle.h"
int g_device_delay_back_count =0;//设备计时,比如坦克打击倒退逻辑
int g_device_type =0; //设备详细类型,让其他地方判断,比如tank0202、car0101
static const deviceconfig_t s_device_configs[] = {
// car0101配置
{
.device_id = DEVICE_CAR0101,
.device_name = "car0101",
.gpio_pins = {5, 6, 7, 10, 16, 20, 22, 23, 24, 25, 26, 27, -1},
.gpio_pwms = {-1},
.gpio_inputs={-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = car0101_middle_pwm,
.emergency_code = 101
},
// car0102配置
{
.device_id = DEVICE_CAR0102,
.device_name = "car0102",
.gpio_pins = {5, 6, 7, 10, 16, 20, 22, 23, 24, 25, 26, 27, -1},
.gpio_pwms = {5, 7, -1},
.gpio_inputs={-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = car0102_speed_stop,
.emergency_code = 102
},
// car0103配置(根据您的需求补充)
{
.device_id = DEVICE_CAR0103,
.device_name = "car0103",
.gpio_pins = {5, 6, 7, 9 , 10, 16, 20, 22, 23, 24, 25, 26, 27,-1},/* 补充GPIO引脚 */
.gpio_pwms = {-1},
.gpio_inputs={11,12,14,17,18,20,-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = car0103_middle,/* 补充速度控制函数 */
.emergency_code = 103
},
// car0104配置(根据您的需求补充)
{
.device_id = DEVICE_CAR0104,
.device_name = "car0104",
.gpio_pins = {5, 6, 7, 9,10, 16, 20, 22, 23, 24, 25, 26, 27,-1},/* 补充GPIO引脚 */
.gpio_pwms = {-1},
.gpio_inputs={11,12,14,20,-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = car0104_stop,/* 补充速度控制函数 */
.emergency_code = 104
},
// car0105配置(根据您的需求补充)
{
.device_id = DEVICE_CAR0105,
.device_name = "car0105",
.gpio_pins = {5, 6, 7, 9,10, 16, 20, 22, 23, 24, 25, 26, -1},/* 补充GPIO引脚 */
.gpio_pwms = {27,-1},
.gpio_inputs={-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = car0101_middle_pwm,/* 补充速度控制函数 */
.emergency_code = 105
},
// car0106 F1赛车(双电机大板): 软PWM 5/7/24/26 对应前后左右
{
.device_id = DEVICE_CAR0106,
.device_name = "car0106",
.gpio_pins = {-1},
.gpio_pwms = {5, 7, 24, 26, -1},
.gpio_inputs={-1},
.device_pwm_init = car0106_pwm_init_empty,
.device_control_stop = car0106_stop,
.emergency_code = 106
},
// car0107 大众POLO R(与0101相同硬件,前后方向相反)
{
.device_id = DEVICE_CAR0107,
.device_name = "car0107",
.gpio_pins = {5, 6, 7, 10, 16, 20, 22, 23, 24, 25, 26, 27, -1},
.gpio_pwms = {-1},
.gpio_inputs={-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = car0107_middle_pwm,
.emergency_code = 107
},
// tank0202配置(根据您的需求补充)
{
.device_id = DEVICE_TANK0202,
.device_name = "tank0202",
.gpio_pins = {6, 16, 20, 22,-1},/* 补充GPIO引脚 */
.gpio_pwms = {5 , 7 ,24,26, 27,-1},
.gpio_inputs={12,-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = tank0202_middle,/* 补充速度控制函数 */
.emergency_code = 202
},
{
.device_id = DEVICE_TANK0203,
.device_name = "tank0203",
.gpio_pins = {6, 16, 20, 22,-1},/* 补充GPIO引脚 */
.gpio_pwms = {5 , 7 ,24, 26, 27,-1},
.gpio_inputs={12,-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = tank0203_middle,/* 补充速度控制函数 */
.emergency_code = 203
},
{
.device_id = DEVICE_TANK0204,
.device_name = "tank0204",
.gpio_pins = {6, 16, 20, 22,-1},/* 补充GPIO引脚 */
.gpio_pwms = {5 , 7 ,24, 26, 27,-1},
.gpio_inputs={12,-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = tank0204_stop,/* 补充速度控制函数 */
.emergency_code = 204
},
{
.device_id = DEVICE_TANK0206,
.device_name = "tank0206",
.gpio_pins = {6, 16, 20, 22, 23,-1},/* 补充GPIO引脚 */
.gpio_pwms = {5 , 7 ,24,26, 27,-1},
.gpio_inputs={-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = tank0206_middle,/* 补充速度控制函数 */
.emergency_code = 206
},
{
.device_id = DEVICE_TANK0207,
.device_name = "tank0207",
.gpio_pins = {6, 16, 20, 22, 23,-1},/* 补充GPIO引脚 */
.gpio_pwms = {5 , 7 ,24,26, 27,-1},
.gpio_inputs={-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = tank0207_middle,/* 补充速度控制函数 */
.emergency_code = 207
},
{
.device_id = DEVICE_SHIP0301,
.device_name = "ship0301",
.gpio_pins = {6, 16, 20, 22, 23,-1},/* 补充GPIO引脚 */
.gpio_pwms = {5 , 7 ,24,26, 27,-1},
.gpio_inputs={-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = ship0301_stop,/* 补充速度控制函数 */
.emergency_code = 301
},
{
.device_id = DEVICE_SHIP0302,
.device_name = "ship0302",
.gpio_pins = {6, 16, 20, 22, 23,-1},
.gpio_pwms = {5 , 7 ,24,26, 27,-1},
.gpio_inputs={-1},
.device_pwm_init = device_ship0302_init,
.device_control_stop = ship0302_stop,
.emergency_code = 302
},
{
.device_id = DEVICE_PAO_PTZ0401,
.device_name = "ptz0401",
.gpio_pins = {5, 6, 7, 10, 16, 20, 22, 23, 24, 25, 26,27,-1},/* 补充GPIO引脚 */
.gpio_pwms = { -1},
.gpio_inputs={-1},
.device_pwm_init = ptz_driver_pwm_hz,
.device_control_stop = ptz_driver_init,
.emergency_code = 401
},
{
.device_id = DEVICE_PAO_PTZ0404,
.device_name = "ptz0404",
.gpio_pins = {5, 6, 7, 10, 16, 20, 22, 23, 24, 25, 26,27,-1},/* 补充GPIO引脚 */
.gpio_pwms = { -1},
.gpio_inputs={-1},
.device_pwm_init = ptz_driver_pwm_hz,
.device_control_stop = ptz_driver_init,
.emergency_code = 404
},
{
.device_id = DEVICE_PAO_PTZ0405,
.device_name = "ptz0405",
.gpio_pins = {5, 7, 10, 16, 20, 22, 23, 24, 25, 26,27,-1},
.gpio_pwms = {-1},
.gpio_inputs={-1},
.device_pwm_init = ptz_driver_pwm_hz,
.device_control_stop = ptz_driver_init,
.emergency_code = 405
},
{
.device_id = DEVICE_PAO_PTZ0406,
.device_name = "ptz0406",
.gpio_pins = {5, 7, 10, 16, 20, 22, 23, 24, 25, 26,27,-1},
.gpio_pwms = {-1},
.gpio_inputs={-1},
.device_pwm_init = ptz_driver_pwm_hz,
.device_control_stop = ptz_driver_init,
.emergency_code = 406
},
{
.device_id = DEVICE_PG_GPS0403,
.device_name = "gps0403",
.gpio_pins = {5, 6, 7, 10, 16, 20, 22, 23, 24, 25, 26,-1},/* 补充GPIO引脚 */
.gpio_pwms = { 27,-1},
.gpio_inputs={-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = car0101_middle_pwm,/* 补充速度控制函数 */
.emergency_code = 403
},
{
.device_id = DEVICE_ROBOT_DOG0501,
.device_name = "dog0501",
.gpio_pins = {6, 10, 16, 20, 22, 23, 25,-1},/* 补充GPIO引脚 */
.gpio_pwms = { 5, 7,24, 26,27,-1},
.gpio_inputs={-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = car0101_middle_pwm,/* 补充速度控制函数 */
.emergency_code = 501
},
{
.device_id = DEVICE_ROBOT_ARM0502,
.device_name = "arm0502",
.gpio_pins = {5, 6, 7, 10, 16, 20, 22, 23, 24, 25, 26,-1},
.gpio_pwms = { 27,-1},
.gpio_inputs={-1},
.device_pwm_init = physics_pwm_init,
.device_control_stop = arm0502_control_stop_init,
.emergency_code = 502
},
// 结束标记
{ .device_id = -1 }
};
bool get_array_length(const int* arr) {
if(arr[0] != -1) { // 遇到-1停止计数
return TRUE;
}
return FALSE;
}
void device_init(int device_id) {
const deviceconfig_t *config = NULL;
// 查找设备配置
for(int i = 0; s_device_configs[i].device_id != -1; i++) {
if(s_device_configs[i].device_id == device_id) {
config = &s_device_configs[i];
break;
}
}
if(!config) {
my_zlog_error("Error: Device ID %d not found!", device_id);
return;
}
// 执行初始化流程
if(get_array_length(config->gpio_pins)==TRUE)init_gpiowpi(config->gpio_pins); // GPIO初始化
if(get_array_length(config->gpio_pwms)==TRUE)init_gpiopwm(config->gpio_pwms); // GPIOsoft_pwm初始化
if(get_array_length(config->gpio_inputs)==TRUE) init_gpio_input(config->gpio_inputs);
g_device_type =config->emergency_code;
config->device_pwm_init(); // PWM初始化
config->device_control_stop(); // 速度控制初始化
if (device_id == DEVICE_CAR0102) {
car0102_pin27_startup(device_id);
}
if (device_id == DEVICE_CAR0107) {
car0107_idle_startup(device_id);
}
my_zlog_debug("%s initialized successfully!", config->device_name);
}
\ No newline at end of file
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
#define AUDIO_USB_ALSA_DEVICE "hw:2,0" #define AUDIO_USB_ALSA_DEVICE "hw:2,0"
#define AUDIO_LOCAL_ALSA_DEVICE "plughw:2,0" #define AUDIO_LOCAL_ALSA_DEVICE "plughw:2,0"
#define AUDIO_LOCAL_PLAY_TIMEOUT_SEC 8 #define AUDIO_ANNOUNCEMENT_TIMEOUT_SEC 20
static int s_audio_status=7; static int s_audio_status=7;
static char s_urlbuf[512]; static char s_urlbuf[512];
...@@ -189,36 +189,104 @@ static void shell_single_quote(char *out, size_t size, const char *in) { ...@@ -189,36 +189,104 @@ static void shell_single_quote(char *out, size_t size, const char *in) {
out[pos] = '\0'; out[pos] = '\0';
} }
static int play_local_audio_file(const char *filepath) { static int pulse_suspend_default_output(int suspend) {
const char *command = suspend
? "pactl suspend-sink @DEFAULT_SINK@ 1 >/dev/null 2>&1"
: "pactl suspend-sink @DEFAULT_SINK@ 0 >/dev/null 2>&1";
int exit_code = audio_system_exit_code(system(command));
if (exit_code != 0) {
my_zlog_warn("%s浏览器音频输出失败 exit=%d",
suspend ? "暂停" : "恢复", exit_code);
}
return exit_code;
}
int audioplay_file_with_browser_preempt(const char *filepath, double volume) {
char quoted_path[1024]; char quoted_path[1024];
char command[2048]; char command[2048];
int ret; int ret;
int exit_code; int exit_code;
int pulse_suspended;
if (filepath == NULL || filepath[0] == '\0') {
return -1;
}
shell_single_quote(quoted_path, sizeof(quoted_path), filepath); shell_single_quote(quoted_path, sizeof(quoted_path), filepath);
volume = audioplay_volume_clamp(volume);
/* 排队等待USB声卡: 如果audio_sink正在播放手机音频, 等它释放 */ /* 先和项目内 DataChannel/本地音频排队,再让浏览器临时释放 USB 声卡。 */
audio_sink_lock_alsa(); audio_sink_lock_alsa();
pulse_suspended = (pulse_suspend_default_output(1) == 0);
if (pulse_suspended) {
snprintf(command, sizeof(command),
/* timeout 使用 KILL,保证卡死的播放进程到 20 秒时立即释放 ALSA。 */
"timeout -s KILL %ds gst-launch-1.0 -q filesrc location=%s ! mpegaudioparse ! mpg123audiodec ! audioconvert ! volume volume=%.3f ! audioresample ! audio/x-raw,channels=2 ! alsasink device=%s sync=true >/dev/null 2>&1",
AUDIO_ANNOUNCEMENT_TIMEOUT_SEC, quoted_path, volume,
AUDIO_LOCAL_ALSA_DEVICE);
} else {
/* PulseAudio 无法暂停时走其默认输出,避免直接打开同一声卡导致 device busy。 */
snprintf(command, sizeof(command), snprintf(command, sizeof(command),
/* sync=true 防止 EOS 提前关 ALSA 丢尾部(只播前半段); channels=2 上混规避 USB 声卡单声道 ring_buffer CRITICAL */ "timeout -s KILL %ds ffplay -nodisp -autoexit -loglevel warning -af \"volume=%.3f\" %s >/dev/null 2>&1",
"timeout %ds gst-launch-1.0 -q filesrc location=%s ! mpegaudioparse ! mpg123audiodec ! audioconvert ! audioresample ! audio/x-raw,channels=2 ! alsasink device=%s sync=true >/dev/null 2>&1", AUDIO_ANNOUNCEMENT_TIMEOUT_SEC, volume, quoted_path);
AUDIO_LOCAL_PLAY_TIMEOUT_SEC, quoted_path, AUDIO_LOCAL_ALSA_DEVICE); }
ret = system(command); ret = system(command);
exit_code = audio_system_exit_code(ret); exit_code = audio_system_exit_code(ret);
if (exit_code != 0) { /* 无论正常结束、播放失败还是 timeout,退出前都恢复浏览器音频。 */
my_zlog_warn("本地音频 GStreamer 播放失败 exit=%d,尝试 ffplay: %s", if (pulse_suspended) {
exit_code, filepath); pulse_suspend_default_output(0);
}
audio_sink_unlock_alsa();
if (exit_code == 137) {
my_zlog_warn("播报超过 %d 秒,已强制停止并释放声卡: %s",
AUDIO_ANNOUNCEMENT_TIMEOUT_SEC, filepath);
}
return exit_code;
}
int audioplay_url_with_browser_preempt(const char *url, double volume) {
char quoted_url[1024];
char command[2048];
int exit_code;
int pulse_suspended;
if (url == NULL || url[0] == '\0') {
return -1;
}
shell_single_quote(quoted_url, sizeof(quoted_url), url);
volume = audioplay_volume_clamp(volume);
audio_sink_lock_alsa();
pulse_suspended = (pulse_suspend_default_output(1) == 0);
if (pulse_suspended) {
snprintf(command, sizeof(command), snprintf(command, sizeof(command),
"timeout %ds ffplay -nodisp -autoexit -loglevel warning %s", "timeout -s KILL %ds ffmpeg -nostdin -hide_banner -loglevel warning -i %s -af \"volume=%.3f\" -f alsa %s >/dev/null 2>&1",
AUDIO_LOCAL_PLAY_TIMEOUT_SEC, quoted_path); AUDIO_ANNOUNCEMENT_TIMEOUT_SEC, quoted_url, volume,
ret = system(command); AUDIO_LOCAL_ALSA_DEVICE);
exit_code = audio_system_exit_code(ret); } else {
snprintf(command, sizeof(command),
"timeout -s KILL %ds ffplay -nodisp -autoexit -loglevel warning -af \"volume=%.3f\" %s >/dev/null 2>&1",
AUDIO_ANNOUNCEMENT_TIMEOUT_SEC, volume, quoted_url);
} }
exit_code = audio_system_exit_code(system(command));
if (pulse_suspended) {
pulse_suspend_default_output(0);
}
audio_sink_unlock_alsa(); audio_sink_unlock_alsa();
if (exit_code == 137) {
my_zlog_warn("网络播报超过 %d 秒,已强制停止并释放声卡: %s",
AUDIO_ANNOUNCEMENT_TIMEOUT_SEC, url);
}
return exit_code; return exit_code;
} }
...@@ -291,26 +359,21 @@ void audioplay_send_mqtt() { ...@@ -291,26 +359,21 @@ void audioplay_send_mqtt() {
//音频播放 //音频播放
void audioplay_cycle(){ void audioplay_cycle(){
char command[2048];
int ret; int ret;
while(1){ while(1){
if(s_audio_status==0){ if(s_audio_status==0){
char *urlmoddle=s_urlbuf; char *urlmoddle=s_urlbuf;
s_audio_volume = audioplay_volume_clamp(s_audio_volume); s_audio_volume = audioplay_volume_clamp(s_audio_volume);
snprintf(command, sizeof(command),
"sudo ffplay -nodisp -autoexit -loglevel quiet -af \"volume=%.3f\" \"%s\"",
s_audio_volume,urlmoddle);
my_zlog_debug("播放地址: %s", s_urlbuf); my_zlog_debug("播放地址: %s", s_urlbuf);
my_zlog_debug("执行播放命令: %s", command); ret = audioplay_url_with_browser_preempt(urlmoddle, s_audio_volume);
ret = system(command);
if (ret != 0) { if (ret != 0) {
my_zlog_error("播放失败"); my_zlog_error("播放失败");
s_audio_status=2; s_audio_status=2;
} }
if (WIFEXITED(ret) && WEXITSTATUS(ret) == 0) { if (ret == 0) {
my_zlog_debug("播放已成功完成 : %s ", s_urlbuf); my_zlog_debug("播放已成功完成 : %s ", s_urlbuf);
s_audio_status=1; s_audio_status=1;
} else { } else {
...@@ -334,7 +397,7 @@ void audioplay_cycle(){ ...@@ -334,7 +397,7 @@ void audioplay_cycle(){
if (local_play_pending) { if (local_play_pending) {
int exit_code; int exit_code;
my_zlog_info("播放本地音频: %s", local_filepath); my_zlog_info("播放本地音频: %s", local_filepath);
exit_code = play_local_audio_file(local_filepath); exit_code = audioplay_file_with_browser_preempt(local_filepath, 1.0);
if (exit_code == 0) { if (exit_code == 0) {
my_zlog_debug("本地音频播放完成: %s", local_filepath); my_zlog_debug("本地音频播放完成: %s", local_filepath);
} else { } else {
......
This diff is collapsed.
...@@ -6,6 +6,13 @@ void audioplay_mqtt_receive(cJSON *body); //接收音频mqtt播放函数 ...@@ -6,6 +6,13 @@ void audioplay_mqtt_receive(cJSON *body); //接收音频mqtt播放函数
void audioplay_local_mqtt_receive(cJSON *body); // 2017 有人驾驶本地音频 void audioplay_local_mqtt_receive(cJSON *body); // 2017 有人驾驶本地音频
void audioplay_cycle();//音频播放线程中函数 void audioplay_cycle();//音频播放线程中函数
/*
* 播放播报文件:与项目内其它 ALSA 播放串行,并在播放期间暂停浏览器使用的
* PulseAudio 默认输出。播放完成或超过 20 秒后会恢复浏览器音频。
*/
int audioplay_file_with_browser_preempt(const char *filepath, double volume);
int audioplay_url_with_browser_preempt(const char *url, double volume);
int audio_wheat_init(); int audio_wheat_init();
int audio_speaker_init(); int audio_speaker_init();
......
#ifndef AUDIOPLAY_H__
#define AUDIOPLAY_H__
#include <cjson/cJSON.h>
void audioplay_mqtt_receive(cJSON *body); //接收音频mqtt播放函数
void audioplay_local_mqtt_receive(cJSON *body); // 2017 有人驾驶本地音频
void audioplay_cycle();//音频播放线程中函数
int audio_wheat_init();
int audio_speaker_init();
int audio_init();
int audio_config_init();//加入配置
#endif
\ No newline at end of file
...@@ -471,46 +471,15 @@ void video_tts_play() { ...@@ -471,46 +471,15 @@ void video_tts_play() {
return; return;
} }
pid_t pid = fork(); /* audioplay_cycle 本身运行在音频线程,这里同步等待可确保声卡一定被恢复。 */
if (pid == 0) { int exit_code = audioplay_file_with_browser_preempt(TTS_ADUINO_PTAH, s_volume);
// 子进程:创建新的会话组,脱离父进程组 if (exit_code == 0) {
setsid(); // 关键!创建新的会话组 my_zlog_info("TTS 播报完成, volume: %.1f", s_volume);
} else {
char command[256]; my_zlog_warn("TTS 播报失败或超时 exit=%d, volume: %.1f", exit_code, s_volume);
snprintf(command, sizeof(command),
"ffmpeg -i " TTS_ADUINO_PTAH " -af \"volume=%.1f\" -f alsa default 2>/dev/null",
s_volume);
// 重定向标准输入输出,避免占用终端
if (freopen("/dev/null", "r", stdin) == NULL) {
my_zlog_error("重定向stdin失败: %s", strerror(errno));
// 可以选择退出或继续
}
if (freopen("/dev/null", "w", stdout) == NULL) {
my_zlog_error("重定向stdout失败: %s", strerror(errno));
}
if (freopen("/dev/null", "w", stderr) == NULL) {
my_zlog_error("重定向stderr失败: %s", strerror(errno));
}
int res = system(command);
_exit(res); // 使用_exit避免清理父进程资源
}
else if (pid > 0) {
// 父进程:非阻塞等待,避免僵尸进程
int status;
waitpid(pid, &status, WNOHANG); // 不阻塞的等待
my_zlog_debug("启动独立播放进程 PID: %d", pid);
s_audio_tts_index = 0;
} }
else {
my_zlog_error("fork()失败");
s_audio_tts_index = 0; s_audio_tts_index = 0;
} }
}
} }
......
This diff is collapsed.
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
#include "common.h" #include "common.h"
#define DEFAULT_MQTT_BROKER_ADDRESS "119.45.167.177" #define DEFAULT_MQTT_BROKER_ADDRESS "mqtt177.controlmelive.com"
#define DEVICE_DEFAULT_VIDEO "https://jywy.yd-ss.com?dev=" #define DEVICE_DEFAULT_VIDEO "https://jywy.yd-ss.com?dev="
typedef struct{ typedef struct{
......
...@@ -213,7 +213,7 @@ static void refresh_cam_browser_only(const char *reason) ...@@ -213,7 +213,7 @@ static void refresh_cam_browser_only(const char *reason)
void message_2_judyverify(cJSON *body) void message_2_judyverify(cJSON *body)
{ {
if (VERIFIED_MODE == FALSE) if (VERIFIED_MODE == false)
{ {
refresh_cam_browser_only("MQTT type2 verify off"); refresh_cam_browser_only("MQTT type2 verify off");
my_zlog_warn("不使用验证"); my_zlog_warn("不使用验证");
...@@ -234,7 +234,7 @@ void message_2_judyverify(cJSON *body) ...@@ -234,7 +234,7 @@ void message_2_judyverify(cJSON *body)
// 当接收到3时候验证 // 当接收到3时候验证
void message_3_judyverify(cJSON *body) void message_3_judyverify(cJSON *body)
{ {
if (VERIFIED_MODE == FALSE) if (VERIFIED_MODE == false)
{ {
message_3(body); message_3(body);
my_zlog_warn("不使用验证"); my_zlog_warn("不使用验证");
...@@ -255,7 +255,7 @@ void message_3_judyverify(cJSON *body) ...@@ -255,7 +255,7 @@ void message_3_judyverify(cJSON *body)
// 当接收到4时候验证 // 当接收到4时候验证
void message_4_judyverify(cJSON *body) void message_4_judyverify(cJSON *body)
{ {
if (VERIFIED_MODE == FALSE) if (VERIFIED_MODE == false)
{ {
message_4(body); message_4(body);
my_zlog_warn("不使用验证"); my_zlog_warn("不使用验证");
...@@ -276,7 +276,7 @@ void message_4_judyverify(cJSON *body) ...@@ -276,7 +276,7 @@ void message_4_judyverify(cJSON *body)
void message_6_steering_judyverify(cJSON *body) void message_6_steering_judyverify(cJSON *body)
{ {
if (VERIFIED_MODE == FALSE) if (VERIFIED_MODE == false)
{ {
steering_mqtt_recv(body); steering_mqtt_recv(body);
my_zlog_warn("不使用验证"); my_zlog_warn("不使用验证");
...@@ -303,7 +303,7 @@ void message_7_joystick_judyverify(cJSON *body) ...@@ -303,7 +303,7 @@ void message_7_joystick_judyverify(cJSON *body)
return; return;
} }
if (VERIFIED_MODE == FALSE) if (VERIFIED_MODE == false)
{ {
ptz_driver_set_joystick_json(joystick_ctrl); ptz_driver_set_joystick_json(joystick_ctrl);
my_zlog_warn("不使用验证"); my_zlog_warn("不使用验证");
......
...@@ -9,9 +9,9 @@ ...@@ -9,9 +9,9 @@
#include "webrtcpush_run.h" #include "webrtcpush_run.h"
/*TRUE为打开验证,FALSE为关闭验证*/ /*TRUE为打开验证,FALSE为关闭验证*/
bool VERIFIED_MODE=FALSE; bool VERIFIED_MODE=false;
static bool s_service_verify=TRUE;//验证判断 static bool s_service_verify=true;//验证判断
int g_verify_count =6000;//判断是否有一分钟 int g_verify_count =6000;//判断是否有一分钟
...@@ -19,7 +19,7 @@ static char s_only_id_middle[11]; ...@@ -19,7 +19,7 @@ static char s_only_id_middle[11];
static char s_secret_key[1024];//存储上一次的topic static char s_secret_key[1024];//存储上一次的topic
static bool s_secret_key_index = TRUE;//用于判断是否对比topic static bool s_secret_key_index = true;//用于判断是否对比topic
/* topic 不同时发后端验证,失败可重试,超过 3 次不通过则判定该 topic 不通过 */ /* topic 不同时发后端验证,失败可重试,超过 3 次不通过则判定该 topic 不通过 */
static char s_pending_verify_token[1024]; static char s_pending_verify_token[1024];
...@@ -172,7 +172,7 @@ int receive_jwt(cJSON *body) { ...@@ -172,7 +172,7 @@ int receive_jwt(cJSON *body) {
my_zlog_debug("token时间戳:%1d",(long)token_time_sec); my_zlog_debug("token时间戳:%1d",(long)token_time_sec);
if(token_time_sec>current_verify_time){ if(token_time_sec>current_verify_time){
if(s_service_verify == TRUE) g_verify_index=0;//验证默认为正确 if(s_service_verify == true) g_verify_index=0;//验证默认为正确
my_zlog_debug("g_verify_index= %d ",g_verify_index); my_zlog_debug("g_verify_index= %d ",g_verify_index);
if(g_verify_count>12000){ if(g_verify_count>12000){
send_jwtser(token); send_jwtser(token);
...@@ -205,7 +205,7 @@ int message2006_verify(cJSON *body){ ...@@ -205,7 +205,7 @@ int message2006_verify(cJSON *body){
if(json_status == NULL || json_onlyid==NULL) { if(json_status == NULL || json_onlyid==NULL) {
my_zlog_warn("验证为空"); my_zlog_warn("验证为空");
s_service_verify = FALSE; s_service_verify = false;
g_verify_index=1; g_verify_index=1;
my_zlog_debug("g_verify_index= %d ",g_verify_index); my_zlog_debug("g_verify_index= %d ",g_verify_index);
return 1; return 1;
...@@ -219,10 +219,10 @@ int message2006_verify(cJSON *body){ ...@@ -219,10 +219,10 @@ int message2006_verify(cJSON *body){
gboolean driver_changed = (s_secret_key[0] != '\0' && gboolean driver_changed = (s_secret_key[0] != '\0' &&
strcmp(s_secret_key, s_pending_verify_token) != 0); strcmp(s_secret_key, s_pending_verify_token) != 0);
my_zlog_info("获得验证正确, onlyid=%s", onlyid); my_zlog_info("获得验证正确, onlyid=%s", onlyid);
s_service_verify = TRUE; s_service_verify = true;
g_verify_index = 0; /* 后端通过后必须清除失败标志,否则正确 token 仍会提示验证不通过 */ g_verify_index = 0; /* 后端通过后必须清除失败标志,否则正确 token 仍会提示验证不通过 */
strcpy(s_secret_key, s_pending_verify_token); strcpy(s_secret_key, s_pending_verify_token);
s_secret_key_index = FALSE; s_secret_key_index = false;
s_verify_retry_count = 0; s_verify_retry_count = 0;
s_verify_retry_after_time = 0; s_verify_retry_after_time = 0;
if (driver_changed) if (driver_changed)
...@@ -238,13 +238,13 @@ int message2006_verify(cJSON *body){ ...@@ -238,13 +238,13 @@ int message2006_verify(cJSON *body){
} else { } else {
my_zlog_warn("topic验证超过%d次不通过,禁止使用, token=%s", VERIFY_RETRY_MAX, s_pending_verify_token); my_zlog_warn("topic验证超过%d次不通过,禁止使用, token=%s", VERIFY_RETRY_MAX, s_pending_verify_token);
g_verify_index = 1; g_verify_index = 1;
s_service_verify = FALSE; s_service_verify = false;
return 2; return 2;
} }
} else { } else {
my_zlog_warn("获得验证错误,禁止使用, onlyid=%s status=%s", onlyid, status); my_zlog_warn("获得验证错误,禁止使用, onlyid=%s status=%s", onlyid, status);
g_verify_index = 1; g_verify_index = 1;
s_service_verify = FALSE; s_service_verify = false;
return 2; return 2;
} }
} }
...@@ -297,14 +297,14 @@ int message2013_recverigy_open(cJSON *body_raw) ...@@ -297,14 +297,14 @@ int message2013_recverigy_open(cJSON *body_raw)
s_verify_open_query_gave_up = false; s_verify_open_query_gave_up = false;
if (verify_status->valueint == 1) { if (verify_status->valueint == 1) {
VERIFIED_MODE = TRUE; VERIFIED_MODE = true;
my_zlog_info("开启验证成功(加密校验通过), verify_status=1"); my_zlog_info("开启验证成功(加密校验通过), verify_status=1");
cJSON_Delete(body); cJSON_Delete(body);
return 0; return 0;
} }
if (verify_status->valueint == 0) { if (verify_status->valueint == 0) {
VERIFIED_MODE = FALSE; VERIFIED_MODE = false;
my_zlog_info("关闭验证成功(加密校验通过), verify_status=0"); my_zlog_info("关闭验证成功(加密校验通过), verify_status=0");
cJSON_Delete(body); cJSON_Delete(body);
return 0; return 0;
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
typedef struct { typedef struct {
gchar *candidate; gchar *candidate;
gchar *mid; gchar *mid;
gchar *ufrag;
} PendingRemoteIce; } PendingRemoteIce;
typedef struct { typedef struct {
...@@ -277,6 +278,7 @@ static void free_pending_remote_ice(gpointer data) ...@@ -277,6 +278,7 @@ static void free_pending_remote_ice(gpointer data)
return; return;
g_free(ice->candidate); g_free(ice->candidate);
g_free(ice->mid); g_free(ice->mid);
g_free(ice->ufrag);
g_free(ice); g_free(ice);
} }
...@@ -1990,14 +1992,24 @@ static void flush_remote_ice(RtcClient *client) ...@@ -1990,14 +1992,24 @@ static void flush_remote_ice(RtcClient *client)
for (;;) { for (;;) {
PendingRemoteIce *ice; PendingRemoteIce *ice;
int pc; int pc;
gboolean ufrag_match;
g_mutex_lock(&client->lock); g_mutex_lock(&client->lock);
ice = g_queue_pop_head(client->pending_remote_ice); ice = g_queue_pop_head(client->pending_remote_ice);
pc = client->pc; pc = client->pc;
ufrag_match = !ice || !ice->ufrag || !client->remote_ice_ufrag ||
g_strcmp0(ice->ufrag, client->remote_ice_ufrag) == 0;
g_mutex_unlock(&client->lock); g_mutex_unlock(&client->lock);
if (!ice) if (!ice)
break; break;
if (pc >= 0) if (pc >= 0 && ufrag_match) {
rtcAddRemoteCandidate(pc, ice->candidate, ice->mid); if (rtcAddRemoteCandidate(pc, ice->candidate, ice->mid) < 0)
my_zlog_warn("libdatachannel: rejected queued remote ICE candidate ufrag=%s",
ice->ufrag ? ice->ufrag : "?");
} else if (!ufrag_match) {
my_zlog_info("libdatachannel: discard stale remote ICE candidate ufrag=%s current=%s",
ice->ufrag ? ice->ufrag : "?",
client->remote_ice_ufrag ? client->remote_ice_ufrag : "?");
}
free_pending_remote_ice(ice); free_pending_remote_ice(ice);
} }
} }
...@@ -2095,8 +2107,12 @@ int rtc_client_start(AppState *app) ...@@ -2095,8 +2107,12 @@ int rtc_client_start(AppState *app)
void rtc_client_reset(AppState *app) void rtc_client_reset(AppState *app)
{ {
RtcClient *client = client_from_app(app); RtcClient *client = client_from_app(app);
if (client) if (client) {
destroy_peer(client); destroy_peer(client);
g_mutex_lock(&client->lock);
g_queue_clear_full(client->pending_remote_ice, free_pending_remote_ice);
g_mutex_unlock(&client->lock);
}
} }
void rtc_client_stop(AppState *app) void rtc_client_stop(AppState *app)
...@@ -2285,6 +2301,9 @@ void rtc_client_handle_remote_ice(AppState *app, JsonObject *candidate_obj) ...@@ -2285,6 +2301,9 @@ void rtc_client_handle_remote_ice(AppState *app, JsonObject *candidate_obj)
RtcClient *client = client_from_app(app); RtcClient *client = client_from_app(app);
const gchar *candidate; const gchar *candidate;
const gchar *mid = NULL; const gchar *mid = NULL;
const gchar *ufrag = NULL;
gchar *candidate_ufrag = NULL;
gboolean queued = FALSE;
int pc; int pc;
if (!client || !candidate_obj || if (!client || !candidate_obj ||
...@@ -2299,18 +2318,48 @@ void rtc_client_handle_remote_ice(AppState *app, JsonObject *candidate_obj) ...@@ -2299,18 +2318,48 @@ void rtc_client_handle_remote_ice(AppState *app, JsonObject *candidate_obj)
json_node_get_value_type(node) == G_TYPE_STRING) json_node_get_value_type(node) == G_TYPE_STRING)
mid = json_object_get_string_member(candidate_obj, "sdpMid"); mid = json_object_get_string_member(candidate_obj, "sdpMid");
} }
if (json_object_has_member(candidate_obj, "usernameFragment")) {
JsonNode *node = json_object_get_member(candidate_obj, "usernameFragment");
if (JSON_NODE_HOLDS_VALUE(node) &&
json_node_get_value_type(node) == G_TYPE_STRING)
ufrag = json_object_get_string_member(candidate_obj, "usernameFragment");
} else if (json_object_has_member(candidate_obj, "ufrag")) {
JsonNode *node = json_object_get_member(candidate_obj, "ufrag");
if (JSON_NODE_HOLDS_VALUE(node) &&
json_node_get_value_type(node) == G_TYPE_STRING)
ufrag = json_object_get_string_member(candidate_obj, "ufrag");
}
if (!ufrag) {
const gchar *tag = strstr(candidate, " ufrag ");
if (tag) {
const gchar *end;
tag += strlen(" ufrag ");
end = strchr(tag, ' ');
candidate_ufrag = end ? g_strndup(tag, (gsize)(end - tag)) : g_strdup(tag);
ufrag = candidate_ufrag;
}
}
g_mutex_lock(&client->lock); g_mutex_lock(&client->lock);
pc = client->pc; pc = client->pc;
if (pc < 0) { if (pc < 0 ||
(ufrag && client->remote_ice_ufrag &&
g_strcmp0(ufrag, client->remote_ice_ufrag) != 0)) {
PendingRemoteIce *pending = g_new0(PendingRemoteIce, 1); PendingRemoteIce *pending = g_new0(PendingRemoteIce, 1);
pending->candidate = g_strdup(candidate); pending->candidate = g_strdup(candidate);
pending->mid = g_strdup(mid); pending->mid = g_strdup(mid);
pending->ufrag = g_strdup(ufrag);
g_queue_push_tail(client->pending_remote_ice, pending); g_queue_push_tail(client->pending_remote_ice, pending);
pc = -1;
queued = TRUE;
} }
g_mutex_unlock(&client->lock); g_mutex_unlock(&client->lock);
if (pc >= 0 && rtcAddRemoteCandidate(pc, candidate, mid) < 0) if (pc >= 0 && rtcAddRemoteCandidate(pc, candidate, mid) < 0)
my_zlog_warn("libdatachannel: rejected remote ICE candidate"); my_zlog_warn("libdatachannel: rejected remote ICE candidate");
else if (queued)
my_zlog_info("libdatachannel: queued remote ICE candidate for ufrag=%s",
ufrag ? ufrag : "?");
g_free(candidate_ufrag);
} }
gboolean rtc_client_is_active(AppState *app) gboolean rtc_client_is_active(AppState *app)
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
* *
* 原生分支:v4l2 采集 → mpph264enc(或 x264enc 回退)→ libdatachannel 发送。 * 原生分支:v4l2 采集 → mpph264enc(或 x264enc 回退)→ libdatachannel 发送。
*/ */
#define WEBRTCPUSH_USE_MPP 1 #define WEBRTCPUSH_USE_MPP 0
/* 正常固定 24fps;弱网以降码率为主,后续若启用动态帧率也不得低于 22fps。 */ /* 正常固定 24fps;弱网以降码率为主,后续若启用动态帧率也不得低于 22fps。 */
#define WEBRTCPUSH_H264_FPS 24 #define WEBRTCPUSH_H264_FPS 24
...@@ -17,10 +17,10 @@ ...@@ -17,10 +17,10 @@
/* /*
* 与 gst_webrtc_pipeline / jywy 浏览器推流对齐的码率策略。 * 与 gst_webrtc_pipeline / jywy 浏览器推流对齐的码率策略。
* 首屏先用 900kbps,不像 1.4Mbps 那样猛冲,也不要低到一进来就糊。 * 首屏先用 1.06Mbps,不像高码率那样猛冲,也不要低到一进来就糊。
* RTCP REMB 只作为码率趋势:小步慢降、慢升、带滞回,尽量接近浏览器的无感自适应。 * RTCP REMB 只作为码率趋势:小步慢降、慢升、带滞回,尽量接近浏览器的无感自适应。
*/ */
#define WEBRTCPUSH_INITIAL_BITRATE 900000U #define WEBRTCPUSH_INITIAL_BITRATE 1060000U
#define WEBRTCPUSH_MIN_BITRATE 800000U #define WEBRTCPUSH_MIN_BITRATE 800000U
#define WEBRTCPUSH_MAX_BITRATE 3000000U #define WEBRTCPUSH_MAX_BITRATE 3000000U
#define WEBRTCPUSH_REMB_UTIL_PERCENT 80U #define WEBRTCPUSH_REMB_UTIL_PERCENT 80U
...@@ -44,6 +44,8 @@ ...@@ -44,6 +44,8 @@
#define WEBRTCPUSH_REMB_SANE_PACING_MAX_MS 100U #define WEBRTCPUSH_REMB_SANE_PACING_MAX_MS 100U
/* IDR 会产生短时大包,不能在这个窗口内把瞬时 pacing 峰值误判成网络拥塞。 */ /* IDR 会产生短时大包,不能在这个窗口内把瞬时 pacing 峰值误判成网络拥塞。 */
#define WEBRTCPUSH_REMB_IDR_GRACE_MS 1000U #define WEBRTCPUSH_REMB_IDR_GRACE_MS 1000U
/* 手机切 Wi-Fi/蜂窝或换手机时,旧成员的 leave 可能晚于新 offer 到达。 */
#define WEBRTCPUSH_PEER_LEAVE_GRACE_MS 8000U
/* 达到 2.3Mbps 后继续发送一段时间,让浏览器有真实流量可重新估计带宽。 */ /* 达到 2.3Mbps 后继续发送一段时间,让浏览器有真实流量可重新估计带宽。 */
#define WEBRTCPUSH_INITIAL_PROBE_HOLD_MS 6000U #define WEBRTCPUSH_INITIAL_PROBE_HOLD_MS 6000U
#define WEBRTCPUSH_PACING_HEADROOM_PERCENT 135U #define WEBRTCPUSH_PACING_HEADROOM_PERCENT 135U
......
...@@ -23,11 +23,15 @@ static gchar *s_ws_url; ...@@ -23,11 +23,15 @@ static gchar *s_ws_url;
static GMainContext *s_main_ctx; static GMainContext *s_main_ctx;
static gchar *s_debounced_offer_sdp = NULL; static gchar *s_debounced_offer_sdp = NULL;
static guint s_debounce_offer_id = 0; static guint s_debounce_offer_id = 0;
static guint s_peer_leave_reset_id = 0;
static guint s_offer_generation = 0;
static guint s_leave_generation = 0;
static gboolean reconnect_cb(gpointer data); static gboolean reconnect_cb(gpointer data);
static void ws_connect(void); static void ws_connect(void);
static void reset_signaling_session(AppState *app, gboolean teardown_pipeline); static void reset_signaling_session(AppState *app, gboolean teardown_pipeline);
static gboolean idle_pipeline_teardown(gpointer p); static gboolean idle_pipeline_teardown(gpointer p);
static gboolean delayed_peer_leave_reset(gpointer p);
static void schedule_tx_flush(void); static void schedule_tx_flush(void);
static void dispatch_json(const gchar *payload); static void dispatch_json(const gchar *payload);
...@@ -228,6 +232,12 @@ static gboolean debounced_offer_fire(gpointer user_data) { ...@@ -228,6 +232,12 @@ static gboolean debounced_offer_fire(gpointer user_data) {
} }
static void schedule_offer(AppState *app, const gchar *sdp) { static void schedule_offer(AppState *app, const gchar *sdp) {
s_offer_generation++;
if (s_peer_leave_reset_id) {
g_source_remove(s_peer_leave_reset_id);
s_peer_leave_reset_id = 0;
my_zlog_info("webrtcpush: new offer cancels pending stale peer-leave reset");
}
if (!app->member_id) { if (!app->member_id) {
g_free(s_pending_offer_sdp); g_free(s_pending_offer_sdp);
s_pending_offer_sdp = g_strdup(sdp); s_pending_offer_sdp = g_strdup(sdp);
...@@ -388,8 +398,14 @@ static void dispatch_json(const gchar *payload) { ...@@ -388,8 +398,14 @@ static void dispatch_json(const gchar *payload) {
if (!g_strcmp0(t, "leave")) { if (!g_strcmp0(t, "leave")) {
if (rtc_client_is_active(s_app)) { if (rtc_client_is_active(s_app)) {
my_zlog_info("webrtcpush: peer left, reset libdatachannel peer"); if (s_peer_leave_reset_id)
g_idle_add(idle_pipeline_teardown, s_app); g_source_remove(s_peer_leave_reset_id);
s_leave_generation = s_offer_generation;
s_peer_leave_reset_id = g_timeout_add(
WEBRTCPUSH_PEER_LEAVE_GRACE_MS,
delayed_peer_leave_reset, s_app);
my_zlog_info("webrtcpush: peer left, defer reset %ums for network/device switch",
WEBRTCPUSH_PEER_LEAVE_GRACE_MS);
} }
g_object_unref(parser); g_object_unref(parser);
return; return;
...@@ -436,6 +452,12 @@ static void dispatch_json(const gchar *payload) { ...@@ -436,6 +452,12 @@ static void dispatch_json(const gchar *payload) {
if (json_node_get_value_type(midn) == G_TYPE_STRING) if (json_node_get_value_type(midn) == G_TYPE_STRING)
json_object_set_string_member(co, "sdpMid", json_object_get_string_member(root, "sdpMid")); json_object_set_string_member(co, "sdpMid", json_object_get_string_member(root, "sdpMid"));
} }
if (json_object_has_member(root, "usernameFragment")) {
JsonNode *ufn = json_object_get_member(root, "usernameFragment");
if (json_node_get_value_type(ufn) == G_TYPE_STRING)
json_object_set_string_member(co, "usernameFragment",
json_object_get_string_member(root, "usernameFragment"));
}
g_idle_add(idle_ice, co); g_idle_add(idle_ice, co);
} }
} }
...@@ -506,6 +528,26 @@ static gboolean idle_pipeline_teardown(gpointer p) { ...@@ -506,6 +528,26 @@ static gboolean idle_pipeline_teardown(gpointer p) {
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
static gboolean delayed_peer_leave_reset(gpointer p) {
AppState *app = p;
guint generation = s_leave_generation;
s_peer_leave_reset_id = 0;
if (!app || generation != s_offer_generation) {
my_zlog_info("webrtcpush: stale peer-leave reset ignored (new offer generation)");
return G_SOURCE_REMOVE;
}
if (rtc_client_media_open(app)) {
my_zlog_info("webrtcpush: stale peer-leave reset ignored (new media already open)");
return G_SOURCE_REMOVE;
}
if (rtc_client_is_active(app)) {
my_zlog_info("webrtcpush: peer leave confirmed, release inactive libdatachannel peer");
rtc_client_reset(app);
}
return G_SOURCE_REMOVE;
}
static void ws_connect(void) { static void ws_connect(void) {
if (s_ws_exiting || !s_app || !s_ws_url) if (s_ws_exiting || !s_app || !s_ws_url)
return; return;
...@@ -610,6 +652,10 @@ void ws_signaling_stop(AppState *app) { ...@@ -610,6 +652,10 @@ void ws_signaling_stop(AppState *app) {
g_source_remove(s_reconnect_id); g_source_remove(s_reconnect_id);
s_reconnect_id = 0; s_reconnect_id = 0;
} }
if (s_peer_leave_reset_id) {
g_source_remove(s_peer_leave_reset_id);
s_peer_leave_reset_id = 0;
}
if (s_ping_id) { if (s_ping_id) {
g_source_remove(s_ping_id); g_source_remove(s_ping_id);
s_ping_id = 0; s_ping_id = 0;
......
...@@ -9,4 +9,4 @@ file perms = 600 ...@@ -9,4 +9,4 @@ file perms = 600
millisecond = "%d(%Y-%m-%d %H:%M:%S).%ms [%V] %m%n" millisecond = "%d(%Y-%m-%d %H:%M:%S).%ms [%V] %m%n"
[rules] [rules]
my_log.* "/home/orangepi/car/master/log/log_2026-07-11.log"; millisecond my_log.* "/home/orangepi/car/master/log/log_2026-07-14.log"; millisecond
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment