本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 ) s! ]) B% F0 ]
0 r2 n& I u/ v

. L% P, |. }1 t K作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html4 r5 s3 E& h8 g- T. U* C" o
0 G* h1 G2 G* S% X* p
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
. X6 W+ n1 B! J7 U7 \
* q3 a5 N0 v, s& c# M1 {两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
; \ M: Q4 M# u% l" U# r' q一、HTTP网页服务器
" a9 ?) b8 m! w. g! p3 u1 c先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)3 d( c1 N$ W3 [+ f$ Z
- {
9 b0 l4 ^, K0 T7 F& O - struct sockaddr_in servaddr;
% \3 i0 ?) H! X0 X3 A, d" E( [ - socklen_t addrsize = sizeof(struct sockaddr);
' d2 d1 p4 e! v2 _ - bzero(&servaddr , sizeof(servaddr));' q' E: w. I0 T1 q: V
- servaddr.sin_family = AF_INET;, w$ g& D1 |$ ], Y; w) m* V
- servaddr.sin_addr.s_addr = inet_addr(ip);
. s4 n. {1 s3 N2 B4 o - servaddr.sin_port = htons(port);& X. k" r& K. ?& Y2 I
- int ret;
1 ]& n- Q3 k, f, A1 P9 V! g - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)2 x1 \$ k5 {4 s! E; V
- {$ ]- Y6 \) w9 Y: b+ T2 a; D' V' z
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);" u" r: r- A o8 e1 z6 B
- return -1;
# Z `* S9 K, g/ z) Q1 a - }
9 d0 ^9 b e9 r# g - int on = 1;
0 Y' t; B$ } r& e {7 o - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)$ l5 {9 D6 K! V+ M5 d; l$ b" O! U3 L
- {
: l- ^# C7 M$ O& m* Z2 l7 F) _6 _ - printf("setsockopt error\n");
- t/ C% I4 I0 ?' r- n3 w - }
# Q; |$ v4 }6 y; n - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
" m J4 C! s+ U - if(ret == -1)+ J, \0 }3 p1 y! I, P
- {( |5 Q2 K3 P% \& t& K+ Q3 a
- printf("Tcp bind faiLED!\n");: x) r& h6 m+ y% B2 z! B W
- return -1;
$ j9 F; [- l* ~" W - }2 l+ O! A9 A" G g
- if(listen(*socket_found , 5) == -1)3 m$ h( ?, l/ O
- {
# N. @- l6 j0 s2 F0 {2 ?. i$ G - printf("Listen failed!\n");
7 @5 `8 b6 Z. G% Q# P4 g - return -1;
5 W3 U; b0 Q$ E6 s - }
+ g5 i# ~7 c' }* u! A' d - return 0;9 r8 h9 t' [. m: L4 B! L6 U5 o
- }
复制代码其中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);
# T# I5 f# A$ q! I - void * Thread_TCP_Web_Recv(void *arg). e: i0 V* ^8 Y( X+ n( _
- {7 F; T4 ~9 b% e& ^- b& o
- 。。。
7 W- ]( ^0 s* U5 ^5 G$ A - while(1)
6 v& _3 b+ {, |& J8 ?* y - {- l" \5 ^* B& `3 b7 c3 M
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
; G# W- J+ C' m - printf("fd_socket_conn = accept()\n");
l9 |* G( _1 i2 t) p' g+ q+ f - 。。。
% c) {; ]2 a4 I* R4 r - recv(fd_socket_conn , recvbuf , 1000 , 0);
/ ^ h" B1 M4 k, c' W% F. Z4 ` - }1 j r4 Q- }. K* H5 k
- 。。。: f6 @! M3 M" Z( f% l0 |; ]- l6 T
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);. Q1 @8 U9 U+ r; R4 I+ c0 j
- pic_tmpbuffer = pic.tmpbuffer;
+ S9 d/ x6 @6 }! k: l - pic.tmpbytesused = buff.bytesused;
2 q6 c" U* _/ {6 H Q( S, f - pic_tmpbytesused = pic.tmpbytesused;
8 V8 O% H' [2 W; {& G7 U - pthread_cond_broadcast(&pct);
0 _0 r9 _, t8 t* s7 X1 G - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
+ C% x, W6 u( V3 w - pthread_cond_t pct;
$ `# i0 J2 [- {3 F1 A* ]8 Y - int main(int argc, char* argv[])
( q3 I) e' x; f: ^. X! \ - {
( t$ G' B' }0 P( G0 ? - ...1 U; j: o$ \" v7 D
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
1 I0 _3 y- C' y I: E+ c8 O - pthread_mutex_init(&pmt , NULL);
+ ~+ r" l; c9 p& V" j* z0 l; o) l - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
% \4 }/ d. o# q1 S$ J - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
. I% P5 Y) b- u$ k4 d- S' H - ...5 x7 Y3 U( b- \. t8 k
- while(1)2 [$ R: U& M, H* s
- {5 a' J M# O( d' H) t1 p
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);8 ^. z7 k6 m# k3 ]( A0 y& `
- ..., _0 N! q2 A2 A D2 W6 N
- }
) m+ N$ A5 u9 ~6 v ^1 A, l( z! e - ...2 s( n1 v+ Y+ Z. s$ @1 b- x
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
. f$ O$ h4 ~% e/ j - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \2 }8 \* z6 p( _- v
- "Server: MJPG-Streamer/0.2\r\n" \
+ |3 h5 U$ W w4 D9 t/ P - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
* J( L( D1 `+ J- {) [1 B$ S1 k6 W - "Pragma: no-cache\r\n" \ f; Z ` G0 l g' g d+ U
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"5 E1 i; |+ Z. v
- #define BOUNDARY "boundarydonotcross" q# G0 I8 i$ Y1 H) Y1 p' O
- printf("preparing header\n");
3 Z4 b4 `0 ?; c A' `/ C8 o1 v - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \& ~( D6 t% r9 a" `# e- y
- "Access-Control-Allow-Origin: *\r\n" \
) j p# d' R/ ?" {( `$ o - STD_HEADER \
3 N- r/ h9 Z0 T( t, v% } - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
/ b( o. N) l5 q" ^ - "\r\n" \" W3 A: i: \, l( v' M9 V0 n
- "--" BOUNDARY "\r\n");4 ^. i9 m: G7 C; K1 x4 f# L$ j
- if(write(fd, buffer, strlen(buffer)) < 0)0 L- @- D7 B! b) W9 d; w
- {4 `. Y. b$ R& a; n" \! R
- free(frame);: D! v4 c0 W" e5 {# D5 q# G
- return;. i9 ~0 I, m- w# b' @
- }
复制代码发送完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" \0 O+ O/ q/ s& _ X
- "Content-Length: %d\r\n" \
& e5 \' q" D3 l' B* Z6 v; E7 r - "X-Timestamp: %d.%06d\r\n" \: E2 Y, N) I7 Q* k, w
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
; D5 D, Y& z7 ]' e: B n0 v - printf("sending intemdiate header\n");+ `$ ~( ^5 T& O9 L0 W
- if(write(fd, buffer, strlen(buffer)) < 0)
7 c- C# X. \5 t* h$ ~) n; C: r) d* T - break;
2 a7 D; x1 K4 I/ S) R4 e1 r - printf("sending frame\n");
$ `- r/ }3 ]0 }: ?0 n# t4 C - if(write(fd, frame, frame_size) < 0)
; L* e9 B" C0 K+ M0 W6 U' N0 H) Z6 u2 S - break;
2 {7 Z8 a1 v( N" r. h% j2 A+ R E2 C - printf("sending boundary\n");% u9 c) |+ }2 x4 Q+ c! L( b
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");- p. B% M! A! p1 H& U) D
- if(write(fd, buffer, strlen(buffer)) < 0)
5 y5 o8 ?( t5 \+ U; v# e - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  & g! _; M. t/ y3 ]4 \ H6 Y1 V$ \

% |8 c7 z. y1 O: {: m! |6 g二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
: z$ B% \8 r3 c) }" {8 _- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
) M, I. D" I+ W" J$ y; C4 ? - {
" s9 C# R& s3 u5 J - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
0 X/ O+ i3 H6 T! N; Y. ~6 f W - if(*socket_found == (~0))4 }% y* r( R9 Y! }
- {! o: W0 M: S! O8 y1 V |; s5 L U! n
- printf("Create udp send socket failed!\n");
7 a- D* w' W( n% D3 H/ [ - return -1;* O8 [) U# ^8 D1 I! ?
- }
, a0 _5 i. d* c) T$ ~$ x* u7 r1 N - addr->sin_family = AF_INET;
3 {+ I! i' N1 Y- V1 ?0 I - addr->sin_addr.s_addr = inet_addr(ip);
; G$ M. K% [0 u% B& |# t - addr->sin_port = htons(port);
/ M* h# I$ A: R. F8 d2 X0 b; w- T. s - memset(addr->sin_zero, 0, 8);$ x9 |# u0 H2 ^/ v" k+ [
- return 0;# z* ?7 \8 W5 q$ A
- }
复制代码 # W7 _, u7 @- |" f, m
; ]7 X2 W; Z+ D/ `+ M0 l/ k而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:2 T+ N8 x6 i) V* R# y% w: p
2 y" W+ n0 v* \$ ^$ P1 [+ @# U' r; r3 F) O. [, D/ ]
- while(fend > 0)
7 w! T2 q+ b8 s - {/ M5 I K" P1 u& g M, a) f0 m
- memset(picture.data , 0 , sizeof(picture.data));
$ f5 P" u: R( e- k: ^8 q - fread(picture.data , UDP_FRAME_LEN , 1, fp);. r0 G$ `5 E" |8 R
- if(fend >= UDP_FRAME_LEN)
9 [! Z$ |; F0 \& o; _ - {
2 r0 [2 C7 ~( C0 @; X. h0 s - picture.length = UDP_FRAME_LEN;
- w5 D1 V- D8 K3 X7 c0 X - picture.fin = 0;# M5 V/ D' u, b/ K
- }
9 B R$ y; G9 l2 x: ?, ^ - else
6 |% G: l6 v0 b/ l - { d. D" r. ^) j5 H5 u
- picture.length = fend;
( B6 }! V* X4 V9 O - picture.fin = 1;% L4 v C/ h4 E3 o$ ^, d/ E
- }
1 ^$ Q2 t. V' X - //printf("sendbytes = %d \n",sendbytes);1 w, ~# \ Y5 @( X( d; Y
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);6 I% w# M9 e/ b+ _$ m: U
- if(sendbytes == -1)( ^. Q- |. P+ e
- {
6 I: W4 Q& }. j# {3 T0 l* W - printf("Send Picture Failed!d\n");
, {- M4 r! o( U$ J: [: n# ^ - return -1;
& H4 g( x5 |* P3 b2 z2 u7 W - }8 _& w7 d" g4 z5 s' z5 R7 Z5 P
- else3 u; g; |) [+ J& K8 O& i1 R
- { |2 e& U4 S$ Q7 n3 r$ h) ~, W* s6 K* n
- fend -= UDP_FRAME_LEN;- ^& T- S" V( V
- }
6 k& P. ^9 g4 p) s$ x8 O2 D. G - }
复制代码 7 d. B, o$ q) Q$ S6 T
6 s- b! A0 l7 H* K9 g
 ; y! x. f1 Y/ J
0 p8 d4 ?' M- D! n
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |