Commit 4687e6c8 authored by 957dd's avatar 957dd

加入了手机去配网

parent d92f8afb
......@@ -978,6 +978,30 @@ cJSON *app_wifi_list_query(void) {
}
/* 切换当前连接到已保存的指定 SSID */
/* 检查指定 SSID 是否已保存(不连接)。
* 供 agent 在切换前做预校验,便于"先回复 app 再实际切换"。 */
int app_wifi_check_saved(const char *ssid)
{
saved_wifi_entry_t entries[MAX_WIFI_ENTRIES];
int count;
int found = 0;
if (!ssid || !ssid[0])
return -1;
count = collect_saved_wifi_entries(entries, MAX_WIFI_ENTRIES);
if (count < 0)
return -1;
for (int i = 0; i < count; i++) {
if (strcmp(entries[i].ssid, ssid) == 0) {
found = 1;
break;
}
}
return found ? 0 : -1;
}
int app_wifi_switch(const char *ssid) {
saved_wifi_entry_t entries[MAX_WIFI_ENTRIES];
int count;
......
......@@ -27,6 +27,10 @@ cJSON *app_wifi_list_query(void);
/* 切换当前连接到已保存的指定 SSID。
* 返回 0=已发起切换(异步), -1=未找到/失败 */
/* 检查指定 SSID 是否已保存(不连接)。
* 返回 0=已保存, -1=未找到 */
int app_wifi_check_saved(const char *ssid);
int app_wifi_switch(const char *ssid);
/* 删除已保存的指定 SSID(受保护的 jking 不可删)。
......
No preview for this file type
......@@ -23,7 +23,6 @@
#define DIAG_LOG_FILENAME_MAX 128
#define DIAG_LOG_PATH_MAX 512
#define DIAG_BROWSER_URL_MAX 512
#define DIAG_MESSAGE_MAX 256
static void diag_send_json(int sock, const struct sockaddr_in *addr, const char *json)
{
......@@ -300,6 +299,13 @@ typedef struct {
time_t mtime;
} log_entry_t;
/* TCP 传输上下文:主线程已完成 socket+bind+listen,线程只做 accept+传输 */
typedef struct {
int server_fd;
log_entry_t entries[DIAG_LOG_MAX_FILES];
int count;
} log_tcp_ctx_t;
static int diag_log_compare_mtime(const void *a, const void *b)
{
const log_entry_t *ea = (const log_entry_t *)a;
......@@ -356,57 +362,33 @@ static int diag_find_recent_logs(log_entry_t *entries, int max_count)
return count;
}
/* TCP 线程:接收已 listen 的 server_fd,做 accept + 文件传输 */
static void *diag_log_tcp_thread(void *arg)
{
log_entry_t *entries = (log_entry_t *)arg;
int server_fd, client_fd;
struct sockaddr_in addr;
int opt = 1;
log_tcp_ctx_t *ctx = (log_tcp_ctx_t *)arg;
int server_fd = ctx->server_fd;
int client_fd;
struct timeval tv;
char filename[DIAG_LOG_FILENAME_MAX + 2];
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
my_zlog_error("设备调试: TCP socket 创建失败");
free(entries);
return NULL;
}
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_port = htons(DIAG_LOG_TCP_PORT);
addr.sin_addr.s_addr = INADDR_ANY;
if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
my_zlog_error("设备调试: TCP bind %d 失败", DIAG_LOG_TCP_PORT);
close(server_fd);
free(entries);
return NULL;
}
if (listen(server_fd, 1) < 0) {
my_zlog_error("设备调试: TCP listen 失败");
close(server_fd);
free(entries);
return NULL;
}
my_zlog_info("设备调试: TCP 日志服务已启动,等待连接 port=%d", DIAG_LOG_TCP_PORT);
struct timeval tv;
/* accept 超时 30 秒 */
tv.tv_sec = 30;
tv.tv_usec = 0;
setsockopt(server_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
my_zlog_info("设备调试: TCP 等待连接 port=%d", DIAG_LOG_TCP_PORT);
client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) {
my_zlog_error("设备调试: TCP accept 超时或失败");
close(server_fd);
free(entries);
free(ctx);
return NULL;
}
my_zlog_info("设备调试: TCP 客户端已连接,开始传输");
/* 循环接收文件名请求 */
while (1) {
ssize_t n;
size_t flen = 0;
......@@ -419,12 +401,13 @@ static void *diag_log_tcp_thread(void *arg)
char buf[4096];
size_t r;
/* 逐字符读文件名直到 \n */
while (flen < sizeof(filename) - 1) {
n = recv(client_fd, &ch, 1, 0);
if (n <= 0) {
close(client_fd);
close(server_fd);
free(entries);
free(ctx);
my_zlog_info("设备调试: TCP 日志传输完成");
return NULL;
}
......@@ -434,11 +417,12 @@ static void *diag_log_tcp_thread(void *arg)
}
filename[flen] = '\0';
/* 安全校验:文件名只能包含字母数字下划线和点 */
int valid = 1;
for (size_t i = 0; i < flen; i++) {
c = (unsigned char)filename[i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '.')) {
(c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.')) {
valid = 0;
break;
}
......@@ -463,6 +447,7 @@ static void *diag_log_tcp_thread(void *arg)
fsize = ftell(f);
rewind(f);
/* 8 字节大端序发送文件大小 */
uint64_t sz = (uint64_t)fsize;
for (int i = 7; i >= 0; i--) {
sizebuf[i] = (uint8_t)(sz & 0xFF);
......@@ -470,6 +455,7 @@ static void *diag_log_tcp_thread(void *arg)
}
send(client_fd, sizebuf, 8, 0);
/* 发送文件内容 */
while ((r = fread(buf, 1, sizeof(buf), f)) > 0) {
size_t sent = 0;
while (sent < r) {
......@@ -487,7 +473,7 @@ static void *diag_log_tcp_thread(void *arg)
close(client_fd);
close(server_fd);
free(entries);
free(ctx);
return NULL;
}
......@@ -499,10 +485,13 @@ void device_diag_handle_log_fetch(int sock, const struct sockaddr_in *addr, cJSO
cJSON *arr;
char *payload;
log_entry_t entries[DIAG_LOG_MAX_FILES];
log_entry_t *thread_entries;
int n;
int server_fd;
struct sockaddr_in tcp_addr;
int opt = 1;
pthread_t tid;
pthread_attr_t attr;
log_tcp_ctx_t *ctx;
pwd_item = cJSON_GetObjectItemCaseSensitive(root, "password");
if (!cJSON_IsString(pwd_item) || !pwd_item->valuestring) {
......@@ -511,6 +500,7 @@ void device_diag_handle_log_fetch(int sock, const struct sockaddr_in *addr, cJSO
password = pwd_item->valuestring;
}
/* 密码校验 */
if (strcmp(password, DIAG_LOG_PASSWORD) != 0) {
resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "cmd", "log_fetch_ready");
......@@ -527,6 +517,7 @@ void device_diag_handle_log_fetch(int sock, const struct sockaddr_in *addr, cJSO
return;
}
/* 查找最近 2 个日志 */
n = diag_find_recent_logs(entries, DIAG_LOG_MAX_FILES);
if (n <= 0) {
resp = cJSON_CreateObject();
......@@ -544,35 +535,36 @@ void device_diag_handle_log_fetch(int sock, const struct sockaddr_in *addr, cJSO
return;
}
thread_entries = (log_entry_t *)malloc(sizeof(log_entry_t) * n);
if (!thread_entries) {
my_zlog_error("设备调试: malloc 失败");
/* 关键修复:先创建 TCP server 并 listen 成功,再回 log_fetch_ready。
* 避免"手机收到 ready 后立即连接,但设备还没 listen,连接被拒绝"的时序问题。 */
server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
my_zlog_error("设备调试: TCP socket 创建失败");
return;
}
for (int i = 0; i < n; i++)
thread_entries[i] = entries[i];
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (pthread_create(&tid, &attr, diag_log_tcp_thread, thread_entries) != 0) {
my_zlog_error("设备调试: TCP 线程创建失败");
free(thread_entries);
pthread_attr_destroy(&attr);
resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "cmd", "log_fetch_ready");
cJSON_AddNumberToObject(resp, "tcpPort", 0);
cJSON_AddItemToObject(resp, "files", cJSON_CreateArray());
cJSON_AddStringToObject(resp, "message", "设备 TCP 服务启动失败");
payload = cJSON_PrintUnformatted(resp);
if (payload) {
diag_send_json(sock, addr, payload);
free(payload);
setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
memset(&tcp_addr, 0, sizeof(tcp_addr));
tcp_addr.sin_family = AF_INET;
tcp_addr.sin_port = htons(DIAG_LOG_TCP_PORT);
tcp_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(server_fd, (struct sockaddr *)&tcp_addr, sizeof(tcp_addr)) < 0) {
my_zlog_error("设备调试: TCP bind %d 失败", DIAG_LOG_TCP_PORT);
close(server_fd);
return;
}
cJSON_Delete(resp);
if (listen(server_fd, 1) < 0) {
my_zlog_error("设备调试: TCP listen 失败");
close(server_fd);
return;
}
pthread_attr_destroy(&attr);
my_zlog_info("设备调试: TCP 已 listen port=%d,准备回 log_fetch_ready", DIAG_LOG_TCP_PORT);
/* 回 log_fetch_ready(此时 listen 已就绪,手机可立即连接) */
resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "cmd", "log_fetch_ready");
cJSON_AddNumberToObject(resp, "tcpPort", DIAG_LOG_TCP_PORT);
......@@ -591,6 +583,30 @@ void device_diag_handle_log_fetch(int sock, const struct sockaddr_in *addr, cJSO
free(payload);
}
cJSON_Delete(resp);
/* 启动 detached 线程做 accept + 传输 */
ctx = (log_tcp_ctx_t *)malloc(sizeof(log_tcp_ctx_t));
if (!ctx) {
my_zlog_error("设备调试: malloc 失败");
close(server_fd);
return;
}
ctx->server_fd = server_fd;
ctx->count = n;
for (int i = 0; i < n; i++)
ctx->entries[i] = entries[i];
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (pthread_create(&tid, &attr, diag_log_tcp_thread, ctx) != 0) {
my_zlog_error("设备调试: TCP 线程创建失败");
free(ctx);
close(server_fd);
pthread_attr_destroy(&attr);
return;
}
pthread_attr_destroy(&attr);
my_zlog_info("设备调试: log_fetch 已启动 TCP %d,共 %d 个日志", DIAG_LOG_TCP_PORT, n);
}
......
......@@ -288,11 +288,50 @@ static gboolean gst_check_required_plugins(void)
my_zlog_warn("runtime_deps: missing GStreamer element %s", g->element);
return FALSE;
}
if (g->element && !gst_element_ready(g->element)) {
if (g->element && g_strcmp0(g->element, "v4l2src") != 0 &&
!gst_element_ready(g->element)) {
my_zlog_warn("runtime_deps: element not ready %s", g->element);
return FALSE;
}
}
/* 摄像头不在 /dev/video0 时,尝试 USB 重置复位到 video0 */
{
gboolean need_reset = TRUE;
for (int i = 0; i < 3; i++) {
gchar *path = g_strdup_printf("/dev/video%d", i);
if (g_file_test(path, G_FILE_TEST_EXISTS) && i == 0) {
/* video0 存在,检查是否是 capture 设备 */
gchar *sys_cap = g_strdup_printf("/sys/class/video4linux/video%d/device_caps", i);
if (g_file_test(sys_cap, G_FILE_TEST_EXISTS)) {
gchar *contents = NULL;
if (g_file_get_contents(sys_cap, &contents, NULL, NULL)) {
guint64 caps = g_ascii_strtoull(g_strstrip(contents), NULL, 0);
if ((caps & 0x00000001U) || (caps & 0x00001000U))
need_reset = FALSE;
g_free(contents);
}
}
g_free(sys_cap);
}
g_free(path);
}
if (need_reset) {
my_zlog_warn("runtime_deps: camera not on /dev/video0, trying USB reset");
/* uvcvideo 模块重载:释放所有 USB 摄像头并重新枚举 */
int rc1 = system("modprobe -r uvcvideo 2>/dev/null");
if (rc1 == 0) {
g_usleep(500000);
system("modprobe uvcvideo 2>/dev/null");
g_usleep(2000000); /* 等 2 秒让设备重新枚举 */
my_zlog_info("runtime_deps: USB camera reset done");
} else {
/* modprobe 失败,尝试 usbreset */
system("for dev in /dev/video*; do usbreset \"$dev\" 2>/dev/null; done");
g_usleep(1000000);
my_zlog_info("runtime_deps: usbreset done");
}
}
}
if (!gst_h264_encoder_ok() || !gst_audio_capture_ok())
return FALSE;
return TRUE;
......
#include "audio_sink.h"
#include "webrtcpush_config.h"
#include "webrtcpush_log.h"
#include <gst/gst.h>
#include <gst/app/gstappsrc.h>
/*
* 手机→设备方向的音频播放管道(按键模式):
* appsrc(opus payload) → opusdec → audioconvert → audioresample → volume → alsasink
*
* 手机端是"按住说话"模式(最长 15s),不是持续推流,因此:
* - on_audio_message 收到包后入队,立即返回(不阻塞 libdatachannel 线程)
* - 独立线程消费队列,维护"按键会话"
* - 会话开始(首包到达):pipeline 切到 PLAYING
* - 会话结束(500ms 无包 或 15s 超时):flush appsrc + pipeline 切到 READY
* 切到 READY 释放 ALSA 设备,避免 alsasink 持续占用/空转
*/
#define AUDIO_SINK_SESSION_TIMEOUT_MS 500 /* 500ms 无包认为按键结束 */
#define AUDIO_SINK_SESSION_MAX_MS 15000 /* 单次按键最长 15s */
typedef struct {
uint8_t *data;
size_t size;
} SinkPacket;
struct AudioSink {
GstElement *pipeline;
GstElement *appsrc;
GstElement *vol;
GAsyncQueue *queue; /* 待处理 Opus 包队列 */
GMutex lock; /* 保护 pipeline 状态切换 */
GThread *thread; /* 消费线程 */
gboolean quit; /* 退出标志 */
gint64 session_start_us; /* 当前按键会话开始时间(0=无会话) */
gint64 last_packet_us; /* 最后一个包到达时间 */
gboolean pipeline_playing; /* pipeline 当前是否 PLAYING */
};
static void flush_queue(AudioSink *src)
{
SinkPacket *pkt;
while ((pkt = g_async_queue_try_pop(src->queue)) != NULL) {
g_free(pkt->data);
g_free(pkt);
}
}
static void set_pipeline_state_locked(AudioSink *src, GstState state)
{
if (!src->pipeline)
return;
/* 切到 READY 时 flush appsrc,避免旧数据残留导致下次会话首帧异常 */
if (state == GST_STATE_READY && src->appsrc) {
gst_element_send_event(src->pipeline,
gst_event_new_flush_start());
gst_element_send_event(src->pipeline,
gst_event_new_flush_stop(FALSE));
}
gst_element_set_state(src->pipeline, state);
src->pipeline_playing = (state == GST_STATE_PLAYING);
}
static gpointer audio_sink_thread(gpointer data)
{
AudioSink *src = data;
gint64 now;
GstBuffer *buf;
while (1) {
/* 等待包,超时 100ms 用于检查会话超时 */
SinkPacket *pkt = g_async_queue_timeout_pop(src->queue, 100 * 1000);
now = g_get_monotonic_time();
g_mutex_lock(&src->lock);
if (src->quit) {
g_mutex_unlock(&src->lock);
if (pkt) { g_free(pkt->data); g_free(pkt); }
break;
}
/* 会话超时检查 */
if (src->session_start_us > 0) {
gint64 idle_ms = (now - src->last_packet_us) / 1000;
gint64 sess_ms = (now - src->session_start_us) / 1000;
if (idle_ms >= AUDIO_SINK_SESSION_TIMEOUT_MS ||
sess_ms >= AUDIO_SINK_SESSION_MAX_MS) {
if (src->pipeline_playing) {
set_pipeline_state_locked(src, GST_STATE_READY);
my_zlog_info("audio_sink: session ended (idle=%lldms sess=%lldms)",
(long long)idle_ms, (long long)sess_ms);
}
src->session_start_us = 0;
flush_queue(src);
/* 丢弃超时后到达的旧包 */
if (pkt) {
g_free(pkt->data);
g_free(pkt);
pkt = NULL;
}
g_mutex_unlock(&src->lock);
continue;
}
}
if (pkt) {
/* 新会话开始 */
if (src->session_start_us == 0) {
src->session_start_us = now;
my_zlog_info("audio_sink: session start");
if (!src->pipeline_playing)
set_pipeline_state_locked(src, GST_STATE_PLAYING);
}
src->last_packet_us = now;
/* push 到 appsrc(pipeline PLAYING 状态) */
if (src->appsrc && src->pipeline_playing) {
buf = gst_buffer_new_wrapped(g_memdup2(pkt->data, pkt->size), pkt->size);
GST_BUFFER_DTS(buf) = GST_CLOCK_TIME_NONE;
GST_BUFFER_PTS(buf) = GST_CLOCK_TIME_NONE;
if (gst_app_src_push_buffer(GST_APP_SRC(src->appsrc), buf) != GST_FLOW_OK) {
my_zlog_warn("audio_sink: push_buffer failed");
}
}
g_free(pkt->data);
g_free(pkt);
}
g_mutex_unlock(&src->lock);
}
return NULL;
}
AudioSink *audio_sink_start(const char *alsa_device, char **error_message)
{
AudioSink *src;
GstElement *pipe, *asrc, *dec, *conv, *resample, *vol, *sink;
GstCaps *caps;
GstStateChangeReturn ret;
if (!alsa_device || !alsa_device[0]) {
if (error_message)
*error_message = g_strdup("no ALSA device");
return NULL;
}
src = g_new0(AudioSink, 1);
pipe = gst_pipeline_new("audio-sink-pipe");
asrc = gst_element_factory_make("appsrc", "asrc");
dec = gst_element_factory_make("opusdec", "dec");
conv = gst_element_factory_make("audioconvert", "conv");
resample = gst_element_factory_make("audioresample", "resample");
vol = gst_element_factory_make("volume", "vol");
sink = gst_element_factory_make("alsasink", "sink");
if (!pipe || !asrc || !dec || !conv || !resample || !vol || !sink) {
if (error_message)
*error_message = g_strdup("failed to create audio sink GStreamer elements");
if (pipe)
gst_object_unref(pipe);
g_free(src);
return NULL;
}
caps = gst_caps_new_empty_simple("audio/x-opus");
g_object_set(asrc,
"caps", caps,
"format", GST_FORMAT_BYTES,
"is-live", TRUE,
"emit-signals", FALSE,
"min-latency", (gint64)0,
"max-bytes", (guint64)(1 * 1024 * 1024),
NULL);
gst_caps_unref(caps);
g_object_set(sink,
"device", alsa_device,
"buffer-time", (gint64)20000,
"latency-time", (gint64)10000,
"sync", FALSE,
NULL);
g_object_set(vol, "volume", 0.5, NULL);
gst_bin_add_many(GST_BIN(pipe), asrc, dec, conv, resample, vol, sink, NULL);
if (!gst_element_link_many(asrc, dec, conv, resample, vol, sink, NULL)) {
if (error_message)
*error_message = g_strdup("failed to link audio sink chain");
gst_object_unref(pipe);
g_free(src);
return NULL;
}
src->pipeline = pipe;
src->appsrc = asrc;
src->vol = vol;
src->queue = g_async_queue_new();
g_mutex_init(&src->lock);
src->session_start_us = 0;
src->pipeline_playing = FALSE;
/* 初始状态 READY(不占 ALSA 设备,等首包到来再 PLAYING) */
ret = gst_element_set_state(pipe, GST_STATE_READY);
if (ret == GST_STATE_CHANGE_FAILURE) {
if (error_message)
*error_message = g_strdup("audio sink pipeline failed to reach READY");
gst_element_set_state(pipe, GST_STATE_NULL);
gst_object_unref(pipe);
g_async_queue_unref(src->queue);
g_mutex_clear(&src->lock);
g_free(src);
return NULL;
}
src->thread = g_thread_new("audio-sink", audio_sink_thread, src);
if (!src->thread) {
if (error_message)
*error_message = g_strdup("failed to create audio sink thread");
audio_sink_stop(src);
return NULL;
}
my_zlog_info("audio_sink: started device=%s opus=%uch %uHz (push-to-talk)",
alsa_device, WEBRTCPUSH_OPUS_CHANNELS, WEBRTCPUSH_OPUS_CLOCKRATE);
return src;
}
void audio_sink_stop(AudioSink *src)
{
if (!src)
return;
if (src->thread) {
g_mutex_lock(&src->lock);
src->quit = TRUE;
g_mutex_unlock(&src->lock);
g_thread_join(src->thread);
src->thread = NULL;
}
if (src->pipeline) {
gst_element_set_state(src->pipeline, GST_STATE_NULL);
gst_object_unref(src->pipeline);
}
if (src->queue) {
flush_queue(src);
g_async_queue_unref(src->queue);
}
g_mutex_clear(&src->lock);
g_free(src);
}
gboolean audio_sink_push_opus(AudioSink *src, const uint8_t *data, size_t size)
{
SinkPacket *pkt;
if (!src || !src->queue || !data || size == 0)
return FALSE;
/* 入队,由消费线程处理(不阻塞 libdatachannel 回调线程) */
pkt = g_new0(SinkPacket, 1);
pkt->data = (uint8_t *)g_memdup2(data, size);
pkt->size = size;
g_async_queue_push(src->queue, pkt);
return TRUE;
}
void audio_sink_set_volume(AudioSink *src, double volume)
{
if (!src || !src->vol)
return;
if (volume < 0.0)
volume = 0.0;
if (volume > 1.0)
volume = 1.0;
g_object_set(src->vol, "volume", volume, NULL);
}
#ifndef AUDIO_SINK_H
#define AUDIO_SINK_H
#include <glib.h>
#include <stddef.h>
#include <stdint.h>
typedef struct AudioSink AudioSink;
/* alsa_device: plughw:X,Y 用于播放 */
AudioSink *audio_sink_start(const char *alsa_device, char **error_message);
void audio_sink_stop(AudioSink *src);
/* 接收 Opus RTP payload 写入播放管道。data 是 Opus payload(不含 RTP 头),size 是字节数 */
gboolean audio_sink_push_opus(AudioSink *src, const uint8_t *data, size_t size);
/* 设置播放音量 0.0~1.0 */
void audio_sink_set_volume(AudioSink *src, double volume);
#endif
......@@ -63,9 +63,14 @@ AudioSource *audio_source_start(const char *alsa_device, char **error_message)
g_object_set(enc,
"bitrate", WEBRTCPUSH_OPUS_BITRATE,
"bitrate-type", 0, /* 0=cbr */
"framesize", 20, /* 20ms 帧 */
"inband-fec", TRUE,
NULL);
/* framesize 属性部分 opusenc 不支持,先检查再设置 */
{
GParamSpec *pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(enc), "framesize");
if (pspec)
g_object_set(enc, "framesize", 20, NULL);
}
g_object_set(sink,
"emit-signals", FALSE,
......
This diff is collapsed.
This diff is collapsed.
#include "volume_control.h"
#include "webrtcpush_log.h"
#include <libsoup/soup.h>
#include <json-glib/json-glib.h>
#include <math.h>
/*
* 定时拉取后端音量接口并设置到 AudioSink。
*
* 接口:GET https://fcrs-api.yd-ss.com/api/drive/use/status/{vehicleId}
* 响应 JSON 中 businessJson 是字符串(需二次解析),取 volume 字段(0~1)。
* businessJson 可能出现在 data.businessJson / data.data.businessJson / data.result.businessJson。
* 每 3 秒拉一次,与手机端逻辑对齐。
*/
#define VC_INTERVAL_SEC 3
#define VC_DEFAULT_VOLUME 0.5
#define VC_VOLUME_EPSILON 0.001 /* 音量变化阈值,避免重复设置 */
#define VC_HTTP_TIMEOUT_SEC 5
#define VC_API_BASE "https://fcrs-api.yd-ss.com/api/drive/use/status/"
struct VolumeControl {
gchar *vehicle_id;
AudioSink *sink;
SoupSession *session;
GThread *thread;
gint stop; /* 原子停止标志 */
gdouble current_volume;
};
/*
* 在 JsonObject 中查找 businessJson 字符串。
* 兼容 data.businessJson / data.data.businessJson / data.result.businessJson
* 以及顶层直接 businessJson 的情况。
*/
static const gchar *find_business_json(JsonObject *obj)
{
JsonNode *node;
JsonObject *child;
if (!obj)
return NULL;
/* 顶层 businessJson */
if (json_object_has_member(obj, "businessJson"))
return json_object_get_string_member(obj, "businessJson");
/* data.* */
if (json_object_has_member(obj, "data")) {
node = json_object_get_member(obj, "data");
if (JSON_NODE_HOLDS_OBJECT(node)) {
child = json_node_get_object(node);
if (json_object_has_member(child, "businessJson"))
return json_object_get_string_member(child, "businessJson");
/* data.data.businessJson */
if (json_object_has_member(child, "data")) {
JsonNode *n2 = json_object_get_member(child, "data");
if (JSON_NODE_HOLDS_OBJECT(n2)) {
JsonObject *c2 = json_node_get_object(n2);
if (json_object_has_member(c2, "businessJson"))
return json_object_get_string_member(c2, "businessJson");
}
}
/* data.result.businessJson */
if (json_object_has_member(child, "result")) {
JsonNode *n2 = json_object_get_member(child, "result");
if (JSON_NODE_HOLDS_OBJECT(n2)) {
JsonObject *c2 = json_node_get_object(n2);
if (json_object_has_member(c2, "businessJson"))
return json_object_get_string_member(c2, "businessJson");
}
}
}
}
/* result.businessJson */
if (json_object_has_member(obj, "result")) {
node = json_object_get_member(obj, "result");
if (JSON_NODE_HOLDS_OBJECT(node)) {
child = json_node_get_object(node);
if (json_object_has_member(child, "businessJson"))
return json_object_get_string_member(child, "businessJson");
}
}
return NULL;
}
/* 从 HTTP 响应体解析音量,范围 0~1。失败返回 -1.0 */
static gdouble parse_volume_from_response(const gchar *body, gsize len)
{
JsonParser *parser;
JsonNode *root;
JsonObject *obj;
const gchar *biz_json;
gdouble volume = -1.0;
GError *err = NULL;
parser = json_parser_new();
if (!json_parser_load_from_data(parser, body, len, &err)) {
my_zlog_warn("volume_control: parse outer JSON failed: %s",
err ? err->message : "unknown");
if (err)
g_error_free(err);
g_object_unref(parser);
return -1.0;
}
root = json_parser_get_root(parser);
if (!root || !JSON_NODE_HOLDS_OBJECT(root)) {
g_object_unref(parser);
return -1.0;
}
obj = json_node_get_object(root);
biz_json = find_business_json(obj);
if (!biz_json || !biz_json[0]) {
g_object_unref(parser);
return -1.0;
}
/* businessJson 是 JSON 字符串,二次解析取 volume */
{
JsonParser *p2 = json_parser_new();
JsonNode *root2;
if (!json_parser_load_from_data(p2, biz_json, -1, &err)) {
my_zlog_warn("volume_control: parse businessJson failed: %s",
err ? err->message : "unknown");
if (err)
g_error_free(err);
g_object_unref(p2);
g_object_unref(parser);
return -1.0;
}
root2 = json_parser_get_root(p2);
if (root2 && JSON_NODE_HOLDS_OBJECT(root2)) {
JsonObject *bobj = json_node_get_object(root2);
if (json_object_has_member(bobj, "volume")) {
JsonNode *vn = json_object_get_member(bobj, "volume");
if (!json_node_is_null(vn))
volume = json_node_get_double(vn);
}
}
g_object_unref(p2);
}
g_object_unref(parser);
return volume;
}
static void fetch_and_apply_volume(VolumeControl *vc)
{
SoupMessage *msg;
gchar *url;
guint status;
const gchar *body;
gsize body_len;
gdouble vol;
url = g_strdup_printf("%s%s", VC_API_BASE, vc->vehicle_id);
msg = soup_message_new("GET", url);
if (!msg) {
my_zlog_warn("volume_control: failed to create request for %s", url);
g_free(url);
return;
}
status = soup_session_send_message(vc->session, msg);
if (status != 200) {
my_zlog_warn("volume_control: HTTP %u for %s", status, url);
g_object_unref(msg);
g_free(url);
return;
}
body = msg->response_body->data;
body_len = msg->response_body->length;
if (!body || body_len == 0) {
my_zlog_warn("volume_control: empty response body for %s", url);
g_object_unref(msg);
g_free(url);
return;
}
vol = parse_volume_from_response(body, body_len);
if (vol < 0.0) {
my_zlog_warn("volume_control: volume not found in response");
} else if (fabs(vol - vc->current_volume) > VC_VOLUME_EPSILON) {
/* 音量有变化才设置并记日志,避免噪音 */
audio_sink_set_volume(vc->sink, vol);
vc->current_volume = vol;
my_zlog_info("volume_control: volume set to %.3f", vol);
}
g_object_unref(msg);
g_free(url);
}
static gpointer volume_control_thread(gpointer data)
{
VolumeControl *vc = data;
while (g_atomic_int_get(&vc->stop) == 0) {
fetch_and_apply_volume(vc);
/* 分段睡眠(3s = 30 * 100ms),便于快速响应停止 */
for (gint i = 0; i < VC_INTERVAL_SEC * 10; i++) {
if (g_atomic_int_get(&vc->stop) != 0)
break;
g_usleep(100 * 1000); /* 100ms */
}
}
return NULL;
}
VolumeControl *volume_control_start(const char *vehicle_id, AudioSink *sink)
{
VolumeControl *vc;
if (!vehicle_id || !vehicle_id[0] || !sink) {
my_zlog_error("volume_control: invalid args");
return NULL;
}
vc = g_new0(VolumeControl, 1);
vc->vehicle_id = g_strdup(vehicle_id);
vc->sink = sink;
vc->current_volume = VC_DEFAULT_VOLUME;
vc->stop = 0;
/* 启动时先设默认音量 0.5 */
audio_sink_set_volume(sink, VC_DEFAULT_VOLUME);
/* libsoup 同步 session + HTTP 超时(部分版本不支持 async 属性,用 soup_session_new 兜底) */
vc->session = soup_session_new();
if (vc->session) {
GParamSpec *pspec;
pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(vc->session), "timeout");
if (pspec)
g_object_set(vc->session, "timeout", (guint)VC_HTTP_TIMEOUT_SEC, NULL);
pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(vc->session), "idle-timeout");
if (pspec)
g_object_set(vc->session, "idle-timeout", (guint)VC_HTTP_TIMEOUT_SEC, NULL);
}
vc->thread = g_thread_new("volume-control", volume_control_thread, vc);
my_zlog_info("volume_control: started vehicle_id=%s interval=%ds",
vehicle_id, VC_INTERVAL_SEC);
return vc;
}
void volume_control_stop(VolumeControl *vc)
{
if (!vc)
return;
g_atomic_int_set(&vc->stop, 1);
if (vc->thread)
g_thread_join(vc->thread);
if (vc->session)
g_object_unref(vc->session);
g_free(vc->vehicle_id);
g_free(vc);
}
#ifndef VOLUME_CONTROL_H
#define VOLUME_CONTROL_H
#include "audio_sink.h"
typedef struct VolumeControl VolumeControl;
/* vehicle_id: 设备号如 CN010200000132;audio_sink: 播放 sink 用于设置音量 */
VolumeControl *volume_control_start(const char *vehicle_id, AudioSink *sink);
void volume_control_stop(VolumeControl *vc);
#endif
......@@ -8,35 +8,46 @@
*
* 原生分支:v4l2 采集 → mpph264enc(或 x264enc 回退)→ libdatachannel 发送。
*/
#define WEBRTCPUSH_USE_MPP 1
#define WEBRTCPUSH_USE_MPP 0
/* 正常固定 24fps;弱网以降码率为主,后续若启用动态帧率也不得低于 22fps。 */
#define WEBRTCPUSH_H264_FPS 24
#define WEBRTCPUSH_H264_MIN_FPS 22
#define WEBRTCPUSH_H264_GOP (WEBRTCPUSH_H264_FPS * 3) /* 3s; PLI still requests IDR */
#define WEBRTCPUSH_H264_GOP (WEBRTCPUSH_H264_FPS * 3) /* 3s; PLI 按需请求 IDR,减少 I 帧码率尖峰 */
/* 与 gst_webrtc_pipeline / jywy 浏览器推流对齐的码率策略 */
#define WEBRTCPUSH_INITIAL_BITRATE 1400000U
/*
* 与 gst_webrtc_pipeline / jywy 浏览器推流对齐的码率策略。
* 首屏先用 900kbps,不像 1.4Mbps 那样猛冲,也不要低到一进来就糊。
* RTCP REMB 只作为码率趋势,下降也做阶梯平滑,避免 MPP 动态切码率时卡顿。
*/
#define WEBRTCPUSH_INITIAL_BITRATE 900000U
#define WEBRTCPUSH_MIN_BITRATE 500000U
#define WEBRTCPUSH_MAX_BITRATE 3200000U
#define WEBRTCPUSH_MAX_BITRATE 2800000U
#define WEBRTCPUSH_REMB_UTIL_PERCENT 80U
#define WEBRTCPUSH_REMB_DOWN_MIN_STEP 100000U
#define WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS 3U
#define WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS 4U
#define WEBRTCPUSH_REMB_SEVERE_PERCENT 65U
#define WEBRTCPUSH_BITRATE_RAMP_UP_MS 2500U
#define WEBRTCPUSH_BITRATE_RAMP_DOWN_MS 500U
#define WEBRTCPUSH_PACING_HEADROOM_PERCENT 125U
#define WEBRTCPUSH_BITRATE_RAMP_DOWN_MS 1500U
#define WEBRTCPUSH_REMB_DOWN_STEP_PERCENT 15U
#define WEBRTCPUSH_REMB_DOWN_STEP_MIN_BPS 80000U
#define WEBRTCPUSH_REMB_DOWN_STEP_MAX_BPS 180000U
#define WEBRTCPUSH_PACING_HEADROOM_PERCENT 140U
#define WEBRTCPUSH_PACING_INTERVAL_MS 5U
#define WEBRTCPUSH_PACING_MAX_BITRATE 4000000U
#define WEBRTCPUSH_PACING_MAX_QUEUE_MS 300U
#define WEBRTCPUSH_PACING_GUARD_COOLDOWN_MS 1000U
#define WEBRTCPUSH_PACING_MAX_QUEUE_MS 250U
#define WEBRTCPUSH_PACING_GUARD_COOLDOWN_MS 2000U
/* 首个/刚恢复的 IDR 允许短暂排队,避免首屏关键帧刚发出就被清队列 */
#define WEBRTCPUSH_PACING_IDR_GRACE_MS 800U
/* pacing 清队列后,若近期已有 IDR,不要反复强制 IDR 造成 I 帧风暴 */
#define WEBRTCPUSH_PACING_RESYNC_IDR_MS 5000U
/* 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_PLI_IDR_THROTTLE_MS 500U
/* 周期性推流指标日志间隔 */
#define WEBRTCPUSH_STATS_LOG_INTERVAL_MS 8000U
......@@ -48,13 +59,26 @@
#define WEBRTCPUSH_H264_PROFILE_LEVEL_ID "42e01f"
/* 拉帧线程与发送线程之间的有界队列(leaky,满则丢最旧帧) */
#define WEBRTCPUSH_SEND_QUEUE_DEPTH 2U
#define WEBRTCPUSH_SEND_QUEUE_DEPTH 1U
/* 管道缓冲:运动场景需适度缓存,过低易 pull timeout / 糊屏 */
#define WEBRTCPUSH_MPP_PRE_ENC_BUFFERS 1 /* 编码前 leaky,低延迟 */
#define WEBRTCPUSH_MPP_POST_ENC_BUFFERS 1 /* 编码后保留 AU */
#define WEBRTCPUSH_APPSINK_MAX_BUFFERS 1
/* MJPEG 解压:1=GStreamer CPU jpegdec 优先;0=mppjpegdec 优先 */
#define WEBRTCPUSH_MJPEG_CPU_DECODE 1
/* MPP 码控收紧:低 REMB 时实际 H264 不能长期高于目标太多 */
#define WEBRTCPUSH_MPP_BPS_MIN_PERCENT 60U
#define WEBRTCPUSH_MPP_BPS_MAX_PERCENT 102U
#define WEBRTCPUSH_MPP_BPS_MIN_FLOOR 200000U
#define WEBRTCPUSH_MPP_QP_MAX 48
#define WEBRTCPUSH_MPP_QP_MAX_I 48
#define WEBRTCPUSH_MPP_QP_MIN 18
#define WEBRTCPUSH_MPP_QP_MIN_I 18
#define WEBRTCPUSH_MPP_MIN_FORCE_KEY_UNIT_NS 2000000000ULL
/* 视频采集:auto | /dev/videoN | test(videotestsrc 测试图案) */
#define WEBRTCPUSH_VIDEO_DEVICE "auto"
......@@ -70,6 +94,15 @@
#define WEBRTCPUSH_OPUS_CHANNELS 1
#define WEBRTCPUSH_AUDIO_QUEUE_DEPTH 8U
/* 音频播放(手机->设备):ALSA 喇叭设备 */
#define WEBRTCPUSH_AUDIO_PLAYBACK_DEVICE "plughw:2,0"
/* 后端音量控制接口 */
#define WEBRTCPUSH_VOLUME_API_BASE "https://fcrs-api.yd-ss.com/api/drive/use/status/"
#define WEBRTCPUSH_VOLUME_INTERVAL_SEC 3
#define WEBRTCPUSH_VOLUME_DEFAULT 0.5
/*
* 设备侧 DataChannel(myDataChannel)与手机 createDataChannel('init') 争用 SCTP,
* 易触发 sctpenc association error 并导致管道闪断/进程崩溃。仅推流可不建。
......
......@@ -445,23 +445,38 @@ static void handle_wifi_switch(int sock, const struct sockaddr_in *addr, cJSON *
ssid = ssid_item->valuestring;
my_zlog_info("WiFi配置助手: 收到 wifi_switch SSID=%s", ssid);
rc = app_wifi_switch(ssid);
success = (rc == 0);
if (rc == 0)
snprintf(message, sizeof(message), "WiFi 切换成功,正在连接 %s", ssid);
else if (rc == -1)
/* 先校验 SSID 是否已保存(不连接),据此先回复 app,
* 再实际发起切换。避免切到新 WiFi 后 app 收不到响应。 */
rc = app_wifi_check_saved(ssid);
if (rc != 0) {
/* 未保存:直接回失败 */
resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "cmd", "wifi_switch_result");
cJSON_AddBoolToObject(resp, "success", 0);
snprintf(message, sizeof(message), "未找到已保存的 WiFi:%s", ssid);
else /* rc == -2:已保存但连接失败 */
snprintf(message, sizeof(message), "WiFi 已保存但连接失败:%s(信号弱或不可达)", ssid);
cJSON_AddStringToObject(resp, "message", message);
payload = cJSON_PrintUnformatted(resp);
if (payload) { send_json(sock, addr, payload); free(payload); }
cJSON_Delete(resp);
return;
}
/* 已保存:立即回成功,告知 app 已发起切换 */
resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "cmd", "wifi_switch_result");
cJSON_AddBoolToObject(resp, "success", success ? 1 : 0);
cJSON_AddBoolToObject(resp, "success", 1);
snprintf(message, sizeof(message), "WiFi 切换成功,正在连接 %s", ssid);
cJSON_AddStringToObject(resp, "message", message);
payload = cJSON_PrintUnformatted(resp);
if (payload) { send_json(sock, addr, payload); free(payload); }
cJSON_Delete(resp);
/* 回复完毕,再实际发起切换(切换结果不再回,切网后 app 收不到) */
rc = app_wifi_switch(ssid);
if (rc != 0)
my_zlog_error("WiFi配置助手: wifi_switch 实际切换失败 SSID=%s rc=%d", ssid, rc);
else
my_zlog_info("WiFi配置助手: wifi_switch 已发起切换 SSID=%s", ssid);
}
static void handle_wifi_delete(int sock, const struct sockaddr_in *addr, cJSON *root)
......
......@@ -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-06.log"; millisecond
my_log.* "/home/orangepi/car/master/log/log_2026-07-08.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