Commit 7200adca authored by 957dd's avatar 957dd

Merge branch 'feature/add_aboard_tts' into 'master'

Feature/add aboard tts See merge request !129
parents 7a0b1304 1ce13541
No preview for this file type
......@@ -28,6 +28,14 @@
#define CAR0107_BACK_US_RUN_MIN 1440 /* val=53 起步即动(须低于死区 1450) */
#define CAR0107_BACK_US_RUN_MAX 1300 /* 后退最大速度再降20%:val=200 -> 1300us */
/* idle 唤醒动作:5分钟无控制后 低速后退 -> 低速前进 -> 前进刹车停止 */
#define CAR0107_IDLE_REVERSE_US CAR0107_BACK_US_RUN_MIN /* 1440us 刚过死区,最低速后退 */
#define CAR0107_IDLE_FORWARD_US CAR0107_FWD_US_RUN_MIN /* 1560us 最低前进行走脉宽 */
#define CAR0107_IDLE_REVERSE_MS 600 /* 后退保持时长 */
#define CAR0107_IDLE_NEUTRAL_MS 150 /* 后退切前进前中位稳定时长 */
#define CAR0107_IDLE_FORWARD_MS 600 /* 前进保持时长 */
#define CAR0107_IDLE_BRAKE_WAIT_MS 250 /* 等待刹车保持+中位解锁收尾到 NEUTRAL */
typedef enum {
CAR0107_ESC_DIR_FORWARD = 1, /* App mode1 前进:高脉宽(与 0101 相反) */
CAR0107_ESC_DIR_BACK = 2, /* App mode2 后退:低脉宽(与 0101 相反) */
......@@ -1392,3 +1400,92 @@ void car0107_esc_thread_close(void)
pthread_mutex_unlock(&s_esc_start_mutex);
my_zlog_info("car0107 esc thread closed");
}
/*
* 0107 idle 唤醒动作:5分钟无控制命令时由 idle 线程调用。
* 顺序:低速后退一点 -> 中位 -> 低速前进一点 -> 前进刹车停止。
* 前进停止遵守电调刹车动作 BRAKE_THEN_NEUTRAL -> NEUTRAL_UNLOCK -> NEUTRAL,
* 由电调专用线程 tick 自动完成刹车保持与中位解锁收尾。
* 动作期间若有真实控制命令介入(control_source 被改为非 NONE)则立即让位。
*/
void car0107_idle_wakeup_maneuver(void)
{
/* 必须保证电调线程在运行,否则刹车序列无法由 tick 自动收尾到 NEUTRAL */
car0107_esc_thread_start();
/* 方向盘回正,后退/前进均走直线 */
pthread_mutex_lock(&s_car0107_hw_mutex);
s_control_source = CTRL_NONE; /* 阻止 speed_smooth_process 串扰速度 */
s_target_angle = 90;
s_current_angle = 90;
car0107_calculate_L_R(90);
/* 1) 低速后退:从中位直接跳到刚过死区的后退脉宽 */
car0107_esc_clear_pending_stop();
s_esc.cmd_dir = CAR0107_ESC_DIR_BACK;
s_esc.last_drive_dir = CAR0107_ESC_DIR_BACK;
s_esc.last_nonzero_mode = CAR0107_ESC_DIR_BACK;
s_esc.target_us = CAR0107_IDLE_REVERSE_US;
s_esc.current_us = CAR0107_IDLE_REVERSE_US;
s_esc.state = CAR0107_ESC_REVERSE;
s_esc.trans_phase = CAR0107_TRANS_RAMP_RUN;
car0107_esc_output_us(s_esc.current_us, 0);
pthread_mutex_unlock(&s_car0107_hw_mutex);
car0107_esc_thread_wake();
my_zlog_info("car0107 idle maneuver: reverse %dus for %dms",
CAR0107_IDLE_REVERSE_US, CAR0107_IDLE_REVERSE_MS);
delay_ms(CAR0107_IDLE_REVERSE_MS);
/* 2) 停止后退:后退松手不刹车,直接回中 */
pthread_mutex_lock(&s_car0107_hw_mutex);
if (s_control_source != CTRL_NONE) {
pthread_mutex_unlock(&s_car0107_hw_mutex);
my_zlog_info("car0107 idle maneuver: yield at neutral (control taken)");
return;
}
car0107_esc_begin_neutral_stop();
car0107_esc_output_us(CAR0107_ESC_US_NEUTRAL, 0);
pthread_mutex_unlock(&s_car0107_hw_mutex);
car0107_esc_thread_wake();
delay_ms(CAR0107_IDLE_NEUTRAL_MS);
/* 3) 低速前进:从中位直接跳到最低前进行走脉宽 */
pthread_mutex_lock(&s_car0107_hw_mutex);
if (s_control_source != CTRL_NONE) {
pthread_mutex_unlock(&s_car0107_hw_mutex);
my_zlog_info("car0107 idle maneuver: yield before forward (control taken)");
return;
}
car0107_esc_clear_pending_stop();
s_esc.cmd_dir = CAR0107_ESC_DIR_FORWARD;
s_esc.last_drive_dir = CAR0107_ESC_DIR_FORWARD;
s_esc.last_nonzero_mode = CAR0107_ESC_DIR_FORWARD;
s_esc.target_us = CAR0107_IDLE_FORWARD_US;
s_esc.current_us = CAR0107_IDLE_FORWARD_US;
s_esc.state = CAR0107_ESC_FORWARD;
s_esc.trans_phase = CAR0107_TRANS_RAMP_RUN;
car0107_esc_output_us(s_esc.current_us, 0);
pthread_mutex_unlock(&s_car0107_hw_mutex);
car0107_esc_thread_wake();
my_zlog_info("car0107 idle maneuver: forward %dus for %dms",
CAR0107_IDLE_FORWARD_US, CAR0107_IDLE_FORWARD_MS);
delay_ms(CAR0107_IDLE_FORWARD_MS);
/* 4) 停止前进:遵守刹车动作,由电调线程 tick 自动收尾 */
pthread_mutex_lock(&s_car0107_hw_mutex);
if (s_control_source != CTRL_NONE) {
pthread_mutex_unlock(&s_car0107_hw_mutex);
my_zlog_info("car0107 idle maneuver: yield before brake (control taken)");
return;
}
car0107_esc_begin_forward_brake();
pthread_mutex_unlock(&s_car0107_hw_mutex);
car0107_esc_thread_wake();
my_zlog_info("car0107 idle maneuver: forward brake %dus for %dms",
car0107_forward_brake_us(), CAR0107_ESC_BRAKE_HOLD_MS);
delay_ms(CAR0107_IDLE_BRAKE_WAIT_MS);
}
......@@ -14,4 +14,7 @@ void car0107_esc_thread_start(void);
void car0107_esc_thread_wake(void);
void car0107_esc_thread_close(void);
/* idle 唤醒动作:低速后退 -> 低速前进 -> 前进刹车停止 */
void car0107_idle_wakeup_maneuver(void);
#endif
......@@ -6,16 +6,12 @@
#define CAR0107_IDLE_CHECK_MS 10000
#define CAR0107_IDLE_TIMEOUT_MS (5 * 60 * 1000)
#define CAR0107_IDLE_STEER_HOLD_MS 450
#define CAR0107_IDLE_STEER_LEFT 115
#define CAR0107_IDLE_STEER_RIGHT 65
typedef struct {
pthread_mutex_t mutex;
ThreadPool_t *pool;
bool shutdown;
bool pool_ready;
int next_left;
} car0107_idle_ctx_t;
static car0107_idle_ctx_t s_idle_ctx = {
......@@ -23,7 +19,6 @@ static car0107_idle_ctx_t s_idle_ctx = {
.pool = NULL,
.shutdown = false,
.pool_ready = false,
.next_left = 1,
};
static void car0107_idle_lock(void)
......@@ -67,16 +62,6 @@ static int car0107_idle_pool_init(void)
return 0;
}
static void car0107_idle_nudge_steering(int turn_left)
{
int angle = turn_left ? CAR0107_IDLE_STEER_LEFT : CAR0107_IDLE_STEER_RIGHT;
/* 速度 PWM 只由持有0107硬件锁的控制路径写,idle线程不直接碰速度。 */
car0107_steering_set_angle_sync(angle);
delay_ms(CAR0107_IDLE_STEER_HOLD_MS);
car0107_steering_set_angle_sync(90);
}
static void car0107_idle_monitor_task(void *arg)
{
(void)arg;
......@@ -85,7 +70,6 @@ static void car0107_idle_monitor_task(void *arg)
while (!car0107_idle_shutdown_get()) {
long long now;
long long last;
int turn_left;
delay_ms(CAR0107_IDLE_CHECK_MS);
if (car0107_idle_shutdown_get() || g_device_type != DEVICE_CAR0107) {
......@@ -101,15 +85,10 @@ static void car0107_idle_monitor_task(void *arg)
continue;
}
car0107_idle_lock();
turn_left = s_idle_ctx.next_left;
s_idle_ctx.next_left = !s_idle_ctx.next_left;
car0107_idle_unlock();
my_zlog_info("car0107 idle %dmin: nudge steer %s",
CAR0107_IDLE_TIMEOUT_MS / 60000,
turn_left ? "left" : "right");
car0107_idle_nudge_steering(turn_left);
my_zlog_info("car0107 idle %dmin: wakeup maneuver (reverse->forward->brake)",
CAR0107_IDLE_TIMEOUT_MS / 60000);
/* 低速后退一点 -> 低速前进一点 -> 前进刹车停止(遵守刹车动作)*/
car0107_idle_wakeup_maneuver();
car0107_notify_control_activity();
}
......@@ -133,7 +112,7 @@ void car0107_idle_startup(int device_id)
car0107_idle_unlock();
car0107_notify_control_activity();
my_zlog_info("car0107 idle monitor enabled: steer nudge every %dmin when idle",
my_zlog_info("car0107 idle monitor enabled: wakeup maneuver every %dmin when idle",
CAR0107_IDLE_TIMEOUT_MS / 60000);
}
......
......@@ -236,9 +236,9 @@ static const deviceconfig_t s_device_configs[] = {
bool get_array_length(const int* arr) {
if(arr[0] != -1) { // 遇到-1停止计数
return TRUE;
return true;
}
return FALSE;
return false;
}
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_pwms)==TRUE)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_pins))init_gpiowpi(config->gpio_pins); // GPIO初始化
if(get_array_length(config->gpio_pwms))init_gpiopwm(config->gpio_pwms); // GPIOsoft_pwm初始化
if(get_array_length(config->gpio_inputs)) init_gpio_input(config->gpio_inputs);
g_device_type =config->emergency_code;
config->device_pwm_init(); // PWM初始化
config->device_control_stop(); // 速度控制初始化
......
#include "common.h"
#include "audioplay.h"
#include "audio_sink.h" /* USB声卡排队锁 */
#include "device_identity.h"
#include "mqtt_init.h"
#include "http_config_mqtt.h"
#include "audiotts_play.h"
#include "wifi_autoconfig.h"
#include <stdio.h>
#include <pthread.h>
#include <strings.h>
#include <unistd.h>
#include <sys/wait.h>
#define AUDIO_USB_ALSA_DEVICE "hw:2,0"
#define AUDIO_LOCAL_ALSA_DEVICE "plughw:2,0"
#define AUDIO_ANNOUNCEMENT_TIMEOUT_SEC 20
static int s_audio_status=7;
static char s_urlbuf[512];
......@@ -21,6 +25,7 @@ static double s_audio_volume=0.8;
static int s_local_play_pending = 0;
static char s_local_filepath[512];
static pthread_mutex_t s_local_play_mutex = PTHREAD_MUTEX_INITIALIZER;
static int local_is_cn_lang(const char *lang) {
return lang != NULL && (strcmp(lang, AUDIO_LANG_ZH) == 0 || strcmp(lang, "cn") == 0);
......@@ -74,12 +79,16 @@ static int local_resolve_filepath(const char *filename, const char *language_ove
}
static void local_queue_play(const char *filename, const char *language) {
if (!local_resolve_filepath(filename, language, s_local_filepath, sizeof(s_local_filepath))) {
char resolved[512];
if (!local_resolve_filepath(filename, language, resolved, sizeof(resolved))) {
my_zlog_warn("2017 本地音频不存在: %s", filename);
return;
}
pthread_mutex_lock(&s_local_play_mutex);
snprintf(s_local_filepath, sizeof(s_local_filepath), "%s", resolved);
s_local_play_pending = 1;
my_zlog_info("2017 已排队本地音频: %s", s_local_filepath);
pthread_mutex_unlock(&s_local_play_mutex);
my_zlog_info("2017 已排队本地音频: %s", resolved);
}
void audioplay_local_mqtt_receive(cJSON *body) {
......@@ -143,6 +152,144 @@ static double audioplay_volume_clamp(double v) {
int audio_wheat_init();
static int audio_system_exit_code(int status) {
if (status == -1) {
return -1;
}
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
}
if (WIFSIGNALED(status)) {
return 128 + WTERMSIG(status);
}
return -1;
}
static void shell_single_quote(char *out, size_t size, const char *in) {
size_t pos = 0;
if (!out || size == 0) {
return;
}
out[pos++] = '\'';
if (in) {
for (const char *p = in; *p && pos + 5 < size; p++) {
if (*p == '\'') {
const char *esc = "'\\''";
for (const char *e = esc; *e && pos + 1 < size; e++) {
out[pos++] = *e;
}
} else {
out[pos++] = *p;
}
}
}
if (pos + 1 < size) {
out[pos++] = '\'';
}
out[pos] = '\0';
}
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 command[2048];
int ret;
int exit_code;
int pulse_suspended;
if (filepath == NULL || filepath[0] == '\0') {
return -1;
}
shell_single_quote(quoted_path, sizeof(quoted_path), filepath);
volume = audioplay_volume_clamp(volume);
/* 先和项目内 DataChannel/本地音频排队,再让浏览器临时释放 USB 声卡。 */
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),
"timeout -s KILL %ds ffplay -nodisp -autoexit -loglevel warning -af \"volume=%.3f\" %s >/dev/null 2>&1",
AUDIO_ANNOUNCEMENT_TIMEOUT_SEC, volume, quoted_path);
}
ret = system(command);
exit_code = audio_system_exit_code(ret);
/* 无论正常结束、播放失败还是 timeout,退出前都恢复浏览器音频。 */
if (pulse_suspended) {
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),
"timeout -s KILL %ds ffmpeg -nostdin -hide_banner -loglevel warning -i %s -af \"volume=%.3f\" -f alsa %s >/dev/null 2>&1",
AUDIO_ANNOUNCEMENT_TIMEOUT_SEC, quoted_url, volume,
AUDIO_LOCAL_ALSA_DEVICE);
} 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();
if (exit_code == 137) {
my_zlog_warn("网络播报超过 %d 秒,已强制停止并释放声卡: %s",
AUDIO_ANNOUNCEMENT_TIMEOUT_SEC, url);
}
return exit_code;
}
//接收音频播放
void audioplay_mqtt_receive(cJSON *json) {
// 解析"audioLink"字段(修正了原始JSON中的拼写错误)
......@@ -212,26 +359,21 @@ void audioplay_send_mqtt() {
//音频播放
void audioplay_cycle(){
char command[1024];
int ret;
while(1){
if(s_audio_status==0){
char *urlmoddle=s_urlbuf;
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", command);
ret = system(command);
ret = audioplay_url_with_browser_preempt(urlmoddle, s_audio_volume);
if (ret != 0) {
my_zlog_error("播放失败");
s_audio_status=2;
}
if (WIFEXITED(ret) && WEXITSTATUS(ret) == 0) {
if (ret == 0) {
my_zlog_debug("播放已成功完成 : %s ", s_urlbuf);
s_audio_status=1;
} else {
......@@ -241,16 +383,25 @@ void audioplay_cycle(){
audioplay_send_mqtt();
}
char local_filepath[sizeof(s_local_filepath)];
int local_play_pending = 0;
pthread_mutex_lock(&s_local_play_mutex);
if (s_local_play_pending) {
s_local_play_pending = 0;
snprintf(command, sizeof(command),
"ffplay -nodisp -autoexit -loglevel quiet \"%s\"", s_local_filepath);
my_zlog_debug("播放本地音频: %s", s_local_filepath);
ret = system(command);
if (WIFEXITED(ret) && WEXITSTATUS(ret) == 0) {
my_zlog_debug("本地音频播放完成: %s", s_local_filepath);
snprintf(local_filepath, sizeof(local_filepath), "%s", s_local_filepath);
local_play_pending = 1;
}
pthread_mutex_unlock(&s_local_play_mutex);
if (local_play_pending) {
int exit_code;
my_zlog_info("播放本地音频: %s", local_filepath);
exit_code = audioplay_file_with_browser_preempt(local_filepath, 1.0);
if (exit_code == 0) {
my_zlog_debug("本地音频播放完成: %s", local_filepath);
} else {
my_zlog_warn("本地音频播放失败: %s", s_local_filepath);
my_zlog_warn("本地音频播放失败 exit=%d: %s", exit_code, local_filepath);
}
}
......@@ -374,7 +525,7 @@ int audio_speaker_init() {
int audio_init(){
delay_s(5);
delay_s(1);
audio_wheat_init();
delay_s(1);
audio_speaker_init();
......@@ -460,4 +611,4 @@ int audio_config_init() {
my_zlog_info("配置已成功追加。");
return 0;
}
\ No newline at end of file
}
This diff is collapsed.
......@@ -6,10 +6,17 @@ void audioplay_mqtt_receive(cJSON *body); //接收音频mqtt播放函数
void audioplay_local_mqtt_receive(cJSON *body); // 2017 有人驾驶本地音频
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_speaker_init();
int audio_init();
int audio_config_init();//加入配置
#endif
\ No newline at end of file
#endif
#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
......@@ -470,46 +470,15 @@ void video_tts_play() {
s_audio_tts_index = 0;
return;
}
pid_t pid = fork();
if (pid == 0) {
// 子进程:创建新的会话组,脱离父进程组
setsid(); // 关键!创建新的会话组
char command[256];
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;
/* audioplay_cycle 本身运行在音频线程,这里同步等待可确保声卡一定被恢复。 */
int exit_code = audioplay_file_with_browser_preempt(TTS_ADUINO_PTAH, s_volume);
if (exit_code == 0) {
my_zlog_info("TTS 播报完成, volume: %.1f", s_volume);
} else {
my_zlog_warn("TTS 播报失败或超时 exit=%d, volume: %.1f", exit_code, s_volume);
}
s_audio_tts_index = 0;
}
}
......@@ -532,4 +501,4 @@ void video_tts_play() {
// s_audio_tts_index == 0;
// }
// }
\ No newline at end of file
This diff is collapsed.
......@@ -8,6 +8,7 @@ pkg_check_modules(WEBRTCPUSH_GST REQUIRED
)
pkg_check_modules(WEBRTCPUSH_JSON REQUIRED json-glib-1.0)
pkg_check_modules(WEBRTCPUSH_SOUP REQUIRED libsoup-2.4)
pkg_check_modules(WEBRTCPUSH_JPEG REQUIRED libjpeg)
file(GLOB_RECURSE MODULES_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/logger/*.c
......@@ -47,6 +48,7 @@ set(MODULES_INCLUDE_DIRS
${WEBRTCPUSH_GST_INCLUDE_DIRS}
${WEBRTCPUSH_JSON_INCLUDE_DIRS}
${WEBRTCPUSH_SOUP_INCLUDE_DIRS}
${WEBRTCPUSH_JPEG_INCLUDE_DIRS}
PARENT_SCOPE
)
......@@ -54,6 +56,7 @@ set(WEBRTCPUSH_LIBRARIES
${WEBRTCPUSH_GST_LIBRARIES}
${WEBRTCPUSH_JSON_LIBRARIES}
${WEBRTCPUSH_SOUP_LIBRARIES}
${WEBRTCPUSH_JPEG_LIBRARIES}
PARENT_SCOPE
)
......@@ -61,5 +64,6 @@ set(WEBRTCPUSH_CFLAGS
${WEBRTCPUSH_GST_CFLAGS_OTHER}
${WEBRTCPUSH_JSON_CFLAGS_OTHER}
${WEBRTCPUSH_SOUP_CFLAGS_OTHER}
${WEBRTCPUSH_JPEG_CFLAGS_OTHER}
PARENT_SCOPE
)
......@@ -3,7 +3,7 @@
#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="
typedef struct{
......
......@@ -213,7 +213,7 @@ static void refresh_cam_browser_only(const char *reason)
void message_2_judyverify(cJSON *body)
{
if (VERIFIED_MODE == FALSE)
if (VERIFIED_MODE == false)
{
refresh_cam_browser_only("MQTT type2 verify off");
my_zlog_warn("不使用验证");
......@@ -234,7 +234,7 @@ void message_2_judyverify(cJSON *body)
// 当接收到3时候验证
void message_3_judyverify(cJSON *body)
{
if (VERIFIED_MODE == FALSE)
if (VERIFIED_MODE == false)
{
message_3(body);
my_zlog_warn("不使用验证");
......@@ -255,7 +255,7 @@ void message_3_judyverify(cJSON *body)
// 当接收到4时候验证
void message_4_judyverify(cJSON *body)
{
if (VERIFIED_MODE == FALSE)
if (VERIFIED_MODE == false)
{
message_4(body);
my_zlog_warn("不使用验证");
......@@ -276,7 +276,7 @@ void message_4_judyverify(cJSON *body)
void message_6_steering_judyverify(cJSON *body)
{
if (VERIFIED_MODE == FALSE)
if (VERIFIED_MODE == false)
{
steering_mqtt_recv(body);
my_zlog_warn("不使用验证");
......@@ -303,7 +303,7 @@ void message_7_joystick_judyverify(cJSON *body)
return;
}
if (VERIFIED_MODE == FALSE)
if (VERIFIED_MODE == false)
{
ptz_driver_set_joystick_json(joystick_ctrl);
my_zlog_warn("不使用验证");
......
......@@ -9,9 +9,9 @@
#include "webrtcpush_run.h"
/*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;//判断是否有一分钟
......@@ -19,7 +19,7 @@ static char s_only_id_middle[11];
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 不通过 */
static char s_pending_verify_token[1024];
......@@ -172,7 +172,7 @@ int receive_jwt(cJSON *body) {
my_zlog_debug("token时间戳:%1d",(long)token_time_sec);
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);
if(g_verify_count>12000){
send_jwtser(token);
......@@ -205,7 +205,7 @@ int message2006_verify(cJSON *body){
if(json_status == NULL || json_onlyid==NULL) {
my_zlog_warn("验证为空");
s_service_verify = FALSE;
s_service_verify = false;
g_verify_index=1;
my_zlog_debug("g_verify_index= %d ",g_verify_index);
return 1;
......@@ -219,10 +219,10 @@ int message2006_verify(cJSON *body){
gboolean driver_changed = (s_secret_key[0] != '\0' &&
strcmp(s_secret_key, s_pending_verify_token) != 0);
my_zlog_info("获得验证正确, onlyid=%s", onlyid);
s_service_verify = TRUE;
s_service_verify = true;
g_verify_index = 0; /* 后端通过后必须清除失败标志,否则正确 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_after_time = 0;
if (driver_changed)
......@@ -238,13 +238,13 @@ int message2006_verify(cJSON *body){
} else {
my_zlog_warn("topic验证超过%d次不通过,禁止使用, token=%s", VERIFY_RETRY_MAX, s_pending_verify_token);
g_verify_index = 1;
s_service_verify = FALSE;
s_service_verify = false;
return 2;
}
} else {
my_zlog_warn("获得验证错误,禁止使用, onlyid=%s status=%s", onlyid, status);
g_verify_index = 1;
s_service_verify = FALSE;
s_service_verify = false;
return 2;
}
}
......@@ -297,14 +297,14 @@ int message2013_recverigy_open(cJSON *body_raw)
s_verify_open_query_gave_up = false;
if (verify_status->valueint == 1) {
VERIFIED_MODE = TRUE;
VERIFIED_MODE = true;
my_zlog_info("开启验证成功(加密校验通过), verify_status=1");
cJSON_Delete(body);
return 0;
}
if (verify_status->valueint == 0) {
VERIFIED_MODE = FALSE;
VERIFIED_MODE = false;
my_zlog_info("关闭验证成功(加密校验通过), verify_status=0");
cJSON_Delete(body);
return 0;
......
#include "audio_sink.h"
#include "webrtcpush_config.h"
#include "webrtcpush_log.h"
#include <gst/gst.h>
#include <gst/app/gstappsrc.h>
/* USB声卡排队锁: audio_sink 和本地音频串行访问 plughw:2,0 */
static GMutex g_alsa_device_lock;
static AudioSink *g_audio_sink_singleton = NULL;
/*
* 手机→设备方向的音频播放管道(按键模式):
* appsrc(opus payload) → opusdec → audioconvert → audioresample → volume → alsasink
*
* 手机端是"按住说话"模式(最长 15s),不是持续推流,因此:
* - on_audio_message 收到包后入队,立即返回(不阻塞 libdatachannel 线程)
* - 独立线程消费队列,维护"按键会话"
* - 会话开始(首包到达):pipeline 切到 PLAYING
* - 会话结束(500ms 无包 或 15s 超时):flush appsrc + pipeline 切到 READY
* 切到 READY 释放 ALSA 设备,避免 alsasink 持续占用/空转
* 手机→设备方向的音频不再走 RTP:手机端 audio 为 recvonly(只收不发),
* 手机→设备的音频通过 DataChannel 发送 MP4,由 decodebin 播放。
* 因此本模块不再创建 GStreamer 接收管道,仅保留 USB 声卡全局锁,
* 供 DataChannel 播放线程与本地提示音串行访问 ALSA 设备使用。
*/
#define AUDIO_SINK_SESSION_TIMEOUT_MS 500 /* 500ms 无包认为按键结束 */
#define AUDIO_SINK_SESSION_MAX_MS 15000 /* 单次按键最长 15s */
typedef struct {
uint8_t *data;
size_t size;
} SinkPacket;
struct AudioSink {
GstElement *pipeline;
GstElement *appsrc;
GstElement *vol;
GAsyncQueue *queue; /* 待处理 Opus 包队列 */
GMutex lock; /* 保护 pipeline 状态切换 */
GThread *thread; /* 消费线程 */
gboolean quit; /* 退出标志 */
gint64 session_start_us; /* 当前按键会话开始时间(0=无会话) */
gint64 last_packet_us; /* 最后一个包到达时间 */
gboolean pipeline_playing; /* pipeline 当前是否 PLAYING */
GMutex lock;
};
static void flush_queue(AudioSink *src)
{
SinkPacket *pkt;
while ((pkt = g_async_queue_try_pop(src->queue)) != NULL) {
g_free(pkt->data);
g_free(pkt);
}
}
void audio_sink_lock_alsa(void) { g_mutex_lock(&g_alsa_device_lock); }
void audio_sink_unlock_alsa(void) { g_mutex_unlock(&g_alsa_device_lock); }
static void set_pipeline_state_locked(AudioSink *src, GstState state)
void audio_sink_interrupt(AudioSink *src)
{
if (!src->pipeline)
return;
/* 切到 READY 时 flush appsrc,避免旧数据残留导致下次会话首帧异常 */
if (state == GST_STATE_READY && src->appsrc) {
gst_element_send_event(src->pipeline,
gst_event_new_flush_start());
gst_element_send_event(src->pipeline,
gst_event_new_flush_stop(FALSE));
}
gst_element_set_state(src->pipeline, state);
src->pipeline_playing = (state == GST_STATE_PLAYING);
}
static gpointer audio_sink_thread(gpointer data)
{
AudioSink *src = data;
gint64 now;
GstBuffer *buf;
while (1) {
/* 等待包,超时 100ms 用于检查会话超时 */
SinkPacket *pkt = g_async_queue_timeout_pop(src->queue, 100 * 1000);
now = g_get_monotonic_time();
g_mutex_lock(&src->lock);
if (src->quit) {
g_mutex_unlock(&src->lock);
if (pkt) { g_free(pkt->data); g_free(pkt); }
break;
}
/* 会话超时检查 */
if (src->session_start_us > 0) {
gint64 idle_ms = (now - src->last_packet_us) / 1000;
gint64 sess_ms = (now - src->session_start_us) / 1000;
if (idle_ms >= AUDIO_SINK_SESSION_TIMEOUT_MS ||
sess_ms >= AUDIO_SINK_SESSION_MAX_MS) {
if (src->pipeline_playing) {
set_pipeline_state_locked(src, GST_STATE_READY);
my_zlog_info("audio_sink: session ended (idle=%lldms sess=%lldms)",
(long long)idle_ms, (long long)sess_ms);
}
src->session_start_us = 0;
flush_queue(src);
/* 丢弃超时后到达的旧包 */
if (pkt) {
g_free(pkt->data);
g_free(pkt);
pkt = NULL;
}
g_mutex_unlock(&src->lock);
continue;
}
}
if (pkt) {
/* 新会话开始 */
if (src->session_start_us == 0) {
src->session_start_us = now;
my_zlog_info("audio_sink: session start");
if (!src->pipeline_playing)
set_pipeline_state_locked(src, GST_STATE_PLAYING);
}
src->last_packet_us = now;
/* push 到 appsrc(pipeline PLAYING 状态) */
if (src->appsrc && src->pipeline_playing) {
buf = gst_buffer_new_wrapped(g_memdup2(pkt->data, pkt->size), pkt->size);
GST_BUFFER_DTS(buf) = GST_CLOCK_TIME_NONE;
GST_BUFFER_PTS(buf) = GST_CLOCK_TIME_NONE;
if (gst_app_src_push_buffer(GST_APP_SRC(src->appsrc), buf) != GST_FLOW_OK) {
my_zlog_warn("audio_sink: push_buffer failed");
}
}
g_free(pkt->data);
g_free(pkt);
}
g_mutex_unlock(&src->lock);
}
return NULL;
(void)src;
/* RTP 接收管道已移除,无会话需要中断;DataChannel 播放通过 lock/unlock 串行访问 ALSA。 */
}
AudioSink *audio_sink_start(const char *alsa_device, char **error_message)
{
AudioSink *src;
GstElement *pipe, *asrc, *dec, *conv, *resample, *vol, *sink;
GstCaps *caps;
GstStateChangeReturn ret;
if (!alsa_device || !alsa_device[0]) {
if (error_message)
*error_message = g_strdup("no ALSA device");
return NULL;
}
(void)alsa_device;
if (error_message)
*error_message = NULL;
src = g_new0(AudioSink, 1);
pipe = gst_pipeline_new("audio-sink-pipe");
asrc = gst_element_factory_make("appsrc", "asrc");
dec = gst_element_factory_make("opusdec", "dec");
conv = gst_element_factory_make("audioconvert", "conv");
resample = gst_element_factory_make("audioresample", "resample");
vol = gst_element_factory_make("volume", "vol");
sink = gst_element_factory_make("alsasink", "sink");
if (!pipe || !asrc || !dec || !conv || !resample || !vol || !sink) {
if (error_message)
*error_message = g_strdup("failed to create audio sink GStreamer elements");
if (pipe)
gst_object_unref(pipe);
g_free(src);
return NULL;
}
caps = gst_caps_new_empty_simple("audio/x-opus");
g_object_set(asrc,
"caps", caps,
"format", GST_FORMAT_BYTES,
"is-live", TRUE,
"emit-signals", FALSE,
"min-latency", (gint64)0,
"max-bytes", (guint64)(1 * 1024 * 1024),
NULL);
gst_caps_unref(caps);
g_object_set(sink,
"device", alsa_device,
"buffer-time", (gint64)20000,
"latency-time", (gint64)10000,
"sync", FALSE,
NULL);
g_object_set(vol, "volume", 0.5, NULL);
gst_bin_add_many(GST_BIN(pipe), asrc, dec, conv, resample, vol, sink, NULL);
if (!gst_element_link_many(asrc, dec, conv, resample, vol, sink, NULL)) {
if (error_message)
*error_message = g_strdup("failed to link audio sink chain");
gst_object_unref(pipe);
g_free(src);
return NULL;
}
src->pipeline = pipe;
src->appsrc = asrc;
src->vol = vol;
src->queue = g_async_queue_new();
g_mutex_init(&src->lock);
src->session_start_us = 0;
src->pipeline_playing = FALSE;
/* 初始状态 READY(不占 ALSA 设备,等首包到来再 PLAYING) */
ret = gst_element_set_state(pipe, GST_STATE_READY);
if (ret == GST_STATE_CHANGE_FAILURE) {
if (error_message)
*error_message = g_strdup("audio sink pipeline failed to reach READY");
gst_element_set_state(pipe, GST_STATE_NULL);
gst_object_unref(pipe);
g_async_queue_unref(src->queue);
g_mutex_clear(&src->lock);
g_free(src);
return NULL;
}
src->thread = g_thread_new("audio-sink", audio_sink_thread, src);
if (!src->thread) {
if (error_message)
*error_message = g_strdup("failed to create audio sink thread");
audio_sink_stop(src);
return NULL;
}
my_zlog_info("audio_sink: started device=%s opus=%uch %uHz (push-to-talk)",
alsa_device, WEBRTCPUSH_OPUS_CHANNELS, WEBRTCPUSH_OPUS_CLOCKRATE);
g_audio_sink_singleton = src;
my_zlog_info("audio_sink: started (RTP receive removed, ALSA lock only)");
return src;
}
......@@ -233,45 +43,15 @@ void audio_sink_stop(AudioSink *src)
{
if (!src)
return;
if (src->thread) {
g_mutex_lock(&src->lock);
src->quit = TRUE;
g_mutex_unlock(&src->lock);
g_thread_join(src->thread);
src->thread = NULL;
}
if (src->pipeline) {
gst_element_set_state(src->pipeline, GST_STATE_NULL);
gst_object_unref(src->pipeline);
}
if (src->queue) {
flush_queue(src);
g_async_queue_unref(src->queue);
}
if (g_audio_sink_singleton == src)
g_audio_sink_singleton = NULL;
g_mutex_clear(&src->lock);
g_free(src);
}
gboolean audio_sink_push_opus(AudioSink *src, const uint8_t *data, size_t size)
{
SinkPacket *pkt;
if (!src || !src->queue || !data || size == 0)
return FALSE;
/* 入队,由消费线程处理(不阻塞 libdatachannel 回调线程) */
pkt = g_new0(SinkPacket, 1);
pkt->data = (uint8_t *)g_memdup2(data, size);
pkt->size = size;
g_async_queue_push(src->queue, pkt);
return TRUE;
}
void audio_sink_set_volume(AudioSink *src, double volume)
{
if (!src || !src->vol)
return;
if (volume < 0.0)
volume = 0.0;
if (volume > 1.0)
volume = 1.0;
g_object_set(src->vol, "volume", volume, NULL);
(void)src;
(void)volume;
/* 无 GStreamer 管道,音量设置为空操作(保留接口供 volume_control 调用)。 */
}
......@@ -17,4 +17,11 @@ gboolean audio_sink_push_opus(AudioSink *src, const uint8_t *data, size_t size);
/* 设置播放音量 0.0~1.0 */
void audio_sink_set_volume(AudioSink *src, double volume);
#endif
/* USB声卡排队锁: 本地音频和audio_sink串行访问同一个USB声卡 */
void audio_sink_lock_alsa(void);
void audio_sink_unlock_alsa(void);
/* 中断当前按键会话, 释放ALSA设备让DataChannel音频能立即播放 */
void audio_sink_interrupt(AudioSink *src);
#endif
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
......@@ -17,37 +17,58 @@
/*
* 与 gst_webrtc_pipeline / jywy 浏览器推流对齐的码率策略。
* 首屏先用 900kbps,不像 1.4Mbps 那样猛冲,也不要低到一进来就糊。
* RTCP REMB 只作为码率趋势,下降也做阶梯平滑,避免 MPP 动态切码率时卡顿
* 首屏先用 1.06Mbps,不像高码率那样猛冲,也不要低到一进来就糊。
* RTCP REMB 只作为码率趋势:小步慢降、慢升、带滞回,尽量接近浏览器的无感自适应
*/
#define WEBRTCPUSH_INITIAL_BITRATE 900000U
#define WEBRTCPUSH_MIN_BITRATE 500000U
#define WEBRTCPUSH_MAX_BITRATE 2800000U
#define WEBRTCPUSH_INITIAL_BITRATE 1060000U
#define WEBRTCPUSH_MIN_BITRATE 800000U
#define WEBRTCPUSH_MAX_BITRATE 3000000U
#define WEBRTCPUSH_REMB_UTIL_PERCENT 80U
#define WEBRTCPUSH_REMB_DOWN_MIN_STEP 100000U
#define WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS 4U
#define WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS 8U
#define WEBRTCPUSH_REMB_SEVERE_CONFIRMATIONS 2U
#define WEBRTCPUSH_REMB_SEVERE_PERCENT 65U
#define WEBRTCPUSH_BITRATE_RAMP_UP_MS 2500U
#define WEBRTCPUSH_BITRATE_RAMP_DOWN_MS 1500U
#define WEBRTCPUSH_REMB_DOWN_STEP_PERCENT 15U
#define WEBRTCPUSH_REMB_DOWN_STEP_MIN_BPS 80000U
#define WEBRTCPUSH_REMB_DOWN_STEP_MAX_BPS 180000U
#define WEBRTCPUSH_PACING_HEADROOM_PERCENT 140U
#define WEBRTCPUSH_BITRATE_RAMP_UP_MS 1200U
#define WEBRTCPUSH_BITRATE_RAMP_DOWN_MS 2000U
#define WEBRTCPUSH_BITRATE_RAMP_UP_PERCENT 12U
#define WEBRTCPUSH_BITRATE_RAMP_UP_STEP_MIN_BPS 70000U
#define WEBRTCPUSH_BITRATE_RAMP_UP_STEP_MAX_BPS 250000U
#define WEBRTCPUSH_REMB_DOWN_STEP_PERCENT 10U
#define WEBRTCPUSH_REMB_DOWN_STEP_MIN_BPS 60000U
#define WEBRTCPUSH_REMB_DOWN_STEP_MAX_BPS 120000U
/* REMB 低估保护:pacing 不堵时,过低浏览器估计不直接压糊 720p。 */
#define WEBRTCPUSH_HEALTHY_PROBE_TARGET_BPS 2700000U
/* 3.375Mbps * 80% = 2.7Mbps. 这是 REMB 原始估计的探测地板,不是编码码率。 */
#define WEBRTCPUSH_REMB_SANE_FLOOR_BPS 3375000U
#define WEBRTCPUSH_REMB_SANE_RAW_MAX_BPS WEBRTCPUSH_REMB_SANE_FLOOR_BPS
#define WEBRTCPUSH_REMB_SANE_PACING_MAX_MS 100U
/* IDR 会产生短时大包,不能在这个窗口内把瞬时 pacing 峰值误判成网络拥塞。 */
#define WEBRTCPUSH_REMB_IDR_GRACE_MS 1000U
/* 手机切 Wi-Fi/蜂窝或换手机时,旧成员的 leave 可能晚于新 offer 到达。 */
#define WEBRTCPUSH_PEER_LEAVE_GRACE_MS 8000U
/* 达到 2.3Mbps 后继续发送一段时间,让浏览器有真实流量可重新估计带宽。 */
#define WEBRTCPUSH_INITIAL_PROBE_HOLD_MS 6000U
#define WEBRTCPUSH_PACING_HEADROOM_PERCENT 135U
#define WEBRTCPUSH_PACING_INTERVAL_MS 5U
#define WEBRTCPUSH_PACING_MAX_BITRATE 4000000U
#define WEBRTCPUSH_PACING_MAX_QUEUE_MS 250U
#define WEBRTCPUSH_PACING_GUARD_COOLDOWN_MS 2000U
#define WEBRTCPUSH_PACING_MAX_BITRATE 4200000U
#define WEBRTCPUSH_PACING_MAX_QUEUE_MS 260U
#define WEBRTCPUSH_PACING_GUARD_COOLDOWN_MS 1000U
/* 首个/刚恢复的 IDR 允许短暂排队,避免首屏关键帧刚发出就被清队列 */
#define WEBRTCPUSH_PACING_IDR_GRACE_MS 800U
#define WEBRTCPUSH_PACING_IDR_GRACE_MS 250U
/* pacing 清队列后,若近期已有 IDR,不要反复强制 IDR 造成 I 帧风暴 */
#define WEBRTCPUSH_PACING_RESYNC_IDR_MS 5000U
/* RK MPP 运行中小幅改 bps 容易顿一下;小变化只调 pacing,少重配硬编。 */
#define WEBRTCPUSH_MPP_RECONFIG_MIN_DELTA_PERCENT 20U
#define WEBRTCPUSH_MPP_RECONFIG_MAX_STEP_PERCENT 25U
#define WEBRTCPUSH_MPP_RECONFIG_MIN_INTERVAL_MS 1000U
/* RTP 分片与 NACK(MTU=1200,留 SRTP/DTLS/FU 余量) */
#define WEBRTCPUSH_RTP_MAX_FRAGMENT 1050U
#define WEBRTCPUSH_NACK_PACKETS 512U
/* PLI/IDR 节流:避免频繁 force-key-unit 拉高瞬时码率 */
#define WEBRTCPUSH_PLI_IDR_THROTTLE_MS 500U
#define WEBRTCPUSH_PLI_IDR_THROTTLE_MS 2000U /* 500->2000: 避免IDR风暴, 每个IDR都加大pacing堆积 */
/* 周期性推流指标日志间隔 */
#define WEBRTCPUSH_STATS_LOG_INTERVAL_MS 8000U
......@@ -66,8 +87,7 @@
#define WEBRTCPUSH_MPP_POST_ENC_BUFFERS 1 /* 编码后保留 AU */
#define WEBRTCPUSH_APPSINK_MAX_BUFFERS 1
/* MJPEG 解压:1=GStreamer CPU jpegdec 优先;0=mppjpegdec 优先 */
#define WEBRTCPUSH_MJPEG_CPU_DECODE 1
/* native MJPEG 解压固定使用 libjpeg-turbo(jpeglib) 手动解码;旧 GStreamer 解码分支已删除 */
/* MPP 码控收紧:低 REMB 时实际 H264 不能长期高于目标太多 */
#define WEBRTCPUSH_MPP_BPS_MIN_PERCENT 60U
......@@ -96,6 +116,15 @@
/* 音频播放(手机->设备):ALSA 喇叭设备 */
#define WEBRTCPUSH_AUDIO_PLAYBACK_DEVICE "plughw:2,0"
/*
* 手机->设备喊话音量策略:
* 0:后端返回 0 就按 0 播放(静音)
* 1:后端返回 0 时按 WEBRTCPUSH_AUDIO_ZERO_VOLUME_MIN 播放,便于测试喊话链路
*/
#define WEBRTCPUSH_AUDIO_ZERO_VOLUME_AS_MIN 0
#define WEBRTCPUSH_AUDIO_ZERO_VOLUME_MIN 0.5
/* 手机喊话结束后短暂保温,避免每个短包都冷启动声卡;到时仍会释放喇叭。 */
#define WEBRTCPUSH_AUDIO_SINK_IDLE_TIMEOUT_MS 3000U
/* 后端音量控制接口 */
#define WEBRTCPUSH_VOLUME_API_BASE "https://fcrs-api.yd-ss.com/api/drive/use/status/"
......@@ -104,10 +133,11 @@
/*
* 设备侧 DataChannel(myDataChannel)与手机 createDataChannel('init') 争用 SCTP,
* 易触发 sctpenc association error 并导致管道闪断/进程崩溃。仅推流可不建。
* 设备侧 DataChannel(myDataChannel):
* - 前端 ondatachannel 后用 remoteChannel 发送 MP3 分片 + "EOF" 给设备播放;
* - 设备也通过该通道发送 Mbps 字符串,更新右上角网络显示。
*/
#define WEBRTCPUSH_ENABLE_DATACHANNEL 0
#define WEBRTCPUSH_ENABLE_DATACHANNEL 1
/* WebRTC 信令 WebSocket 主机(路径 /websocket?dev=设备号) */
#define WEBRTCPUSH_SIGNAL_HOST "signal.yd-ss.com"
......
......@@ -23,11 +23,15 @@ static gchar *s_ws_url;
static GMainContext *s_main_ctx;
static gchar *s_debounced_offer_sdp = NULL;
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 void ws_connect(void);
static void reset_signaling_session(AppState *app, gboolean teardown_pipeline);
static gboolean idle_pipeline_teardown(gpointer p);
static gboolean delayed_peer_leave_reset(gpointer p);
static void schedule_tx_flush(void);
static void dispatch_json(const gchar *payload);
......@@ -228,6 +232,12 @@ static gboolean debounced_offer_fire(gpointer user_data) {
}
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) {
g_free(s_pending_offer_sdp);
s_pending_offer_sdp = g_strdup(sdp);
......@@ -388,8 +398,14 @@ static void dispatch_json(const gchar *payload) {
if (!g_strcmp0(t, "leave")) {
if (rtc_client_is_active(s_app)) {
my_zlog_info("webrtcpush: peer left, reset libdatachannel peer");
g_idle_add(idle_pipeline_teardown, s_app);
if (s_peer_leave_reset_id)
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);
return;
......@@ -436,6 +452,12 @@ static void dispatch_json(const gchar *payload) {
if (json_node_get_value_type(midn) == G_TYPE_STRING)
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);
}
}
......@@ -506,6 +528,26 @@ static gboolean idle_pipeline_teardown(gpointer p) {
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) {
if (s_ws_exiting || !s_app || !s_ws_url)
return;
......@@ -610,6 +652,10 @@ void ws_signaling_stop(AppState *app) {
g_source_remove(s_reconnect_id);
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) {
g_source_remove(s_ping_id);
s_ping_id = 0;
......
......@@ -9,4 +9,4 @@ file perms = 600
millisecond = "%d(%Y-%m-%d %H:%M:%S).%ms [%V] %m%n"
[rules]
my_log.* "/home/orangepi/car/master/log/log_2026-07-08.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