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
7200adca
Commit
7200adca
authored
Jul 17, 2026
by
957dd
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'feature/add_aboard_tts' into 'master'
Feature/add aboard tts See merge request
!129
parents
7a0b1304
1ce13541
Show whitespace changes
Inline
Side-by-side
Showing
22 changed files
with
3001 additions
and
548 deletions
+3001
-548
main
build/main
+0
-0
car0107_control.c
drivers/devicecontrol/car/car0107_control.c
+97
-0
car0107_control.h
drivers/devicecontrol/car/car0107_control.h
+3
-0
car0107_idle.c
drivers/devicecontrol/car/car0107_idle.c
+5
-26
device_init.c
drivers/gpio/device_init.c
+5
-5
audioplay.c
drivers/sensors/audioplay.c
+168
-17
audioplay.c.bak-audio-preempt-20260717
drivers/sensors/audioplay.c.bak-audio-preempt-20260717
+551
-0
audioplay.h
drivers/sensors/audioplay.h
+7
-0
audioplay.h.bak-audio-preempt-20260717
drivers/sensors/audioplay.h.bak-audio-preempt-20260717
+16
-0
audiotts_play.c
drivers/sensors/audiotts_play.c
+6
-37
audiotts_play.c.bak-audio-preempt-20260717
drivers/sensors/audiotts_play.c.bak-audio-preempt-20260717
+536
-0
CMakeLists.txt
modules/CMakeLists.txt
+4
-0
http_config_mqtt.h
modules/http/http_config_mqtt.h
+1
-1
mqtt_infor_handle.c
modules/mqtt/mqtt_infor_handle.c
+5
-5
mqtt_verify.c
modules/mqtt/mqtt_verify.c
+11
-11
audio_sink.c
modules/webrtcpush/audio_sink.c
+23
-243
audio_sink.h
modules/webrtcpush/audio_sink.h
+8
-0
mpp_h264_source.c
modules/webrtcpush/mpp_h264_source.c
+889
-84
rtc_client.c
modules/webrtcpush/rtc_client.c
+565
-94
webrtcpush_config.h
modules/webrtcpush/webrtcpush_config.h
+52
-22
ws_signaling.c
modules/webrtcpush/ws_signaling.c
+48
-2
zlog.conf
zlog.conf
+1
-1
No files found.
build/main
View file @
7200adca
No preview for this file type
drivers/devicecontrol/car/car0107_control.c
View file @
7200adca
...
...
@@ -28,6 +28,14 @@
#define CAR0107_BACK_US_RUN_MIN 1440
/* val=53 起步即动(须低于死区 1450) */
#define CAR0107_BACK_US_RUN_MAX 1300
/* 后退最大速度再降20%:val=200 -> 1300us */
/* idle 唤醒动作:5分钟无控制后 低速后退 -> 低速前进 -> 前进刹车停止 */
#define CAR0107_IDLE_REVERSE_US CAR0107_BACK_US_RUN_MIN
/* 1440us 刚过死区,最低速后退 */
#define CAR0107_IDLE_FORWARD_US CAR0107_FWD_US_RUN_MIN
/* 1560us 最低前进行走脉宽 */
#define CAR0107_IDLE_REVERSE_MS 600
/* 后退保持时长 */
#define CAR0107_IDLE_NEUTRAL_MS 150
/* 后退切前进前中位稳定时长 */
#define CAR0107_IDLE_FORWARD_MS 600
/* 前进保持时长 */
#define CAR0107_IDLE_BRAKE_WAIT_MS 250
/* 等待刹车保持+中位解锁收尾到 NEUTRAL */
typedef
enum
{
CAR0107_ESC_DIR_FORWARD
=
1
,
/* App mode1 前进:高脉宽(与 0101 相反) */
CAR0107_ESC_DIR_BACK
=
2
,
/* App mode2 后退:低脉宽(与 0101 相反) */
...
...
@@ -1392,3 +1400,92 @@ void car0107_esc_thread_close(void)
pthread_mutex_unlock
(
&
s_esc_start_mutex
);
my_zlog_info
(
"car0107 esc thread closed"
);
}
/*
* 0107 idle 唤醒动作:5分钟无控制命令时由 idle 线程调用。
* 顺序:低速后退一点 -> 中位 -> 低速前进一点 -> 前进刹车停止。
* 前进停止遵守电调刹车动作 BRAKE_THEN_NEUTRAL -> NEUTRAL_UNLOCK -> NEUTRAL,
* 由电调专用线程 tick 自动完成刹车保持与中位解锁收尾。
* 动作期间若有真实控制命令介入(control_source 被改为非 NONE)则立即让位。
*/
void
car0107_idle_wakeup_maneuver
(
void
)
{
/* 必须保证电调线程在运行,否则刹车序列无法由 tick 自动收尾到 NEUTRAL */
car0107_esc_thread_start
();
/* 方向盘回正,后退/前进均走直线 */
pthread_mutex_lock
(
&
s_car0107_hw_mutex
);
s_control_source
=
CTRL_NONE
;
/* 阻止 speed_smooth_process 串扰速度 */
s_target_angle
=
90
;
s_current_angle
=
90
;
car0107_calculate_L_R
(
90
);
/* 1) 低速后退:从中位直接跳到刚过死区的后退脉宽 */
car0107_esc_clear_pending_stop
();
s_esc
.
cmd_dir
=
CAR0107_ESC_DIR_BACK
;
s_esc
.
last_drive_dir
=
CAR0107_ESC_DIR_BACK
;
s_esc
.
last_nonzero_mode
=
CAR0107_ESC_DIR_BACK
;
s_esc
.
target_us
=
CAR0107_IDLE_REVERSE_US
;
s_esc
.
current_us
=
CAR0107_IDLE_REVERSE_US
;
s_esc
.
state
=
CAR0107_ESC_REVERSE
;
s_esc
.
trans_phase
=
CAR0107_TRANS_RAMP_RUN
;
car0107_esc_output_us
(
s_esc
.
current_us
,
0
);
pthread_mutex_unlock
(
&
s_car0107_hw_mutex
);
car0107_esc_thread_wake
();
my_zlog_info
(
"car0107 idle maneuver: reverse %dus for %dms"
,
CAR0107_IDLE_REVERSE_US
,
CAR0107_IDLE_REVERSE_MS
);
delay_ms
(
CAR0107_IDLE_REVERSE_MS
);
/* 2) 停止后退:后退松手不刹车,直接回中 */
pthread_mutex_lock
(
&
s_car0107_hw_mutex
);
if
(
s_control_source
!=
CTRL_NONE
)
{
pthread_mutex_unlock
(
&
s_car0107_hw_mutex
);
my_zlog_info
(
"car0107 idle maneuver: yield at neutral (control taken)"
);
return
;
}
car0107_esc_begin_neutral_stop
();
car0107_esc_output_us
(
CAR0107_ESC_US_NEUTRAL
,
0
);
pthread_mutex_unlock
(
&
s_car0107_hw_mutex
);
car0107_esc_thread_wake
();
delay_ms
(
CAR0107_IDLE_NEUTRAL_MS
);
/* 3) 低速前进:从中位直接跳到最低前进行走脉宽 */
pthread_mutex_lock
(
&
s_car0107_hw_mutex
);
if
(
s_control_source
!=
CTRL_NONE
)
{
pthread_mutex_unlock
(
&
s_car0107_hw_mutex
);
my_zlog_info
(
"car0107 idle maneuver: yield before forward (control taken)"
);
return
;
}
car0107_esc_clear_pending_stop
();
s_esc
.
cmd_dir
=
CAR0107_ESC_DIR_FORWARD
;
s_esc
.
last_drive_dir
=
CAR0107_ESC_DIR_FORWARD
;
s_esc
.
last_nonzero_mode
=
CAR0107_ESC_DIR_FORWARD
;
s_esc
.
target_us
=
CAR0107_IDLE_FORWARD_US
;
s_esc
.
current_us
=
CAR0107_IDLE_FORWARD_US
;
s_esc
.
state
=
CAR0107_ESC_FORWARD
;
s_esc
.
trans_phase
=
CAR0107_TRANS_RAMP_RUN
;
car0107_esc_output_us
(
s_esc
.
current_us
,
0
);
pthread_mutex_unlock
(
&
s_car0107_hw_mutex
);
car0107_esc_thread_wake
();
my_zlog_info
(
"car0107 idle maneuver: forward %dus for %dms"
,
CAR0107_IDLE_FORWARD_US
,
CAR0107_IDLE_FORWARD_MS
);
delay_ms
(
CAR0107_IDLE_FORWARD_MS
);
/* 4) 停止前进:遵守刹车动作,由电调线程 tick 自动收尾 */
pthread_mutex_lock
(
&
s_car0107_hw_mutex
);
if
(
s_control_source
!=
CTRL_NONE
)
{
pthread_mutex_unlock
(
&
s_car0107_hw_mutex
);
my_zlog_info
(
"car0107 idle maneuver: yield before brake (control taken)"
);
return
;
}
car0107_esc_begin_forward_brake
();
pthread_mutex_unlock
(
&
s_car0107_hw_mutex
);
car0107_esc_thread_wake
();
my_zlog_info
(
"car0107 idle maneuver: forward brake %dus for %dms"
,
car0107_forward_brake_us
(),
CAR0107_ESC_BRAKE_HOLD_MS
);
delay_ms
(
CAR0107_IDLE_BRAKE_WAIT_MS
);
}
drivers/devicecontrol/car/car0107_control.h
View file @
7200adca
...
...
@@ -14,4 +14,7 @@ void car0107_esc_thread_start(void);
void
car0107_esc_thread_wake
(
void
);
void
car0107_esc_thread_close
(
void
);
/* idle 唤醒动作:低速后退 -> 低速前进 -> 前进刹车停止 */
void
car0107_idle_wakeup_maneuver
(
void
);
#endif
drivers/devicecontrol/car/car0107_idle.c
View file @
7200adca
...
...
@@ -6,16 +6,12 @@
#define CAR0107_IDLE_CHECK_MS 10000
#define CAR0107_IDLE_TIMEOUT_MS (5 * 60 * 1000)
#define CAR0107_IDLE_STEER_HOLD_MS 450
#define CAR0107_IDLE_STEER_LEFT 115
#define CAR0107_IDLE_STEER_RIGHT 65
typedef
struct
{
pthread_mutex_t
mutex
;
ThreadPool_t
*
pool
;
bool
shutdown
;
bool
pool_ready
;
int
next_left
;
}
car0107_idle_ctx_t
;
static
car0107_idle_ctx_t
s_idle_ctx
=
{
...
...
@@ -23,7 +19,6 @@ static car0107_idle_ctx_t s_idle_ctx = {
.
pool
=
NULL
,
.
shutdown
=
false
,
.
pool_ready
=
false
,
.
next_left
=
1
,
};
static
void
car0107_idle_lock
(
void
)
...
...
@@ -67,16 +62,6 @@ static int car0107_idle_pool_init(void)
return
0
;
}
static
void
car0107_idle_nudge_steering
(
int
turn_left
)
{
int
angle
=
turn_left
?
CAR0107_IDLE_STEER_LEFT
:
CAR0107_IDLE_STEER_RIGHT
;
/* 速度 PWM 只由持有0107硬件锁的控制路径写,idle线程不直接碰速度。 */
car0107_steering_set_angle_sync
(
angle
);
delay_ms
(
CAR0107_IDLE_STEER_HOLD_MS
);
car0107_steering_set_angle_sync
(
90
);
}
static
void
car0107_idle_monitor_task
(
void
*
arg
)
{
(
void
)
arg
;
...
...
@@ -85,7 +70,6 @@ static void car0107_idle_monitor_task(void *arg)
while
(
!
car0107_idle_shutdown_get
())
{
long
long
now
;
long
long
last
;
int
turn_left
;
delay_ms
(
CAR0107_IDLE_CHECK_MS
);
if
(
car0107_idle_shutdown_get
()
||
g_device_type
!=
DEVICE_CAR0107
)
{
...
...
@@ -101,15 +85,10 @@ static void car0107_idle_monitor_task(void *arg)
continue
;
}
car0107_idle_lock
();
turn_left
=
s_idle_ctx
.
next_left
;
s_idle_ctx
.
next_left
=
!
s_idle_ctx
.
next_left
;
car0107_idle_unlock
();
my_zlog_info
(
"car0107 idle %dmin: nudge steer %s"
,
CAR0107_IDLE_TIMEOUT_MS
/
60000
,
turn_left
?
"left"
:
"right"
);
car0107_idle_nudge_steering
(
turn_left
);
my_zlog_info
(
"car0107 idle %dmin: wakeup maneuver (reverse->forward->brake)"
,
CAR0107_IDLE_TIMEOUT_MS
/
60000
);
/* 低速后退一点 -> 低速前进一点 -> 前进刹车停止(遵守刹车动作)*/
car0107_idle_wakeup_maneuver
();
car0107_notify_control_activity
();
}
...
...
@@ -133,7 +112,7 @@ void car0107_idle_startup(int device_id)
car0107_idle_unlock
();
car0107_notify_control_activity
();
my_zlog_info
(
"car0107 idle monitor enabled:
steer nudge
every %dmin when idle"
,
my_zlog_info
(
"car0107 idle monitor enabled:
wakeup maneuver
every %dmin when idle"
,
CAR0107_IDLE_TIMEOUT_MS
/
60000
);
}
...
...
drivers/gpio/device_init.c
View file @
7200adca
...
...
@@ -236,9 +236,9 @@ static const deviceconfig_t s_device_configs[] = {
bool
get_array_length
(
const
int
*
arr
)
{
if
(
arr
[
0
]
!=
-
1
)
{
// 遇到-1停止计数
return
TRUE
;
return
true
;
}
return
FALSE
;
return
false
;
}
void
device_init
(
int
device_id
)
{
...
...
@@ -258,9 +258,9 @@ void device_init(int device_id) {
}
// 执行初始化流程
if
(
get_array_length
(
config
->
gpio_pins
)
==
TRUE
)
init_gpiowpi
(
config
->
gpio_pins
);
// GPIO初始化
if
(
get_array_length
(
config
->
gpio_pwms
)
==
TRUE
)
init_gpiopwm
(
config
->
gpio_pwms
);
// GPIOsoft_pwm初始化
if
(
get_array_length
(
config
->
gpio_inputs
)
==
TRUE
)
init_gpio_input
(
config
->
gpio_inputs
);
if
(
get_array_length
(
config
->
gpio_pins
))
init_gpiowpi
(
config
->
gpio_pins
);
// GPIO初始化
if
(
get_array_length
(
config
->
gpio_pwms
))
init_gpiopwm
(
config
->
gpio_pwms
);
// GPIOsoft_pwm初始化
if
(
get_array_length
(
config
->
gpio_inputs
))
init_gpio_input
(
config
->
gpio_inputs
);
g_device_type
=
config
->
emergency_code
;
config
->
device_pwm_init
();
// PWM初始化
config
->
device_control_stop
();
// 速度控制初始化
...
...
drivers/sensors/audioplay.c
View file @
7200adca
#include "common.h"
#include "audioplay.h"
#include "audio_sink.h"
/* USB声卡排队锁 */
#include "device_identity.h"
#include "mqtt_init.h"
#include "http_config_mqtt.h"
#include "audiotts_play.h"
#include "wifi_autoconfig.h"
#include <stdio.h>
#include <pthread.h>
#include <strings.h>
#include <unistd.h>
#include <sys/wait.h>
#define AUDIO_USB_ALSA_DEVICE "hw:2,0"
#define AUDIO_LOCAL_ALSA_DEVICE "plughw:2,0"
#define AUDIO_ANNOUNCEMENT_TIMEOUT_SEC 20
static
int
s_audio_status
=
7
;
static
char
s_urlbuf
[
512
];
...
...
@@ -21,6 +25,7 @@ static double s_audio_volume=0.8;
static
int
s_local_play_pending
=
0
;
static
char
s_local_filepath
[
512
];
static
pthread_mutex_t
s_local_play_mutex
=
PTHREAD_MUTEX_INITIALIZER
;
static
int
local_is_cn_lang
(
const
char
*
lang
)
{
return
lang
!=
NULL
&&
(
strcmp
(
lang
,
AUDIO_LANG_ZH
)
==
0
||
strcmp
(
lang
,
"cn"
)
==
0
);
...
...
@@ -74,12 +79,16 @@ static int local_resolve_filepath(const char *filename, const char *language_ove
}
static
void
local_queue_play
(
const
char
*
filename
,
const
char
*
language
)
{
if
(
!
local_resolve_filepath
(
filename
,
language
,
s_local_filepath
,
sizeof
(
s_local_filepath
)))
{
char
resolved
[
512
];
if
(
!
local_resolve_filepath
(
filename
,
language
,
resolved
,
sizeof
(
resolved
)))
{
my_zlog_warn
(
"2017 本地音频不存在: %s"
,
filename
);
return
;
}
pthread_mutex_lock
(
&
s_local_play_mutex
);
snprintf
(
s_local_filepath
,
sizeof
(
s_local_filepath
),
"%s"
,
resolved
);
s_local_play_pending
=
1
;
my_zlog_info
(
"2017 已排队本地音频: %s"
,
s_local_filepath
);
pthread_mutex_unlock
(
&
s_local_play_mutex
);
my_zlog_info
(
"2017 已排队本地音频: %s"
,
resolved
);
}
void
audioplay_local_mqtt_receive
(
cJSON
*
body
)
{
...
...
@@ -143,6 +152,144 @@ static double audioplay_volume_clamp(double v) {
int
audio_wheat_init
();
static
int
audio_system_exit_code
(
int
status
)
{
if
(
status
==
-
1
)
{
return
-
1
;
}
if
(
WIFEXITED
(
status
))
{
return
WEXITSTATUS
(
status
);
}
if
(
WIFSIGNALED
(
status
))
{
return
128
+
WTERMSIG
(
status
);
}
return
-
1
;
}
static
void
shell_single_quote
(
char
*
out
,
size_t
size
,
const
char
*
in
)
{
size_t
pos
=
0
;
if
(
!
out
||
size
==
0
)
{
return
;
}
out
[
pos
++
]
=
'\''
;
if
(
in
)
{
for
(
const
char
*
p
=
in
;
*
p
&&
pos
+
5
<
size
;
p
++
)
{
if
(
*
p
==
'\''
)
{
const
char
*
esc
=
"'
\\
''"
;
for
(
const
char
*
e
=
esc
;
*
e
&&
pos
+
1
<
size
;
e
++
)
{
out
[
pos
++
]
=
*
e
;
}
}
else
{
out
[
pos
++
]
=
*
p
;
}
}
}
if
(
pos
+
1
<
size
)
{
out
[
pos
++
]
=
'\''
;
}
out
[
pos
]
=
'\0'
;
}
static
int
pulse_suspend_default_output
(
int
suspend
)
{
const
char
*
command
=
suspend
?
"pactl suspend-sink @DEFAULT_SINK@ 1 >/dev/null 2>&1"
:
"pactl suspend-sink @DEFAULT_SINK@ 0 >/dev/null 2>&1"
;
int
exit_code
=
audio_system_exit_code
(
system
(
command
));
if
(
exit_code
!=
0
)
{
my_zlog_warn
(
"%s浏览器音频输出失败 exit=%d"
,
suspend
?
"暂停"
:
"恢复"
,
exit_code
);
}
return
exit_code
;
}
int
audioplay_file_with_browser_preempt
(
const
char
*
filepath
,
double
volume
)
{
char
quoted_path
[
1024
];
char
command
[
2048
];
int
ret
;
int
exit_code
;
int
pulse_suspended
;
if
(
filepath
==
NULL
||
filepath
[
0
]
==
'\0'
)
{
return
-
1
;
}
shell_single_quote
(
quoted_path
,
sizeof
(
quoted_path
),
filepath
);
volume
=
audioplay_volume_clamp
(
volume
);
/* 先和项目内 DataChannel/本地音频排队,再让浏览器临时释放 USB 声卡。 */
audio_sink_lock_alsa
();
pulse_suspended
=
(
pulse_suspend_default_output
(
1
)
==
0
);
if
(
pulse_suspended
)
{
snprintf
(
command
,
sizeof
(
command
),
/* timeout 使用 KILL,保证卡死的播放进程到 20 秒时立即释放 ALSA。 */
"timeout -s KILL %ds gst-launch-1.0 -q filesrc location=%s ! mpegaudioparse ! mpg123audiodec ! audioconvert ! volume volume=%.3f ! audioresample ! audio/x-raw,channels=2 ! alsasink device=%s sync=true >/dev/null 2>&1"
,
AUDIO_ANNOUNCEMENT_TIMEOUT_SEC
,
quoted_path
,
volume
,
AUDIO_LOCAL_ALSA_DEVICE
);
}
else
{
/* PulseAudio 无法暂停时走其默认输出,避免直接打开同一声卡导致 device busy。 */
snprintf
(
command
,
sizeof
(
command
),
"timeout -s KILL %ds ffplay -nodisp -autoexit -loglevel warning -af
\"
volume=%.3f
\"
%s >/dev/null 2>&1"
,
AUDIO_ANNOUNCEMENT_TIMEOUT_SEC
,
volume
,
quoted_path
);
}
ret
=
system
(
command
);
exit_code
=
audio_system_exit_code
(
ret
);
/* 无论正常结束、播放失败还是 timeout,退出前都恢复浏览器音频。 */
if
(
pulse_suspended
)
{
pulse_suspend_default_output
(
0
);
}
audio_sink_unlock_alsa
();
if
(
exit_code
==
137
)
{
my_zlog_warn
(
"播报超过 %d 秒,已强制停止并释放声卡: %s"
,
AUDIO_ANNOUNCEMENT_TIMEOUT_SEC
,
filepath
);
}
return
exit_code
;
}
int
audioplay_url_with_browser_preempt
(
const
char
*
url
,
double
volume
)
{
char
quoted_url
[
1024
];
char
command
[
2048
];
int
exit_code
;
int
pulse_suspended
;
if
(
url
==
NULL
||
url
[
0
]
==
'\0'
)
{
return
-
1
;
}
shell_single_quote
(
quoted_url
,
sizeof
(
quoted_url
),
url
);
volume
=
audioplay_volume_clamp
(
volume
);
audio_sink_lock_alsa
();
pulse_suspended
=
(
pulse_suspend_default_output
(
1
)
==
0
);
if
(
pulse_suspended
)
{
snprintf
(
command
,
sizeof
(
command
),
"timeout -s KILL %ds ffmpeg -nostdin -hide_banner -loglevel warning -i %s -af
\"
volume=%.3f
\"
-f alsa %s >/dev/null 2>&1"
,
AUDIO_ANNOUNCEMENT_TIMEOUT_SEC
,
quoted_url
,
volume
,
AUDIO_LOCAL_ALSA_DEVICE
);
}
else
{
snprintf
(
command
,
sizeof
(
command
),
"timeout -s KILL %ds ffplay -nodisp -autoexit -loglevel warning -af
\"
volume=%.3f
\"
%s >/dev/null 2>&1"
,
AUDIO_ANNOUNCEMENT_TIMEOUT_SEC
,
volume
,
quoted_url
);
}
exit_code
=
audio_system_exit_code
(
system
(
command
));
if
(
pulse_suspended
)
{
pulse_suspend_default_output
(
0
);
}
audio_sink_unlock_alsa
();
if
(
exit_code
==
137
)
{
my_zlog_warn
(
"网络播报超过 %d 秒,已强制停止并释放声卡: %s"
,
AUDIO_ANNOUNCEMENT_TIMEOUT_SEC
,
url
);
}
return
exit_code
;
}
//接收音频播放
void
audioplay_mqtt_receive
(
cJSON
*
json
)
{
// 解析"audioLink"字段(修正了原始JSON中的拼写错误)
...
...
@@ -212,26 +359,21 @@ void audioplay_send_mqtt() {
//音频播放
void
audioplay_cycle
(){
char
command
[
1024
];
int
ret
;
while
(
1
){
if
(
s_audio_status
==
0
){
char
*
urlmoddle
=
s_urlbuf
;
s_audio_volume
=
audioplay_volume_clamp
(
s_audio_volume
);
snprintf
(
command
,
sizeof
(
command
),
"sudo ffplay -nodisp -autoexit -loglevel quiet -af
\"
volume=%.3f
\"
\"
%s
\"
"
,
s_audio_volume
,
urlmoddle
);
my_zlog_debug
(
"播放地址: %s"
,
s_urlbuf
);
my_zlog_debug
(
"执行播放命令: %s"
,
command
);
ret
=
system
(
command
);
ret
=
audioplay_url_with_browser_preempt
(
urlmoddle
,
s_audio_volume
);
if
(
ret
!=
0
)
{
my_zlog_error
(
"播放失败"
);
s_audio_status
=
2
;
}
if
(
WIFEXITED
(
ret
)
&&
WEXITSTATUS
(
ret
)
==
0
)
{
if
(
ret
==
0
)
{
my_zlog_debug
(
"播放已成功完成 : %s "
,
s_urlbuf
);
s_audio_status
=
1
;
}
else
{
...
...
@@ -241,16 +383,25 @@ void audioplay_cycle(){
audioplay_send_mqtt
();
}
char
local_filepath
[
sizeof
(
s_local_filepath
)];
int
local_play_pending
=
0
;
pthread_mutex_lock
(
&
s_local_play_mutex
);
if
(
s_local_play_pending
)
{
s_local_play_pending
=
0
;
snprintf
(
command
,
sizeof
(
command
),
"ffplay -nodisp -autoexit -loglevel quiet
\"
%s
\"
"
,
s_local_filepath
);
my_zlog_debug
(
"播放本地音频: %s"
,
s_local_filepath
);
ret
=
system
(
command
);
if
(
WIFEXITED
(
ret
)
&&
WEXITSTATUS
(
ret
)
==
0
)
{
my_zlog_debug
(
"本地音频播放完成: %s"
,
s_local_filepath
);
snprintf
(
local_filepath
,
sizeof
(
local_filepath
),
"%s"
,
s_local_filepath
);
local_play_pending
=
1
;
}
pthread_mutex_unlock
(
&
s_local_play_mutex
);
if
(
local_play_pending
)
{
int
exit_code
;
my_zlog_info
(
"播放本地音频: %s"
,
local_filepath
);
exit_code
=
audioplay_file_with_browser_preempt
(
local_filepath
,
1
.
0
);
if
(
exit_code
==
0
)
{
my_zlog_debug
(
"本地音频播放完成: %s"
,
local_filepath
);
}
else
{
my_zlog_warn
(
"本地音频播放失败
: %s"
,
s_
local_filepath
);
my_zlog_warn
(
"本地音频播放失败
exit=%d: %s"
,
exit_code
,
local_filepath
);
}
}
...
...
@@ -374,7 +525,7 @@ int audio_speaker_init() {
int
audio_init
(){
delay_s
(
5
);
delay_s
(
1
);
audio_wheat_init
();
delay_s
(
1
);
audio_speaker_init
();
...
...
drivers/sensors/audioplay.c.bak-audio-preempt-20260717
0 → 100644
View file @
7200adca
#include "common.h"
#include "audioplay.h"
#include "audio_sink.h" /* USB声卡排队锁 */
#include "device_identity.h"
#include "mqtt_init.h"
#include "http_config_mqtt.h"
#include "audiotts_play.h"
#include "wifi_autoconfig.h"
#include <stdio.h>
#include <pthread.h>
#include <strings.h>
#include <unistd.h>
#include <sys/wait.h>
#define AUDIO_USB_ALSA_DEVICE "hw:2,0"
#define AUDIO_LOCAL_ALSA_DEVICE "plughw:2,0"
#define AUDIO_LOCAL_PLAY_TIMEOUT_SEC 8
static int s_audio_status=7;
static char s_urlbuf[512];
static char s_keybuf[256];
static double s_audio_volume=0.8;
#define AUDIO_DRIVING_LEVEL_MAX 30
static int s_local_play_pending = 0;
static char s_local_filepath[512];
static pthread_mutex_t s_local_play_mutex = PTHREAD_MUTEX_INITIALIZER;
static int local_is_cn_lang(const char *lang) {
return lang != NULL && (strcmp(lang, AUDIO_LANG_ZH) == 0 || strcmp(lang, "cn") == 0);
}
static int local_is_en_lang(const char *lang) {
return lang != NULL &&
(strcmp(lang, AUDIO_LANG_EN) == 0 || strcmp(lang, "en") == 0 || strcmp(lang, "EN") == 0);
}
static void local_resolve_lang(const char *language_override, char *out, size_t size) {
if (language_override == NULL || language_override[0] == '\0' ||
strcasecmp(language_override, "default") == 0) {
snprintf(out, size, "%s", audio_get_language());
return;
}
if (local_is_cn_lang(language_override)) {
snprintf(out, size, "%s", AUDIO_LANG_ZH);
return;
}
if (local_is_en_lang(language_override)) {
snprintf(out, size, "%s", AUDIO_LANG_EN);
return;
}
snprintf(out, size, "%s", audio_get_language());
my_zlog_warn("2017 未知 language: %s,使用当前语言: %s", language_override, out);
}
static int local_resolve_filepath(const char *filename, const char *language_override,
char *out, size_t size) {
char lang[8];
int use_default_lang = (language_override == NULL || language_override[0] == '\0' ||
strcasecmp(language_override, "default") == 0);
local_resolve_lang(language_override, lang, sizeof(lang));
if (local_is_cn_lang(lang)) {
snprintf(out, size, "%s/%s", AUDIO_LOCAL_BASE, filename);
return access(out, F_OK) == 0;
}
snprintf(out, size, "%s/%s/%s", AUDIO_LOCAL_BASE, lang, filename);
if (access(out, F_OK) == 0) {
return 1;
}
if (use_default_lang) {
snprintf(out, size, "%s/%s", AUDIO_LOCAL_BASE, filename);
return access(out, F_OK) == 0;
}
return 0;
}
static void local_queue_play(const char *filename, const char *language) {
char resolved[512];
if (!local_resolve_filepath(filename, language, resolved, sizeof(resolved))) {
my_zlog_warn("2017 本地音频不存在: %s", filename);
return;
}
pthread_mutex_lock(&s_local_play_mutex);
snprintf(s_local_filepath, sizeof(s_local_filepath), "%s", resolved);
s_local_play_pending = 1;
pthread_mutex_unlock(&s_local_play_mutex);
my_zlog_info("2017 已排队本地音频: %s", resolved);
}
void audioplay_local_mqtt_receive(cJSON *body) {
if (!cJSON_IsObject(body)) {
my_zlog_warn("2017 body 无效");
return;
}
cJSON *status = cJSON_GetObjectItemCaseSensitive(body, "status");
cJSON *level_item = cJSON_GetObjectItemCaseSensitive(body, "level");
cJSON *language = cJSON_GetObjectItemCaseSensitive(body, "language");
if (!cJSON_IsString(status) || status->valuestring == NULL) {
my_zlog_warn("2017 缺少 status");
return;
}
const char *lang = "default";
if (cJSON_IsString(language) && language->valuestring != NULL) {
lang = language->valuestring;
}
if (strcmp(status->valuestring, "speed") == 0) {
local_queue_play("Speedupforoneminute.mp3", lang);
return;
}
if (strcmp(status->valuestring, "drivinglevel") == 0) {
int level = 0;
if (cJSON_IsString(level_item) && level_item->valuestring != NULL) {
level = atoi(level_item->valuestring);
} else if (cJSON_IsNumber(level_item)) {
level = level_item->valueint;
} else {
my_zlog_warn("2017 drivinglevel 缺少 level");
return;
}
if (level < 1 || level > AUDIO_DRIVING_LEVEL_MAX) {
my_zlog_warn("2017 drivinglevel level 无效: %d (1-%d)",
level, AUDIO_DRIVING_LEVEL_MAX);
return;
}
char filename[48];
snprintf(filename, sizeof(filename), "DrivingLevel%d.mp3", level);
local_queue_play(filename, lang);
return;
}
my_zlog_warn("2017 未知 status: %s", status->valuestring);
}
static double audioplay_volume_clamp(double v) {
if (v < 0.0) {
return 0.0;
}
if (v > 2.0) {
return 2.0;
}
return v;
}
int audio_wheat_init();
static int audio_system_exit_code(int status) {
if (status == -1) {
return -1;
}
if (WIFEXITED(status)) {
return WEXITSTATUS(status);
}
if (WIFSIGNALED(status)) {
return 128 + WTERMSIG(status);
}
return -1;
}
static void shell_single_quote(char *out, size_t size, const char *in) {
size_t pos = 0;
if (!out || size == 0) {
return;
}
out[pos++] = '\'';
if (in) {
for (const char *p = in; *p && pos + 5 < size; p++) {
if (*p == '\'') {
const char *esc = "'\\''";
for (const char *e = esc; *e && pos + 1 < size; e++) {
out[pos++] = *e;
}
} else {
out[pos++] = *p;
}
}
}
if (pos + 1 < size) {
out[pos++] = '\'';
}
out[pos] = '\0';
}
static int play_local_audio_file(const char *filepath) {
char quoted_path[1024];
char command[2048];
int ret;
int exit_code;
shell_single_quote(quoted_path, sizeof(quoted_path), filepath);
/* 排队等待USB声卡: 如果audio_sink正在播放手机音频, 等它释放 */
audio_sink_lock_alsa();
snprintf(command, sizeof(command),
/* sync=true 防止 EOS 提前关 ALSA 丢尾部(只播前半段); channels=2 上混规避 USB 声卡单声道 ring_buffer CRITICAL */
"timeout %ds gst-launch-1.0 -q filesrc location=%s ! mpegaudioparse ! mpg123audiodec ! audioconvert ! audioresample ! audio/x-raw,channels=2 ! alsasink device=%s sync=true >/dev/null 2>&1",
AUDIO_LOCAL_PLAY_TIMEOUT_SEC, quoted_path, AUDIO_LOCAL_ALSA_DEVICE);
ret = system(command);
exit_code = audio_system_exit_code(ret);
if (exit_code != 0) {
my_zlog_warn("本地音频 GStreamer 播放失败 exit=%d,尝试 ffplay: %s",
exit_code, filepath);
snprintf(command, sizeof(command),
"timeout %ds ffplay -nodisp -autoexit -loglevel warning %s",
AUDIO_LOCAL_PLAY_TIMEOUT_SEC, quoted_path);
ret = system(command);
exit_code = audio_system_exit_code(ret);
}
audio_sink_unlock_alsa();
return exit_code;
}
//接收音频播放
void audioplay_mqtt_receive(cJSON *json) {
// 解析"audioLink"字段(修正了原始JSON中的拼写错误)
cJSON *audio_link = cJSON_GetObjectItemCaseSensitive(json, "audioLink");
if (cJSON_IsString(audio_link) && (audio_link->valuestring != NULL)) {
my_zlog_debug("音频链接: %s", audio_link->valuestring);
char *url=audio_link->valuestring;
snprintf(s_urlbuf, sizeof(s_urlbuf), "%s", url);
} else {
my_zlog_warn("错误:无法解析音频链接字段");
}
// 解析"key"字段
cJSON *key_char = cJSON_GetObjectItemCaseSensitive(json, "key");
if (cJSON_IsString(key_char) && (key_char->valuestring != NULL)) {
my_zlog_debug("音频链接KEY: %s", key_char->valuestring);
char *key=key_char->valuestring;
snprintf(s_keybuf, sizeof(s_keybuf), "%s", key);
} else {
my_zlog_warn("错误:无法解析音频链接");
s_audio_status=5;
}
// 解析"status"字段
cJSON *s_audio_status_val = cJSON_GetObjectItemCaseSensitive(json, "status");
if (cJSON_IsNumber(s_audio_status_val)) {
my_zlog_debug("标志: %d", s_audio_status_val->valueint);
s_audio_status=s_audio_status_val->valueint;
} else {
my_zlog_warn("错误:无法解析标志字段");
}
// 解析"volume"字段
cJSON *volume = cJSON_GetObjectItemCaseSensitive(json, "volume");
if (cJSON_IsNumber(volume)) {
my_zlog_debug("声量大小: %.3f", volume->valuedouble);
s_audio_volume = audioplay_volume_clamp(volume->valuedouble);
} else {
my_zlog_warn("错误:无法解析声量字段");
}
}
//发送音频播放是否完毕
void audioplay_send_mqtt() {
cJSON *root = cJSON_CreateObject();
cJSON *body = cJSON_CreateObject();
cJSON *head = cJSON_CreateObject();
// 添加各个字段到 JSON 对象
cJSON_AddStringToObject(body, "type", "audio");
cJSON_AddStringToObject(body, "audioLink", s_urlbuf);
cJSON_AddStringToObject(body, "key", s_keybuf);
cJSON_AddNumberToObject(body, "status", s_audio_status);
cJSON_AddNumberToObject(body, "volume", s_audio_volume);
cJSON_AddNumberToObject(head, "message_type",3001);
cJSON_AddItemToObject(root, "body", body);
cJSON_AddItemToObject(root, "head",head);
// 将 JSON 对象转换为字符串
char* json_string = cJSON_PrintUnformatted(root);
my_zlog_debug("%s",json_string);
mqtt_publish_to_all(mqtt_topic_pure_number(), json_string, 0);
free(json_string);
cJSON_Delete(root);
}
//音频播放
void audioplay_cycle(){
char command[2048];
int ret;
while(1){
if(s_audio_status==0){
char *urlmoddle=s_urlbuf;
s_audio_volume = audioplay_volume_clamp(s_audio_volume);
snprintf(command, sizeof(command),
"sudo ffplay -nodisp -autoexit -loglevel quiet -af \"volume=%.3f\" \"%s\"",
s_audio_volume,urlmoddle);
my_zlog_debug("播放地址: %s", s_urlbuf);
my_zlog_debug("执行播放命令: %s", command);
ret = system(command);
if (ret != 0) {
my_zlog_error("播放失败");
s_audio_status=2;
}
if (WIFEXITED(ret) && WEXITSTATUS(ret) == 0) {
my_zlog_debug("播放已成功完成 : %s ", s_urlbuf);
s_audio_status=1;
} else {
my_zlog_warn("播放失败或中断: %s ", s_urlbuf);
s_audio_status=2;
}
audioplay_send_mqtt();
}
char local_filepath[sizeof(s_local_filepath)];
int local_play_pending = 0;
pthread_mutex_lock(&s_local_play_mutex);
if (s_local_play_pending) {
s_local_play_pending = 0;
snprintf(local_filepath, sizeof(local_filepath), "%s", s_local_filepath);
local_play_pending = 1;
}
pthread_mutex_unlock(&s_local_play_mutex);
if (local_play_pending) {
int exit_code;
my_zlog_info("播放本地音频: %s", local_filepath);
exit_code = play_local_audio_file(local_filepath);
if (exit_code == 0) {
my_zlog_debug("本地音频播放完成: %s", local_filepath);
} else {
my_zlog_warn("本地音频播放失败 exit=%d: %s", exit_code, local_filepath);
}
}
video_tts_play();
delay_ms(100);
}
}
static void trim_line(char *s)
{
if (!s)
return;
s[strcspn(s, "\r\n")] = '\0';
}
static int pulse_pactl_exit_code(int status)
{
if (status == -1)
return -1;
if (WIFEXITED(status))
return WEXITSTATUS(status);
return -1;
}
static int pulse_output_means_already_loaded(const char *output)
{
if (!output || !output[0])
return 0;
return strstr(output, "already loaded") != NULL ||
strstr(output, "Already loaded") != NULL ||
strstr(output, "Module exists") != NULL;
}
static int pulse_module_exists(const char *module_name, const char *device)
{
FILE *fp = popen("pactl list modules short 2>/dev/null", "r");
char line[512];
char needle[96];
if (!fp)
return 0;
snprintf(needle, sizeof(needle), "device=%s", device);
while (fgets(line, sizeof(line), fp) != NULL) {
if (strstr(line, module_name) != NULL && strstr(line, needle) != NULL) {
pclose(fp);
return 1;
}
}
pclose(fp);
return 0;
}
static int pulse_load_alsa_module(const char *module_name, const char *label)
{
char cmd[256];
char output[512];
char line[256];
if (pulse_module_exists(module_name, AUDIO_USB_ALSA_DEVICE)) {
my_zlog_info("%s 已注册,跳过 (%s device=%s)",
label, module_name, AUDIO_USB_ALSA_DEVICE);
return 0;
}
snprintf(cmd, sizeof(cmd), "pactl load-module %s device=%s 2>&1",
module_name, AUDIO_USB_ALSA_DEVICE);
FILE *fp = popen(cmd, "r");
if (!fp) {
my_zlog_warn("%s 注册失败: 无法执行 pactl (%s)", label, strerror(errno));
return -1;
}
output[0] = '\0';
while (fgets(line, sizeof(line), fp) != NULL) {
trim_line(line);
if (!line[0])
continue;
if (output[0]) {
strncat(output, "; ", sizeof(output) - strlen(output) - 1);
}
strncat(output, line, sizeof(output) - strlen(output) - 1);
}
int status = pclose(fp);
int exit_code = pulse_pactl_exit_code(status);
if (exit_code == 0) {
my_zlog_info("%s 注册成功 (%s device=%s)%s%s",
label, module_name, AUDIO_USB_ALSA_DEVICE,
output[0] ? ", pactl: " : "", output[0] ? output : "");
return 0;
}
if (pulse_output_means_already_loaded(output) ||
pulse_module_exists(module_name, AUDIO_USB_ALSA_DEVICE)) {
my_zlog_info("%s 已注册,跳过 (%s device=%s)%s%s",
label, module_name, AUDIO_USB_ALSA_DEVICE,
output[0] ? ": " : "", output[0] ? output : "");
return 0;
}
my_zlog_warn("%s 注册失败 (%s device=%s): exit=%d%s%s",
label, module_name, AUDIO_USB_ALSA_DEVICE, exit_code,
output[0] ? ", pactl: " : ", pactl 无输出",
output[0] ? output : "");
return -1;
}
int audio_wheat_init(){
return pulse_load_alsa_module("module-alsa-sink", "喇叭");
}
int audio_speaker_init() {
return pulse_load_alsa_module("module-alsa-source", "麦");
}
int audio_init(){
delay_s(1);
audio_wheat_init();
delay_s(1);
audio_speaker_init();
return 0;
}
#define CONFIG_FILE "/usr/share/pulseaudio/alsa-mixer/profile-sets/default.conf"
static const char *s_yundea_block =
"# 为 Yundea 1076 USB 二合一设备添加配置\n"
"[Mapping yundea-input]\n"
"device-strings = hw:%f,0\n"
"direction = input\n"
"priority = 100\n"
"channel-map = mono\n"
"\n"
"[Mapping yundea-output]\n"
"device-strings = hw:%f,0\n"
"direction = output\n"
"priority = 100\n"
"channel-map = left,right\n"
"\n"
"[Profile yundea-duplex]\n"
"input-mappings = yundea-input\n"
"output-mappings = yundea-output\n"
"priority = 200\n";
/*
*功能:加入usb声卡配置
*
*/
int audio_config_init() {
delay_s(12);
FILE *fp = fopen(CONFIG_FILE, "r");
if (!fp) {
perror("无法打开配置文件进行读取");
return 1;
}
// 获取文件大小
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
fseek(fp, 0, SEEK_SET);
char *buffer = malloc(size + 1);
if (!buffer) {
my_zlog_error("内存分配失败");
fclose(fp);
return 1;
}
size_t result =fread(buffer, 1, size, fp);
if (result != size) {
my_zlog_warn("Error: Failed to read audio data");
}
buffer[size] = '\0';
fclose(fp);
// 检查是否包含完整配置块
if (strstr(buffer, s_yundea_block) != NULL) {
my_zlog_debug("配置文件中已包含指定配置,跳过添加。");
free(buffer);
return 0;
}
free(buffer);
my_zlog_info("未找到指定配置,正在追加到文件末尾...");
// 打开文件追加配置
fp = fopen(CONFIG_FILE, "a");
if (!fp) {
my_zlog_error("无法打开配置文件进行写入");
return 1;
}
fprintf(fp, "\n%s", s_yundea_block);
fclose(fp);
my_zlog_info("配置已成功追加。");
return 0;
}
drivers/sensors/audioplay.h
View file @
7200adca
...
...
@@ -6,6 +6,13 @@ void audioplay_mqtt_receive(cJSON *body); //接收音频mqtt播放函数
void
audioplay_local_mqtt_receive
(
cJSON
*
body
);
// 2017 有人驾驶本地音频
void
audioplay_cycle
();
//音频播放线程中函数
/*
* 播放播报文件:与项目内其它 ALSA 播放串行,并在播放期间暂停浏览器使用的
* PulseAudio 默认输出。播放完成或超过 20 秒后会恢复浏览器音频。
*/
int
audioplay_file_with_browser_preempt
(
const
char
*
filepath
,
double
volume
);
int
audioplay_url_with_browser_preempt
(
const
char
*
url
,
double
volume
);
int
audio_wheat_init
();
int
audio_speaker_init
();
...
...
drivers/sensors/audioplay.h.bak-audio-preempt-20260717
0 → 100644
View file @
7200adca
#ifndef AUDIOPLAY_H__
#define AUDIOPLAY_H__
#include <cjson/cJSON.h>
void audioplay_mqtt_receive(cJSON *body); //接收音频mqtt播放函数
void audioplay_local_mqtt_receive(cJSON *body); // 2017 有人驾驶本地音频
void audioplay_cycle();//音频播放线程中函数
int audio_wheat_init();
int audio_speaker_init();
int audio_init();
int audio_config_init();//加入配置
#endif
\ No newline at end of file
drivers/sensors/audiotts_play.c
View file @
7200adca
...
...
@@ -471,46 +471,15 @@ void video_tts_play() {
return
;
}
pid_t
pid
=
fork
();
if
(
pid
==
0
)
{
// 子进程:创建新的会话组,脱离父进程组
setsid
();
// 关键!创建新的会话组
char
command
[
256
];
snprintf
(
command
,
sizeof
(
command
),
"ffmpeg -i "
TTS_ADUINO_PTAH
" -af
\"
volume=%.1f
\"
-f alsa default 2>/dev/null"
,
s_volume
);
// 重定向标准输入输出,避免占用终端
if
(
freopen
(
"/dev/null"
,
"r"
,
stdin
)
==
NULL
)
{
my_zlog_error
(
"重定向stdin失败: %s"
,
strerror
(
errno
));
// 可以选择退出或继续
}
if
(
freopen
(
"/dev/null"
,
"w"
,
stdout
)
==
NULL
)
{
my_zlog_error
(
"重定向stdout失败: %s"
,
strerror
(
errno
));
}
if
(
freopen
(
"/dev/null"
,
"w"
,
stderr
)
==
NULL
)
{
my_zlog_error
(
"重定向stderr失败: %s"
,
strerror
(
errno
));
}
int
res
=
system
(
command
);
_exit
(
res
);
// 使用_exit避免清理父进程资源
}
else
if
(
pid
>
0
)
{
// 父进程:非阻塞等待,避免僵尸进程
int
status
;
waitpid
(
pid
,
&
status
,
WNOHANG
);
// 不阻塞的等待
my_zlog_debug
(
"启动独立播放进程 PID: %d"
,
pid
);
s_audio_tts_index
=
0
;
/* audioplay_cycle 本身运行在音频线程,这里同步等待可确保声卡一定被恢复。 */
int
exit_code
=
audioplay_file_with_browser_preempt
(
TTS_ADUINO_PTAH
,
s_volume
);
if
(
exit_code
==
0
)
{
my_zlog_info
(
"TTS 播报完成, volume: %.1f"
,
s_volume
);
}
else
{
my_zlog_warn
(
"TTS 播报失败或超时 exit=%d, volume: %.1f"
,
exit_code
,
s_volume
);
}
else
{
my_zlog_error
(
"fork()失败"
);
s_audio_tts_index
=
0
;
}
}
}
...
...
drivers/sensors/audiotts_play.c.bak-audio-preempt-20260717
0 → 100644
View file @
7200adca
#include "audiotts_play.h"
#include "audioplay.h"
#include "device_identity.h"
#include "mqtt_init.h"
#include "http_config_mqtt.h"
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/buffer.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#define TTS_ADUINO_PTAH PROJECT_ROOT_DIR "/audio.mp3"
// 全局变量:存储 HTTP 响应数据
static char *s_http_response = NULL;
static size_t s_http_response_len = 0;
static bool s_audio_tts_index=0;
static double s_volume=1.0;
// HTTP 响应回调函数:接收服务器返回的 JSON 数据
static size_t http_write_callback(void *ptr, size_t size, size_t nmemb, void *userdata) {
size_t total = size * nmemb;
// 扩容并拷贝响应数据
char *temp = realloc(s_http_response, s_http_response_len + total + 1);
if (!temp) {
my_zlog_error("Realloc response buffer failed");
return 0;
}
s_http_response = temp;
memcpy(s_http_response + s_http_response_len, ptr, total);
s_http_response_len += total;
s_http_response[s_http_response_len] = '\0'; // 末尾加结束符
return total;
}
// 生成 UUID(去除连字符,保证 reqid 唯一性)
static void generate_uuid(char *buf, size_t len) {
if (len < 33) return; // UUID 去除连字符后 32 位
uuid_t uuid;
uuid_generate_random(uuid);
uuid_unparse(uuid, buf);
// 移除连字符
char *p = strchr(buf, '-');
while (p) {
memmove(p, p + 1, strlen(p));
p = strchr(buf, '-');
}
}
// Base64 解码(将响应中的音频数据解码为二进制)
static unsigned char *base64_decode(const char *input, size_t *output_len) {
BIO *bio, *b64;
size_t len = strlen(input);
unsigned char *buffer = (unsigned char *)malloc(len * 3 / 4 + 1024); // 预留足够空间
if (!buffer) return NULL;
// 初始化 OpenSSL Base64 解码
OpenSSL_add_all_algorithms(); // 初始化 OpenSSL 算法(避免解码失败)
b64 = BIO_new(BIO_f_base64());
bio = BIO_new_mem_buf(input, len);
bio = BIO_push(b64, bio);
// 禁用换行符处理(关键:避免 Base64 字符串含换行导致解码失败)
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL);
*output_len = BIO_read(bio, buffer, len * 3 / 4 + 1024);
// 清理资源
BIO_free_all(bio);
// 调整缓冲区大小为实际解码长度
buffer = realloc(buffer, *output_len);
return buffer;
}
/**
* 保存音频数据到文件(目录已存在)
* @param data 音频数据指针
* @param len 音频数据长度
* @param filepath 完整文件路径
* @return 0成功,1失败
*/
static int save_audio_file(const void *data, size_t len, const char *filepath) {
if (!data || len == 0 || !filepath) {
my_zlog_error("Invalid parameters for save_audio_file\n");
return 1;
}
FILE *fp = fopen(filepath, "wb");
if (!fp) {
my_zlog_error( "Open file %s failed: %s\n", filepath, strerror(errno));
return 1;
}
size_t written = fwrite(data, 1, len, fp);
fclose(fp);
if (written != len) {
my_zlog_error("Write file %s incomplete: %zu/%zu bytes", filepath, written, len);
remove(filepath); // 删除不完整的文件
return 1;
}
my_zlog_debug("Success! Audio saved to: %s (%zu bytes)", filepath, written);
return 0;
}
/*
*功能:获得随机字符串,不使用uuid
*/
static char* generate_random_string() {
// 设置随机种子(建议在main函数中调用一次即可)
static int seeded = 0;
if (!seeded) {
srand((unsigned int)time(NULL));
seeded = 1;
}
// 动态分配内存(10个字符 + 终止符)
char* str = (char*)malloc(11 * sizeof(char));
if (!str) return NULL;
// 生成数字字符(ASCII 48-57)
for (int i = 0; i < 10; i++) {
str[i] = '0' + (rand() % 10);
}
str[10] = '\0'; // 添加终止符
return str;
}
char *get_audio_text_tts(char *date ,bool flag){
static char text[256] = {0};
if(flag==1){
return text;
}else {
size_t date_len = strlen(date);
if (date) {
strcpy(text, date);
text[date_len] = '\0'; // 确保以null结尾
} else {
text[0] = '\0'; // 如果date为NULL,清空文本
}
}
}
/**
* 保存音频数据到文件(目录已存在)
* @param text 传入需要转化的文字指针
* @return 0成功,1失败
*/
int audio_tts(const char *text) {
// 1. 配置参数(替换为你的真实信息,已验证有效)
const char *appid = "5745089507"; // 你的火山引擎 AppID
const char *access_token = "0BeoBb_DOjrxfM4aTsJfUFVVS0k5V9mt"; // 你的访问令牌
const char *voice_type = "zh_female_linjianvhai_moon_bigtts"; // 非流式音色
const char *cluster = "volcano_tts"; // 固定值(必选)
char *uid = generate_random_string(); // 任意非空字符串
const char *encoding = "mp3"; // 输出格式(mp3/wav/pcm/ogg_opus)
const char *endpoint = "https://openspeech.bytedance.com/api/v1/tts"; // HTTP 接口地址
// 2. 生成唯一 reqid(每次调用必须不同)
char reqid[33] = {0};
generate_uuid(reqid, sizeof(reqid));
my_zlog_debug("Generated ReqID: %s", reqid);
// 3. 构建请求 JSON(严格遵循接口参数格式)
cJSON *root = cJSON_CreateObject();
if (!root) {
my_zlog_error("Create JSON root failed");
free(uid);
return 1;
}
// app 节点(必选)
cJSON *app = cJSON_CreateObject();
cJSON_AddStringToObject(app, "appid", appid);
cJSON_AddStringToObject(app, "token", access_token);
cJSON_AddStringToObject(app, "cluster", cluster);
cJSON_AddItemToObject(root, "app", app);
// user 节点(必选)
cJSON *user = cJSON_CreateObject();
cJSON_AddStringToObject(user, "uid", uid);
cJSON_AddItemToObject(root, "user", user);
// audio 节点(必选)
cJSON *audio = cJSON_CreateObject();
cJSON_AddStringToObject(audio, "voice_type", voice_type);
cJSON_AddStringToObject(audio, "encoding", encoding);
cJSON_AddNumberToObject(audio, "speed_ratio", 1.0); // 语速(0.1~2.0)
cJSON_AddNumberToObject(audio, "rate", 16000); // 采样率(默认24000)
cJSON_AddNumberToObject(audio, "loudness_ratio", 1.0); // 音量(0.5~2.0)
cJSON_AddBoolToObject(audio, "enable_emotion", true); // 开启情感
cJSON_AddStringToObject(audio, "emotion", "gentle"); // 情感类型(gentle=温柔)
cJSON_AddNumberToObject(audio, "emotion_scale", 4.0); // 情感强度(1~5,默认4)
cJSON_AddItemToObject(root, "audio", audio);
// request 节点(必选)
cJSON *request = cJSON_CreateObject();
cJSON_AddStringToObject(request, "reqid", reqid);
cJSON_AddStringToObject(request, "text", text);
cJSON_AddStringToObject(request, "operation", "query"); // HTTP 非流式固定为 query
cJSON_AddStringToObject(request, "extra_param", "{\"disable_markdown_filter\":true}"); // 附加参数
cJSON_AddItemToObject(root, "request", request);
// 生成 JSON 字符串(无格式,减少传输体积)
char *payload = cJSON_PrintUnformatted(root);
cJSON_Delete(root);
if (!payload) {
my_zlog_error("Generate JSON payload failed");
free(uid);
return 1;
}
my_zlog_debug("Request Payload: %s", payload);
// 4. 初始化 curl 并配置 HTTP 请求
curl_global_init(CURL_GLOBAL_DEFAULT);
CURL *curl = curl_easy_init();
if (!curl) {
my_zlog_error("Initialize curl failed");
free(payload);
free(uid);
return 1;
}
// 设置请求头(关键:Authorization 分号分隔,Content-Type 为 JSON)
struct curl_slist *headers = NULL;
char auth_header[256] = {0};
snprintf(auth_header, sizeof(auth_header), "Authorization: Bearer;%s", access_token);
headers = curl_slist_append(headers, auth_header);
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, "Accept: application/json");
if (s_http_response == NULL) {
s_http_response_len = 0;
}else {
return 1;
}
// curl 核心配置
curl_easy_setopt(curl, CURLOPT_URL, endpoint);
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_POST, 1L); // POST 方法
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, payload); // 请求体
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, strlen(payload));
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, http_write_callback); // 接收响应回调
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L); // 开发环境禁用 SSL 证书校验(生产环境建议启用)
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 15L); // 超时时间 15 秒(避免网络慢导致超时)
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L); // 连接超时 10 秒
// 5. 执行 HTTP 请求
my_zlog_debug("Sending request to %s...", endpoint);
CURLcode res = curl_easy_perform(curl);
if (res != CURLE_OK) {
my_zlog_error("curl_easy_perform failed: %s", curl_easy_strerror(res));
free(uid);
free(payload);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
return 1;
}
// 6. 解析 HTTP 响应(JSON 格式)
my_zlog_debug("Response Length: %zu bytes", s_http_response_len);
if (s_http_response_len == 0) {
my_zlog_error("Empty response from server");
free(uid);
free(payload);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
return 1;
}
// 解析 JSON 响应
cJSON *response_json = cJSON_Parse(s_http_response);
if (!response_json) {
my_zlog_error("Parse response JSON failed: %s", cJSON_GetErrorPtr());
free(uid);
free(payload);
free(s_http_response);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
return 1;
}
// // 打印完整响应(便于调试,可根据需要注释)
// char *response_str = cJSON_Print(response_json);
// printf("Full Response JSON (abbreviated):\n%s\n", response_str);
// free(response_str);
// 提取响应状态(火山引擎该接口成功码:3000 + message=Success)
cJSON *code = cJSON_GetObjectItem(response_json, "code");
cJSON *message = cJSON_GetObjectItem(response_json, "message");
if (!code || code->valueint != 3000 || !message || strcmp(message->valuestring, "Success") != 0) {
my_zlog_error("Request failed! Code: %d, Message: %s",
code ? code->valueint : -1,
message ? message->valuestring : "No message");
cJSON_Delete(response_json);
free(uid);
free(payload);
free(s_http_response);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
return 1;
}
my_zlog_debug("Request succeeded! Code: %d, Message: %s", code->valueint, message->valuestring);
// 关键:直接提取 data 字段的 Base64 音频字符串(接口实际返回格式)
cJSON *audio_base64 = cJSON_GetObjectItem(response_json, "data");
if (!audio_base64 || !audio_base64->valuestring || strlen(audio_base64->valuestring) == 0) {
my_zlog_error("Response missing audio data (data field is empty)");
cJSON_Delete(response_json);
free(uid);
free(payload);
free(s_http_response);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
return 1;
}
my_zlog_debug("Audio Base64 Length: %zu bytes", strlen(audio_base64->valuestring));
// 7. Base64 解码音频数据
size_t audio_len = 0;
unsigned char *audio_data = base64_decode(audio_base64->valuestring, &audio_len);
if (!audio_data || audio_len == 0) {
my_zlog_error("Base64 decode audio failed");
cJSON_Delete(response_json);
free(uid);
free(payload);
free(s_http_response);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
return 1;
}
my_zlog_debug("Decoded Audio Length: %zu bytes", audio_len);
if(save_audio_file(audio_data, audio_len, TTS_ADUINO_PTAH) != 0) {//保存文件
my_zlog_error("save_audio_file fail");
}
// 9. 清理资源(避免内存泄漏)
cJSON_Delete(response_json);
free(uid);
free(audio_data);
free(payload);
free(s_http_response);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
curl_global_cleanup();
// 初始化响应缓冲
s_http_response = NULL;
s_http_response_len = 0;
return 0;
}
/*
*功能: 设置播放标识
*/
void set_audio_tts_index(bool index){
s_audio_tts_index=index;
}
/*
*功能: 发送mqtt
*/
int mqtt_audio_tts_send(int status){
// 创建根对象
cJSON* root = cJSON_CreateObject();
if (root == NULL) {
return 1;
}
// 创建body对象
cJSON* body = cJSON_CreateObject();
if (body == NULL) {
cJSON_Delete(root);
return 1;
}
cJSON *head = cJSON_CreateObject();
if (head == NULL) {
cJSON_Delete(root);
return 1;
}
// 将body添加到root
cJSON_AddItemToObject(root, "body", body);
// 添加type和status字段到body
cJSON_AddStringToObject(body, "type", "tts_audio");
cJSON_AddNumberToObject(body, "status", status);
cJSON_AddNumberToObject(head, "message_type",3020);
cJSON_AddItemToObject(root, "body", body);
cJSON_AddItemToObject(root, "head",head);
// 生成JSON字符串
char* json_str = cJSON_PrintUnformatted(root);
my_zlog_debug("%s",json_str);
mqtt_publish_to_all(mqtt_topic_pure_number(), json_str, 0);
free(json_str);
cJSON_Delete(root);
return 0;
}
int mqtt_audio_tts(cJSON *body){
// 解析type字段
cJSON* type = cJSON_GetObjectItem(body, "type");
if(type == NULL || strcmp(type->valuestring, "tts_audio") != 0){
my_zlog_error("type is tts_audio error");
return 1;
}
// 解析date字段
cJSON* date = cJSON_GetObjectItem(body, "date");
if(date!=NULL){
get_audio_text_tts(date->valuestring,0);
my_zlog_debug("text:%s",date->valuestring);
}else {
return 1;
}
// 解析status字段
cJSON* status = cJSON_GetObjectItem(body, "status");
if(mqtt_audio_tts_send(status->valueint)==0){
set_audio_tts_index(status->valueint);
}else{
set_audio_tts_index(0);
my_zlog_error("statis error");
return 1;
}
cJSON* volume = cJSON_GetObjectItem(body, "volume");
s_volume = volume->valuedouble;
return 0;
}
/*
*功能,tts播放
*/
void video_tts_play() {
if(s_audio_tts_index == 1) {
if(audio_tts(get_audio_text_tts(NULL, 1))!=0){
my_zlog_error("audio_tts fail");
s_audio_tts_index = 0;
return;
}
pid_t pid = fork();
if (pid == 0) {
// 子进程:创建新的会话组,脱离父进程组
setsid(); // 关键!创建新的会话组
char command[256];
snprintf(command, sizeof(command),
"ffmpeg -i " TTS_ADUINO_PTAH " -af \"volume=%.1f\" -f alsa default 2>/dev/null",
s_volume);
// 重定向标准输入输出,避免占用终端
if (freopen("/dev/null", "r", stdin) == NULL) {
my_zlog_error("重定向stdin失败: %s", strerror(errno));
// 可以选择退出或继续
}
if (freopen("/dev/null", "w", stdout) == NULL) {
my_zlog_error("重定向stdout失败: %s", strerror(errno));
}
if (freopen("/dev/null", "w", stderr) == NULL) {
my_zlog_error("重定向stderr失败: %s", strerror(errno));
}
int res = system(command);
_exit(res); // 使用_exit避免清理父进程资源
}
else if (pid > 0) {
// 父进程:非阻塞等待,避免僵尸进程
int status;
waitpid(pid, &status, WNOHANG); // 不阻塞的等待
my_zlog_debug("启动独立播放进程 PID: %d", pid);
s_audio_tts_index = 0;
}
else {
my_zlog_error("fork()失败");
s_audio_tts_index = 0;
}
}
}
// void video_tts_play(){
// if(s_audio_tts_index == 1) {
// audio_tts(get_audio_text_tts(NULL,1));
// char command[256];
// snprintf(command, sizeof(command),
// "ffmpeg -i " TTS_ADUINO_PTAH " -af \"volume=%.1f\" -f alsa default",
// s_volume);
// int res = system(command);
// if(res == 0)
// my_zlog_info("play success, volume: %.1f", s_volume);
// else
// my_zlog_error("system play fail, volume: %.1f", s_volume);
// s_audio_tts_index == 0;
// }
// }
\ No newline at end of file
modules/CMakeLists.txt
View file @
7200adca
...
...
@@ -8,6 +8,7 @@ pkg_check_modules(WEBRTCPUSH_GST REQUIRED
)
pkg_check_modules
(
WEBRTCPUSH_JSON REQUIRED json-glib-1.0
)
pkg_check_modules
(
WEBRTCPUSH_SOUP REQUIRED libsoup-2.4
)
pkg_check_modules
(
WEBRTCPUSH_JPEG REQUIRED libjpeg
)
file
(
GLOB_RECURSE MODULES_SOURCES
${
CMAKE_CURRENT_SOURCE_DIR
}
/logger/*.c
...
...
@@ -47,6 +48,7 @@ set(MODULES_INCLUDE_DIRS
${
WEBRTCPUSH_GST_INCLUDE_DIRS
}
${
WEBRTCPUSH_JSON_INCLUDE_DIRS
}
${
WEBRTCPUSH_SOUP_INCLUDE_DIRS
}
${
WEBRTCPUSH_JPEG_INCLUDE_DIRS
}
PARENT_SCOPE
)
...
...
@@ -54,6 +56,7 @@ set(WEBRTCPUSH_LIBRARIES
${
WEBRTCPUSH_GST_LIBRARIES
}
${
WEBRTCPUSH_JSON_LIBRARIES
}
${
WEBRTCPUSH_SOUP_LIBRARIES
}
${
WEBRTCPUSH_JPEG_LIBRARIES
}
PARENT_SCOPE
)
...
...
@@ -61,5 +64,6 @@ set(WEBRTCPUSH_CFLAGS
${
WEBRTCPUSH_GST_CFLAGS_OTHER
}
${
WEBRTCPUSH_JSON_CFLAGS_OTHER
}
${
WEBRTCPUSH_SOUP_CFLAGS_OTHER
}
${
WEBRTCPUSH_JPEG_CFLAGS_OTHER
}
PARENT_SCOPE
)
modules/http/http_config_mqtt.h
View file @
7200adca
...
...
@@ -3,7 +3,7 @@
#include "common.h"
#define DEFAULT_MQTT_BROKER_ADDRESS "
119.45.167.177
"
#define DEFAULT_MQTT_BROKER_ADDRESS "
mqtt177.controlmelive.com
"
#define DEVICE_DEFAULT_VIDEO "https://jywy.yd-ss.com?dev="
typedef
struct
{
...
...
modules/mqtt/mqtt_infor_handle.c
View file @
7200adca
...
...
@@ -213,7 +213,7 @@ static void refresh_cam_browser_only(const char *reason)
void
message_2_judyverify
(
cJSON
*
body
)
{
if
(
VERIFIED_MODE
==
FALSE
)
if
(
VERIFIED_MODE
==
false
)
{
refresh_cam_browser_only
(
"MQTT type2 verify off"
);
my_zlog_warn
(
"不使用验证"
);
...
...
@@ -234,7 +234,7 @@ void message_2_judyverify(cJSON *body)
// 当接收到3时候验证
void
message_3_judyverify
(
cJSON
*
body
)
{
if
(
VERIFIED_MODE
==
FALSE
)
if
(
VERIFIED_MODE
==
false
)
{
message_3
(
body
);
my_zlog_warn
(
"不使用验证"
);
...
...
@@ -255,7 +255,7 @@ void message_3_judyverify(cJSON *body)
// 当接收到4时候验证
void
message_4_judyverify
(
cJSON
*
body
)
{
if
(
VERIFIED_MODE
==
FALSE
)
if
(
VERIFIED_MODE
==
false
)
{
message_4
(
body
);
my_zlog_warn
(
"不使用验证"
);
...
...
@@ -276,7 +276,7 @@ void message_4_judyverify(cJSON *body)
void
message_6_steering_judyverify
(
cJSON
*
body
)
{
if
(
VERIFIED_MODE
==
FALSE
)
if
(
VERIFIED_MODE
==
false
)
{
steering_mqtt_recv
(
body
);
my_zlog_warn
(
"不使用验证"
);
...
...
@@ -303,7 +303,7 @@ void message_7_joystick_judyverify(cJSON *body)
return
;
}
if
(
VERIFIED_MODE
==
FALSE
)
if
(
VERIFIED_MODE
==
false
)
{
ptz_driver_set_joystick_json
(
joystick_ctrl
);
my_zlog_warn
(
"不使用验证"
);
...
...
modules/mqtt/mqtt_verify.c
View file @
7200adca
...
...
@@ -9,9 +9,9 @@
#include "webrtcpush_run.h"
/*TRUE为打开验证,FALSE为关闭验证*/
bool
VERIFIED_MODE
=
FALSE
;
bool
VERIFIED_MODE
=
false
;
static
bool
s_service_verify
=
TRUE
;
//验证判断
static
bool
s_service_verify
=
true
;
//验证判断
int
g_verify_count
=
6000
;
//判断是否有一分钟
...
...
@@ -19,7 +19,7 @@ static char s_only_id_middle[11];
static
char
s_secret_key
[
1024
];
//存储上一次的topic
static
bool
s_secret_key_index
=
TRUE
;
//用于判断是否对比topic
static
bool
s_secret_key_index
=
true
;
//用于判断是否对比topic
/* topic 不同时发后端验证,失败可重试,超过 3 次不通过则判定该 topic 不通过 */
static
char
s_pending_verify_token
[
1024
];
...
...
@@ -172,7 +172,7 @@ int receive_jwt(cJSON *body) {
my_zlog_debug
(
"token时间戳:%1d"
,(
long
)
token_time_sec
);
if
(
token_time_sec
>
current_verify_time
){
if
(
s_service_verify
==
TRUE
)
g_verify_index
=
0
;
//验证默认为正确
if
(
s_service_verify
==
true
)
g_verify_index
=
0
;
//验证默认为正确
my_zlog_debug
(
"g_verify_index= %d "
,
g_verify_index
);
if
(
g_verify_count
>
12000
){
send_jwtser
(
token
);
...
...
@@ -205,7 +205,7 @@ int message2006_verify(cJSON *body){
if
(
json_status
==
NULL
||
json_onlyid
==
NULL
)
{
my_zlog_warn
(
"验证为空"
);
s_service_verify
=
FALSE
;
s_service_verify
=
false
;
g_verify_index
=
1
;
my_zlog_debug
(
"g_verify_index= %d "
,
g_verify_index
);
return
1
;
...
...
@@ -219,10 +219,10 @@ int message2006_verify(cJSON *body){
gboolean
driver_changed
=
(
s_secret_key
[
0
]
!=
'\0'
&&
strcmp
(
s_secret_key
,
s_pending_verify_token
)
!=
0
);
my_zlog_info
(
"获得验证正确, onlyid=%s"
,
onlyid
);
s_service_verify
=
TRUE
;
s_service_verify
=
true
;
g_verify_index
=
0
;
/* 后端通过后必须清除失败标志,否则正确 token 仍会提示验证不通过 */
strcpy
(
s_secret_key
,
s_pending_verify_token
);
s_secret_key_index
=
FALSE
;
s_secret_key_index
=
false
;
s_verify_retry_count
=
0
;
s_verify_retry_after_time
=
0
;
if
(
driver_changed
)
...
...
@@ -238,13 +238,13 @@ int message2006_verify(cJSON *body){
}
else
{
my_zlog_warn
(
"topic验证超过%d次不通过,禁止使用, token=%s"
,
VERIFY_RETRY_MAX
,
s_pending_verify_token
);
g_verify_index
=
1
;
s_service_verify
=
FALSE
;
s_service_verify
=
false
;
return
2
;
}
}
else
{
my_zlog_warn
(
"获得验证错误,禁止使用, onlyid=%s status=%s"
,
onlyid
,
status
);
g_verify_index
=
1
;
s_service_verify
=
FALSE
;
s_service_verify
=
false
;
return
2
;
}
}
...
...
@@ -297,14 +297,14 @@ int message2013_recverigy_open(cJSON *body_raw)
s_verify_open_query_gave_up
=
false
;
if
(
verify_status
->
valueint
==
1
)
{
VERIFIED_MODE
=
TRUE
;
VERIFIED_MODE
=
true
;
my_zlog_info
(
"开启验证成功(加密校验通过), verify_status=1"
);
cJSON_Delete
(
body
);
return
0
;
}
if
(
verify_status
->
valueint
==
0
)
{
VERIFIED_MODE
=
FALSE
;
VERIFIED_MODE
=
false
;
my_zlog_info
(
"关闭验证成功(加密校验通过), verify_status=0"
);
cJSON_Delete
(
body
);
return
0
;
...
...
modules/webrtcpush/audio_sink.c
View file @
7200adca
#include "audio_sink.h"
#include "webrtcpush_config.h"
#include "webrtcpush_log.h"
#include <gst/gst.h>
#include <gst/app/gstappsrc.h>
/* USB声卡排队锁: audio_sink 和本地音频串行访问 plughw:2,0 */
static
GMutex
g_alsa_device_lock
;
static
AudioSink
*
g_audio_sink_singleton
=
NULL
;
/*
* 手机→设备方向的音频播放管道(按键模式):
* appsrc(opus payload) → opusdec → audioconvert → audioresample → volume → alsasink
*
* 手机端是"按住说话"模式(最长 15s),不是持续推流,因此:
* - on_audio_message 收到包后入队,立即返回(不阻塞 libdatachannel 线程)
* - 独立线程消费队列,维护"按键会话"
* - 会话开始(首包到达):pipeline 切到 PLAYING
* - 会话结束(500ms 无包 或 15s 超时):flush appsrc + pipeline 切到 READY
* 切到 READY 释放 ALSA 设备,避免 alsasink 持续占用/空转
* 手机→设备方向的音频不再走 RTP:手机端 audio 为 recvonly(只收不发),
* 手机→设备的音频通过 DataChannel 发送 MP4,由 decodebin 播放。
* 因此本模块不再创建 GStreamer 接收管道,仅保留 USB 声卡全局锁,
* 供 DataChannel 播放线程与本地提示音串行访问 ALSA 设备使用。
*/
#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 */
GMutex
lock
;
};
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
);
}
}
void
audio_sink_lock_alsa
(
void
)
{
g_mutex_lock
(
&
g_alsa_device_lock
);
}
void
audio_sink_unlock_alsa
(
void
)
{
g_mutex_unlock
(
&
g_alsa_device_lock
);
}
static
void
set_pipeline_state_locked
(
AudioSink
*
src
,
GstState
state
)
void
audio_sink_interrupt
(
AudioSink
*
src
)
{
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
;
(
void
)
src
;
/* RTP 接收管道已移除,无会话需要中断;DataChannel 播放通过 lock/unlock 串行访问 ALSA。 */
}
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
])
{
(
void
)
alsa_device
;
if
(
error_message
)
*
error_message
=
g_strdup
(
"no ALSA device"
);
return
NULL
;
}
*
error_message
=
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
);
g_audio_sink_singleton
=
src
;
my_zlog_info
(
"audio_sink: started (RTP receive removed, ALSA lock only)"
);
return
src
;
}
...
...
@@ -233,45 +43,15 @@ 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
);
}
if
(
g_audio_sink_singleton
==
src
)
g_audio_sink_singleton
=
NULL
;
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
);
(
void
)
src
;
(
void
)
volume
;
/* 无 GStreamer 管道,音量设置为空操作(保留接口供 volume_control 调用)。 */
}
modules/webrtcpush/audio_sink.h
View file @
7200adca
...
...
@@ -17,4 +17,11 @@ 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
);
/* USB声卡排队锁: 本地音频和audio_sink串行访问同一个USB声卡 */
void
audio_sink_lock_alsa
(
void
);
void
audio_sink_unlock_alsa
(
void
);
/* 中断当前按键会话, 释放ALSA设备让DataChannel音频能立即播放 */
void
audio_sink_interrupt
(
AudioSink
*
src
);
#endif
\ No newline at end of file
modules/webrtcpush/mpp_h264_source.c
View file @
7200adca
...
...
@@ -4,9 +4,13 @@
#include "webrtcpush_log.h"
#include <gst/app/gstappsink.h>
#include <gst/app/gstappsrc.h>
#include <gst/video/video-event.h>
#include <fcntl.h>
#include <stdio.h>
#include <jpeglib.h>
#include <linux/videodev2.h>
#include <setjmp.h>
#include <string.h>
#include <sys/ioctl.h>
#include <unistd.h>
...
...
@@ -19,6 +23,10 @@
#define MPP_VIDEO_MAX_BPS WEBRTCPUSH_MAX_BITRATE
#define MPP_H264_PROFILE 66
/* Baseline: 与 SDP profile-level-id=42e01f 一致,低延迟优先 */
#define MPP_IDR_EVENT_RETRY_US 500000
#define MJPEG_DECODE_WARN_US 15000
#define MJPEG_DECODE_WARN_INTERVAL_US 5000000
#define PERF_LOG_FRAMES (WEBRTCPUSH_H264_FPS * 5)
#define TIMING_RING_SIZE 32
#define WRTC_V4L2_CAP_VIDEO_CAPTURE (1u << 0)
#define WRTC_V4L2_CAP_VIDEO_CAPTURE_MPLANE (1u << 12)
...
...
@@ -30,25 +38,67 @@ typedef enum {
CAPTURE_MJPEG_720P30
,
}
CaptureMode
;
typedef
struct
{
guint64
pts_ns
;
gint64
capture_us
;
gint64
push_us
;
gint64
enc_in_us
;
guint
decode_us
;
guint
convert_us
;
gboolean
valid
;
}
FrameTiming
;
struct
MppH264Source
{
GstElement
*
pipeline
;
GstElement
*
capture_pipeline
;
GstElement
*
enc
;
GstElement
*
parse
;
GstElement
*
eq
;
GstElement
*
appsink
;
GstElement
*
mjpeg_sink
;
GstElement
*
raw_appsrc
;
GstElement
*
vsrc
;
/* v4l2src 指针,用于 recover 时更新设备 */
gboolean
use_mpp
;
gboolean
manual_mjpeg
;
volatile
gboolean
stop_decode
;
GThread
*
decode_thread
;
guint8
*
frame_buf
;
size_t
frame_size
;
size_t
frame_capacity
;
guint8
*
i420_buf
;
size_t
i420_capacity
;
guint8
*
nv12_buf
;
size_t
nv12_capacity
;
guint8
*
jpeg_tmp_u
;
guint8
*
jpeg_tmp_v
;
size_t
jpeg_tmp_capacity
;
guint
keyframe_req
;
gint
pending_bitrate_bps
;
guint
applied_bitrate_bps
;
volatile
gboolean
pending_idr
;
gboolean
pending_idr_event_sent
;
gint64
last_keyframe_event_us
;
volatile
gboolean
pending_recover
;
gulong
enc_sink_probe_id
;
guint
bus_watch_id
;
guint
capture_bus_watch_id
;
gchar
*
sprop_parameter_sets
;
GMutex
timing_lock
;
FrameTiming
timings
[
TIMING_RING_SIZE
];
guint
timing_next
;
guint
perf_frames
;
guint64
perf_decode_sum_us
;
guint64
perf_convert_sum_us
;
guint64
perf_encode_sum_us
;
guint64
perf_total_sum_us
;
guint
perf_decode_max_us
;
guint
perf_convert_max_us
;
guint
perf_encode_max_us
;
guint
perf_total_max_us
;
guint
jpeg_slow_count
;
guint
jpeg_slow_last_us
;
guint
jpeg_slow_max_us
;
gint64
jpeg_slow_last_warn_us
;
};
static
gboolean
pull_sample
(
MppH264Source
*
src
,
gboolean
require_idr
,
...
...
@@ -56,6 +106,10 @@ static gboolean pull_sample(MppH264Source *src, gboolean require_idr,
size_t
*
size
,
gboolean
*
is_idr
,
guint64
*
pts_ns
);
static
gboolean
send_force_key_unit
(
MppH264Source
*
src
);
static
void
apply_pending_bitrate
(
MppH264Source
*
src
);
static
gpointer
mjpeg_decode_thread
(
gpointer
user_data
);
static
void
timing_mark_encoder_in
(
MppH264Source
*
src
,
guint64
pts_ns
,
gint64
enc_in_us
);
static
gboolean
bus_watch_cb
(
GstBus
*
bus
,
GstMessage
*
msg
,
gpointer
user_data
)
{
...
...
@@ -243,7 +297,7 @@ static CaptureMode probe_720p_capture_mode(const char *device)
return
CAPTURE_MJPEG_720P24
;
}
if
(
mjpg30_ok
)
{
my_zlog_info
(
"mpp_h264_source: %s 720p MJPEG@30fps ->
mppjpegdec
-> videorate@%dfps"
,
my_zlog_info
(
"mpp_h264_source: %s 720p MJPEG@30fps ->
libjpeg-turbo
-> videorate@%dfps"
,
device
,
WEBRTCPUSH_H264_FPS
);
return
CAPTURE_MJPEG_720P30
;
}
...
...
@@ -276,6 +330,194 @@ static void configure_post_enc_queue(GstElement *q)
NULL
);
}
static
void
object_set_uint_if_exists
(
GObject
*
obj
,
const
gchar
*
name
,
guint
value
)
{
if
(
obj
&&
g_object_class_find_property
(
G_OBJECT_GET_CLASS
(
obj
),
name
))
g_object_set
(
obj
,
name
,
value
,
NULL
);
}
static
void
object_set_int_if_exists
(
GObject
*
obj
,
const
gchar
*
name
,
gint
value
)
{
if
(
obj
&&
g_object_class_find_property
(
G_OBJECT_GET_CLASS
(
obj
),
name
))
g_object_set
(
obj
,
name
,
value
,
NULL
);
}
static
gboolean
ensure_byte_buffer
(
guint8
**
buf
,
size_t
*
capacity
,
size_t
need
)
{
if
(
!
buf
||
!
capacity
||
need
==
0
)
return
FALSE
;
if
(
*
capacity
>=
need
&&
*
buf
)
return
TRUE
;
*
buf
=
g_realloc
(
*
buf
,
need
);
*
capacity
=
need
;
return
*
buf
!=
NULL
;
}
static
void
configure_raw_appsrc
(
GstElement
*
appsrc
,
gint
fps
)
{
if
(
!
appsrc
)
return
;
g_object_set
(
appsrc
,
"is-live"
,
TRUE
,
"format"
,
GST_FORMAT_TIME
,
"block"
,
FALSE
,
"do-timestamp"
,
FALSE
,
NULL
);
object_set_uint_if_exists
(
G_OBJECT
(
appsrc
),
"max-buffers"
,
2
);
/* appsrc leaky-type: 2 = downstream/drop oldest on recent GStreamer. */
object_set_int_if_exists
(
G_OBJECT
(
appsrc
),
"leaky-type"
,
2
);
{
gchar
*
cap_str
=
g_strdup_printf
(
"video/x-raw,format=NV12,width=%d,height=%d,framerate=%d/1"
,
MPP_VIDEO_WIDTH
,
MPP_VIDEO_HEIGHT
,
fps
);
GstCaps
*
caps
=
gst_caps_from_string
(
cap_str
);
g_object_set
(
appsrc
,
"caps"
,
caps
,
NULL
);
gst_caps_unref
(
caps
);
g_free
(
cap_str
);
}
}
static
void
timing_store
(
MppH264Source
*
src
,
guint64
pts_ns
,
gint64
capture_us
,
gint64
push_us
,
guint
decode_us
,
guint
convert_us
)
{
FrameTiming
*
t
;
if
(
!
src
||
pts_ns
==
G_MAXUINT64
)
return
;
g_mutex_lock
(
&
src
->
timing_lock
);
t
=
&
src
->
timings
[
src
->
timing_next
++
%
TIMING_RING_SIZE
];
t
->
pts_ns
=
pts_ns
;
t
->
capture_us
=
capture_us
;
t
->
push_us
=
push_us
;
t
->
enc_in_us
=
0
;
t
->
decode_us
=
decode_us
;
t
->
convert_us
=
convert_us
;
t
->
valid
=
TRUE
;
g_mutex_unlock
(
&
src
->
timing_lock
);
}
static
void
timing_mark_encoder_in
(
MppH264Source
*
src
,
guint64
pts_ns
,
gint64
enc_in_us
)
{
guint
i
;
if
(
!
src
||
pts_ns
==
G_MAXUINT64
)
return
;
g_mutex_lock
(
&
src
->
timing_lock
);
for
(
i
=
0
;
i
<
TIMING_RING_SIZE
;
i
++
)
{
FrameTiming
*
t
=
&
src
->
timings
[
i
];
if
(
t
->
valid
&&
t
->
pts_ns
==
pts_ns
)
{
t
->
enc_in_us
=
enc_in_us
;
break
;
}
}
g_mutex_unlock
(
&
src
->
timing_lock
);
}
static
gboolean
timing_take
(
MppH264Source
*
src
,
guint64
pts_ns
,
FrameTiming
*
out
)
{
guint
i
;
gboolean
found
=
FALSE
;
if
(
!
src
||
!
out
||
pts_ns
==
G_MAXUINT64
)
return
FALSE
;
g_mutex_lock
(
&
src
->
timing_lock
);
for
(
i
=
0
;
i
<
TIMING_RING_SIZE
;
i
++
)
{
FrameTiming
*
t
=
&
src
->
timings
[
i
];
if
(
t
->
valid
&&
t
->
pts_ns
==
pts_ns
)
{
*
out
=
*
t
;
t
->
valid
=
FALSE
;
found
=
TRUE
;
break
;
}
}
g_mutex_unlock
(
&
src
->
timing_lock
);
return
found
;
}
static
void
perf_record
(
MppH264Source
*
src
,
const
FrameTiming
*
t
,
gint64
out_us
)
{
guint
encode_us
;
guint
total_us
;
if
(
!
src
||
!
t
)
return
;
encode_us
=
(
t
->
enc_in_us
>
0
&&
out_us
>
t
->
enc_in_us
)
?
(
guint
)(
out_us
-
t
->
enc_in_us
)
:
(
t
->
push_us
>
0
&&
out_us
>
t
->
push_us
)
?
(
guint
)(
out_us
-
t
->
push_us
)
:
0
;
total_us
=
(
t
->
capture_us
>
0
&&
out_us
>
t
->
capture_us
)
?
(
guint
)(
out_us
-
t
->
capture_us
)
:
0
;
src
->
perf_frames
++
;
src
->
perf_decode_sum_us
+=
t
->
decode_us
;
src
->
perf_convert_sum_us
+=
t
->
convert_us
;
src
->
perf_encode_sum_us
+=
encode_us
;
src
->
perf_total_sum_us
+=
total_us
;
if
(
t
->
decode_us
>
src
->
perf_decode_max_us
)
src
->
perf_decode_max_us
=
t
->
decode_us
;
if
(
t
->
convert_us
>
src
->
perf_convert_max_us
)
src
->
perf_convert_max_us
=
t
->
convert_us
;
if
(
encode_us
>
src
->
perf_encode_max_us
)
src
->
perf_encode_max_us
=
encode_us
;
if
(
total_us
>
src
->
perf_total_max_us
)
src
->
perf_total_max_us
=
total_us
;
if
(
src
->
perf_frames
>=
PERF_LOG_FRAMES
)
{
guint
n
=
src
->
perf_frames
;
my_zlog_info
(
"mpp_h264_source: perf avg/max jpeg_decode=%u/%u ms i420_nv12=%u/%u ms mpp_encode=%u/%u ms total=%u/%u ms"
,
(
guint
)(
src
->
perf_decode_sum_us
/
n
/
1000U
),
src
->
perf_decode_max_us
/
1000U
,
(
guint
)(
src
->
perf_convert_sum_us
/
n
/
1000U
),
src
->
perf_convert_max_us
/
1000U
,
(
guint
)(
src
->
perf_encode_sum_us
/
n
/
1000U
),
src
->
perf_encode_max_us
/
1000U
,
(
guint
)(
src
->
perf_total_sum_us
/
n
/
1000U
),
src
->
perf_total_max_us
/
1000U
);
src
->
perf_frames
=
0
;
src
->
perf_decode_sum_us
=
0
;
src
->
perf_convert_sum_us
=
0
;
src
->
perf_encode_sum_us
=
0
;
src
->
perf_total_sum_us
=
0
;
src
->
perf_decode_max_us
=
0
;
src
->
perf_convert_max_us
=
0
;
src
->
perf_encode_max_us
=
0
;
src
->
perf_total_max_us
=
0
;
}
}
static
void
note_slow_jpeg_decode
(
MppH264Source
*
src
,
guint
decode_us
)
{
gint64
now_us
;
if
(
!
src
||
decode_us
<=
MJPEG_DECODE_WARN_US
)
return
;
now_us
=
g_get_monotonic_time
();
src
->
jpeg_slow_count
++
;
src
->
jpeg_slow_last_us
=
decode_us
;
if
(
decode_us
>
src
->
jpeg_slow_max_us
)
src
->
jpeg_slow_max_us
=
decode_us
;
if
(
src
->
jpeg_slow_last_warn_us
>
0
&&
now_us
-
src
->
jpeg_slow_last_warn_us
<
MJPEG_DECODE_WARN_INTERVAL_US
)
return
;
my_zlog_warn
(
"mpp_h264_source: JPEG decode slow count=%u last=%u us max=%u us (>15ms, rate-limited 5s)"
,
src
->
jpeg_slow_count
,
src
->
jpeg_slow_last_us
,
src
->
jpeg_slow_max_us
);
src
->
jpeg_slow_count
=
0
;
src
->
jpeg_slow_last_us
=
0
;
src
->
jpeg_slow_max_us
=
0
;
src
->
jpeg_slow_last_warn_us
=
now_us
;
}
static
void
configure_v4l2_src
(
GstElement
*
src
,
gboolean
compressed_mjpeg
)
{
if
(
!
src
)
...
...
@@ -295,6 +537,316 @@ static void configure_v4l2_src(GstElement *src, gboolean compressed_mjpeg)
}
}
typedef
struct
{
struct
jpeg_error_mgr
pub
;
jmp_buf
jump
;
char
message
[
JMSG_LENGTH_MAX
];
}
MjpegErrorMgr
;
static
void
mjpeg_error_exit
(
j_common_ptr
cinfo
)
{
MjpegErrorMgr
*
err
=
(
MjpegErrorMgr
*
)
cinfo
->
err
;
(
*
cinfo
->
err
->
format_message
)(
cinfo
,
err
->
message
);
longjmp
(
err
->
jump
,
1
);
}
static
gboolean
decode_mjpeg_to_i420
(
MppH264Source
*
src
,
const
guint8
*
jpeg_data
,
size_t
jpeg_size
,
guint
*
decode_us_out
)
{
struct
jpeg_decompress_struct
cinfo
;
MjpegErrorMgr
jerr
;
gint64
start_us
;
gboolean
ok
=
FALSE
;
guint
width
;
guint
height
;
guint
uv_width
;
guint
uv_height
;
guint
i420_size
;
gint
h0
,
v0
,
h1
,
v1
,
h2
,
v2
;
gboolean
is_420
;
gboolean
is_422
;
if
(
decode_us_out
)
*
decode_us_out
=
0
;
if
(
!
src
||
!
jpeg_data
||
jpeg_size
==
0
)
return
FALSE
;
memset
(
&
cinfo
,
0
,
sizeof
(
cinfo
));
memset
(
&
jerr
,
0
,
sizeof
(
jerr
));
cinfo
.
err
=
jpeg_std_error
(
&
jerr
.
pub
);
jerr
.
pub
.
error_exit
=
mjpeg_error_exit
;
if
(
setjmp
(
jerr
.
jump
))
{
my_zlog_warn
(
"mpp_h264_source: libjpeg-turbo decode failed: %s"
,
jerr
.
message
[
0
]
?
jerr
.
message
:
"unknown"
);
jpeg_destroy_decompress
(
&
cinfo
);
return
FALSE
;
}
start_us
=
g_get_monotonic_time
();
jpeg_create_decompress
(
&
cinfo
);
jpeg_mem_src
(
&
cinfo
,
(
unsigned
char
*
)
jpeg_data
,
(
unsigned
long
)
jpeg_size
);
jpeg_read_header
(
&
cinfo
,
TRUE
);
width
=
cinfo
.
image_width
;
height
=
cinfo
.
image_height
;
if
(
width
!=
MPP_VIDEO_WIDTH
||
height
!=
MPP_VIDEO_HEIGHT
||
cinfo
.
num_components
!=
3
)
{
my_zlog_warn
(
"mpp_h264_source: unsupported MJPEG %ux%u components=%d"
,
width
,
height
,
cinfo
.
num_components
);
jpeg_destroy_decompress
(
&
cinfo
);
return
FALSE
;
}
h0
=
cinfo
.
comp_info
[
0
].
h_samp_factor
;
v0
=
cinfo
.
comp_info
[
0
].
v_samp_factor
;
h1
=
cinfo
.
comp_info
[
1
].
h_samp_factor
;
v1
=
cinfo
.
comp_info
[
1
].
v_samp_factor
;
h2
=
cinfo
.
comp_info
[
2
].
h_samp_factor
;
v2
=
cinfo
.
comp_info
[
2
].
v_samp_factor
;
is_420
=
h0
==
2
&&
v0
==
2
&&
h1
==
1
&&
v1
==
1
&&
h2
==
1
&&
v2
==
1
;
is_422
=
h0
==
2
&&
v0
==
1
&&
h1
==
1
&&
v1
==
1
&&
h2
==
1
&&
v2
==
1
;
if
(
!
is_420
&&
!
is_422
)
{
my_zlog_warn
(
"mpp_h264_source: unsupported MJPEG subsampling Y=%dx%d Cb=%dx%d Cr=%dx%d"
,
h0
,
v0
,
h1
,
v1
,
h2
,
v2
);
jpeg_destroy_decompress
(
&
cinfo
);
return
FALSE
;
}
uv_width
=
width
/
2
;
uv_height
=
height
/
2
;
i420_size
=
width
*
height
+
uv_width
*
uv_height
*
2
;
if
(
!
ensure_byte_buffer
(
&
src
->
i420_buf
,
&
src
->
i420_capacity
,
i420_size
))
{
jpeg_destroy_decompress
(
&
cinfo
);
return
FALSE
;
}
if
(
is_422
&&
(
!
ensure_byte_buffer
(
&
src
->
jpeg_tmp_u
,
&
src
->
jpeg_tmp_capacity
,
uv_width
*
DCTSIZE
)
||
!
ensure_byte_buffer
(
&
src
->
jpeg_tmp_v
,
&
src
->
jpeg_tmp_capacity
,
uv_width
*
DCTSIZE
)))
{
jpeg_destroy_decompress
(
&
cinfo
);
return
FALSE
;
}
cinfo
.
raw_data_out
=
TRUE
;
cinfo
.
out_color_space
=
JCS_YCbCr
;
cinfo
.
do_fancy_upsampling
=
FALSE
;
cinfo
.
dct_method
=
JDCT_IFAST
;
jpeg_start_decompress
(
&
cinfo
);
{
guint8
*
y_plane
=
src
->
i420_buf
;
guint8
*
u_plane
=
y_plane
+
width
*
height
;
guint8
*
v_plane
=
u_plane
+
uv_width
*
uv_height
;
const
guint
raw_rows
=
cinfo
.
max_v_samp_factor
*
DCTSIZE
;
JSAMPROW
y_rows
[
DCTSIZE
*
4
];
JSAMPROW
u_rows
[
DCTSIZE
*
4
];
JSAMPROW
v_rows
[
DCTSIZE
*
4
];
JSAMPARRAY
planes
[
3
]
=
{
y_rows
,
u_rows
,
v_rows
};
while
(
cinfo
.
output_scanline
<
cinfo
.
output_height
)
{
guint
base_y
=
cinfo
.
output_scanline
;
guint
i
;
JDIMENSION
got
;
for
(
i
=
0
;
i
<
raw_rows
;
i
++
)
{
guint
y
=
base_y
+
i
;
if
(
y
>=
height
)
y
=
height
-
1
;
y_rows
[
i
]
=
y_plane
+
y
*
width
;
}
if
(
is_420
)
{
guint
chroma_base
=
base_y
/
2
;
for
(
i
=
0
;
i
<
DCTSIZE
;
i
++
)
{
guint
cy
=
chroma_base
+
i
;
if
(
cy
>=
uv_height
)
cy
=
uv_height
-
1
;
u_rows
[
i
]
=
u_plane
+
cy
*
uv_width
;
v_rows
[
i
]
=
v_plane
+
cy
*
uv_width
;
}
}
else
{
for
(
i
=
0
;
i
<
DCTSIZE
;
i
++
)
{
u_rows
[
i
]
=
src
->
jpeg_tmp_u
+
i
*
uv_width
;
v_rows
[
i
]
=
src
->
jpeg_tmp_v
+
i
*
uv_width
;
}
}
got
=
jpeg_read_raw_data
(
&
cinfo
,
planes
,
raw_rows
);
if
(
got
==
0
)
break
;
if
(
is_422
)
{
guint
chroma_base
=
base_y
/
2
;
guint
pairs
=
got
/
2
;
for
(
i
=
0
;
i
<
pairs
&&
chroma_base
+
i
<
uv_height
;
i
++
)
{
guint
x
;
guint8
*
dst_u
=
u_plane
+
(
chroma_base
+
i
)
*
uv_width
;
guint8
*
dst_v
=
v_plane
+
(
chroma_base
+
i
)
*
uv_width
;
guint8
*
u0
=
src
->
jpeg_tmp_u
+
(
i
*
2
)
*
uv_width
;
guint8
*
u1
=
src
->
jpeg_tmp_u
+
(
i
*
2
+
1
)
*
uv_width
;
guint8
*
v0p
=
src
->
jpeg_tmp_v
+
(
i
*
2
)
*
uv_width
;
guint8
*
v1p
=
src
->
jpeg_tmp_v
+
(
i
*
2
+
1
)
*
uv_width
;
for
(
x
=
0
;
x
<
uv_width
;
x
++
)
{
dst_u
[
x
]
=
(
guint8
)(((
guint
)
u0
[
x
]
+
(
guint
)
u1
[
x
]
+
1U
)
>>
1
);
dst_v
[
x
]
=
(
guint8
)(((
guint
)
v0p
[
x
]
+
(
guint
)
v1p
[
x
]
+
1U
)
>>
1
);
}
}
}
}
}
jpeg_finish_decompress
(
&
cinfo
);
jpeg_destroy_decompress
(
&
cinfo
);
ok
=
TRUE
;
if
(
decode_us_out
)
*
decode_us_out
=
(
guint
)(
g_get_monotonic_time
()
-
start_us
);
return
ok
;
}
static
gboolean
i420_to_nv12
(
MppH264Source
*
src
,
guint
*
convert_us_out
)
{
const
guint
width
=
MPP_VIDEO_WIDTH
;
const
guint
height
=
MPP_VIDEO_HEIGHT
;
const
guint
y_size
=
width
*
height
;
const
guint
uv_width
=
width
/
2
;
const
guint
uv_height
=
height
/
2
;
const
guint
frame_size
=
y_size
+
y_size
/
2
;
const
guint8
*
u_plane
;
const
guint8
*
v_plane
;
guint8
*
uv_dst
;
gint64
start_us
;
guint
row
;
if
(
convert_us_out
)
*
convert_us_out
=
0
;
if
(
!
src
||
!
src
->
i420_buf
)
return
FALSE
;
if
(
!
ensure_byte_buffer
(
&
src
->
nv12_buf
,
&
src
->
nv12_capacity
,
frame_size
))
return
FALSE
;
start_us
=
g_get_monotonic_time
();
memcpy
(
src
->
nv12_buf
,
src
->
i420_buf
,
y_size
);
u_plane
=
src
->
i420_buf
+
y_size
;
v_plane
=
u_plane
+
uv_width
*
uv_height
;
uv_dst
=
src
->
nv12_buf
+
y_size
;
for
(
row
=
0
;
row
<
uv_height
;
row
++
)
{
guint
col
;
const
guint8
*
u
=
u_plane
+
row
*
uv_width
;
const
guint8
*
v
=
v_plane
+
row
*
uv_width
;
guint8
*
dst
=
uv_dst
+
row
*
width
;
for
(
col
=
0
;
col
<
uv_width
;
col
++
)
{
dst
[
col
*
2
]
=
u
[
col
];
dst
[
col
*
2
+
1
]
=
v
[
col
];
}
}
if
(
convert_us_out
)
*
convert_us_out
=
(
guint
)(
g_get_monotonic_time
()
-
start_us
);
return
TRUE
;
}
static
gpointer
mjpeg_decode_thread
(
gpointer
user_data
)
{
MppH264Source
*
src
=
user_data
;
guint64
fallback_pts
=
0
;
const
GstClockTime
frame_duration
=
gst_util_uint64_scale_int
(
1
,
GST_SECOND
,
WEBRTCPUSH_H264_FPS
);
while
(
src
&&
!
src
->
stop_decode
)
{
GstSample
*
sample
;
GstBuffer
*
inbuf
;
GstMapInfo
inmap
;
GstBuffer
*
outbuf
;
GstMapInfo
outmap
;
GstFlowReturn
flow
;
GstClockTime
pts
;
GstClockTime
duration
;
gint64
capture_us
;
gint64
push_us
;
guint
decode_us
=
0
;
guint
convert_us
=
0
;
const
guint
nv12_size
=
MPP_VIDEO_WIDTH
*
MPP_VIDEO_HEIGHT
*
3
/
2
;
if
(
!
src
->
mjpeg_sink
||
!
src
->
raw_appsrc
)
{
g_usleep
(
10000
);
continue
;
}
sample
=
gst_app_sink_try_pull_sample
(
GST_APP_SINK
(
src
->
mjpeg_sink
),
200
*
GST_MSECOND
);
if
(
!
sample
)
continue
;
inbuf
=
gst_sample_get_buffer
(
sample
);
if
(
!
inbuf
)
{
gst_sample_unref
(
sample
);
continue
;
}
pts
=
GST_BUFFER_PTS
(
inbuf
);
duration
=
GST_BUFFER_DURATION
(
inbuf
);
if
(
!
GST_CLOCK_TIME_IS_VALID
(
pts
))
{
pts
=
fallback_pts
;
fallback_pts
+=
frame_duration
;
}
if
(
!
GST_CLOCK_TIME_IS_VALID
(
duration
))
duration
=
frame_duration
;
else
duration
=
frame_duration
;
capture_us
=
g_get_monotonic_time
();
if
(
!
gst_buffer_map
(
inbuf
,
&
inmap
,
GST_MAP_READ
))
{
gst_sample_unref
(
sample
);
continue
;
}
if
(
!
decode_mjpeg_to_i420
(
src
,
inmap
.
data
,
inmap
.
size
,
&
decode_us
))
{
gst_buffer_unmap
(
inbuf
,
&
inmap
);
gst_sample_unref
(
sample
);
continue
;
}
gst_buffer_unmap
(
inbuf
,
&
inmap
);
gst_sample_unref
(
sample
);
note_slow_jpeg_decode
(
src
,
decode_us
);
if
(
!
i420_to_nv12
(
src
,
&
convert_us
))
continue
;
outbuf
=
gst_buffer_new_allocate
(
NULL
,
nv12_size
,
NULL
);
if
(
!
outbuf
)
continue
;
if
(
!
gst_buffer_map
(
outbuf
,
&
outmap
,
GST_MAP_WRITE
))
{
gst_buffer_unref
(
outbuf
);
continue
;
}
memcpy
(
outmap
.
data
,
src
->
nv12_buf
,
nv12_size
);
gst_buffer_unmap
(
outbuf
,
&
outmap
);
GST_BUFFER_PTS
(
outbuf
)
=
pts
;
GST_BUFFER_DTS
(
outbuf
)
=
GST_CLOCK_TIME_NONE
;
GST_BUFFER_DURATION
(
outbuf
)
=
duration
;
push_us
=
g_get_monotonic_time
();
timing_store
(
src
,
(
guint64
)
pts
,
capture_us
,
push_us
,
decode_us
,
convert_us
);
flow
=
gst_app_src_push_buffer
(
GST_APP_SRC
(
src
->
raw_appsrc
),
outbuf
);
if
(
flow
!=
GST_FLOW_OK
&&
!
src
->
stop_decode
)
my_zlog_warn
(
"mpp_h264_source: appsrc push failed flow=%d"
,
flow
);
}
if
(
src
&&
src
->
raw_appsrc
)
gst_app_src_end_of_stream
(
GST_APP_SRC
(
src
->
raw_appsrc
));
return
NULL
;
}
static
GstElement
*
make_h264_encoder
(
gboolean
prefer_mpp
,
gboolean
use_test
,
gboolean
*
use_mpp_out
)
{
...
...
@@ -383,11 +935,20 @@ static GstPadProbeReturn enc_sink_idr_probe(GstPad *pad, GstPadProbeInfo *info,
gpointer
user_data
)
{
MppH264Source
*
src
=
user_data
;
GstBuffer
*
buffer
;
(
void
)
pad
;
if
(
!
(
GST_PAD_PROBE_INFO_TYPE
(
info
)
&
GST_PAD_PROBE_TYPE_BUFFER
))
return
GST_PAD_PROBE_OK
;
if
(
!
src
||
!
src
->
pending_idr
)
if
(
!
src
)
return
GST_PAD_PROBE_OK
;
buffer
=
GST_PAD_PROBE_INFO_BUFFER
(
info
);
if
(
buffer
)
timing_mark_encoder_in
(
src
,
(
guint64
)
GST_BUFFER_PTS
(
buffer
),
g_get_monotonic_time
());
if
(
!
src
->
pending_idr
)
return
GST_PAD_PROBE_OK
;
{
...
...
@@ -564,18 +1125,255 @@ static gboolean link_pipeline(GstElement *pipe, GstElement **elements, guint cou
return
TRUE
;
}
static
gboolean
start_manual_mjpeg_pipeline
(
MppH264Source
*
src
,
const
char
*
vdev
,
CaptureMode
capture_mode
,
char
**
error_message
)
{
GstElement
*
cap_pipe
=
NULL
;
GstElement
*
enc_pipe
=
NULL
;
GstElement
*
vsrc
=
NULL
;
GstElement
*
caps_in
=
NULL
;
GstElement
*
jpegparse
=
NULL
;
GstElement
*
mjpeg_sink
=
NULL
;
GstElement
*
rawsrc
=
NULL
;
GstElement
*
vrate
=
NULL
;
GstElement
*
caps_nv12
=
NULL
;
GstElement
*
q
=
NULL
;
GstElement
*
eq
=
NULL
;
GstElement
*
enc
=
NULL
;
GstElement
*
parse
=
NULL
;
GstElement
*
caps_h264
=
NULL
;
GstElement
*
sink
=
NULL
;
gboolean
use_mpp
=
FALSE
;
gint
mjpeg_fps
=
(
capture_mode
==
CAPTURE_MJPEG_720P24
)
?
WEBRTCPUSH_H264_FPS
:
30
;
GstStateChangeReturn
ret
;
cap_pipe
=
gst_pipeline_new
(
"mjpeg-capture-pipe"
);
enc_pipe
=
gst_pipeline_new
(
"mpp-h264-pipe"
);
vsrc
=
gst_element_factory_make
(
"v4l2src"
,
"vsrc"
);
caps_in
=
gst_element_factory_make
(
"capsfilter"
,
"caps_in"
);
jpegparse
=
gst_element_factory_make
(
"jpegparse"
,
"jpegparse"
);
mjpeg_sink
=
gst_element_factory_make
(
"appsink"
,
"mjpeg_sink"
);
rawsrc
=
gst_element_factory_make
(
"appsrc"
,
"rawsrc"
);
vrate
=
gst_element_factory_make
(
"videorate"
,
"vrate"
);
caps_nv12
=
gst_element_factory_make
(
"capsfilter"
,
"caps_nv12"
);
q
=
gst_element_factory_make
(
"queue"
,
"vq"
);
eq
=
gst_element_factory_make
(
"queue"
,
"eq"
);
enc
=
make_h264_encoder
(
TRUE
,
FALSE
,
&
use_mpp
);
parse
=
gst_element_factory_make
(
"h264parse"
,
"parse"
);
caps_h264
=
gst_element_factory_make
(
"capsfilter"
,
"caps_h264"
);
sink
=
gst_element_factory_make
(
"appsink"
,
"sink"
);
if
(
!
cap_pipe
||
!
enc_pipe
||
!
vsrc
||
!
caps_in
||
!
jpegparse
||
!
mjpeg_sink
||
!
rawsrc
||
!
vrate
||
!
caps_nv12
||
!
q
||
!
eq
||
!
enc
||
!
parse
||
!
caps_h264
||
!
sink
)
{
if
(
error_message
)
*
error_message
=
g_strdup
(
"manual MJPEG element create failed"
);
goto
fail
;
}
g_object_set
(
vsrc
,
"device"
,
vdev
,
NULL
);
configure_v4l2_src
(
vsrc
,
TRUE
);
{
gchar
*
cap_str
=
g_strdup_printf
(
"image/jpeg,width=%d,height=%d,framerate=%d/1"
,
MPP_VIDEO_WIDTH
,
MPP_VIDEO_HEIGHT
,
mjpeg_fps
);
GstCaps
*
caps
=
gst_caps_from_string
(
cap_str
);
g_object_set
(
caps_in
,
"caps"
,
caps
,
NULL
);
gst_caps_unref
(
caps
);
g_free
(
cap_str
);
}
g_object_set
(
mjpeg_sink
,
"emit-signals"
,
FALSE
,
"sync"
,
FALSE
,
"max-buffers"
,
2
,
"drop"
,
TRUE
,
NULL
);
configure_raw_appsrc
(
rawsrc
,
mjpeg_fps
);
g_object_set
(
vrate
,
"max-rate"
,
WEBRTCPUSH_H264_FPS
,
"drop-only"
,
TRUE
,
NULL
);
configure_pre_enc_queue
(
q
);
configure_post_enc_queue
(
eq
);
g_object_set
(
parse
,
"config-interval"
,
-
1
,
NULL
);
{
gchar
*
cap_str
=
g_strdup_printf
(
"video/x-raw,format=NV12,width=%d,height=%d,framerate=%d/1"
,
MPP_VIDEO_WIDTH
,
MPP_VIDEO_HEIGHT
,
WEBRTCPUSH_H264_FPS
);
GstCaps
*
caps
=
gst_caps_from_string
(
cap_str
);
g_object_set
(
caps_nv12
,
"caps"
,
caps
,
NULL
);
gst_caps_unref
(
caps
);
g_free
(
cap_str
);
}
{
GstCaps
*
hc
=
gst_caps_from_string
(
"video/x-h264,stream-format=byte-stream,alignment=au"
);
g_object_set
(
caps_h264
,
"caps"
,
hc
,
NULL
);
gst_caps_unref
(
hc
);
}
g_object_set
(
sink
,
"emit-signals"
,
FALSE
,
"sync"
,
FALSE
,
"max-buffers"
,
WEBRTCPUSH_APPSINK_MAX_BUFFERS
,
"drop"
,
TRUE
,
NULL
);
{
GstElement
*
cap_elems
[]
=
{
vsrc
,
caps_in
,
jpegparse
,
mjpeg_sink
};
GstElement
*
enc_elems
[]
=
{
rawsrc
,
vrate
,
caps_nv12
,
q
,
enc
,
parse
,
caps_h264
,
eq
,
sink
};
if
(
!
link_pipeline
(
cap_pipe
,
cap_elems
,
G_N_ELEMENTS
(
cap_elems
)))
{
if
(
error_message
)
*
error_message
=
g_strdup
(
"failed to link MJPEG capture chain"
);
goto
fail
;
}
if
(
!
link_pipeline
(
enc_pipe
,
enc_elems
,
G_N_ELEMENTS
(
enc_elems
)))
{
if
(
error_message
)
*
error_message
=
g_strdup
(
"failed to link appsrc H264 encode chain"
);
goto
fail
;
}
}
src
->
manual_mjpeg
=
TRUE
;
src
->
capture_pipeline
=
cap_pipe
;
src
->
pipeline
=
enc_pipe
;
src
->
vsrc
=
vsrc
;
src
->
mjpeg_sink
=
mjpeg_sink
;
src
->
raw_appsrc
=
rawsrc
;
src
->
enc
=
enc
;
src
->
parse
=
parse
;
src
->
eq
=
eq
;
src
->
appsink
=
sink
;
src
->
use_mpp
=
use_mpp
;
{
GstPad
*
enc_sink
=
gst_element_get_static_pad
(
enc
,
"sink"
);
if
(
enc_sink
)
{
src
->
enc_sink_probe_id
=
gst_pad_add_probe
(
enc_sink
,
GST_PAD_PROBE_TYPE_BUFFER
,
enc_sink_idr_probe
,
src
,
NULL
);
gst_object_unref
(
enc_sink
);
}
}
{
GstBus
*
bus
=
gst_element_get_bus
(
enc_pipe
);
if
(
bus
)
{
src
->
bus_watch_id
=
gst_bus_add_watch
(
bus
,
bus_watch_cb
,
src
);
gst_object_unref
(
bus
);
}
}
{
GstBus
*
bus
=
gst_element_get_bus
(
cap_pipe
);
if
(
bus
)
{
src
->
capture_bus_watch_id
=
gst_bus_add_watch
(
bus
,
bus_watch_cb
,
src
);
gst_object_unref
(
bus
);
}
}
gst_pipeline_set_latency
(
GST_PIPELINE
(
enc_pipe
),
0
);
gst_pipeline_set_latency
(
GST_PIPELINE
(
cap_pipe
),
0
);
ret
=
gst_element_set_state
(
enc_pipe
,
GST_STATE_PLAYING
);
if
(
ret
==
GST_STATE_CHANGE_FAILURE
)
{
if
(
error_message
)
*
error_message
=
g_strdup
(
"manual encoder pipeline failed to PLAYING"
);
goto
fail_started
;
}
ret
=
gst_element_set_state
(
cap_pipe
,
GST_STATE_PLAYING
);
if
(
ret
==
GST_STATE_CHANGE_FAILURE
)
{
if
(
error_message
)
*
error_message
=
g_strdup
(
"manual capture pipeline failed to PLAYING"
);
goto
fail_started
;
}
src
->
decode_thread
=
g_thread_new
(
"mjpeg-decode"
,
mjpeg_decode_thread
,
src
);
{
const
guint8
*
warm_data
;
size_t
warm_size
;
gboolean
warm_idr
;
if
(
!
pull_sample
(
src
,
FALSE
,
8000
,
&
warm_data
,
&
warm_size
,
&
warm_idr
,
NULL
))
{
if
(
error_message
)
*
error_message
=
g_strdup
(
"manual encoder warmup timeout (no frame in 8s)"
);
my_zlog_error
(
"mpp_h264_source: manual MJPEG warmup failed"
);
goto
fail_started
;
}
my_zlog_info
(
"mpp_h264_source: warmup OK first_au=%zu bytes idr=%d"
,
warm_size
,
warm_idr
?
1
:
0
);
}
my_zlog_info
(
"mpp_h264_source: 720p MJPEG@%dfps -> libjpeg-turbo(I420) -> NV12@%dfps -> mpph264enc"
,
mjpeg_fps
,
WEBRTCPUSH_H264_FPS
);
my_zlog_info
(
"mpp_h264_source: live encoder started 720p (mpp=%d pre_buf=%d post_buf=%d)"
,
use_mpp
?
1
:
0
,
WEBRTCPUSH_MPP_PRE_ENC_BUFFERS
,
WEBRTCPUSH_MPP_POST_ENC_BUFFERS
);
return
TRUE
;
fail_started:
if
(
src
)
{
src
->
stop_decode
=
TRUE
;
if
(
src
->
raw_appsrc
)
gst_app_src_end_of_stream
(
GST_APP_SRC
(
src
->
raw_appsrc
));
if
(
src
->
capture_pipeline
)
gst_element_set_state
(
src
->
capture_pipeline
,
GST_STATE_NULL
);
if
(
src
->
pipeline
)
gst_element_set_state
(
src
->
pipeline
,
GST_STATE_NULL
);
if
(
src
->
decode_thread
)
{
g_thread_join
(
src
->
decode_thread
);
src
->
decode_thread
=
NULL
;
}
if
(
src
->
bus_watch_id
)
{
g_source_remove
(
src
->
bus_watch_id
);
src
->
bus_watch_id
=
0
;
}
if
(
src
->
capture_bus_watch_id
)
{
g_source_remove
(
src
->
capture_bus_watch_id
);
src
->
capture_bus_watch_id
=
0
;
}
if
(
src
->
capture_pipeline
)
{
gst_object_unref
(
src
->
capture_pipeline
);
src
->
capture_pipeline
=
NULL
;
}
if
(
src
->
pipeline
)
{
gst_object_unref
(
src
->
pipeline
);
src
->
pipeline
=
NULL
;
}
src
->
vsrc
=
NULL
;
src
->
mjpeg_sink
=
NULL
;
src
->
raw_appsrc
=
NULL
;
src
->
enc
=
NULL
;
src
->
parse
=
NULL
;
src
->
eq
=
NULL
;
src
->
appsink
=
NULL
;
}
return
FALSE
;
fail:
if
(
cap_pipe
)
{
gst_element_set_state
(
cap_pipe
,
GST_STATE_NULL
);
gst_object_unref
(
cap_pipe
);
}
if
(
enc_pipe
)
{
gst_element_set_state
(
enc_pipe
,
GST_STATE_NULL
);
gst_object_unref
(
enc_pipe
);
}
return
FALSE
;
}
MppH264Source
*
mpp_h264_source_start
(
const
char
*
video_device
,
char
**
error_message
)
{
MppH264Source
*
src
;
GstElement
*
pipe
,
*
vsrc
,
*
caps_in
=
NULL
,
*
jpegparse
=
NULL
,
*
jpegdec
=
NULL
;
GstElement
*
pipe
,
*
vsrc
,
*
caps_in
=
NULL
;
GstElement
*
conv
=
NULL
,
*
vrate
,
*
caps_nv12
,
*
enc
,
*
parse
,
*
q
,
*
eq
,
*
caps_h264
,
*
sink
;
gchar
*
auto_v4l
=
NULL
;
const
gchar
*
vdev
=
video_device
;
gboolean
use_test
=
FALSE
;
gboolean
use_auto
=
FALSE
;
gboolean
use_mpp
=
FALSE
;
gboolean
use_mpp_jpeg
=
FALSE
;
gint
mjpeg_fps
=
WEBRTCPUSH_H264_FPS
;
CaptureMode
capture_mode
=
CAPTURE_NONE
;
GstStateChangeReturn
ret
;
...
...
@@ -583,6 +1381,7 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
gst_init
(
NULL
,
NULL
);
src
=
g_new0
(
MppH264Source
,
1
);
g_mutex_init
(
&
src
->
timing_lock
);
use_test
=
(
vdev
&&
g_strcmp0
(
vdev
,
"test"
)
==
0
);
use_auto
=
(
!
vdev
||
g_strcmp0
(
vdev
,
"auto"
)
==
0
);
...
...
@@ -609,6 +1408,16 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
return
NULL
;
}
if
(
!
use_test
&&
capture_mode
!=
CAPTURE_RAW_NV12_720P
)
{
if
(
start_manual_mjpeg_pipeline
(
src
,
vdev
,
capture_mode
,
error_message
))
{
g_free
(
auto_v4l
);
return
src
;
}
g_free
(
auto_v4l
);
g_free
(
src
);
return
NULL
;
}
pipe
=
gst_pipeline_new
(
"mpp-h264-pipe"
);
vrate
=
gst_element_factory_make
(
"videorate"
,
"vrate"
);
caps_nv12
=
gst_element_factory_make
(
"capsfilter"
,
"caps_nv12"
);
...
...
@@ -638,62 +1447,6 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
g_free
(
cap_str
);
}
my_zlog_info
(
"mpp_h264_source: 720p NV12 direct capture (no JPEG decode)"
);
}
else
{
vsrc
=
gst_element_factory_make
(
"v4l2src"
,
"vsrc"
);
caps_in
=
gst_element_factory_make
(
"capsfilter"
,
"caps_in"
);
jpegparse
=
gst_element_factory_make
(
"jpegparse"
,
"jpegparse"
);
if
(
WEBRTCPUSH_MJPEG_CPU_DECODE
)
{
jpegdec
=
gst_element_factory_make
(
"jpegdec"
,
"jpegdec"
);
use_mpp_jpeg
=
FALSE
;
if
(
jpegdec
)
{
conv
=
gst_element_factory_make
(
"videoconvert"
,
"jpegconv"
);
g_object_set
(
jpegdec
,
"idct-method"
,
1
,
/* ifast */
"discard-corrupted-frames"
,
TRUE
,
"qos"
,
TRUE
,
NULL
);
}
else
{
jpegdec
=
gst_element_factory_make
(
"mppjpegdec"
,
"jpegdec"
);
if
(
jpegdec
)
{
use_mpp_jpeg
=
TRUE
;
g_object_set
(
jpegdec
,
"format"
,
23
,
"fast-mode"
,
TRUE
,
NULL
);
}
}
}
else
{
jpegdec
=
gst_element_factory_make
(
"mppjpegdec"
,
"jpegdec"
);
if
(
jpegdec
)
{
use_mpp_jpeg
=
TRUE
;
g_object_set
(
jpegdec
,
"format"
,
23
,
"fast-mode"
,
TRUE
,
NULL
);
}
else
{
jpegdec
=
gst_element_factory_make
(
"jpegdec"
,
"jpegdec"
);
use_mpp_jpeg
=
FALSE
;
if
(
jpegdec
)
{
conv
=
gst_element_factory_make
(
"videoconvert"
,
"jpegconv"
);
g_object_set
(
jpegdec
,
"idct-method"
,
1
,
"discard-corrupted-frames"
,
TRUE
,
"qos"
,
TRUE
,
NULL
);
}
}
}
mjpeg_fps
=
(
capture_mode
==
CAPTURE_MJPEG_720P24
)
?
WEBRTCPUSH_H264_FPS
:
30
;
g_object_set
(
vsrc
,
"device"
,
vdev
,
NULL
);
configure_v4l2_src
(
vsrc
,
TRUE
);
{
gchar
*
cap_str
=
g_strdup_printf
(
"image/jpeg,width=%d,height=%d,framerate=%d/1"
,
MPP_VIDEO_WIDTH
,
MPP_VIDEO_HEIGHT
,
mjpeg_fps
);
GstCaps
*
caps
=
gst_caps_from_string
(
cap_str
);
g_object_set
(
caps_in
,
"caps"
,
caps
,
NULL
);
gst_caps_unref
(
caps
);
g_free
(
cap_str
);
}
my_zlog_info
(
"mpp_h264_source: 720p MJPEG@%dfps -> %s%s -> NV12@%dfps"
,
mjpeg_fps
,
use_mpp_jpeg
?
"mppjpegdec"
:
"jpegdec"
,
use_mpp_jpeg
?
""
:
"+videoconvert"
,
WEBRTCPUSH_H264_FPS
);
}
if
(
!
pipe
||
!
vsrc
||
!
vrate
||
!
caps_nv12
||
!
q
||
!
eq
||
!
enc
||
!
parse
||
...
...
@@ -756,27 +1509,6 @@ MppH264Source *mpp_h264_source_start(const char *video_device, char **error_mess
g_free
(
src
);
return
NULL
;
}
}
else
{
gboolean
linked
;
if
(
use_mpp_jpeg
)
{
GstElement
*
elems
[]
=
{
vsrc
,
caps_in
,
jpegparse
,
jpegdec
,
vrate
,
caps_nv12
,
q
,
enc
,
parse
,
caps_h264
,
eq
,
sink
};
linked
=
jpegparse
&&
jpegdec
&&
link_pipeline
(
pipe
,
elems
,
G_N_ELEMENTS
(
elems
));
}
else
{
GstElement
*
elems
[]
=
{
vsrc
,
caps_in
,
jpegparse
,
jpegdec
,
conv
,
vrate
,
caps_nv12
,
q
,
enc
,
parse
,
caps_h264
,
eq
,
sink
};
linked
=
jpegparse
&&
jpegdec
&&
conv
&&
link_pipeline
(
pipe
,
elems
,
G_N_ELEMENTS
(
elems
));
}
if
(
!
linked
)
{
if
(
error_message
)
*
error_message
=
g_strdup
(
"failed to link MJPEG 720p chain"
);
gst_object_unref
(
pipe
);
g_free
(
auto_v4l
);
g_free
(
src
);
return
NULL
;
}
}
src
->
vsrc
=
vsrc
;
...
...
@@ -878,6 +1610,11 @@ static gboolean pull_sample(MppH264Source *src, gboolean require_idr,
}
pts
=
GST_BUFFER_PTS
(
buffer
);
if
(
src
->
manual_mjpeg
&&
GST_CLOCK_TIME_IS_VALID
(
pts
))
{
FrameTiming
timing
;
if
(
timing_take
(
src
,
(
guint64
)
pts
,
&
timing
))
perf_record
(
src
,
&
timing
,
g_get_monotonic_time
());
}
idr
=
buffer_is_idr
(
src
->
frame_buf
,
src
->
frame_size
);
if
(
idr
&&
src
->
pending_idr
)
{
src
->
pending_idr
=
FALSE
;
...
...
@@ -914,6 +1651,9 @@ gboolean mpp_h264_source_pull(MppH264Source *src, gboolean force_idr,
if
(
!
src
||
!
src
->
appsink
||
!
data
||
!
size
)
return
FALSE
;
/* 编码帧之间应用动态码率,避免从 REMB/RTP 回调线程直接重配编码器。 */
apply_pending_bitrate
(
src
);
/* bus 报 ERROR 后 pipeline 卡死,这里及时恢复避免长期黑屏 */
if
(
src
->
pending_recover
&&
mpp_h264_source_recover
(
src
))
{
src
->
pending_idr
=
TRUE
;
...
...
@@ -942,7 +1682,7 @@ gboolean mpp_h264_source_pull(MppH264Source *src, gboolean force_idr,
return
pull_sample
(
src
,
FALSE
,
timeout_ms
,
data
,
size
,
is_idr
,
pts_ns
);
}
void
mpp_h264_source_set_bitrate
(
MppH264Source
*
src
,
guint
bitrate_bps
)
static
void
apply_bitrate_now
(
MppH264Source
*
src
,
guint
bitrate_bps
)
{
GstElementFactory
*
f
;
const
gchar
*
name
;
...
...
@@ -976,6 +1716,36 @@ void mpp_h264_source_set_bitrate(MppH264Source *src, guint bitrate_bps)
}
else
if
(
name
&&
g_str_has_prefix
(
name
,
"x264enc"
))
{
g_object_set
(
src
->
enc
,
"bitrate"
,
bitrate_bps
/
1000
,
NULL
);
}
src
->
applied_bitrate_bps
=
bitrate_bps
;
my_zlog_info
(
"mpp_h264_source: bitrate applied at frame boundary %u kbps"
,
bitrate_bps
/
1000
);
}
static
void
apply_pending_bitrate
(
MppH264Source
*
src
)
{
gint
pending
;
if
(
!
src
)
return
;
pending
=
g_atomic_int_get
(
&
src
->
pending_bitrate_bps
);
if
(
pending
<=
0
||
!
g_atomic_int_compare_and_exchange
(
&
src
->
pending_bitrate_bps
,
pending
,
0
))
return
;
if
((
guint
)
pending
==
src
->
applied_bitrate_bps
)
return
;
apply_bitrate_now
(
src
,
(
guint
)
pending
);
}
void
mpp_h264_source_set_bitrate
(
MppH264Source
*
src
,
guint
bitrate_bps
)
{
if
(
!
src
||
!
bitrate_bps
)
return
;
if
(
bitrate_bps
<
MPP_VIDEO_MIN_BPS
)
bitrate_bps
=
MPP_VIDEO_MIN_BPS
;
if
(
bitrate_bps
>
MPP_VIDEO_MAX_BPS
)
bitrate_bps
=
MPP_VIDEO_MAX_BPS
;
g_atomic_int_set
(
&
src
->
pending_bitrate_bps
,
(
gint
)
bitrate_bps
);
}
gboolean
mpp_h264_source_recover
(
MppH264Source
*
src
)
...
...
@@ -991,6 +1761,8 @@ gboolean mpp_h264_source_recover(MppH264Source *src)
my_zlog_warn
(
"mpp_h264_source: recovering pipeline (NULL -> PLAYING)"
);
src
->
pending_idr
=
FALSE
;
src
->
pending_recover
=
FALSE
;
if
(
src
->
capture_pipeline
)
gst_element_set_state
(
src
->
capture_pipeline
,
GST_STATE_NULL
);
gst_element_set_state
(
src
->
pipeline
,
GST_STATE_NULL
);
/* 重新探测摄像头设备(USB 重枚举后可能从 video0 变成 video1/2) */
if
(
src
->
vsrc
)
{
...
...
@@ -1013,6 +1785,13 @@ gboolean mpp_h264_source_recover(MppH264Source *src)
my_zlog_error
(
"mpp_h264_source: recover failed to reach PLAYING"
);
return
FALSE
;
}
if
(
src
->
capture_pipeline
)
{
ret
=
gst_element_set_state
(
src
->
capture_pipeline
,
GST_STATE_PLAYING
);
if
(
ret
==
GST_STATE_CHANGE_FAILURE
)
{
my_zlog_error
(
"mpp_h264_source: capture recover failed to reach PLAYING"
);
return
FALSE
;
}
}
if
(
!
pull_sample
(
src
,
FALSE
,
5000
,
&
data
,
&
size
,
&
idr
,
NULL
))
{
my_zlog_error
(
"mpp_h264_source: recover warmup failed"
);
return
FALSE
;
...
...
@@ -1036,12 +1815,31 @@ void mpp_h264_source_stop(MppH264Source *src)
if
(
!
src
)
return
;
src
->
stop_decode
=
TRUE
;
if
(
src
->
raw_appsrc
)
gst_app_src_end_of_stream
(
GST_APP_SRC
(
src
->
raw_appsrc
));
if
(
src
->
capture_pipeline
)
gst_element_set_state
(
src
->
capture_pipeline
,
GST_STATE_NULL
);
if
(
src
->
pipeline
)
gst_element_set_state
(
src
->
pipeline
,
GST_STATE_NULL
);
if
(
src
->
decode_thread
)
{
g_thread_join
(
src
->
decode_thread
);
src
->
decode_thread
=
NULL
;
}
if
(
src
->
bus_watch_id
)
{
g_source_remove
(
src
->
bus_watch_id
);
src
->
bus_watch_id
=
0
;
}
if
(
src
->
capture_bus_watch_id
)
{
g_source_remove
(
src
->
capture_bus_watch_id
);
src
->
capture_bus_watch_id
=
0
;
}
if
(
src
->
capture_pipeline
)
{
gst_object_unref
(
src
->
capture_pipeline
);
src
->
capture_pipeline
=
NULL
;
}
if
(
src
->
pipeline
)
{
gst_element_set_state
(
src
->
pipeline
,
GST_STATE_NULL
);
gst_object_unref
(
src
->
pipeline
);
src
->
pipeline
=
NULL
;
}
...
...
@@ -1049,7 +1847,14 @@ void mpp_h264_source_stop(MppH264Source *src)
src
->
parse
=
NULL
;
src
->
eq
=
NULL
;
src
->
appsink
=
NULL
;
src
->
mjpeg_sink
=
NULL
;
src
->
raw_appsrc
=
NULL
;
g_free
(
src
->
sprop_parameter_sets
);
g_free
(
src
->
frame_buf
);
g_free
(
src
->
i420_buf
);
g_free
(
src
->
nv12_buf
);
g_free
(
src
->
jpeg_tmp_u
);
g_free
(
src
->
jpeg_tmp_v
);
g_mutex_clear
(
&
src
->
timing_lock
);
g_free
(
src
);
}
modules/webrtcpush/rtc_client.c
View file @
7200adca
...
...
@@ -10,12 +10,18 @@
#include <rtc/rtc.h>
#include <glib/gstdio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define WEBRTCPUSH_DC_AUDIO_MAX_BYTES (2U * 1024U * 1024U)
#define WEBRTCPUSH_DC_AUDIO_DIR "/tmp/webrtcpush_audio"
typedef
struct
{
gchar
*
candidate
;
gchar
*
mid
;
gchar
*
ufrag
;
}
PendingRemoteIce
;
typedef
struct
{
...
...
@@ -49,6 +55,13 @@ typedef struct {
}
RtcFrameQueue
;
typedef
struct
{
guint8
*
data
;
gsize
size
;
guint
seq
;
void
*
client_ptr
;
/* RtcClient*, 用void*避免前向声明问题 */
}
DcMp3PlayTask
;
typedef
struct
{
AppState
*
app
;
GMutex
lock
;
GMutex
send_lock
;
...
...
@@ -71,13 +84,18 @@ typedef struct {
guint64
last_source_pts
;
gboolean
source_pts_valid
;
guint
target_bitrate
;
guint
applied_mpp_bitrate
;
guint
remb_ceiling
;
guint
last_remb_bitrate
;
guint
remb_filtered
;
guint
remb_down_samples
;
gboolean
initial_probe_complete
;
gint64
probe_target_reached_us
;
gint64
last_bitrate_ramp_us
;
gint64
last_mpp_bitrate_apply_us
;
gint64
last_idr_request_us
;
gint64
last_stats_log_us
;
gint64
last_remb_sane_log_us
;
gchar
*
remote_ice_ufrag
;
gchar
*
video_mid
;
gint
video_mline_index
;
...
...
@@ -87,15 +105,24 @@ typedef struct {
AudioSink
*
audio_sink
;
VolumeControl
*
volume_ctrl
;
int
audio_track
;
int
audio_payload_type
;
gboolean
audio_track_open
;
gboolean
audio_send_enabled
;
gboolean
audio_recv_enabled
;
GThread
*
audio_send_thread
;
guint32
audio_rtp_timestamp
;
guint64
audio_pts_base
;
guint64
audio_last_pts
;
gboolean
audio_pts_valid
;
/* DataChannel: 前端 MP3 分片 + 右上角 Mbps */
int
data_channel
;
GByteArray
*
dc_audio_buf
;
guint
dc_audio_seq
;
/* 排队播放: 多片段连续播放不间隔 */
GAsyncQueue
*
dc_play_queue
;
/* DcMp3PlayTask队列 */
GThread
*
dc_play_thread
;
/* 常驻播放线程 */
volatile
gboolean
dc_play_quit
;
GQueue
*
pending_local_ice
;
GQueue
*
pending_remote_ice
;
}
RtcClient
;
...
...
@@ -251,6 +278,7 @@ static void free_pending_remote_ice(gpointer data)
return
;
g_free
(
ice
->
candidate
);
g_free
(
ice
->
mid
);
g_free
(
ice
->
ufrag
);
g_free
(
ice
);
}
...
...
@@ -527,6 +555,10 @@ static gboolean request_idr(RtcClient *client, gboolean bypass_throttle)
return
FALSE
;
now
=
g_get_monotonic_time
();
g_mutex_lock
(
&
client
->
lock
);
if
(
client
->
need_idr
)
{
g_mutex_unlock
(
&
client
->
lock
);
return
FALSE
;
}
if
(
!
bypass_throttle
&&
client
->
last_idr_request_us
>
0
&&
now
-
client
->
last_idr_request_us
<
(
gint64
)
WEBRTCPUSH_PLI_IDR_THROTTLE_MS
*
1000
)
{
...
...
@@ -582,29 +614,241 @@ static void RTC_API on_state_change(int pc, rtcState state, void *ptr)
}
}
/* 常驻播放线程: 从队列取任务连续播放, 队列空时释放ALSA锁 */
static
gpointer
dc_play_queue_thread
(
gpointer
data
)
{
RtcClient
*
client
=
data
;
gboolean
holds_lock
=
FALSE
;
int
idle_count
=
0
;
while
(
!
client
->
dc_play_quit
)
{
/* 等300ms看有没有新任务, 有就播, 连续3次空就释放锁 */
DcMp3PlayTask
*
task
=
g_async_queue_timeout_pop
(
client
->
dc_play_queue
,
300000
);
if
(
!
task
)
{
/* 队列空 */
if
(
holds_lock
)
{
idle_count
++
;
if
(
idle_count
>=
3
)
{
/* 约1s无新任务, 释放ALSA锁让本地音频能播 */
audio_sink_unlock_alsa
();
holds_lock
=
FALSE
;
idle_count
=
0
;
}
}
continue
;
}
idle_count
=
0
;
if
(
!
task
->
data
||
task
->
size
==
0
)
{
g_free
(
task
->
data
);
g_free
(
task
);
continue
;
}
/* 首次或有任务时获取锁, 播放期间不释放 */
if
(
!
holds_lock
)
{
RtcClient
*
rc
=
(
RtcClient
*
)
task
->
client_ptr
;
audio_sink_interrupt
(
rc
?
rc
->
audio_sink
:
NULL
);
audio_sink_lock_alsa
();
holds_lock
=
TRUE
;
}
/* 写文件 + playbin播放 */
gchar
*
path
=
g_strdup_printf
(
"%s/phone_%u.bin"
,
WEBRTCPUSH_DC_AUDIO_DIR
,
task
->
seq
);
GError
*
err
=
NULL
;
if
(
g_mkdir_with_parents
(
WEBRTCPUSH_DC_AUDIO_DIR
,
0700
)
!=
0
||
!
g_file_set_contents
(
path
,
(
const
gchar
*
)
task
->
data
,
(
gssize
)
task
->
size
,
&
err
))
{
my_zlog_warn
(
"libdatachannel: save phone audio failed: %s"
,
err
?
err
->
message
:
"?"
);
if
(
err
)
g_error_free
(
err
);
g_free
(
path
);
g_free
(
task
->
data
);
g_free
(
task
);
continue
;
}
gchar
*
quoted
=
g_shell_quote
(
path
);
gchar
*
cmd
=
g_strdup_printf
(
/*
* 修复两个问题:
* 1) sync=false 会让 playbin 在 EOS 时立即关闭 ALSA 设备, ring buffer
* 尾部尚未播完就被 snd_pcm_drop 丢弃, 表现为"只播放前面部分"。
* 改 sync=true 让 sink 按时钟节拍播放并在 EOS 时 drain 完整音频。
* 2) USB 声卡(plughw:2,0)播单声道会触发
* gst_audio_ring_buffer_set_channel_positions 的 "should not be reached"
* CRITICAL; 用 audioconvert 上混到 2 声道规避。
*/
"timeout 20s gst-launch-1.0 -q playbin uri=file://%s audio-sink=
\"
audioconvert ! audioresample ! audio/x-raw,channels=2 ! alsasink device=%s sync=true
\"
video-sink=fakesink"
,
quoted
,
WEBRTCPUSH_AUDIO_PLAYBACK_DEVICE
);
int
status
=
-
1
;
gchar
*
child_out
=
NULL
,
*
child_err
=
NULL
;
my_zlog_info
(
"libdatachannel: playing %u bytes (seq=%u)"
,
(
guint
)
task
->
size
,
task
->
seq
);
/* capture & discard child stdout/stderr: suppress rga_api version + gst progress noise */
if
(
!
g_spawn_command_line_sync
(
cmd
,
&
child_out
,
&
child_err
,
&
status
,
&
err
))
{
my_zlog_warn
(
"libdatachannel: play phone audio failed: %s"
,
err
?
err
->
message
:
"?"
);
if
(
err
)
g_error_free
(
err
);
}
else
if
(
status
==
0
)
{
my_zlog_info
(
"libdatachannel: phone audio played (%u bytes)"
,
(
guint
)
task
->
size
);
}
else
{
my_zlog_warn
(
"libdatachannel: phone audio playback exit=%d (%u bytes)"
,
status
,
(
guint
)
task
->
size
);
}
g_remove
(
path
);
g_free
(
child_out
);
g_free
(
child_err
);
g_free
(
cmd
);
g_free
(
quoted
);
g_free
(
path
);
g_free
(
task
->
data
);
g_free
(
task
);
}
/* 退出前释放锁 */
if
(
holds_lock
)
audio_sink_unlock_alsa
();
return
NULL
;
}
static
void
start_dc_mp3_playback
(
RtcClient
*
client
,
guint8
*
data
,
gsize
size
)
{
DcMp3PlayTask
*
task
;
/* 接收手机发来的 Opus RTP,depayload 后送入播放管道 */
static
void
RTC_API
on_audio_message
(
int
track
,
const
char
*
message
,
int
size
,
void
*
ptr
)
if
(
!
client
||
!
data
||
size
==
0
)
return
;
/* 首次播放时创建队列+线程 */
if
(
!
client
->
dc_play_queue
)
{
client
->
dc_play_queue
=
g_async_queue_new
();
client
->
dc_play_quit
=
FALSE
;
client
->
dc_play_thread
=
g_thread_new
(
"dc-play-q"
,
dc_play_queue_thread
,
client
);
}
task
=
g_new0
(
DcMp3PlayTask
,
1
);
task
->
data
=
data
;
task
->
size
=
size
;
task
->
seq
=
++
client
->
dc_audio_seq
;
task
->
client_ptr
=
client
;
g_async_queue_push
(
client
->
dc_play_queue
,
task
);
}
static
gboolean
dc_message_is_eof
(
const
char
*
message
,
int
size
)
{
if
(
!
message
)
return
FALSE
;
if
(
size
<
0
)
return
strcmp
(
message
,
"EOF"
)
==
0
;
return
size
==
3
&&
memcmp
(
message
,
"EOF"
,
3
)
==
0
;
}
static
void
RTC_API
on_data_channel_message
(
int
dc
,
const
char
*
message
,
int
size
,
void
*
ptr
)
{
RtcClient
*
client
=
ptr
;
const
guint8
*
rtp
=
(
const
guint8
*
)
message
;
const
guint8
*
payload
;
int
payload_len
,
header_len
;
guint8
*
play_data
=
NULL
;
gsize
play_size
=
0
;
(
void
)
track
;
if
(
!
client
||
!
client
->
audio_sink
||
!
client
->
audio_recv_enabled
||
!
message
||
size
<
12
)
(
void
)
dc
;
if
(
!
client
||
!
message
)
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
(
dc_message_is_eof
(
message
,
size
))
{
g_mutex_lock
(
&
client
->
lock
);
if
(
client
->
dc_audio_buf
&&
client
->
dc_audio_buf
->
len
>
0
)
{
play_size
=
client
->
dc_audio_buf
->
len
;
play_data
=
g_memdup2
(
client
->
dc_audio_buf
->
data
,
play_size
);
g_byte_array_set_size
(
client
->
dc_audio_buf
,
0
);
}
g_mutex_unlock
(
&
client
->
lock
);
if
(
play_data
)
{
my_zlog_info
(
"libdatachannel: DataChannel MP3 EOF, playback %u bytes"
,
(
guint
)
play_size
);
start_dc_mp3_playback
(
client
,
play_data
,
play_size
);
}
else
{
my_zlog_warn
(
"libdatachannel: DataChannel MP3 EOF with empty buffer"
);
}
return
;
}
if
(
size
<=
0
)
return
;
g_mutex_lock
(
&
client
->
lock
);
if
(
!
client
->
dc_audio_buf
)
client
->
dc_audio_buf
=
g_byte_array_new
();
if
(
client
->
dc_audio_buf
->
len
+
(
guint
)
size
>
WEBRTCPUSH_DC_AUDIO_MAX_BYTES
)
{
my_zlog_warn
(
"libdatachannel: DataChannel MP3 too large (%u + %d), drop buffer"
,
client
->
dc_audio_buf
->
len
,
size
);
g_byte_array_set_size
(
client
->
dc_audio_buf
,
0
);
}
else
{
g_byte_array_append
(
client
->
dc_audio_buf
,
(
const
guint8
*
)
message
,
(
guint
)
size
);
}
if
(
size
<=
header_len
)
g_mutex_unlock
(
&
client
->
lock
);
}
static
void
send_datachannel_mbps
(
RtcClient
*
client
,
guint
actual_kbps
)
{
int
dc
;
guint
target_bitrate
;
char
msg
[
32
];
double
mbps
;
if
(
!
client
)
return
;
g_mutex_lock
(
&
client
->
lock
);
dc
=
client
->
data_channel
;
target_bitrate
=
client
->
target_bitrate
;
g_mutex_unlock
(
&
client
->
lock
);
if
(
dc
<
0
||
!
rtcIsOpen
(
dc
))
return
;
mbps
=
actual_kbps
>
0
?
((
double
)
actual_kbps
/
1000
.
0
)
:
((
double
)
target_bitrate
/
1000000
.
0
);
snprintf
(
msg
,
sizeof
(
msg
),
"%.2f"
,
mbps
);
rtcSendMessage
(
dc
,
msg
,
-
1
);
}
static
void
RTC_API
on_data_channel_open
(
int
dc
,
void
*
ptr
)
{
RtcClient
*
client
=
ptr
;
char
label
[
64
]
=
{
0
};
rtcGetDataChannelLabel
(
dc
,
label
,
(
int
)
sizeof
(
label
));
my_zlog_info
(
"libdatachannel: DataChannel open label=%s"
,
label
[
0
]
?
label
:
"?"
);
send_datachannel_mbps
(
client
,
0
);
}
static
void
RTC_API
on_data_channel_closed
(
int
dc
,
void
*
ptr
)
{
RtcClient
*
client
=
ptr
;
if
(
!
client
)
return
;
g_mutex_lock
(
&
client
->
lock
);
if
(
client
->
data_channel
==
dc
)
client
->
data_channel
=
-
1
;
g_mutex_unlock
(
&
client
->
lock
);
my_zlog_info
(
"libdatachannel: DataChannel closed"
);
}
static
void
RTC_API
on_data_channel_error
(
int
dc
,
const
char
*
error
,
void
*
ptr
)
{
(
void
)
dc
;
(
void
)
ptr
;
my_zlog_warn
(
"libdatachannel: DataChannel error: %s"
,
error
?
error
:
"?"
);
}
static
void
setup_data_channel_callbacks
(
RtcClient
*
client
,
int
dc
)
{
if
(
!
client
||
dc
<
0
)
return
;
payload
=
rtp
+
header_len
;
payload_len
=
size
-
header_len
;
audio_sink_push_opus
(
client
->
audio_sink
,
payload
,
(
size_t
)
payload_len
);
rtcSetUserPointer
(
dc
,
client
);
rtcSetOpenCallback
(
dc
,
on_data_channel_open
);
rtcSetClosedCallback
(
dc
,
on_data_channel_closed
);
rtcSetErrorCallback
(
dc
,
on_data_channel_error
);
rtcSetMessageCallback
(
dc
,
on_data_channel_message
);
}
static
void
RTC_API
on_incoming_data_channel
(
int
pc
,
int
dc
,
void
*
ptr
)
{
RtcClient
*
client
=
ptr
;
char
label
[
64
]
=
{
0
};
(
void
)
pc
;
rtcGetDataChannelLabel
(
dc
,
label
,
(
int
)
sizeof
(
label
));
my_zlog_info
(
"libdatachannel: incoming DataChannel label=%s"
,
label
[
0
]
?
label
:
"?"
);
setup_data_channel_callbacks
(
client
,
dc
);
}
static
void
RTC_API
on_track_open
(
int
track
,
void
*
ptr
)
...
...
@@ -684,7 +928,7 @@ static void RTC_API on_pli(int track, void *ptr)
return
;
requested
=
request_idr
(
client
,
FALSE
);
if
(
!
requested
)
{
my_zlog_debug
(
"libdatachannel: PLI
throttled, skip duplicate IDR request
"
);
my_zlog_debug
(
"libdatachannel: PLI
ignored, IDR already pending or throttled
"
);
return
;
}
g_mutex_lock
(
&
client
->
lock
);
...
...
@@ -771,18 +1015,56 @@ static int clear_stale_pacing(RtcClient *client, int track,
return
dropped
;
}
static
guint
bitrate_delta
(
guint
a
,
guint
b
)
{
return
a
>
b
?
a
-
b
:
b
-
a
;
}
static
void
apply_bitrate
(
RtcClient
*
client
,
int
track
,
guint
bitrate
)
{
guint
b
;
guint
old_b
;
guint
old_mpp_b
;
guint
mpp_apply_b
=
0
;
guint
mpp_delta
;
gint64
now_us
;
gint64
last_mpp_apply_us
;
gboolean
apply_mpp
=
FALSE
;
if
(
!
client
)
return
;
b
=
clamp_bitrate
(
bitrate
);
now_us
=
g_get_monotonic_time
();
g_mutex_lock
(
&
client
->
lock
);
old_b
=
client
->
target_bitrate
;
old_mpp_b
=
client
->
applied_mpp_bitrate
;
last_mpp_apply_us
=
client
->
last_mpp_bitrate_apply_us
;
if
(
old_mpp_b
==
0
)
old_mpp_b
=
old_b
?
old_b
:
b
;
client
->
target_bitrate
=
b
;
mpp_delta
=
bitrate_delta
(
old_mpp_b
,
b
);
if
((
guint64
)
mpp_delta
*
100U
>=
(
guint64
)
old_mpp_b
*
WEBRTCPUSH_MPP_RECONFIG_MIN_DELTA_PERCENT
&&
now_us
-
last_mpp_apply_us
>=
(
gint64
)
WEBRTCPUSH_MPP_RECONFIG_MIN_INTERVAL_MS
*
1000
)
{
apply_mpp
=
TRUE
;
mpp_apply_b
=
b
;
if
(
b
>
old_mpp_b
)
{
guint
max_up
=
(
guint
)(((
guint64
)
old_mpp_b
*
(
100U
+
WEBRTCPUSH_MPP_RECONFIG_MAX_STEP_PERCENT
))
/
100U
);
if
(
mpp_apply_b
>
max_up
)
mpp_apply_b
=
max_up
;
}
else
{
guint
min_down
=
(
guint
)(((
guint64
)
old_mpp_b
*
(
100U
-
WEBRTCPUSH_MPP_RECONFIG_MAX_STEP_PERCENT
))
/
100U
);
if
(
mpp_apply_b
<
min_down
)
mpp_apply_b
=
min_down
;
}
mpp_apply_b
=
clamp_bitrate
(
mpp_apply_b
);
client
->
applied_mpp_bitrate
=
mpp_apply_b
;
client
->
last_mpp_bitrate_apply_us
=
now_us
;
}
g_mutex_unlock
(
&
client
->
lock
);
if
(
old_b
==
b
)
...
...
@@ -794,10 +1076,17 @@ static void apply_bitrate(RtcClient *client, int track, guint bitrate)
rtcSetPacingBitrate
(
track
,
pacing_bitrate_for_encoder
(
b
));
g_mutex_unlock
(
&
client
->
send_lock
);
}
mpp_h264_source_set_bitrate
(
client
->
h264_source
,
b
);
my_zlog_info
(
"libdatachannel: RTCP bitrate control -> MPP encoder=%u->%u kbps pacing=%u kbps"
,
old_b
/
1000
,
b
/
1000
,
if
(
apply_mpp
)
{
/* 这里只投递原子目标;真正的 mpph264enc 属性更新在拉帧边界执行。 */
mpp_h264_source_set_bitrate
(
client
->
h264_source
,
mpp_apply_b
);
my_zlog_info
(
"libdatachannel: bitrate control -> target=%u->%u kbps, MPP queued=%u->%u kbps pacing=%u kbps"
,
old_b
/
1000
,
b
/
1000
,
old_mpp_b
/
1000
,
mpp_apply_b
/
1000
,
pacing_bitrate_for_encoder
(
b
)
/
1000
);
}
else
{
my_zlog_info
(
"libdatachannel: bitrate control -> target=%u->%u kbps, MPP hold=%u kbps pacing=%u kbps"
,
old_b
/
1000
,
b
/
1000
,
old_mpp_b
/
1000
,
pacing_bitrate_for_encoder
(
b
)
/
1000
);
}
}
/* Public Internet: climb slowly so a transient REMB spike cannot build latency. */
...
...
@@ -824,11 +1113,11 @@ static void ramp_bitrate_toward_ceiling(RtcClient *client, int track)
return
;
/* confirmed decreases are applied by on_remb() */
diff
=
tgt
-
cur
;
step
=
(
guint
)(((
guint64
)
cur
*
12U
)
/
100U
);
if
(
step
<
50000U
)
step
=
50000U
;
if
(
step
>
250000U
)
step
=
250000U
;
step
=
(
guint
)(((
guint64
)
cur
*
WEBRTCPUSH_BITRATE_RAMP_UP_PERCENT
)
/
100U
);
if
(
step
<
WEBRTCPUSH_BITRATE_RAMP_UP_STEP_MIN_BPS
)
step
=
WEBRTCPUSH_BITRATE_RAMP_UP_STEP_MIN_BPS
;
if
(
step
>
WEBRTCPUSH_BITRATE_RAMP_UP_STEP_MAX_BPS
)
step
=
WEBRTCPUSH_BITRATE_RAMP_UP_STEP_MAX_BPS
;
if
(
step
>
diff
)
step
=
diff
;
next
=
cur
+
step
;
...
...
@@ -865,44 +1154,100 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
RtcClient
*
client
=
ptr
;
guint
current
;
guint
usable
;
guint
estimate_bitrate
;
guint
filtered
;
guint
ceiling
;
guint
next
=
0
;
guint
down_samples
=
0
;
guint
required_down_samples
=
WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS
;
gboolean
severe_down
=
FALSE
;
gboolean
can_step_down
=
FALSE
;
gboolean
have_pacing
;
gboolean
first_frame_sent
;
gboolean
recent_idr
=
FALSE
;
gboolean
remb_sane_floor
=
FALSE
;
gboolean
log_remb_sane_floor
=
FALSE
;
gboolean
probe_completed_now
=
FALSE
;
guint
pacing_packets
=
0
;
guint
pacing_bytes
=
0
;
guint
pacing_delay_ms
=
0
;
gint64
now_us
;
if
(
!
client
||
bitrate
==
0
)
return
;
now_us
=
g_get_monotonic_time
();
usable
=
clamp_bitrate
(
(
guint
)(((
guint64
)
bitrate
*
WEBRTCPUSH_REMB_UTIL_PERCENT
)
/
100U
)
);
have_pacing
=
pacing_queue_snapshot
(
client
,
track
,
&
pacing_packets
,
&
pacing_bytes
,
&
pacing_delay_ms
);
g_mutex_lock
(
&
client
->
lock
);
current
=
client
->
target_bitrate
;
first_frame_sent
=
client
->
first_frame_sent
;
recent_idr
=
client
->
last_idr_sent_us
>
0
&&
now_us
-
client
->
last_idr_sent_us
<
(
gint64
)
WEBRTCPUSH_REMB_IDR_GRACE_MS
*
1000
;
client
->
last_remb_bitrate
=
bitrate
;
can_step_down
=
now_us
-
client
->
last_bitrate_ramp_us
>=
(
gint64
)
WEBRTCPUSH_BITRATE_RAMP_DOWN_MS
*
1000
;
/*
* REMB 在低发送量时容易形成“低码率 -> 低估计 -> 永远无法探测”的闭环。
* 初次连接主动爬到健康探测目标并保持一段时间,让接收端用真实流量重新估计。
* 探测完成后仍保留“发送端受限”保护:pacing 健康时,低 REMB 只能阻止
* 继续上升,不能把发送量越压越低。只有持续积压且不在 IDR 窗口才允许下降。
*/
if
(
!
client
->
initial_probe_complete
)
{
if
(
current
+
WEBRTCPUSH_REMB_DOWN_MIN_STEP
>=
WEBRTCPUSH_HEALTHY_PROBE_TARGET_BPS
)
{
if
(
client
->
probe_target_reached_us
==
0
)
client
->
probe_target_reached_us
=
now_us
;
else
if
(
now_us
-
client
->
probe_target_reached_us
>=
(
gint64
)
WEBRTCPUSH_INITIAL_PROBE_HOLD_MS
*
1000
)
{
client
->
initial_probe_complete
=
TRUE
;
probe_completed_now
=
TRUE
;
}
}
}
estimate_bitrate
=
bitrate
;
if
(
first_frame_sent
&&
have_pacing
&&
bitrate
<
WEBRTCPUSH_REMB_SANE_RAW_MAX_BPS
&&
(
pacing_delay_ms
<=
WEBRTCPUSH_REMB_SANE_PACING_MAX_MS
||
recent_idr
))
{
estimate_bitrate
=
WEBRTCPUSH_REMB_SANE_FLOOR_BPS
;
remb_sane_floor
=
TRUE
;
if
(
client
->
last_remb_sane_log_us
==
0
||
now_us
-
client
->
last_remb_sane_log_us
>=
5000000
)
{
client
->
last_remb_sane_log_us
=
now_us
;
log_remb_sane_floor
=
TRUE
;
}
}
usable
=
clamp_bitrate
(
(
guint
)(((
guint64
)
estimate_bitrate
*
WEBRTCPUSH_REMB_UTIL_PERCENT
)
/
100U
));
if
(
client
->
remb_filtered
==
0
)
{
filtered
=
bitrate
;
}
else
if
(
bitrate
<
client
->
remb_filtered
)
{
filtered
=
estimate_
bitrate
;
}
else
if
(
estimate_
bitrate
<
client
->
remb_filtered
)
{
/* React to a real fall, but do not mirror every 200ms estimator sample. */
filtered
=
(
guint
)(((
guint64
)
client
->
remb_filtered
*
2U
+
bitrate
)
/
3U
);
filtered
=
(
guint
)(((
guint64
)
client
->
remb_filtered
*
2U
+
estimate_
bitrate
)
/
3U
);
}
else
{
/* Rising bandwidth
needs sustained evidence.
*/
filtered
=
(
guint
)(((
guint64
)
client
->
remb_filtered
*
7U
+
bitrate
)
/
8
U
);
/* Rising bandwidth
: 3:1 权重,跌落后能较快爬回(原 7:1 太慢)
*/
filtered
=
(
guint
)(((
guint64
)
client
->
remb_filtered
*
3U
+
estimate_bitrate
)
/
4
U
);
}
/*
* 浏览器/手机端的 REMB 有时会在 pacing 几乎为空时给出 500~900kbps
* 的低估值。这个场景不是网络堵塞,若继续拿旧 filtered 参与 ceiling,
* 会把 720p 长时间压回 800kbps。保护命中时把 filtered 也抬到 sane floor,
* 让发送端按“可疑但不堵”的网络慢慢爬升,而不是突然降清晰度。
*/
if
(
remb_sane_floor
&&
filtered
<
estimate_bitrate
)
filtered
=
estimate_bitrate
;
severe_down
=
((
guint64
)
usable
*
100U
)
<
((
guint64
)
current
*
WEBRTCPUSH_REMB_SEVERE_PERCENT
);
if
(
severe_down
)
{
/* Reset the filter too, otherwise its old high value immediately rebounds. */
filtered
=
bitrate
;
filtered
=
estimate_
bitrate
;
ceiling
=
usable
;
client
->
remb_down_samples
=
0
;
required_down_samples
=
WEBRTCPUSH_REMB_SEVERE_CONFIRMATIONS
;
}
else
{
ceiling
=
clamp_bitrate
(
(
guint
)(((
guint64
)
filtered
*
WEBRTCPUSH_REMB_UTIL_PERCENT
)
/
100U
));
...
...
@@ -911,14 +1256,13 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
client
->
remb_ceiling
=
ceiling
;
if
(
ceiling
+
WEBRTCPUSH_REMB_DOWN_MIN_STEP
<=
current
)
{
if
(
!
severe_down
)
client
->
remb_down_samples
++
;
down_samples
=
client
->
remb_down_samples
;
if
(
severe_down
||
client
->
remb_down_samples
>=
WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS
)
{
if
(
client
->
remb_down_samples
>=
required_down_samples
)
{
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"
,
my_zlog_debug
(
"libdatachannel: REMB down held %u/%u raw=%u filtered=%u ceiling=%u current=%u kbps"
,
down_samples
,
required_down_samples
,
bitrate
/
1000
,
filtered
/
1000
,
ceiling
/
1000
,
current
/
1000
);
return
;
...
...
@@ -933,24 +1277,39 @@ static void RTC_API on_remb(int track, unsigned int bitrate, void *ptr)
}
g_mutex_unlock
(
&
client
->
lock
);
if
(
probe_completed_now
)
{
my_zlog_info
(
"libdatachannel: initial bandwidth probe complete at %u kbps; steady sender-limited REMB guard enabled"
,
current
/
1000
);
}
if
(
log_remb_sane_floor
)
{
my_zlog_info
(
"libdatachannel: RTCP REMB sender-limited raw=%u kbps, use=%u kbps (pacing=%u pkt/%u bytes/%ums idr_grace=%d)"
,
bitrate
/
1000
,
estimate_bitrate
/
1000
,
pacing_packets
,
pacing_bytes
,
pacing_delay_ms
,
recent_idr
?
1
:
0
);
}
if
(
next
)
{
apply_bitrate
(
client
,
track
,
next
);
if
(
severe_down
)
{
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
,
ceiling
/
1000
);
my_zlog_info
(
"libdatachannel: RTCP REMB raw=%u
%s
filtered=%u kbps, encoder=%u kbps ceiling=%u kbps, severe smoothed"
,
bitrate
/
1000
,
remb_sane_floor
?
" sane-floor"
:
""
,
filtered
/
1000
,
next
/
1000
,
ceiling
/
1000
);
}
else
{
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
,
ceiling
/
1000
,
down_samples
);
my_zlog_info
(
"libdatachannel: RTCP REMB raw=%u%s filtered=%u kbps, encoder=%u kbps ceiling=%u kbps after %u samples"
,
bitrate
/
1000
,
remb_sane_floor
?
" sane-floor"
:
""
,
filtered
/
1000
,
next
/
1000
,
ceiling
/
1000
,
down_samples
);
}
}
else
if
(
ceiling
>
current
)
{
my_zlog_debug
(
"libdatachannel: REMB raw=%u filtered=%u ceiling=%u kbps (slow ramp)"
,
bitrate
/
1000
,
filtered
/
1000
,
ceiling
/
1000
);
my_zlog_debug
(
"libdatachannel: REMB raw=%u%s filtered=%u ceiling=%u kbps (slow ramp)"
,
bitrate
/
1000
,
remb_sane_floor
?
" sane-floor"
:
""
,
filtered
/
1000
,
ceiling
/
1000
);
}
else
if
(
down_samples
>
0
)
{
my_zlog_debug
(
"libdatachannel: REMB down candidate %u/%u raw=%u filtered=%u kbps"
,
down_samples
,
WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS
,
bitrate
/
1000
,
filtered
/
1000
);
my_zlog_debug
(
"libdatachannel: REMB down candidate %u/%u raw=%u%s filtered=%u kbps"
,
down_samples
,
required_down_samples
,
bitrate
/
1000
,
remb_sane_floor
?
" sane-floor"
:
""
,
filtered
/
1000
);
}
}
...
...
@@ -1117,29 +1476,46 @@ static gpointer send_thread_main(gpointer data)
now
-
last_pacing_flush_us
>=
(
gint64
)
WEBRTCPUSH_PACING_GUARD_COOLDOWN_MS
*
1000
)
{
guint
dropped_frames
=
0
;
int
dropped
=
clear_stale_pacing
(
client
,
track
,
&
dropped_frames
);
int
dropped
;
gboolean
in_idr_grace
;
gboolean
should_request_idr
;
gint64
last_idr_us
;
gint64
last_idr_request_us
;
gboolean
need_idr_pending
;
last_pacing_flush_us
=
now
;
g_mutex_lock
(
&
client
->
lock
);
last_idr_us
=
client
->
last_idr_sent_us
;
last_idr_request_us
=
client
->
last_idr_request_us
;
need_idr_pending
=
client
->
need_idr
;
in_idr_grace
=
last_idr_us
>
0
&&
now
-
last_idr_us
<
(
gint64
)
WEBRTCPUSH_PACING_IDR_GRACE_MS
*
1000
;
g_mutex_unlock
(
&
client
->
lock
);
if
(
in_idr_grace
)
{
my_zlog_warn
(
"libdatachannel: pacing backlog %u packets/%u bytes ~= %ums; IDR grace, hold queue/bitrate"
,
pacing_packets
,
pacing_bytes
,
pacing_delay_ms
);
continue
;
}
last_pacing_flush_us
=
now
;
dropped
=
clear_stale_pacing
(
client
,
track
,
&
dropped_frames
);
should_request_idr
=
!
need_idr_pending
&&
!
need_idr_pending
&&
dropped
>
0
&&
(
last_idr_us
==
0
||
now
-
last_idr_us
>=
(
gint64
)
WEBRTCPUSH_PACING_RESYNC_IDR_MS
*
1000
)
&&
(
last_idr_request_us
==
0
||
now
-
last_idr_request_us
>=
(
gint64
)
WEBRTCPUSH_PACING_RESYNC_IDR_MS
*
1000
);
g_mutex_unlock
(
&
client
->
lock
);
if
(
should_request_idr
)
{
request_idr
(
client
,
TRUE
);
my_zlog_warn
(
"libdatachannel: pacing backlog %u packets/%u bytes ~= %ums; dropped=%d RTP/%u encoded and request IDR"
,
pacing_packets
,
pacing_bytes
,
pacing_delay_ms
,
dropped
>
0
?
dropped
:
0
,
dropped_frames
);
}
else
{
my_zlog_warn
(
"libdatachannel: pacing backlog %u packets/%u bytes ~= %ums; dropped=%d RTP/%u encoded without IDR (recent/pending)"
,
my_zlog_warn
(
"libdatachannel: pacing backlog %u packets/%u bytes ~= %ums; dropped=%d RTP/%u encoded without IDR (recent/pending
/no-drop
)"
,
pacing_packets
,
pacing_bytes
,
pacing_delay_ms
,
dropped
>
0
?
dropped
:
0
,
dropped_frames
);
}
...
...
@@ -1165,13 +1541,27 @@ static gpointer send_thread_main(gpointer data)
actual_fps_x10
=
(
guint
)(((
guint64
)
sent_window_frames
*
10U
*
1000000U
)
/
elapsed_us
);
}
my_zlog_info
(
"libdatachannel: stats encoder=%u actual_h264=%u kbps sent_fps=%u.%u raw_remb=%u filtered_remb=%u ceiling=%u kbps pacing=%u pkt/%u bytes/%ums queue_dropped=%u"
,
my_zlog_info
(
"libdatachannel: stats encoder=%u actual_h264=%u kbps sent_fps=%u.%u raw_remb=%u filtered_remb=%u ceiling=%u kbps pacing=%u pkt/%u bytes/%ums queue_dropped=%u
probe=%s
"
,
target_bps
/
1000
,
actual_kbps
,
actual_fps_x10
/
10
,
actual_fps_x10
%
10
,
raw_remb_bps
/
1000
,
filtered_remb_bps
/
1000
,
remb_bps
/
1000
,
pacing_packets
,
pacing_bytes
,
pacing_delay_ms
,
queue_dropped
);
queue_dropped
,
client
->
initial_probe_complete
?
"done"
:
"active"
);
send_datachannel_mbps
(
client
,
actual_kbps
);
/* 修复: MPP CBR失控时actual>>target, pacing必须跟actual否则永远堆积 */
if
(
actual_kbps
>
0
)
{
guint
actual_bps
=
actual_kbps
*
1000U
;
guint
new_pacing
=
(
guint
)((
guint64
)
actual_bps
*
WEBRTCPUSH_PACING_HEADROOM_PERCENT
/
100U
);
if
(
new_pacing
>
WEBRTCPUSH_PACING_MAX_BITRATE
)
new_pacing
=
WEBRTCPUSH_PACING_MAX_BITRATE
;
g_mutex_lock
(
&
client
->
send_lock
);
if
(
client
->
track
==
track
)
rtcSetPacingBitrate
(
track
,
new_pacing
);
g_mutex_unlock
(
&
client
->
send_lock
);
}
sent_window_bytes
=
0
;
sent_window_frames
=
0
;
sent_window_start_us
=
now
;
...
...
@@ -1385,17 +1775,11 @@ static gboolean parse_offer(const char *sdp, OfferVideo *video, OfferAudio *audi
}
}
if
(
in_audio
)
{
/* 手机 audio 固定 recvonly(只收不发),设备 answer 固定 sendonly,
* 不再解析 offer 方向;仅取 mid 和 opus rtpmap。 */
if
(
g_str_has_prefix
(
line
,
"a=mid:"
))
{
g_free
(
audio
->
mid
);
audio
->
mid
=
g_strdup
(
line
+
strlen
(
"a=mid:"
));
}
else
if
(
g_str_has_prefix
(
line
,
"a=sendonly"
))
{
audio
->
direction
=
1
;
}
else
if
(
g_str_has_prefix
(
line
,
"a=recvonly"
))
{
audio
->
direction
=
2
;
}
else
if
(
g_str_has_prefix
(
line
,
"a=sendrecv"
))
{
audio
->
direction
=
3
;
}
else
if
(
g_str_has_prefix
(
line
,
"a=inactive"
))
{
audio
->
direction
=
4
;
}
else
if
(
g_str_has_prefix
(
line
,
"a=rtpmap:"
))
{
gint
pt
=
-
1
;
gchar
codec
[
32
]
=
{
0
};
...
...
@@ -1425,20 +1809,25 @@ static void destroy_peer(RtcClient *client)
int
pc
;
int
track
;
int
audio_track
;
int
data_channel
;
g_mutex_lock
(
&
client
->
lock
);
pc
=
client
->
pc
;
track
=
client
->
track
;
audio_track
=
client
->
audio_track
;
data_channel
=
client
->
data_channel
;
client
->
pc
=
-
1
;
client
->
track
=
-
1
;
client
->
data_channel
=
-
1
;
client
->
track_open
=
FALSE
;
client
->
first_frame_sent
=
FALSE
;
client
->
last_idr_sent_us
=
0
;
client
->
audio_track
=
-
1
;
client
->
audio_payload_type
=
-
1
;
client
->
audio_track_open
=
FALSE
;
client
->
audio_send_enabled
=
FALSE
;
client
->
audio_recv_enabled
=
FALSE
;
if
(
client
->
dc_audio_buf
)
g_byte_array_set_size
(
client
->
dc_audio_buf
,
0
);
client
->
answer_sent
=
FALSE
;
client
->
need_idr
=
TRUE
;
client
->
source_pts_valid
=
FALSE
;
...
...
@@ -1458,6 +1847,10 @@ static void destroy_peer(RtcClient *client)
rtcClose
(
audio_track
);
rtcDeleteTrack
(
audio_track
);
}
if
(
data_channel
>=
0
)
{
rtcClose
(
data_channel
);
rtcDeleteDataChannel
(
data_channel
);
}
if
(
pc
>=
0
)
{
rtcClosePeerConnection
(
pc
);
rtcDeletePeerConnection
(
pc
);
...
...
@@ -1535,34 +1928,22 @@ static gboolean setup_audio_track(RtcClient *client, int pc,
rtcTrackInit
track_init
;
rtcPacketizerInit
packetizer
;
guint32
ssrc
=
g_random_int
();
gboolean
offer_send
;
gboolean
offer_recv
;
gboolean
send_audio
;
gboolean
recv_audio
;
int
track
;
if
(
ssrc
==
0
)
ssrc
=
1
;
memset
(
&
track_init
,
0
,
sizeof
(
track_init
));
/* SDP 未声明 direction 时按 WebRTC 默认 sendrecv 处理。 */
offer_send
=
(
audio
->
direction
==
0
||
audio
->
direction
==
1
||
audio
->
direction
==
3
);
offer_recv
=
(
audio
->
direction
==
0
||
audio
->
direction
==
2
||
audio
->
direction
==
3
);
send_audio
=
offer_recv
&&
client
->
audio_source
;
recv_audio
=
offer_send
&&
client
->
audio_sink
;
if
(
send_audio
&&
recv_audio
)
track_init
.
direction
=
RTC_DIRECTION_SENDRECV
;
else
if
(
send_audio
)
/* 手机端 audio 为 recvonly(只收不发),设备固定 sendonly 推 Opus 给手机。
* 手机→设备的音频走 DataChannel 发 MP4,不再通过 RTP 接收。 */
send_audio
=
client
->
audio_source
!=
NULL
;
if
(
send_audio
)
track_init
.
direction
=
RTC_DIRECTION_SENDONLY
;
else
if
(
recv_audio
)
track_init
.
direction
=
RTC_DIRECTION_RECVONLY
;
else
track_init
.
direction
=
RTC_DIRECTION_INACTIVE
;
my_zlog_info
(
"libdatachannel: audio track direction:
offer=%d answer=%d
"
,
audio
->
direction
,
(
int
)
track_init
.
direction
);
my_zlog_info
(
"libdatachannel: audio track direction:
answer=%d (sendonly)
"
,
(
int
)
track_init
.
direction
);
track_init
.
codec
=
RTC_CODEC_OPUS
;
track_init
.
payloadType
=
audio
->
payload_type
;
track_init
.
ssrc
=
ssrc
;
...
...
@@ -1578,8 +1959,6 @@ static gboolean setup_audio_track(RtcClient *client, int pc,
rtcSetOpenCallback
(
track
,
on_track_open
);
rtcSetClosedCallback
(
track
,
on_track_closed
);
rtcSetErrorCallback
(
track
,
on_track_error
);
if
(
recv_audio
)
rtcSetMessageCallback
(
track
,
on_audio_message
);
memset
(
&
packetizer
,
0
,
sizeof
(
packetizer
));
packetizer
.
ssrc
=
ssrc
;
...
...
@@ -1597,13 +1976,12 @@ static gboolean setup_audio_track(RtcClient *client, int pc,
return
FALSE
;
}
}
/* recv_audio 的 RTP payload 由 on_audio_message 送入 audio_sink。 */
g_mutex_lock
(
&
client
->
lock
);
client
->
audio_rtp_timestamp
=
packetizer
.
timestamp
;
client
->
audio_payload_type
=
audio
->
payload_type
;
client
->
audio_pts_valid
=
FALSE
;
client
->
audio_send_enabled
=
send_audio
;
client
->
audio_recv_enabled
=
recv_audio
;
g_mutex_unlock
(
&
client
->
lock
);
*
track_out
=
track
;
return
TRUE
;
...
...
@@ -1614,14 +1992,24 @@ static void flush_remote_ice(RtcClient *client)
for
(;;)
{
PendingRemoteIce
*
ice
;
int
pc
;
gboolean
ufrag_match
;
g_mutex_lock
(
&
client
->
lock
);
ice
=
g_queue_pop_head
(
client
->
pending_remote_ice
);
pc
=
client
->
pc
;
ufrag_match
=
!
ice
||
!
ice
->
ufrag
||
!
client
->
remote_ice_ufrag
||
g_strcmp0
(
ice
->
ufrag
,
client
->
remote_ice_ufrag
)
==
0
;
g_mutex_unlock
(
&
client
->
lock
);
if
(
!
ice
)
break
;
if
(
pc
>=
0
)
rtcAddRemoteCandidate
(
pc
,
ice
->
candidate
,
ice
->
mid
);
if
(
pc
>=
0
&&
ufrag_match
)
{
if
(
rtcAddRemoteCandidate
(
pc
,
ice
->
candidate
,
ice
->
mid
)
<
0
)
my_zlog_warn
(
"libdatachannel: rejected queued remote ICE candidate ufrag=%s"
,
ice
->
ufrag
?
ice
->
ufrag
:
"?"
);
}
else
if
(
!
ufrag_match
)
{
my_zlog_info
(
"libdatachannel: discard stale remote ICE candidate ufrag=%s current=%s"
,
ice
->
ufrag
?
ice
->
ufrag
:
"?"
,
client
->
remote_ice_ufrag
?
client
->
remote_ice_ufrag
:
"?"
);
}
free_pending_remote_ice
(
ice
);
}
}
...
...
@@ -1641,12 +2029,19 @@ int rtc_client_start(AppState *app)
client
->
app
=
app
;
client
->
pc
=
-
1
;
client
->
track
=
-
1
;
client
->
audio_track
=
-
1
;
client
->
audio_payload_type
=
-
1
;
client
->
data_channel
=
-
1
;
client
->
target_bitrate
=
WEBRTCPUSH_INITIAL_BITRATE
;
client
->
applied_mpp_bitrate
=
WEBRTCPUSH_INITIAL_BITRATE
;
client
->
remb_ceiling
=
WEBRTCPUSH_INITIAL_BITRATE
;
client
->
initial_probe_complete
=
FALSE
;
client
->
probe_target_reached_us
=
0
;
client
->
last_remb_bitrate
=
0
;
client
->
remb_filtered
=
0
;
client
->
remb_down_samples
=
0
;
client
->
last_bitrate_ramp_us
=
g_get_monotonic_time
();
client
->
last_mpp_bitrate_apply_us
=
client
->
last_bitrate_ramp_us
;
client
->
pending_local_ice
=
g_queue_new
();
client
->
pending_remote_ice
=
g_queue_new
();
g_mutex_init
(
&
client
->
lock
);
...
...
@@ -1712,8 +2107,12 @@ int rtc_client_start(AppState *app)
void
rtc_client_reset
(
AppState
*
app
)
{
RtcClient
*
client
=
client_from_app
(
app
);
if
(
client
)
if
(
client
)
{
destroy_peer
(
client
);
g_mutex_lock
(
&
client
->
lock
);
g_queue_clear_full
(
client
->
pending_remote_ice
,
free_pending_remote_ice
);
g_mutex_unlock
(
&
client
->
lock
);
}
}
void
rtc_client_stop
(
AppState
*
app
)
...
...
@@ -1746,6 +2145,25 @@ void rtc_client_stop(AppState *app)
clear_local_ice
(
client
);
g_mutex_unlock
(
&
client
->
lock
);
g_queue_free
(
client
->
pending_local_ice
);
if
(
client
->
dc_audio_buf
)
g_byte_array_unref
(
client
->
dc_audio_buf
);
/* 停止播放队列线程 */
if
(
client
->
dc_play_queue
)
{
client
->
dc_play_quit
=
TRUE
;
g_async_queue_push
(
client
->
dc_play_queue
,
GINT_TO_POINTER
(
-
1
));
/* 唤醒 */
if
(
client
->
dc_play_thread
)
g_thread_join
(
client
->
dc_play_thread
);
/* 清空队列剩余任务 */
while
(
1
)
{
gpointer
p
=
g_async_queue_try_pop
(
client
->
dc_play_queue
);
if
(
!
p
||
p
==
GINT_TO_POINTER
(
-
1
))
break
;
DcMp3PlayTask
*
t
=
p
;
if
(
t
)
{
g_free
(
t
->
data
);
g_free
(
t
);
}
}
g_async_queue_unref
(
client
->
dc_play_queue
);
client
->
dc_play_queue
=
NULL
;
client
->
dc_play_thread
=
NULL
;
}
mpp_h264_source_stop
(
client
->
h264_source
);
frame_queue_fini
(
&
client
->
frame_queue
);
g_cond_clear
(
&
client
->
track_cond
);
...
...
@@ -1764,6 +2182,7 @@ int rtc_client_handle_offer(AppState *app, const char *sdp)
int
pc
;
int
track
=
-
1
;
int
audio_track
=
-
1
;
int
data_channel
=
-
1
;
gboolean
duplicate
;
if
(
!
client
||
!
sdp
)
...
...
@@ -1791,7 +2210,7 @@ int rtc_client_handle_offer(AppState *app, const char *sdp)
config
.
iceServers
=
s_ice_servers
;
config
.
iceServersCount
=
(
int
)
G_N_ELEMENTS
(
s_ice_servers
);
config
.
certificateType
=
RTC_CERTIFICATE_ECDSA
;
config
.
forceMediaTransport
=
TRUE
;
/* 预分配 SRTP 媒体通道
,与 ENABLE_DATACHANNEL=0 一致
*/
config
.
forceMediaTransport
=
TRUE
;
/* 预分配 SRTP 媒体通道
;DataChannel 使用同一个 PC 的 SCTP m-line
*/
config
.
mtu
=
1200
;
pc
=
rtcCreatePeerConnection
(
&
config
);
if
(
pc
<
0
)
{
...
...
@@ -1803,7 +2222,21 @@ int rtc_client_handle_offer(AppState *app, const char *sdp)
rtcSetLocalDescriptionCallback
(
pc
,
on_local_description
);
rtcSetLocalCandidateCallback
(
pc
,
on_local_candidate
);
rtcSetStateChangeCallback
(
pc
,
on_state_change
);
rtcSetDataChannelCallback
(
pc
,
on_incoming_data_channel
);
#if WEBRTCPUSH_ENABLE_DATACHANNEL
data_channel
=
rtcCreateDataChannel
(
pc
,
"myDataChannel"
);
if
(
data_channel
>=
0
)
{
setup_data_channel_callbacks
(
client
,
data_channel
);
my_zlog_info
(
"libdatachannel: DataChannel created label=myDataChannel"
);
}
else
{
my_zlog_warn
(
"libdatachannel: failed to create DataChannel"
);
}
#endif
if
(
!
setup_track
(
client
,
pc
,
&
video
,
&
track
))
{
if
(
data_channel
>=
0
)
rtcDeleteDataChannel
(
data_channel
);
rtcDeletePeerConnection
(
pc
);
offer_video_clear
(
&
video
);
offer_audio_clear
(
&
audio
);
...
...
@@ -1827,15 +2260,20 @@ int rtc_client_handle_offer(AppState *app, const char *sdp)
client
->
pc
=
pc
;
client
->
track
=
track
;
client
->
audio_track
=
audio_track
;
client
->
data_channel
=
data_channel
;
client
->
video_mline_index
=
video
.
mline_index
;
client
->
video_mid
=
g_strdup
(
video
.
mid
);
client
->
remote_ice_ufrag
=
g_strdup
(
video
.
ice_ufrag
);
client
->
target_bitrate
=
WEBRTCPUSH_INITIAL_BITRATE
;
client
->
applied_mpp_bitrate
=
WEBRTCPUSH_INITIAL_BITRATE
;
client
->
remb_ceiling
=
WEBRTCPUSH_INITIAL_BITRATE
;
client
->
initial_probe_complete
=
FALSE
;
client
->
probe_target_reached_us
=
0
;
client
->
last_remb_bitrate
=
0
;
client
->
remb_filtered
=
0
;
client
->
remb_down_samples
=
0
;
client
->
last_bitrate_ramp_us
=
g_get_monotonic_time
();
client
->
last_mpp_bitrate_apply_us
=
client
->
last_bitrate_ramp_us
;
client
->
need_idr
=
TRUE
;
/* 内联,避免 request_idr 二次加锁死锁 */
client
->
last_idr_request_us
=
g_get_monotonic_time
();
client
->
source_pts_valid
=
FALSE
;
...
...
@@ -1863,6 +2301,9 @@ void rtc_client_handle_remote_ice(AppState *app, JsonObject *candidate_obj)
RtcClient
*
client
=
client_from_app
(
app
);
const
gchar
*
candidate
;
const
gchar
*
mid
=
NULL
;
const
gchar
*
ufrag
=
NULL
;
gchar
*
candidate_ufrag
=
NULL
;
gboolean
queued
=
FALSE
;
int
pc
;
if
(
!
client
||
!
candidate_obj
||
...
...
@@ -1877,18 +2318,48 @@ void rtc_client_handle_remote_ice(AppState *app, JsonObject *candidate_obj)
json_node_get_value_type
(
node
)
==
G_TYPE_STRING
)
mid
=
json_object_get_string_member
(
candidate_obj
,
"sdpMid"
);
}
if
(
json_object_has_member
(
candidate_obj
,
"usernameFragment"
))
{
JsonNode
*
node
=
json_object_get_member
(
candidate_obj
,
"usernameFragment"
);
if
(
JSON_NODE_HOLDS_VALUE
(
node
)
&&
json_node_get_value_type
(
node
)
==
G_TYPE_STRING
)
ufrag
=
json_object_get_string_member
(
candidate_obj
,
"usernameFragment"
);
}
else
if
(
json_object_has_member
(
candidate_obj
,
"ufrag"
))
{
JsonNode
*
node
=
json_object_get_member
(
candidate_obj
,
"ufrag"
);
if
(
JSON_NODE_HOLDS_VALUE
(
node
)
&&
json_node_get_value_type
(
node
)
==
G_TYPE_STRING
)
ufrag
=
json_object_get_string_member
(
candidate_obj
,
"ufrag"
);
}
if
(
!
ufrag
)
{
const
gchar
*
tag
=
strstr
(
candidate
,
" ufrag "
);
if
(
tag
)
{
const
gchar
*
end
;
tag
+=
strlen
(
" ufrag "
);
end
=
strchr
(
tag
,
' '
);
candidate_ufrag
=
end
?
g_strndup
(
tag
,
(
gsize
)(
end
-
tag
))
:
g_strdup
(
tag
);
ufrag
=
candidate_ufrag
;
}
}
g_mutex_lock
(
&
client
->
lock
);
pc
=
client
->
pc
;
if
(
pc
<
0
)
{
if
(
pc
<
0
||
(
ufrag
&&
client
->
remote_ice_ufrag
&&
g_strcmp0
(
ufrag
,
client
->
remote_ice_ufrag
)
!=
0
))
{
PendingRemoteIce
*
pending
=
g_new0
(
PendingRemoteIce
,
1
);
pending
->
candidate
=
g_strdup
(
candidate
);
pending
->
mid
=
g_strdup
(
mid
);
pending
->
ufrag
=
g_strdup
(
ufrag
);
g_queue_push_tail
(
client
->
pending_remote_ice
,
pending
);
pc
=
-
1
;
queued
=
TRUE
;
}
g_mutex_unlock
(
&
client
->
lock
);
if
(
pc
>=
0
&&
rtcAddRemoteCandidate
(
pc
,
candidate
,
mid
)
<
0
)
my_zlog_warn
(
"libdatachannel: rejected remote ICE candidate"
);
else
if
(
queued
)
my_zlog_info
(
"libdatachannel: queued remote ICE candidate for ufrag=%s"
,
ufrag
?
ufrag
:
"?"
);
g_free
(
candidate_ufrag
);
}
gboolean
rtc_client_is_active
(
AppState
*
app
)
...
...
modules/webrtcpush/webrtcpush_config.h
View file @
7200adca
...
...
@@ -17,37 +17,58 @@
/*
* 与 gst_webrtc_pipeline / jywy 浏览器推流对齐的码率策略。
* 首屏先用
900kbps,不像 1.4Mbps
那样猛冲,也不要低到一进来就糊。
* RTCP REMB 只作为码率趋势
,下降也做阶梯平滑,避免 MPP 动态切码率时卡顿
。
* 首屏先用
1.06Mbps,不像高码率
那样猛冲,也不要低到一进来就糊。
* RTCP REMB 只作为码率趋势
:小步慢降、慢升、带滞回,尽量接近浏览器的无感自适应
。
*/
#define WEBRTCPUSH_INITIAL_BITRATE
90
0000U
#define WEBRTCPUSH_MIN_BITRATE
5
00000U
#define WEBRTCPUSH_MAX_BITRATE
28
00000U
#define WEBRTCPUSH_INITIAL_BITRATE
106
0000U
#define WEBRTCPUSH_MIN_BITRATE
8
00000U
#define WEBRTCPUSH_MAX_BITRATE
30
00000U
#define WEBRTCPUSH_REMB_UTIL_PERCENT 80U
#define WEBRTCPUSH_REMB_DOWN_MIN_STEP 100000U
#define WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS 4U
#define WEBRTCPUSH_REMB_DOWN_CONFIRMATIONS 8U
#define WEBRTCPUSH_REMB_SEVERE_CONFIRMATIONS 2U
#define WEBRTCPUSH_REMB_SEVERE_PERCENT 65U
#define WEBRTCPUSH_BITRATE_RAMP_UP_MS 2500U
#define WEBRTCPUSH_BITRATE_RAMP_DOWN_MS 1500U
#define WEBRTCPUSH_REMB_DOWN_STEP_PERCENT 15U
#define WEBRTCPUSH_REMB_DOWN_STEP_MIN_BPS 80000U
#define WEBRTCPUSH_REMB_DOWN_STEP_MAX_BPS 180000U
#define WEBRTCPUSH_PACING_HEADROOM_PERCENT 140U
#define WEBRTCPUSH_BITRATE_RAMP_UP_MS 1200U
#define WEBRTCPUSH_BITRATE_RAMP_DOWN_MS 2000U
#define WEBRTCPUSH_BITRATE_RAMP_UP_PERCENT 12U
#define WEBRTCPUSH_BITRATE_RAMP_UP_STEP_MIN_BPS 70000U
#define WEBRTCPUSH_BITRATE_RAMP_UP_STEP_MAX_BPS 250000U
#define WEBRTCPUSH_REMB_DOWN_STEP_PERCENT 10U
#define WEBRTCPUSH_REMB_DOWN_STEP_MIN_BPS 60000U
#define WEBRTCPUSH_REMB_DOWN_STEP_MAX_BPS 120000U
/* REMB 低估保护:pacing 不堵时,过低浏览器估计不直接压糊 720p。 */
#define WEBRTCPUSH_HEALTHY_PROBE_TARGET_BPS 2700000U
/* 3.375Mbps * 80% = 2.7Mbps. 这是 REMB 原始估计的探测地板,不是编码码率。 */
#define WEBRTCPUSH_REMB_SANE_FLOOR_BPS 3375000U
#define WEBRTCPUSH_REMB_SANE_RAW_MAX_BPS WEBRTCPUSH_REMB_SANE_FLOOR_BPS
#define WEBRTCPUSH_REMB_SANE_PACING_MAX_MS 100U
/* IDR 会产生短时大包,不能在这个窗口内把瞬时 pacing 峰值误判成网络拥塞。 */
#define WEBRTCPUSH_REMB_IDR_GRACE_MS 1000U
/* 手机切 Wi-Fi/蜂窝或换手机时,旧成员的 leave 可能晚于新 offer 到达。 */
#define WEBRTCPUSH_PEER_LEAVE_GRACE_MS 8000U
/* 达到 2.3Mbps 后继续发送一段时间,让浏览器有真实流量可重新估计带宽。 */
#define WEBRTCPUSH_INITIAL_PROBE_HOLD_MS 6000U
#define WEBRTCPUSH_PACING_HEADROOM_PERCENT 135U
#define WEBRTCPUSH_PACING_INTERVAL_MS 5U
#define WEBRTCPUSH_PACING_MAX_BITRATE 4
0
00000U
#define WEBRTCPUSH_PACING_MAX_QUEUE_MS 2
5
0U
#define WEBRTCPUSH_PACING_GUARD_COOLDOWN_MS
2
000U
#define WEBRTCPUSH_PACING_MAX_BITRATE 4
2
00000U
#define WEBRTCPUSH_PACING_MAX_QUEUE_MS 2
6
0U
#define WEBRTCPUSH_PACING_GUARD_COOLDOWN_MS
1
000U
/* 首个/刚恢复的 IDR 允许短暂排队,避免首屏关键帧刚发出就被清队列 */
#define WEBRTCPUSH_PACING_IDR_GRACE_MS
80
0U
#define WEBRTCPUSH_PACING_IDR_GRACE_MS
25
0U
/* pacing 清队列后,若近期已有 IDR,不要反复强制 IDR 造成 I 帧风暴 */
#define WEBRTCPUSH_PACING_RESYNC_IDR_MS 5000U
/* RK MPP 运行中小幅改 bps 容易顿一下;小变化只调 pacing,少重配硬编。 */
#define WEBRTCPUSH_MPP_RECONFIG_MIN_DELTA_PERCENT 20U
#define WEBRTCPUSH_MPP_RECONFIG_MAX_STEP_PERCENT 25U
#define WEBRTCPUSH_MPP_RECONFIG_MIN_INTERVAL_MS 1000U
/* RTP 分片与 NACK(MTU=1200,留 SRTP/DTLS/FU 余量) */
#define WEBRTCPUSH_RTP_MAX_FRAGMENT 1050U
#define WEBRTCPUSH_NACK_PACKETS 512U
/* PLI/IDR 节流:避免频繁 force-key-unit 拉高瞬时码率 */
#define WEBRTCPUSH_PLI_IDR_THROTTLE_MS
500U
#define WEBRTCPUSH_PLI_IDR_THROTTLE_MS
2000U
/* 500->2000: 避免IDR风暴, 每个IDR都加大pacing堆积 */
/* 周期性推流指标日志间隔 */
#define WEBRTCPUSH_STATS_LOG_INTERVAL_MS 8000U
...
...
@@ -66,8 +87,7 @@
#define WEBRTCPUSH_MPP_POST_ENC_BUFFERS 1
/* 编码后保留 AU */
#define WEBRTCPUSH_APPSINK_MAX_BUFFERS 1
/* MJPEG 解压:1=GStreamer CPU jpegdec 优先;0=mppjpegdec 优先 */
#define WEBRTCPUSH_MJPEG_CPU_DECODE 1
/* native MJPEG 解压固定使用 libjpeg-turbo(jpeglib) 手动解码;旧 GStreamer 解码分支已删除 */
/* MPP 码控收紧:低 REMB 时实际 H264 不能长期高于目标太多 */
#define WEBRTCPUSH_MPP_BPS_MIN_PERCENT 60U
...
...
@@ -96,6 +116,15 @@
/* 音频播放(手机->设备):ALSA 喇叭设备 */
#define WEBRTCPUSH_AUDIO_PLAYBACK_DEVICE "plughw:2,0"
/*
* 手机->设备喊话音量策略:
* 0:后端返回 0 就按 0 播放(静音)
* 1:后端返回 0 时按 WEBRTCPUSH_AUDIO_ZERO_VOLUME_MIN 播放,便于测试喊话链路
*/
#define WEBRTCPUSH_AUDIO_ZERO_VOLUME_AS_MIN 0
#define WEBRTCPUSH_AUDIO_ZERO_VOLUME_MIN 0.5
/* 手机喊话结束后短暂保温,避免每个短包都冷启动声卡;到时仍会释放喇叭。 */
#define WEBRTCPUSH_AUDIO_SINK_IDLE_TIMEOUT_MS 3000U
/* 后端音量控制接口 */
#define WEBRTCPUSH_VOLUME_API_BASE "https://fcrs-api.yd-ss.com/api/drive/use/status/"
...
...
@@ -104,10 +133,11 @@
/*
* 设备侧 DataChannel(myDataChannel)与手机 createDataChannel('init') 争用 SCTP,
* 易触发 sctpenc association error 并导致管道闪断/进程崩溃。仅推流可不建。
* 设备侧 DataChannel(myDataChannel):
* - 前端 ondatachannel 后用 remoteChannel 发送 MP3 分片 + "EOF" 给设备播放;
* - 设备也通过该通道发送 Mbps 字符串,更新右上角网络显示。
*/
#define WEBRTCPUSH_ENABLE_DATACHANNEL
0
#define WEBRTCPUSH_ENABLE_DATACHANNEL
1
/* WebRTC 信令 WebSocket 主机(路径 /websocket?dev=设备号) */
#define WEBRTCPUSH_SIGNAL_HOST "signal.yd-ss.com"
...
...
modules/webrtcpush/ws_signaling.c
View file @
7200adca
...
...
@@ -23,11 +23,15 @@ static gchar *s_ws_url;
static
GMainContext
*
s_main_ctx
;
static
gchar
*
s_debounced_offer_sdp
=
NULL
;
static
guint
s_debounce_offer_id
=
0
;
static
guint
s_peer_leave_reset_id
=
0
;
static
guint
s_offer_generation
=
0
;
static
guint
s_leave_generation
=
0
;
static
gboolean
reconnect_cb
(
gpointer
data
);
static
void
ws_connect
(
void
);
static
void
reset_signaling_session
(
AppState
*
app
,
gboolean
teardown_pipeline
);
static
gboolean
idle_pipeline_teardown
(
gpointer
p
);
static
gboolean
delayed_peer_leave_reset
(
gpointer
p
);
static
void
schedule_tx_flush
(
void
);
static
void
dispatch_json
(
const
gchar
*
payload
);
...
...
@@ -228,6 +232,12 @@ static gboolean debounced_offer_fire(gpointer user_data) {
}
static
void
schedule_offer
(
AppState
*
app
,
const
gchar
*
sdp
)
{
s_offer_generation
++
;
if
(
s_peer_leave_reset_id
)
{
g_source_remove
(
s_peer_leave_reset_id
);
s_peer_leave_reset_id
=
0
;
my_zlog_info
(
"webrtcpush: new offer cancels pending stale peer-leave reset"
);
}
if
(
!
app
->
member_id
)
{
g_free
(
s_pending_offer_sdp
);
s_pending_offer_sdp
=
g_strdup
(
sdp
);
...
...
@@ -388,8 +398,14 @@ static void dispatch_json(const gchar *payload) {
if
(
!
g_strcmp0
(
t
,
"leave"
))
{
if
(
rtc_client_is_active
(
s_app
))
{
my_zlog_info
(
"webrtcpush: peer left, reset libdatachannel peer"
);
g_idle_add
(
idle_pipeline_teardown
,
s_app
);
if
(
s_peer_leave_reset_id
)
g_source_remove
(
s_peer_leave_reset_id
);
s_leave_generation
=
s_offer_generation
;
s_peer_leave_reset_id
=
g_timeout_add
(
WEBRTCPUSH_PEER_LEAVE_GRACE_MS
,
delayed_peer_leave_reset
,
s_app
);
my_zlog_info
(
"webrtcpush: peer left, defer reset %ums for network/device switch"
,
WEBRTCPUSH_PEER_LEAVE_GRACE_MS
);
}
g_object_unref
(
parser
);
return
;
...
...
@@ -436,6 +452,12 @@ static void dispatch_json(const gchar *payload) {
if
(
json_node_get_value_type
(
midn
)
==
G_TYPE_STRING
)
json_object_set_string_member
(
co
,
"sdpMid"
,
json_object_get_string_member
(
root
,
"sdpMid"
));
}
if
(
json_object_has_member
(
root
,
"usernameFragment"
))
{
JsonNode
*
ufn
=
json_object_get_member
(
root
,
"usernameFragment"
);
if
(
json_node_get_value_type
(
ufn
)
==
G_TYPE_STRING
)
json_object_set_string_member
(
co
,
"usernameFragment"
,
json_object_get_string_member
(
root
,
"usernameFragment"
));
}
g_idle_add
(
idle_ice
,
co
);
}
}
...
...
@@ -506,6 +528,26 @@ static gboolean idle_pipeline_teardown(gpointer p) {
return
G_SOURCE_REMOVE
;
}
static
gboolean
delayed_peer_leave_reset
(
gpointer
p
)
{
AppState
*
app
=
p
;
guint
generation
=
s_leave_generation
;
s_peer_leave_reset_id
=
0
;
if
(
!
app
||
generation
!=
s_offer_generation
)
{
my_zlog_info
(
"webrtcpush: stale peer-leave reset ignored (new offer generation)"
);
return
G_SOURCE_REMOVE
;
}
if
(
rtc_client_media_open
(
app
))
{
my_zlog_info
(
"webrtcpush: stale peer-leave reset ignored (new media already open)"
);
return
G_SOURCE_REMOVE
;
}
if
(
rtc_client_is_active
(
app
))
{
my_zlog_info
(
"webrtcpush: peer leave confirmed, release inactive libdatachannel peer"
);
rtc_client_reset
(
app
);
}
return
G_SOURCE_REMOVE
;
}
static
void
ws_connect
(
void
)
{
if
(
s_ws_exiting
||
!
s_app
||
!
s_ws_url
)
return
;
...
...
@@ -610,6 +652,10 @@ void ws_signaling_stop(AppState *app) {
g_source_remove
(
s_reconnect_id
);
s_reconnect_id
=
0
;
}
if
(
s_peer_leave_reset_id
)
{
g_source_remove
(
s_peer_leave_reset_id
);
s_peer_leave_reset_id
=
0
;
}
if
(
s_ping_id
)
{
g_source_remove
(
s_ping_id
);
s_ping_id
=
0
;
...
...
zlog.conf
View file @
7200adca
...
...
@@ -9,4 +9,4 @@ file perms = 600
millisecond
=
"%d(%Y-%m-%d %H:%M:%S).%ms [%V] %m%n"
[
rules
]
my_log
.*
"/home/orangepi/car/master/log/log_2026-07-
08
.log"
;
millisecond
my_log
.*
"/home/orangepi/car/master/log/log_2026-07-
14
.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