本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 + {6 m, \( F+ @- I3 b f
" ?7 L$ L& W ~ L
# U9 D2 y0 ~" u1 [- Z/ ^作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
2 b! C& @& Q" a/ t
% h; s+ ~$ J) }+ J/ r$ N2 ]本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 4 ^5 e, J3 i. X/ J
 7 y4 M9 {! ]' V0 |) p/ ]. G3 {
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 7 k1 B$ X5 e& S$ \; M/ g( ]
一、HTTP网页服务器
% R6 q8 Z" K2 g, H' l- l3 w先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)" x: s1 d7 X5 |6 s- M6 ^2 g/ E# z
- {
: E( R& V( ^- H8 |2 `# _ l - struct sockaddr_in servaddr;
1 v; ?! R' d" d; m$ {$ Z4 z - socklen_t addrsize = sizeof(struct sockaddr);
# c) m2 c; i5 [$ q! l - bzero(&servaddr , sizeof(servaddr));, t4 [8 Y4 L2 C C* O' A
- servaddr.sin_family = AF_INET;- \5 J; t" X m
- servaddr.sin_addr.s_addr = inet_addr(ip);& T& W! H' p- \2 e$ T' x+ Y; I& D
- servaddr.sin_port = htons(port); r! S4 F! h( r0 m# f( x( r* V
- int ret;5 }0 n3 ^, S: z- f% M
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
: q# E7 z ?" x9 Y - {: C8 v5 p" X, r3 _3 t* D7 h6 g
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
& d. k* z- [4 v: D - return -1;) a+ r7 s( G+ C, p
- }8 L3 f$ I$ ?6 }8 G0 F
- int on = 1;
' ]$ ^& f5 J" a _! t1 n) e% n+ g - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)6 k# m2 m, b, u$ I& ]
- {
8 A- W$ v" S6 `5 e# E% ]& L - printf("setsockopt error\n");$ N* v: c: [% |. t5 i s5 m" G
- }
! L% E/ l% P: F: ^5 ^ - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
: R1 g* A9 d/ K% L4 L* Z - if(ret == -1)$ R1 O3 t# a% |
- {
$ N& P t4 E- y - printf("Tcp bind faiLED!\n");! m# B- }/ R" ], Z. t2 V
- return -1;( v8 E# e* \* g0 ]1 g" K3 S2 g
- }
, A/ G$ E' n+ f0 g2 u - if(listen(*socket_found , 5) == -1)
2 [' E/ c9 r3 B# Z) d0 V - {- J( G ~1 { k1 r! f
- printf("Listen failed!\n");
2 G+ b% t& ~2 @( d - return -1;9 S0 B, `4 ?& t
- }
9 K2 c9 E8 X% }$ Z. W/ d& y2 P! \ n - return 0;6 k4 X. k8 a4 G8 d; p
- }
复制代码其中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 W2 K: T2 w3 q0 O - void * Thread_TCP_Web_Recv(void *arg)" n. j3 R7 ^" I
- {
( b* x7 \/ U6 b1 ~7 M - 。。。
\9 g& Z4 k5 q9 S - while(1)
/ ]7 G: J+ L2 r# J. a, r2 h4 l - {
, F; M% O+ x6 e - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
! F/ S. g \9 ?. l$ c - printf("fd_socket_conn = accept()\n");
' O% G- v( Q' f5 B3 n( r5 t+ ]: K1 d! T" X$ i - 。。。
/ u8 o+ c$ t7 x) _ - recv(fd_socket_conn , recvbuf , 1000 , 0);6 s& M# r% ]) @* E
- }
) ?* _7 n* ~* | c - 。。。
& L# t0 i# [5 Z2 ^# K& {; R! R! I - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);1 C \# @) F" S8 @& K
- pic_tmpbuffer = pic.tmpbuffer;
8 }9 i: S" j* V) d - pic.tmpbytesused = buff.bytesused;
/ a6 O6 F% x! B9 C+ N P* r - pic_tmpbytesused = pic.tmpbytesused;
1 ~. i" L8 |. F - pthread_cond_broadcast(&pct);
) C' G( o/ s# e6 q& y/ y) b - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
) c: M6 H9 P4 c3 E& y9 v - pthread_cond_t pct;8 P+ W& i: i5 n& }& C0 ~3 H
- int main(int argc, char* argv[]); z6 `! [& ~" w, h( d' X/ K* \" ]% Q
- {0 c3 d6 q2 [2 a }2 Q" S" h
- ...
% j; X3 V6 ~; ?8 R - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
. J( M5 J+ M: i - pthread_mutex_init(&pmt , NULL);
( y, z# J- a, i& Y( E3 C1 ? - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);) [2 G1 ]9 x! B, c
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);2 ^3 \# H" b1 A1 B, F
- ...
! O2 K0 E8 u/ Z. ?4 p - while(1)$ H* h5 C* u/ S, I- r7 h
- {
9 L5 X+ _+ \' e4 L& Y7 f9 z - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
: E. e2 e# v! q) j6 a - ...4 j" X% [. z; z
- }
0 U- I6 a, y' w2 A( r. W - ...$ `( B+ C6 Q/ s! h
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
0 n: Z/ Z" v" L4 r# E3 A - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
3 e, l8 c: t0 k - "Server: MJPG-Streamer/0.2\r\n" \
; D' E" u+ R" S) \% D - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
+ V. e) t1 o5 w - "Pragma: no-cache\r\n" \
# Q0 Z/ W: g: t; @' J/ x r' i/ \; N - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"$ {7 z* K3 s( ]4 |
- #define BOUNDARY "boundarydonotcross"& s( }- v' H. c' W# N
- printf("preparing header\n");
7 E/ u4 k& m) L, _4 s) |( h - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \; y) l( G+ S {
- "Access-Control-Allow-Origin: *\r\n" \8 `- M: p1 |2 L
- STD_HEADER \
1 J# S3 M9 l* F - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
7 f. v& K! ^4 L& @ - "\r\n" \9 N9 B$ y6 H3 p
- "--" BOUNDARY "\r\n");/ j0 i2 G" C0 U
- if(write(fd, buffer, strlen(buffer)) < 0)
* W& h, B5 @% I& }0 _/ R - {
( P" b/ w& }8 m) G! Q7 { - free(frame);1 `; U3 _! @% f( b8 i6 t. o
- return;
0 o; q; s6 j' {. q2 C7 d - }
复制代码发送完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" \
1 {* v: {! v6 k9 H, f - "Content-Length: %d\r\n" \2 @) o& O r/ k5 f7 g
- "X-Timestamp: %d.%06d\r\n" \% l9 T$ X% M' g+ ]
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
5 T. {; v) _2 ]5 p! g" d% H9 ^5 r - printf("sending intemdiate header\n");+ h0 x" W/ r7 z! M9 m7 g9 W
- if(write(fd, buffer, strlen(buffer)) < 0)
* ^& U4 n1 ?( L/ `, v - break;
) s. l, i; q* ?' m& z - printf("sending frame\n");
# [6 A- O* z5 u. C' x3 ]/ ] - if(write(fd, frame, frame_size) < 0)+ G3 Z8 Q. W$ r! L1 r9 Z
- break;; |$ ? Q R7 |! O% R9 _5 ~: }/ S
- printf("sending boundary\n");, T, |, v- g! f/ ~
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");4 R# ?6 Z8 I- {- o3 C3 T
- if(write(fd, buffer, strlen(buffer)) < 0)
, p) f( X, Y" u& C+ \, \ - 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& a' e5 F; w/ Z( P
- L& y$ E& B& b二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:: X( D1 k- I# G6 C# N5 e
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
* s1 u# K7 Y9 {4 x - {5 }0 }+ B' [# a7 x# x
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
8 u0 W; E* m& C. ] - if(*socket_found == (~0))5 @: ?. g9 f' c
- {: ]3 h! z) Z9 Q6 {2 {- s
- printf("Create udp send socket failed!\n");# i1 ]4 L0 F% G
- return -1;
( a: h) F, X. s' ~ - }9 J; o! g$ C5 {2 e( [( p1 g
- addr->sin_family = AF_INET;
$ B+ e: d$ j0 R1 V. L b - addr->sin_addr.s_addr = inet_addr(ip);8 e: w2 l- S! c0 V7 F+ H* K: D
- addr->sin_port = htons(port);$ m+ j* i5 ^3 l. g& S/ _; W% J9 R
- memset(addr->sin_zero, 0, 8);: [7 O$ k3 m/ d2 E
- return 0;. o: k2 q$ Q( ?- k2 }4 J
- }
复制代码
9 v: F) u `: N, H- ^# h
8 ~ n' C1 s: E% V% b2 m) q- Z+ [而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:* m$ _9 J$ D) q' G/ h
9 q5 N# W8 W9 N0 ~
$ B5 ^3 r1 |1 B" Y4 S1 m0 g
- while(fend > 0)
+ d) U* O7 G5 \% {6 E; b - {% j b7 [2 Y- K, _) {8 {: f. T3 k
- memset(picture.data , 0 , sizeof(picture.data));1 E4 K5 l$ V8 l+ t
- fread(picture.data , UDP_FRAME_LEN , 1, fp);, K# j! q) p& c
- if(fend >= UDP_FRAME_LEN)
8 l. E( i+ y. `' o! C) E( {) N - {
$ @' z0 r$ ]6 G8 s6 X - picture.length = UDP_FRAME_LEN;
" g Q5 n) v# M* I2 z5 n - picture.fin = 0;, M4 u0 S6 ^" A; k) `9 G' n
- }) z/ F8 A* K& t
- else: v3 |- }, X0 p6 k+ h* v- B9 O
- {4 ]) Q+ o# m3 ?1 ]( F" F$ Y
- picture.length = fend;5 `; a' b$ b1 F% g+ u
- picture.fin = 1;( C$ b$ `( B' h6 v1 n
- }
) }- h7 z% F/ r3 I7 n& w - //printf("sendbytes = %d \n",sendbytes);0 g0 y: |) s+ R5 h
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
. t6 @8 n( C0 {6 t4 e1 d0 [ - if(sendbytes == -1)
! I. n- ?6 D! }0 t5 o$ f& i - {
( U* p G$ z, F$ j7 k5 w - printf("Send Picture Failed!d\n");# c+ \+ c }! `2 } H' Y
- return -1;3 n8 }4 o" X/ @. [, y
- }
: f; e2 p; q9 V8 z - else
8 p5 G$ u: j I - {
7 |; g$ ~1 v; ]' O - fend -= UDP_FRAME_LEN;4 g& F5 m3 D1 G
- }
7 R3 r% N7 }! Z0 M, { - }
复制代码 , U, t( R- Q; J7 D/ j4 e* I
) H4 a6 b8 c" l: T % S8 R: ]3 j) @& M( k8 H
8 @4 c1 W4 ~: V$ z+ ]iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |