Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
C
car-controlserver
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
wenzhongjian
car-controlserver
Commits
4687e6c8
Commit
4687e6c8
authored
Jul 08, 2026
by
957dd
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
加入了手机去配网
parent
d92f8afb
Expand all
Show whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
796 additions
and
87 deletions
+796
-87
device_wifi_manager.c
app/device_change/device_wifi_manager.c
+24
-0
device_wifi_manager.h
app/device_change/device_wifi_manager.h
+4
-0
main
build/main
+0
-0
device_diag.c
modules/device_debug/device_diag.c
+79
-63
runtime_deps.c
modules/runtime_deps/runtime_deps.c
+40
-1
audio_sink.c
modules/webrtcpush/audio_sink.c
+277
-0
audio_sink.h
modules/webrtcpush/audio_sink.h
+20
-0
audio_source.c
modules/webrtcpush/audio_source.c
+6
-1
mpp_h264_source.c
modules/webrtcpush/mpp_h264_source.c
+0
-0
rtc_client.c
modules/webrtcpush/rtc_client.c
+0
-0
volume_control.c
modules/webrtcpush/volume_control.c
+264
-0
volume_control.h
modules/webrtcpush/volume_control.h
+12
-0
webrtcpush_config.h
modules/webrtcpush/webrtcpush_config.h
+45
-12
wifi_config_agent.c
modules/wifi_config/wifi_config_agent.c
+24
-9
zlog.conf
zlog.conf
+1
-1
No files found.
app/device_change/device_wifi_manager.c
View file @
4687e6c8
...
@@ -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
;
...
...
app/device_change/device_wifi_manager.h
View file @
4687e6c8
...
@@ -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 不可删)。
...
...
build/main
View file @
4687e6c8
No preview for this file type
modules/device_debug/device_diag.c
View file @
4687e6c8
...
@@ -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
);
}
}
...
...
modules/runtime_deps/runtime_deps.c
View file @
4687e6c8
...
@@ -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
;
...
...
modules/webrtcpush/audio_sink.c
0 → 100644
View file @
4687e6c8
#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
);
}
modules/webrtcpush/audio_sink.h
0 → 100644
View file @
4687e6c8
#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
modules/webrtcpush/audio_source.c
View file @
4687e6c8
...
@@ -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
,
...
...
modules/webrtcpush/mpp_h264_source.c
View file @
4687e6c8
This diff is collapsed.
Click to expand it.
modules/webrtcpush/rtc_client.c
View file @
4687e6c8
This diff is collapsed.
Click to expand it.
modules/webrtcpush/volume_control.c
0 → 100644
View file @
4687e6c8
#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
);
}
modules/webrtcpush/volume_control.h
0 → 100644
View file @
4687e6c8
#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
modules/webrtcpush/webrtcpush_config.h
View file @
4687e6c8
...
@@ -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
32
00000U
#define WEBRTCPUSH_MAX_BITRATE
28
00000U
#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
3
U
#define WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS
4
U
#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
2
00U
#define WEBRTCPUSH_PLI_IDR_THROTTLE_MS
5
00U
/* 周期性推流指标日志间隔 */
/* 周期性推流指标日志间隔 */
#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
2
U
#define WEBRTCPUSH_SEND_QUEUE_DEPTH
1
U
/* 管道缓冲:运动场景需适度缓存,过低易 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 并导致管道闪断/进程崩溃。仅推流可不建。
...
...
modules/wifi_config/wifi_config_agent.c
View file @
4687e6c8
...
@@ -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
)
...
...
zlog.conf
View file @
4687e6c8
...
@@ -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-0
6
.log"
;
millisecond
my_log
.*
"/home/orangepi/car/master/log/log_2026-07-0
8
.log"
;
millisecond
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment