本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
! {( z0 u$ \& Q" {5 t
$ s+ ^% Z$ W; A+ f$ h: ?) W
( B$ L! @- l3 h3 q0 s/ N作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
1 i' K* \( E) [6 m
1 e" W1 r3 ?/ d% v+ O, v3 U2 y本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
1 `- f/ `! A$ R! a# m; Q- W! }" O + ^0 q0 n( \/ n5 {
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 ! r- g9 Q% R' H7 J6 F
一、HTTP网页服务器8 [3 W: {: G4 u# k
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
& x. O! p* B* _ - {
2 v: V! q8 I0 N1 l% c4 f* W& U& S - struct sockaddr_in servaddr;
( i5 B1 F! u( B) }/ P& L" b - socklen_t addrsize = sizeof(struct sockaddr);
! N; `. @! @% i - bzero(&servaddr , sizeof(servaddr));
9 s9 y3 G5 v) J( r8 I# G - servaddr.sin_family = AF_INET;
1 g$ l7 S" { Y( l5 s - servaddr.sin_addr.s_addr = inet_addr(ip);
0 D2 r" g) T; e - servaddr.sin_port = htons(port); B7 C% E2 d' m5 ^ Q! t* f5 u
- int ret;
7 ~ R! b% A4 D - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)8 x( y+ A7 u7 ?" z5 y0 ?& ]
- {- @8 L8 k; b/ r
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
' `7 n) P1 d* E - return -1;; V# D. L) ?8 P6 `
- }
' X* f2 E3 {1 z! @: F7 J - int on = 1;$ R4 @1 A. }, k
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)( V1 t4 o+ q/ Y. f3 M
- {3 X( c6 J! [! u$ l* B* ?
- printf("setsockopt error\n");/ b* I; n+ ^$ v- ~; J) c" r
- }. {; g% A' t3 Y& p
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);9 e, Y* J) @* ^/ x/ O
- if(ret == -1)8 ?5 b9 b& \6 S' H
- {8 z. u- _7 M# v
- printf("Tcp bind faiLED!\n");
* f% N0 {& E3 H5 S, w - return -1;
0 o, D6 D/ U7 p% O - }
' t3 Z1 t* O W) S - if(listen(*socket_found , 5) == -1)
) D# v4 a u2 B" t& n x# T8 Z - {9 J: O& O. y( v5 y
- printf("Listen failed!\n");5 W7 B$ \6 z: F* n4 q% ?
- return -1;
D+ ]0 R; S4 M9 [! a - }
2 b+ f6 S6 O$ Y- ]; W - return 0;4 L& \" J$ ]7 u! G; ]
- }
复制代码其中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);
* m' V+ D; Q* [* w/ c. p - void * Thread_TCP_Web_Recv(void *arg)
- a( w6 S; Y. O2 k$ ? - { }, Q6 k) r4 c: O' q, f
- 。。。
4 X9 t* p5 s4 @$ T - while(1)8 C4 }0 m z5 Z2 w; g1 }
- {
# ]) ?: K8 K1 F9 h3 {8 q - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
- Y. |7 _$ b8 l* N7 v4 n: h - printf("fd_socket_conn = accept()\n");
) Q( g4 F% q0 a - 。。。- T8 X6 l" ~1 ]. r& O
- recv(fd_socket_conn , recvbuf , 1000 , 0);
2 i/ R. E8 l1 I - }& g0 d! L" Z H7 s: d6 U
- 。。。9 l8 i% B; {0 P7 G! T
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
7 z a$ I1 z9 z, Q1 U - pic_tmpbuffer = pic.tmpbuffer; a) v2 B4 M$ |* N: ]) n2 ^$ v
- pic.tmpbytesused = buff.bytesused;
: c) C* `2 j T1 i$ Q - pic_tmpbytesused = pic.tmpbytesused; a+ n2 a7 f; K" v$ k
- pthread_cond_broadcast(&pct);
3 h' u3 ^# S q% U0 M - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
" ^3 E$ R0 d; K# a - pthread_cond_t pct;
& E6 I- w& R- a9 e6 A - int main(int argc, char* argv[])
5 x/ O+ |* L4 g& X7 x2 c - {( u0 h, g+ \! W/ q3 u
- ...
0 c! A8 K" T* d- M* V5 d - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);2 C3 N3 C" B9 h, D# X, O
- pthread_mutex_init(&pmt , NULL);$ s7 B8 U/ O2 I. o
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);0 M4 H9 E. w" }. h. {) F
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
/ h$ F0 V3 h* J x2 W* H - ...
; F. H: ] @% N! e4 w - while(1)( q: E. G8 v9 K. T6 n
- {
# S3 l! t; g! {4 _4 l - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);- J- T8 i0 | [/ x! ]
- ...9 R( b3 |9 y8 I
- }' ~+ \' ^, D p" \% b
- ...2 e) E+ g% ?+ Y% o3 q' L
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
2 B6 u% c+ E) ^4 ` - </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 G5 \& m# A0 w$ A - "Server: MJPG-Streamer/0.2\r\n" \
+ J/ J2 O/ W" S" E - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
$ n6 E8 ]$ o7 E2 |' ?7 n" r - "Pragma: no-cache\r\n" \
# S* g& k( l" j) i) H, G# F - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"- X1 S9 ^& @: e$ V( ~) ~
- #define BOUNDARY "boundarydonotcross"
( M4 U6 ? q( ]3 j9 |& p - printf("preparing header\n");( K* D9 d( a% p% m$ V- [. T
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
$ S2 p0 V8 j8 l( V8 L( O0 D% w4 U - "Access-Control-Allow-Origin: *\r\n" \
9 W3 \* G# j- O6 A7 g - STD_HEADER \5 V) s9 j# q& ?/ H- m* f; q; q
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \! b( X1 e( T0 ?$ {) @! J$ \
- "\r\n" \: q* x2 S. s6 ~2 r5 ~4 V' ]
- "--" BOUNDARY "\r\n");2 h+ K2 s" h b% _' O$ _
- if(write(fd, buffer, strlen(buffer)) < 0)6 W) M' l' e: N
- {8 |5 F& u3 B; @3 q
- free(frame);
7 B" N9 Q- @2 N/ J - return;; z* K: U9 d, p
- }
复制代码发送完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" \
: u0 Y: {" A0 T- C0 @; E* M - "Content-Length: %d\r\n" \; B0 r* H" z' w1 V, b
- "X-Timestamp: %d.%06d\r\n" \
* D5 Z! H+ [7 |7 G - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);: m2 ^: r$ d# P8 z7 e% a" f8 b
- printf("sending intemdiate header\n");
* W8 h$ ?* j4 u A0 M - if(write(fd, buffer, strlen(buffer)) < 0)
: T% b# b$ Z# X9 ~2 o: J* s- x9 { - break;
6 t" m" i1 s- v9 P( K. n4 I - printf("sending frame\n");; t0 o' P' {2 Q! J* o
- if(write(fd, frame, frame_size) < 0). P+ [. f; c: G" T
- break;2 e2 F6 C1 Z9 L6 Y! `7 I1 m4 g
- printf("sending boundary\n");
2 z" u! f* g" g: [3 H - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
/ v+ |% T8 a# D E& \& D# a( p - if(write(fd, buffer, strlen(buffer)) < 0)+ e$ F' P1 Y/ Y* w7 z3 ?0 k9 w
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
- H( s2 ?3 W" R9 ?# o) `
- X9 v: M+ B0 f5 U# r# s二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
/ t' B" Y1 U' J$ x8 J6 Y7 l1 N- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)9 i9 D- x9 J O% y
- {9 _% I1 M6 Q- q' _
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);- H% h3 H- k g$ T9 b
- if(*socket_found == (~0))
; \9 Q- `: \ Z8 w* q - {
2 g8 ]$ e) |6 X. v/ J) X$ Q - printf("Create udp send socket failed!\n");8 p, j. I+ H* R# P' ~/ K$ H/ Q$ p% J
- return -1;* V1 O5 x# }( W9 G+ }4 c8 d
- }
: h; m+ k) @' \* t' G9 Q - addr->sin_family = AF_INET;2 ^( C+ _" Z& L# ]: X) R: w: L( N
- addr->sin_addr.s_addr = inet_addr(ip);
; M3 x" ]5 l; S" n' W' v+ Q - addr->sin_port = htons(port);
, p: J- _; Q3 {* C - memset(addr->sin_zero, 0, 8);
3 g) b% j3 j7 t2 u' v - return 0;1 o; \8 z4 ]; s- H) N) J
- }
复制代码 ; c1 @# h2 W% A5 x* @2 ]9 K2 i
; {& `% I. d3 R9 b5 x2 v* t$ y而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:2 ^( C# j1 B8 ^* M8 K; }$ {4 X
# p, ?# H0 w" r" Q& e& D. r* C: F
+ U% v# C/ U4 S/ B( ?5 O2 M- while(fend > 0)3 k# \& _1 G* W4 u
- {
0 q6 f/ I$ T" ^- c - memset(picture.data , 0 , sizeof(picture.data));
; ~3 D( e5 d+ T2 z4 x5 l: C - fread(picture.data , UDP_FRAME_LEN , 1, fp);
# U2 L# ~$ m& | B+ M+ ~8 Q - if(fend >= UDP_FRAME_LEN)
3 \+ r% E; x6 h/ }) F5 x - {6 z( M$ f: Y8 D" A( A
- picture.length = UDP_FRAME_LEN;
( o* P, d! o+ \2 N" F- N, g - picture.fin = 0;4 \* S g6 I- v- i7 q6 ^! e3 ^$ G+ k- ~
- }
4 A0 }9 t: W5 y6 V$ ?, ?2 W3 B - else
/ A; @7 m5 p" c+ }" F - {: i: e6 @# P) y' ~) p3 \
- picture.length = fend;
6 R6 d) Y2 {7 f* t6 p - picture.fin = 1;
f3 w, n. o( _* X9 ` - }
" j% [9 g, ` H/ K( u - //printf("sendbytes = %d \n",sendbytes);
5 }1 _) i, r/ W4 e - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
! B( X0 p3 ?5 z, I, Y - if(sendbytes == -1)
' k/ X! k' A9 w - {" J2 t3 h6 u) A5 U6 a
- printf("Send Picture Failed!d\n");. f6 n& L0 a: M; _
- return -1;
2 `' K0 N0 T3 r# A" R* G$ x6 \6 W - } v+ ] b4 [( ]5 I
- else" ^; M0 V$ a/ A1 d& h4 ?
- {
6 m: h% M" J; { - fend -= UDP_FRAME_LEN;
9 U5 Q* f, S( e+ K$ ?5 C - }
1 L6 T; Q$ W$ b! ` - }
复制代码 5 g- a2 Z0 ?& z: g6 G: L- V S
, v% u! q% M' F! G4 W
 ; _$ I, s0 ]! t# r0 u. H( O4 i, ]
* S1 m9 d1 ^, R) c, m
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |