本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 1 }" Z _: Y) Y3 y: Q" V
1 R& ]& _' Z ^! {+ ^$ G
& w2 h4 @# U* y. s( ?1 i4 k ]; p作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
6 U3 _& k: P5 T5 v( e9 E s
: y$ w1 A, b0 `% z8 Y4 j本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 : {2 s8 e+ u2 c; U5 q3 x( u

% X9 M5 b4 ~ d. M两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 4 x4 X3 y- q, K+ v' L% Z, H0 w
一、HTTP网页服务器) w4 ]: ]6 \2 b6 [
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
/ a7 c# Y3 ^) i6 g5 a - {
" V. D# c8 z4 q6 e V# X - struct sockaddr_in servaddr;/ F4 b2 T" h B# r! b
- socklen_t addrsize = sizeof(struct sockaddr);# j3 H* ^' s5 F( V; j. v4 Z
- bzero(&servaddr , sizeof(servaddr));
4 O' N3 g7 g& q$ y - servaddr.sin_family = AF_INET;0 L. z" d* E0 F: }
- servaddr.sin_addr.s_addr = inet_addr(ip);
4 L i6 W1 `+ M3 q - servaddr.sin_port = htons(port);, x3 d$ B2 _# t! J% }& U
- int ret; c* `4 `8 k- B* \# D
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
" f' y# u: t: G - {
! \- v0 h+ D* N: I7 D6 n# _8 A - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
9 f4 P) P, I9 V - return -1; I+ Z9 u" m$ I0 r
- }
- U( }# j' K0 d0 H# @9 b+ g - int on = 1;, a X. r% Y; G% G
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
7 p6 { }* J3 T o - {
" O, h, ?5 I1 _, ^, W( c0 z - printf("setsockopt error\n");
6 T z& e9 N7 N7 M: M3 L5 Y# \ - }; ]6 Y% \4 |) O9 f' i$ x
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
/ M* y0 X: o7 c( ] - if(ret == -1)
% ] Z" E( _) q5 x' H7 w* L - {
: h' d; I$ A0 I# F$ }1 T - printf("Tcp bind faiLED!\n");
, J; a* |( e. V) | - return -1;
7 u( E& C# u' G' E0 Q: k1 L - }1 h* ?- U& k7 t% Y, ], m% |# s
- if(listen(*socket_found , 5) == -1)% l7 o$ x& g( G1 Z; ~" U2 y
- {
; f. N# r( b5 D/ d& s, B. X4 ~ - printf("Listen failed!\n"); S5 {- ^% L a4 w# t
- return -1;5 O& A9 U% X% m- X
- }
# k' ?* u5 \# N% q - return 0;4 s( f* b$ }. E; b! n% k
- }
复制代码其中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);% @" V$ _6 i' p5 A- G
- void * Thread_TCP_Web_Recv(void *arg)
, }; x& \+ J6 c; x2 f6 F+ u - {
9 l4 f6 f1 W4 q% D2 d - 。。。
- c( e2 B; D# ]8 J9 _9 I' Z- D - while(1)
5 q0 ~8 a1 i1 c w - {
1 ]+ q# @1 U( V' v. i; e; R - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);$ h( I( L$ U7 C+ u; a
- printf("fd_socket_conn = accept()\n");+ V& r6 a1 |: o( k7 \. B8 y' e
- 。。。
$ S+ [1 T9 u z8 u. C - recv(fd_socket_conn , recvbuf , 1000 , 0); k: o, `7 L& s& D1 R$ q
- }
. b6 z& c' w$ y8 _$ d - 。。。
$ q0 z( Q' i5 K: _ V" [ - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
" P' o! v+ ]. n4 _; q - pic_tmpbuffer = pic.tmpbuffer;
6 E ?4 a$ D. N2 e) j - pic.tmpbytesused = buff.bytesused;6 w" ^5 b" t8 H% @
- pic_tmpbytesused = pic.tmpbytesused;2 [ i. `8 z2 V# m. A; c2 D
- pthread_cond_broadcast(&pct);/ K6 Y9 x0 {" ]' I/ I
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;# W# N- s4 U6 b& h0 A$ `
- pthread_cond_t pct; S q6 D8 T0 G& J5 ?: E/ L5 J( A
- int main(int argc, char* argv[])/ u O0 O% _; J0 t4 c+ y
- {
% B1 S( h5 [% H, K. P1 ~/ S - ...
/ q8 J( a7 ^+ M" c: q4 w5 o' [ - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
" W7 P: l4 m6 g - pthread_mutex_init(&pmt , NULL);. w, U% u( H2 K- r- z, Q
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
$ |7 i8 n, l) t8 O- k6 K2 G - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
6 ]. u3 R7 l9 {* Q5 m - ...# M/ n5 J6 J- M# p; U' s
- while(1)" X9 v3 ~) W8 N4 p6 ]! W% `8 U
- {
. Q4 F$ V# p' y4 L& |4 C - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
% |# U4 Z0 J8 z. |" \ - ...
! W X4 i% n; z; @) X i# a - }
* Q, p$ \- l; e; a& e# T - ...; n9 h. m8 P! j0 S' ^% ^( n4 D
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">9 Z; C3 M# h0 |
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
9 |1 O: e' V1 p1 C* y - "Server: MJPG-Streamer/0.2\r\n" \
* a" ?2 U m4 h4 g - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \0 W, V5 T0 f3 w1 r4 V9 P# x0 e O
- "Pragma: no-cache\r\n" \
8 ^0 B' ~* [5 Y - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
7 X3 p6 H* K+ \" _% y1 V* M9 K - #define BOUNDARY "boundarydonotcross"
2 R9 _/ l- H {9 _ G s - printf("preparing header\n");( Y3 D- o: v$ Q
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \ \/ X/ G9 i" z3 T) [, N2 \
- "Access-Control-Allow-Origin: *\r\n" \4 y% ~* w' r; a" p, l" m2 Y
- STD_HEADER \
, q' v2 P$ @, W1 V - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
2 h8 H- A. p! q6 ], m% y - "\r\n" \
% J6 L& S2 {3 m - "--" BOUNDARY "\r\n");
4 N6 x3 r. H* T - if(write(fd, buffer, strlen(buffer)) < 0)
# X- B( {, \ X - {
/ v- ~( |. b2 G2 I( M& ^ - free(frame);
4 ]9 Q: j0 M9 d. S6 u) |. h - return;
/ t; {0 U1 T: X7 P) }! b. g - }
复制代码发送完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" \
+ g* M$ x: ^5 W - "Content-Length: %d\r\n" \4 c$ x) V; n; d+ @# L; Y2 z% e
- "X-Timestamp: %d.%06d\r\n" \6 t" T# j% q7 I
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);9 t$ x- j) \% v" o* C* H( o! x
- printf("sending intemdiate header\n");% @; V, m, F( l: d3 Q; X; X2 m* l
- if(write(fd, buffer, strlen(buffer)) < 0), u( Z8 S5 |- i6 b8 F% S
- break;
1 S, I: I+ ]8 n. e' c$ s9 [8 ~. A - printf("sending frame\n");- J, N0 D1 d4 C. [
- if(write(fd, frame, frame_size) < 0)- {: [. n4 m; s7 i0 a! I
- break;
$ V0 l: ~7 q& k5 K5 `/ Z - printf("sending boundary\n");
# X: X# g5 X! @ - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
2 g1 Y* C: U( ?* ?4 n. f4 w; M - if(write(fd, buffer, strlen(buffer)) < 0)
6 V3 P2 b! N8 e: k. z3 u: E( J, r - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  , j4 n; u: x2 ?2 f8 w& ^
 % @8 y; A, H) Q6 w \7 D
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:5 c# P8 o# h- E3 @& F# n
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port) y2 E6 N9 G! ]. ]( @$ e
- {; ~* a- o: Y: a, s7 o
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);" e8 n& D# j6 Y. w
- if(*socket_found == (~0))& i; Q/ o B- L6 B( y* f" x. r( Y
- {" C- q* q" e! H0 O @7 E
- printf("Create udp send socket failed!\n");
, e' r6 D. Y9 S( ]1 o7 N$ | - return -1;* m7 c- l: H: A q# l: s
- }
$ @+ Y- P5 F* W3 M7 K - addr->sin_family = AF_INET;2 d r% o% |( T5 _9 e: G7 @/ I4 Y
- addr->sin_addr.s_addr = inet_addr(ip);
; j( F/ m: G8 z% b8 y" \! R9 B - addr->sin_port = htons(port);" S, v; M$ T: V1 z/ d
- memset(addr->sin_zero, 0, 8);7 D, Q3 x6 F# V% {* {
- return 0;
* p! {; C7 l8 I! p& f2 Y - }
复制代码
( o2 ^# f; s( H8 @$ q/ K* f8 M+ w! B1 H: c4 N
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:- f4 g: i% F, J6 \& R
7 s' o- j' n1 g7 u* n; Z: X5 C: N: w+ h5 b
- while(fend > 0)
( Q8 q/ V7 C* Q. Y. q: K - {' Q$ v( G. p1 s0 ^9 X/ Y
- memset(picture.data , 0 , sizeof(picture.data));# u& t" k( v# T" W4 E" R/ h
- fread(picture.data , UDP_FRAME_LEN , 1, fp);
' \2 l$ X+ V* Y - if(fend >= UDP_FRAME_LEN)+ M2 F9 `$ N7 Y- y2 ]8 j$ m; u
- {
0 L$ n0 e& ~ w1 p' k! A- | - picture.length = UDP_FRAME_LEN;# Q, U1 ^7 A4 B: d! r( Y$ |
- picture.fin = 0;
7 F( I& f3 N# t0 r: L - }3 v3 _* x% `4 {& O1 t/ h1 y
- else
1 e9 x, U5 P: C8 W: c - {
. a! z$ r) D0 [ N" _ - picture.length = fend;
C, m7 b& o, y+ U# A: G G" A - picture.fin = 1;
4 G/ Q7 A/ Y- O; }6 f# q - }5 |) w. P8 N8 i$ V* \
- //printf("sendbytes = %d \n",sendbytes);7 }, A3 W4 B$ \) G
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
) L; h$ n0 p( j. E3 n; ^ - if(sendbytes == -1)4 x; I# P+ e' S( U6 Z0 A! y. t
- {& K1 \ F6 Q) s
- printf("Send Picture Failed!d\n");
B( `. R% Q$ g4 U2 X6 H3 y$ I - return -1;; y8 z; X3 q( F! @& a5 Z
- }
Z2 p2 |. {& g. q - else
) K7 t. N2 ^5 ? - {
, H& q: N4 {' W" ~% X - fend -= UDP_FRAME_LEN;
. t v& G' Z8 Y$ U6 Y - }
, k+ q+ |& s* o) Z - }
复制代码 `: ^: H$ H# C5 x8 n% [- X
1 C. b) v, l& O% J% d

5 r- s2 u/ q. P. z4 w+ [+ X8 Y$ d# W: }+ J/ k
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |