Skip to content

Commit 38cd575

Browse files
committed
fix: recover Actions Micro encoder failures
1 parent 3952d6a commit 38cd575

3 files changed

Lines changed: 227 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## Unreleased
44

5+
- Recover a failed persistent FFmpeg process inside the Actions Micro backend
6+
with one bounded same-frame retry, while preserving the live HID transport
7+
session and its command/video sequence numbers.
8+
- Verify that each encoder handoff contains SPS, PPS, and IDR NAL units; report
9+
the FFmpeg exit status, bounded HID input samples, and encoder generations.
510
- Add dual-hidraw input polling, a missed-heartbeat session watchdog, and a
611
VID/PID-scoped `USBDEVFS_RESET` before Actions Micro transport reopen.
712
- Reopen the kernel frame stream after transport recovery, resubmit the newest

backends/actions-micro/actions_micro_backend.c

Lines changed: 184 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@
4545
#define MISSED_HEARTBEAT_LIMIT 3ULL
4646
#define MAX_INPUT_READS_PER_TICK 32U
4747
#define HEARTBEAT_LOG_INTERVAL 60U
48+
/* 输入状态按固定间隔采样;编码故障只允许当前帧本地重试一次。 */
49+
#define INPUT_SAMPLE_INTERVAL 60U
50+
#define INPUT_SAMPLE_PREFIX_BYTES 16U
51+
#define MAX_ENCODER_ATTEMPTS 2U
52+
#define ENCODER_EXIT_GRACE_NS 20000000L
4853
#define USB_RESET_MIN_INTERVAL_NS 60000000000ULL
4954
#define USB_SYSFS_DEVICES "/sys/bus/usb/devices"
5055
#define USB_DEVICE_NODE_LENGTH 64U
@@ -89,6 +94,8 @@ struct actions_encoder {
8994
int input_fd;
9095
int output_fd;
9196
uint32_t format;
97+
/* 编码代次只跟随 FFmpeg 重建,不改变固件传输序号。 */
98+
uint64_t generation;
9299
unsigned char *packed_frame;
93100
size_t packed_capacity;
94101
unsigned char *encoded;
@@ -116,15 +123,18 @@ struct actions_context {
116123
uint64_t reports;
117124
/* 输入与复位状态把无声会话掉线纳入可观测、可恢复的路径。 */
118125
uint64_t input_reports;
126+
uint64_t input_reports_by_endpoint[2];
119127
uint64_t last_input_ns;
120128
uint64_t last_heartbeat_sent_ns;
121129
uint64_t input_timeout_ns;
122130
uint64_t heartbeat_success_count;
131+
uint64_t encoder_restarts;
123132
uint32_t width;
124133
uint32_t height;
125134
int hid[2];
126135
bool transport_failed;
127136
bool usb_reset_attempted;
137+
bool encoder_handoff_pending;
128138
};
129139

130140
struct byte_buffer {
@@ -133,6 +143,15 @@ struct byte_buffer {
133143
size_t capacity;
134144
};
135145

146+
/* 首个编码访问单元必须证明参数集和 IDR 完整,AUD 仅记录兼容性证据。 */
147+
struct nal_summary {
148+
bool aud;
149+
bool sps;
150+
bool pps;
151+
bool idr;
152+
unsigned int count;
153+
};
154+
136155
static uint16_t read_le16(const unsigned char *data)
137156
{
138157
return (uint16_t)data[0] | ((uint16_t)data[1] << 8);
@@ -300,7 +319,7 @@ static int find_actions_micro_usb_node(char device_path[USB_DEVICE_NODE_LENGTH])
300319
return result;
301320
}
302321

303-
/* 会话丢失后用 USBDEVFS_RESET 复现已验证的物理拔插重连前提。 */
322+
/* 会话丢失后仅尝试主机侧 USB reset;该动作不切断 VBUS,也不等同物理拔插。 */
304323
static int reset_actions_micro_usb_device(void)
305324
{
306325
char device_path[USB_DEVICE_NODE_LENGTH];
@@ -1031,6 +1050,27 @@ static int send_due_heartbeats(struct actions_context *state, uint64_t now_ns)
10311050
return result;
10321051
}
10331052

1053+
/* 固件输入只输出有界前缀和限频计数,便于识别确认报文且不淹没现场日志。 */
1054+
static void log_hid_input_sample(int input_index,
1055+
const unsigned char *report, ssize_t bytes_read,
1056+
uint64_t endpoint_count)
1057+
{
1058+
size_t prefix_bytes = (size_t)bytes_read;
1059+
size_t index = 0;
1060+
1061+
if (prefix_bytes > INPUT_SAMPLE_PREFIX_BYTES) {
1062+
prefix_bytes = INPUT_SAMPLE_PREFIX_BYTES;
1063+
}
1064+
fprintf(stderr,
1065+
"actions-micro: HID input sample input=%d count=%llu bytes=%zd prefix=",
1066+
input_index, (unsigned long long)endpoint_count, bytes_read);
1067+
while (index < prefix_bytes) {
1068+
fprintf(stderr, "%02x", report[index]);
1069+
++index;
1070+
}
1071+
fputc('\n', stderr);
1072+
}
1073+
10341074
/* tick 轮询两个输入接口并排空缓存,避免固件状态报告在环形缓冲区积压。 */
10351075
static int poll_hid_inputs(struct actions_context *state, uint64_t now_ns)
10361076
{
@@ -1080,6 +1120,14 @@ static int poll_hid_inputs(struct actions_context *state, uint64_t now_ns)
10801120
if (bytes_read > 0) {
10811121
state->last_input_ns = now_ns;
10821122
++state->input_reports;
1123+
++state->input_reports_by_endpoint[index];
1124+
/* 前四次及每六十次采样一次,保留状态变化的时间锚点。 */
1125+
if (state->input_reports_by_endpoint[index] <= 4ULL ||
1126+
state->input_reports_by_endpoint[index] %
1127+
INPUT_SAMPLE_INTERVAL == 0ULL) {
1128+
log_hid_input_sample(index, report, bytes_read,
1129+
state->input_reports_by_endpoint[index]);
1130+
}
10831131
++reads;
10841132
drained = false;
10851133
} else if (bytes_read == 0) {
@@ -1149,8 +1197,17 @@ static void close_encoder_descriptors(struct actions_encoder *encoder)
11491197

11501198
static void stop_encoder(struct actions_encoder *encoder)
11511199
{
1152-
int status;
1200+
struct timespec exit_grace = {0, ENCODER_EXIT_GRACE_NS};
1201+
pid_t process = encoder->process;
1202+
pid_t waited = -1;
1203+
int status = 0;
1204+
bool parent_stopped = false;
11531205

1206+
/* 关闭管道前先采样退出状态,避免把父进程触发的 EOF 误报为自然退出。 */
1207+
if (process > 0) {
1208+
waited = waitpid(process, &status, WNOHANG);
1209+
parent_stopped = waited == 0;
1210+
}
11541211
if (encoder->input_fd >= 0) {
11551212
close(encoder->input_fd);
11561213
encoder->input_fd = -1;
@@ -1159,13 +1216,34 @@ static void stop_encoder(struct actions_encoder *encoder)
11591216
close(encoder->output_fd);
11601217
encoder->output_fd = -1;
11611218
}
1162-
if (encoder->process > 0) {
1163-
if (waitpid(encoder->process, &status, WNOHANG) == 0) {
1164-
kill(encoder->process, SIGTERM);
1165-
while (waitpid(encoder->process, &status, 0) < 0 &&
1219+
/* stdout EOF 后给 FFmpeg 有界收尾窗口,优先保留真实退出码或信号。 */
1220+
if (process > 0 && waited == 0) {
1221+
(void)nanosleep(&exit_grace, NULL);
1222+
waited = waitpid(process, &status, WNOHANG);
1223+
}
1224+
if (process > 0) {
1225+
if (waited == 0) {
1226+
(void)kill(process, SIGTERM);
1227+
while ((waited = waitpid(process, &status, 0)) < 0 &&
11661228
errno == EINTR) {
11671229
}
11681230
}
1231+
/* 区分自然退出与后端主动停止,定位无 stderr 的 FFmpeg EOF。 */
1232+
if (waited == process && WIFEXITED(status)) {
1233+
fprintf(stderr,
1234+
"actions-micro: ffmpeg process %s pid=%ld exit=%d\n",
1235+
parent_stopped ? "stopped" : "exited",
1236+
(long)process, WEXITSTATUS(status));
1237+
} else if (waited == process && WIFSIGNALED(status)) {
1238+
fprintf(stderr,
1239+
"actions-micro: ffmpeg process %s pid=%ld signal=%d\n",
1240+
parent_stopped ? "stopped" : "exited",
1241+
(long)process, WTERMSIG(status));
1242+
} else if (waited < 0 && errno != ECHILD) {
1243+
fprintf(stderr,
1244+
"actions-micro: ffmpeg waitpid failed pid=%ld errno=%d (%s)\n",
1245+
(long)process, errno, strerror(errno));
1246+
}
11691247
encoder->process = -1;
11701248
}
11711249
encoder->format = USBDISPLAY_FORMAT_INVALID;
@@ -1265,6 +1343,9 @@ static int start_encoder(struct actions_context *state, uint32_t format)
12651343
result = -errno;
12661344
} else {
12671345
state->encoder.format = format;
1346+
/* 新编码器首帧必须重新交接参数集和 IDR,但 USB 序号保持连续。 */
1347+
++state->encoder.generation;
1348+
state->encoder_handoff_pending = true;
12681349
}
12691350
}
12701351
if (input_pipe[1] >= 0) {
@@ -1461,7 +1542,8 @@ static int append_annex_b_nal(struct byte_buffer *buffer,
14611542

14621543
static int split_access_unit(const unsigned char *data, size_t length,
14631544
struct byte_buffer *configuration,
1464-
struct byte_buffer *video, bool *idr)
1545+
struct byte_buffer *video, bool *idr,
1546+
struct nal_summary *summary)
14651547
{
14661548
size_t start_length;
14671549
size_t next_start_length = 0;
@@ -1471,6 +1553,7 @@ static int split_access_unit(const unsigned char *data, size_t length,
14711553
int result = 0;
14721554

14731555
*idr = false;
1556+
memset(summary, 0, sizeof(*summary));
14741557
while (start < length && result == 0) {
14751558
next = find_start_code(data, length, start + start_length + 1,
14761559
&next_start_length);
@@ -1479,11 +1562,17 @@ static int split_access_unit(const unsigned char *data, size_t length,
14791562
break;
14801563
}
14811564
nal_type = data[start + start_length] & 0x1fU;
1565+
++summary->count;
1566+
summary->aud = summary->aud || nal_type == 9;
1567+
summary->sps = summary->sps || nal_type == 7;
1568+
summary->pps = summary->pps || nal_type == 8;
1569+
summary->idr = summary->idr || nal_type == 5;
14821570
if (nal_type == 6 || nal_type == 7 || nal_type == 8) {
14831571
result = append_annex_b_nal(configuration, data + start,
14841572
next - start,
14851573
start_length);
14861574
} else if (nal_type != 9) {
1575+
/* 授权 replay 的固件序列不含 AUD,继续只传参数集和图像 NAL。 */
14871576
result = append_annex_b_nal(video, data + start,
14881577
next - start,
14891578
start_length);
@@ -1571,15 +1660,59 @@ static int send_video_message(struct actions_context *state,
15711660
return result;
15721661
}
15731662

1663+
/* 单次编码尝试只处理 FFmpeg 和 NAL 拆分,USB/HID 发送必须在成功后执行。 */
1664+
static int encode_frame_once(struct actions_context *state,
1665+
const struct usbdisplay_frame *frame,
1666+
size_t packed_bytes,
1667+
struct byte_buffer *configuration,
1668+
struct byte_buffer *video, bool *idr,
1669+
struct nal_summary *summary)
1670+
{
1671+
size_t encoded_bytes = 0;
1672+
int result = 0;
1673+
1674+
if (state->encoder.format != frame->format) {
1675+
result = start_encoder(state, frame->format);
1676+
}
1677+
if (result == 0) {
1678+
result = write_all_logged(state->encoder.input_fd,
1679+
state->encoder.packed_frame, packed_bytes,
1680+
"ffmpeg input pipe");
1681+
}
1682+
if (result == 0) {
1683+
result = read_encoded_access_unit(state, &encoded_bytes);
1684+
}
1685+
if (result == 0) {
1686+
result = split_access_unit(state->encoder.encoded, encoded_bytes,
1687+
configuration, video, idr, summary);
1688+
}
1689+
/* 编码器重建后的首帧缺参数集或 IDR 时不得交给仍存活的固件会话。 */
1690+
if (result == 0 && state->encoder_handoff_pending &&
1691+
(!summary->sps || !summary->pps || !summary->idr)) {
1692+
fprintf(stderr,
1693+
"actions-micro: incomplete encoder handoff generation=%llu "
1694+
"nal-count=%u aud=%u sps=%u pps=%u idr=%u\n",
1695+
(unsigned long long)state->encoder.generation,
1696+
summary->count, summary->aud ? 1U : 0U,
1697+
summary->sps ? 1U : 0U, summary->pps ? 1U : 0U,
1698+
summary->idr ? 1U : 0U);
1699+
result = -EPROTO;
1700+
}
1701+
1702+
return result;
1703+
}
1704+
15741705
static int actions_submit(void *context,
15751706
const struct usbdisplay_frame *frame)
15761707
{
15771708
struct actions_context *state = context;
15781709
struct byte_buffer configuration = {0};
15791710
struct byte_buffer video = {0};
1711+
struct nal_summary summary = {0};
15801712
size_t packed_bytes = 0;
1581-
size_t encoded_bytes = 0;
15821713
bool idr = false;
1714+
bool encoded = false;
1715+
unsigned int attempt = 0;
15831716
int result = 0;
15841717

15851718
if (frame->width != state->width || frame->height != state->height) {
@@ -1588,23 +1721,32 @@ static int actions_submit(void *context,
15881721
if (result == 0) {
15891722
result = send_due_heartbeats(state, monotonic_nanoseconds());
15901723
}
1591-
if (result == 0 && state->encoder.format != frame->format) {
1592-
result = start_encoder(state, frame->format);
1593-
}
15941724
if (result == 0) {
15951725
result = pack_frame(state, frame, &packed_bytes);
15961726
}
1597-
if (result == 0) {
1598-
result = write_all_logged(state->encoder.input_fd,
1599-
state->encoder.packed_frame, packed_bytes,
1600-
"ffmpeg input pipe");
1601-
}
1602-
if (result == 0) {
1603-
result = read_encoded_access_unit(state, &encoded_bytes);
1604-
}
1605-
if (result == 0) {
1606-
result = split_access_unit(state->encoder.encoded, encoded_bytes,
1607-
&configuration, &video, &idr);
1727+
/* 编码器 EOF 不等于 USB 会话丢失;原帧最多本地重建并重试一次。 */
1728+
while (result == 0 && !encoded && attempt < MAX_ENCODER_ATTEMPTS) {
1729+
configuration.length = 0;
1730+
video.length = 0;
1731+
idr = false;
1732+
memset(&summary, 0, sizeof(summary));
1733+
result = encode_frame_once(state, frame, packed_bytes,
1734+
&configuration, &video, &idr, &summary);
1735+
++attempt;
1736+
if (result == 0) {
1737+
encoded = true;
1738+
} else if (attempt < MAX_ENCODER_ATTEMPTS) {
1739+
++state->encoder_restarts;
1740+
fprintf(stderr,
1741+
"actions-micro: encoder local recovery attempt=%u "
1742+
"generation=%llu frames=%llu video-sequence=%u error=%s\n",
1743+
attempt,
1744+
(unsigned long long)state->encoder.generation,
1745+
(unsigned long long)state->frames,
1746+
state->video_sequence, strerror(-result));
1747+
stop_encoder(&state->encoder);
1748+
result = 0;
1749+
}
16081750
}
16091751
if (result == 0 && configuration.length != 0) {
16101752
result = send_video_message(state, VIDEO_CONFIG,
@@ -1618,6 +1760,18 @@ static int actions_submit(void *context,
16181760
result = send_video_message(state, idr ? VIDEO_IDR : VIDEO_P,
16191761
video.data, video.length);
16201762
}
1763+
if (result == 0 && state->encoder_handoff_pending) {
1764+
/* replay 首段无 AUD;日志保留源 AUD 与实际参数集/IDR 交接证据。 */
1765+
fprintf(stderr,
1766+
"actions-micro: encoder handoff complete generation=%llu "
1767+
"nal-count=%u source-aud=%u sps=%u pps=%u idr=%u "
1768+
"video-sequence=%u\n",
1769+
(unsigned long long)state->encoder.generation,
1770+
summary.count, summary.aud ? 1U : 0U,
1771+
summary.sps ? 1U : 0U, summary.pps ? 1U : 0U,
1772+
summary.idr ? 1U : 0U, state->video_sequence - 1U);
1773+
state->encoder_handoff_pending = false;
1774+
}
16211775
if (result == 0) {
16221776
++state->frames;
16231777
}
@@ -1668,12 +1822,18 @@ static void actions_close(void *context)
16681822
close_hidraw_descriptors(state);
16691823
if (state->frames != 0 || state->reports != 0 ||
16701824
state->input_reports != 0) {
1825+
/* 关闭摘要同时区分输入端点与本地编码恢复,不再混同 USB 重开。 */
16711826
fprintf(stderr,
16721827
"actions-micro: closed frames=%llu reports=%llu "
1673-
"input-reports=%llu\n",
1828+
"input-reports=%llu input0=%llu input1=%llu "
1829+
"encoder-generation=%llu encoder-restarts=%llu\n",
16741830
(unsigned long long)state->frames,
16751831
(unsigned long long)state->reports,
1676-
(unsigned long long)state->input_reports);
1832+
(unsigned long long)state->input_reports,
1833+
(unsigned long long)state->input_reports_by_endpoint[0],
1834+
(unsigned long long)state->input_reports_by_endpoint[1],
1835+
(unsigned long long)state->encoder.generation,
1836+
(unsigned long long)state->encoder_restarts);
16771837
}
16781838
free(state->encoder.encoded);
16791839
free(state->encoder.packed_frame);

0 commit comments

Comments
 (0)