本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
+ [, H) J+ F: f8 N
8 x0 o( w3 ^* c y4 ~% ~! z 0 ~& |' B8 ]) R1 w' v, \7 h
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
. ]$ ?, I0 M0 m6 R( q4 T; F! V8 c O8 S
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
! _" T0 n8 a5 W- o7 G4 e* C4 V 1 }- P( U2 U f1 ~( N; N% R
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 ; Q- g4 S$ e: r2 C+ c' d, r: d
一、HTTP网页服务器
& P7 u: n6 p! r( _' h, Z先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
6 l, B4 ~+ K& m( u3 u - {* C, V6 ] x4 g- ]; l: F
- struct sockaddr_in servaddr;, X+ s# | j7 x2 X: r
- socklen_t addrsize = sizeof(struct sockaddr);# x5 E W4 B4 K3 [/ _
- bzero(&servaddr , sizeof(servaddr));% a6 l4 g. w9 i3 O$ _
- servaddr.sin_family = AF_INET;
: A' `/ \" m" y" m. t* w3 y - servaddr.sin_addr.s_addr = inet_addr(ip);
V+ V/ e& q6 F# Z/ D# w+ K" {1 i - servaddr.sin_port = htons(port);
9 U" @ ?- X6 [) a - int ret;
. e5 G( `# R. J+ m+ z: H - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)1 X1 c, A5 ]% F# M8 b
- {, T8 t+ ^- W* x9 I. q- Y
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);! b( n. M; a' p |
- return -1;
( r4 d) z& x( O' Y* k# i3 ~ - }
/ h" [2 c& `" z: D9 _. Z7 w - int on = 1;
5 c; H, e- A Z# k% F7 ^ - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
- o3 p Z! B- H5 v$ k9 x4 E4 Q - {; q" r( Q% v2 c
- printf("setsockopt error\n");" N7 i, M8 e0 C7 s' t* p; G* [
- }
8 s5 t9 p% N6 I S - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
4 t: M! M' q/ U: f, l8 q% n) J+ \ - if(ret == -1)# G9 C% \% ]+ o( B9 @6 m" x8 A
- {
3 \, H5 k( [: X5 N - printf("Tcp bind faiLED!\n");) n4 c, s$ h& {; Y) u
- return -1;/ C1 E. x7 f% t) }% Q' Z( O, L* F
- }
8 Q+ ^0 G" Z. K) R W0 c" [ - if(listen(*socket_found , 5) == -1)
/ [8 y# O6 j2 }* v - {
& j7 i/ n, {/ C! K( H - printf("Listen failed!\n");2 z+ ]2 G9 Y: F7 F: R$ r
- return -1;: `" c+ F0 }9 o! w
- }$ C# z: Q. U3 J5 i7 A: q
- return 0;, \- V7 x+ P1 T+ i6 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);2 G7 Q" P0 n5 O" @1 ^% r
- void * Thread_TCP_Web_Recv(void *arg)
; i* g8 B" P+ V - {+ d2 U& U/ P& a- Q9 U6 b; H
- 。。。
0 i+ k% g2 Q8 |8 S - while(1)
1 }/ C/ X- C. Z6 n8 G' B! C1 j5 J - {: A p0 p. |0 m: E6 X' U/ `$ K
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
- @( A& U- L8 Z5 j/ W' S/ B9 K( p) t - printf("fd_socket_conn = accept()\n");
( L9 W; C1 _) X* l% f1 b - 。。。" i9 Z! {) c) X3 N" A ]
- recv(fd_socket_conn , recvbuf , 1000 , 0);
8 a! i% \) H' |* Q, } - }
) j: n- r. z) A - 。。。
& x' j$ R$ z- o/ C' S$ O/ U - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
8 }3 G' n4 J2 C5 b- d& d& ]0 A - pic_tmpbuffer = pic.tmpbuffer;
- p& E( L5 w( B. l3 d- }! E) M - pic.tmpbytesused = buff.bytesused;9 }0 ?% F, ^3 s3 m) I, P
- pic_tmpbytesused = pic.tmpbytesused;6 S$ [8 G+ |9 k' x+ U
- pthread_cond_broadcast(&pct);
; v. n/ |4 {( u2 g2 \9 D" Y - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;; v9 m$ n0 l; `* Y3 }
- pthread_cond_t pct;
" B2 F9 S8 p! w6 ^: E - int main(int argc, char* argv[])
K+ y, ]4 ^- ~ - {
5 [4 L8 \8 y/ h9 {2 I. l8 p - ...2 e/ L. e# S& a! f7 x& U3 m
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
3 O' |- c! ]$ u! W- c; V - pthread_mutex_init(&pmt , NULL);+ N# Y( n5 _! o+ [7 e" }
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
2 j- `8 z/ m* \+ @; s/ u - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);9 U3 q5 p2 R$ W; Z8 a+ z
- ...
' B* L; F8 l0 E - while(1)
. U4 }) a, f1 O$ @ - {
" g& i! [. ^4 t9 p0 [6 _& \2 y O - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);' q+ S( {; K6 C" [' q8 \7 C
- ...' e0 a6 N3 g; T0 a2 C
- }
4 I5 ~8 c1 s7 E. U8 h7 S$ m - ...% S; Y: K+ w+ p; R$ `
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
' [! {4 Z& R n t* p' N - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \& \- C J- r. y. }" T7 o
- "Server: MJPG-Streamer/0.2\r\n" \
0 y6 \' d* o5 r$ w1 V7 j7 O - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \/ C% }& m- p- w
- "Pragma: no-cache\r\n" \
+ J4 b8 T* q7 x - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"6 m& C- H7 ~8 a7 r0 W8 j
- #define BOUNDARY "boundarydonotcross"1 h( }% p) @2 [( f( L+ j
- printf("preparing header\n");
, L8 C5 T1 K; _9 l# q - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \4 g/ r/ d" c/ B1 J$ \3 \5 ^% X
- "Access-Control-Allow-Origin: *\r\n" \
) A' I6 c/ `+ w( T5 k; e f# @ - STD_HEADER \( N; U! `2 u) H+ ]% t. G2 ^7 `
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
4 k" W' W) W& N6 l8 Y9 ?* G) } - "\r\n" \! b; |4 [6 Y6 B; S) X
- "--" BOUNDARY "\r\n");+ V# @& b/ n& T) O, C2 q1 {. O' H
- if(write(fd, buffer, strlen(buffer)) < 0)- B# ?2 C- m4 q. `( I# {! M, N. S
- { i+ c* L" a( I
- free(frame);& _) [7 K8 y/ S0 n
- return;
/ Q4 m Y+ C/ W! P& j - }
复制代码发送完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" \
( u% P' W7 C7 ^! j - "Content-Length: %d\r\n" \# C# R4 N: }$ b& p7 l$ I% p
- "X-Timestamp: %d.%06d\r\n" \
' A. R, D9 i& S! E& N1 U5 Y - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);! x& f s6 Q" c+ r
- printf("sending intemdiate header\n");
( p# N: x$ g, M' S) m% u - if(write(fd, buffer, strlen(buffer)) < 0)
* R) l# }; [) e/ Y' q! C/ W# b0 O - break;' Q" S7 g) Y, K; o( y; h
- printf("sending frame\n");( V @1 K' K, k/ _& l3 D
- if(write(fd, frame, frame_size) < 0)6 {! T, t* p2 t; L4 L& G4 U
- break;+ Z- ^( ^$ c; T' Q0 z8 ?
- printf("sending boundary\n");
% j1 u' R% q% ]6 \5 d( S. S; K' L - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");5 F6 z! F, P+ ~7 E$ e( y
- if(write(fd, buffer, strlen(buffer)) < 0)
" z6 ?7 n$ y! R+ s" 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
9 n" h6 `7 A' D" b* H 8 L0 X) N3 k, b5 p( _/ z) V& e4 v
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:4 D2 h+ V# T% I% |# f3 j
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
3 \4 Y% v( ?" A* _' T: ` - {
. k" ]# J+ J! h s: j4 F8 B - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);. X. A1 U4 \. o2 m, f/ z6 b, b
- if(*socket_found == (~0))
/ M5 T% a8 c5 n0 H - {
" _- t+ X, l" B/ Y. K+ S - printf("Create udp send socket failed!\n");
1 Y1 I6 p! ~! M; H* } - return -1;) A- w1 j' \# |+ z& O7 ^
- }
' s4 D: x" e _ - addr->sin_family = AF_INET;
; F+ X* q1 D2 J - addr->sin_addr.s_addr = inet_addr(ip);
0 ?- d* |! N8 Q/ W3 H D0 \! c - addr->sin_port = htons(port);
. q3 `3 P- K$ A: {8 B; } - memset(addr->sin_zero, 0, 8);% b3 }, l5 @" [6 [# l: u; H% t
- return 0;
, w( i) \( x" {3 |8 O - }
复制代码
1 N. ^& W6 I) ]* A/ f' }! _" I0 A; A5 F
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
6 @. T. j6 E4 W x& H6 D: T! @4 O4 f- [ l0 Q* k
- w2 M( D) Q) Z6 G) {
- while(fend > 0)+ H- q8 b, M& P* P9 Q
- {
; B, C h/ v0 s2 [2 a - memset(picture.data , 0 , sizeof(picture.data));8 w* G7 |! N0 p3 L7 a- p+ A
- fread(picture.data , UDP_FRAME_LEN , 1, fp);
. f) `. R& @2 {! w* f H( D' @ - if(fend >= UDP_FRAME_LEN)
3 O$ }* i5 x- A9 w1 I* D% _ - {
7 g# Y' x- M5 c0 D S- E+ ^2 E* a - picture.length = UDP_FRAME_LEN;
$ h' Z$ z/ l" `0 Y - picture.fin = 0;
, v$ H4 D( B: l. \# @* S$ g - }9 Q X+ O1 n% I2 s. `
- else1 Q( F5 O- ?. G* z4 l% K: t/ w
- {$ L: Y- O* x: A/ S
- picture.length = fend;5 x6 @' M- A7 R+ `. `* W3 r5 T& a) [
- picture.fin = 1;
/ V; @1 Z' X f r% a: R - }
- Z6 @' U! @% k2 w. d - //printf("sendbytes = %d \n",sendbytes);- ^( K3 j- R( e6 x, J
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);, z. a7 s: R# J
- if(sendbytes == -1)1 D. r, t5 g+ F8 K
- {! k: V* D8 T5 h; R$ B& W% p; y
- printf("Send Picture Failed!d\n");
3 h- ~* ]6 M0 @6 B8 A* k - return -1;
! G2 e& O5 ^2 I. H1 C+ p# ? - }
. g4 L+ b& ^2 w3 ~ - else
2 j+ Z) K" A% V5 q O7 d( K9 W* Z - {
; M4 L) c2 }# l, @0 j - fend -= UDP_FRAME_LEN;( \/ x7 S* I% q1 g* D6 c
- }
2 p! b4 p: a3 Q; \' R - }
复制代码
6 E) t' z, i1 C5 f2 G/ x" N8 q; K+ F @6 H# c

4 ]! [/ i" R% X7 K3 x9 @& b3 ?
; @4 L9 d( W- _) G0 y# HiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |