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
130140struct 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+
136155static 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,也不等同物理拔插 。 */
304323static 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 轮询两个输入接口并排空缓存,避免固件状态报告在环形缓冲区积压。 */
10351075static 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
11501198static 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
14621543static 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+
15741705static 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