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,
......
......@@ -17,7 +17,8 @@
#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 matches negotiated 42e01f */
#define MPP_H264_PROFILE 66 /* Baseline: 与 SDP profile-level-id=42e01f 一致,低延迟优先 */
#define MPP_IDR_EVENT_RETRY_US 500000
#define WRTC_V4L2_CAP_VIDEO_CAPTURE (1u << 0)
#define WRTC_V4L2_CAP_VIDEO_CAPTURE_MPLANE (1u << 12)
......@@ -42,6 +43,8 @@ struct MppH264Source {
size_t frame_capacity;
guint keyframe_req;
volatile gboolean pending_idr;
gboolean pending_idr_event_sent;
gint64 last_keyframe_event_us;
volatile gboolean pending_recover;
gulong enc_sink_probe_id;
guint bus_watch_id;
......@@ -52,6 +55,7 @@ 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 send_force_key_unit(MppH264Source *src);
static gboolean bus_watch_cb(GstBus *bus, GstMessage *msg, gpointer user_data)
{
......@@ -283,8 +287,12 @@ static void configure_v4l2_src(GstElement *src, gboolean compressed_mjpeg)
* mmap 兼容性最好,性能损失可接受(720p@24fps)。
*/
g_object_set(src, "io-mode", 2, NULL);
/* 减少内核 DQBUF 缓冲,降低采集延迟 */
/* 减少内核 DQBUF 缓冲,降低采集延迟(部分 v4l2src 无此属性,先检查) */
{
GParamSpec *pspec = g_object_class_find_property(G_OBJECT_GET_CLASS(src), "buffer-size");
if (pspec)
g_object_set(src, "buffer-size", 1024, NULL);
}
}
static GstElement *make_h264_encoder(gboolean prefer_mpp, gboolean use_test,
......@@ -300,11 +308,19 @@ static GstElement *make_h264_encoder(gboolean prefer_mpp, gboolean use_test,
"gop", MPP_VIDEO_GOP,
"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),
"bps-min", (guint)(((guint64)MPP_VIDEO_START_BPS *
WEBRTCPUSH_MPP_BPS_MIN_PERCENT) / 100U),
"bps-max", (guint)(((guint64)MPP_VIDEO_START_BPS *
WEBRTCPUSH_MPP_BPS_MAX_PERCENT) / 100U),
"header-mode", 1,
"profile", MPP_H264_PROFILE,
"level", 31,
"qp-min", WEBRTCPUSH_MPP_QP_MIN,
"qp-min-i", WEBRTCPUSH_MPP_QP_MIN_I,
"qp-max", WEBRTCPUSH_MPP_QP_MAX,
"qp-max-i", WEBRTCPUSH_MPP_QP_MAX_I,
"min-force-key-unit-interval",
(guint64)WEBRTCPUSH_MPP_MIN_FORCE_KEY_UNIT_NS,
"qos", TRUE,
"zero-copy-pkt", TRUE,
NULL);
......@@ -368,31 +384,73 @@ static GstPadProbeReturn enc_sink_idr_probe(GstPad *pad, GstPadProbeInfo *info,
{
MppH264Source *src = user_data;
(void)pad;
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);
gint64 now_us = g_get_monotonic_time();
if (src->pending_idr_event_sent &&
now_us - src->last_keyframe_event_us < MPP_IDR_EVENT_RETRY_US)
return GST_PAD_PROBE_OK;
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);
src->last_keyframe_event_us = now_us;
if (send_force_key_unit(src)) {
src->pending_idr_event_sent = TRUE;
my_zlog_info("mpp_h264_source: IDR request sent (%u)", src->keyframe_req);
} else {
my_zlog_warn("mpp_h264_source: in-stream IDR not handled");
src->pending_idr_event_sent = FALSE;
my_zlog_warn("mpp_h264_source: in-stream IDR request not handled");
}
}
return GST_PAD_PROBE_OK;
}
static gboolean send_force_key_unit(MppH264Source *src)
{
GstPad *src_pad;
GstEvent *ev;
GstClockTime running_time = 0;
gboolean ok = FALSE;
if (!src || !src->enc)
return FALSE;
if (src->pipeline) {
GstClock *clock = gst_element_get_clock(src->pipeline);
if (clock) {
GstClockTime now = gst_clock_get_time(clock);
GstClockTime base = gst_element_get_base_time(src->pipeline);
if (GST_CLOCK_TIME_IS_VALID(now) && GST_CLOCK_TIME_IS_VALID(base) &&
now >= base)
running_time = now - base;
gst_object_unref(clock);
}
}
/*
* force-key-unit 是 upstream event,应从编码器 src pad 往上游发送。
* 发到 sink pad 会触发 wrong-direction,并且 PLI 后经常等不到真正 IDR。
*/
src_pad = gst_element_get_static_pad(src->enc, "src");
if (src_pad) {
ev = gst_video_event_new_upstream_force_key_unit(
running_time, TRUE, src->keyframe_req);
ok = gst_pad_send_event(src_pad, ev);
gst_object_unref(src_pad);
}
if (!ok) {
ev = gst_video_event_new_upstream_force_key_unit(
running_time, TRUE, src->keyframe_req);
ok = gst_element_send_event(src->enc, ev);
}
return ok;
}
static void request_keyframe(MppH264Source *src)
{
if (!src)
......@@ -402,23 +460,10 @@ static void request_keyframe(MppH264Source *src)
src->keyframe_req++;
src->pending_idr = TRUE;
/* 直接向编码器 sink pad 发 force-key-unit(upstream event 标准发送方式) */
if (src->enc) {
GstPad *sink_pad = gst_element_get_static_pad(src->enc, "sink");
if (sink_pad) {
GstEvent *ev = gst_video_event_new_upstream_force_key_unit(
GST_CLOCK_TIME_NONE, TRUE, src->keyframe_req);
gboolean ok = gst_pad_send_event(sink_pad, ev);
gst_object_unref(sink_pad);
if (!ok)
my_zlog_warn("mpp_h264_source: force-key-unit send failed on sink pad");
} else {
/* fallback: element_send_event */
GstEvent *ev = gst_video_event_new_upstream_force_key_unit(
GST_CLOCK_TIME_NONE, TRUE, src->keyframe_req);
gst_element_send_event(src->enc, ev);
}
}
src->pending_idr_event_sent = FALSE;
src->last_keyframe_event_us = 0;
/* IDR 由 enc_sink_idr_probe 在下一个 buffer 经过时发送,
避免外部直接发 sink pad 触发 wrong-direction 警告 */
my_zlog_info("mpp_h264_source: IDR queued (%u)", src->keyframe_req);
}
......@@ -597,12 +642,40 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
vsrc = gst_element_factory_make("v4l2src", "vsrc");
caps_in = gst_element_factory_make("capsfilter", "caps_in");
jpegparse = gst_element_factory_make("jpegparse", "jpegparse");
if (WEBRTCPUSH_MJPEG_CPU_DECODE) {
jpegdec = gst_element_factory_make("jpegdec", "jpegdec");
use_mpp_jpeg = FALSE;
if (jpegdec) {
conv = gst_element_factory_make("videoconvert", "jpegconv");
g_object_set(jpegdec,
"idct-method", 1, /* ifast */
"discard-corrupted-frames", TRUE,
"qos", TRUE,
NULL);
} else {
jpegdec = gst_element_factory_make("mppjpegdec", "jpegdec");
if (jpegdec) {
use_mpp_jpeg = TRUE;
g_object_set(jpegdec, "format", 23, "fast-mode", TRUE, NULL);
}
}
} else {
jpegdec = gst_element_factory_make("mppjpegdec", "jpegdec");
if (jpegdec) {
use_mpp_jpeg = TRUE;
g_object_set(jpegdec, "format", 23, "fast-mode", TRUE, NULL);
} else {
jpegdec = gst_element_factory_make("jpegdec", "jpegdec");
use_mpp_jpeg = FALSE;
if (jpegdec) {
conv = gst_element_factory_make("videoconvert", "jpegconv");
g_object_set(jpegdec,
"idct-method", 1,
"discard-corrupted-frames", TRUE,
"qos", TRUE,
NULL);
}
}
}
mjpeg_fps = (capture_mode == CAPTURE_MJPEG_720P24) ? WEBRTCPUSH_H264_FPS : 30;
g_object_set(vsrc, "device", vdev, NULL);
......@@ -616,9 +689,10 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
gst_caps_unref(caps);
g_free(cap_str);
}
my_zlog_info("mpp_h264_source: 720p MJPEG@%dfps -> %s -> NV12@%dfps",
my_zlog_info("mpp_h264_source: 720p MJPEG@%dfps -> %s%s -> NV12@%dfps",
mjpeg_fps,
use_mpp_jpeg ? "mppjpegdec" : "jpegdec",
use_mpp_jpeg ? "" : "+videoconvert",
WEBRTCPUSH_H264_FPS);
}
......@@ -683,10 +757,19 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
return NULL;
}
} else {
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))) {
gboolean linked;
if (use_mpp_jpeg) {
GstElement *elems[] = {vsrc, caps_in, jpegparse, jpegdec, vrate,
caps_nv12, q, enc, parse, caps_h264, eq, sink};
linked = jpegparse && jpegdec &&
link_pipeline(pipe, elems, G_N_ELEMENTS(elems));
} else {
GstElement *elems[] = {vsrc, caps_in, jpegparse, jpegdec, conv, vrate,
caps_nv12, q, enc, parse, caps_h264, eq, sink};
linked = jpegparse && jpegdec && conv &&
link_pipeline(pipe, elems, G_N_ELEMENTS(elems));
}
if (!linked) {
if (error_message)
*error_message = g_strdup("failed to link MJPEG 720p chain");
gst_object_unref(pipe);
......@@ -774,7 +857,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 < 20) {
while (attempts < 120) { /* require_idr 模式下用 deadline 控制超时,不靠 attempts */
gint wait_ms = (gint)((deadline - g_get_monotonic_time()) / 1000);
if (wait_ms < 1)
......@@ -796,6 +879,12 @@ static gboolean pull_sample(MppH264Source *src, gboolean require_idr,
pts = GST_BUFFER_PTS(buffer);
idr = buffer_is_idr(src->frame_buf, src->frame_size);
if (idr && src->pending_idr) {
src->pending_idr = FALSE;
src->pending_idr_event_sent = FALSE;
src->last_keyframe_event_us = 0;
my_zlog_info("mpp_h264_source: IDR output confirmed (%u)", src->keyframe_req);
}
gst_sample_unref(sample);
if (require_idr && !idr) {
......@@ -826,8 +915,11 @@ gboolean mpp_h264_source_pull(MppH264Source *src, gboolean force_idr,
return FALSE;
/* bus 报 ERROR 后 pipeline 卡死,这里及时恢复避免长期黑屏 */
if (src->pending_recover && mpp_h264_source_recover(src))
if (src->pending_recover && mpp_h264_source_recover(src)) {
src->pending_idr = TRUE;
src->pending_idr_event_sent = FALSE;
src->last_keyframe_event_us = 0;
}
if (timeout_ms < 1)
timeout_ms = (1000 / WEBRTCPUSH_H264_FPS) + 60;
......@@ -838,6 +930,10 @@ gboolean mpp_h264_source_pull(MppH264Source *src, gboolean force_idr,
if (pull_sample(src, TRUE, idr_timeout, data, size, is_idr, pts_ns))
return TRUE;
my_zlog_warn("mpp_h264_source: IDR wait timeout, drain latest frame");
/* 超时后清 pending_idr,避免无限重试导致卡死;下一个 PLI 会重新请求 */
src->pending_idr = FALSE;
src->pending_idr_event_sent = FALSE;
src->last_keyframe_event_us = 0;
if (pull_sample(src, FALSE, 500, data, size, is_idr, pts_ns))
return TRUE;
return FALSE;
......@@ -862,13 +958,21 @@ void mpp_h264_source_set_bitrate(MppH264Source *src, guint bitrate_bps)
f = gst_element_get_factory(src->enc);
name = f ? gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(f)) : NULL;
if (name && g_str_has_prefix(name, "mpph264enc")) {
guint bmin = (guint)(bitrate_bps * 0.80);
guint bmax = (guint)(bitrate_bps * 1.08);
if (bmin < MPP_VIDEO_MIN_BPS)
bmin = MPP_VIDEO_MIN_BPS;
guint bmin = (guint)(((guint64)bitrate_bps *
WEBRTCPUSH_MPP_BPS_MIN_PERCENT) / 100U);
guint bmax = (guint)(((guint64)bitrate_bps *
WEBRTCPUSH_MPP_BPS_MAX_PERCENT) / 100U);
if (bmin < WEBRTCPUSH_MPP_BPS_MIN_FLOOR)
bmin = WEBRTCPUSH_MPP_BPS_MIN_FLOOR;
if (bmax < bitrate_bps)
bmax = bitrate_bps;
if (bmax > MPP_VIDEO_MAX_BPS)
bmax = MPP_VIDEO_MAX_BPS;
g_object_set(src->enc, "bps", bitrate_bps, "bps-min", bmin, "bps-max", bmax, NULL);
g_object_set(src->enc,
"bps", bitrate_bps,
"bps-min", bmin,
"bps-max", bmax,
NULL);
} else if (name && g_str_has_prefix(name, "x264enc")) {
g_object_set(src->enc, "bitrate", bitrate_bps / 1000, NULL);
}
......
......@@ -2,6 +2,8 @@
#include "mpp_h264_source.h"
#include "audio_source.h"
#include "audio_sink.h"
#include "volume_control.h"
#include "webrtcpush_config.h"
#include "webrtcpush_log.h"
#include "ws_signaling.h"
......@@ -27,6 +29,7 @@ typedef struct {
gint payload_type;
gchar *mid;
gboolean found;
gint direction; /* 0=unknown 1=sendonly 2=recvonly 3=sendrecv 4=inactive */
} OfferAudio;
typedef struct {
......@@ -58,6 +61,7 @@ typedef struct {
gboolean track_open;
gboolean need_idr;
gboolean first_frame_sent; /* 首帧 IDR 不受 pacing flush 影响 */
gint64 last_idr_sent_us;
gboolean answer_sent;
int pc;
int track;
......@@ -80,8 +84,12 @@ typedef struct {
/* 音频 Opus track */
AudioSource *audio_source;
AudioSink *audio_sink;
VolumeControl *volume_ctrl;
int audio_track;
gboolean audio_track_open;
gboolean audio_send_enabled;
gboolean audio_recv_enabled;
GThread *audio_send_thread;
guint32 audio_rtp_timestamp;
guint64 audio_pts_base;
......@@ -155,15 +163,23 @@ static void frame_queue_stop(RtcFrameQueue *q)
g_mutex_unlock(&q->lock);
}
static void frame_queue_clear(RtcClient *client)
static guint frame_queue_clear(RtcClient *client)
{
RtcFrameQueue *q = &client->frame_queue;
RtcH264Frame *frame;
guint dropped = 0;
if (!client)
return 0;
g_mutex_lock(&q->lock);
while ((frame = g_queue_pop_head(&q->queue)) != NULL)
while ((frame = g_queue_pop_head(&q->queue)) != NULL) {
dropped++;
q->dropped_total++;
rtc_h264_frame_free(frame);
}
g_mutex_unlock(&q->lock);
return dropped;
}
static void frame_queue_push(RtcClient *client, RtcH264Frame *frame)
......@@ -172,6 +188,7 @@ static void frame_queue_push(RtcClient *client, RtcH264Frame *frame)
RtcH264Frame *old;
gboolean overflowed = FALSE;
gboolean request_resync = FALSE;
guint dropped = 0;
g_mutex_lock(&q->lock);
if (q->stopping) {
......@@ -182,11 +199,13 @@ static void frame_queue_push(RtcClient *client, RtcH264Frame *frame)
if (g_queue_get_length(&q->queue) >= q->max_depth) {
overflowed = TRUE;
while ((old = g_queue_pop_head(&q->queue)) != NULL) {
dropped++;
q->dropped_total++;
rtc_h264_frame_free(old);
}
}
if (overflowed && !frame->is_idr) {
dropped++;
q->dropped_total++;
rtc_h264_frame_free(frame);
request_resync = TRUE;
......@@ -203,7 +222,8 @@ static void frame_queue_push(RtcClient *client, RtcH264Frame *frame)
client->source_pts_valid = FALSE;
client->last_idr_request_us = g_get_monotonic_time();
g_mutex_unlock(&client->lock);
my_zlog_warn("libdatachannel: encoded-frame queue overflow; request clean IDR");
my_zlog_warn("libdatachannel: encoded-frame queue overflow; dropped=%u frame(s), request clean IDR",
dropped);
}
}
......@@ -437,6 +457,7 @@ static void RTC_API on_local_description(int pc, const char *sdp,
g_free(patched);
patched = with_msid;
}
my_zlog_info("libdatachannel: SDP answer:\n%s", patched ? patched : sdp);
json = build_description_json(client->app, "answer", patched ? patched : sdp);
g_free(patched);
if (!json) {
......@@ -462,6 +483,7 @@ static void RTC_API on_local_candidate(int pc, const char *candidate,
if (!client || !candidate || !candidate[0])
return;
my_zlog_info("libdatachannel: local candidate mid=%s: %s", mid ? mid : "?", candidate);
json = build_candidate_json(client, candidate, mid);
if (!json)
return;
......@@ -560,6 +582,31 @@ static void RTC_API on_state_change(int pc, rtcState state, void *ptr)
}
}
/* 接收手机发来的 Opus RTP,depayload 后送入播放管道 */
static void RTC_API on_audio_message(int track, const char *message, int size, void *ptr)
{
RtcClient *client = ptr;
const guint8 *rtp = (const guint8 *)message;
const guint8 *payload;
int payload_len, header_len;
(void)track;
if (!client || !client->audio_sink || !client->audio_recv_enabled ||
!message || size < 12)
return;
header_len = 12 + ((rtp[0] & 0x0F) * 4);
if (size > header_len && (rtp[0] & 0x10)) {
int ext_len = ((rtp[header_len + 2] << 8) | rtp[header_len + 3]) * 4 + 4;
header_len += ext_len;
}
if (size <= header_len)
return;
payload = rtp + header_len;
payload_len = size - header_len;
audio_sink_push_opus(client->audio_sink, payload, (size_t)payload_len);
}
static void RTC_API on_track_open(int track, void *ptr)
{
RtcClient *client = ptr;
......@@ -573,6 +620,7 @@ static void RTC_API on_track_open(int track, void *ptr)
client->track_open = TRUE;
client->source_pts_valid = FALSE;
client->first_frame_sent = FALSE;
client->last_idr_sent_us = 0;
video_open = TRUE;
}
if (client->audio_track == track) {
......@@ -604,6 +652,7 @@ static void RTC_API on_track_closed(int track, void *ptr)
if (client->track == track) {
client->track_open = FALSE;
client->first_frame_sent = FALSE;
client->last_idr_sent_us = 0;
video_closed = TRUE;
}
if (client->audio_track == track) {
......@@ -630,6 +679,7 @@ static void RTC_API on_pli(int track, void *ptr)
RtcClient *client = ptr;
gboolean requested;
int dropped = 0;
guint dropped_frames = 0;
if (!client || client->track != track)
return;
requested = request_idr(client, FALSE);
......@@ -643,9 +693,9 @@ static void RTC_API on_pli(int track, void *ptr)
g_mutex_lock(&client->send_lock);
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);
dropped_frames = frame_queue_clear(client);
my_zlog_info("libdatachannel: RTCP PLI -> MPP IDR; dropped %d RTP packet(s), %u encoded frame(s)",
dropped > 0 ? dropped : 0, dropped_frames);
}
static guint clamp_bitrate(guint bitrate)
......@@ -702,11 +752,12 @@ static gboolean pacing_queue_snapshot(RtcClient *client, int track,
return TRUE;
}
static int clear_stale_pacing(RtcClient *client, int track)
static int clear_stale_pacing(RtcClient *client, int track,
guint *dropped_frames_out)
{
int dropped = 0;
guint dropped_frames;
request_idr(client, TRUE);
g_mutex_lock(&client->lock);
client->source_pts_valid = FALSE;
g_mutex_unlock(&client->lock);
......@@ -714,22 +765,29 @@ static int clear_stale_pacing(RtcClient *client, int track)
if (client->track == track)
dropped = rtcClearPacingQueue(track);
g_mutex_unlock(&client->send_lock);
frame_queue_clear(client);
dropped_frames = frame_queue_clear(client);
if (dropped_frames_out)
*dropped_frames_out = dropped_frames;
return dropped;
}
static void apply_bitrate(RtcClient *client, int track, guint bitrate)
{
guint b;
guint old_b;
if (!client)
return;
b = clamp_bitrate(bitrate);
g_mutex_lock(&client->lock);
old_b = client->target_bitrate;
client->target_bitrate = b;
g_mutex_unlock(&client->lock);
if (old_b == b)
return;
if (track >= 0) {
g_mutex_lock(&client->send_lock);
if (client->track == track)
......@@ -737,6 +795,9 @@ static void apply_bitrate(RtcClient *client, int track, guint bitrate)
g_mutex_unlock(&client->send_lock);
}
mpp_h264_source_set_bitrate(client->h264_source, b);
my_zlog_info("libdatachannel: RTCP bitrate control -> MPP encoder=%u->%u kbps pacing=%u kbps",
old_b / 1000, b / 1000,
pacing_bitrate_for_encoder(b) / 1000);
}
/* Public Internet: climb slowly so a transient REMB spike cannot build latency. */
......@@ -780,6 +841,25 @@ static void ramp_bitrate_toward_ceiling(RtcClient *client, int track)
cur / 1000, next / 1000, tgt / 1000);
}
static guint bitrate_step_down_toward(guint current, guint ceiling)
{
guint diff;
guint step;
if (ceiling >= current)
return current;
diff = current - ceiling;
step = (guint)(((guint64)current * WEBRTCPUSH_REMB_DOWN_STEP_PERCENT) / 100U);
if (step < WEBRTCPUSH_REMB_DOWN_STEP_MIN_BPS)
step = WEBRTCPUSH_REMB_DOWN_STEP_MIN_BPS;
if (step > WEBRTCPUSH_REMB_DOWN_STEP_MAX_BPS)
step = WEBRTCPUSH_REMB_DOWN_STEP_MAX_BPS;
if (step > diff)
step = diff;
return current - step;
}
static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
{
RtcClient *client = ptr;
......@@ -790,8 +870,8 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
guint next = 0;
guint down_samples = 0;
gboolean severe_down = FALSE;
gboolean can_step_down = FALSE;
gint64 now_us;
int dropped = 0;
if (!client || bitrate == 0)
return;
......@@ -803,6 +883,8 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
g_mutex_lock(&client->lock);
current = client->target_bitrate;
client->last_remb_bitrate = bitrate;
can_step_down = now_us - client->last_bitrate_ramp_us >=
(gint64)WEBRTCPUSH_BITRATE_RAMP_DOWN_MS * 1000;
if (client->remb_filtered == 0) {
filtered = bitrate;
......@@ -834,15 +916,17 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
down_samples = client->remb_down_samples;
if (severe_down ||
client->remb_down_samples >= WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS) {
if (!can_step_down) {
g_mutex_unlock(&client->lock);
my_zlog_debug("libdatachannel: REMB down held raw=%u filtered=%u ceiling=%u current=%u kbps",
bitrate / 1000, filtered / 1000,
ceiling / 1000, current / 1000);
return;
}
client->remb_down_samples = 0;
client->target_bitrate = ceiling;
/* target_bitrate is committed by apply_bitrate() after optional flush. */
client->last_bitrate_ramp_us = now_us;
next = ceiling;
}
if (severe_down) {
client->need_idr = TRUE;
client->last_idr_request_us = now_us;
client->source_pts_valid = FALSE;
next = bitrate_step_down_toward(current, ceiling);
}
} else if (ceiling >= current) {
client->remb_down_samples = 0;
......@@ -850,25 +934,15 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
g_mutex_unlock(&client->lock);
if (next) {
g_mutex_lock(&client->send_lock);
if (client->track == track) {
if (severe_down) {
dropped = rtcClearPacingQueue(track);
frame_queue_clear(client);
}
rtcSetPacingBitrate(track, pacing_bitrate_for_encoder(next));
}
g_mutex_unlock(&client->send_lock);
mpp_h264_source_set_bitrate(client->h264_source, next);
apply_bitrate(client, track, next);
if (severe_down) {
my_zlog_info("libdatachannel: REMB raw=%u filtered=%u kbps, encoder=%u pacing=%u kbps, resync dropped=%d RTP packets",
my_zlog_info("libdatachannel: RTCP REMB raw=%u filtered=%u kbps, encoder=%u kbps ceiling=%u kbps, severe smoothed",
bitrate / 1000, filtered / 1000, next / 1000,
pacing_bitrate_for_encoder(next) / 1000,
dropped > 0 ? dropped : 0);
ceiling / 1000);
} else {
my_zlog_info("libdatachannel: REMB raw=%u filtered=%u kbps, encoder=%u pacing=%u kbps after %u samples",
my_zlog_info("libdatachannel: RTCP REMB raw=%u filtered=%u kbps, encoder=%u kbps ceiling=%u kbps after %u samples",
bitrate / 1000, filtered / 1000, next / 1000,
pacing_bitrate_for_encoder(next) / 1000, down_samples);
ceiling / 1000, down_samples);
}
} else if (ceiling > current) {
my_zlog_debug("libdatachannel: REMB raw=%u filtered=%u ceiling=%u kbps (slow ramp)",
......@@ -916,13 +990,11 @@ static gpointer pull_thread_main(gpointer data)
RtcClient *client = data;
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 track_open;
gboolean force_idr;
gboolean need_idr_now;
const guint8 *frame_data;
size_t frame_size;
......@@ -933,17 +1005,22 @@ static gpointer pull_thread_main(gpointer data)
g_mutex_lock(&client->lock);
stopping = client->stopping;
track_open = client->track_open && client->track >= 0;
force_idr = track_open && client->need_idr;
need_idr_now = track_open && client->need_idr;
g_mutex_unlock(&client->lock);
if (stopping)
break;
if (!track_open) {
g_usleep(20000);
continue;
}
{
gint pull_timeout = force_idr ? 2500 : pull_ms;
if (need_idr_now)
mpp_h264_source_request_keyframe(client->h264_source);
if (!mpp_h264_source_pull(client->h264_source, force_idr, &frame_data,
{
if (!mpp_h264_source_pull(client->h264_source, FALSE, &frame_data,
&frame_size, &is_idr, &source_pts,
pull_timeout)) {
pull_ms)) {
gint64 now_us = g_get_monotonic_time();
pull_fail_streak++;
if (now_us - last_pull_warn_us >= 3000000 ||
......@@ -967,22 +1044,14 @@ static gpointer pull_thread_main(gpointer data)
if (!track_open)
continue;
if (is_idr) {
g_mutex_lock(&client->lock);
need_idr_now = client->need_idr;
g_mutex_unlock(&client->lock);
if (need_idr_now && !is_idr) {
idr_discard_streak++;
if (idr_discard_streak >= WEBRTCPUSH_H264_FPS) {
g_mutex_lock(&client->lock);
if (client->need_idr) {
client->need_idr = FALSE;
g_mutex_unlock(&client->lock);
idr_discard_streak = 0;
my_zlog_warn("libdatachannel: IDR wait >2s, resume with latest frames");
client->last_idr_sent_us = g_get_monotonic_time();
}
continue;
g_mutex_unlock(&client->lock);
}
idr_discard_streak = 0;
queued = rtc_h264_frame_new(frame_data, frame_size, is_idr, source_pts);
frame_queue_push(client, queued);
......@@ -1047,11 +1116,33 @@ static gpointer send_thread_main(gpointer data)
client->first_frame_sent &&
now - last_pacing_flush_us >=
(gint64)WEBRTCPUSH_PACING_GUARD_COOLDOWN_MS * 1000) {
int dropped = clear_stale_pacing(client, track);
guint dropped_frames = 0;
int dropped = clear_stale_pacing(client, track, &dropped_frames);
gboolean should_request_idr;
gint64 last_idr_us;
gboolean need_idr_pending;
last_pacing_flush_us = now;
my_zlog_warn("libdatachannel: pacing backlog %u packets/%u bytes ~= %ums; dropped=%d and request IDR",
g_mutex_lock(&client->lock);
last_idr_us = client->last_idr_sent_us;
need_idr_pending = client->need_idr;
should_request_idr =
!need_idr_pending &&
(last_idr_us == 0 ||
now - last_idr_us >=
(gint64)WEBRTCPUSH_PACING_RESYNC_IDR_MS * 1000);
g_mutex_unlock(&client->lock);
if (should_request_idr) {
request_idr(client, TRUE);
my_zlog_warn("libdatachannel: pacing backlog %u packets/%u bytes ~= %ums; dropped=%d RTP/%u encoded and request IDR",
pacing_packets, pacing_bytes, pacing_delay_ms,
dropped > 0 ? dropped : 0);
dropped > 0 ? dropped : 0, dropped_frames);
} else {
my_zlog_warn("libdatachannel: pacing backlog %u packets/%u bytes ~= %ums; dropped=%d RTP/%u encoded without IDR (recent/pending)",
pacing_packets, pacing_bytes, pacing_delay_ms,
dropped > 0 ? dropped : 0, dropped_frames);
}
}
}
......@@ -1145,8 +1236,10 @@ static gpointer send_thread_main(gpointer data)
g_mutex_lock(&client->lock);
client->rtp_timestamp = timestamp + timestamp_step;
if (frame->is_idr)
if (frame->is_idr) {
client->need_idr = FALSE;
client->last_idr_sent_us = g_get_monotonic_time();
}
g_mutex_unlock(&client->lock);
rtc_h264_frame_free(frame);
......@@ -1164,6 +1257,7 @@ static gpointer audio_send_thread_main(gpointer data)
for (;;) {
gboolean stopping;
gboolean audio_send_enabled;
int track;
const uint8_t *frame_data;
size_t frame_size;
......@@ -1177,14 +1271,17 @@ static gpointer audio_send_thread_main(gpointer data)
break;
g_mutex_lock(&client->lock);
while (!client->stopping && (!client->audio_track_open || client->audio_track < 0))
while (!client->stopping &&
(!client->audio_track_open || client->audio_track < 0 ||
!client->audio_send_enabled))
g_cond_wait(&client->track_cond, &client->lock);
stopping = client->stopping;
track = client->audio_track;
audio_send_enabled = client->audio_send_enabled;
g_mutex_unlock(&client->lock);
if (stopping)
break;
if (track < 0)
if (track < 0 || !audio_send_enabled)
continue;
if (!audio_source_pull(client->audio_source, &frame_data, &frame_size,
......@@ -1239,6 +1336,7 @@ static void offer_video_clear(OfferVideo *video)
static void offer_audio_clear(OfferAudio *audio)
{
audio->direction = 0;
g_free(audio->mid);
memset(audio, 0, sizeof(*audio));
audio->payload_type = -1;
......@@ -1290,6 +1388,14 @@ static gboolean parse_offer(const char *sdp, OfferVideo *video, OfferAudio *audi
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=sendonly")) {
audio->direction = 1;
} else if (g_str_has_prefix(line, "a=recvonly")) {
audio->direction = 2;
} else if (g_str_has_prefix(line, "a=sendrecv")) {
audio->direction = 3;
} else if (g_str_has_prefix(line, "a=inactive")) {
audio->direction = 4;
} else if (g_str_has_prefix(line, "a=rtpmap:")) {
gint pt = -1;
gchar codec[32] = {0};
......@@ -1328,8 +1434,11 @@ static void destroy_peer(RtcClient *client)
client->track = -1;
client->track_open = FALSE;
client->first_frame_sent = FALSE;
client->last_idr_sent_us = 0;
client->audio_track = -1;
client->audio_track_open = FALSE;
client->audio_send_enabled = FALSE;
client->audio_recv_enabled = FALSE;
client->answer_sent = FALSE;
client->need_idr = TRUE;
client->source_pts_valid = FALSE;
......@@ -1426,12 +1535,34 @@ static gboolean setup_audio_track(RtcClient *client, int pc,
rtcTrackInit track_init;
rtcPacketizerInit packetizer;
guint32 ssrc = g_random_int();
gboolean offer_send;
gboolean offer_recv;
gboolean send_audio;
gboolean recv_audio;
int track;
if (ssrc == 0)
ssrc = 1;
memset(&track_init, 0, sizeof(track_init));
/* SDP 未声明 direction 时按 WebRTC 默认 sendrecv 处理。 */
offer_send = (audio->direction == 0 || audio->direction == 1 ||
audio->direction == 3);
offer_recv = (audio->direction == 0 || audio->direction == 2 ||
audio->direction == 3);
send_audio = offer_recv && client->audio_source;
recv_audio = offer_send && client->audio_sink;
if (send_audio && recv_audio)
track_init.direction = RTC_DIRECTION_SENDRECV;
else if (send_audio)
track_init.direction = RTC_DIRECTION_SENDONLY;
else if (recv_audio)
track_init.direction = RTC_DIRECTION_RECVONLY;
else
track_init.direction = RTC_DIRECTION_INACTIVE;
my_zlog_info("libdatachannel: audio track direction: offer=%d answer=%d",
audio->direction, (int)track_init.direction);
track_init.codec = RTC_CODEC_OPUS;
track_init.payloadType = audio->payload_type;
track_init.ssrc = ssrc;
......@@ -1447,6 +1578,8 @@ static gboolean setup_audio_track(RtcClient *client, int pc,
rtcSetOpenCallback(track, on_track_open);
rtcSetClosedCallback(track, on_track_closed);
rtcSetErrorCallback(track, on_track_error);
if (recv_audio)
rtcSetMessageCallback(track, on_audio_message);
memset(&packetizer, 0, sizeof(packetizer));
packetizer.ssrc = ssrc;
......@@ -1456,16 +1589,21 @@ static gboolean setup_audio_track(RtcClient *client, int pc,
packetizer.sequenceNumber = (guint16)g_random_int();
packetizer.timestamp = g_random_int();
if (send_audio) {
if (rtcSetOpusPacketizer(track, &packetizer) < 0 ||
rtcChainRtcpSrReporter(track) < 0 ||
rtcChainRtcpNackResponder(track, WEBRTCPUSH_NACK_PACKETS) < 0) {
rtcDeleteTrack(track);
return FALSE;
}
}
/* recv_audio 的 RTP payload 由 on_audio_message 送入 audio_sink。 */
g_mutex_lock(&client->lock);
client->audio_rtp_timestamp = packetizer.timestamp;
client->audio_pts_valid = FALSE;
client->audio_send_enabled = send_audio;
client->audio_recv_enabled = recv_audio;
g_mutex_unlock(&client->lock);
*track_out = track;
return TRUE;
......@@ -1547,6 +1685,22 @@ int rtc_client_start(AppState *app)
g_free(audio_err);
}
}
/* 音频播放(手机->设备) */
{
char *play_err = NULL;
client->audio_sink = audio_sink_start(WEBRTCPUSH_AUDIO_PLAYBACK_DEVICE, &play_err);
if (client->audio_sink) {
my_zlog_info("libdatachannel: audio sink started device=%s", WEBRTCPUSH_AUDIO_PLAYBACK_DEVICE);
if (app->dev_room && app->dev_room[0]) {
client->volume_ctrl = volume_control_start(app->dev_room, client->audio_sink);
my_zlog_info("libdatachannel: volume control started vehicle=%s", app->dev_room);
}
} else {
my_zlog_warn("libdatachannel: audio sink start failed: %s", play_err ? play_err : "?");
g_free(play_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,
......@@ -1580,6 +1734,10 @@ void rtc_client_stop(AppState *app)
g_thread_join(client->send_thread);
if (client->audio_send_thread)
g_thread_join(client->audio_send_thread);
if (client->volume_ctrl)
volume_control_stop(client->volume_ctrl);
if (client->audio_sink)
audio_sink_stop(client->audio_sink);
if (client->audio_source)
audio_source_stop(client->audio_source);
......@@ -1653,16 +1811,18 @@ int rtc_client_handle_offer(AppState *app, const char *sdp)
return -1;
}
/* 若手机 offer 包含 audio m-line 且设备端音频源已启动,建 Opus track */
if (audio.found && audio.payload_type >= 0 && client->audio_source) {
/* 若手机 offer 包含 audio m-line,按本机麦克风/喇叭能力协商方向。 */
if (audio.found && audio.payload_type >= 0 &&
(client->audio_source || client->audio_sink)) {
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);
my_zlog_info("libdatachannel: Opus audio track created mid=%s pt=%d dir=%d",
audio.mid, audio.payload_type, audio.direction);
} else {
my_zlog_warn("libdatachannel: failed to create Opus audio track");
}
}
g_mutex_lock(&client->lock);
client->pc = pc;
client->track = track;
......
#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