本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
1 `& q1 s L5 m4 Q% V- y* T# W5 e
 : H6 j6 u/ ]; M! I+ \) o
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
( x8 G1 k* b% c8 } }6 o& i1 y) p {3 [- y
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 # Z% \! A7 M# K E3 T0 U+ _0 `. Z

& ` J: u! d) @+ y& \两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 1 }/ G+ j$ Q: d
一、HTTP网页服务器
: ?4 m& v5 A: }# m @' B先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
& g% [4 O9 V" `8 j - {5 j2 b' x0 K2 v2 O" d e
- struct sockaddr_in servaddr;
& w6 ?; k6 v4 D1 O. _: t3 B+ u! V+ s - socklen_t addrsize = sizeof(struct sockaddr);5 R! \- x( W8 ]/ w ^) `# J, B
- bzero(&servaddr , sizeof(servaddr));3 t) {" k7 h5 `+ T
- servaddr.sin_family = AF_INET;# {3 H# r# N& T3 t8 `
- servaddr.sin_addr.s_addr = inet_addr(ip);
9 Y. I" Q3 }2 u/ h& `8 f - servaddr.sin_port = htons(port);, H3 l9 W/ o! \& p# {
- int ret;
( g2 n- X/ W6 ?- u% [ - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)) s; g0 g5 H9 x. N( b6 m/ {; Y
- {4 r7 x. D7 L0 K6 ]8 _! ?
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
/ k" C# y& W! { - return -1;
- R0 K) }) m+ V2 K$ L } - }
& R/ A7 _+ j; [ - int on = 1;/ P- R9 n5 V. l: [( q4 @2 a
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)- T2 P0 J6 u2 F C2 e
- {
8 u$ T9 ~3 ?, n- B7 f - printf("setsockopt error\n");9 X. e8 P7 ?9 e
- }+ Z. z% H7 l5 ~0 s9 B* A8 C
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);% q! |& K- ?' p2 J: Y" Z
- if(ret == -1)1 F" m( W& C/ i5 v% K1 ]6 q( Z
- {: S" f9 j- k+ ~" b
- printf("Tcp bind faiLED!\n");
% b( x2 g1 K0 Z/ D8 E - return -1;( i1 u$ j1 ?- d" c1 U
- }' ~1 p: V2 M5 i7 G9 p
- if(listen(*socket_found , 5) == -1)
$ W3 p- h5 u! N' d& h h/ D% ~ - {. k3 ^; L" E. R3 S7 g5 b
- printf("Listen failed!\n");
- {! z# d7 g0 a6 Z - return -1;- U4 F9 Q$ T, s8 u u
- }! j& ]+ L/ h- t# g
- return 0;
( t" V! j5 V7 u; P$ V - }
复制代码其中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);
' @2 h/ m8 V/ Y/ j - void * Thread_TCP_Web_Recv(void *arg)) x4 o4 A" c8 H6 i) n
- {( G9 ?( M, c9 u: w
- 。。。( G5 c! a& w4 r! B4 Z' s" B6 d
- while(1)2 m# F, }' e. {4 j9 j- k% l F
- {" h5 n6 M# k" n, }. U5 ?. ?% ?8 n
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
$ N- Y; h5 ^( ^9 U* M4 H- k1 ~) R - printf("fd_socket_conn = accept()\n");/ r3 o7 r, d8 z& ]: V4 z# s+ \- K
- 。。。
- n8 o M9 \! R% |& p4 Z - recv(fd_socket_conn , recvbuf , 1000 , 0);
2 m& }" v& g6 L( B! M ~# L5 s - }; s, @& S2 S# M4 i
- 。。。% u) `/ L% n, Y# Y. q" a2 g
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
8 P7 L% D6 G4 O; s Y - pic_tmpbuffer = pic.tmpbuffer;
5 V& I* ^9 B5 [' W0 B* t) p - pic.tmpbytesused = buff.bytesused;
' L$ t( v( R3 t - pic_tmpbytesused = pic.tmpbytesused;( C$ {1 A& u/ \
- pthread_cond_broadcast(&pct);
) Q: P1 |- p+ @4 C& p - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;/ {% P- W& U( }2 y: H+ U: J! J
- pthread_cond_t pct;
/ B3 I1 Y2 i& U: W& L - int main(int argc, char* argv[])9 o5 `7 X ?! ]8 _/ W) J
- {2 I* l8 W1 w7 n' e l! @# L' d3 |
- ...
' J0 `4 P4 i1 R6 l$ `2 O$ ^/ ~ - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);, v/ H0 X! a' z" E0 F
- pthread_mutex_init(&pmt , NULL);0 F p% V, @8 [4 v# f. g
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
$ H$ X) `8 e) W" ]9 I4 @- F - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);. Z# l+ L4 v) G! N
- ...% y' ~6 G1 [. B0 B% O
- while(1)
8 _0 y! v( M1 Q3 K6 ?; G* m! O - {3 o- ]& \0 W" Y# F3 B9 r" \
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
& C4 ~( z6 [- l/ M9 \ - ...
& J" Y8 P2 q! y - }
7 w( d8 R; p" r4 ]4 a - ...& w$ X6 h' x f9 S- z3 d
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
- ^1 S5 |% L$ w. D+ B - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
) r: ^' D( Q$ [6 M+ w1 g - "Server: MJPG-Streamer/0.2\r\n" \2 L& b$ E0 x/ {
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \( c' B, ^& ^6 u' i) ?/ f
- "Pragma: no-cache\r\n" \
2 W+ I6 r5 q& n. t) D/ f. X - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
0 D- N$ N: d) \) `+ ~ - #define BOUNDARY "boundarydonotcross"/ L. ^2 ^! F, Z1 u/ l1 x/ p
- printf("preparing header\n");
7 G- g4 {4 O2 ]3 v - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \5 l6 K" l( D& O; Y$ A
- "Access-Control-Allow-Origin: *\r\n" \
6 y0 L* m$ \5 H8 S; o - STD_HEADER \
. X; p+ V+ F1 _: i; ~$ J/ }7 |4 T - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \" V! b, [0 O1 u. l
- "\r\n" \
6 |* ~6 d7 X A) V5 h4 O - "--" BOUNDARY "\r\n");
- u, K2 c1 N0 x+ k& {7 f/ @2 j - if(write(fd, buffer, strlen(buffer)) < 0)/ T2 ]. j' L& @5 z' }' P5 O
- {" W1 b( F! h, s2 r
- free(frame);
9 K' X3 k/ _5 v- f6 s - return;
7 K# v" ^* V+ I) m - }
复制代码发送完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" \
# b# ~" |3 R( R& R; w1 Q [" z! _ - "Content-Length: %d\r\n" \+ v* ]$ p* l1 n/ k+ r, ]0 K
- "X-Timestamp: %d.%06d\r\n" \
) x% g$ H: ]3 M" }2 `+ L% M - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
6 k2 H& |( h2 V6 u, O1 V- V+ X* ^ - printf("sending intemdiate header\n");
+ S: C% m& \0 d. M, M7 x' ^ - if(write(fd, buffer, strlen(buffer)) < 0)
+ c; P# d) V% I - break;4 M2 ^/ ]1 c% X: e7 d& v
- printf("sending frame\n");2 @! \- C3 ^7 j! P
- if(write(fd, frame, frame_size) < 0): d% V' h# j0 t: L. Z8 {
- break;0 A; x+ j+ J; ^+ {8 ^
- printf("sending boundary\n");
9 f: H/ [8 g. l' d4 F" S) W7 ~ - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");% U6 [ ^3 e S6 {
- if(write(fd, buffer, strlen(buffer)) < 0)0 L! Y9 ~3 o" v. p+ Q
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  5 f( ?& x) P" Q/ V) _5 D4 _
 ; \& _9 s8 r. j2 H: f5 J z
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
5 j6 |" s: W, ~" ?9 q+ t0 A- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
3 z* K% O. P$ D+ C - {
# @" A) u, d! Z& D U; G3 K# N - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
& H8 r& F2 d3 Q5 E z - if(*socket_found == (~0))
1 m) V3 k# W4 X - {- l$ Z, h% ?7 {! v
- printf("Create udp send socket failed!\n");
5 X8 k9 F( O H' L. B! p5 N - return -1;
6 g* M+ H5 A+ T1 v: T! e ` - }
4 w" d) A$ `8 I$ e/ ~ - addr->sin_family = AF_INET;6 Q$ R7 G9 A! ]: h
- addr->sin_addr.s_addr = inet_addr(ip);, T& f4 X, e1 D
- addr->sin_port = htons(port);; M R W0 X! z
- memset(addr->sin_zero, 0, 8);
5 |! Y. j/ _2 ^( x - return 0;
, T$ i; ?+ b$ q* S* p - }
复制代码 9 e, ]* ~' K" }, L0 I, s
! N# b; J% a# I: A2 b而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可: J' V: Q1 V7 R! ~# }9 z; D) `7 Q% X9 I
) D2 N8 o) a# ]6 _0 L" s. q% B% G( U" Y
- while(fend > 0)
4 d: z$ ]/ e2 u/ L+ L4 H& E# U - {' {, S1 w; y' f
- memset(picture.data , 0 , sizeof(picture.data));& R) u7 e# D# Q7 J/ d: j
- fread(picture.data , UDP_FRAME_LEN , 1, fp);
8 n) v' H% Q4 |1 M( }" g - if(fend >= UDP_FRAME_LEN)! K1 t) S' |+ ^5 R+ z+ @
- {) M( ?$ o3 [4 P8 D7 P- h: S4 z3 l9 Q
- picture.length = UDP_FRAME_LEN;9 ~& Z% l/ }: p" h7 Q6 E
- picture.fin = 0;
5 n7 ?; W# w x4 q% I2 u3 F1 N - }1 J" l" l' R; i0 s5 U. y
- else+ A6 b. v0 P; `
- {6 ^* z% ^# k7 D; A( P1 l3 K! U" T7 g
- picture.length = fend;
2 p9 W+ d$ c' p3 ~3 U- t$ A - picture.fin = 1;, [7 P% O9 @7 D5 w8 i& z( z
- }! k1 |) h5 y- O- B9 n4 A7 J. w
- //printf("sendbytes = %d \n",sendbytes);' N- N, `8 ~/ n2 |
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);1 `/ q" z; D, m# S
- if(sendbytes == -1)
3 Z. M4 K) t- e' g. c, h. @: C - {* T- f, K9 y; x6 U# X0 `
- printf("Send Picture Failed!d\n");' X' {' y" N4 P0 O
- return -1;# }7 a: s0 f6 }0 X3 I3 E ~
- }5 M0 M& b$ M% q% R
- else
, X( D+ P6 ]' u" N - {
2 X; a! | ?# K# L, s7 d$ o: N - fend -= UDP_FRAME_LEN;
- i% N# C I5 M; J0 ^ - }% U1 Z+ b- |( D# Q/ c3 Z* ~
- }
复制代码 / n% S6 z+ {; ]' Q+ r
3 N$ M' v/ G5 _ }! r; V0 I" Y8 n2 g/ \

& Y* l) ]: l7 O" F f
7 I* F8 f0 o& |9 ZiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |