本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
# G" c: u0 {' x S/ M0 B; W8 P
9 c" P5 }9 u% M6 }+ p( \
N: ?: T' E! U$ H* p; g9 x6 |+ |作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html8 F* |5 U$ Z! `2 U, A
. ^$ K5 s- E: A7 m, u) K
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
1 t/ R! b' Q3 \8 v4 A 1 F) v) Q# E# L0 ~3 y" C
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
) J) M( K( i% v一、HTTP网页服务器& ~) U( R9 n, R3 a( A4 [( j& h# K7 F
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
k+ i% e# U& [8 h5 m - {
( R4 N% K! y" q& i5 `* _5 M - struct sockaddr_in servaddr;
" S; x8 T* S. o - socklen_t addrsize = sizeof(struct sockaddr);8 F( {' V: @; t' E, Y8 ?
- bzero(&servaddr , sizeof(servaddr));
, C! z/ _7 S( a( u1 t - servaddr.sin_family = AF_INET;1 u9 p1 `' b) J6 T
- servaddr.sin_addr.s_addr = inet_addr(ip);8 ^3 }% B. X* d2 p5 ~; f
- servaddr.sin_port = htons(port);" i- L' w4 L3 x$ h% `4 x
- int ret;" p7 c/ Z7 l! S+ g) H* ^
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
/ F- S( `; z/ C3 S ~1 L - {
% ~) f r2 E) g! L - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
( \) {/ K7 S" ^) f7 y5 T: t8 T6 `5 v - return -1;
6 y4 ?! v( m0 x$ s( J - }
. K; B* k6 J3 J2 g6 t; R% M; w - int on = 1;8 a; x5 v/ a& z/ e
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
/ u: ]/ X) T/ @2 q N - {& H4 m* ]- X: v$ Z% L3 r* Z
- printf("setsockopt error\n");
3 T( f; p2 U# a - }
( B) J7 ]0 G) K0 ^+ e, H0 h - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);% V" ^4 h& e- m$ ~, p. n
- if(ret == -1)* \5 B, @: r# v
- {" r4 W' b3 U) ]+ ~8 f8 p V2 Q
- printf("Tcp bind faiLED!\n");0 A: z9 Y0 C) ?5 {2 r0 r# m
- return -1;
7 ~1 j8 W/ |$ j9 | `* |( ~ - }
4 h! N* y8 ]* U8 f0 P - if(listen(*socket_found , 5) == -1)
% n% M# J3 s x! r - {
2 q5 ]% _7 I' @2 r6 j - printf("Listen failed!\n");' _. W9 k3 b4 ]' X
- return -1;3 ]7 t { ]) z" C$ n; L) z' C3 }
- }: }- k5 F9 [4 L8 u3 _
- return 0;, W( _: ~ N" f) r$ Y7 v1 R2 r
- }
复制代码其中setsockopt()函数是可选的,一般只用于规避socket()函数的建立错误。 建立了TCP服务器后,返回的socklen_t型实参在后面的HTTP网页服务器中需要用到。 HTTP网页服务器所属的TCP操作是需要另起轮询线程来让客户端进行accept()握手操作的,accept()之前的listen()倒是只需要执行一次即可,accept()握手操作和recv()接收操作需要创建一个死循环线程: - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
8 ]3 y i& O/ m9 h - void * Thread_TCP_Web_Recv(void *arg)
! ]$ D( G3 R Y1 x# P# p - {
. \' M& X! G; ^; S: j! z - 。。。
?$ @& ~! t; W( r7 T% `/ r - while(1)
- {+ [1 v" Y1 @4 L. p' } - {4 o. e5 X) @4 Z6 ~
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
. [6 I7 a- h, O6 A. D! z9 L* X - printf("fd_socket_conn = accept()\n");
) p& C4 d# ? i& ^1 W1 Q6 i - 。。。, K3 B9 { r! Z' j% E
- recv(fd_socket_conn , recvbuf , 1000 , 0);3 ?( c/ b8 n6 M( n6 H! {6 M
- }
- s# T; Q8 J( A. g/ M/ g- { - 。。。) q: z/ w3 {7 J" ?0 T+ y
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
1 W/ c" M2 {1 a5 C - pic_tmpbuffer = pic.tmpbuffer;
* M+ p$ @) i5 U& d! N - pic.tmpbytesused = buff.bytesused;
2 X. ~5 u* B8 P w: } - pic_tmpbytesused = pic.tmpbytesused;5 K+ i; ^1 s9 x2 d. k0 V
- pthread_cond_broadcast(&pct);
. B/ [' N5 c6 s - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
4 c1 |. [$ I* U - pthread_cond_t pct;( i6 H5 Q5 L9 G; i5 q% k
- int main(int argc, char* argv[])6 A1 _ @5 U5 {3 e o9 Y7 u; o' e
- {
4 d) @$ |) \9 a ` - .../ s3 i8 [ E7 |; V
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
+ U y5 c0 o1 s - pthread_mutex_init(&pmt , NULL);
; D& ~' e" w& V3 R9 U( M$ K9 y' y - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
% r- G0 l# J5 M$ `( h6 G+ U - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
- d7 b! S! k& f: S/ o: T9 z - ...
3 G0 K/ w9 o" u5 c2 R - while(1)
0 y. g+ X. @8 u; ~: M3 z - {
0 k: l, }5 z' U0 p! E - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
3 c$ S" o6 h5 ]5 y4 c& S2 n - ..., M8 `& p- d8 q% I l/ c
- }9 U. f) [8 `3 }$ `6 O* i; K6 j% o
- ...0 f% `! V! H; E* L
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">0 o* [6 [6 N) H7 i7 V& k
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
. n: H3 [/ r# ]: L h5 D( X - "Server: MJPG-Streamer/0.2\r\n" \6 `! M' v) ]* h: ]* o
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \: o- s8 b: [) @
- "Pragma: no-cache\r\n" \
1 G0 [; b+ M* m$ }6 S- r - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
: x' Y4 \, K9 f l% } - #define BOUNDARY "boundarydonotcross"6 C8 A0 D8 M# T) ]4 y ]1 R, e- }
- printf("preparing header\n");; Y. z; a8 A4 c* t9 Y
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \4 l$ y" }0 u" |: f8 C9 j
- "Access-Control-Allow-Origin: *\r\n" \
4 q$ X* o& l4 K2 U0 m4 J7 f - STD_HEADER \( _; U" F+ Q$ X6 B9 O# ?* u
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
4 I8 ?( }' k! G* U/ V9 ~8 R# |$ i0 N - "\r\n" \2 Z: b9 N" C. m0 V" u/ o! s2 P
- "--" BOUNDARY "\r\n");9 v G3 e, {( b# s5 U3 o
- if(write(fd, buffer, strlen(buffer)) < 0)) a6 T4 M$ R% T9 L0 M7 q
- {
/ Q8 c( U. r6 w, I3 h - free(frame);
+ J' q& I1 c+ K+ E, w) d0 V: ~ - return;
( }* R- B9 L& f' Y$ K" O - }
复制代码发送完HTTP标准头之后,就需要发送内容头(Content-Type),这处的Content-Type为image/jpeg,同样,HTTP标准协议里面image支持的类型远不止jpeg一种,发送完内容头之后就是正文和boundary结尾,这样帧完整的HTTP头发送到指定的TCP GET地址,就会在浏览器中显示刚刚发送的图片: - <pre class="prettyprint lang-cpp" style="box-sizing: border-box; font-family: Monaco, Menlo, Consolas, "Courier New", monospace; font-size: 16px; white-space: pre-wrap; line-height: 1.38462; color: rgb(51, 51, 51); word-break: break-all; background-color: rgb(245, 245, 245); border: 0px; border-radius: 4px; vertical-align: baseline;"> sprintf(buffer, "Content-Type: image/jpeg\r\n" \6 a4 B; t( }3 q* ?
- "Content-Length: %d\r\n" \6 u5 ]2 p5 j ~/ y$ Y
- "X-Timestamp: %d.%06d\r\n" \4 ~+ r! R% `2 l; ^$ v2 ?
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);/ V2 ?/ V0 m) N
- printf("sending intemdiate header\n");& }4 e- L% z" Q1 K
- if(write(fd, buffer, strlen(buffer)) < 0)
8 R k& K* \- A! H6 u( N2 j - break;
- A& N7 Z9 b6 k4 j+ A2 o: q - printf("sending frame\n");
0 t( L+ A9 J) U% S0 I - if(write(fd, frame, frame_size) < 0)
/ x% o# B) }& R) p2 i - break;" q& K$ ]* N; ?1 b
- printf("sending boundary\n");
7 L# v/ m3 V" u3 v1 s - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");4 @8 ]+ B- g5 n3 g' ~
- if(write(fd, buffer, strlen(buffer)) < 0)
# k! s3 w, m' o3 K - break;</pre><p style="box-sizing: border-box; border: 0px; font-size: 16px; vertical-align: baseline; line-height: 26px; color: rgb(51, 51, 51); font-family: " helvetica="" neue",="" helvetica,="" tahoma,="" arial,="" "microsoft="" yahei",="" "hiragino="" sans="" gb",="" "wenquanyi="" micro="" hei",="" sans-serif;"=""></p>
复制代码另外需要说明的是,TCP服务器线程在发送MJPEG流的时候是死循环发送的,因此TCP客户端在发送完GET指令之后,就会收到TCP服务器循环发送的图像缓存,TCP客户端会陷入忙等待状态无法再对外发送任何GET或者POST指令,从客户端使用者角度来看的效果就是网页一直在等待。  & Q( O, e! E$ g$ y6 B

+ b' \5 ?% ^$ Y. m二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
, _( Z Q$ a3 O4 i5 @# J/ g- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)* @. {" g6 I, K9 U) P
- {
- K8 z1 w% ^% B, k - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
. Y3 [8 r/ t- x) x* L - if(*socket_found == (~0))9 o+ w( i- m2 K4 Z( ]. ]
- {
$ M3 {3 L$ Y3 q3 T+ N3 J - printf("Create udp send socket failed!\n");" p4 N m6 t- W$ w
- return -1;) c: P: a3 K5 P7 F
- }
) s2 B g- O8 R/ _' C - addr->sin_family = AF_INET;
/ G: ]5 e u$ A5 h( e - addr->sin_addr.s_addr = inet_addr(ip);
& o% V8 Z6 L3 ]8 P' T - addr->sin_port = htons(port);) @, ~: o5 h: U+ l1 w- L
- memset(addr->sin_zero, 0, 8);
! ~8 r) H o* Q2 d" a - return 0;0 {( i$ \, T5 T4 R2 R6 d9 y2 L( O
- }
复制代码
* b% N! k2 _- D* Q' j, Z% l0 A' a. `2 s. J4 k0 z! `
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:9 f I& ~3 B0 t: ?/ @) ~9 V
* Q6 h0 q8 I' @) z; R
: q. ?7 ] k U
- while(fend > 0)& E5 g* ^ T% w9 r/ ^, |! B# U
- {. Z" |& v) O. V0 L U& r2 T
- memset(picture.data , 0 , sizeof(picture.data)); b& }1 P: N3 k% L `* Q) {
- fread(picture.data , UDP_FRAME_LEN , 1, fp);9 E0 e: t7 ~/ Q1 j
- if(fend >= UDP_FRAME_LEN)1 `$ b* i+ G+ D) O; i+ d8 N
- {; j; {% L7 [7 S# e \
- picture.length = UDP_FRAME_LEN;
! @6 u" R" y$ s2 T - picture.fin = 0;' {/ a" k$ s& c: A( U2 K
- }1 d1 w4 t, M f0 b3 i( k
- else
& z" o0 n t! `0 b7 P - {: x( r7 ?1 `3 ~+ C1 J$ }
- picture.length = fend;' Q$ g2 s9 n0 `2 q& ?* y* E% Z
- picture.fin = 1;, G% ?' ]& l5 X2 W8 M( n! ]
- }
: z8 S6 h- @6 n. N; t. p9 [3 S - //printf("sendbytes = %d \n",sendbytes);
6 f; H5 n; A7 y - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
' ]6 c" Y& E$ l - if(sendbytes == -1)* Y7 L, R5 t) R' M& v
- {. O0 l) n- ^( `' E# E/ x2 z6 u; l
- printf("Send Picture Failed!d\n");, U: ]3 \2 \. [2 | b# j' y
- return -1;
$ Z( n2 O' n! r# _7 o1 v - }
/ F: `' z3 b9 i - else
) H$ m9 q4 t. S8 r+ X( Z2 ` - {# ] \! A& z* N; g
- fend -= UDP_FRAME_LEN;
W6 G$ r! V( q. ` - }6 W" e5 B! U- X2 ~) t" O
- }
复制代码
$ Y& V+ k1 ~% Y* g* q* k9 _9 g$ i4 G, G M, z5 I% A9 [/ `9 t9 V

6 _5 q$ Z7 `$ c/ ~% \. B
p, |* r, j1 V4 biMX8MPlus 核心板: https://www.forlinx.com/product/136.html |