Commit 7f0fd331 authored by 957dd's avatar 957dd

第三次未改完提交

parent 2eb47ff8
......@@ -725,6 +725,41 @@ int orange_pi_save_wifi(const char *ssid, const char *password) {
return 0;
}
int orange_pi_connect_wifi(const char *ssid, const char *password) {
char command[512];
int ret;
if (!ssid || !ssid[0])
return -1;
if (password && password[0]) {
if (validate_wifi_credentials(ssid, password) != 0)
return -1;
} else if (strlen(ssid) >= SSID_MAX_LENGTH || strpbrk(ssid, "\"`$|;\\")) {
my_zlog_error("SSID无效");
return -1;
}
if (!wifi_connection_exists(ssid) && enforce_saved_wifi_limit(ssid) != 0)
return -1;
if (password && password[0]) {
snprintf(command, sizeof(command),
"nmcli device wifi connect '%s' password '%s'", ssid, password);
} else {
snprintf(command, sizeof(command), "nmcli device wifi connect '%s'", ssid);
}
ret = system(command);
if (ret != 0) {
my_zlog_error("nmcli connect 失败: %s (ret=%d)", ssid, ret);
return -1;
}
my_zlog_info("[SUCCESS] 已连接WiFi: %s", ssid);
return 0;
}
static int ensure_wifi_pool(void) {
pthread_mutex_lock(&s_wifi_pool_mutex);
if (!s_wifi_pool)
......
......@@ -12,4 +12,10 @@ void device_wifi_rec_delete(cJSON *body);
//接收到保存wifi消息
void device_wifi_rec_sava(cJSON *body);
/* 保存 WiFi 配置(含满 10 个自动删最旧逻辑),不立即连接 */
int orange_pi_save_wifi(const char *ssid, const char *password);
/* 保存并立即连接 WiFi(含满 10 个自动删最旧逻辑) */
int orange_pi_connect_wifi(const char *ssid, const char *password);
#endif
\ No newline at end of file
......@@ -72,6 +72,11 @@ static int init_ipc_unix_server(void)
return ipc_start_unix_server(NULL);
}
static int init_wifi_config_agent(void)
{
return wifi_config_agent_start();
}
static int init_worker_threads(void)
{
return thread_start_init(thread_exit_time, thread_mqtt_beat,
......@@ -99,6 +104,7 @@ static const AppInitStep s_init_steps[] = {
{APP_INIT_MODULE_SERVICE, "mqtt_video", http_mqtt_video_init, 1, -6, "请求mqtt服务器和video数据失败"},
{APP_INIT_MODULE_SERVICE, "go_deploy", init_go_deploy, 0, 0, "Go 程序检查/部署失败"},
{APP_INIT_MODULE_SERVICE, "ipc_unix_server", init_ipc_unix_server, 1, -8, "IPC Unix Socket Server 启动失败"},
{APP_INIT_MODULE_SERVICE, "wifi_config_agent", init_wifi_config_agent, 0, 0, "WiFi配置助手UDP服务启动失败"},
{APP_INIT_RUNTIME, "worker_threads", init_worker_threads, 1, -7, "多线程初始化失败"},
};
......@@ -137,6 +143,7 @@ static int run_init_steps(void)
static void app_shutdown(void)
{
wifi_config_agent_stop();
thread_end_close();
device_end_close(g_device_type);
self_control_thread_close(); // 销毁自控的线程
......
......@@ -68,10 +68,13 @@ void *thread_exit_time(void *arg)
}
}
if (g_devcontrol_exit_count >= 5)
if (g_devcontrol_exit_count == 5)
{
device_stop(g_device_type);
g_devcontrol_exit_count = 6;
}
else if (g_devcontrol_exit_count > 5)
{
g_devcontrol_exit_count = 5;
}
pthread_mutex_unlock(&g_exit_count_mutex);
}
......@@ -148,8 +151,14 @@ static void run_chromium_cam_loop(void)
delay_s(5);
while (1) {
if (s_webrtc_index == 1) {
if (is_browser_running() == true)
if (!browser_auto_restart_allowed()) {
delay_ms(200);
continue;
}
if (is_browser_running() == true) {
delay_ms(200);
continue;
}
my_zlog_info("open cam (Chromium fallback)");
opencamsh();
}
......
No preview for this file type
......@@ -2,14 +2,76 @@
#include "car0107_control.h"
#include "modules_common.h"
#include "gpio_common.h"
#include "mqtt_infor_handle.h"
#define CTRL_NONE 0
#define CTRL_PHONE 1
#define CTRL_STEERING 2
/* 双向车模电调 RC PWM:50Hz,脉宽 1000~2000us,中位 1500us */
#define CAR0107_ESC_US_MIN 1000
#define CAR0107_ESC_US_MAX 2000
#define CAR0107_ESC_US_NEUTRAL 1500
#define CAR0107_ESC_DEADZONE_LO 1450
#define CAR0107_ESC_DEADZONE_HI 1550
#define CAR0107_ESC_RAMP_STEP_NEUTRAL 8
#define CAR0107_ESC_RAMP_STEP_RUN 4 /* 正常前进/后退线性爬升:约 4us/tick */
#define CAR0107_ESC_NEUTRAL_HOLD_TICKS 8
#define CAR0107_ESC_BRAKE_HOLD_TICKS 20 /* 约 500ms @25ms tick,满刹保持 */
#define CAR0107_PHONE_VAL_MAX 200 /* App 前后油门最大值 */
#define CAR0107_FORWARD_PWM_CAP 83 /* 前进 PWM 封顶 */
#define CAR0107_FWD_US_RUN_MIN (79 * 20) /* val=53 前进最低行走 ~1580us */
#define CAR0107_FWD_US_RUN_MAX (CAR0107_FORWARD_PWM_CAP * 20) /* val=200 ~1660us */
#define CAR0107_BACK_US_RUN_MIN (71 * 20) /* val=53 后退最低行走 ~1420us */
#define CAR0107_BACK_US_RUN_MAX (58 * 20) /* val=200 后退最深 ~1160us */
typedef enum {
CAR0107_ESC_DIR_FORWARD = 1, /* App mode1 前进:高脉宽(与 0101 相反) */
CAR0107_ESC_DIR_BACK = 2, /* App mode2 后退:低脉宽(与 0101 相反) */
} car0107_esc_dir_t;
typedef enum {
CAR0107_ESC_STOP = 0,
CAR0107_ESC_FORWARD,
CAR0107_ESC_REVERSE,
CAR0107_ESC_TRANSITION
} car0107_esc_state_t;
typedef enum {
CAR0107_TRANS_BRAKE = 0, /* 前进松手/换后退:1000us 满刹 */
CAR0107_TRANS_NEUTRAL_HOLD, /* 回中位 1500us */
CAR0107_TRANS_RAMP_RUN /* 慢速爬到目标油门 */
} car0107_trans_phase_t;
typedef struct {
car0107_esc_state_t state;
car0107_trans_phase_t trans_phase;
int current_us;
int target_us;
int cmd_dir;
int pending_dir;
int pending_gval;
int brake_dir;
int brake_ticks;
int neutral_hold_ticks;
} car0107_esc_ctx_t;
#define STEER_STEP_TURN 60
#define STEER_STEP_RETURN 90
#define REV_TARGET_SCALE_NUM 6
#define REV_TARGET_SCALE_DEN 10
#define CAR0107_MAX_INPUT_SPEED 200
#define CAR0107_ACTIVE_THRESHOLD 50
#define CAR0107_MAX_TARGET_BACK 100
#define CAR0107_MAX_TARGET_FRONT 80
#define CAR0107_ESC_RELEASE_MAX 52 /* val<=52:松手/刹车/停下 */
#define CAR0107_ESC_GVAL_MIN 53 /* val>=53 才视为有效油门 */
#define STEER_GVAL_MAX 150
static pthread_mutex_t s_car0107_hw_mutex = PTHREAD_MUTEX_INITIALIZER;
static int s_control_source = CTRL_NONE;
static long long s_last_control_ms = 0;
static int s_control_source = CTRL_NONE;
static int s_target_angle = 90;
static int s_current_angle = 90;
static int s_steering_mode = 0;
......@@ -21,37 +83,38 @@ static int s_current_speed = 0;
static int s_speed_mode = 0;
static int s_speed_timeout = 0;
static int s_phone_speed_mode = 0;
static int s_phone_current_gval = 0;
static int s_phone_pending_mode = 0;
static int s_phone_pending_gval = 0;
#define STEER_STEP_TURN 60
#define STEER_STEP_RETURN 90
#define REV_TARGET_SCALE_NUM 6
#define REV_TARGET_SCALE_DEN 10
#define ACCEL_STEP 5
#define DECEL_STEP 6
#define BRAKE_STEP 10
#define CAR0107_MAX_INPUT_SPEED 200
#define CAR0107_ACTIVE_THRESHOLD 50
#define CAR0107_MAX_TARGET_BACK 100
#define CAR0107_MAX_TARGET_FRONT 80
#define STEER_GVAL_MAX 120
#define CAR0107_PWM_FORWARD_MIN 58
#define CAR0107_PWM_BACK_MAX 84
static int car0107_clamp_input(int val) {
if (val < 0) {
return 0;
}
if (val > CAR0107_MAX_INPUT_SPEED) {
return CAR0107_MAX_INPUT_SPEED;
}
/* App 每帧分开发 mode1/mode2 两条 MQTT,必须合并后再决策,避免后退时 mode1 串扰成前进 */
static int s_phone_mode1_val = 0;
static int s_phone_mode2_val = 0;
static int s_phone_last_mode = 0; /* 最近更新的通道,双通道同时有油时以它为准 */
static car0107_esc_ctx_t s_esc = {
.state = CAR0107_ESC_STOP,
.trans_phase = CAR0107_TRANS_BRAKE,
.current_us = CAR0107_ESC_US_NEUTRAL,
.target_us = CAR0107_ESC_US_NEUTRAL,
.cmd_dir = 0,
.pending_dir = 0,
.pending_gval = 0,
.brake_dir = 0,
.brake_ticks = 0,
.neutral_hold_ticks = 0,
};
static int car0107_clamp(int val, int lo, int hi)
{
if (val < lo) return lo;
if (val > hi) return hi;
return val;
}
static int car0107_input_to_gval(int val) {
static int car0107_clamp_input(int val)
{
return car0107_clamp(val, 0, CAR0107_PHONE_VAL_MAX);
}
static int car0107_input_to_gval(int val)
{
val = car0107_clamp_input(val);
if (val < CAR0107_ACTIVE_THRESHOLD) {
return 0;
......@@ -61,7 +124,8 @@ static int car0107_input_to_gval(int val) {
(CAR0107_MAX_INPUT_SPEED - CAR0107_ACTIVE_THRESHOLD);
}
static int car0107_input_to_target_speed(int val, int max_target) {
static int car0107_input_to_target_speed(int val, int max_target)
{
val = car0107_clamp_input(val);
if (val < CAR0107_ACTIVE_THRESHOLD) {
return 0;
......@@ -70,23 +134,494 @@ static int car0107_input_to_target_speed(int val, int max_target) {
(CAR0107_MAX_INPUT_SPEED - CAR0107_ACTIVE_THRESHOLD);
}
static void car0107_calculate_L_R(int angle) {
static int car0107_speed_to_gval(int speed)
{
if (speed <= 0) {
return 0;
}
int gval = 50 + (speed * 100) / 160;
return car0107_clamp(gval, 0, STEER_GVAL_MAX);
}
/* App 前后油门:53~200 线性映射脉宽;val=53 即有最低行走速度,200 封顶 */
static int car0107_phone_val_to_us(int dir, int raw_val)
{
int span;
int t;
if (raw_val < CAR0107_ESC_GVAL_MIN) {
return CAR0107_ESC_US_NEUTRAL;
}
raw_val = car0107_clamp(raw_val, CAR0107_ESC_GVAL_MIN, CAR0107_PHONE_VAL_MAX);
span = CAR0107_PHONE_VAL_MAX - CAR0107_ESC_GVAL_MIN;
if (span <= 0) {
return CAR0107_ESC_US_NEUTRAL;
}
t = raw_val - CAR0107_ESC_GVAL_MIN;
if (dir == CAR0107_ESC_DIR_FORWARD) {
return CAR0107_FWD_US_RUN_MIN +
t * (CAR0107_FWD_US_RUN_MAX - CAR0107_FWD_US_RUN_MIN) / span;
}
if (dir == CAR0107_ESC_DIR_BACK) {
return CAR0107_BACK_US_RUN_MIN -
t * (CAR0107_BACK_US_RUN_MIN - CAR0107_BACK_US_RUN_MAX) / span;
}
return CAR0107_ESC_US_NEUTRAL;
}
/* 方向盘内部 gval(约 53~150) */
static int car0107_steer_gval_to_us(int dir, int gval)
{
int span;
int t;
if (gval < CAR0107_ESC_GVAL_MIN) {
return CAR0107_ESC_US_NEUTRAL;
}
gval = car0107_clamp(gval, CAR0107_ESC_GVAL_MIN, STEER_GVAL_MAX);
span = STEER_GVAL_MAX - CAR0107_ESC_GVAL_MIN;
if (span <= 0) {
return CAR0107_ESC_US_NEUTRAL;
}
t = gval - CAR0107_ESC_GVAL_MIN;
if (dir == CAR0107_ESC_DIR_FORWARD) {
return CAR0107_FWD_US_RUN_MIN +
t * (CAR0107_FWD_US_RUN_MAX - CAR0107_FWD_US_RUN_MIN) / span;
}
if (dir == CAR0107_ESC_DIR_BACK) {
return CAR0107_BACK_US_RUN_MIN -
t * (CAR0107_BACK_US_RUN_MIN - CAR0107_BACK_US_RUN_MAX) / span;
}
return CAR0107_ESC_US_NEUTRAL;
}
static int car0107_throttle_to_us(int dir, int val)
{
if (s_control_source == CTRL_PHONE) {
return car0107_phone_val_to_us(dir, val);
}
return car0107_steer_gval_to_us(dir, val);
}
static int car0107_phone_clamp_val(int raw_val)
{
if (raw_val <= CAR0107_ESC_RELEASE_MAX) {
return 0;
}
return car0107_clamp(raw_val, CAR0107_ESC_GVAL_MIN, CAR0107_PHONE_VAL_MAX);
}
/*
* 仅前进松手/换后退时打 1000us 刹车。
* 正常后退最深约 1160us,与刹车 1000us 分开。
*/
static int car0107_forward_brake_us(void)
{
return CAR0107_ESC_US_MIN;
}
static int car0107_forward_brake_pwm(void)
{
return car0107_forward_brake_us() / 20;
}
static void car0107_esc_write_pwm(int pwm_val)
{
pwm_val = car0107_clamp(pwm_val, CAR0107_ESC_US_MIN / 20, CAR0107_ESC_US_MAX / 20);
pwmWrite(PWM_PIN_SPEED, pwm_val);
s_esc.current_us = pwm_val * 20;
}
static void car0107_esc_begin_forward_brake(int pending_dir, int pending_gval)
{
s_esc.state = CAR0107_ESC_TRANSITION;
s_esc.trans_phase = CAR0107_TRANS_BRAKE;
s_esc.pending_dir = pending_dir;
s_esc.pending_gval = pending_gval;
s_esc.brake_dir = CAR0107_ESC_DIR_BACK;
s_esc.brake_ticks = CAR0107_ESC_BRAKE_HOLD_TICKS;
s_esc.neutral_hold_ticks = 0;
s_esc.target_us = car0107_forward_brake_us();
s_esc.current_us = s_esc.target_us;
car0107_esc_write_pwm(car0107_forward_brake_pwm());
}
static void car0107_esc_begin_neutral_stop(void)
{
s_esc.cmd_dir = 0;
s_esc.pending_dir = 0;
s_esc.pending_gval = 0;
s_esc.brake_dir = 0;
s_esc.brake_ticks = 0;
s_esc.neutral_hold_ticks = 0;
s_esc.target_us = CAR0107_ESC_US_NEUTRAL;
s_esc.state = CAR0107_ESC_STOP;
}
static int car0107_esc_forward_motion_active(void)
{
if (s_esc.cmd_dir != CAR0107_ESC_DIR_FORWARD) {
return 0;
}
if (s_esc.state == CAR0107_ESC_FORWARD) {
return 1;
}
if (s_esc.state == CAR0107_ESC_TRANSITION &&
s_esc.trans_phase == CAR0107_TRANS_RAMP_RUN) {
return 1;
}
if (s_esc.state == CAR0107_ESC_TRANSITION &&
s_esc.trans_phase == CAR0107_TRANS_BRAKE &&
s_esc.brake_dir == CAR0107_ESC_DIR_BACK) {
return 1;
}
return 0;
}
static int car0107_esc_back_motion_active(void)
{
if (s_esc.cmd_dir != CAR0107_ESC_DIR_BACK) {
return 0;
}
if (s_esc.state == CAR0107_ESC_REVERSE) {
return 1;
}
if (s_esc.state == CAR0107_ESC_TRANSITION &&
s_esc.trans_phase == CAR0107_TRANS_RAMP_RUN) {
return 1;
}
return 0;
}
static void car0107_notify_control_locked(void)
{
s_last_control_ms = get_current_time_millis();
}
void car0107_notify_control_activity(void)
{
pthread_mutex_lock(&s_car0107_hw_mutex);
car0107_notify_control_locked();
pthread_mutex_unlock(&s_car0107_hw_mutex);
}
long long car0107_last_control_ms(void)
{
long long ts;
pthread_mutex_lock(&s_car0107_hw_mutex);
ts = s_last_control_ms;
pthread_mutex_unlock(&s_car0107_hw_mutex);
return ts;
}
int car0107_esc_is_active(void)
{
int active;
pthread_mutex_lock(&s_car0107_hw_mutex);
if (s_esc.state == CAR0107_ESC_TRANSITION) {
active = (s_esc.trans_phase == CAR0107_TRANS_BRAKE);
} else if (s_esc.state == CAR0107_ESC_STOP) {
active = 0;
} else {
active = (s_esc.current_us < CAR0107_ESC_DEADZONE_LO ||
s_esc.current_us > CAR0107_ESC_DEADZONE_HI);
}
pthread_mutex_unlock(&s_car0107_hw_mutex);
return active;
}
static void car0107_esc_output_us(int us, int snap_deadzone)
{
int pwm_val;
us = car0107_clamp(us, CAR0107_ESC_US_MIN, CAR0107_ESC_US_MAX);
if (snap_deadzone &&
us >= CAR0107_ESC_DEADZONE_LO && us <= CAR0107_ESC_DEADZONE_HI) {
us = CAR0107_ESC_US_NEUTRAL;
}
pwm_val = us / 20;
car0107_esc_write_pwm(pwm_val);
}
static void car0107_esc_write_us(int us)
{
car0107_esc_output_us(us, 1);
}
static void car0107_esc_start_ramp(int dir, int throttle_val)
{
int from_stop = (s_esc.state == CAR0107_ESC_STOP);
s_esc.cmd_dir = dir;
s_esc.pending_dir = 0;
s_esc.pending_gval = 0;
s_esc.brake_dir = 0;
s_esc.target_us = car0107_throttle_to_us(dir, throttle_val);
s_esc.state = CAR0107_ESC_TRANSITION;
s_esc.trans_phase = CAR0107_TRANS_RAMP_RUN;
if (from_stop) {
/* val>52 首帧立即到对应脉宽(53=最低行走速度) */
s_esc.current_us = s_esc.target_us;
}
}
static void car0107_esc_begin_transition(int pending_dir, int pending_gval)
{
if (s_esc.state == CAR0107_ESC_TRANSITION &&
s_esc.pending_dir == pending_dir &&
s_esc.trans_phase != CAR0107_TRANS_BRAKE) {
s_esc.pending_gval = pending_gval;
return;
}
/* 前进中换后退:先 1000us 刹车 */
car0107_esc_begin_forward_brake(pending_dir, pending_gval);
}
static void car0107_esc_request_stop(void)
{
if (car0107_esc_forward_motion_active()) {
car0107_esc_begin_forward_brake(0, 0);
return;
}
if (car0107_esc_back_motion_active()) {
car0107_esc_begin_neutral_stop();
return;
}
if (s_esc.state == CAR0107_ESC_TRANSITION &&
s_esc.trans_phase == CAR0107_TRANS_BRAKE) {
return;
}
if (s_esc.state == CAR0107_ESC_TRANSITION &&
s_esc.trans_phase == CAR0107_TRANS_NEUTRAL_HOLD) {
s_esc.pending_dir = 0;
s_esc.pending_gval = 0;
return;
}
car0107_esc_begin_neutral_stop();
}
static void car0107_esc_request(int dir, int gval)
{
if (dir == 0 || gval < CAR0107_ESC_GVAL_MIN) {
car0107_esc_request_stop();
return;
}
if (s_esc.state == CAR0107_ESC_STOP) {
car0107_esc_start_ramp(dir, gval);
return;
}
if (s_esc.state == CAR0107_ESC_TRANSITION) {
if (s_esc.trans_phase == CAR0107_TRANS_BRAKE ||
s_esc.trans_phase == CAR0107_TRANS_NEUTRAL_HOLD) {
s_esc.pending_dir = dir;
s_esc.pending_gval = gval;
return;
}
if (s_esc.trans_phase == CAR0107_TRANS_RAMP_RUN) {
s_esc.target_us = car0107_throttle_to_us(s_esc.cmd_dir, gval);
return;
}
return;
}
/* 后退切前进:不刹车,直接爬升 */
if (s_esc.cmd_dir == CAR0107_ESC_DIR_BACK && dir == CAR0107_ESC_DIR_FORWARD) {
car0107_esc_start_ramp(dir, gval);
return;
}
/* 前进切后退:先 1000us 刹车 */
if (s_esc.cmd_dir == CAR0107_ESC_DIR_FORWARD && dir == CAR0107_ESC_DIR_BACK) {
car0107_esc_begin_transition(dir, gval);
return;
}
s_esc.cmd_dir = dir;
s_esc.pending_dir = 0;
s_esc.pending_gval = 0;
s_esc.target_us = car0107_throttle_to_us(dir, gval);
s_esc.state = (dir == CAR0107_ESC_DIR_FORWARD) ? CAR0107_ESC_FORWARD : CAR0107_ESC_REVERSE;
}
static int car0107_esc_ramp_step_for(int current, int target)
{
(void)current;
(void)target;
return CAR0107_ESC_RAMP_STEP_RUN;
}
static void car0107_esc_apply_phone(int dir, int gval)
{
if (gval < CAR0107_ESC_GVAL_MIN) {
return;
}
car0107_esc_request(dir, gval);
/* 不在此处瞬间写满 PWM;刹车/回中/慢爬均由 esc_tick 输出 */
}
static void car0107_phone_apply_merged(void)
{
int fwd = s_phone_mode1_val;
int back = s_phone_mode2_val;
int fwd_val = car0107_phone_clamp_val(fwd);
int back_val = car0107_phone_clamp_val(back);
/* mode1/mode2 收到 val=0 或 <=52 都表示停下;以最近更新的通道为准 */
if (s_phone_last_mode == 2) {
if (back_val > 0) {
car0107_esc_apply_phone(CAR0107_ESC_DIR_BACK, back_val);
} else {
car0107_esc_request_stop();
}
return;
}
if (s_phone_last_mode == 1) {
if (fwd_val > 0) {
car0107_esc_apply_phone(CAR0107_ESC_DIR_FORWARD, fwd_val);
} else {
car0107_esc_request_stop();
}
return;
}
if (fwd_val == 0 && back_val == 0) {
car0107_esc_request_stop();
}
}
static int car0107_esc_ramp_toward(int current, int target, int step)
{
if (current < target) {
current += step;
if (current > target) {
current = target;
}
} else if (current > target) {
current -= step;
if (current < target) {
current = target;
}
}
return current;
}
static void car0107_esc_tick(void)
{
switch (s_esc.state) {
case CAR0107_ESC_STOP:
s_esc.current_us = car0107_esc_ramp_toward(s_esc.current_us,
CAR0107_ESC_US_NEUTRAL,
CAR0107_ESC_RAMP_STEP_NEUTRAL);
break;
case CAR0107_ESC_FORWARD:
case CAR0107_ESC_REVERSE:
s_esc.current_us = car0107_esc_ramp_toward(
s_esc.current_us,
s_esc.target_us,
car0107_esc_ramp_step_for(s_esc.current_us, s_esc.target_us));
break;
case CAR0107_ESC_TRANSITION:
if (s_esc.trans_phase == CAR0107_TRANS_BRAKE) {
car0107_esc_write_pwm(car0107_forward_brake_pwm());
if (s_esc.brake_ticks > 0) {
s_esc.brake_ticks--;
}
if (s_esc.brake_ticks <= 0) {
s_esc.trans_phase = CAR0107_TRANS_NEUTRAL_HOLD;
s_esc.neutral_hold_ticks = CAR0107_ESC_NEUTRAL_HOLD_TICKS;
s_esc.current_us = CAR0107_ESC_US_NEUTRAL;
s_esc.target_us = CAR0107_ESC_US_NEUTRAL;
}
} else if (s_esc.trans_phase == CAR0107_TRANS_NEUTRAL_HOLD) {
s_esc.current_us = CAR0107_ESC_US_NEUTRAL;
if (s_esc.neutral_hold_ticks > 0) {
s_esc.neutral_hold_ticks--;
}
if (s_esc.neutral_hold_ticks <= 0) {
if (s_esc.pending_dir == 0) {
s_esc.state = CAR0107_ESC_STOP;
s_esc.cmd_dir = 0;
s_esc.target_us = CAR0107_ESC_US_NEUTRAL;
s_esc.trans_phase = CAR0107_TRANS_BRAKE;
} else {
s_esc.cmd_dir = s_esc.pending_dir;
s_esc.target_us = car0107_throttle_to_us(s_esc.pending_dir,
s_esc.pending_gval);
s_esc.current_us = CAR0107_ESC_US_NEUTRAL;
s_esc.trans_phase = CAR0107_TRANS_RAMP_RUN;
}
s_esc.pending_dir = 0;
s_esc.pending_gval = 0;
}
} else if (s_esc.trans_phase == CAR0107_TRANS_RAMP_RUN) {
s_esc.current_us = car0107_esc_ramp_toward(s_esc.current_us,
s_esc.target_us,
CAR0107_ESC_RAMP_STEP_RUN);
if (s_esc.current_us == s_esc.target_us) {
s_esc.state = (s_esc.cmd_dir == CAR0107_ESC_DIR_FORWARD) ?
CAR0107_ESC_FORWARD : CAR0107_ESC_REVERSE;
s_esc.trans_phase = CAR0107_TRANS_BRAKE;
}
}
break;
default:
break;
}
if (s_esc.state != CAR0107_ESC_TRANSITION ||
s_esc.trans_phase != CAR0107_TRANS_BRAKE) {
int snap = (s_esc.state == CAR0107_ESC_STOP);
car0107_esc_output_us(s_esc.current_us, snap);
}
}
static void car0107_calculate_L_R(int angle)
{
if (angle < 0) {
angle = 0;
} else if (angle > 180) {
angle = 180;
}
float pulse_width=( angle / 180.0) *2 + 0.5;
float pulse_width = (angle / 180.0) * 2 + 0.5;
float period = 1000.0 / 50;
int val = (int)((pulse_width / period) * 1000);
pwmWrite(PWM_PIN_CHANGE,val);
int val = (int)((pulse_width / period) * 1000);
pwmWrite(PWM_PIN_CHANGE, val);
}
void car0107_steering_set_angle_sync(int angle)
{
pthread_mutex_lock(&s_car0107_hw_mutex);
s_current_angle = angle;
s_target_angle = angle;
car0107_calculate_L_R(angle);
pthread_mutex_unlock(&s_car0107_hw_mutex);
}
static void car0107_reset_steering_state(void) {
static void car0107_reset_steering_state(void)
{
s_control_source = CTRL_NONE;
s_target_speed = 0;
s_current_speed = 0;
s_speed_mode = 0;
s_phone_mode1_val = 0;
s_phone_mode2_val = 0;
s_phone_last_mode = 0;
s_target_angle = 90;
s_current_angle = 90;
s_steering_mode = 0;
......@@ -95,147 +630,77 @@ static void car0107_reset_steering_state(void) {
s_steering_timeout = 0;
}
static void car0107_reset_phone_state(void) {
s_phone_speed_mode = 0;
s_phone_current_gval = 0;
s_phone_pending_mode = 0;
s_phone_pending_gval = 0;
static void car0107_reset_esc_state(void)
{
s_esc.state = CAR0107_ESC_STOP;
s_esc.trans_phase = CAR0107_TRANS_BRAKE;
s_esc.target_us = CAR0107_ESC_US_NEUTRAL;
s_esc.cmd_dir = 0;
s_esc.pending_dir = 0;
s_esc.pending_gval = 0;
s_esc.brake_dir = 0;
s_esc.brake_ticks = 0;
s_esc.neutral_hold_ticks = 0;
}
static void car0107_apply_stop(void) {
pwmWrite(PWM_PIN_SPEED, 75);
static void car0107_apply_stop(void)
{
car0107_reset_esc_state();
car0107_esc_write_us(CAR0107_ESC_US_NEUTRAL);
car0107_calculate_L_R(90);
}
static void car0107_stop_speed_now(void) {
static void car0107_stop_speed_now(void)
{
s_target_speed = 0;
s_current_speed = 0;
s_speed_mode = 0;
s_speed_timeout = 0;
car0107_reset_phone_state();
car0107_apply_stop();
}
static int car0107_speed_to_gval(int speed) {
if (speed <= 0) {
return 0;
}
int gval = 50 + (speed * (STEER_GVAL_MAX - 50)) / CAR0107_MAX_TARGET_BACK;
if (gval > STEER_GVAL_MAX) {
gval = STEER_GVAL_MAX;
}
return gval;
car0107_esc_request(0, 0);
}
/*车中间值,停止舵机中间函数*/
void car0107_middle_pwm() {
void car0107_middle_pwm(void)
{
pthread_mutex_lock(&s_car0107_hw_mutex);
car0107_reset_steering_state();
car0107_reset_phone_state();
car0107_apply_stop();
/* 异常停止不算用户控制,不刷新 idle 保活计时 */
pthread_mutex_unlock(&s_car0107_hw_mutex);
}
static void car0107_mode_1_flont(int gval) {
int a=0,b=1;
if (gval < 50) {
pwmWrite(PWM_PIN_SPEED, 75);
} else if (gval <= 60) {
pwmWrite(PWM_PIN_SPEED, 71-b+a);
}else if (gval <= 70) {
pwmWrite(PWM_PIN_SPEED, 70-b+a);
} else if(gval >70){
int back_speed = 70 - (gval-70)/10-b+a;
if (back_speed < CAR0107_PWM_FORWARD_MIN) {
back_speed = CAR0107_PWM_FORWARD_MIN;
}
pwmWrite(PWM_PIN_SPEED, back_speed);
}
}
static void car0107_mode_2_back(int gval) {
int b=0,a=1;
if (gval < 50) {
pwmWrite(PWM_PIN_SPEED, 75);
}
else if (gval <= 60) {
pwmWrite(PWM_PIN_SPEED, 79+b-a);
}else if (gval <= 70) {
pwmWrite(PWM_PIN_SPEED, 80+b-a);
}else if(gval >70){
int flont_speed = 80+(gval-70)/10+b-a;
if (flont_speed > CAR0107_PWM_BACK_MAX) {
flont_speed = CAR0107_PWM_BACK_MAX;
}
pwmWrite(PWM_PIN_SPEED, flont_speed);
}
}
static void car0107_mode_3_left(int gval) {
int b=15;
if(gval<45){
car0107_calculate_L_R(90);
}else if(gval<70){
car0107_calculate_L_R(50+gval+b);
}else if(gval>=70){
car0107_calculate_L_R(135);
}
}
static void car0107_mode_4_right(int gval) {
int b=10;
if(gval<45){
car0107_calculate_L_R(90);
}else if(gval<70){
car0107_calculate_L_R(130-gval-b);
}else if(gval>=70){
car0107_calculate_L_R(35);
}
}
static void car0107_phone_apply_speed(void) {
if (s_phone_speed_mode == 1) {
car0107_mode_2_back(s_phone_current_gval);
} else if (s_phone_speed_mode == 2) {
car0107_mode_1_flont(s_phone_current_gval);
static void car0107_mode_3_left(int gval)
{
int b = 15;
if (gval < 45) {
car0107_calculate_L_R(90);
} else if (gval < 70) {
car0107_calculate_L_R(50 + gval + b);
} else {
pwmWrite(PWM_PIN_SPEED, 75);
car0107_calculate_L_R(135);
}
}
static void car0107_set_phone_speed_command(int new_mode, int new_gval) {
if (new_gval < CAR0107_ACTIVE_THRESHOLD) {
car0107_reset_phone_state();
car0107_apply_stop();
return;
}
if (s_phone_current_gval > CAR0107_ACTIVE_THRESHOLD &&
s_phone_speed_mode != 0 && new_mode != s_phone_speed_mode) {
s_phone_pending_mode = new_mode;
s_phone_pending_gval = new_gval;
s_phone_speed_mode = 0;
pwmWrite(PWM_PIN_SPEED, 75);
return;
}
if (s_phone_speed_mode == 0 && s_phone_pending_mode != 0) {
s_phone_pending_mode = new_mode;
s_phone_pending_gval = new_gval;
return;
static void car0107_mode_4_right(int gval)
{
int b = 10;
if (gval < 45) {
car0107_calculate_L_R(90);
} else if (gval < 70) {
car0107_calculate_L_R(130 - gval - b);
} else {
car0107_calculate_L_R(35);
}
s_phone_pending_mode = 0;
s_phone_pending_gval = 0;
s_phone_speed_mode = new_mode;
s_phone_current_gval = new_gval;
car0107_phone_apply_speed();
}
void car0107_control_change(int *buf) {
void car0107_control_change(int *buf)
{
int mode = buf[1];
int raw_val = car0107_clamp_input(buf[2]);
pthread_mutex_lock(&g_exit_count_mutex);
g_devcontrol_exit_count = 0;
pthread_mutex_unlock(&g_exit_count_mutex);
pthread_mutex_lock(&s_car0107_hw_mutex);
s_control_source = CTRL_PHONE;
s_target_speed = 0;
......@@ -243,65 +708,40 @@ void car0107_control_change(int *buf) {
s_speed_mode = 0;
s_speed_timeout = 0;
s_steering_timeout = 0;
car0107_notify_control_locked();
if ((mode == 1 || mode == 2) && raw_val == 0) {
car0107_stop_speed_now();
if (mode == 1) {
s_phone_last_mode = 1;
s_phone_mode1_val = raw_val;
car0107_phone_apply_merged();
car0107_esc_tick();
pthread_mutex_unlock(&s_car0107_hw_mutex);
return;
}
int val = car0107_input_to_gval(raw_val);
switch(mode){
case 1:
car0107_set_phone_speed_command(1, val);
break;
case 2:
car0107_set_phone_speed_command(2, val);
break;
case 3:
car0107_mode_3_left(val);
break;
case 4:
car0107_mode_4_right(val);
break;
default:
break;
}
pthread_mutex_unlock(&s_car0107_hw_mutex);
}
static void car0107_phone_smooth_process(void) {
if (s_control_source != CTRL_PHONE) {
return;
}
if (s_phone_speed_mode != 0 || s_phone_pending_mode == 0) {
return;
}
if (s_phone_current_gval < CAR0107_ACTIVE_THRESHOLD) {
s_phone_speed_mode = s_phone_pending_mode;
s_phone_current_gval = s_phone_pending_gval;
s_phone_pending_mode = 0;
s_phone_pending_gval = 0;
car0107_phone_apply_speed();
if (mode == 2) {
s_phone_last_mode = 2;
s_phone_mode2_val = raw_val;
car0107_phone_apply_merged();
car0107_esc_tick();
pthread_mutex_unlock(&s_car0107_hw_mutex);
return;
}
s_phone_current_gval -= BRAKE_STEP;
if (s_phone_current_gval < CAR0107_ACTIVE_THRESHOLD) {
s_phone_current_gval = 0;
pwmWrite(PWM_PIN_SPEED, 75);
s_phone_speed_mode = s_phone_pending_mode;
s_phone_current_gval = s_phone_pending_gval;
s_phone_pending_mode = 0;
s_phone_pending_gval = 0;
car0107_phone_apply_speed();
} else {
pwmWrite(PWM_PIN_SPEED, 75);
switch (mode) {
case 3:
car0107_mode_3_left(raw_val);
break;
case 4:
car0107_mode_4_right(raw_val);
break;
default:
break;
}
pthread_mutex_unlock(&s_car0107_hw_mutex);
}
static void car0107_speed_smooth_process(void) {
static void car0107_speed_smooth_process(void)
{
if (s_control_source != CTRL_STEERING) {
return;
}
......@@ -309,7 +749,7 @@ static void car0107_speed_smooth_process(void) {
s_speed_timeout--;
if (s_speed_timeout <= 0) {
s_current_speed = 0;
car0107_apply_stop();
car0107_esc_request(0, 0);
if (s_steering_timeout <= 0) {
s_control_source = CTRL_NONE;
}
......@@ -317,35 +757,37 @@ static void car0107_speed_smooth_process(void) {
}
int limited_target = s_target_speed;
if (s_speed_mode == 2) {
if (s_speed_mode == 1) {
limited_target = (limited_target * REV_TARGET_SCALE_NUM) / REV_TARGET_SCALE_DEN;
}
if (s_speed_mode == 0) {
if (s_current_speed > 0) {
s_current_speed -= DECEL_STEP;
s_current_speed -= 6;
if (s_current_speed < 0) {
s_current_speed = 0;
}
}
car0107_esc_request(0, 0);
} else if (s_speed_mode == 1 || s_speed_mode == 2) {
if (s_current_speed < limited_target) {
s_current_speed += ACCEL_STEP;
s_current_speed += 5;
} else if (s_current_speed > limited_target) {
s_current_speed -= DECEL_STEP;
s_current_speed -= 6;
}
}
if (s_current_speed <= 0 || s_speed_mode == 0) {
car0107_apply_stop();
} else if (s_speed_mode == 1) {
car0107_mode_2_back(car0107_speed_to_gval(s_current_speed));
} else if (s_speed_mode == 2) {
car0107_mode_1_flont(car0107_speed_to_gval(s_current_speed));
if (s_current_speed <= 0) {
car0107_esc_request(0, 0);
} else {
int gval = car0107_speed_to_gval(s_current_speed);
int dir = (s_speed_mode == 1) ? CAR0107_ESC_DIR_BACK : CAR0107_ESC_DIR_FORWARD;
car0107_esc_request(dir, gval);
}
}
}
static void car0107_steering_angle_smooth_process(void) {
static void car0107_steering_angle_smooth_process(void)
{
if (s_control_source != CTRL_STEERING) {
return;
}
......@@ -398,23 +840,25 @@ static void car0107_steering_angle_smooth_process(void) {
car0107_calculate_L_R(s_current_angle);
}
void car0107_steering_smooth_all(void) {
void car0107_steering_smooth_all(void)
{
pthread_mutex_lock(&s_car0107_hw_mutex);
car0107_phone_smooth_process();
car0107_esc_tick();
car0107_speed_smooth_process();
car0107_steering_angle_smooth_process();
pthread_mutex_unlock(&s_car0107_hw_mutex);
}
void car0107_control_steering_change_process(int *buf) {
void car0107_control_steering_change_process(int *buf)
{
int mode = buf[1];
int val = buf[2];
pthread_mutex_lock(&s_car0107_hw_mutex);
s_control_source = CTRL_STEERING;
car0107_reset_phone_state();
s_steering_timeout = 200;
s_speed_timeout = 200;
car0107_notify_control_locked();
if (mode == 1) {
val = car0107_clamp_input(val);
......
......@@ -6,4 +6,9 @@ void car0107_middle_pwm(void);
void car0107_control_steering_change_process(int *buf);
void car0107_steering_smooth_all(void);
void car0107_notify_control_activity(void);
long long car0107_last_control_ms(void);
int car0107_esc_is_active(void);
void car0107_steering_set_angle_sync(int angle);
#endif
#include "car0107_idle.h"
#include "car0107_control.h"
#include "devcontrol_common.h"
#include "modules_common.h"
#include "gpio_common.h"
#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 = {
.mutex = PTHREAD_MUTEX_INITIALIZER,
.pool = NULL,
.shutdown = false,
.pool_ready = false,
.next_left = 1,
};
static void car0107_idle_lock(void)
{
pthread_mutex_lock(&s_idle_ctx.mutex);
}
static void car0107_idle_unlock(void)
{
pthread_mutex_unlock(&s_idle_ctx.mutex);
}
static bool car0107_idle_shutdown_get(void)
{
bool shutdown;
car0107_idle_lock();
shutdown = s_idle_ctx.shutdown;
car0107_idle_unlock();
return shutdown;
}
static int car0107_idle_pool_init(void)
{
car0107_idle_lock();
if (s_idle_ctx.pool_ready) {
car0107_idle_unlock();
return 0;
}
s_idle_ctx.pool = thread_pool_init(1, 1);
if (!s_idle_ctx.pool) {
car0107_idle_unlock();
my_zlog_error("car0107 idle thread pool init failed");
return -1;
}
s_idle_ctx.pool_ready = true;
car0107_idle_unlock();
my_zlog_info("car0107 idle monitor thread pool opened");
return 0;
}
static void car0107_idle_nudge_steering(int turn_left)
{
int angle = turn_left ? CAR0107_IDLE_STEER_LEFT : CAR0107_IDLE_STEER_RIGHT;
/* 刷新电调中位,避免长时间无 PWM 后接收机休眠 */
pwmWrite(PWM_PIN_SPEED, 75);
car0107_steering_set_angle_sync(angle);
delay_ms(CAR0107_IDLE_STEER_HOLD_MS);
car0107_steering_set_angle_sync(90);
pwmWrite(PWM_PIN_SPEED, 75);
}
static void car0107_idle_monitor_task(void *arg)
{
(void)arg;
my_zlog_info("car0107 idle monitor task start");
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) {
break;
}
now = get_current_time_millis();
last = car0107_last_control_ms();
if (last <= 0 || (now - last) < CAR0107_IDLE_TIMEOUT_MS) {
continue;
}
if (car0107_esc_is_active()) {
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);
car0107_notify_control_activity();
}
my_zlog_info("car0107 idle monitor task exit");
}
void car0107_idle_startup(int device_id)
{
if (device_id != DEVICE_CAR0107) {
return;
}
if (car0107_idle_pool_init() != 0) {
return;
}
car0107_idle_lock();
if (thread_pool_add_task(s_idle_ctx.pool, car0107_idle_monitor_task, NULL) != 0) {
my_zlog_error("car0107 idle monitor task submit failed");
}
car0107_idle_unlock();
car0107_notify_control_activity();
my_zlog_info("car0107 idle monitor enabled: steer nudge every %dmin when idle",
CAR0107_IDLE_TIMEOUT_MS / 60000);
}
void car0107_idle_thread_close(void)
{
car0107_idle_lock();
s_idle_ctx.shutdown = true;
car0107_idle_unlock();
if (s_idle_ctx.pool) {
thread_pool_destroy(s_idle_ctx.pool);
s_idle_ctx.pool = NULL;
s_idle_ctx.pool_ready = false;
}
my_zlog_info("car0107 idle monitor thread pool closed");
}
#ifndef CAR0107_IDLE_H__
#define CAR0107_IDLE_H__
void car0107_idle_startup(int device_id);
void car0107_idle_thread_close(void);
#endif
......@@ -136,7 +136,7 @@ static const device_abnormal_close_t s_devcontrol_config[]= {
{
.device_id = DEVICE_CAR0107,
.device_abnormal_stop = car0107_middle_pwm,
.device_close = NULL,
.device_close = car0107_idle_thread_close,
.gpio_pin_pulled=pin_all_default,
.gpio_pwm_pulled=pwm_all_default
},
......
......@@ -10,6 +10,7 @@
#include "car0105_control.h"
#include "car0106_control.h"
#include "car0107_control.h"
#include "car0107_idle.h"
#include "ptz_common.h"
#include "tank0202_control.h"
#include "tank0203_control.h"
......
......@@ -2,6 +2,7 @@
#include "devcontrol_common.h"
#include "common.h"
#include "gpio_init.h"
#include "car0107_idle.h"
int g_device_delay_back_count =0;//设备计时,比如坦克打击倒退逻辑
......@@ -267,6 +268,9 @@ void device_init(int device_id) {
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);
......
......@@ -374,9 +374,9 @@ int audio_speaker_init() {
int audio_init(){
delay_s(10);
delay_s(5);
audio_wheat_init();
delay_s(3);
delay_s(1);
audio_speaker_init();
return 0;
......@@ -410,7 +410,7 @@ static const char *s_yundea_block =
*/
int audio_config_init() {
delay_s(20);//延迟30s
delay_s(12);
FILE *fp = fopen(CONFIG_FILE, "r");
if (!fp) {
......
......@@ -11,6 +11,8 @@
#include "go_deploy.h"
#include "tailscale_deploy.h"
#include "runtime_deps.h"
#include "wifi_config_agent.h"
#include "stream_debug_agent.h"
#include "webrtcpush_run.h"
#endif
\ No newline at end of file
......@@ -21,6 +21,8 @@ file(GLOB_RECURSE MODULES_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/tailscale_deploy/*.c
${CMAKE_CURRENT_SOURCE_DIR}/webrtcpush/*.c
${CMAKE_CURRENT_SOURCE_DIR}/runtime_deps/*.c
${CMAKE_CURRENT_SOURCE_DIR}/wifi_config/*.c
${CMAKE_CURRENT_SOURCE_DIR}/stream_debug/*.c
)
set(MODULES_SOURCES
......@@ -40,6 +42,8 @@ set(MODULES_INCLUDE_DIRS
${CMAKE_CURRENT_SOURCE_DIR}/tailscale_deploy
${CMAKE_CURRENT_SOURCE_DIR}/webrtcpush
${CMAKE_CURRENT_SOURCE_DIR}/runtime_deps
${CMAKE_CURRENT_SOURCE_DIR}/wifi_config
${CMAKE_CURRENT_SOURCE_DIR}/stream_debug
${WEBRTCPUSH_GST_INCLUDE_DIRS}
${WEBRTCPUSH_JSON_INCLUDE_DIRS}
${WEBRTCPUSH_SOUP_INCLUDE_DIRS}
......
......@@ -4,8 +4,12 @@
#include "device_identity.h"
#include "http_config_mqtt.h"
#include "pthrpoll.h"
#include "delay.h"
#include <dirent.h>
static char s_webcam[2048];//存放启动火狐网站命令
static char s_active_stream_url[768]; /* 最近一次启动 chromium 的完整推流网址 */
static int s_open_cam_enter_count = 0; // 记录 opencamsh 进入次数
static int s_open_cam_fail_streak = 0; // 连续启动失败次数
static int s_refresh_cam_enter_count = 0; // 记录 refresh_cam 进入次数
......@@ -17,37 +21,54 @@ static pthread_mutex_t s_refresh_stat_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t s_refresh_run_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t s_refresh_cool_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t s_refresh_pool_mutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t s_browser_ops_mutex = PTHREAD_MUTEX_INITIALIZER;
static time_t s_refresh_next_allowed = 0; /* time(NULL) 之前不允许再提交刷新 */
static time_t s_browser_retry_after = 0;
static volatile int s_browser_external_switch = 0;
static ThreadPool_t *s_refresh_pool = NULL;
#define BROWSER_AUTO_RESTART_FAIL_SEC 5
static int browser_launch_chromium(const char *launch_url);
static int browser_chromium_found_cb(int pid, const char *cmdline, void *ctx);
int opencamsh() {
char urls[512];
int cmd_ret = 0;
int rc;
s_open_cam_enter_count++;
my_zlog_info("opencamsh enter count:%d", s_open_cam_enter_count);
snprintf(urls, sizeof(urls), "%s%s", g_mqtt_cam_config_t->videourl, mqtt_topic_pure_number());
//setenv("DISPLAY", ":0", 1);//设置环境变量https://jywy.yd-ss.com?dev=controcar0004 --new-window sudo
snprintf(s_webcam, sizeof(s_webcam), "su - orangepi -c \"chromium-browser --use-fake-ui-for-media-stream %s\"", urls);
cmd_ret = system(s_webcam);
if (cmd_ret != 0) {
pthread_mutex_lock(&s_browser_ops_mutex);
if (s_browser_external_switch) {
pthread_mutex_unlock(&s_browser_ops_mutex);
my_zlog_debug("opencamsh skipped: external stream switch in progress");
return -1;
}
rc = browser_launch_chromium(urls);
pthread_mutex_unlock(&s_browser_ops_mutex);
if (rc != 0) {
s_open_cam_fail_streak++;
s_browser_retry_after = time(NULL) + BROWSER_AUTO_RESTART_FAIL_SEC;
if (s_open_cam_fail_streak >= 10 && (s_open_cam_fail_streak % 10) == 0) {
my_zlog_error("chromium start fail, ret:%d, continuous fail:%d, enter count:%d",
cmd_ret, s_open_cam_fail_streak, s_open_cam_enter_count);
my_zlog_error("chromium start fail, continuous fail:%d, enter count:%d",
s_open_cam_fail_streak, s_open_cam_enter_count);
} else {
my_zlog_warn("chromium start fail, ret:%d, continuous fail:%d, enter count:%d",
cmd_ret, s_open_cam_fail_streak, s_open_cam_enter_count);
my_zlog_warn("chromium start fail, continuous fail:%d, enter count:%d",
s_open_cam_fail_streak, s_open_cam_enter_count);
}
} else {
if (s_open_cam_fail_streak > 0) {
my_zlog_info("chromium start recovered, previous continuous fail:%d", s_open_cam_fail_streak);
}
s_open_cam_fail_streak = 0;
return -1;
}
if (s_open_cam_fail_streak > 0) {
my_zlog_info("chromium start recovered, previous continuous fail:%d", s_open_cam_fail_streak);
}
my_zlog_debug("%s",s_webcam);
s_open_cam_fail_streak = 0;
s_browser_retry_after = 0;
my_zlog_debug("open cam");
return 0;
}
......@@ -197,13 +218,437 @@ void refresh_cam(void)
REFRESH_CAM_COOLDOWN_SEC);
}
// 检查Chromium进程是否已运行
bool is_browser_running() {
FILE *fp = popen("pgrep chromium", "r");
if (fp == NULL) return false;
// 检查Chromium进程是否已运行(直接扫 /proc,避免 UDP 线程 popen 死锁)
static int browser_read_pid_cmdline(int pid, char *cmdline, size_t cmdline_size)
{
char path[64];
FILE *cf;
size_t n;
size_t i;
snprintf(path, sizeof(path), "/proc/%d/cmdline", pid);
cf = fopen(path, "r");
if (!cf) {
return -1;
}
n = fread(cmdline, 1, cmdline_size - 1, cf);
fclose(cf);
if (n == 0) {
return -1;
}
cmdline[n] = '\0';
for (i = 0; i < n; i++) {
if (cmdline[i] == '\0') {
cmdline[i] = ' ';
}
}
return 0;
}
char buf[16];
bool running = (fgets(buf, sizeof(buf), fp) != NULL);
pclose(fp);
static int browser_is_chromium_cmdline(const char *cmdline)
{
if (!cmdline || !cmdline[0]) {
return 0;
}
if (strstr(cmdline, "chromium-browser") != NULL) {
return 1;
}
if (strstr(cmdline, "/chromium-browser") != NULL) {
return 1;
}
return 0;
}
/* 仅认推流主进程,排除 zygote/gpu 等子进程 */
static int browser_is_stream_chromium_cmdline(const char *cmdline)
{
if (!browser_is_chromium_cmdline(cmdline)) {
return 0;
}
if (strstr(cmdline, "--type=") != NULL) {
return 0;
}
if (strstr(cmdline, "--use-fake-ui-for-media-stream") != NULL) {
return 1;
}
if (strstr(cmdline, "https://") != NULL || strstr(cmdline, "http://") != NULL) {
return 1;
}
return 0;
}
void browser_begin_external_switch(void)
{
pthread_mutex_lock(&s_browser_ops_mutex);
s_browser_external_switch = 1;
pthread_mutex_unlock(&s_browser_ops_mutex);
}
void browser_end_external_switch(void)
{
pthread_mutex_lock(&s_browser_ops_mutex);
s_browser_external_switch = 0;
pthread_mutex_unlock(&s_browser_ops_mutex);
}
int browser_auto_restart_allowed(void)
{
time_t now = time(NULL);
pthread_mutex_lock(&s_browser_ops_mutex);
if (s_browser_external_switch) {
pthread_mutex_unlock(&s_browser_ops_mutex);
return 0;
}
if (s_browser_retry_after > 0 && now < s_browser_retry_after) {
pthread_mutex_unlock(&s_browser_ops_mutex);
return 0;
}
pthread_mutex_unlock(&s_browser_ops_mutex);
return 1;
}
typedef int (*browser_chromium_cb)(int pid, const char *cmdline, void *ctx);
static int browser_chromium_url_cb(int pid, const char *cmdline, void *ctx);
static int browser_foreach_chromium(browser_chromium_cb cb, void *ctx)
{
DIR *dir;
struct dirent *ent;
if (!cb) {
return -1;
}
dir = opendir("/proc");
if (!dir) {
return -1;
}
while ((ent = readdir(dir)) != NULL) {
char cmdline[2048];
int pid;
int rc;
if (ent->d_name[0] < '1' || ent->d_name[0] > '9') {
continue;
}
pid = atoi(ent->d_name);
if (pid <= 0) {
continue;
}
if (browser_read_pid_cmdline(pid, cmdline, sizeof(cmdline)) != 0) {
continue;
}
if (!browser_is_chromium_cmdline(cmdline)) {
continue;
}
if (cb == browser_chromium_found_cb && !browser_is_stream_chromium_cmdline(cmdline)) {
continue;
}
rc = cb(pid, cmdline, ctx);
if (rc != 0) {
closedir(dir);
return rc;
}
}
closedir(dir);
return 0;
}
static int browser_chromium_found_cb(int pid, const char *cmdline, void *ctx)
{
(void)pid;
(void)cmdline;
*(bool *)ctx = true;
return 1;
}
bool is_browser_running() {
bool running = false;
(void)browser_foreach_chromium(browser_chromium_found_cb, &running);
return running;
}
static void browser_prepare_display_env(void)
{
setenv("DISPLAY", ":0", 1);
setenv("XAUTHORITY", "/home/orangepi/.Xauthority", 1);
}
static void browser_run_shell_cmd(const char *cmd)
{
int status;
if (!cmd || !cmd[0]) {
return;
}
status = system(cmd);
if (status != 0) {
my_zlog_debug("browser shell cmd status=%d: %s", status, cmd);
}
}
static int browser_kill_chromium(void)
{
int i;
for (i = 0; i < 12; i++) {
if (!is_browser_running()) {
return 0;
}
if (i < 3) {
browser_run_shell_cmd("pkill -TERM -f 'chromium-browser --use-fake-ui' >/dev/null 2>&1");
browser_run_shell_cmd("pkill -TERM -f 'chromium-browser.*http' >/dev/null 2>&1");
browser_run_shell_cmd("su - orangepi -c \"pkill -TERM -f 'chromium-browser --use-fake-ui'; pkill -TERM chromium\" >/dev/null 2>&1");
} else {
browser_run_shell_cmd("pkill -9 -f 'chromium-browser --use-fake-ui' >/dev/null 2>&1");
browser_run_shell_cmd("pkill -9 -f 'chromium-browser.*http' >/dev/null 2>&1");
browser_run_shell_cmd("su - orangepi -c \"pkill -9 -f 'chromium-browser --use-fake-ui'; pkill -9 chromium\" >/dev/null 2>&1");
}
delay_ms(250);
}
return is_browser_running() ? -1 : 0;
}
static int browser_wait_chromium_running(int timeout_ms)
{
int elapsed = 0;
while (elapsed < timeout_ms) {
if (is_browser_running()) {
return 0;
}
delay_ms(200);
elapsed += 200;
}
return -1;
}
static int browser_read_url_from_proc(char *url, size_t url_size)
{
struct {
char *url;
size_t url_size;
int found;
} ctx;
if (!url || url_size == 0) {
return -1;
}
ctx.url = url;
ctx.url_size = url_size;
ctx.found = 0;
browser_foreach_chromium(browser_chromium_url_cb, &ctx);
return ctx.found ? 0 : -1;
}
static int browser_chromium_url_cb(int pid, const char *cmdline, void *ctx)
{
struct {
char *url;
size_t url_size;
int found;
} *search = ctx;
const char *http_pos;
size_t len;
(void)pid;
http_pos = strstr(cmdline, "https://");
if (!http_pos) {
http_pos = strstr(cmdline, "http://");
}
if (!http_pos) {
return 0;
}
len = 0;
while (http_pos[len] != '\0' && http_pos[len] != ' ' && len + 1 < search->url_size) {
len++;
}
memcpy(search->url, http_pos, len);
search->url[len] = '\0';
search->found = 1;
return 1;
}
int browser_get_stream_url_info(char *url, size_t url_size, bool *running)
{
bool chromium_running = is_browser_running();
if (running) {
*running = chromium_running;
}
if (!url || url_size == 0) {
return chromium_running ? 0 : -1;
}
url[0] = '\0';
if (!chromium_running) {
return 0;
}
if (s_active_stream_url[0] != '\0') {
snprintf(url, url_size, "%s", s_active_stream_url);
return 0;
}
if (browser_read_url_from_proc(url, url_size) == 0) {
return 0;
}
if (g_mqtt_cam_config_t && g_mqtt_cam_config_t->videourl[0] != '\0') {
const char *device_no = mqtt_topic_pure_number();
snprintf(url, url_size, "%s%s",
g_mqtt_cam_config_t->videourl,
device_no ? device_no : "");
}
return 0;
}
static void shell_escape_single_quoted(const char *input, char *out, size_t out_size)
{
size_t i = 0;
size_t j = 0;
if (!out || out_size == 0) {
return;
}
out[0] = '\0';
if (!input) {
return;
}
for (i = 0; input[i] != '\0' && j + 4 < out_size; i++) {
if (input[i] == '\'') {
out[j++] = '\'';
out[j++] = '\\';
out[j++] = '\'';
out[j++] = '\'';
} else {
out[j++] = input[i];
}
}
out[j] = '\0';
}
static int browser_validate_stream_url(const char *url)
{
size_t len;
if (!url || !url[0]) {
return -1;
}
len = strlen(url);
if (len >= sizeof(g_mqtt_cam_config_t->videourl)) {
return -1;
}
if (strncmp(url, "http://", 7) != 0 && strncmp(url, "https://", 8) != 0) {
return -1;
}
return 0;
}
static void browser_build_launch_url(const char *url, char *launch_url, size_t launch_size)
{
size_t len;
const char *device_no;
if (!url || !launch_url || launch_size == 0) {
return;
}
len = strlen(url);
device_no = mqtt_topic_pure_number();
if (len > 0 && url[len - 1] == '=') {
snprintf(launch_url, launch_size, "%s%s", url, device_no ? device_no : "");
return;
}
if (len >= 5 && strcmp(url + len - 5, "?dev=") == 0) {
snprintf(launch_url, launch_size, "%s%s", url, device_no ? device_no : "");
return;
}
snprintf(launch_url, launch_size, "%s", url);
}
static int browser_launch_chromium(const char *launch_url)
{
char escaped_url[1024];
char cmd[2048];
int ret;
if (!launch_url || !launch_url[0]) {
return -1;
}
browser_prepare_display_env();
shell_escape_single_quoted(launch_url, escaped_url, sizeof(escaped_url));
snprintf(cmd, sizeof(cmd),
"su - orangepi -c 'export DISPLAY=:0 XAUTHORITY=/home/orangepi/.Xauthority; "
"nohup chromium-browser --use-fake-ui-for-media-stream %s "
">/tmp/chromium-stream.log 2>&1 &'",
escaped_url);
my_zlog_info("browser_launch_chromium: %s", launch_url);
ret = system(cmd);
if (ret != 0) {
my_zlog_error("browser_launch_chromium failed ret=%d", ret);
return -1;
}
if (browser_wait_chromium_running(5000) != 0) {
my_zlog_error("browser_launch_chromium: chromium not running after launch");
return -1;
}
strncpy(s_active_stream_url, launch_url, sizeof(s_active_stream_url) - 1);
s_active_stream_url[sizeof(s_active_stream_url) - 1] = '\0';
return 0;
}
int browser_switch_stream_url(const char *url, char *effective_url, size_t effective_size)
{
char launch_url[768];
int ret;
if (browser_validate_stream_url(url) != 0) {
my_zlog_error("browser_switch_stream_url: invalid url");
return -1;
}
browser_build_launch_url(url, launch_url, sizeof(launch_url));
strncpy(g_mqtt_cam_config_t->videourl, url, sizeof(g_mqtt_cam_config_t->videourl) - 1);
g_mqtt_cam_config_t->videourl[sizeof(g_mqtt_cam_config_t->videourl) - 1] = '\0';
my_zlog_info("browser_switch_stream_url: kill old chromium, launch %s", launch_url);
browser_begin_external_switch();
if (browser_kill_chromium() != 0) {
my_zlog_warn("browser_switch_stream_url: old chromium may still be exiting, continue launch");
}
delay_ms(1000);
pthread_mutex_lock(&s_browser_ops_mutex);
ret = browser_launch_chromium(launch_url);
pthread_mutex_unlock(&s_browser_ops_mutex);
browser_end_external_switch();
if (ret != 0) {
s_browser_retry_after = time(NULL) + BROWSER_AUTO_RESTART_FAIL_SEC;
return -1;
}
if (effective_url && effective_size > 0) {
snprintf(effective_url, effective_size, "%s", launch_url);
}
my_zlog_info("browser_switch_stream_url: ok %s", launch_url);
return 0;
}
......@@ -7,8 +7,18 @@
int opencamsh();
void refresh_cam();//刷新摄像头函数
void refresh_cam(void);//刷新摄像头函数
bool is_browser_running();
void browser_begin_external_switch(void);
void browser_end_external_switch(void);
int browser_auto_restart_allowed(void);
/* 推流调试:仅浏览器 webrtc 模式可切换;effective_url 输出实际打开的完整网址 */
int browser_switch_stream_url(const char *url, char *effective_url, size_t effective_size);
/* 查询 chromium 运行状态与当前推流网址(url 缓冲区未运行时置空串) */
int browser_get_stream_url_info(char *url, size_t url_size, bool *running);
#endif
\ No newline at end of file
#include "stream_debug_agent.h"
#include "stream_debug_config.h"
#include "browser_open.h"
#include "webrtcpush_run.h"
#include "wifi_config_agent.h"
#include "http_config_mqtt.h"
#include "common.h"
static const char *stream_debug_current_mode(void)
{
if (webrtcpush_should_use_mpp()) {
return "c_lang";
}
return "webrtc";
}
static int stream_debug_send_json(int sock, const struct sockaddr_in *addr, const char *json)
{
if (!json || !addr || sock < 0) {
return -1;
}
return sendto(sock, json, strlen(json), 0,
(const struct sockaddr *)addr, sizeof(*addr)) >= 0 ? 0 : -1;
}
void stream_debug_handle_mode_query(int sock, const struct sockaddr_in *addr)
{
cJSON *root = cJSON_CreateObject();
const char *mode = stream_debug_current_mode();
char *payload;
if (!root) {
return;
}
cJSON_AddStringToObject(root, "cmd", "stream_mode_response");
cJSON_AddStringToObject(root, "mode", mode);
payload = cJSON_PrintUnformatted(root);
if (payload) {
stream_debug_send_json(sock, addr, payload);
my_zlog_info("推流调试: stream_mode_query -> mode=%s", mode);
free(payload);
}
cJSON_Delete(root);
}
static void stream_debug_send_url_result(int sock, const struct sockaddr_in *addr,
int success, const char *message,
const char *url)
{
cJSON *root = cJSON_CreateObject();
char *payload;
if (!root) {
return;
}
cJSON_AddStringToObject(root, "cmd", "stream_url_result");
cJSON_AddBoolToObject(root, "success", success ? 1 : 0);
if (message && message[0]) {
cJSON_AddStringToObject(root, "message", message);
}
if (url && url[0]) {
cJSON_AddStringToObject(root, "url", url);
}
payload = cJSON_PrintUnformatted(root);
if (payload) {
stream_debug_send_json(sock, addr, payload);
free(payload);
}
cJSON_Delete(root);
}
void stream_debug_handle_url_update(int sock, const struct sockaddr_in *addr, cJSON *root)
{
cJSON *url_item = cJSON_GetObjectItemCaseSensitive(root, "url");
const char *url;
char effective_url[STREAM_DEBUG_URL_MAX];
int rc;
if (strcmp(stream_debug_current_mode(), "webrtc") != 0) {
stream_debug_send_url_result(sock, addr, 0,
"当前为 C 语言推流,不支持切换网址,请修改设备配置",
NULL);
my_zlog_warn("推流调试: stream_url_update 拒绝(c_lang 模式)");
return;
}
if (!cJSON_IsString(url_item) || !url_item->valuestring || !url_item->valuestring[0]) {
stream_debug_send_url_result(sock, addr, 0, "缺少有效的 url 字段", NULL);
return;
}
url = url_item->valuestring;
my_zlog_info("推流调试: 收到 stream_url_update url=%s", url);
rc = browser_switch_stream_url(url, effective_url, sizeof(effective_url));
if (rc == 0) {
stream_debug_send_url_result(sock, addr, 1, "推流网址已切换", effective_url);
my_zlog_info("推流调试: 推流网址切换成功 -> %s", effective_url);
} else {
stream_debug_send_url_result(sock, addr, 0, "推流网址切换失败", NULL);
my_zlog_warn("推流调试: 推流网址切换失败");
}
wifi_config_agent_rebind_socket();
}
void stream_debug_handle_url_query(int sock, const struct sockaddr_in *addr)
{
cJSON *root = cJSON_CreateObject();
char url_buf[STREAM_DEBUG_URL_MAX];
bool running = false;
char *payload;
my_zlog_info("推流调试: 收到 stream_url_query");
if (!root) {
return;
}
cJSON_AddStringToObject(root, "cmd", "stream_url_response");
if (strcmp(stream_debug_current_mode(), "webrtc") != 0) {
cJSON_AddBoolToObject(root, "success", 0);
cJSON_AddBoolToObject(root, "running", 0);
cJSON_AddStringToObject(root, "message", "当前为 C 语言推流,不支持查询推流网址");
my_zlog_warn("推流调试: stream_url_query 拒绝(c_lang 模式)");
} else {
url_buf[0] = '\0';
(void)browser_get_stream_url_info(url_buf, sizeof(url_buf), &running);
cJSON_AddBoolToObject(root, "success", 1);
cJSON_AddBoolToObject(root, "running", running ? 1 : 0);
cJSON_AddStringToObject(root, "url", running ? url_buf : "");
cJSON_AddStringToObject(root, "message", running ? "当前推流网址" : "浏览器未启动");
my_zlog_info("推流调试: stream_url_query -> running=%d url=%s",
running ? 1 : 0, running ? url_buf : "");
}
payload = cJSON_PrintUnformatted(root);
if (payload) {
stream_debug_send_json(sock, addr, payload);
free(payload);
}
cJSON_Delete(root);
wifi_config_agent_rebind_socket();
}
#ifndef STREAM_DEBUG_AGENT_H__
#define STREAM_DEBUG_AGENT_H__
#include <netinet/in.h>
#include <cJSON.h>
void stream_debug_handle_mode_query(int sock, const struct sockaddr_in *addr);
void stream_debug_handle_url_update(int sock, const struct sockaddr_in *addr, cJSON *root);
void stream_debug_handle_url_query(int sock, const struct sockaddr_in *addr);
#endif
#ifndef STREAM_DEBUG_CONFIG_H__
#define STREAM_DEBUG_CONFIG_H__
#define STREAM_DEBUG_URL_MAX 512
#define STREAM_DEBUG_MESSAGE_MAX 256
#define STREAM_DEBUG_CHROMIUM_STOP_MS 1000
#endif
#include "audio_source.h"
#include "webrtcpush_config.h"
#include "webrtcpush_log.h"
#include <gst/gst.h>
#include <gst/app/gstappsink.h>
struct AudioSource {
GstElement *pipeline;
GstElement *appsink;
GstElement *enc;
guint8 *frame_buf; /* 缓存最新帧数据,避免 sample 生命周期问题 */
};
AudioSource *audio_source_start(const char *alsa_device, char **error_message)
{
AudioSource *src;
GstElement *pipe, *asrc, *conv, *resample, *caps_f, *enc, *sink;
GstCaps *caps;
GstStateChangeReturn ret;
if (!alsa_device || !alsa_device[0]) {
if (error_message)
*error_message = g_strdup("no ALSA device");
return NULL;
}
src = g_new0(AudioSource, 1);
pipe = gst_pipeline_new("audio-pipe");
if (!g_strcmp0(alsa_device, "test")) {
asrc = gst_element_factory_make("audiotestsrc", "asrc");
if (asrc)
g_object_set(asrc, "wave", 0, "freq", 440, NULL);
} else {
asrc = gst_element_factory_make("alsasrc", "asrc");
if (asrc)
g_object_set(asrc, "device", alsa_device, NULL);
}
conv = gst_element_factory_make("audioconvert", "conv");
resample = gst_element_factory_make("audioresample", "resample");
caps_f = gst_element_factory_make("capsfilter", "caps_f");
enc = gst_element_factory_make("opusenc", "enc");
sink = gst_element_factory_make("appsink", "sink");
if (!pipe || !asrc || !conv || !resample || !caps_f || !enc || !sink) {
if (error_message)
*error_message = g_strdup("failed to create audio GStreamer elements");
if (pipe)
gst_object_unref(pipe);
g_free(src);
return NULL;
}
/* Opus 要求 48000Hz;单声道足够车机麦 */
caps = gst_caps_new_simple("audio/x-raw",
"rate", G_TYPE_INT, WEBRTCPUSH_OPUS_CLOCKRATE,
"channels", G_TYPE_INT, WEBRTCPUSH_OPUS_CHANNELS,
"format", G_TYPE_STRING, "S16LE", NULL);
g_object_set(caps_f, "caps", caps, NULL);
gst_caps_unref(caps);
g_object_set(enc,
"bitrate", WEBRTCPUSH_OPUS_BITRATE,
"cbr", TRUE,
"framesize", 20, /* 20ms 帧 */
"inband-fec", TRUE,
NULL);
g_object_set(sink,
"emit-signals", FALSE,
"drop", FALSE,
"max-buffers", WEBRTCPUSH_AUDIO_QUEUE_DEPTH,
NULL);
gst_bin_add_many(GST_BIN(pipe), asrc, conv, resample, caps_f, enc, sink, NULL);
if (!gst_element_link_many(asrc, conv, resample, caps_f, enc, sink, NULL)) {
if (error_message)
*error_message = g_strdup("failed to link audio chain");
gst_object_unref(pipe);
g_free(src);
return NULL;
}
src->pipeline = pipe;
src->appsink = sink;
src->enc = enc;
ret = gst_element_set_state(pipe, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
if (error_message)
*error_message = g_strdup("audio pipeline failed to reach PLAYING");
gst_element_set_state(pipe, GST_STATE_NULL);
gst_object_unref(pipe);
g_free(src);
return NULL;
}
my_zlog_info("audio_source: started device=%s opus=%uch %uHz %ukbps",
alsa_device, WEBRTCPUSH_OPUS_CHANNELS, WEBRTCPUSH_OPUS_CLOCKRATE,
WEBRTCPUSH_OPUS_BITRATE / 1000);
return src;
}
void audio_source_stop(AudioSource *src)
{
if (!src)
return;
if (src->pipeline) {
gst_element_set_state(src->pipeline, GST_STATE_NULL);
gst_object_unref(src->pipeline);
}
g_free(src->frame_buf);
g_free(src);
}
gboolean audio_source_pull(AudioSource *src, const uint8_t **data, size_t *size,
guint64 *pts_ns, gint timeout_ms)
{
GstAppSink *appsink;
GstSample *sample;
GstBuffer *buffer;
GstMapInfo map;
if (!src || !src->appsink || !data || !size || timeout_ms < 1)
return FALSE;
appsink = GST_APP_SINK(src->appsink);
sample = gst_app_sink_try_pull_sample(appsink, (guint64)timeout_ms * GST_MSECOND);
if (!sample)
return FALSE;
buffer = gst_sample_get_buffer(sample);
if (!buffer || !gst_buffer_map(buffer, &map, GST_MAP_READ)) {
gst_sample_unref(sample);
return FALSE;
}
g_free(src->frame_buf);
src->frame_buf = g_memdup2(map.data, map.size);
*data = src->frame_buf;
*size = map.size;
if (pts_ns)
*pts_ns = GST_BUFFER_PTS(buffer);
gst_buffer_unmap(buffer, &map);
gst_sample_unref(sample);
return TRUE;
}
#ifndef AUDIO_SOURCE_H
#define AUDIO_SOURCE_H
#include <glib.h>
#include <stddef.h>
#include <stdint.h>
typedef struct AudioSource AudioSource;
/* alsa_device: plughw:X,Y | "test"(audiotestsrc 测试音) */
AudioSource *audio_source_start(const char *alsa_device, char **error_message);
void audio_source_stop(AudioSource *src);
/* 拉一帧 Opus payload;数据在下次 pull 前有效。timeout_ms:appsink 等待毫秒。 */
gboolean audio_source_pull(AudioSource *src, const uint8_t **data, size_t *size,
guint64 *pts_ns, gint timeout_ms);
#endif
......@@ -5,30 +5,90 @@
#include <gst/app/gstappsink.h>
#include <gst/video/video-event.h>
#include <fcntl.h>
#include <linux/videodev2.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
#define MPP_VIDEO_WIDTH 1280
#define MPP_VIDEO_HEIGHT 720
#define MPP_VIDEO_GOP WEBRTCPUSH_H264_GOP
#define MPP_VIDEO_START_BPS 1400000u
#define MPP_VIDEO_START_BPS WEBRTCPUSH_INITIAL_BITRATE
#define MPP_VIDEO_MIN_BPS WEBRTCPUSH_MIN_BITRATE
#define MPP_VIDEO_MAX_BPS WEBRTCPUSH_MAX_BITRATE
#define MPP_H264_PROFILE 66 /* Baseline:板端 mpph264enc 最稳 */
#define WRTC_V4L2_CAP_VIDEO_CAPTURE (1u << 0)
#define WRTC_V4L2_CAP_VIDEO_CAPTURE_MPLANE (1u << 12)
typedef enum {
CAPTURE_NONE = 0,
CAPTURE_RAW_NV12_720P,
CAPTURE_MJPEG_720P24,
CAPTURE_MJPEG_720P30,
} CaptureMode;
struct MppH264Source {
GstElement *pipeline;
GstElement *enc;
GstElement *parse;
GstElement *eq;
GstElement *appsink;
gboolean use_mpp;
guint8 *frame_buf;
size_t frame_size;
size_t frame_capacity;
guint keyframe_req;
volatile gboolean pending_idr;
gulong enc_sink_probe_id;
guint bus_watch_id;
gchar *sprop_parameter_sets;
};
static gboolean pull_sample(MppH264Source *src, gboolean require_idr,
gint timeout_ms, const uint8_t **data,
size_t *size, gboolean *is_idr,
guint64 *pts_ns);
static gboolean bus_watch_cb(GstBus *bus, GstMessage *msg, gpointer user_data)
{
MppH264Source *src = user_data;
(void)bus;
if (!src)
return TRUE;
switch (GST_MESSAGE_TYPE(msg)) {
case GST_MESSAGE_ERROR: {
GError *err = NULL;
gchar *dbg = NULL;
gst_message_parse_error(msg, &err, &dbg);
my_zlog_error("mpp_h264_source: pipeline error: %s (%s)",
err ? err->message : "unknown", dbg ? dbg : "");
g_clear_error(&err);
g_free(dbg);
break;
}
case GST_MESSAGE_WARNING: {
GError *err = NULL;
gchar *dbg = NULL;
gst_message_parse_warning(msg, &err, &dbg);
my_zlog_warn("mpp_h264_source: pipeline warning: %s (%s)",
err ? err->message : "unknown", dbg ? dbg : "");
g_clear_error(&err);
g_free(dbg);
break;
}
case GST_MESSAGE_EOS:
my_zlog_warn("mpp_h264_source: pipeline EOS");
break;
default:
break;
}
return TRUE;
}
static gchar *pick_v4l2_capture_device(void)
{
for (int i = 0; i < 32; i++) {
......@@ -61,23 +121,164 @@ static gchar *pick_v4l2_capture_device(void)
return NULL;
}
static void configure_leaky_queue(GstElement *q)
static guint frmival_to_fps(const struct v4l2_frmivalenum *fi)
{
if (!fi)
return 0;
if (fi->type == V4L2_FRMIVAL_TYPE_DISCRETE) {
if (!fi->discrete.denominator || !fi->discrete.numerator)
return 0;
return fi->discrete.denominator / fi->discrete.numerator;
}
if (fi->type == V4L2_FRMIVAL_TYPE_STEPWISE ||
fi->type == V4L2_FRMIVAL_TYPE_CONTINUOUS) {
if (!fi->stepwise.min.denominator || !fi->stepwise.min.numerator)
return 0;
return fi->stepwise.min.denominator / fi->stepwise.min.numerator;
}
return 0;
}
static gboolean frmival_meets_fps(const struct v4l2_frmivalenum *fi, guint min_fps)
{
guint fps = frmival_to_fps(fi);
return fps >= min_fps;
}
static gboolean size_is_720p(const struct v4l2_frmsizeenum *fs)
{
if (fs->type == V4L2_FRMSIZE_TYPE_DISCRETE)
return fs->discrete.width == MPP_VIDEO_WIDTH &&
fs->discrete.height == MPP_VIDEO_HEIGHT;
if (fs->type == V4L2_FRMSIZE_TYPE_STEPWISE ||
fs->type == V4L2_FRMSIZE_TYPE_CONTINUOUS)
return fs->stepwise.min_width <= MPP_VIDEO_WIDTH &&
fs->stepwise.max_width >= MPP_VIDEO_WIDTH &&
fs->stepwise.min_height <= MPP_VIDEO_HEIGHT &&
fs->stepwise.max_height >= MPP_VIDEO_HEIGHT;
return FALSE;
}
/* 探测 720p 采集:优先 NV12/YUYV@24fps+,否则 MJPEG@30(本机 USB 头常见情况) */
static CaptureMode probe_720p_capture_mode(const char *device)
{
int fd;
struct v4l2_fmtdesc fmt;
gboolean raw_ok = FALSE;
gboolean mjpg24_ok = FALSE;
gboolean mjpg30_ok = FALSE;
if (!device || !device[0])
return CAPTURE_NONE;
fd = open(device, O_RDONLY | O_NONBLOCK);
if (fd < 0)
return CAPTURE_NONE;
memset(&fmt, 0, sizeof(fmt));
fmt.type = V4L2_BUF_TYPE_VIDEO_CAPTURE;
for (; ioctl(fd, VIDIOC_ENUM_FMT, &fmt) == 0; fmt.index++) {
struct v4l2_frmsizeenum fs;
memset(&fs, 0, sizeof(fs));
fs.pixel_format = fmt.pixelformat;
fs.index = 0;
for (; ioctl(fd, VIDIOC_ENUM_FRAMESIZES, &fs) == 0; fs.index++) {
struct v4l2_frmivalenum fi;
if (!size_is_720p(&fs))
continue;
memset(&fi, 0, sizeof(fi));
fi.pixel_format = fmt.pixelformat;
fi.index = 0;
if (fs.type == V4L2_FRMSIZE_TYPE_DISCRETE) {
fi.width = fs.discrete.width;
fi.height = fs.discrete.height;
} else {
fi.width = MPP_VIDEO_WIDTH;
fi.height = MPP_VIDEO_HEIGHT;
}
for (; ioctl(fd, VIDIOC_ENUM_FRAMEINTERVALS, &fi) == 0; fi.index++) {
guint fps = frmival_to_fps(&fi);
if (fmt.pixelformat == V4L2_PIX_FMT_MJPEG) {
/* 必须能直出目标帧率;30fps 不能冒充 24fps(v4l2 会 not-negotiated) */
if (fps == WEBRTCPUSH_H264_FPS)
mjpg24_ok = TRUE;
if (fps >= 30)
mjpg30_ok = TRUE;
}
if ((fmt.pixelformat == V4L2_PIX_FMT_NV12 ||
fmt.pixelformat == V4L2_PIX_FMT_YUYV) &&
fps >= WEBRTCPUSH_H264_FPS) {
raw_ok = TRUE;
}
}
}
}
close(fd);
if (raw_ok) {
my_zlog_info("mpp_h264_source: %s supports 720p raw >=%dfps, use NV12/YUYV direct",
device, WEBRTCPUSH_H264_FPS);
return CAPTURE_RAW_NV12_720P;
}
if (mjpg24_ok) {
my_zlog_info("mpp_h264_source: %s supports 720p MJPEG@%dfps direct",
device, WEBRTCPUSH_H264_FPS);
return CAPTURE_MJPEG_720P24;
}
if (mjpg30_ok) {
my_zlog_info("mpp_h264_source: %s 720p MJPEG@30fps -> mppjpegdec -> videorate@%dfps",
device, WEBRTCPUSH_H264_FPS);
return CAPTURE_MJPEG_720P30;
}
my_zlog_warn("mpp_h264_source: %s has no suitable 720p capture mode", device);
return CAPTURE_NONE;
}
static void configure_pre_enc_queue(GstElement *q)
{
if (!q)
return;
g_object_set(q,
"max-size-buffers", 1,
"max-size-buffers", WEBRTCPUSH_MPP_PRE_ENC_BUFFERS,
"max-size-bytes", 0,
"max-size-time", (guint64)0,
"max-size-time", (guint64)GST_SECOND * 2 / WEBRTCPUSH_H264_FPS,
"leaky", 2,
NULL);
}
static void configure_v4l2_low_latency(GstElement *src)
static void configure_post_enc_queue(GstElement *q)
{
if (!q)
return;
g_object_set(q,
"max-size-buffers", WEBRTCPUSH_MPP_POST_ENC_BUFFERS,
"max-size-bytes", 0,
"max-size-time", (guint64)GST_SECOND / WEBRTCPUSH_H264_FPS,
"leaky", 0,
NULL);
}
static void configure_v4l2_src(GstElement *src, gboolean compressed_mjpeg)
{
if (!src)
return;
g_object_set(src, "do-timestamp", TRUE, NULL);
/*
* MJPEG 压缩流用 mmap;原始 NV12/YUYV 再尝试 DMABUF 零拷贝进 MPP。
*/
if (compressed_mjpeg)
g_object_set(src, "io-mode", 2, NULL);
else
g_object_set(src, "io-mode", 4, NULL);
}
static GstElement *make_h264_encoder(gboolean prefer_mpp, gboolean use_test,
......@@ -91,17 +292,17 @@ static GstElement *make_h264_encoder(gboolean prefer_mpp, gboolean use_test,
*use_mpp_out = TRUE;
g_object_set(enc,
"gop", MPP_VIDEO_GOP,
"rc-mode", 1,
"rc-mode", 1, /* CBR */
"bps", MPP_VIDEO_START_BPS,
"bps-min", (guint)(MPP_VIDEO_START_BPS * 0.80),
"bps-max", (guint)(MPP_VIDEO_START_BPS * 1.08),
"header-mode", 1,
"profile", 66,
"profile", MPP_H264_PROFILE,
"level", 31,
"qos", TRUE,
"zero-copy-pkt", TRUE,
NULL);
my_zlog_info("mpp_h264_source: mpph264enc %dx%d@%dfps GOP=%d",
my_zlog_info("mpp_h264_source: mpph264enc %dx%d@%dfps GOP=%d Baseline/CBR",
MPP_VIDEO_WIDTH, MPP_VIDEO_HEIGHT, WEBRTCPUSH_H264_FPS,
MPP_VIDEO_GOP);
return enc;
......@@ -117,7 +318,7 @@ static GstElement *make_h264_encoder(gboolean prefer_mpp, gboolean use_test,
"bitrate", MPP_VIDEO_START_BPS / 1000,
"key-int-max", MPP_VIDEO_GOP,
"byte-stream", TRUE,
"profile", "baseline",
"profile", "main",
NULL);
my_zlog_info("mpp_h264_source: x264enc software fallback");
return enc;
......@@ -132,6 +333,8 @@ static gboolean buffer_is_idr(const guint8 *data, size_t size)
while (i + 3 < size) {
size_t sc = 0;
guint8 nal_type;
if (i + 4 <= size && data[i] == 0 && data[i + 1] == 0 &&
data[i + 2] == 0 && data[i + 3] == 1)
sc = 4;
......@@ -145,44 +348,69 @@ static gboolean buffer_is_idr(const guint8 *data, size_t size)
i += sc;
if (i >= size)
break;
if ((data[i] & 0x1f) == 5)
nal_type = data[i] & 0x1f;
if (nal_type == 5)
return TRUE;
if (nal_type == 1)
return FALSE;
}
return FALSE;
}
static gboolean send_force_key_unit_on_pad(GstPad *pad)
static GstPadProbeReturn enc_sink_idr_probe(GstPad *pad, GstPadProbeInfo *info,
gpointer user_data)
{
GstEvent *event;
MppH264Source *src = user_data;
if (!pad)
return FALSE;
event = gst_video_event_new_upstream_force_key_unit(
GST_CLOCK_TIME_NONE, TRUE, 0);
return gst_pad_send_event(pad, event);
if (!(GST_PAD_PROBE_INFO_TYPE(info) & GST_PAD_PROBE_TYPE_BUFFER))
return GST_PAD_PROBE_OK;
if (!src || !src->pending_idr)
return GST_PAD_PROBE_OK;
{
GstEvent *ev = gst_video_event_new_upstream_force_key_unit(
GST_CLOCK_TIME_NONE, TRUE, src->keyframe_req);
gboolean ok = gst_pad_send_event(pad, ev);
if (!ok && src->enc) {
ev = gst_video_event_new_upstream_force_key_unit(
GST_CLOCK_TIME_NONE, TRUE, src->keyframe_req);
ok = gst_element_send_event(src->enc, ev);
}
if (ok) {
src->pending_idr = FALSE;
my_zlog_info("mpp_h264_source: IDR armed in-stream (%u)", src->keyframe_req);
} else {
my_zlog_warn("mpp_h264_source: in-stream IDR not handled");
}
}
return GST_PAD_PROBE_OK;
}
static void request_keyframe(MppH264Source *src)
{
gboolean ok = FALSE;
if (!src)
return;
if (src->pending_idr)
return;
src->keyframe_req++;
/* Upstream force-key-unit 必须交给编码器的 src pad 处理。 */
src->pending_idr = TRUE;
/* 兜底:直接向编码器发 force-key-unit,不依赖 enc_sink_probe 触发时机 */
if (src->enc) {
GstPad *pad = gst_element_get_static_pad(src->enc, "src");
ok = send_force_key_unit_on_pad(pad);
if (pad)
gst_object_unref(pad);
GstEvent *ev = gst_video_event_new_upstream_force_key_unit(
GST_CLOCK_TIME_NONE, TRUE, src->keyframe_req);
if (!gst_element_send_event(src->enc, ev)) {
GstPad *sink_pad = gst_element_get_static_pad(src->enc, "sink");
if (sink_pad) {
ev = gst_video_event_new_upstream_force_key_unit(
GST_CLOCK_TIME_NONE, TRUE, src->keyframe_req);
gst_pad_send_event(sink_pad, ev);
gst_object_unref(sink_pad);
}
}
}
if (ok)
my_zlog_info("mpp_h264_source: requested IDR (%u)", src->keyframe_req);
else
my_zlog_warn("mpp_h264_source: IDR request not handled");
my_zlog_info("mpp_h264_source: IDR queued (%u)", src->keyframe_req);
}
void mpp_h264_source_request_keyframe(MppH264Source *src)
......@@ -190,6 +418,63 @@ void mpp_h264_source_request_keyframe(MppH264Source *src)
request_keyframe(src);
}
static gboolean extract_sprop_from_au(MppH264Source *src, const guint8 *data,
size_t size)
{
gchar *sps_b64 = NULL;
gchar *pps_b64 = NULL;
if (!src || !data || size < 5 || src->sprop_parameter_sets)
return src && src->sprop_parameter_sets != NULL;
for (size_t i = 0; i + 4 < size;) {
size_t nal_start = 0;
guint8 nal_type;
if (data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1) {
nal_start = i + 3;
i += 3;
} else if (data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 &&
data[i + 3] == 1) {
nal_start = i + 4;
i += 4;
} else {
i++;
continue;
}
nal_type = data[nal_start] & 0x1f;
{
size_t j = nal_start + 1;
while (j + 3 < size) {
if (data[j] == 0 && data[j + 1] == 0 &&
(data[j + 2] == 1 || (data[j + 2] == 0 && data[j + 3] == 1)))
break;
j++;
}
if (j + 3 >= size)
j = size;
if (nal_type == 7 && !sps_b64)
sps_b64 = g_base64_encode(data + nal_start, j - nal_start);
else if (nal_type == 8 && !pps_b64)
pps_b64 = g_base64_encode(data + nal_start, j - nal_start);
i = j;
}
if (sps_b64 && pps_b64)
break;
}
if (sps_b64 && pps_b64) {
g_free(src->sprop_parameter_sets);
src->sprop_parameter_sets = g_strdup_printf("%s,%s", sps_b64, pps_b64);
my_zlog_info("mpp_h264_source: captured sprop for SDP (%zu+%zu b64)",
strlen(sps_b64), strlen(pps_b64));
}
g_free(sps_b64);
g_free(pps_b64);
return src->sprop_parameter_sets != NULL;
}
static gboolean copy_frame(MppH264Source *src, GstBuffer *buffer)
{
GstMapInfo map;
......@@ -203,22 +488,41 @@ static gboolean copy_frame(MppH264Source *src, GstBuffer *buffer)
}
memcpy(src->frame_buf, map.data, map.size);
src->frame_size = map.size;
extract_sprop_from_au(src, src->frame_buf, src->frame_size);
gst_buffer_unmap(buffer, &map);
return TRUE;
}
static gboolean link_pipeline(GstElement *pipe, GstElement **elements, guint count)
{
guint i;
if (!pipe || !elements || count < 2)
return FALSE;
for (i = 0; i < count; i++)
gst_bin_add(GST_BIN(pipe), elements[i]);
for (i = 0; i + 1 < count; i++) {
if (!gst_element_link(elements[i], elements[i + 1]))
return FALSE;
}
return TRUE;
}
MppH264Source *mpp_h264_source_start(const char *video_device, char **error_message)
{
MppH264Source *src;
GstElement *pipe, *vsrc, *caps_jpeg = NULL, *jpegparse = NULL, *jpegdec = NULL;
GstElement *vrate, *conv, *caps1, *enc, *parse, *q, *caps_h264, *sink;
GstElement *pipe, *vsrc, *caps_in = NULL, *jpegparse = NULL, *jpegdec = NULL;
GstElement *conv = NULL, *vrate, *caps_nv12, *enc, *parse, *q, *eq, *caps_h264, *sink;
gchar *auto_v4l = NULL;
const gchar *vdev = video_device;
gboolean use_test = FALSE;
gboolean use_auto = FALSE;
gboolean use_mjpg = FALSE;
gboolean use_mpp = FALSE;
gboolean use_mpp_jpeg = FALSE;
gint mjpeg_fps = WEBRTCPUSH_H264_FPS;
CaptureMode capture_mode = CAPTURE_NONE;
GstStateChangeReturn ret;
if (!gst_is_initialized())
......@@ -234,21 +538,55 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
if (auto_v4l) {
my_zlog_info("mpp_h264_source: auto V4L2 %s", auto_v4l);
vdev = auto_v4l;
capture_mode = probe_720p_capture_mode(vdev);
} else {
my_zlog_warn("mpp_h264_source: no V4L2 device, videotestsrc");
use_test = TRUE;
}
} else if (!use_test) {
capture_mode = probe_720p_capture_mode(vdev);
}
if (!use_test && capture_mode == CAPTURE_NONE) {
if (error_message)
*error_message = g_strdup("no 720p capture mode on camera");
g_free(auto_v4l);
g_free(src);
return NULL;
}
pipe = gst_pipeline_new("mpp-h264-pipe");
vrate = gst_element_factory_make("videorate", "vrate");
caps_nv12 = gst_element_factory_make("capsfilter", "caps_nv12");
q = gst_element_factory_make("queue", "vq");
eq = gst_element_factory_make("queue", "eq");
enc = make_h264_encoder(TRUE, use_test, &use_mpp);
parse = gst_element_factory_make("h264parse", "parse");
caps_h264 = gst_element_factory_make("capsfilter", "caps_h264");
sink = gst_element_factory_make("appsink", "sink");
if (use_test) {
vsrc = gst_element_factory_make("videotestsrc", "vsrc");
g_object_set(vsrc, "is-live", TRUE, NULL);
} else {
conv = gst_element_factory_make("videoconvert", "conv");
} else if (capture_mode == CAPTURE_RAW_NV12_720P) {
vsrc = gst_element_factory_make("v4l2src", "vsrc");
caps_in = gst_element_factory_make("capsfilter", "caps_in");
g_object_set(vsrc, "device", vdev, NULL);
configure_v4l2_low_latency(vsrc);
caps_jpeg = gst_element_factory_make("capsfilter", "caps_jpeg");
configure_v4l2_src(vsrc, FALSE);
{
gchar *cap_str = g_strdup_printf(
"video/x-raw,format=NV12,width=%d,height=%d,framerate=%d/1",
MPP_VIDEO_WIDTH, MPP_VIDEO_HEIGHT, WEBRTCPUSH_H264_FPS);
GstCaps *caps = gst_caps_from_string(cap_str);
g_object_set(caps_in, "caps", caps, NULL);
gst_caps_unref(caps);
g_free(cap_str);
}
my_zlog_info("mpp_h264_source: 720p NV12 direct capture (no JPEG decode)");
} else {
vsrc = gst_element_factory_make("v4l2src", "vsrc");
caps_in = gst_element_factory_make("capsfilter", "caps_in");
jpegparse = gst_element_factory_make("jpegparse", "jpegparse");
jpegdec = gst_element_factory_make("mppjpegdec", "jpegdec");
if (jpegdec) {
......@@ -257,32 +595,26 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
} else {
jpegdec = gst_element_factory_make("jpegdec", "jpegdec");
}
if (caps_jpeg && jpegparse && jpegdec) {
gchar *jc_str = g_strdup_printf(
"image/jpeg,width=%d,height=%d,framerate=30/1",
MPP_VIDEO_WIDTH, MPP_VIDEO_HEIGHT);
GstCaps *jc = gst_caps_from_string(jc_str);
g_object_set(caps_jpeg, "caps", jc, NULL);
gst_caps_unref(jc);
g_free(jc_str);
use_mjpg = TRUE;
my_zlog_info("mpp_h264_source: JPEG decoder=%s",
use_mpp_jpeg ? "mppjpegdec" : "jpegdec software fallback");
mjpeg_fps = (capture_mode == CAPTURE_MJPEG_720P24) ? WEBRTCPUSH_H264_FPS : 30;
g_object_set(vsrc, "device", vdev, NULL);
configure_v4l2_src(vsrc, TRUE);
{
gchar *cap_str = g_strdup_printf(
"image/jpeg,width=%d,height=%d,framerate=%d/1",
MPP_VIDEO_WIDTH, MPP_VIDEO_HEIGHT, mjpeg_fps);
GstCaps *caps = gst_caps_from_string(cap_str);
g_object_set(caps_in, "caps", caps, NULL);
gst_caps_unref(caps);
g_free(cap_str);
}
my_zlog_info("mpp_h264_source: 720p MJPEG@%dfps -> %s -> NV12@%dfps",
mjpeg_fps,
use_mpp_jpeg ? "mppjpegdec" : "jpegdec",
WEBRTCPUSH_H264_FPS);
}
conv = gst_element_factory_make("videoconvert", "conv");
vrate = gst_element_factory_make("videorate", "vrate");
caps1 = gst_element_factory_make("capsfilter", "caps1");
q = gst_element_factory_make("queue", "vq");
enc = make_h264_encoder(TRUE, use_test, &use_mpp);
parse = gst_element_factory_make("h264parse", "parse");
caps_h264 = gst_element_factory_make("capsfilter", "caps_h264");
sink = gst_element_factory_make("appsink", "sink");
if (!pipe || !vsrc || !conv || !vrate || !caps1 || !q || !enc || !parse ||
!caps_h264 || !sink || (!use_test && !use_mjpg)) {
g_set_error_literal(NULL, 0, 0, "GStreamer element create failed");
if (!pipe || !vsrc || !vrate || !caps_nv12 || !q || !eq || !enc || !parse ||
!caps_h264 || !sink) {
if (error_message)
*error_message = g_strdup("GStreamer element create failed");
g_free(auto_v4l);
......@@ -291,18 +623,18 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
}
g_object_set(vrate, "max-rate", WEBRTCPUSH_H264_FPS, "drop-only", TRUE, NULL);
configure_leaky_queue(q);
configure_pre_enc_queue(q);
configure_post_enc_queue(eq);
g_object_set(parse, "config-interval", -1, NULL);
{
gchar *cap_str = g_strdup_printf(
"video/x-raw,format=%s,width=%d,height=%d,framerate=%d/1",
use_mpp ? "NV12" : "I420",
"video/x-raw,format=NV12,width=%d,height=%d,framerate=%d/1",
use_test ? 640 : MPP_VIDEO_WIDTH,
use_test ? 480 : MPP_VIDEO_HEIGHT,
WEBRTCPUSH_H264_FPS);
GstCaps *caps = gst_caps_from_string(cap_str);
g_object_set(caps1, "caps", caps, NULL);
g_object_set(caps_nv12, "caps", caps, NULL);
gst_caps_unref(caps);
g_free(cap_str);
}
......@@ -317,29 +649,37 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
g_object_set(sink,
"emit-signals", FALSE,
"sync", FALSE,
"max-buffers", 1,
"max-buffers", WEBRTCPUSH_APPSINK_MAX_BUFFERS,
"drop", TRUE,
NULL);
if (use_mjpg) {
gst_bin_add_many(GST_BIN(pipe), vsrc, caps_jpeg, jpegparse, jpegdec, conv, vrate,
caps1, q, enc, parse, caps_h264, sink, NULL);
if (!gst_element_link_many(vsrc, caps_jpeg, jpegparse, jpegdec, conv, vrate,
caps1, q, enc, parse, caps_h264, sink, NULL)) {
if (use_test) {
GstElement *elems[] = {vsrc, conv, vrate, caps_nv12, q, enc, parse, caps_h264, eq, sink};
if (!link_pipeline(pipe, elems, G_N_ELEMENTS(elems))) {
if (error_message)
*error_message = g_strdup("failed to link test chain");
gst_object_unref(pipe);
g_free(auto_v4l);
g_free(src);
return NULL;
}
} else if (capture_mode == CAPTURE_RAW_NV12_720P) {
GstElement *elems[] = {vsrc, caps_in, vrate, caps_nv12, q, enc, parse, caps_h264, eq, sink};
if (!link_pipeline(pipe, elems, G_N_ELEMENTS(elems))) {
if (error_message)
*error_message = g_strdup("failed to link MJPG capture chain");
*error_message = g_strdup("failed to link raw NV12 chain");
gst_object_unref(pipe);
g_free(auto_v4l);
g_free(src);
return NULL;
}
} else {
gst_bin_add_many(GST_BIN(pipe), vsrc, conv, vrate, caps1, q, enc, parse,
caps_h264, sink, NULL);
if (!gst_element_link_many(vsrc, conv, vrate, caps1, q, enc, parse,
caps_h264, sink, NULL)) {
GstElement *elems[] = {vsrc, caps_in, jpegparse, jpegdec, vrate, caps_nv12,
q, enc, parse, caps_h264, eq, sink};
if (!jpegparse || !jpegdec ||
!link_pipeline(pipe, elems, G_N_ELEMENTS(elems))) {
if (error_message)
*error_message = g_strdup("failed to link video chain");
*error_message = g_strdup("failed to link MJPEG 720p chain");
gst_object_unref(pipe);
g_free(auto_v4l);
g_free(src);
......@@ -350,9 +690,19 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
src->pipeline = pipe;
src->enc = enc;
src->parse = parse;
src->eq = eq;
src->appsink = sink;
src->use_mpp = use_mpp;
{
GstPad *enc_sink = gst_element_get_static_pad(enc, "sink");
if (enc_sink) {
src->enc_sink_probe_id = gst_pad_add_probe(
enc_sink, GST_PAD_PROBE_TYPE_BUFFER, enc_sink_idr_probe, src, NULL);
gst_object_unref(enc_sink);
}
}
gst_pipeline_set_latency(GST_PIPELINE(pipe), 0);
ret = gst_element_set_state(pipe, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
......@@ -363,7 +713,34 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
return NULL;
}
my_zlog_info("mpp_h264_source: live encoder started (mpp=%d)", use_mpp ? 1 : 0);
{
GstBus *bus = gst_element_get_bus(pipe);
if (bus) {
src->bus_watch_id = gst_bus_add_watch(bus, bus_watch_cb, src);
gst_object_unref(bus);
}
}
{
const guint8 *warm_data;
size_t warm_size;
gboolean warm_idr;
if (!pull_sample(src, FALSE, 8000, &warm_data, &warm_size, &warm_idr, NULL)) {
if (error_message)
*error_message = g_strdup("encoder warmup timeout (no frame in 8s)");
my_zlog_error("mpp_h264_source: warmup failed, check camera /dev/video*");
mpp_h264_source_stop(src);
g_free(auto_v4l);
return NULL;
}
my_zlog_info("mpp_h264_source: warmup OK first_au=%zu bytes idr=%d",
warm_size, warm_idr ? 1 : 0);
}
my_zlog_info("mpp_h264_source: live encoder started 720p (mpp=%d pre_buf=%d post_buf=%d)",
use_mpp ? 1 : 0, WEBRTCPUSH_MPP_PRE_ENC_BUFFERS,
WEBRTCPUSH_MPP_POST_ENC_BUFFERS);
g_free(auto_v4l);
return src;
}
......@@ -387,7 +764,7 @@ static gboolean pull_sample(MppH264Source *src, gboolean require_idr,
appsink = GST_APP_SINK(src->appsink);
deadline = g_get_monotonic_time() + (gint64)timeout_ms * 1000;
while (attempts < 40) {
while (attempts < 20) {
gint wait_ms = (gint)((deadline - g_get_monotonic_time()) / 1000);
if (wait_ms < 1)
......@@ -439,15 +816,17 @@ gboolean mpp_h264_source_pull(MppH264Source *src, gboolean force_idr,
return FALSE;
if (timeout_ms < 1)
timeout_ms = 80;
timeout_ms = (1000 / WEBRTCPUSH_H264_FPS) + 60;
if (force_idr) {
idr_timeout = timeout_ms < 2000 ? 2000 : timeout_ms;
request_keyframe(src);
if (pull_sample(src, TRUE, idr_timeout, data, size, is_idr, pts_ns))
return TRUE;
my_zlog_warn("mpp_h264_source: IDR wait timeout, fallback to latest frame");
return pull_sample(src, FALSE, 200, data, size, is_idr, pts_ns);
my_zlog_warn("mpp_h264_source: IDR wait timeout, drain latest frame");
if (pull_sample(src, FALSE, 500, data, size, is_idr, pts_ns))
return TRUE;
return FALSE;
}
return pull_sample(src, FALSE, timeout_ms, data, size, is_idr, pts_ns);
......@@ -481,16 +860,52 @@ void mpp_h264_source_set_bitrate(MppH264Source *src, guint bitrate_bps)
}
}
gboolean mpp_h264_source_recover(MppH264Source *src)
{
const guint8 *data;
size_t size;
gboolean idr;
GstStateChangeReturn ret;
if (!src || !src->pipeline)
return FALSE;
my_zlog_warn("mpp_h264_source: recovering pipeline (NULL -> PLAYING)");
src->pending_idr = FALSE;
gst_element_set_state(src->pipeline, GST_STATE_NULL);
g_usleep(300000);
ret = gst_element_set_state(src->pipeline, GST_STATE_PLAYING);
if (ret == GST_STATE_CHANGE_FAILURE) {
my_zlog_error("mpp_h264_source: recover failed to reach PLAYING");
return FALSE;
}
if (!pull_sample(src, FALSE, 5000, &data, &size, &idr, NULL)) {
my_zlog_error("mpp_h264_source: recover warmup failed");
return FALSE;
}
my_zlog_info("mpp_h264_source: recover OK first_au=%zu bytes", size);
return TRUE;
}
gboolean mpp_h264_source_is_mpp_encoder(const MppH264Source *src)
{
return src && src->use_mpp;
}
const gchar *mpp_h264_source_get_sprop(const MppH264Source *src)
{
return src ? src->sprop_parameter_sets : NULL;
}
void mpp_h264_source_stop(MppH264Source *src)
{
if (!src)
return;
if (src->bus_watch_id) {
g_source_remove(src->bus_watch_id);
src->bus_watch_id = 0;
}
if (src->pipeline) {
gst_element_set_state(src->pipeline, GST_STATE_NULL);
gst_object_unref(src->pipeline);
......@@ -498,7 +913,9 @@ void mpp_h264_source_stop(MppH264Source *src)
}
src->enc = NULL;
src->parse = NULL;
src->eq = NULL;
src->appsink = NULL;
g_free(src->sprop_parameter_sets);
g_free(src->frame_buf);
g_free(src);
}
......@@ -22,4 +22,9 @@ void mpp_h264_source_request_keyframe(MppH264Source *src);
gboolean mpp_h264_source_is_mpp_encoder(const MppH264Source *src);
const gchar *mpp_h264_source_get_sprop(const MppH264Source *src);
/* 管道无帧时尝试 NULL→PLAYING 恢复 */
gboolean mpp_h264_source_recover(MppH264Source *src);
#endif
#include "rtc_client.h"
#include "mpp_h264_source.h"
#include "audio_source.h"
#include "webrtcpush_config.h"
#include "webrtcpush_log.h"
#include "ws_signaling.h"
......@@ -23,10 +24,35 @@ typedef struct {
} OfferVideo;
typedef struct {
gint payload_type;
gchar *mid;
gboolean found;
} OfferAudio;
typedef struct {
guint8 *data;
gsize size;
gboolean is_idr;
guint64 pts_ns;
} RtcH264Frame;
typedef struct {
GMutex lock;
GCond not_empty;
GQueue queue;
guint max_depth;
volatile gboolean stopping;
guint dropped_total;
} RtcFrameQueue;
typedef struct {
AppState *app;
GMutex lock;
GMutex send_lock;
GThread *sender_thread;
GCond track_cond;
GThread *pull_thread;
GThread *send_thread;
RtcFrameQueue frame_queue;
MppH264Source *h264_source;
gboolean stopping;
gboolean track_open;
......@@ -42,9 +68,22 @@ typedef struct {
guint target_bitrate;
guint remb_ceiling;
gint64 last_bitrate_ramp_us;
gint64 last_idr_request_us;
gint64 last_stats_log_us;
gchar *remote_ice_ufrag;
gchar *video_mid;
gint video_mline_index;
/* 音频 Opus track */
AudioSource *audio_source;
int audio_track;
gboolean audio_track_open;
GThread *audio_send_thread;
guint32 audio_rtp_timestamp;
guint64 audio_pts_base;
guint64 audio_last_pts;
gboolean audio_pts_valid;
GQueue *pending_local_ice;
GQueue *pending_remote_ice;
} RtcClient;
......@@ -61,6 +100,106 @@ static RtcClient *client_from_app(AppState *app)
return app ? (RtcClient *)app->rtc_client : NULL;
}
static RtcH264Frame *rtc_h264_frame_new(const guint8 *data, gsize size,
gboolean is_idr, guint64 pts_ns)
{
RtcH264Frame *frame = g_new(RtcH264Frame, 1);
frame->data = g_memdup2(data, size);
frame->size = size;
frame->is_idr = is_idr;
frame->pts_ns = pts_ns;
return frame;
}
static void rtc_h264_frame_free(RtcH264Frame *frame)
{
if (!frame)
return;
g_free(frame->data);
g_free(frame);
}
static void frame_queue_init(RtcFrameQueue *q, guint max_depth)
{
g_mutex_init(&q->lock);
g_cond_init(&q->not_empty);
g_queue_init(&q->queue);
q->max_depth = max_depth;
q->stopping = FALSE;
q->dropped_total = 0;
}
static void frame_queue_fini(RtcFrameQueue *q)
{
RtcH264Frame *frame;
g_mutex_lock(&q->lock);
while ((frame = g_queue_pop_head(&q->queue)) != NULL)
rtc_h264_frame_free(frame);
g_mutex_unlock(&q->lock);
g_queue_clear(&q->queue);
g_cond_clear(&q->not_empty);
g_mutex_clear(&q->lock);
}
static void frame_queue_stop(RtcFrameQueue *q)
{
g_mutex_lock(&q->lock);
q->stopping = TRUE;
g_cond_broadcast(&q->not_empty);
g_mutex_unlock(&q->lock);
}
static void frame_queue_clear(RtcClient *client)
{
RtcFrameQueue *q = &client->frame_queue;
RtcH264Frame *frame;
g_mutex_lock(&q->lock);
while ((frame = g_queue_pop_head(&q->queue)) != NULL)
rtc_h264_frame_free(frame);
g_mutex_unlock(&q->lock);
}
static void frame_queue_push(RtcClient *client, RtcH264Frame *frame)
{
RtcFrameQueue *q = &client->frame_queue;
RtcH264Frame *old;
g_mutex_lock(&q->lock);
if (q->stopping) {
g_mutex_unlock(&q->lock);
rtc_h264_frame_free(frame);
return;
}
while (g_queue_get_length(&q->queue) >= q->max_depth) {
old = g_queue_pop_head(&q->queue);
q->dropped_total++;
rtc_h264_frame_free(old);
}
g_queue_push_tail(&q->queue, frame);
g_cond_signal(&q->not_empty);
g_mutex_unlock(&q->lock);
}
static RtcH264Frame *frame_queue_pop(RtcClient *client, gint timeout_ms)
{
RtcFrameQueue *q = &client->frame_queue;
RtcH264Frame *frame = NULL;
gint64 end_us = g_get_monotonic_time() + (gint64)timeout_ms * 1000;
g_mutex_lock(&q->lock);
while (g_queue_is_empty(&q->queue) && !q->stopping) {
if (!g_cond_wait_until(&q->not_empty, &q->lock, end_us))
break;
}
if (!g_queue_is_empty(&q->queue))
frame = g_queue_pop_head(&q->queue);
g_mutex_unlock(&q->lock);
return frame;
}
static void free_pending_remote_ice(gpointer data)
{
PendingRemoteIce *ice = data;
......@@ -177,16 +316,68 @@ static void flush_local_ice(RtcClient *client)
ws_signaling_service_burst(1);
}
static gchar *sdp_replace_first(const gchar *haystack, const gchar *needle,
const gchar *replacement)
{
const gchar *pos;
if (!haystack || !needle || !replacement)
return g_strdup(haystack);
pos = strstr(haystack, needle);
if (!pos)
return g_strdup(haystack);
{
gsize prefix = (gsize)(pos - haystack);
GString *out = g_string_new_len(haystack, prefix);
g_string_append(out, replacement);
g_string_append(out, pos + strlen(needle));
return g_string_free(out, FALSE);
}
}
static gchar *sdp_patch_h264_answer(const gchar *sdp, const gchar *sprop)
{
gchar *out;
gchar *needle;
gchar *insert;
if (!sdp)
return NULL;
out = g_strdup(sdp);
if (!sprop || !sprop[0] || strstr(out, "sprop-parameter-sets"))
return out;
needle = g_strdup_printf("profile-level-id=%s", WEBRTCPUSH_H264_PROFILE_LEVEL_ID);
insert = g_strdup_printf("profile-level-id=%s;sprop-parameter-sets=%s",
WEBRTCPUSH_H264_PROFILE_LEVEL_ID, sprop);
if (strstr(out, needle)) {
gchar *replaced = sdp_replace_first(out, needle, insert);
g_free(out);
out = replaced;
my_zlog_info("libdatachannel: SDP answer patched with sprop-parameter-sets");
} else {
my_zlog_warn("libdatachannel: SDP answer missing profile-level-id, sprop not patched");
}
g_free(needle);
g_free(insert);
return out;
}
static void RTC_API on_local_description(int pc, const char *sdp,
const char *type, void *ptr)
{
RtcClient *client = ptr;
gchar *json;
gchar *patched = NULL;
const gchar *sprop;
(void)pc;
if (!client || !sdp || g_strcmp0(type, "answer") != 0)
return;
json = build_description_json(client->app, "answer", sdp);
sprop = mpp_h264_source_get_sprop(client->h264_source);
patched = sdp_patch_h264_answer(sdp, sprop);
json = build_description_json(client->app, "answer", patched ? patched : sdp);
g_free(patched);
if (!json) {
my_zlog_error("libdatachannel: cannot send answer without memberId");
return;
......@@ -238,6 +429,33 @@ static const char *rtc_state_name(rtcState state)
}
}
static void signal_track_ready(RtcClient *client)
{
if (!client)
return;
g_cond_broadcast(&client->track_cond);
}
static gboolean request_idr(RtcClient *client, gboolean bypass_throttle)
{
gint64 now;
if (!client)
return FALSE;
now = g_get_monotonic_time();
g_mutex_lock(&client->lock);
if (!bypass_throttle && client->last_idr_request_us > 0 &&
now - client->last_idr_request_us <
(gint64)WEBRTCPUSH_PLI_IDR_THROTTLE_MS * 1000) {
g_mutex_unlock(&client->lock);
return FALSE;
}
client->need_idr = TRUE;
client->last_idr_request_us = now;
g_mutex_unlock(&client->lock);
return TRUE;
}
static void RTC_API on_state_change(int pc, rtcState state, void *ptr)
{
RtcClient *client = ptr;
......@@ -247,9 +465,9 @@ static void RTC_API on_state_change(int pc, rtcState state, void *ptr)
return;
if (state == RTC_CONNECTED) {
g_mutex_lock(&client->lock);
client->need_idr = TRUE;
client->source_pts_valid = FALSE;
g_mutex_unlock(&client->lock);
request_idr(client, TRUE);
}
}
......@@ -261,11 +479,19 @@ static void RTC_API on_track_open(int track, void *ptr)
g_mutex_lock(&client->lock);
if (client->track == track) {
client->track_open = TRUE;
client->need_idr = TRUE;
client->source_pts_valid = FALSE;
}
if (client->audio_track == track) {
client->audio_track_open = TRUE;
client->audio_pts_valid = FALSE;
}
g_mutex_unlock(&client->lock);
my_zlog_info("libdatachannel: H264 track open, requesting SPS/PPS+IDR");
frame_queue_clear(client);
request_idr(client, TRUE);
signal_track_ready(client);
my_zlog_info("libdatachannel: track open (video=%d audio=%d), requesting IDR",
client->track == track ? 1 : 0,
client->audio_track == track ? 1 : 0);
}
static void RTC_API on_track_closed(int track, void *ptr)
......@@ -276,8 +502,14 @@ static void RTC_API on_track_closed(int track, void *ptr)
g_mutex_lock(&client->lock);
if (client->track == track)
client->track_open = FALSE;
if (client->audio_track == track)
client->audio_track_open = FALSE;
g_mutex_unlock(&client->lock);
my_zlog_warn("libdatachannel: H264 track closed");
frame_queue_clear(client);
signal_track_ready(client);
my_zlog_warn("libdatachannel: track closed (video=%d audio=%d)",
client->track == track ? 1 : 0,
client->audio_track == track ? 1 : 0);
}
static void RTC_API on_track_error(int track, const char *error, void *ptr)
......@@ -293,14 +525,16 @@ static void RTC_API on_pli(int track, void *ptr)
int dropped = 0;
if (!client)
return;
if (!request_idr(client, FALSE))
my_zlog_debug("libdatachannel: PLI throttled, skip duplicate IDR request");
g_mutex_lock(&client->lock);
client->need_idr = TRUE;
client->source_pts_valid = FALSE;
g_mutex_unlock(&client->lock);
g_mutex_lock(&client->send_lock);
if (client->track == track)
dropped = rtcClearPacingQueue(track);
g_mutex_unlock(&client->send_lock);
frame_queue_clear(client);
my_zlog_info("libdatachannel: PLI received, dropped %d queued RTP packet(s), next is IDR",
dropped > 0 ? dropped : 0);
}
......@@ -366,11 +600,11 @@ static void ramp_bitrate_toward_ceiling(RtcClient *client, int track)
diff = (cur > tgt) ? (cur - tgt) : (tgt - cur);
if (cur < tgt) {
step = diff / 8;
if (step < 50000)
step = 50000;
if (step > 150000)
step = 150000;
step = diff / 4;
if (step < 100000)
step = 100000;
if (step > 300000)
step = 300000;
next = cur + step;
if (next > tgt)
next = tgt;
......@@ -412,11 +646,16 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
current = client->target_bitrate;
client->remb_ceiling = ceiling;
if (ceiling < current) {
severe_down = ((guint64)ceiling * 100) < ((guint64)current * 75);
if (!severe_down && (current - ceiling) < WEBRTCPUSH_REMB_DOWN_MIN_STEP) {
g_mutex_unlock(&client->lock);
return;
}
client->target_bitrate = ceiling;
next = ceiling;
severe_down = ((guint64)ceiling * 100) < ((guint64)current * 75);
if (severe_down) {
client->need_idr = TRUE;
client->last_idr_request_us = g_get_monotonic_time();
client->source_pts_valid = FALSE;
}
}
......@@ -425,8 +664,10 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
if (next) {
g_mutex_lock(&client->send_lock);
if (client->track == track) {
if (severe_down)
if (severe_down) {
dropped = rtcClearPacingQueue(track);
frame_queue_clear(client);
}
rtcSetPacingBitrate(track, pacing_bitrate_for_encoder(next));
}
g_mutex_unlock(&client->send_lock);
......@@ -449,12 +690,19 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
static void maybe_ramp_bitrate(RtcClient *client, int track, gint64 now_us)
{
guint cur;
guint tgt;
gint64 interval;
if (!client || track < 0)
return;
g_mutex_lock(&client->lock);
if (now_us - client->last_bitrate_ramp_us <
(gint64)WEBRTCPUSH_BITRATE_RAMP_MS * 1000) {
cur = client->target_bitrate;
tgt = client->remb_ceiling;
interval = (tgt > cur) ? (gint64)WEBRTCPUSH_BITRATE_RAMP_UP_MS
: (gint64)WEBRTCPUSH_BITRATE_RAMP_DOWN_MS;
if (now_us - client->last_bitrate_ramp_us < interval * 1000) {
g_mutex_unlock(&client->lock);
return;
}
......@@ -472,78 +720,155 @@ static guint32 pts_delta_to_rtp(guint64 delta_ns)
return (guint32)ticks;
}
static gpointer sender_thread_main(gpointer data)
static gpointer pull_thread_main(gpointer data)
{
RtcClient *client = data;
const gint64 frame_us = 1000000 / WEBRTCPUSH_H264_FPS;
const guint32 timestamp_step = 90000 / WEBRTCPUSH_H264_FPS;
gint64 next_frame_us = 0;
const gint pull_ms = (1000 / WEBRTCPUSH_H264_FPS) + 100;
guint pull_fail_streak = 0;
guint idr_discard_streak = 0;
gint64 last_pull_warn_us = 0;
for (;;) {
gboolean stopping;
gboolean open;
gboolean track_open;
gboolean force_idr;
int track;
guint32 timestamp;
const guint8 *frame;
gboolean need_idr_now;
const guint8 *frame_data;
size_t frame_size;
gboolean is_idr;
guint64 source_pts = G_MAXUINT64;
gint64 now;
guint64 source_pts;
RtcH264Frame *queued;
g_mutex_lock(&client->lock);
stopping = client->stopping;
open = client->track_open;
track = client->track;
force_idr = client->need_idr;
timestamp = client->rtp_timestamp;
track_open = client->track_open && client->track >= 0;
force_idr = track_open && client->need_idr;
g_mutex_unlock(&client->lock);
if (stopping)
break;
if (!open || track < 0) {
next_frame_us = 0;
g_usleep(10000);
continue;
{
gint pull_timeout = force_idr ? 2500 : pull_ms;
if (!mpp_h264_source_pull(client->h264_source, force_idr, &frame_data,
&frame_size, &is_idr, &source_pts,
pull_timeout)) {
gint64 now_us = g_get_monotonic_time();
pull_fail_streak++;
if (now_us - last_pull_warn_us >= 3000000 ||
pull_fail_streak == 1 || pull_fail_streak % 20 == 0) {
my_zlog_warn("libdatachannel: live H264 frame pull timeout (streak=%u)",
pull_fail_streak);
last_pull_warn_us = now_us;
}
if (pull_fail_streak >= 20) {
if (mpp_h264_source_recover(client->h264_source))
pull_fail_streak = 0;
g_usleep(200000);
} else {
g_usleep(5000);
}
continue;
}
pull_fail_streak = 0;
}
now = g_get_monotonic_time();
maybe_ramp_bitrate(client, track, now);
if (next_frame_us == 0 || now - next_frame_us > frame_us * 4)
next_frame_us = now;
if (now < next_frame_us) {
g_usleep((gulong)(next_frame_us - now));
if (!track_open)
continue;
}
{
gint pull_timeout = force_idr ? 4000 : 120;
g_mutex_lock(&client->lock);
need_idr_now = client->need_idr;
g_mutex_unlock(&client->lock);
if (!mpp_h264_source_pull(client->h264_source, force_idr, &frame, &frame_size,
&is_idr, &source_pts, pull_timeout)) {
my_zlog_warn("libdatachannel: live H264 frame pull timeout");
next_frame_us = now + frame_us / 2;
continue;
if (need_idr_now && !is_idr) {
idr_discard_streak++;
if (idr_discard_streak >= WEBRTCPUSH_H264_FPS) {
g_mutex_lock(&client->lock);
client->need_idr = FALSE;
g_mutex_unlock(&client->lock);
idr_discard_streak = 0;
my_zlog_warn("libdatachannel: IDR wait >2s, resume with latest frames");
}
continue;
}
idr_discard_streak = 0;
queued = rtc_h264_frame_new(frame_data, frame_size, is_idr, source_pts);
frame_queue_push(client, queued);
}
return NULL;
}
static gpointer send_thread_main(gpointer data)
{
RtcClient *client = data;
const guint32 timestamp_step = 90000 / WEBRTCPUSH_H264_FPS;
const gint pop_ms = (1000 / WEBRTCPUSH_H264_FPS) + 200;
guint send_fail_streak = 0;
for (;;) {
gboolean stopping;
int track;
guint target_bps;
guint remb_bps;
guint queue_dropped;
guint32 timestamp;
RtcH264Frame *frame;
gint64 now;
g_mutex_lock(&client->lock);
stopping = client->stopping;
g_mutex_unlock(&client->lock);
if (stopping)
break;
g_mutex_lock(&client->lock);
while (!client->stopping && (!client->track_open || client->track < 0))
g_cond_wait(&client->track_cond, &client->lock);
stopping = client->stopping;
track = client->track;
g_mutex_unlock(&client->lock);
if (stopping)
break;
if (track < 0)
continue;
now = g_get_monotonic_time();
maybe_ramp_bitrate(client, track, now);
g_mutex_lock(&client->lock);
if (client->need_idr && !is_idr) {
if (client->last_stats_log_us == 0 ||
now - client->last_stats_log_us >=
(gint64)WEBRTCPUSH_STATS_LOG_INTERVAL_MS * 1000) {
target_bps = client->target_bitrate;
remb_bps = client->remb_ceiling;
client->last_stats_log_us = now;
g_mutex_unlock(&client->lock);
my_zlog_warn("libdatachannel: discard non-IDR while waiting for resync");
next_frame_us = g_get_monotonic_time();
queue_dropped = client->frame_queue.dropped_total;
my_zlog_info("libdatachannel: stats encoder=%u kbps remb_ceiling=%u kbps queue_dropped=%u",
target_bps / 1000, remb_bps / 1000, queue_dropped);
} else {
g_mutex_unlock(&client->lock);
}
frame = frame_queue_pop(client, pop_ms);
if (!frame) {
if (client->frame_queue.stopping)
break;
continue;
}
if (source_pts != G_MAXUINT64) {
if (!client->source_pts_valid || source_pts < client->last_source_pts) {
client->source_pts_base = source_pts;
client->last_source_pts = source_pts;
g_mutex_lock(&client->lock);
if (frame->pts_ns != G_MAXUINT64) {
if (!client->source_pts_valid || frame->pts_ns < client->last_source_pts) {
client->source_pts_base = frame->pts_ns;
client->last_source_pts = frame->pts_ns;
client->rtp_timestamp_base = client->rtp_timestamp;
client->source_pts_valid = TRUE;
}
timestamp = client->rtp_timestamp_base +
pts_delta_to_rtp(source_pts - client->source_pts_base);
client->last_source_pts = source_pts;
pts_delta_to_rtp(frame->pts_ns - client->source_pts_base);
client->last_source_pts = frame->pts_ns;
} else {
timestamp = client->rtp_timestamp;
}
......@@ -552,20 +877,109 @@ static gpointer sender_thread_main(gpointer data)
g_mutex_lock(&client->send_lock);
if (!rtcIsOpen(track) ||
rtcSetTrackRtpTimestamp(track, timestamp) < 0 ||
rtcSendMessage(track, (const char *)frame, (int)frame_size) < 0) {
rtcSendMessage(track, (const char *)frame->data, (int)frame->size) < 0) {
g_mutex_unlock(&client->send_lock);
my_zlog_warn("libdatachannel: rtcSendMessage failed");
g_usleep(20000);
send_fail_streak++;
my_zlog_warn("libdatachannel: rtcSendMessage failed (%u)",
send_fail_streak);
if (send_fail_streak >= WEBRTCPUSH_SEND_FAIL_RESET) {
my_zlog_error("libdatachannel: send failed %u times, waiting for track reopen",
send_fail_streak);
g_mutex_lock(&client->lock);
client->track_open = FALSE;
client->need_idr = TRUE;
g_mutex_unlock(&client->lock);
signal_track_ready(client);
send_fail_streak = 0;
}
rtc_h264_frame_free(frame);
g_usleep(send_fail_streak >= 10 ? 200000 : 20000U * send_fail_streak);
continue;
}
g_mutex_unlock(&client->send_lock);
send_fail_streak = 0;
g_mutex_lock(&client->lock);
client->rtp_timestamp = timestamp + timestamp_step;
if (is_idr)
if (frame->is_idr)
client->need_idr = FALSE;
g_mutex_unlock(&client->lock);
next_frame_us += frame_us;
rtc_h264_frame_free(frame);
}
return NULL;
}
static gpointer audio_send_thread_main(gpointer data)
{
RtcClient *client = data;
/* Opus 20ms 帧 → 50fps;RTP 时间戳步进 = 48000/50 = 960 */
const guint32 timestamp_step = WEBRTCPUSH_OPUS_CLOCKRATE / 50;
const gint pull_ms = 200;
guint send_fail_streak = 0;
for (;;) {
gboolean stopping;
int track;
const uint8_t *frame_data;
size_t frame_size;
guint64 pts_ns;
guint32 timestamp;
g_mutex_lock(&client->lock);
stopping = client->stopping;
g_mutex_unlock(&client->lock);
if (stopping)
break;
g_mutex_lock(&client->lock);
while (!client->stopping && (!client->audio_track_open || client->audio_track < 0))
g_cond_wait(&client->track_cond, &client->lock);
stopping = client->stopping;
track = client->audio_track;
g_mutex_unlock(&client->lock);
if (stopping)
break;
if (track < 0)
continue;
if (!audio_source_pull(client->audio_source, &frame_data, &frame_size,
&pts_ns, pull_ms))
continue;
g_mutex_lock(&client->lock);
if (!client->audio_pts_valid || pts_ns < client->audio_last_pts) {
client->audio_pts_base = pts_ns;
client->audio_last_pts = pts_ns;
client->audio_pts_valid = TRUE;
}
timestamp = client->audio_rtp_timestamp +
(guint32)((pts_ns - client->audio_pts_base) *
(guint64)WEBRTCPUSH_OPUS_CLOCKRATE / G_GUINT64_CONSTANT(1000000000));
client->audio_last_pts = pts_ns;
g_mutex_unlock(&client->lock);
g_mutex_lock(&client->send_lock);
if (!rtcIsOpen(track) ||
rtcSetTrackRtpTimestamp(track, timestamp) < 0 ||
rtcSendMessage(track, (const char *)frame_data, (int)frame_size) < 0) {
g_mutex_unlock(&client->send_lock);
send_fail_streak++;
if (send_fail_streak >= WEBRTCPUSH_SEND_FAIL_RESET) {
g_mutex_lock(&client->lock);
client->audio_track_open = FALSE;
g_mutex_unlock(&client->lock);
send_fail_streak = 0;
}
g_usleep(20000);
continue;
}
g_mutex_unlock(&client->send_lock);
send_fail_streak = 0;
g_mutex_lock(&client->lock);
client->audio_rtp_timestamp = timestamp + timestamp_step;
g_mutex_unlock(&client->lock);
}
return NULL;
}
......@@ -579,44 +993,73 @@ static void offer_video_clear(OfferVideo *video)
video->mline_index = -1;
}
static gboolean parse_video_offer(const char *sdp, OfferVideo *video)
static void offer_audio_clear(OfferAudio *audio)
{
g_free(audio->mid);
memset(audio, 0, sizeof(*audio));
audio->payload_type = -1;
}
static gboolean parse_offer(const char *sdp, OfferVideo *video, OfferAudio *audio)
{
gchar **lines;
gboolean in_video = FALSE;
gboolean in_audio = FALSE;
gint mline = -1;
gint i;
memset(video, 0, sizeof(*video));
video->payload_type = -1;
video->mline_index = -1;
memset(audio, 0, sizeof(*audio));
audio->payload_type = -1;
audio->found = FALSE;
lines = g_strsplit(sdp, "\n", -1);
for (i = 0; lines[i]; i++) {
gchar *line = g_strchomp(lines[i]);
if (g_str_has_prefix(line, "m=")) {
mline++;
in_video = g_str_has_prefix(line, "m=video ");
in_audio = g_str_has_prefix(line, "m=audio ");
if (in_video)
video->mline_index = mline;
if (in_audio)
audio->found = TRUE;
continue;
}
if (!video->ice_ufrag && g_str_has_prefix(line, "a=ice-ufrag:"))
video->ice_ufrag = g_strdup(line + strlen("a=ice-ufrag:"));
if (!in_video)
continue;
if (g_str_has_prefix(line, "a=mid:")) {
g_free(video->mid);
video->mid = g_strdup(line + strlen("a=mid:"));
} else if (g_str_has_prefix(line, "a=rtpmap:")) {
gint pt = -1;
gchar codec[32] = {0};
if (sscanf(line, "a=rtpmap:%d %31[^/]/90000", &pt, codec) == 2 &&
!g_ascii_strcasecmp(codec, "H264") && video->payload_type < 0)
video->payload_type = pt;
if (in_video) {
if (g_str_has_prefix(line, "a=mid:")) {
g_free(video->mid);
video->mid = g_strdup(line + strlen("a=mid:"));
} else if (g_str_has_prefix(line, "a=rtpmap:")) {
gint pt = -1;
gchar codec[32] = {0};
if (sscanf(line, "a=rtpmap:%d %31[^/]/90000", &pt, codec) == 2 &&
!g_ascii_strcasecmp(codec, "H264") && video->payload_type < 0)
video->payload_type = pt;
}
}
if (in_audio) {
if (g_str_has_prefix(line, "a=mid:")) {
g_free(audio->mid);
audio->mid = g_strdup(line + strlen("a=mid:"));
} else if (g_str_has_prefix(line, "a=rtpmap:")) {
gint pt = -1;
gchar codec[32] = {0};
if (sscanf(line, "a=rtpmap:%d %31[^/]/48000", &pt, codec) == 2 &&
!g_ascii_strcasecmp(codec, "opus") && audio->payload_type < 0)
audio->payload_type = pt;
}
}
}
g_strfreev(lines);
if (!video->mid && video->mline_index >= 0)
video->mid = g_strdup_printf("%d", video->mline_index);
if (audio->found && !audio->mid)
audio->mid = g_strdup_printf("%d", video->mline_index + 1);
return video->payload_type >= 0 && video->mid && video->ice_ufrag;
}
......@@ -631,13 +1074,17 @@ static void destroy_peer(RtcClient *client)
{
int pc;
int track;
int audio_track;
g_mutex_lock(&client->lock);
pc = client->pc;
track = client->track;
audio_track = client->audio_track;
client->pc = -1;
client->track = -1;
client->track_open = FALSE;
client->audio_track = -1;
client->audio_track_open = FALSE;
client->answer_sent = FALSE;
client->need_idr = TRUE;
client->source_pts_valid = FALSE;
......@@ -645,12 +1092,18 @@ static void destroy_peer(RtcClient *client)
g_clear_pointer(&client->remote_ice_ufrag, g_free);
g_clear_pointer(&client->video_mid, g_free);
g_mutex_unlock(&client->lock);
frame_queue_clear(client);
signal_track_ready(client);
g_mutex_lock(&client->send_lock);
if (track >= 0) {
rtcClose(track);
rtcDeleteTrack(track);
}
if (audio_track >= 0) {
rtcClose(audio_track);
rtcDeleteTrack(audio_track);
}
if (pc >= 0) {
rtcClosePeerConnection(pc);
rtcDeletePeerConnection(pc);
......@@ -677,7 +1130,9 @@ static gboolean setup_track(RtcClient *client, int pc,
track_init.name = "car-video";
track_init.msid = "car-stream";
track_init.trackId = "car-video";
track_init.profile = "profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1";
track_init.profile =
"profile-level-id=" WEBRTCPUSH_H264_PROFILE_LEVEL_ID
";packetization-mode=1;level-asymmetry-allowed=1";
track = rtcAddTrackEx(pc, &track_init);
if (track < 0)
return FALSE;
......@@ -694,7 +1149,7 @@ static gboolean setup_track(RtcClient *client, int pc,
packetizer.clockRate = 90000;
packetizer.sequenceNumber = (guint16)g_random_int();
packetizer.timestamp = g_random_int();
packetizer.maxFragmentSize = 1150;
packetizer.maxFragmentSize = WEBRTCPUSH_RTP_MAX_FRAGMENT;
packetizer.nalSeparator = RTC_NAL_SEPARATOR_START_SEQUENCE;
if (rtcSetH264Packetizer(track, &packetizer) < 0 ||
......@@ -718,6 +1173,57 @@ static gboolean setup_track(RtcClient *client, int pc,
return TRUE;
}
static gboolean setup_audio_track(RtcClient *client, int pc,
const OfferAudio *audio, int *track_out)
{
rtcTrackInit track_init;
rtcPacketizerInit packetizer;
guint32 ssrc = g_random_int();
int track;
if (ssrc == 0)
ssrc = 1;
memset(&track_init, 0, sizeof(track_init));
track_init.direction = RTC_DIRECTION_SENDONLY;
track_init.codec = RTC_CODEC_OPUS;
track_init.payloadType = audio->payload_type;
track_init.ssrc = ssrc;
track_init.mid = audio->mid;
track_init.name = "car-audio";
track_init.msid = "car-stream";
track_init.trackId = "car-audio";
track = rtcAddTrackEx(pc, &track_init);
if (track < 0)
return FALSE;
rtcSetUserPointer(track, client);
rtcSetOpenCallback(track, on_track_open);
rtcSetClosedCallback(track, on_track_closed);
rtcSetErrorCallback(track, on_track_error);
memset(&packetizer, 0, sizeof(packetizer));
packetizer.ssrc = ssrc;
packetizer.cname = "car-audio";
packetizer.payloadType = (guint8)audio->payload_type;
packetizer.clockRate = WEBRTCPUSH_OPUS_CLOCKRATE;
packetizer.sequenceNumber = (guint16)g_random_int();
packetizer.timestamp = g_random_int();
if (rtcSetOpusPacketizer(track, &packetizer) < 0 ||
rtcChainRtcpSrReporter(track) < 0 ||
rtcChainRtcpNackResponder(track, WEBRTCPUSH_NACK_PACKETS) < 0) {
rtcDeleteTrack(track);
return FALSE;
}
g_mutex_lock(&client->lock);
client->audio_rtp_timestamp = packetizer.timestamp;
client->audio_pts_valid = FALSE;
g_mutex_unlock(&client->lock);
*track_out = track;
return TRUE;
}
static void flush_remote_ice(RtcClient *client)
{
for (;;) {
......@@ -757,6 +1263,7 @@ int rtc_client_start(AppState *app)
client->pending_remote_ice = g_queue_new();
g_mutex_init(&client->lock);
g_mutex_init(&client->send_lock);
g_cond_init(&client->track_cond);
client->h264_source = mpp_h264_source_start(video_device, &error_message);
if (!client->h264_source) {
......@@ -767,17 +1274,34 @@ int rtc_client_start(AppState *app)
g_queue_free(client->pending_remote_ice);
g_mutex_clear(&client->send_lock);
g_mutex_clear(&client->lock);
g_cond_clear(&client->track_cond);
g_free(client);
return -1;
}
app->rtc_client = client;
client->sender_thread = g_thread_new("rtc-h264-send", sender_thread_main, client);
frame_queue_init(&client->frame_queue, WEBRTCPUSH_SEND_QUEUE_DEPTH);
client->pull_thread = g_thread_new("rtc-h264-pull", pull_thread_main, client);
client->send_thread = g_thread_new("rtc-h264-send", send_thread_main, client);
mpp_h264_source_set_bitrate(client->h264_source, client->target_bitrate);
my_zlog_info("libdatachannel: MPP live source started device=%s mpp=%d fps=%d",
/* 音频源(可选;app->enable_audio 由 setup_alsa_device 探测决定) */
if (app->enable_audio && app->audio_alsa_device && app->audio_alsa_device[0]) {
char *audio_err = NULL;
client->audio_source = audio_source_start(app->audio_alsa_device, &audio_err);
if (client->audio_source) {
client->audio_send_thread = g_thread_new("rtc-opus-send", audio_send_thread_main, client);
my_zlog_info("libdatachannel: audio source started device=%s", app->audio_alsa_device);
} else {
my_zlog_warn("libdatachannel: audio source start failed: %s", audio_err ? audio_err : "?");
g_free(audio_err);
}
}
my_zlog_info("libdatachannel: MPP live source started device=%s mpp=%d fps=%d threads=pull+send queue=%u",
video_device,
mpp_h264_source_is_mpp_encoder(client->h264_source) ? 1 : 0,
WEBRTCPUSH_H264_FPS);
WEBRTCPUSH_H264_FPS,
WEBRTCPUSH_SEND_QUEUE_DEPTH);
return 0;
}
......@@ -797,9 +1321,17 @@ void rtc_client_stop(AppState *app)
destroy_peer(client);
g_mutex_lock(&client->lock);
client->stopping = TRUE;
g_cond_broadcast(&client->track_cond);
g_mutex_unlock(&client->lock);
if (client->sender_thread)
g_thread_join(client->sender_thread);
frame_queue_stop(&client->frame_queue);
if (client->pull_thread)
g_thread_join(client->pull_thread);
if (client->send_thread)
g_thread_join(client->send_thread);
if (client->audio_send_thread)
g_thread_join(client->audio_send_thread);
if (client->audio_source)
audio_source_stop(client->audio_source);
g_queue_free_full(client->pending_remote_ice, free_pending_remote_ice);
g_mutex_lock(&client->lock);
......@@ -807,6 +1339,8 @@ void rtc_client_stop(AppState *app)
g_mutex_unlock(&client->lock);
g_queue_free(client->pending_local_ice);
mpp_h264_source_stop(client->h264_source);
frame_queue_fini(&client->frame_queue);
g_cond_clear(&client->track_cond);
g_mutex_clear(&client->send_lock);
g_mutex_clear(&client->lock);
app->rtc_client = NULL;
......@@ -817,16 +1351,19 @@ int rtc_client_handle_offer(AppState *app, const char *sdp)
{
RtcClient *client = client_from_app(app);
OfferVideo video;
OfferAudio audio;
rtcConfiguration config;
int pc;
int track = -1;
int audio_track = -1;
gboolean duplicate;
if (!client || !sdp)
return -1;
if (!parse_video_offer(sdp, &video)) {
if (!parse_offer(sdp, &video, &audio)) {
my_zlog_error("libdatachannel: phone offer has no usable H264 video");
offer_video_clear(&video);
offer_audio_clear(&audio);
return -1;
}
......@@ -836,6 +1373,7 @@ int rtc_client_handle_offer(AppState *app, const char *sdp)
g_mutex_unlock(&client->lock);
if (duplicate) {
offer_video_clear(&video);
offer_audio_clear(&audio);
return 0;
}
......@@ -844,7 +1382,7 @@ int rtc_client_handle_offer(AppState *app, const char *sdp)
config.iceServers = s_ice_servers;
config.iceServersCount = (int)G_N_ELEMENTS(s_ice_servers);
config.certificateType = RTC_CERTIFICATE_ECDSA;
config.forceMediaTransport = TRUE;
config.forceMediaTransport = TRUE; /* 预分配 SRTP 媒体通道,与 ENABLE_DATACHANNEL=0 一致 */
config.mtu = 1200;
pc = rtcCreatePeerConnection(&config);
if (pc < 0) {
......@@ -859,20 +1397,32 @@ int rtc_client_handle_offer(AppState *app, const char *sdp)
if (!setup_track(client, pc, &video, &track)) {
rtcDeletePeerConnection(pc);
offer_video_clear(&video);
offer_audio_clear(&audio);
my_zlog_error("libdatachannel: failed to create H264 track/handlers");
return -1;
}
/* 若手机 offer 包含 audio m-line 且设备端音频源已启动,建 Opus track */
if (audio.found && audio.payload_type >= 0 && client->audio_source) {
if (setup_audio_track(client, pc, &audio, &audio_track)) {
my_zlog_info("libdatachannel: Opus audio track created mid=%s pt=%d",
audio.mid, audio.payload_type);
} else {
my_zlog_warn("libdatachannel: failed to create Opus audio track");
}
}
g_mutex_lock(&client->lock);
client->pc = pc;
client->track = track;
client->audio_track = audio_track;
client->video_mline_index = video.mline_index;
client->video_mid = g_strdup(video.mid);
client->remote_ice_ufrag = g_strdup(video.ice_ufrag);
client->target_bitrate = WEBRTCPUSH_INITIAL_BITRATE;
client->remb_ceiling = WEBRTCPUSH_INITIAL_BITRATE;
client->last_bitrate_ramp_us = g_get_monotonic_time();
client->need_idr = TRUE;
request_idr(client, TRUE);
client->source_pts_valid = FALSE;
g_mutex_unlock(&client->lock);
......@@ -881,11 +1431,13 @@ int rtc_client_handle_offer(AppState *app, const char *sdp)
if (rtcSetRemoteDescription(pc, sdp, "offer") < 0) {
my_zlog_error("libdatachannel: rtcSetRemoteDescription failed");
offer_video_clear(&video);
offer_audio_clear(&audio);
destroy_peer(client);
return -1;
}
flush_remote_ice(client);
offer_video_clear(&video);
offer_audio_clear(&audio);
return 0;
}
......
......@@ -14,18 +14,43 @@
/* 正常固定 24fps;弱网以降码率为主,后续若启用动态帧率也不得低于 22fps。 */
#define WEBRTCPUSH_H264_FPS 24
#define WEBRTCPUSH_H264_MIN_FPS 22
#define WEBRTCPUSH_H264_GOP (WEBRTCPUSH_H264_FPS * 3)
#define WEBRTCPUSH_H264_GOP (WEBRTCPUSH_H264_FPS) /* 1s 一个 IDR */
/* 与 gst_webrtc_pipeline / jywy 浏览器推流对齐的码率策略 */
#define WEBRTCPUSH_INITIAL_BITRATE 1400000U /* 起播 1.4 Mbps */
#define WEBRTCPUSH_INITIAL_BITRATE 1800000U
#define WEBRTCPUSH_MIN_BITRATE 500000U
#define WEBRTCPUSH_MAX_BITRATE 3200000U
#define WEBRTCPUSH_REMB_UTIL_PERCENT 82U /* 链路容量扣除音频/RTP 余量 */
#define WEBRTCPUSH_BITRATE_RAMP_MS 1000U /* 每秒向目标码率逼近一档 */
#define WEBRTCPUSH_REMB_UTIL_PERCENT 85U
#define WEBRTCPUSH_REMB_DOWN_MIN_STEP 50000U /* 非急跌时 <50kbps 不调编码器 */
#define WEBRTCPUSH_BITRATE_RAMP_UP_MS 500U /* 升码更快恢复画质 */
#define WEBRTCPUSH_BITRATE_RAMP_DOWN_MS 1000U
#define WEBRTCPUSH_PACING_HEADROOM_PERCENT 108U
#define WEBRTCPUSH_PACING_INTERVAL_MS 5U
#define WEBRTCPUSH_PACING_HEADROOM_PERCENT 120U
#define WEBRTCPUSH_PACING_MAX_BITRATE 3750000U
#define WEBRTCPUSH_NACK_PACKETS 1024U
/* 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 200U
/* 周期性推流指标日志间隔 */
#define WEBRTCPUSH_STATS_LOG_INTERVAL_MS 8000U
/* 连续发送失败退避与上限 */
#define WEBRTCPUSH_SEND_FAIL_RESET 30U
/* H.264 Baseline@L3.1,兼容性最好 */
#define WEBRTCPUSH_H264_PROFILE_LEVEL_ID "42e01f"
/* 拉帧线程与发送线程之间的有界队列(leaky,满则丢最旧帧) */
#define WEBRTCPUSH_SEND_QUEUE_DEPTH 4U
/* 管道缓冲:运动场景需适度缓存,过低易 pull timeout / 糊屏 */
#define WEBRTCPUSH_MPP_PRE_ENC_BUFFERS 3 /* 编码前 leaky */
#define WEBRTCPUSH_MPP_POST_ENC_BUFFERS 2 /* 编码后保留 AU */
#define WEBRTCPUSH_APPSINK_MAX_BUFFERS 2
/* 视频采集:auto | /dev/videoN | test(videotestsrc 测试图案) */
#define WEBRTCPUSH_VIDEO_DEVICE "auto"
......@@ -36,6 +61,12 @@
*/
#define WEBRTCPUSH_ALSA_DEVICE "auto"
/* Opus 音频编码参数(上行麦克风推流) */
#define WEBRTCPUSH_OPUS_BITRATE 64000U
#define WEBRTCPUSH_OPUS_CLOCKRATE 48000U
#define WEBRTCPUSH_OPUS_CHANNELS 1
#define WEBRTCPUSH_AUDIO_QUEUE_DEPTH 8U
/*
* 设备侧 DataChannel(myDataChannel)与手机 createDataChannel('init') 争用 SCTP,
* 易触发 sctpenc association error 并导致管道闪断/进程崩溃。仅推流可不建。
......
......@@ -172,7 +172,7 @@ int webrtcpush_run_blocking(void)
s_app.prefer_mpp = TRUE;
s_app.device_offerer = FALSE;
s_app.verbose = WEBRTCPUSH_DEBUG ? TRUE : FALSE;
s_app.enable_audio = FALSE; /* 视频先行;音频后续 ALSA→Opus track */
setup_alsa_device(&s_app); /* enable_audio 由 ALSA 设备探测结果决定 */
s_app.loop = g_main_loop_new(NULL, FALSE);
......
#include "wifi_config_agent.h"
#include "wifi_config_config.h"
#include "stream_debug_agent.h"
#include "device_identity.h"
#include "device_fileopen.h"
#include "device_wifi_manager.h"
#include "common.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
static int s_wifi_config_sock = -1;
static pthread_t s_wifi_config_thread;
static volatile int s_wifi_config_running = 0;
static pthread_mutex_t s_wifi_config_sock_mutex = PTHREAD_MUTEX_INITIALIZER;
static int wifi_config_bind_socket(void)
{
int reuse = 1;
struct sockaddr_in addr;
if (s_wifi_config_sock >= 0) {
close(s_wifi_config_sock);
s_wifi_config_sock = -1;
}
s_wifi_config_sock = socket(AF_INET, SOCK_DGRAM, 0);
if (s_wifi_config_sock < 0) {
my_zlog_error("WiFi配置助手: 创建 socket 失败: %s", strerror(errno));
return -1;
}
if (setsockopt(s_wifi_config_sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)) < 0) {
my_zlog_warn("WiFi配置助手: SO_REUSEADDR 设置失败");
}
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(WIFI_CONFIG_UDP_PORT);
if (bind(s_wifi_config_sock, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
my_zlog_error("WiFi配置助手: 绑定端口 %d 失败: %s",
WIFI_CONFIG_UDP_PORT, strerror(errno));
close(s_wifi_config_sock);
s_wifi_config_sock = -1;
return -1;
}
return 0;
}
void wifi_config_agent_rebind_socket(void)
{
pthread_mutex_lock(&s_wifi_config_sock_mutex);
if (wifi_config_bind_socket() == 0) {
my_zlog_info("WiFi配置助手: UDP %d 已重新绑定", WIFI_CONFIG_UDP_PORT);
}
pthread_mutex_unlock(&s_wifi_config_sock_mutex);
}
static int is_stream_debug_cmd(const char *cmd)
{
return cmd && (strcmp(cmd, "stream_mode_query") == 0 ||
strcmp(cmd, "stream_url_update") == 0 ||
strcmp(cmd, "stream_url_query") == 0);
}
static int is_command_available(const char *cmd)
{
char check_cmd[256];
snprintf(check_cmd, sizeof(check_cmd), "which %s > /dev/null 2>&1", cmd);
return system(check_cmd) == 0;
}
static int ufw_is_active(void)
{
FILE *fp = popen("ufw status 2>/dev/null", "r");
char line[256];
if (!fp)
return 0;
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "Status: active") != NULL) {
pclose(fp);
return 1;
}
}
pclose(fp);
return 0;
}
static int ufw_has_udp_port_rule(int port)
{
char cmd[160];
snprintf(cmd, sizeof(cmd), "ufw status 2>/dev/null | grep -q '%d/udp'", port);
return system(cmd) == 0;
}
/* ufw 已启用且未放行时自动执行 ufw allow;无 ufw 或未启用则跳过 */
static void ensure_firewall_udp_port(int port)
{
char allow_cmd[128];
int ret;
if (!is_command_available("ufw")) {
my_zlog_debug("WiFi配置助手: 未安装 ufw,跳过防火墙检查");
return;
}
if (!ufw_is_active()) {
my_zlog_debug("WiFi配置助手: ufw 未启用,跳过防火墙放行");
return;
}
if (ufw_has_udp_port_rule(port)) {
my_zlog_debug("WiFi配置助手: UDP %d 已在 ufw 中放行", port);
return;
}
snprintf(allow_cmd, sizeof(allow_cmd), "ufw allow %d/udp >/dev/null 2>&1", port);
my_zlog_info("WiFi配置助手: UDP %d 未放行,正在执行 ufw allow ...", port);
ret = system(allow_cmd);
if (ret == 0 && ufw_has_udp_port_rule(port)) {
my_zlog_info("WiFi配置助手: UDP %d 放行成功", port);
return;
}
my_zlog_warn("WiFi配置助手: UDP %d 自动放行失败,App 可能无法发现设备", port);
}
typedef struct {
const char *code;
const char *name;
} wifi_device_type_entry_t;
static const wifi_device_type_entry_t s_device_type_table[] = {
{"0101", "大闪电"},
{"0102", "小闪电"},
{"0103", "挖掘机"},
{"0104", "推土机"},
{"0105", "人坐超大车"},
{"0106", "F1赛车"},
{"0107", "大众POLO R"},
{"0202", "坦克"},
{"0203", "M1A2坦克"},
{"0204", "美卡隆坦克"},
{"0206", "水坦克"},
{"0207", "射水弹坦克"},
{"0301", "34号船"},
{"0302", "ship0302船"},
{"0401", "云台"},
{"0403", "定位设备"},
{"0404", "云台"},
{"0405", "云台0405"},
{"0406", "水枪炮台"},
{"0501", "机器狗"},
{"0502", "机械臂"},
};
static const char *device_type_name_from_code(const char *type_code)
{
size_t i;
if (!type_code || type_code[0] == '\0')
return "香橙派";
for (i = 0; i < sizeof(s_device_type_table) / sizeof(s_device_type_table[0]); i++) {
if (strcmp(type_code, s_device_type_table[i].code) == 0)
return s_device_type_table[i].name;
}
return "香橙派";
}
static void normalize_device_no(char *device_no, size_t size)
{
size_t len;
if (!device_no || size == 0)
return;
device_no[size - 1] = '\0';
len = strcspn(device_no, "\r\n");
device_no[len] = '\0';
}
static const char *get_pure_device_no(void)
{
const char *device_no = mqtt_topic_pure_number();
if (device_no && device_no[0] != '\0' && strcmp(device_no, "00000000000000") != 0)
return device_no;
device_no = device_id_function();
if (device_no && device_no[0] != '\0')
return device_no;
return NULL;
}
static int parse_device_no_parts(const char *device_no, char *first2, size_t first2_sz,
char *type_code, size_t type_code_sz,
char *last4, size_t last4_sz)
{
char normalized[WIFI_CONFIG_DEVICE_NO_LEN + 1];
if (!device_no || !first2 || !type_code || !last4)
return -1;
snprintf(normalized, sizeof(normalized), "%s", device_no);
normalize_device_no(normalized, sizeof(normalized));
if (strlen(normalized) < WIFI_CONFIG_DEVICE_NO_LEN)
return -1;
snprintf(first2, first2_sz, "%.2s", normalized);
snprintf(type_code, type_code_sz, "%.4s", normalized + 2);
snprintf(last4, last4_sz, "%.4s", normalized + 10);
return 0;
}
static void generate_device_id(char *buf, size_t size)
{
const char *device_no = get_pure_device_no();
if (!buf || size == 0)
return;
if (device_no) {
snprintf(buf, size, "%s", device_no);
normalize_device_no(buf, size);
return;
}
snprintf(buf, size, "00000000000000");
}
static void generate_device_name(char *buf, size_t size)
{
const char *device_no = get_pure_device_no();
char first2[3];
char type_code[5];
char last4[5];
const char *type_name;
if (!buf || size == 0)
return;
if (!device_no || parse_device_no_parts(device_no, first2, sizeof(first2),
type_code, sizeof(type_code),
last4, sizeof(last4)) != 0) {
snprintf(buf, size, "香橙派");
return;
}
type_name = device_type_name_from_code(type_code);
snprintf(buf, size, "%s%s-%s", type_name, first2, last4);
}
static int send_json(int sock, const struct sockaddr_in *addr, const char *json)
{
ssize_t n;
if (!json || !addr || sock < 0)
return -1;
n = sendto(sock, json, strlen(json), 0, (const struct sockaddr *)addr, sizeof(*addr));
return n >= 0 ? 0 : -1;
}
static void send_config_result(int sock, const struct sockaddr_in *addr, int success,
const char *message)
{
cJSON *root = cJSON_CreateObject();
if (!root)
return;
cJSON_AddStringToObject(root, "cmd", "config_result");
cJSON_AddBoolToObject(root, "success", success ? 1 : 0);
if (message && message[0])
cJSON_AddStringToObject(root, "message", message);
char *payload = cJSON_PrintUnformatted(root);
if (payload) {
send_json(sock, addr, payload);
free(payload);
}
cJSON_Delete(root);
}
static void handle_discover(int sock, const struct sockaddr_in *addr)
{
char device_id[WIFI_CONFIG_DEVICE_ID_MAX];
char device_name[WIFI_CONFIG_DEVICE_NAME_MAX];
cJSON *root;
generate_device_id(device_id, sizeof(device_id));
generate_device_name(device_name, sizeof(device_name));
root = cJSON_CreateObject();
if (!root)
return;
cJSON_AddStringToObject(root, "cmd", "discover_response");
cJSON_AddStringToObject(root, "deviceId", device_id);
cJSON_AddStringToObject(root, "name", device_name);
char *payload = cJSON_PrintUnformatted(root);
if (payload) {
send_json(sock, addr, payload);
free(payload);
}
cJSON_Delete(root);
}
static void handle_config(int sock, const struct sockaddr_in *addr, cJSON *root)
{
cJSON *ssid_item = cJSON_GetObjectItemCaseSensitive(root, "ssid");
cJSON *password_item = cJSON_GetObjectItemCaseSensitive(root, "password");
cJSON *action_item = cJSON_GetObjectItemCaseSensitive(root, "action");
const char *ssid = NULL;
const char *password = "";
const char *action = "connect";
char message[WIFI_CONFIG_MESSAGE_MAX];
int success;
if (!cJSON_IsString(ssid_item) || !ssid_item->valuestring || !ssid_item->valuestring[0]) {
send_config_result(sock, addr, 0, "缺少有效的 ssid 字段");
return;
}
ssid = ssid_item->valuestring;
if (cJSON_IsString(password_item) && password_item->valuestring)
password = password_item->valuestring;
if (cJSON_IsString(action_item) && action_item->valuestring && action_item->valuestring[0])
action = action_item->valuestring;
my_zlog_info("WiFi配置助手: 收到配网请求 SSID=%s action=%s", ssid, action);
if (strcmp(action, "save") == 0) {
success = orange_pi_save_wifi(ssid, password) == 0;
snprintf(message, sizeof(message), "%s",
success ? "WiFi 配置已保存" : "WiFi 保存失败");
} else {
success = orange_pi_connect_wifi(ssid, password) == 0;
snprintf(message, sizeof(message), "%s",
success ? "WiFi 配置已保存并连接成功" : "WiFi 连接失败");
}
my_zlog_info("WiFi配置助手: 配网结果 success=%d, message=%s", success, message);
send_config_result(sock, addr, success, message);
}
static void process_message(int sock, const char *buf, const struct sockaddr_in *addr)
{
cJSON *root = cJSON_Parse(buf);
cJSON *cmd_item;
if (!root) {
my_zlog_warn("WiFi配置助手: JSON 解析失败");
return;
}
cmd_item = cJSON_GetObjectItemCaseSensitive(root, "cmd");
if (!cJSON_IsString(cmd_item) || !cmd_item->valuestring) {
cJSON_Delete(root);
return;
}
if (is_stream_debug_cmd(cmd_item->valuestring)) {
wifi_config_agent_rebind_socket();
}
if (strcmp(cmd_item->valuestring, "discover") == 0) {
handle_discover(sock, addr);
} else if (strcmp(cmd_item->valuestring, "config") == 0) {
handle_config(sock, addr, root);
} else if (strcmp(cmd_item->valuestring, "stream_mode_query") == 0) {
stream_debug_handle_mode_query(sock, addr);
} else if (strcmp(cmd_item->valuestring, "stream_url_update") == 0) {
stream_debug_handle_url_update(sock, addr, root);
} else if (strcmp(cmd_item->valuestring, "stream_url_query") == 0) {
stream_debug_handle_url_query(sock, addr);
}
cJSON_Delete(root);
}
static void *wifi_config_agent_thread(void *arg)
{
char buf[WIFI_CONFIG_RECV_BUF_SIZE];
char device_id[WIFI_CONFIG_DEVICE_ID_MAX];
char device_name[WIFI_CONFIG_DEVICE_NAME_MAX];
char first2[3];
char type_code[5];
char last4[5];
const char *type_name;
(void)arg;
generate_device_id(device_id, sizeof(device_id));
generate_device_name(device_name, sizeof(device_name));
if (parse_device_no_parts(get_pure_device_no(), first2, sizeof(first2),
type_code, sizeof(type_code), last4, sizeof(last4)) == 0) {
type_name = device_type_name_from_code(type_code);
my_zlog_info("WiFi配置助手对接服务已启动 (UDP)");
my_zlog_info(" 设备号: %s", device_id);
my_zlog_info(" 类型: %s (编码 %s)", type_name, type_code);
my_zlog_info(" 短码: %s%s", first2, last4);
my_zlog_info(" 显示名称: %s", device_name);
} else {
my_zlog_info("WiFi配置助手对接服务已启动 (UDP)");
my_zlog_info(" 设备号: %s", device_id);
my_zlog_info(" 显示名称: %s", device_name);
}
my_zlog_info(" 监听: 0.0.0.0:%d/udp", WIFI_CONFIG_UDP_PORT);
my_zlog_info("等待 App 连接...");
while (s_wifi_config_running) {
struct sockaddr_in client;
socklen_t client_len = sizeof(client);
ssize_t n = recvfrom(s_wifi_config_sock, buf, sizeof(buf) - 1, 0,
(struct sockaddr *)&client, &client_len);
if (n < 0) {
if (!s_wifi_config_running)
break;
if (errno == EINTR)
continue;
my_zlog_error("WiFi配置助手: recvfrom 失败: %s", strerror(errno));
continue;
}
buf[n] = '\0';
process_message(s_wifi_config_sock, buf, &client);
}
return NULL;
}
int wifi_config_agent_start(void)
{
if (s_wifi_config_running)
return 0;
ensure_firewall_udp_port(WIFI_CONFIG_UDP_PORT);
pthread_mutex_lock(&s_wifi_config_sock_mutex);
if (wifi_config_bind_socket() != 0) {
pthread_mutex_unlock(&s_wifi_config_sock_mutex);
return -1;
}
pthread_mutex_unlock(&s_wifi_config_sock_mutex);
s_wifi_config_running = 1;
if (pthread_create(&s_wifi_config_thread, NULL, wifi_config_agent_thread, NULL) != 0) {
my_zlog_error("WiFi配置助手: 创建线程失败");
s_wifi_config_running = 0;
pthread_mutex_lock(&s_wifi_config_sock_mutex);
close(s_wifi_config_sock);
s_wifi_config_sock = -1;
pthread_mutex_unlock(&s_wifi_config_sock_mutex);
return -1;
}
return 0;
}
void wifi_config_agent_stop(void)
{
if (!s_wifi_config_running)
return;
s_wifi_config_running = 0;
if (s_wifi_config_sock >= 0) {
shutdown(s_wifi_config_sock, SHUT_RDWR);
close(s_wifi_config_sock);
s_wifi_config_sock = -1;
}
pthread_join(s_wifi_config_thread, NULL);
}
#ifndef WIFI_CONFIG_AGENT_H__
#define WIFI_CONFIG_AGENT_H__
int wifi_config_agent_start(void);
void wifi_config_agent_stop(void);
void wifi_config_agent_rebind_socket(void);
#endif
#ifndef WIFI_CONFIG_CONFIG_H__
#define WIFI_CONFIG_CONFIG_H__
#define WIFI_CONFIG_UDP_PORT 18888
#define WIFI_CONFIG_RECV_BUF_SIZE 2048
#define WIFI_CONFIG_DEVICE_ID_MAX 32
#define WIFI_CONFIG_DEVICE_NAME_MAX 64
#define WIFI_CONFIG_MESSAGE_MAX 256
#define WIFI_CONFIG_DEVICE_NO_LEN 14
#endif
......@@ -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-01.log"; millisecond
my_log.* "/home/orangepi/car/master/log/log_2026-07-02.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