本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 5 y0 ^" {2 Q5 d5 w; G$ y( V: t! A" a, c
: S1 C8 K: I9 Q: T

" e/ ~5 |. a% \% g+ W$ [# f' s作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html/ j. F0 x* `! e& Q m1 a: [" i
; w9 a8 M( y/ s, F% W3 i, b
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 5 S: w: _) K, N5 u) k4 @$ H% {
 $ w2 I' B) k7 V
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 8 B7 d' O8 D- {
一、HTTP网页服务器
/ d* Z8 n; X/ H4 a7 ]% G先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port). ~# d! `- |. x3 J L" Y7 Z
- { x; A- |2 e. D* I j9 ~
- struct sockaddr_in servaddr;" b) J: _: G8 B- D* e& X$ l# g' i
- socklen_t addrsize = sizeof(struct sockaddr);
' M2 {9 |( Q: d+ a2 U+ O7 r - bzero(&servaddr , sizeof(servaddr));
+ ?4 Z# I9 C4 I" b - servaddr.sin_family = AF_INET;$ B0 e7 B: u- B9 u1 B
- servaddr.sin_addr.s_addr = inet_addr(ip);( @ w0 H( K# E2 j i9 r9 u A
- servaddr.sin_port = htons(port);. t( G+ E9 }9 K+ v; c
- int ret;
: z( e: k& ?* |8 D - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1). d9 H s9 O2 a, n# d) B* n
- {
: y) a! Y7 K8 O% u - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
4 j. r' J( L4 ]' g' B i# F - return -1;; L3 ^3 z# D' u, I
- }
; P' r+ ]. X# U9 ?7 s G' r' l% _ - int on = 1;
* x" Y0 T: F, g2 z - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
| O5 k! D4 @* L! u* {8 r - {
2 E8 J( { e9 Y6 L - printf("setsockopt error\n");
: N3 }, ?" s1 W0 v5 D - }$ X0 J) Q9 t, X7 o
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
: d) y; g$ c. i8 o( U - if(ret == -1)
- T- { G/ ?0 L5 {- F0 E - {
% P E: G& V0 t2 `$ Z5 N - printf("Tcp bind faiLED!\n");
: ?1 X: q, h& u- N6 G - return -1;
9 l5 |6 I6 v" h1 D - }
" z+ {2 l! I4 A" ~( Y" a* M/ ~ - if(listen(*socket_found , 5) == -1)
9 P8 L& q% l# D$ n/ N - {
0 t3 n- J% B$ S0 v - printf("Listen failed!\n");8 A! [; J% _8 \
- return -1;
$ j, h& W; v% v4 v* D( |' d) _! } - }
! K8 w9 | i" r5 l, Q - return 0;; U' W# O7 ]0 o2 M8 F ?
- }
复制代码其中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);6 {- k4 O" s. i
- void * Thread_TCP_Web_Recv(void *arg)
3 _5 c: S1 M4 x6 F9 x! z - {# Q2 R& I: \# H" C1 V! o
- 。。。* ]! L% ], z. J- @
- while(1)
: v# q6 {# P6 K) v# c- _ - {
) B0 X1 x, I/ M/ c9 s - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
: J8 ~0 n- d5 E7 I e - printf("fd_socket_conn = accept()\n");: c* `6 `& r2 w1 _" s, B
- 。。。
5 B; z0 n# @4 H h8 a* c% R - recv(fd_socket_conn , recvbuf , 1000 , 0);
# l9 D& F: m% P: f9 s! _ - }8 v$ g U; P1 L) x
- 。。。
2 J# Y! c" B$ f# Q7 ^# G - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);! v7 @4 E3 P S! [/ j
- pic_tmpbuffer = pic.tmpbuffer;
7 q6 l8 G0 A$ | - pic.tmpbytesused = buff.bytesused;; B$ i, T: {. q5 c9 v# G( G9 E
- pic_tmpbytesused = pic.tmpbytesused;
, b; p# r- |2 ?* O* s! R - pthread_cond_broadcast(&pct);
" r6 ~8 W7 L$ K - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
# J, b. k. m5 C4 t% v/ ? - pthread_cond_t pct;
5 i! F0 B. [' \+ A6 W - int main(int argc, char* argv[])" F6 M( j/ B% @( y
- {. P2 T1 y' [* a. ?! G2 o; M
- ..., r- K6 [' Z" q' t. |
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);' o, j1 b* ~/ D% E1 i
- pthread_mutex_init(&pmt , NULL);
0 G4 P& c4 Q% {: o. F; x* r" i - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
! \/ [% H7 P# O- w; ` - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
; M, @& ]; d+ {3 x9 D$ h - ...! T# t+ ^. e7 t/ W$ w
- while(1)
8 u4 d! S; T7 o9 H - {
# t& t* }0 V* X- j" z d- ^" W - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
( R0 p0 Y/ f$ d' e* s - ...
+ a8 ?4 r8 j" J9 [0 p - }, o, s7 m( u b M9 O* C& Z* W
- ...8 |$ Q; K/ V1 `4 j. A4 ]5 U8 B
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
7 L% D! J) r' a% h# c p - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
1 ]/ g9 s i% w# M - "Server: MJPG-Streamer/0.2\r\n" \
! u, \: g4 o( c' v. ?: k - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \1 s4 g8 m4 I0 c, Y) f' o0 G* d
- "Pragma: no-cache\r\n" \& ^7 V. b5 W& d
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"/ `# E o1 a N, S; R4 r4 l4 b% y, q
- #define BOUNDARY "boundarydonotcross"7 g# k* U4 c) K; B
- printf("preparing header\n");# t, l# N- [3 k, j+ `/ y
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \" |- L5 l$ K5 O) N0 b
- "Access-Control-Allow-Origin: *\r\n" \! p) A" d. s/ Z4 b" k
- STD_HEADER \7 M: W% s6 q: h9 u& B
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
3 L, T" N1 d1 C" k - "\r\n" \
& Z+ B; _! `+ N; V - "--" BOUNDARY "\r\n");& b! Y7 b6 I4 s$ C" d
- if(write(fd, buffer, strlen(buffer)) < 0)* Z2 |$ ]- e `0 X; H' c
- {
: e) Q `& G% }% U* D& H V - free(frame);
$ q: J0 |+ V' N0 T - return;# S" K g7 T$ X- g2 R! j* w1 V
- }
复制代码发送完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" \
# s5 Z" S# {7 K7 b - "Content-Length: %d\r\n" \
! f- c( b6 B0 v; w* ~+ M2 R - "X-Timestamp: %d.%06d\r\n" \1 }* P6 V4 m& c2 r* n- ]
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec); g) b5 d% s) C9 W; M0 B% |& Q
- printf("sending intemdiate header\n");( G- t2 ]5 e' S3 x
- if(write(fd, buffer, strlen(buffer)) < 0)% j4 ~8 n% T5 Q5 @. F/ R! k" {
- break;; i, J' L3 n, h7 b* S; h) b
- printf("sending frame\n");
8 l1 q6 I2 M8 u& y - if(write(fd, frame, frame_size) < 0)
1 j' \0 d) b3 w B: ? - break;7 W [ T- D4 Y) k
- printf("sending boundary\n");7 n9 c7 m( r; ~/ \( v
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
9 D0 A- H" N4 t v" m! P - if(write(fd, buffer, strlen(buffer)) < 0)& ~% P' J) S! N+ n0 t, N
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
4 I7 \$ P! V0 J, U% r' w
) @1 [* n4 I3 `2 W$ \* W二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:8 T* J! ~% y4 K. v
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
9 z R; Q; B. ~9 J, T+ Y* t6 r - {* N5 K8 ]$ N- ^: Y2 u# S, i: `# y
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);* M0 @% h% v7 F5 P5 a4 q
- if(*socket_found == (~0))* J' m0 x- _' M* r5 R7 z: B
- {
$ e3 o3 o: b6 ^+ X A) r5 |1 q8 Q - printf("Create udp send socket failed!\n");
8 J& @$ [) f; m6 l$ s0 C4 Y - return -1;
1 D5 I& J6 X. W6 H7 v. y - }
9 w$ X3 _- [3 a4 n$ \/ i - addr->sin_family = AF_INET;
2 P) S- ~1 s" H/ \2 Y" @ - addr->sin_addr.s_addr = inet_addr(ip);
% d* i4 N3 r. G5 }- x' ` - addr->sin_port = htons(port);
6 S7 c! c j1 U - memset(addr->sin_zero, 0, 8);
( ^" T3 u! e. N6 Z9 o9 E0 F$ L4 t - return 0;3 K4 ^( D+ v+ q
- }
复制代码 / r2 `4 {' @$ \3 a# ~
3 [/ l/ C0 M- f5 g# f/ U
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
# |/ a/ |& p8 I7 O0 T/ h8 ]' _( o% s R8 K+ \; g2 Z
6 ~/ X. L3 M* ~7 _# A7 c0 |- while(fend > 0)0 k k" F2 _8 ?& Y9 o4 o) z
- {6 Q b5 k! F+ p) Y
- memset(picture.data , 0 , sizeof(picture.data));
" R+ E& I5 h. _' a- t - fread(picture.data , UDP_FRAME_LEN , 1, fp);
" ?5 H- q) ?/ q0 y4 ~0 c% N - if(fend >= UDP_FRAME_LEN)
: p7 p& F6 r; g' q - {6 C3 U4 p/ s/ B: S, k) `
- picture.length = UDP_FRAME_LEN;
5 x) _1 |7 e' ^! i- f+ Q' P4 b9 h - picture.fin = 0;! q: p* p. ~1 q! v! a5 Z! D' S
- }
2 q* X& d( }/ ~- }3 f - else7 u; K5 ?* m& p \+ ]" S! Q) y% }/ l) z
- {1 u) ?" m5 h" U, O- o
- picture.length = fend;: S Q9 s ^9 M, P- j
- picture.fin = 1;/ j9 V& Q7 s, |3 T
- }
- H8 v, F& p! m5 A0 v4 [ - //printf("sendbytes = %d \n",sendbytes);( m. z9 d' q6 v( q/ n
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
3 `4 g& y+ I; W# T2 T' E0 t* u - if(sendbytes == -1)
, B6 t9 C4 g7 r' x2 E4 ] - {, E( _9 t5 {' c, T" N, i% x1 |
- printf("Send Picture Failed!d\n");
! B2 `" |+ A3 D% T - return -1;) v; h$ S. c! N( y+ g
- }' B$ B+ n6 r K H
- else
. \& Z% v' R: g9 {9 o+ L" F* b; r - {
$ h% J4 Q# x. I( e( S i' S - fend -= UDP_FRAME_LEN;
* v* o) J- r. U& Q4 R7 | - } x |/ U- L* b& }! Z; t
- }
复制代码
9 v3 t! P/ V' D0 R. R; ~/ p7 ~
0 a* Q }+ ~( p" I& X, V 3 }/ Q6 B, o( d
4 Z% |$ x' v. B3 o
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |