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

加入了手机去配网

parent d92f8afb
...@@ -978,6 +978,30 @@ cJSON *app_wifi_list_query(void) { ...@@ -978,6 +978,30 @@ cJSON *app_wifi_list_query(void) {
} }
/* 切换当前连接到已保存的指定 SSID */ /* 切换当前连接到已保存的指定 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) { int app_wifi_switch(const char *ssid) {
saved_wifi_entry_t entries[MAX_WIFI_ENTRIES]; saved_wifi_entry_t entries[MAX_WIFI_ENTRIES];
int count; int count;
......
...@@ -27,6 +27,10 @@ cJSON *app_wifi_list_query(void); ...@@ -27,6 +27,10 @@ cJSON *app_wifi_list_query(void);
/* 切换当前连接到已保存的指定 SSID。 /* 切换当前连接到已保存的指定 SSID。
* 返回 0=已发起切换(异步), -1=未找到/失败 */ * 返回 0=已发起切换(异步), -1=未找到/失败 */
/* 检查指定 SSID 是否已保存(不连接)。
* 返回 0=已保存, -1=未找到 */
int app_wifi_check_saved(const char *ssid);
int app_wifi_switch(const char *ssid); int app_wifi_switch(const char *ssid);
/* 删除已保存的指定 SSID(受保护的 jking 不可删)。 /* 删除已保存的指定 SSID(受保护的 jking 不可删)。
......
No preview for this file type
...@@ -23,7 +23,6 @@ ...@@ -23,7 +23,6 @@
#define DIAG_LOG_FILENAME_MAX 128 #define DIAG_LOG_FILENAME_MAX 128
#define DIAG_LOG_PATH_MAX 512 #define DIAG_LOG_PATH_MAX 512
#define DIAG_BROWSER_URL_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) static void diag_send_json(int sock, const struct sockaddr_in *addr, const char *json)
{ {
...@@ -300,6 +299,13 @@ typedef struct { ...@@ -300,6 +299,13 @@ typedef struct {
time_t mtime; time_t mtime;
} log_entry_t; } 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) static int diag_log_compare_mtime(const void *a, const void *b)
{ {
const log_entry_t *ea = (const log_entry_t *)a; 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) ...@@ -356,57 +362,33 @@ static int diag_find_recent_logs(log_entry_t *entries, int max_count)
return count; return count;
} }
/* TCP 线程:接收已 listen 的 server_fd,做 accept + 文件传输 */
static void *diag_log_tcp_thread(void *arg) static void *diag_log_tcp_thread(void *arg)
{ {
log_entry_t *entries = (log_entry_t *)arg; log_tcp_ctx_t *ctx = (log_tcp_ctx_t *)arg;
int server_fd, client_fd; int server_fd = ctx->server_fd;
struct sockaddr_in addr; int client_fd;
int opt = 1; struct timeval tv;
char filename[DIAG_LOG_FILENAME_MAX + 2]; char filename[DIAG_LOG_FILENAME_MAX + 2];
server_fd = socket(AF_INET, SOCK_STREAM, 0); /* accept 超时 30 秒 */
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;
tv.tv_sec = 30; tv.tv_sec = 30;
tv.tv_usec = 0; tv.tv_usec = 0;
setsockopt(server_fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); 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); client_fd = accept(server_fd, NULL, NULL);
if (client_fd < 0) { if (client_fd < 0) {
my_zlog_error("设备调试: TCP accept 超时或失败"); my_zlog_error("设备调试: TCP accept 超时或失败");
close(server_fd); close(server_fd);
free(entries); free(ctx);
return NULL; return NULL;
} }
my_zlog_info("设备调试: TCP 客户端已连接,开始传输");
/* 循环接收文件名请求 */
while (1) { while (1) {
ssize_t n; ssize_t n;
size_t flen = 0; size_t flen = 0;
...@@ -419,12 +401,13 @@ static void *diag_log_tcp_thread(void *arg) ...@@ -419,12 +401,13 @@ static void *diag_log_tcp_thread(void *arg)
char buf[4096]; char buf[4096];
size_t r; size_t r;
/* 逐字符读文件名直到 \n */
while (flen < sizeof(filename) - 1) { while (flen < sizeof(filename) - 1) {
n = recv(client_fd, &ch, 1, 0); n = recv(client_fd, &ch, 1, 0);
if (n <= 0) { if (n <= 0) {
close(client_fd); close(client_fd);
close(server_fd); close(server_fd);
free(entries); free(ctx);
my_zlog_info("设备调试: TCP 日志传输完成"); my_zlog_info("设备调试: TCP 日志传输完成");
return NULL; return NULL;
} }
...@@ -434,11 +417,12 @@ static void *diag_log_tcp_thread(void *arg) ...@@ -434,11 +417,12 @@ static void *diag_log_tcp_thread(void *arg)
} }
filename[flen] = '\0'; filename[flen] = '\0';
/* 安全校验:文件名只能包含字母数字下划线和点 */
int valid = 1; int valid = 1;
for (size_t i = 0; i < flen; i++) { for (size_t i = 0; i < flen; i++) {
c = (unsigned char)filename[i]; c = (unsigned char)filename[i];
if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || if (!((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') || c == '_' || c == '.')) { (c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.')) {
valid = 0; valid = 0;
break; break;
} }
...@@ -463,6 +447,7 @@ static void *diag_log_tcp_thread(void *arg) ...@@ -463,6 +447,7 @@ static void *diag_log_tcp_thread(void *arg)
fsize = ftell(f); fsize = ftell(f);
rewind(f); rewind(f);
/* 8 字节大端序发送文件大小 */
uint64_t sz = (uint64_t)fsize; uint64_t sz = (uint64_t)fsize;
for (int i = 7; i >= 0; i--) { for (int i = 7; i >= 0; i--) {
sizebuf[i] = (uint8_t)(sz & 0xFF); sizebuf[i] = (uint8_t)(sz & 0xFF);
...@@ -470,6 +455,7 @@ static void *diag_log_tcp_thread(void *arg) ...@@ -470,6 +455,7 @@ static void *diag_log_tcp_thread(void *arg)
} }
send(client_fd, sizebuf, 8, 0); send(client_fd, sizebuf, 8, 0);
/* 发送文件内容 */
while ((r = fread(buf, 1, sizeof(buf), f)) > 0) { while ((r = fread(buf, 1, sizeof(buf), f)) > 0) {
size_t sent = 0; size_t sent = 0;
while (sent < r) { while (sent < r) {
...@@ -487,7 +473,7 @@ static void *diag_log_tcp_thread(void *arg) ...@@ -487,7 +473,7 @@ static void *diag_log_tcp_thread(void *arg)
close(client_fd); close(client_fd);
close(server_fd); close(server_fd);
free(entries); free(ctx);
return NULL; return NULL;
} }
...@@ -499,10 +485,13 @@ void device_diag_handle_log_fetch(int sock, const struct sockaddr_in *addr, cJSO ...@@ -499,10 +485,13 @@ void device_diag_handle_log_fetch(int sock, const struct sockaddr_in *addr, cJSO
cJSON *arr; cJSON *arr;
char *payload; char *payload;
log_entry_t entries[DIAG_LOG_MAX_FILES]; log_entry_t entries[DIAG_LOG_MAX_FILES];
log_entry_t *thread_entries;
int n; int n;
int server_fd;
struct sockaddr_in tcp_addr;
int opt = 1;
pthread_t tid; pthread_t tid;
pthread_attr_t attr; pthread_attr_t attr;
log_tcp_ctx_t *ctx;
pwd_item = cJSON_GetObjectItemCaseSensitive(root, "password"); pwd_item = cJSON_GetObjectItemCaseSensitive(root, "password");
if (!cJSON_IsString(pwd_item) || !pwd_item->valuestring) { 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 ...@@ -511,6 +500,7 @@ void device_diag_handle_log_fetch(int sock, const struct sockaddr_in *addr, cJSO
password = pwd_item->valuestring; password = pwd_item->valuestring;
} }
/* 密码校验 */
if (strcmp(password, DIAG_LOG_PASSWORD) != 0) { if (strcmp(password, DIAG_LOG_PASSWORD) != 0) {
resp = cJSON_CreateObject(); resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "cmd", "log_fetch_ready"); 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 ...@@ -527,6 +517,7 @@ void device_diag_handle_log_fetch(int sock, const struct sockaddr_in *addr, cJSO
return; return;
} }
/* 查找最近 2 个日志 */
n = diag_find_recent_logs(entries, DIAG_LOG_MAX_FILES); n = diag_find_recent_logs(entries, DIAG_LOG_MAX_FILES);
if (n <= 0) { if (n <= 0) {
resp = cJSON_CreateObject(); resp = cJSON_CreateObject();
...@@ -544,35 +535,36 @@ void device_diag_handle_log_fetch(int sock, const struct sockaddr_in *addr, cJSO ...@@ -544,35 +535,36 @@ void device_diag_handle_log_fetch(int sock, const struct sockaddr_in *addr, cJSO
return; return;
} }
thread_entries = (log_entry_t *)malloc(sizeof(log_entry_t) * n); /* 关键修复:先创建 TCP server 并 listen 成功,再回 log_fetch_ready。
if (!thread_entries) { * 避免"手机收到 ready 后立即连接,但设备还没 listen,连接被拒绝"的时序问题。 */
my_zlog_error("设备调试: malloc 失败"); server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd < 0) {
my_zlog_error("设备调试: TCP socket 创建失败");
return; return;
} }
for (int i = 0; i < n; i++)
thread_entries[i] = entries[i];
pthread_attr_init(&attr); setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
if (pthread_create(&tid, &attr, diag_log_tcp_thread, thread_entries) != 0) { memset(&tcp_addr, 0, sizeof(tcp_addr));
my_zlog_error("设备调试: TCP 线程创建失败"); tcp_addr.sin_family = AF_INET;
free(thread_entries); tcp_addr.sin_port = htons(DIAG_LOG_TCP_PORT);
pthread_attr_destroy(&attr); tcp_addr.sin_addr.s_addr = INADDR_ANY;
resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "cmd", "log_fetch_ready"); if (bind(server_fd, (struct sockaddr *)&tcp_addr, sizeof(tcp_addr)) < 0) {
cJSON_AddNumberToObject(resp, "tcpPort", 0); my_zlog_error("设备调试: TCP bind %d 失败", DIAG_LOG_TCP_PORT);
cJSON_AddItemToObject(resp, "files", cJSON_CreateArray()); close(server_fd);
cJSON_AddStringToObject(resp, "message", "设备 TCP 服务启动失败"); return;
payload = cJSON_PrintUnformatted(resp);
if (payload) {
diag_send_json(sock, addr, payload);
free(payload);
} }
cJSON_Delete(resp);
if (listen(server_fd, 1) < 0) {
my_zlog_error("设备调试: TCP listen 失败");
close(server_fd);
return; 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(); resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "cmd", "log_fetch_ready"); cJSON_AddStringToObject(resp, "cmd", "log_fetch_ready");
cJSON_AddNumberToObject(resp, "tcpPort", DIAG_LOG_TCP_PORT); 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 ...@@ -591,6 +583,30 @@ void device_diag_handle_log_fetch(int sock, const struct sockaddr_in *addr, cJSO
free(payload); free(payload);
} }
cJSON_Delete(resp); 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); my_zlog_info("设备调试: log_fetch 已启动 TCP %d,共 %d 个日志", DIAG_LOG_TCP_PORT, n);
} }
......
...@@ -288,11 +288,50 @@ static gboolean gst_check_required_plugins(void) ...@@ -288,11 +288,50 @@ static gboolean gst_check_required_plugins(void)
my_zlog_warn("runtime_deps: missing GStreamer element %s", g->element); my_zlog_warn("runtime_deps: missing GStreamer element %s", g->element);
return FALSE; 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); my_zlog_warn("runtime_deps: element not ready %s", g->element);
return FALSE; 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()) if (!gst_h264_encoder_ok() || !gst_audio_capture_ok())
return FALSE; return FALSE;
return TRUE; 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) ...@@ -63,9 +63,14 @@ AudioSource *audio_source_start(const char *alsa_device, char **error_message)
g_object_set(enc, g_object_set(enc,
"bitrate", WEBRTCPUSH_OPUS_BITRATE, "bitrate", WEBRTCPUSH_OPUS_BITRATE,
"bitrate-type", 0, /* 0=cbr */ "bitrate-type", 0, /* 0=cbr */
"framesize", 20, /* 20ms 帧 */
"inband-fec", TRUE, "inband-fec", TRUE,
NULL); 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, g_object_set(sink,
"emit-signals", FALSE, "emit-signals", FALSE,
......
...@@ -17,7 +17,8 @@ ...@@ -17,7 +17,8 @@
#define MPP_VIDEO_START_BPS WEBRTCPUSH_INITIAL_BITRATE #define MPP_VIDEO_START_BPS WEBRTCPUSH_INITIAL_BITRATE
#define MPP_VIDEO_MIN_BPS WEBRTCPUSH_MIN_BITRATE #define MPP_VIDEO_MIN_BPS WEBRTCPUSH_MIN_BITRATE
#define MPP_VIDEO_MAX_BPS WEBRTCPUSH_MAX_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 (1u << 0)
#define WRTC_V4L2_CAP_VIDEO_CAPTURE_MPLANE (1u << 12) #define WRTC_V4L2_CAP_VIDEO_CAPTURE_MPLANE (1u << 12)
...@@ -42,6 +43,8 @@ struct MppH264Source { ...@@ -42,6 +43,8 @@ struct MppH264Source {
size_t frame_capacity; size_t frame_capacity;
guint keyframe_req; guint keyframe_req;
volatile gboolean pending_idr; volatile gboolean pending_idr;
gboolean pending_idr_event_sent;
gint64 last_keyframe_event_us;
volatile gboolean pending_recover; volatile gboolean pending_recover;
gulong enc_sink_probe_id; gulong enc_sink_probe_id;
guint bus_watch_id; guint bus_watch_id;
...@@ -52,6 +55,7 @@ static gboolean pull_sample(MppH264Source *src, gboolean require_idr, ...@@ -52,6 +55,7 @@ static gboolean pull_sample(MppH264Source *src, gboolean require_idr,
gint timeout_ms, const uint8_t **data, gint timeout_ms, const uint8_t **data,
size_t *size, gboolean *is_idr, size_t *size, gboolean *is_idr,
guint64 *pts_ns); guint64 *pts_ns);
static gboolean send_force_key_unit(MppH264Source *src);
static gboolean bus_watch_cb(GstBus *bus, GstMessage *msg, gpointer user_data) 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) ...@@ -283,8 +287,12 @@ static void configure_v4l2_src(GstElement *src, gboolean compressed_mjpeg)
* mmap 兼容性最好,性能损失可接受(720p@24fps)。 * mmap 兼容性最好,性能损失可接受(720p@24fps)。
*/ */
g_object_set(src, "io-mode", 2, NULL); 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); g_object_set(src, "buffer-size", 1024, NULL);
}
} }
static GstElement *make_h264_encoder(gboolean prefer_mpp, gboolean use_test, 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, ...@@ -300,11 +308,19 @@ static GstElement *make_h264_encoder(gboolean prefer_mpp, gboolean use_test,
"gop", MPP_VIDEO_GOP, "gop", MPP_VIDEO_GOP,
"rc-mode", 1, /* CBR */ "rc-mode", 1, /* CBR */
"bps", MPP_VIDEO_START_BPS, "bps", MPP_VIDEO_START_BPS,
"bps-min", (guint)(MPP_VIDEO_START_BPS * 0.80), "bps-min", (guint)(((guint64)MPP_VIDEO_START_BPS *
"bps-max", (guint)(MPP_VIDEO_START_BPS * 1.08), WEBRTCPUSH_MPP_BPS_MIN_PERCENT) / 100U),
"bps-max", (guint)(((guint64)MPP_VIDEO_START_BPS *
WEBRTCPUSH_MPP_BPS_MAX_PERCENT) / 100U),
"header-mode", 1, "header-mode", 1,
"profile", MPP_H264_PROFILE, "profile", MPP_H264_PROFILE,
"level", 31, "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, "qos", TRUE,
"zero-copy-pkt", TRUE, "zero-copy-pkt", TRUE,
NULL); NULL);
...@@ -368,31 +384,73 @@ static GstPadProbeReturn enc_sink_idr_probe(GstPad *pad, GstPadProbeInfo *info, ...@@ -368,31 +384,73 @@ static GstPadProbeReturn enc_sink_idr_probe(GstPad *pad, GstPadProbeInfo *info,
{ {
MppH264Source *src = user_data; MppH264Source *src = user_data;
(void)pad;
if (!(GST_PAD_PROBE_INFO_TYPE(info) & GST_PAD_PROBE_TYPE_BUFFER)) if (!(GST_PAD_PROBE_INFO_TYPE(info) & GST_PAD_PROBE_TYPE_BUFFER))
return GST_PAD_PROBE_OK; return GST_PAD_PROBE_OK;
if (!src || !src->pending_idr) if (!src || !src->pending_idr)
return GST_PAD_PROBE_OK; return GST_PAD_PROBE_OK;
{ {
GstEvent *ev = gst_video_event_new_upstream_force_key_unit( gint64 now_us = g_get_monotonic_time();
GST_CLOCK_TIME_NONE, TRUE, src->keyframe_req); if (src->pending_idr_event_sent &&
gboolean ok = gst_pad_send_event(pad, ev); now_us - src->last_keyframe_event_us < MPP_IDR_EVENT_RETRY_US)
return GST_PAD_PROBE_OK;
if (!ok && src->enc) { src->last_keyframe_event_us = now_us;
ev = gst_video_event_new_upstream_force_key_unit( if (send_force_key_unit(src)) {
GST_CLOCK_TIME_NONE, TRUE, src->keyframe_req); src->pending_idr_event_sent = TRUE;
ok = gst_element_send_event(src->enc, ev); my_zlog_info("mpp_h264_source: IDR request sent (%u)", src->keyframe_req);
}
if (ok) {
src->pending_idr = FALSE;
my_zlog_info("mpp_h264_source: IDR armed in-stream (%u)", src->keyframe_req);
} else { } 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; 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) static void request_keyframe(MppH264Source *src)
{ {
if (!src) if (!src)
...@@ -402,23 +460,10 @@ static void request_keyframe(MppH264Source *src) ...@@ -402,23 +460,10 @@ static void request_keyframe(MppH264Source *src)
src->keyframe_req++; src->keyframe_req++;
src->pending_idr = TRUE; src->pending_idr = TRUE;
/* 直接向编码器 sink pad 发 force-key-unit(upstream event 标准发送方式) */ src->pending_idr_event_sent = FALSE;
if (src->enc) { src->last_keyframe_event_us = 0;
GstPad *sink_pad = gst_element_get_static_pad(src->enc, "sink"); /* IDR 由 enc_sink_idr_probe 在下一个 buffer 经过时发送,
if (sink_pad) { 避免外部直接发 sink pad 触发 wrong-direction 警告 */
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);
}
}
my_zlog_info("mpp_h264_source: IDR queued (%u)", src->keyframe_req); 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 ...@@ -597,12 +642,40 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
vsrc = gst_element_factory_make("v4l2src", "vsrc"); vsrc = gst_element_factory_make("v4l2src", "vsrc");
caps_in = gst_element_factory_make("capsfilter", "caps_in"); caps_in = gst_element_factory_make("capsfilter", "caps_in");
jpegparse = gst_element_factory_make("jpegparse", "jpegparse"); 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"); jpegdec = gst_element_factory_make("mppjpegdec", "jpegdec");
if (jpegdec) { if (jpegdec) {
use_mpp_jpeg = TRUE; use_mpp_jpeg = TRUE;
g_object_set(jpegdec, "format", 23, "fast-mode", TRUE, NULL); g_object_set(jpegdec, "format", 23, "fast-mode", TRUE, NULL);
} else { } else {
jpegdec = gst_element_factory_make("jpegdec", "jpegdec"); 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; mjpeg_fps = (capture_mode == CAPTURE_MJPEG_720P24) ? WEBRTCPUSH_H264_FPS : 30;
g_object_set(vsrc, "device", vdev, NULL); g_object_set(vsrc, "device", vdev, NULL);
...@@ -616,9 +689,10 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess ...@@ -616,9 +689,10 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
gst_caps_unref(caps); gst_caps_unref(caps);
g_free(cap_str); 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, mjpeg_fps,
use_mpp_jpeg ? "mppjpegdec" : "jpegdec", use_mpp_jpeg ? "mppjpegdec" : "jpegdec",
use_mpp_jpeg ? "" : "+videoconvert",
WEBRTCPUSH_H264_FPS); WEBRTCPUSH_H264_FPS);
} }
...@@ -683,10 +757,19 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess ...@@ -683,10 +757,19 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
return NULL; return NULL;
} }
} else { } else {
GstElement *elems[] = {vsrc, caps_in, jpegparse, jpegdec, vrate, caps_nv12, gboolean linked;
q, enc, parse, caps_h264, eq, sink}; if (use_mpp_jpeg) {
if (!jpegparse || !jpegdec || GstElement *elems[] = {vsrc, caps_in, jpegparse, jpegdec, vrate,
!link_pipeline(pipe, elems, G_N_ELEMENTS(elems))) { 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) if (error_message)
*error_message = g_strdup("failed to link MJPEG 720p chain"); *error_message = g_strdup("failed to link MJPEG 720p chain");
gst_object_unref(pipe); gst_object_unref(pipe);
...@@ -774,7 +857,7 @@ static gboolean pull_sample(MppH264Source *src, gboolean require_idr, ...@@ -774,7 +857,7 @@ static gboolean pull_sample(MppH264Source *src, gboolean require_idr,
appsink = GST_APP_SINK(src->appsink); appsink = GST_APP_SINK(src->appsink);
deadline = g_get_monotonic_time() + (gint64)timeout_ms * 1000; 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); gint wait_ms = (gint)((deadline - g_get_monotonic_time()) / 1000);
if (wait_ms < 1) if (wait_ms < 1)
...@@ -796,6 +879,12 @@ static gboolean pull_sample(MppH264Source *src, gboolean require_idr, ...@@ -796,6 +879,12 @@ static gboolean pull_sample(MppH264Source *src, gboolean require_idr,
pts = GST_BUFFER_PTS(buffer); pts = GST_BUFFER_PTS(buffer);
idr = buffer_is_idr(src->frame_buf, src->frame_size); 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); gst_sample_unref(sample);
if (require_idr && !idr) { if (require_idr && !idr) {
...@@ -826,8 +915,11 @@ gboolean mpp_h264_source_pull(MppH264Source *src, gboolean force_idr, ...@@ -826,8 +915,11 @@ gboolean mpp_h264_source_pull(MppH264Source *src, gboolean force_idr,
return FALSE; return FALSE;
/* bus 报 ERROR 后 pipeline 卡死,这里及时恢复避免长期黑屏 */ /* 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 = TRUE;
src->pending_idr_event_sent = FALSE;
src->last_keyframe_event_us = 0;
}
if (timeout_ms < 1) if (timeout_ms < 1)
timeout_ms = (1000 / WEBRTCPUSH_H264_FPS) + 60; timeout_ms = (1000 / WEBRTCPUSH_H264_FPS) + 60;
...@@ -838,6 +930,10 @@ gboolean mpp_h264_source_pull(MppH264Source *src, gboolean force_idr, ...@@ -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)) if (pull_sample(src, TRUE, idr_timeout, data, size, is_idr, pts_ns))
return TRUE; return TRUE;
my_zlog_warn("mpp_h264_source: IDR wait timeout, drain latest frame"); 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)) if (pull_sample(src, FALSE, 500, data, size, is_idr, pts_ns))
return TRUE; return TRUE;
return FALSE; return FALSE;
...@@ -862,13 +958,21 @@ void mpp_h264_source_set_bitrate(MppH264Source *src, guint bitrate_bps) ...@@ -862,13 +958,21 @@ void mpp_h264_source_set_bitrate(MppH264Source *src, guint bitrate_bps)
f = gst_element_get_factory(src->enc); f = gst_element_get_factory(src->enc);
name = f ? gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(f)) : NULL; name = f ? gst_plugin_feature_get_name(GST_PLUGIN_FEATURE(f)) : NULL;
if (name && g_str_has_prefix(name, "mpph264enc")) { if (name && g_str_has_prefix(name, "mpph264enc")) {
guint bmin = (guint)(bitrate_bps * 0.80); guint bmin = (guint)(((guint64)bitrate_bps *
guint bmax = (guint)(bitrate_bps * 1.08); WEBRTCPUSH_MPP_BPS_MIN_PERCENT) / 100U);
if (bmin < MPP_VIDEO_MIN_BPS) guint bmax = (guint)(((guint64)bitrate_bps *
bmin = MPP_VIDEO_MIN_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) if (bmax > MPP_VIDEO_MAX_BPS)
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")) { } else if (name && g_str_has_prefix(name, "x264enc")) {
g_object_set(src->enc, "bitrate", bitrate_bps / 1000, NULL); g_object_set(src->enc, "bitrate", bitrate_bps / 1000, NULL);
} }
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
#include "mpp_h264_source.h" #include "mpp_h264_source.h"
#include "audio_source.h" #include "audio_source.h"
#include "audio_sink.h"
#include "volume_control.h"
#include "webrtcpush_config.h" #include "webrtcpush_config.h"
#include "webrtcpush_log.h" #include "webrtcpush_log.h"
#include "ws_signaling.h" #include "ws_signaling.h"
...@@ -27,6 +29,7 @@ typedef struct { ...@@ -27,6 +29,7 @@ typedef struct {
gint payload_type; gint payload_type;
gchar *mid; gchar *mid;
gboolean found; gboolean found;
gint direction; /* 0=unknown 1=sendonly 2=recvonly 3=sendrecv 4=inactive */
} OfferAudio; } OfferAudio;
typedef struct { typedef struct {
...@@ -58,6 +61,7 @@ typedef struct { ...@@ -58,6 +61,7 @@ typedef struct {
gboolean track_open; gboolean track_open;
gboolean need_idr; gboolean need_idr;
gboolean first_frame_sent; /* 首帧 IDR 不受 pacing flush 影响 */ gboolean first_frame_sent; /* 首帧 IDR 不受 pacing flush 影响 */
gint64 last_idr_sent_us;
gboolean answer_sent; gboolean answer_sent;
int pc; int pc;
int track; int track;
...@@ -80,8 +84,12 @@ typedef struct { ...@@ -80,8 +84,12 @@ typedef struct {
/* 音频 Opus track */ /* 音频 Opus track */
AudioSource *audio_source; AudioSource *audio_source;
AudioSink *audio_sink;
VolumeControl *volume_ctrl;
int audio_track; int audio_track;
gboolean audio_track_open; gboolean audio_track_open;
gboolean audio_send_enabled;
gboolean audio_recv_enabled;
GThread *audio_send_thread; GThread *audio_send_thread;
guint32 audio_rtp_timestamp; guint32 audio_rtp_timestamp;
guint64 audio_pts_base; guint64 audio_pts_base;
...@@ -155,15 +163,23 @@ static void frame_queue_stop(RtcFrameQueue *q) ...@@ -155,15 +163,23 @@ static void frame_queue_stop(RtcFrameQueue *q)
g_mutex_unlock(&q->lock); g_mutex_unlock(&q->lock);
} }
static void frame_queue_clear(RtcClient *client) static guint frame_queue_clear(RtcClient *client)
{ {
RtcFrameQueue *q = &client->frame_queue; RtcFrameQueue *q = &client->frame_queue;
RtcH264Frame *frame; RtcH264Frame *frame;
guint dropped = 0;
if (!client)
return 0;
g_mutex_lock(&q->lock); 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); rtc_h264_frame_free(frame);
}
g_mutex_unlock(&q->lock); g_mutex_unlock(&q->lock);
return dropped;
} }
static void frame_queue_push(RtcClient *client, RtcH264Frame *frame) static void frame_queue_push(RtcClient *client, RtcH264Frame *frame)
...@@ -172,6 +188,7 @@ static void frame_queue_push(RtcClient *client, RtcH264Frame *frame) ...@@ -172,6 +188,7 @@ static void frame_queue_push(RtcClient *client, RtcH264Frame *frame)
RtcH264Frame *old; RtcH264Frame *old;
gboolean overflowed = FALSE; gboolean overflowed = FALSE;
gboolean request_resync = FALSE; gboolean request_resync = FALSE;
guint dropped = 0;
g_mutex_lock(&q->lock); g_mutex_lock(&q->lock);
if (q->stopping) { if (q->stopping) {
...@@ -182,11 +199,13 @@ static void frame_queue_push(RtcClient *client, RtcH264Frame *frame) ...@@ -182,11 +199,13 @@ static void frame_queue_push(RtcClient *client, RtcH264Frame *frame)
if (g_queue_get_length(&q->queue) >= q->max_depth) { if (g_queue_get_length(&q->queue) >= q->max_depth) {
overflowed = TRUE; overflowed = TRUE;
while ((old = g_queue_pop_head(&q->queue)) != NULL) { while ((old = g_queue_pop_head(&q->queue)) != NULL) {
dropped++;
q->dropped_total++; q->dropped_total++;
rtc_h264_frame_free(old); rtc_h264_frame_free(old);
} }
} }
if (overflowed && !frame->is_idr) { if (overflowed && !frame->is_idr) {
dropped++;
q->dropped_total++; q->dropped_total++;
rtc_h264_frame_free(frame); rtc_h264_frame_free(frame);
request_resync = TRUE; request_resync = TRUE;
...@@ -203,7 +222,8 @@ static void frame_queue_push(RtcClient *client, RtcH264Frame *frame) ...@@ -203,7 +222,8 @@ static void frame_queue_push(RtcClient *client, RtcH264Frame *frame)
client->source_pts_valid = FALSE; client->source_pts_valid = FALSE;
client->last_idr_request_us = g_get_monotonic_time(); client->last_idr_request_us = g_get_monotonic_time();
g_mutex_unlock(&client->lock); 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, ...@@ -437,6 +457,7 @@ static void RTC_API on_local_description(int pc, const char *sdp,
g_free(patched); g_free(patched);
patched = with_msid; patched = with_msid;
} }
my_zlog_info("libdatachannel: SDP answer:\n%s", patched ? patched : sdp);
json = build_description_json(client->app, "answer", patched ? patched : sdp); json = build_description_json(client->app, "answer", patched ? patched : sdp);
g_free(patched); g_free(patched);
if (!json) { if (!json) {
...@@ -462,6 +483,7 @@ static void RTC_API on_local_candidate(int pc, const char *candidate, ...@@ -462,6 +483,7 @@ static void RTC_API on_local_candidate(int pc, const char *candidate,
if (!client || !candidate || !candidate[0]) if (!client || !candidate || !candidate[0])
return; return;
my_zlog_info("libdatachannel: local candidate mid=%s: %s", mid ? mid : "?", candidate);
json = build_candidate_json(client, candidate, mid); json = build_candidate_json(client, candidate, mid);
if (!json) if (!json)
return; return;
...@@ -560,6 +582,31 @@ static void RTC_API on_state_change(int pc, rtcState state, void *ptr) ...@@ -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) static void RTC_API on_track_open(int track, void *ptr)
{ {
RtcClient *client = ptr; RtcClient *client = ptr;
...@@ -573,6 +620,7 @@ static void RTC_API on_track_open(int track, void *ptr) ...@@ -573,6 +620,7 @@ static void RTC_API on_track_open(int track, void *ptr)
client->track_open = TRUE; client->track_open = TRUE;
client->source_pts_valid = FALSE; client->source_pts_valid = FALSE;
client->first_frame_sent = FALSE; client->first_frame_sent = FALSE;
client->last_idr_sent_us = 0;
video_open = TRUE; video_open = TRUE;
} }
if (client->audio_track == track) { if (client->audio_track == track) {
...@@ -604,6 +652,7 @@ static void RTC_API on_track_closed(int track, void *ptr) ...@@ -604,6 +652,7 @@ static void RTC_API on_track_closed(int track, void *ptr)
if (client->track == track) { if (client->track == track) {
client->track_open = FALSE; client->track_open = FALSE;
client->first_frame_sent = FALSE; client->first_frame_sent = FALSE;
client->last_idr_sent_us = 0;
video_closed = TRUE; video_closed = TRUE;
} }
if (client->audio_track == track) { if (client->audio_track == track) {
...@@ -630,6 +679,7 @@ static void RTC_API on_pli(int track, void *ptr) ...@@ -630,6 +679,7 @@ static void RTC_API on_pli(int track, void *ptr)
RtcClient *client = ptr; RtcClient *client = ptr;
gboolean requested; gboolean requested;
int dropped = 0; int dropped = 0;
guint dropped_frames = 0;
if (!client || client->track != track) if (!client || client->track != track)
return; return;
requested = request_idr(client, FALSE); requested = request_idr(client, FALSE);
...@@ -643,9 +693,9 @@ static void RTC_API on_pli(int track, void *ptr) ...@@ -643,9 +693,9 @@ static void RTC_API on_pli(int track, void *ptr)
g_mutex_lock(&client->send_lock); g_mutex_lock(&client->send_lock);
dropped = rtcClearPacingQueue(track); dropped = rtcClearPacingQueue(track);
g_mutex_unlock(&client->send_lock); g_mutex_unlock(&client->send_lock);
frame_queue_clear(client); dropped_frames = frame_queue_clear(client);
my_zlog_info("libdatachannel: PLI received, dropped %d queued RTP packet(s), next is IDR", my_zlog_info("libdatachannel: RTCP PLI -> MPP IDR; dropped %d RTP packet(s), %u encoded frame(s)",
dropped > 0 ? dropped : 0); dropped > 0 ? dropped : 0, dropped_frames);
} }
static guint clamp_bitrate(guint bitrate) static guint clamp_bitrate(guint bitrate)
...@@ -702,11 +752,12 @@ static gboolean pacing_queue_snapshot(RtcClient *client, int track, ...@@ -702,11 +752,12 @@ static gboolean pacing_queue_snapshot(RtcClient *client, int track,
return TRUE; 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; int dropped = 0;
guint dropped_frames;
request_idr(client, TRUE);
g_mutex_lock(&client->lock); g_mutex_lock(&client->lock);
client->source_pts_valid = FALSE; client->source_pts_valid = FALSE;
g_mutex_unlock(&client->lock); g_mutex_unlock(&client->lock);
...@@ -714,22 +765,29 @@ static int clear_stale_pacing(RtcClient *client, int track) ...@@ -714,22 +765,29 @@ static int clear_stale_pacing(RtcClient *client, int track)
if (client->track == track) if (client->track == track)
dropped = rtcClearPacingQueue(track); dropped = rtcClearPacingQueue(track);
g_mutex_unlock(&client->send_lock); 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; return dropped;
} }
static void apply_bitrate(RtcClient *client, int track, guint bitrate) static void apply_bitrate(RtcClient *client, int track, guint bitrate)
{ {
guint b; guint b;
guint old_b;
if (!client) if (!client)
return; return;
b = clamp_bitrate(bitrate); b = clamp_bitrate(bitrate);
g_mutex_lock(&client->lock); g_mutex_lock(&client->lock);
old_b = client->target_bitrate;
client->target_bitrate = b; client->target_bitrate = b;
g_mutex_unlock(&client->lock); g_mutex_unlock(&client->lock);
if (old_b == b)
return;
if (track >= 0) { if (track >= 0) {
g_mutex_lock(&client->send_lock); g_mutex_lock(&client->send_lock);
if (client->track == track) if (client->track == track)
...@@ -737,6 +795,9 @@ static void apply_bitrate(RtcClient *client, int track, guint bitrate) ...@@ -737,6 +795,9 @@ static void apply_bitrate(RtcClient *client, int track, guint bitrate)
g_mutex_unlock(&client->send_lock); g_mutex_unlock(&client->send_lock);
} }
mpp_h264_source_set_bitrate(client->h264_source, b); 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. */ /* 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) ...@@ -780,6 +841,25 @@ static void ramp_bitrate_toward_ceiling(RtcClient *client, int track)
cur / 1000, next / 1000, tgt / 1000); 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) static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
{ {
RtcClient *client = ptr; RtcClient *client = ptr;
...@@ -790,8 +870,8 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr) ...@@ -790,8 +870,8 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
guint next = 0; guint next = 0;
guint down_samples = 0; guint down_samples = 0;
gboolean severe_down = FALSE; gboolean severe_down = FALSE;
gboolean can_step_down = FALSE;
gint64 now_us; gint64 now_us;
int dropped = 0;
if (!client || bitrate == 0) if (!client || bitrate == 0)
return; return;
...@@ -803,6 +883,8 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr) ...@@ -803,6 +883,8 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
g_mutex_lock(&client->lock); g_mutex_lock(&client->lock);
current = client->target_bitrate; current = client->target_bitrate;
client->last_remb_bitrate = 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) { if (client->remb_filtered == 0) {
filtered = bitrate; filtered = bitrate;
...@@ -834,15 +916,17 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr) ...@@ -834,15 +916,17 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
down_samples = client->remb_down_samples; down_samples = client->remb_down_samples;
if (severe_down || if (severe_down ||
client->remb_down_samples >= WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS) { 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->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; client->last_bitrate_ramp_us = now_us;
next = ceiling; next = bitrate_step_down_toward(current, ceiling);
}
if (severe_down) {
client->need_idr = TRUE;
client->last_idr_request_us = now_us;
client->source_pts_valid = FALSE;
} }
} else if (ceiling >= current) { } else if (ceiling >= current) {
client->remb_down_samples = 0; client->remb_down_samples = 0;
...@@ -850,25 +934,15 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr) ...@@ -850,25 +934,15 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
g_mutex_unlock(&client->lock); g_mutex_unlock(&client->lock);
if (next) { if (next) {
g_mutex_lock(&client->send_lock); apply_bitrate(client, track, next);
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);
if (severe_down) { 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, bitrate / 1000, filtered / 1000, next / 1000,
pacing_bitrate_for_encoder(next) / 1000, ceiling / 1000);
dropped > 0 ? dropped : 0);
} else { } 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, bitrate / 1000, filtered / 1000, next / 1000,
pacing_bitrate_for_encoder(next) / 1000, down_samples); ceiling / 1000, down_samples);
} }
} else if (ceiling > current) { } else if (ceiling > current) {
my_zlog_debug("libdatachannel: REMB raw=%u filtered=%u ceiling=%u kbps (slow ramp)", 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) ...@@ -916,13 +990,11 @@ static gpointer pull_thread_main(gpointer data)
RtcClient *client = data; RtcClient *client = data;
const gint pull_ms = (1000 / WEBRTCPUSH_H264_FPS) + 100; const gint pull_ms = (1000 / WEBRTCPUSH_H264_FPS) + 100;
guint pull_fail_streak = 0; guint pull_fail_streak = 0;
guint idr_discard_streak = 0;
gint64 last_pull_warn_us = 0; gint64 last_pull_warn_us = 0;
for (;;) { for (;;) {
gboolean stopping; gboolean stopping;
gboolean track_open; gboolean track_open;
gboolean force_idr;
gboolean need_idr_now; gboolean need_idr_now;
const guint8 *frame_data; const guint8 *frame_data;
size_t frame_size; size_t frame_size;
...@@ -933,17 +1005,22 @@ static gpointer pull_thread_main(gpointer data) ...@@ -933,17 +1005,22 @@ static gpointer pull_thread_main(gpointer data)
g_mutex_lock(&client->lock); g_mutex_lock(&client->lock);
stopping = client->stopping; stopping = client->stopping;
track_open = client->track_open && client->track >= 0; 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); g_mutex_unlock(&client->lock);
if (stopping) if (stopping)
break; break;
if (!track_open) {
g_usleep(20000);
continue;
}
{ if (need_idr_now)
gint pull_timeout = force_idr ? 2500 : pull_ms; 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, &frame_size, &is_idr, &source_pts,
pull_timeout)) { pull_ms)) {
gint64 now_us = g_get_monotonic_time(); gint64 now_us = g_get_monotonic_time();
pull_fail_streak++; pull_fail_streak++;
if (now_us - last_pull_warn_us >= 3000000 || if (now_us - last_pull_warn_us >= 3000000 ||
...@@ -967,22 +1044,14 @@ static gpointer pull_thread_main(gpointer data) ...@@ -967,22 +1044,14 @@ static gpointer pull_thread_main(gpointer data)
if (!track_open) if (!track_open)
continue; continue;
if (is_idr) {
g_mutex_lock(&client->lock); g_mutex_lock(&client->lock);
need_idr_now = client->need_idr; if (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);
client->need_idr = FALSE; client->need_idr = FALSE;
g_mutex_unlock(&client->lock); client->last_idr_sent_us = g_get_monotonic_time();
idr_discard_streak = 0;
my_zlog_warn("libdatachannel: IDR wait >2s, resume with latest frames");
} }
continue; g_mutex_unlock(&client->lock);
} }
idr_discard_streak = 0;
queued = rtc_h264_frame_new(frame_data, frame_size, is_idr, source_pts); queued = rtc_h264_frame_new(frame_data, frame_size, is_idr, source_pts);
frame_queue_push(client, queued); frame_queue_push(client, queued);
...@@ -1047,11 +1116,33 @@ static gpointer send_thread_main(gpointer data) ...@@ -1047,11 +1116,33 @@ static gpointer send_thread_main(gpointer data)
client->first_frame_sent && client->first_frame_sent &&
now - last_pacing_flush_us >= now - last_pacing_flush_us >=
(gint64)WEBRTCPUSH_PACING_GUARD_COOLDOWN_MS * 1000) { (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; 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, 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) ...@@ -1145,8 +1236,10 @@ static gpointer send_thread_main(gpointer data)
g_mutex_lock(&client->lock); g_mutex_lock(&client->lock);
client->rtp_timestamp = timestamp + timestamp_step; client->rtp_timestamp = timestamp + timestamp_step;
if (frame->is_idr) if (frame->is_idr) {
client->need_idr = FALSE; client->need_idr = FALSE;
client->last_idr_sent_us = g_get_monotonic_time();
}
g_mutex_unlock(&client->lock); g_mutex_unlock(&client->lock);
rtc_h264_frame_free(frame); rtc_h264_frame_free(frame);
...@@ -1164,6 +1257,7 @@ static gpointer audio_send_thread_main(gpointer data) ...@@ -1164,6 +1257,7 @@ static gpointer audio_send_thread_main(gpointer data)
for (;;) { for (;;) {
gboolean stopping; gboolean stopping;
gboolean audio_send_enabled;
int track; int track;
const uint8_t *frame_data; const uint8_t *frame_data;
size_t frame_size; size_t frame_size;
...@@ -1177,14 +1271,17 @@ static gpointer audio_send_thread_main(gpointer data) ...@@ -1177,14 +1271,17 @@ static gpointer audio_send_thread_main(gpointer data)
break; break;
g_mutex_lock(&client->lock); 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); g_cond_wait(&client->track_cond, &client->lock);
stopping = client->stopping; stopping = client->stopping;
track = client->audio_track; track = client->audio_track;
audio_send_enabled = client->audio_send_enabled;
g_mutex_unlock(&client->lock); g_mutex_unlock(&client->lock);
if (stopping) if (stopping)
break; break;
if (track < 0) if (track < 0 || !audio_send_enabled)
continue; continue;
if (!audio_source_pull(client->audio_source, &frame_data, &frame_size, if (!audio_source_pull(client->audio_source, &frame_data, &frame_size,
...@@ -1239,6 +1336,7 @@ static void offer_video_clear(OfferVideo *video) ...@@ -1239,6 +1336,7 @@ static void offer_video_clear(OfferVideo *video)
static void offer_audio_clear(OfferAudio *audio) static void offer_audio_clear(OfferAudio *audio)
{ {
audio->direction = 0;
g_free(audio->mid); g_free(audio->mid);
memset(audio, 0, sizeof(*audio)); memset(audio, 0, sizeof(*audio));
audio->payload_type = -1; audio->payload_type = -1;
...@@ -1290,6 +1388,14 @@ static gboolean parse_offer(const char *sdp, OfferVideo *video, OfferAudio *audi ...@@ -1290,6 +1388,14 @@ static gboolean parse_offer(const char *sdp, OfferVideo *video, OfferAudio *audi
if (g_str_has_prefix(line, "a=mid:")) { if (g_str_has_prefix(line, "a=mid:")) {
g_free(audio->mid); g_free(audio->mid);
audio->mid = g_strdup(line + strlen("a=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:")) { } else if (g_str_has_prefix(line, "a=rtpmap:")) {
gint pt = -1; gint pt = -1;
gchar codec[32] = {0}; gchar codec[32] = {0};
...@@ -1328,8 +1434,11 @@ static void destroy_peer(RtcClient *client) ...@@ -1328,8 +1434,11 @@ static void destroy_peer(RtcClient *client)
client->track = -1; client->track = -1;
client->track_open = FALSE; client->track_open = FALSE;
client->first_frame_sent = FALSE; client->first_frame_sent = FALSE;
client->last_idr_sent_us = 0;
client->audio_track = -1; client->audio_track = -1;
client->audio_track_open = FALSE; client->audio_track_open = FALSE;
client->audio_send_enabled = FALSE;
client->audio_recv_enabled = FALSE;
client->answer_sent = FALSE; client->answer_sent = FALSE;
client->need_idr = TRUE; client->need_idr = TRUE;
client->source_pts_valid = FALSE; client->source_pts_valid = FALSE;
...@@ -1426,12 +1535,34 @@ static gboolean setup_audio_track(RtcClient *client, int pc, ...@@ -1426,12 +1535,34 @@ static gboolean setup_audio_track(RtcClient *client, int pc,
rtcTrackInit track_init; rtcTrackInit track_init;
rtcPacketizerInit packetizer; rtcPacketizerInit packetizer;
guint32 ssrc = g_random_int(); guint32 ssrc = g_random_int();
gboolean offer_send;
gboolean offer_recv;
gboolean send_audio;
gboolean recv_audio;
int track; int track;
if (ssrc == 0) if (ssrc == 0)
ssrc = 1; ssrc = 1;
memset(&track_init, 0, sizeof(track_init)); 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; 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.codec = RTC_CODEC_OPUS;
track_init.payloadType = audio->payload_type; track_init.payloadType = audio->payload_type;
track_init.ssrc = ssrc; track_init.ssrc = ssrc;
...@@ -1447,6 +1578,8 @@ static gboolean setup_audio_track(RtcClient *client, int pc, ...@@ -1447,6 +1578,8 @@ static gboolean setup_audio_track(RtcClient *client, int pc,
rtcSetOpenCallback(track, on_track_open); rtcSetOpenCallback(track, on_track_open);
rtcSetClosedCallback(track, on_track_closed); rtcSetClosedCallback(track, on_track_closed);
rtcSetErrorCallback(track, on_track_error); rtcSetErrorCallback(track, on_track_error);
if (recv_audio)
rtcSetMessageCallback(track, on_audio_message);
memset(&packetizer, 0, sizeof(packetizer)); memset(&packetizer, 0, sizeof(packetizer));
packetizer.ssrc = ssrc; packetizer.ssrc = ssrc;
...@@ -1456,16 +1589,21 @@ static gboolean setup_audio_track(RtcClient *client, int pc, ...@@ -1456,16 +1589,21 @@ static gboolean setup_audio_track(RtcClient *client, int pc,
packetizer.sequenceNumber = (guint16)g_random_int(); packetizer.sequenceNumber = (guint16)g_random_int();
packetizer.timestamp = g_random_int(); packetizer.timestamp = g_random_int();
if (send_audio) {
if (rtcSetOpusPacketizer(track, &packetizer) < 0 || if (rtcSetOpusPacketizer(track, &packetizer) < 0 ||
rtcChainRtcpSrReporter(track) < 0 || rtcChainRtcpSrReporter(track) < 0 ||
rtcChainRtcpNackResponder(track, WEBRTCPUSH_NACK_PACKETS) < 0) { rtcChainRtcpNackResponder(track, WEBRTCPUSH_NACK_PACKETS) < 0) {
rtcDeleteTrack(track); rtcDeleteTrack(track);
return FALSE; return FALSE;
} }
}
/* recv_audio 的 RTP payload 由 on_audio_message 送入 audio_sink。 */
g_mutex_lock(&client->lock); g_mutex_lock(&client->lock);
client->audio_rtp_timestamp = packetizer.timestamp; client->audio_rtp_timestamp = packetizer.timestamp;
client->audio_pts_valid = FALSE; client->audio_pts_valid = FALSE;
client->audio_send_enabled = send_audio;
client->audio_recv_enabled = recv_audio;
g_mutex_unlock(&client->lock); g_mutex_unlock(&client->lock);
*track_out = track; *track_out = track;
return TRUE; return TRUE;
...@@ -1547,6 +1685,22 @@ int rtc_client_start(AppState *app) ...@@ -1547,6 +1685,22 @@ int rtc_client_start(AppState *app)
g_free(audio_err); 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", my_zlog_info("libdatachannel: MPP live source started device=%s mpp=%d fps=%d threads=pull+send queue=%u",
video_device, video_device,
mpp_h264_source_is_mpp_encoder(client->h264_source) ? 1 : 0, mpp_h264_source_is_mpp_encoder(client->h264_source) ? 1 : 0,
...@@ -1580,6 +1734,10 @@ void rtc_client_stop(AppState *app) ...@@ -1580,6 +1734,10 @@ void rtc_client_stop(AppState *app)
g_thread_join(client->send_thread); g_thread_join(client->send_thread);
if (client->audio_send_thread) if (client->audio_send_thread)
g_thread_join(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) if (client->audio_source)
audio_source_stop(client->audio_source); audio_source_stop(client->audio_source);
...@@ -1653,16 +1811,18 @@ int rtc_client_handle_offer(AppState *app, const char *sdp) ...@@ -1653,16 +1811,18 @@ int rtc_client_handle_offer(AppState *app, const char *sdp)
return -1; return -1;
} }
/* 若手机 offer 包含 audio m-line 且设备端音频源已启动,建 Opus track */ /* 若手机 offer 包含 audio m-line,按本机麦克风/喇叭能力协商方向。 */
if (audio.found && audio.payload_type >= 0 && client->audio_source) { if (audio.found && audio.payload_type >= 0 &&
(client->audio_source || client->audio_sink)) {
if (setup_audio_track(client, pc, &audio, &audio_track)) { if (setup_audio_track(client, pc, &audio, &audio_track)) {
my_zlog_info("libdatachannel: Opus audio track created mid=%s pt=%d", my_zlog_info("libdatachannel: Opus audio track created mid=%s pt=%d dir=%d",
audio.mid, audio.payload_type); audio.mid, audio.payload_type, audio.direction);
} else { } else {
my_zlog_warn("libdatachannel: failed to create Opus audio track"); my_zlog_warn("libdatachannel: failed to create Opus audio track");
} }
} }
g_mutex_lock(&client->lock); g_mutex_lock(&client->lock);
client->pc = pc; client->pc = pc;
client->track = track; 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 @@ ...@@ -8,35 +8,46 @@
* *
* 原生分支:v4l2 采集 → mpph264enc(或 x264enc 回退)→ libdatachannel 发送。 * 原生分支:v4l2 采集 → mpph264enc(或 x264enc 回退)→ libdatachannel 发送。
*/ */
#define WEBRTCPUSH_USE_MPP 1 #define WEBRTCPUSH_USE_MPP 0
/* 正常固定 24fps;弱网以降码率为主,后续若启用动态帧率也不得低于 22fps。 */ /* 正常固定 24fps;弱网以降码率为主,后续若启用动态帧率也不得低于 22fps。 */
#define WEBRTCPUSH_H264_FPS 24 #define WEBRTCPUSH_H264_FPS 24
#define WEBRTCPUSH_H264_MIN_FPS 22 #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_MIN_BITRATE 500000U
#define WEBRTCPUSH_MAX_BITRATE 3200000U #define WEBRTCPUSH_MAX_BITRATE 2800000U
#define WEBRTCPUSH_REMB_UTIL_PERCENT 80U #define WEBRTCPUSH_REMB_UTIL_PERCENT 80U
#define WEBRTCPUSH_REMB_DOWN_MIN_STEP 100000U #define WEBRTCPUSH_REMB_DOWN_MIN_STEP 100000U
#define WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS 3U #define WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS 4U
#define WEBRTCPUSH_REMB_SEVERE_PERCENT 65U #define WEBRTCPUSH_REMB_SEVERE_PERCENT 65U
#define WEBRTCPUSH_BITRATE_RAMP_UP_MS 2500U #define WEBRTCPUSH_BITRATE_RAMP_UP_MS 2500U
#define WEBRTCPUSH_BITRATE_RAMP_DOWN_MS 500U #define WEBRTCPUSH_BITRATE_RAMP_DOWN_MS 1500U
#define WEBRTCPUSH_PACING_HEADROOM_PERCENT 125U #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_INTERVAL_MS 5U
#define WEBRTCPUSH_PACING_MAX_BITRATE 4000000U #define WEBRTCPUSH_PACING_MAX_BITRATE 4000000U
#define WEBRTCPUSH_PACING_MAX_QUEUE_MS 300U #define WEBRTCPUSH_PACING_MAX_QUEUE_MS 250U
#define WEBRTCPUSH_PACING_GUARD_COOLDOWN_MS 1000U #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 余量) */ /* RTP 分片与 NACK(MTU=1200,留 SRTP/DTLS/FU 余量) */
#define WEBRTCPUSH_RTP_MAX_FRAGMENT 1050U #define WEBRTCPUSH_RTP_MAX_FRAGMENT 1050U
#define WEBRTCPUSH_NACK_PACKETS 512U #define WEBRTCPUSH_NACK_PACKETS 512U
/* PLI/IDR 节流:避免频繁 force-key-unit 拉高瞬时码率 */ /* PLI/IDR 节流:避免频繁 force-key-unit 拉高瞬时码率 */
#define WEBRTCPUSH_PLI_IDR_THROTTLE_MS 200U #define WEBRTCPUSH_PLI_IDR_THROTTLE_MS 500U
/* 周期性推流指标日志间隔 */ /* 周期性推流指标日志间隔 */
#define WEBRTCPUSH_STATS_LOG_INTERVAL_MS 8000U #define WEBRTCPUSH_STATS_LOG_INTERVAL_MS 8000U
...@@ -48,13 +59,26 @@ ...@@ -48,13 +59,26 @@
#define WEBRTCPUSH_H264_PROFILE_LEVEL_ID "42e01f" #define WEBRTCPUSH_H264_PROFILE_LEVEL_ID "42e01f"
/* 拉帧线程与发送线程之间的有界队列(leaky,满则丢最旧帧) */ /* 拉帧线程与发送线程之间的有界队列(leaky,满则丢最旧帧) */
#define WEBRTCPUSH_SEND_QUEUE_DEPTH 2U #define WEBRTCPUSH_SEND_QUEUE_DEPTH 1U
/* 管道缓冲:运动场景需适度缓存,过低易 pull timeout / 糊屏 */ /* 管道缓冲:运动场景需适度缓存,过低易 pull timeout / 糊屏 */
#define WEBRTCPUSH_MPP_PRE_ENC_BUFFERS 1 /* 编码前 leaky,低延迟 */ #define WEBRTCPUSH_MPP_PRE_ENC_BUFFERS 1 /* 编码前 leaky,低延迟 */
#define WEBRTCPUSH_MPP_POST_ENC_BUFFERS 1 /* 编码后保留 AU */ #define WEBRTCPUSH_MPP_POST_ENC_BUFFERS 1 /* 编码后保留 AU */
#define WEBRTCPUSH_APPSINK_MAX_BUFFERS 1 #define WEBRTCPUSH_APPSINK_MAX_BUFFERS 1
/* MJPEG 解压:1=GStreamer CPU jpegdec 优先;0=mppjpegdec 优先 */
#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 测试图案) */ /* 视频采集:auto | /dev/videoN | test(videotestsrc 测试图案) */
#define WEBRTCPUSH_VIDEO_DEVICE "auto" #define WEBRTCPUSH_VIDEO_DEVICE "auto"
...@@ -70,6 +94,15 @@ ...@@ -70,6 +94,15 @@
#define WEBRTCPUSH_OPUS_CHANNELS 1 #define WEBRTCPUSH_OPUS_CHANNELS 1
#define WEBRTCPUSH_AUDIO_QUEUE_DEPTH 8U #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, * 设备侧 DataChannel(myDataChannel)与手机 createDataChannel('init') 争用 SCTP,
* 易触发 sctpenc association error 并导致管道闪断/进程崩溃。仅推流可不建。 * 易触发 sctpenc association error 并导致管道闪断/进程崩溃。仅推流可不建。
......
...@@ -445,23 +445,38 @@ static void handle_wifi_switch(int sock, const struct sockaddr_in *addr, cJSON * ...@@ -445,23 +445,38 @@ static void handle_wifi_switch(int sock, const struct sockaddr_in *addr, cJSON *
ssid = ssid_item->valuestring; ssid = ssid_item->valuestring;
my_zlog_info("WiFi配置助手: 收到 wifi_switch SSID=%s", ssid); my_zlog_info("WiFi配置助手: 收到 wifi_switch SSID=%s", ssid);
rc = app_wifi_switch(ssid); /* 先校验 SSID 是否已保存(不连接),据此先回复 app,
success = (rc == 0); * 再实际发起切换。避免切到新 WiFi 后 app 收不到响应。 */
if (rc == 0) rc = app_wifi_check_saved(ssid);
snprintf(message, sizeof(message), "WiFi 切换成功,正在连接 %s", ssid); if (rc != 0) {
else if (rc == -1) /* 未保存:直接回失败 */
resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "cmd", "wifi_switch_result");
cJSON_AddBoolToObject(resp, "success", 0);
snprintf(message, sizeof(message), "未找到已保存的 WiFi:%s", ssid); snprintf(message, sizeof(message), "未找到已保存的 WiFi:%s", ssid);
else /* rc == -2:已保存但连接失败 */ cJSON_AddStringToObject(resp, "message", message);
snprintf(message, sizeof(message), "WiFi 已保存但连接失败:%s(信号弱或不可达)", ssid); payload = cJSON_PrintUnformatted(resp);
if (payload) { send_json(sock, addr, payload); free(payload); }
cJSON_Delete(resp);
return;
}
/* 已保存:立即回成功,告知 app 已发起切换 */
resp = cJSON_CreateObject(); resp = cJSON_CreateObject();
cJSON_AddStringToObject(resp, "cmd", "wifi_switch_result"); 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); cJSON_AddStringToObject(resp, "message", message);
payload = cJSON_PrintUnformatted(resp); payload = cJSON_PrintUnformatted(resp);
if (payload) { send_json(sock, addr, payload); free(payload); } if (payload) { send_json(sock, addr, payload); free(payload); }
cJSON_Delete(resp); 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) static void handle_wifi_delete(int sock, const struct sockaddr_in *addr, cJSON *root)
......
...@@ -9,4 +9,4 @@ file perms = 600 ...@@ -9,4 +9,4 @@ file perms = 600
millisecond = "%d(%Y-%m-%d %H:%M:%S).%ms [%V] %m%n" millisecond = "%d(%Y-%m-%d %H:%M:%S).%ms [%V] %m%n"
[rules] [rules]
my_log.* "/home/orangepi/car/master/log/log_2026-07-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