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 @@ ...@@ -28,6 +28,14 @@
#define CAR0107_BACK_US_RUN_MIN 1440 /* val=53 起步即动(须低于死区 1450) */ #define CAR0107_BACK_US_RUN_MIN 1440 /* val=53 起步即动(须低于死区 1450) */
#define CAR0107_BACK_US_RUN_MAX 1300 /* 后退最大速度再降20%:val=200 -> 1300us */ #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 { typedef enum {
CAR0107_ESC_DIR_FORWARD = 1, /* App mode1 前进:高脉宽(与 0101 相反) */ CAR0107_ESC_DIR_FORWARD = 1, /* App mode1 前进:高脉宽(与 0101 相反) */
CAR0107_ESC_DIR_BACK = 2, /* App mode2 后退:低脉宽(与 0101 相反) */ CAR0107_ESC_DIR_BACK = 2, /* App mode2 后退:低脉宽(与 0101 相反) */
...@@ -1392,3 +1400,92 @@ void car0107_esc_thread_close(void) ...@@ -1392,3 +1400,92 @@ void car0107_esc_thread_close(void)
pthread_mutex_unlock(&s_esc_start_mutex); pthread_mutex_unlock(&s_esc_start_mutex);
my_zlog_info("car0107 esc thread closed"); 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); ...@@ -14,4 +14,7 @@ void car0107_esc_thread_start(void);
void car0107_esc_thread_wake(void); void car0107_esc_thread_wake(void);
void car0107_esc_thread_close(void); void car0107_esc_thread_close(void);
/* idle 唤醒动作:低速后退 -> 低速前进 -> 前进刹车停止 */
void car0107_idle_wakeup_maneuver(void);
#endif #endif
...@@ -6,16 +6,12 @@ ...@@ -6,16 +6,12 @@
#define CAR0107_IDLE_CHECK_MS 10000 #define CAR0107_IDLE_CHECK_MS 10000
#define CAR0107_IDLE_TIMEOUT_MS (5 * 60 * 1000) #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 { typedef struct {
pthread_mutex_t mutex; pthread_mutex_t mutex;
ThreadPool_t *pool; ThreadPool_t *pool;
bool shutdown; bool shutdown;
bool pool_ready; bool pool_ready;
int next_left;
} car0107_idle_ctx_t; } car0107_idle_ctx_t;
static car0107_idle_ctx_t s_idle_ctx = { static car0107_idle_ctx_t s_idle_ctx = {
...@@ -23,7 +19,6 @@ static car0107_idle_ctx_t s_idle_ctx = { ...@@ -23,7 +19,6 @@ static car0107_idle_ctx_t s_idle_ctx = {
.pool = NULL, .pool = NULL,
.shutdown = false, .shutdown = false,
.pool_ready = false, .pool_ready = false,
.next_left = 1,
}; };
static void car0107_idle_lock(void) static void car0107_idle_lock(void)
...@@ -67,16 +62,6 @@ static int car0107_idle_pool_init(void) ...@@ -67,16 +62,6 @@ static int car0107_idle_pool_init(void)
return 0; 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) static void car0107_idle_monitor_task(void *arg)
{ {
(void)arg; (void)arg;
...@@ -85,7 +70,6 @@ static void car0107_idle_monitor_task(void *arg) ...@@ -85,7 +70,6 @@ static void car0107_idle_monitor_task(void *arg)
while (!car0107_idle_shutdown_get()) { while (!car0107_idle_shutdown_get()) {
long long now; long long now;
long long last; long long last;
int turn_left;
delay_ms(CAR0107_IDLE_CHECK_MS); delay_ms(CAR0107_IDLE_CHECK_MS);
if (car0107_idle_shutdown_get() || g_device_type != DEVICE_CAR0107) { if (car0107_idle_shutdown_get() || g_device_type != DEVICE_CAR0107) {
...@@ -101,15 +85,10 @@ static void car0107_idle_monitor_task(void *arg) ...@@ -101,15 +85,10 @@ static void car0107_idle_monitor_task(void *arg)
continue; continue;
} }
car0107_idle_lock(); my_zlog_info("car0107 idle %dmin: wakeup maneuver (reverse->forward->brake)",
turn_left = s_idle_ctx.next_left; CAR0107_IDLE_TIMEOUT_MS / 60000);
s_idle_ctx.next_left = !s_idle_ctx.next_left; /* 低速后退一点 -> 低速前进一点 -> 前进刹车停止(遵守刹车动作)*/
car0107_idle_unlock(); car0107_idle_wakeup_maneuver();
my_zlog_info("car0107 idle %dmin: nudge steer %s",
CAR0107_IDLE_TIMEOUT_MS / 60000,
turn_left ? "left" : "right");
car0107_idle_nudge_steering(turn_left);
car0107_notify_control_activity(); car0107_notify_control_activity();
} }
...@@ -133,7 +112,7 @@ void car0107_idle_startup(int device_id) ...@@ -133,7 +112,7 @@ void car0107_idle_startup(int device_id)
car0107_idle_unlock(); car0107_idle_unlock();
car0107_notify_control_activity(); 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); CAR0107_IDLE_TIMEOUT_MS / 60000);
} }
......
...@@ -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 "common.h" #include "common.h"
#include "audioplay.h" #include "audioplay.h"
#include "audio_sink.h" /* USB声卡排队锁 */
#include "device_identity.h" #include "device_identity.h"
#include "mqtt_init.h" #include "mqtt_init.h"
#include "http_config_mqtt.h" #include "http_config_mqtt.h"
#include "audiotts_play.h" #include "audiotts_play.h"
#include "wifi_autoconfig.h" #include "wifi_autoconfig.h"
#include <stdio.h> #include <stdio.h>
#include <pthread.h>
#include <strings.h> #include <strings.h>
#include <unistd.h> #include <unistd.h>
#include <sys/wait.h> #include <sys/wait.h>
#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_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];
...@@ -21,6 +25,7 @@ static double s_audio_volume=0.8; ...@@ -21,6 +25,7 @@ static double s_audio_volume=0.8;
static int s_local_play_pending = 0; static int s_local_play_pending = 0;
static char s_local_filepath[512]; 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) { static int local_is_cn_lang(const char *lang) {
return lang != NULL && (strcmp(lang, AUDIO_LANG_ZH) == 0 || strcmp(lang, "cn") == 0); 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 ...@@ -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) { 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); my_zlog_warn("2017 本地音频不存在: %s", filename);
return; return;
} }
pthread_mutex_lock(&s_local_play_mutex);
snprintf(s_local_filepath, sizeof(s_local_filepath), "%s", resolved);
s_local_play_pending = 1; 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) { void audioplay_local_mqtt_receive(cJSON *body) {
...@@ -143,6 +152,144 @@ static double audioplay_volume_clamp(double v) { ...@@ -143,6 +152,144 @@ static double audioplay_volume_clamp(double v) {
int audio_wheat_init(); 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) { void audioplay_mqtt_receive(cJSON *json) {
// 解析"audioLink"字段(修正了原始JSON中的拼写错误) // 解析"audioLink"字段(修正了原始JSON中的拼写错误)
...@@ -212,26 +359,21 @@ void audioplay_send_mqtt() { ...@@ -212,26 +359,21 @@ void audioplay_send_mqtt() {
//音频播放 //音频播放
void audioplay_cycle(){ void audioplay_cycle(){
char command[1024];
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 {
...@@ -241,16 +383,25 @@ void audioplay_cycle(){ ...@@ -241,16 +383,25 @@ void audioplay_cycle(){
audioplay_send_mqtt(); 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) { if (s_local_play_pending) {
s_local_play_pending = 0; s_local_play_pending = 0;
snprintf(command, sizeof(command), snprintf(local_filepath, sizeof(local_filepath), "%s", s_local_filepath);
"ffplay -nodisp -autoexit -loglevel quiet \"%s\"", s_local_filepath); local_play_pending = 1;
my_zlog_debug("播放本地音频: %s", s_local_filepath); }
ret = system(command); pthread_mutex_unlock(&s_local_play_mutex);
if (WIFEXITED(ret) && WEXITSTATUS(ret) == 0) {
my_zlog_debug("本地音频播放完成: %s", s_local_filepath); 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 { } 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() { ...@@ -374,7 +525,7 @@ int audio_speaker_init() {
int audio_init(){ int audio_init(){
delay_s(5); delay_s(1);
audio_wheat_init(); audio_wheat_init();
delay_s(1); delay_s(1);
audio_speaker_init(); audio_speaker_init();
......
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.
...@@ -8,6 +8,7 @@ pkg_check_modules(WEBRTCPUSH_GST REQUIRED ...@@ -8,6 +8,7 @@ pkg_check_modules(WEBRTCPUSH_GST REQUIRED
) )
pkg_check_modules(WEBRTCPUSH_JSON REQUIRED json-glib-1.0) pkg_check_modules(WEBRTCPUSH_JSON REQUIRED json-glib-1.0)
pkg_check_modules(WEBRTCPUSH_SOUP REQUIRED libsoup-2.4) pkg_check_modules(WEBRTCPUSH_SOUP REQUIRED libsoup-2.4)
pkg_check_modules(WEBRTCPUSH_JPEG REQUIRED libjpeg)
file(GLOB_RECURSE MODULES_SOURCES file(GLOB_RECURSE MODULES_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/logger/*.c ${CMAKE_CURRENT_SOURCE_DIR}/logger/*.c
...@@ -47,6 +48,7 @@ set(MODULES_INCLUDE_DIRS ...@@ -47,6 +48,7 @@ set(MODULES_INCLUDE_DIRS
${WEBRTCPUSH_GST_INCLUDE_DIRS} ${WEBRTCPUSH_GST_INCLUDE_DIRS}
${WEBRTCPUSH_JSON_INCLUDE_DIRS} ${WEBRTCPUSH_JSON_INCLUDE_DIRS}
${WEBRTCPUSH_SOUP_INCLUDE_DIRS} ${WEBRTCPUSH_SOUP_INCLUDE_DIRS}
${WEBRTCPUSH_JPEG_INCLUDE_DIRS}
PARENT_SCOPE PARENT_SCOPE
) )
...@@ -54,6 +56,7 @@ set(WEBRTCPUSH_LIBRARIES ...@@ -54,6 +56,7 @@ set(WEBRTCPUSH_LIBRARIES
${WEBRTCPUSH_GST_LIBRARIES} ${WEBRTCPUSH_GST_LIBRARIES}
${WEBRTCPUSH_JSON_LIBRARIES} ${WEBRTCPUSH_JSON_LIBRARIES}
${WEBRTCPUSH_SOUP_LIBRARIES} ${WEBRTCPUSH_SOUP_LIBRARIES}
${WEBRTCPUSH_JPEG_LIBRARIES}
PARENT_SCOPE PARENT_SCOPE
) )
...@@ -61,5 +64,6 @@ set(WEBRTCPUSH_CFLAGS ...@@ -61,5 +64,6 @@ set(WEBRTCPUSH_CFLAGS
${WEBRTCPUSH_GST_CFLAGS_OTHER} ${WEBRTCPUSH_GST_CFLAGS_OTHER}
${WEBRTCPUSH_JSON_CFLAGS_OTHER} ${WEBRTCPUSH_JSON_CFLAGS_OTHER}
${WEBRTCPUSH_SOUP_CFLAGS_OTHER} ${WEBRTCPUSH_SOUP_CFLAGS_OTHER}
${WEBRTCPUSH_JPEG_CFLAGS_OTHER}
PARENT_SCOPE PARENT_SCOPE
) )
...@@ -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;
......
#include "audio_sink.h" #include "audio_sink.h"
#include "webrtcpush_config.h"
#include "webrtcpush_log.h" #include "webrtcpush_log.h"
#include <gst/gst.h> /* USB声卡排队锁: audio_sink 和本地音频串行访问 plughw:2,0 */
#include <gst/app/gstappsrc.h> static GMutex g_alsa_device_lock;
static AudioSink *g_audio_sink_singleton = NULL;
/* /*
* 手机→设备方向的音频播放管道(按键模式): * 手机→设备方向的音频不再走 RTP:手机端 audio 为 recvonly(只收不发),
* appsrc(opus payload) → opusdec → audioconvert → audioresample → volume → alsasink * 手机→设备的音频通过 DataChannel 发送 MP4,由 decodebin 播放。
* * 因此本模块不再创建 GStreamer 接收管道,仅保留 USB 声卡全局锁,
* 手机端是"按住说话"模式(最长 15s),不是持续推流,因此: * 供 DataChannel 播放线程与本地提示音串行访问 ALSA 设备使用。
* - on_audio_message 收到包后入队,立即返回(不阻塞 libdatachannel 线程)
* - 独立线程消费队列,维护"按键会话"
* - 会话开始(首包到达):pipeline 切到 PLAYING
* - 会话结束(500ms 无包 或 15s 超时):flush appsrc + pipeline 切到 READY
* 切到 READY 释放 ALSA 设备,避免 alsasink 持续占用/空转
*/ */
#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 { struct AudioSink {
GstElement *pipeline; GMutex lock;
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 */
}; };
static void flush_queue(AudioSink *src) 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); }
SinkPacket *pkt;
while ((pkt = g_async_queue_try_pop(src->queue)) != NULL) {
g_free(pkt->data);
g_free(pkt);
}
}
static void set_pipeline_state_locked(AudioSink *src, GstState state) void audio_sink_interrupt(AudioSink *src)
{ {
if (!src->pipeline) (void)src;
return; /* RTP 接收管道已移除,无会话需要中断;DataChannel 播放通过 lock/unlock 串行访问 ALSA。 */
/* 切到 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;
} }
AudioSink *audio_sink_start(const char *alsa_device, char **error_message) AudioSink *audio_sink_start(const char *alsa_device, char **error_message)
{ {
AudioSink *src; AudioSink *src;
GstElement *pipe, *asrc, *dec, *conv, *resample, *vol, *sink; (void)alsa_device;
GstCaps *caps;
GstStateChangeReturn ret;
if (!alsa_device || !alsa_device[0]) {
if (error_message) if (error_message)
*error_message = g_strdup("no ALSA device"); *error_message = NULL;
return NULL;
}
src = g_new0(AudioSink, 1); 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); g_mutex_init(&src->lock);
src->session_start_us = 0; g_audio_sink_singleton = src;
src->pipeline_playing = FALSE; my_zlog_info("audio_sink: started (RTP receive removed, ALSA lock only)");
/* 初始状态 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);
return src; return src;
} }
...@@ -233,45 +43,15 @@ void audio_sink_stop(AudioSink *src) ...@@ -233,45 +43,15 @@ void audio_sink_stop(AudioSink *src)
{ {
if (!src) if (!src)
return; return;
if (src->thread) { if (g_audio_sink_singleton == src)
g_mutex_lock(&src->lock); g_audio_sink_singleton = NULL;
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);
}
g_mutex_clear(&src->lock); g_mutex_clear(&src->lock);
g_free(src); 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) void audio_sink_set_volume(AudioSink *src, double volume)
{ {
if (!src || !src->vol) (void)src;
return; (void)volume;
if (volume < 0.0) /* 无 GStreamer 管道,音量设置为空操作(保留接口供 volume_control 调用)。 */
volume = 0.0;
if (volume > 1.0)
volume = 1.0;
g_object_set(src->vol, "volume", volume, NULL);
} }
...@@ -17,4 +17,11 @@ gboolean audio_sink_push_opus(AudioSink *src, const uint8_t *data, size_t size); ...@@ -17,4 +17,11 @@ gboolean audio_sink_push_opus(AudioSink *src, const uint8_t *data, size_t size);
/* 设置播放音量 0.0~1.0 */ /* 设置播放音量 0.0~1.0 */
void audio_sink_set_volume(AudioSink *src, double volume); void audio_sink_set_volume(AudioSink *src, double volume);
/* 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 #endif
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
...@@ -17,37 +17,58 @@ ...@@ -17,37 +17,58 @@
/* /*
* 与 gst_webrtc_pipeline / jywy 浏览器推流对齐的码率策略。 * 与 gst_webrtc_pipeline / jywy 浏览器推流对齐的码率策略。
* 首屏先用 900kbps,不像 1.4Mbps 那样猛冲,也不要低到一进来就糊。 * 首屏先用 1.06Mbps,不像高码率那样猛冲,也不要低到一进来就糊。
* RTCP REMB 只作为码率趋势,下降也做阶梯平滑,避免 MPP 动态切码率时卡顿 * RTCP REMB 只作为码率趋势:小步慢降、慢升、带滞回,尽量接近浏览器的无感自适应
*/ */
#define WEBRTCPUSH_INITIAL_BITRATE 900000U #define WEBRTCPUSH_INITIAL_BITRATE 1060000U
#define WEBRTCPUSH_MIN_BITRATE 500000U #define WEBRTCPUSH_MIN_BITRATE 800000U
#define WEBRTCPUSH_MAX_BITRATE 2800000U #define WEBRTCPUSH_MAX_BITRATE 3000000U
#define WEBRTCPUSH_REMB_UTIL_PERCENT 80U #define WEBRTCPUSH_REMB_UTIL_PERCENT 80U
#define WEBRTCPUSH_REMB_DOWN_MIN_STEP 100000U #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_REMB_SEVERE_PERCENT 65U
#define WEBRTCPUSH_BITRATE_RAMP_UP_MS 2500U #define WEBRTCPUSH_BITRATE_RAMP_UP_MS 1200U
#define WEBRTCPUSH_BITRATE_RAMP_DOWN_MS 1500U #define WEBRTCPUSH_BITRATE_RAMP_DOWN_MS 2000U
#define WEBRTCPUSH_REMB_DOWN_STEP_PERCENT 15U #define WEBRTCPUSH_BITRATE_RAMP_UP_PERCENT 12U
#define WEBRTCPUSH_REMB_DOWN_STEP_MIN_BPS 80000U #define WEBRTCPUSH_BITRATE_RAMP_UP_STEP_MIN_BPS 70000U
#define WEBRTCPUSH_REMB_DOWN_STEP_MAX_BPS 180000U #define WEBRTCPUSH_BITRATE_RAMP_UP_STEP_MAX_BPS 250000U
#define WEBRTCPUSH_PACING_HEADROOM_PERCENT 140U #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_INTERVAL_MS 5U
#define WEBRTCPUSH_PACING_MAX_BITRATE 4000000U #define WEBRTCPUSH_PACING_MAX_BITRATE 4200000U
#define WEBRTCPUSH_PACING_MAX_QUEUE_MS 250U #define WEBRTCPUSH_PACING_MAX_QUEUE_MS 260U
#define WEBRTCPUSH_PACING_GUARD_COOLDOWN_MS 2000U #define WEBRTCPUSH_PACING_GUARD_COOLDOWN_MS 1000U
/* 首个/刚恢复的 IDR 允许短暂排队,避免首屏关键帧刚发出就被清队列 */ /* 首个/刚恢复的 IDR 允许短暂排队,避免首屏关键帧刚发出就被清队列 */
#define WEBRTCPUSH_PACING_IDR_GRACE_MS 800U #define WEBRTCPUSH_PACING_IDR_GRACE_MS 250U
/* pacing 清队列后,若近期已有 IDR,不要反复强制 IDR 造成 I 帧风暴 */ /* pacing 清队列后,若近期已有 IDR,不要反复强制 IDR 造成 I 帧风暴 */
#define WEBRTCPUSH_PACING_RESYNC_IDR_MS 5000U #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 余量) */ /* RTP 分片与 NACK(MTU=1200,留 SRTP/DTLS/FU 余量) */
#define WEBRTCPUSH_RTP_MAX_FRAGMENT 1050U #define WEBRTCPUSH_RTP_MAX_FRAGMENT 1050U
#define WEBRTCPUSH_NACK_PACKETS 512U #define WEBRTCPUSH_NACK_PACKETS 512U
/* PLI/IDR 节流:避免频繁 force-key-unit 拉高瞬时码率 */ /* 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 #define WEBRTCPUSH_STATS_LOG_INTERVAL_MS 8000U
...@@ -66,8 +87,7 @@ ...@@ -66,8 +87,7 @@
#define WEBRTCPUSH_MPP_POST_ENC_BUFFERS 1 /* 编码后保留 AU */ #define WEBRTCPUSH_MPP_POST_ENC_BUFFERS 1 /* 编码后保留 AU */
#define WEBRTCPUSH_APPSINK_MAX_BUFFERS 1 #define WEBRTCPUSH_APPSINK_MAX_BUFFERS 1
/* MJPEG 解压:1=GStreamer CPU jpegdec 优先;0=mppjpegdec 优先 */ /* native MJPEG 解压固定使用 libjpeg-turbo(jpeglib) 手动解码;旧 GStreamer 解码分支已删除 */
#define WEBRTCPUSH_MJPEG_CPU_DECODE 1
/* MPP 码控收紧:低 REMB 时实际 H264 不能长期高于目标太多 */ /* MPP 码控收紧:低 REMB 时实际 H264 不能长期高于目标太多 */
#define WEBRTCPUSH_MPP_BPS_MIN_PERCENT 60U #define WEBRTCPUSH_MPP_BPS_MIN_PERCENT 60U
...@@ -96,6 +116,15 @@ ...@@ -96,6 +116,15 @@
/* 音频播放(手机->设备):ALSA 喇叭设备 */ /* 音频播放(手机->设备):ALSA 喇叭设备 */
#define WEBRTCPUSH_AUDIO_PLAYBACK_DEVICE "plughw:2,0" #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/" #define WEBRTCPUSH_VOLUME_API_BASE "https://fcrs-api.yd-ss.com/api/drive/use/status/"
...@@ -104,10 +133,11 @@ ...@@ -104,10 +133,11 @@
/* /*
* 设备侧 DataChannel(myDataChannel)与手机 createDataChannel('init') 争用 SCTP, * 设备侧 DataChannel(myDataChannel):
* 易触发 sctpenc association error 并导致管道闪断/进程崩溃。仅推流可不建。 * - 前端 ondatachannel 后用 remoteChannel 发送 MP3 分片 + "EOF" 给设备播放;
* - 设备也通过该通道发送 Mbps 字符串,更新右上角网络显示。
*/ */
#define WEBRTCPUSH_ENABLE_DATACHANNEL 0 #define WEBRTCPUSH_ENABLE_DATACHANNEL 1
/* WebRTC 信令 WebSocket 主机(路径 /websocket?dev=设备号) */ /* WebRTC 信令 WebSocket 主机(路径 /websocket?dev=设备号) */
#define WEBRTCPUSH_SIGNAL_HOST "signal.yd-ss.com" #define WEBRTCPUSH_SIGNAL_HOST "signal.yd-ss.com"
......
...@@ -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-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