本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
7 s @7 [# I; e* }+ I+ [
* n/ Y5 }( G0 J! p5 Q, t ( K" Y3 L5 C2 B& A: C: \
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
3 p" ^6 O9 U" T* z, I M
, Y: c* k' ~# ?2 K本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 6 A+ x6 O ? p! \

* m( W' i$ B* G s两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 # R" G3 i; r5 R4 Q5 P) q* Y: N; {0 F
一、HTTP网页服务器$ w2 k2 Z. ]3 e) ]" F; _' I
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port). R, e M4 J% f6 W7 d- ~2 |
- {- r( B4 }. W& `' ~1 H% J1 F7 |# }
- struct sockaddr_in servaddr;& O* X% M* ~- A: V: l! a# p
- socklen_t addrsize = sizeof(struct sockaddr);+ u6 @# l( z& m( }9 D! h
- bzero(&servaddr , sizeof(servaddr));
" G- Y5 V! A0 ~8 p - servaddr.sin_family = AF_INET;
1 [+ w9 r1 X) b! k2 [ ~3 p) \ - servaddr.sin_addr.s_addr = inet_addr(ip);
2 _+ \+ [1 M: D& ^9 T6 U( q% z - servaddr.sin_port = htons(port);3 s" ?; W+ A" I
- int ret;
0 g# s! h2 D% |$ n ?2 x - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1), S1 t4 ?' s! p; a
- {; F2 y a+ d- P& K& y- ^
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);# J( Y% _5 `+ p$ R" [
- return -1;
: [% f G5 I, N# u: M4 Y - }
8 w& u0 @/ y! h- z5 ]+ l. v - int on = 1;) o5 o6 i7 v" p
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
8 o# D/ m1 [ n2 B. g, q - {2 m8 A8 a, T9 n0 R2 q
- printf("setsockopt error\n");
' A) T+ {% o% [& ~( U - }( J- Q) V: E {! p
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
M! q. O& j9 z, P9 n - if(ret == -1)* q- J @9 M$ N% P1 D. o
- {* k1 K- N9 X7 L% z4 X9 {
- printf("Tcp bind faiLED!\n");
! i4 e- o# K: W/ N; G% a; U - return -1;% M& L- ^, N/ N8 y+ |
- }/ v% G3 ]; E5 A( M) |* K2 _
- if(listen(*socket_found , 5) == -1)
. w% J% r& I) S7 ` - {$ e; S. y! r4 R: r
- printf("Listen failed!\n");
3 O/ T$ L, _; i+ S, A; N - return -1;
a0 T1 Z' }6 e- m - }* z/ h% j* i4 A% D; {
- return 0;8 }& O3 C0 u2 k/ T/ |8 f
- }
复制代码其中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);
" \5 u+ s3 b. g5 k3 {& t9 h6 q - void * Thread_TCP_Web_Recv(void *arg)1 j& b6 b/ ?. w. |+ P5 k$ H
- {& g e- f1 D5 f8 K9 Q: H+ t
- 。。。& }) n p. G3 _; T& B- A: {
- while(1) x0 r" w. |0 ~+ h7 n
- {
! m# P1 t$ Z! ]0 C - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);, t; Y! [6 S$ n/ Q8 I/ b
- printf("fd_socket_conn = accept()\n");$ v# k6 a" G( s3 a- ]
- 。。。$ I' g- w; {/ q3 ]
- recv(fd_socket_conn , recvbuf , 1000 , 0);* X. R+ k- M- q; d9 S
- }
" c! L0 t: C, q! D - 。。。
/ F: l" s' O) e+ G8 v$ W - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);) F) I2 J( i% |0 @
- pic_tmpbuffer = pic.tmpbuffer;' H7 F; `+ m8 |8 d* ]( p4 r. e
- pic.tmpbytesused = buff.bytesused;
9 h% \' j" r2 o/ S- z+ O - pic_tmpbytesused = pic.tmpbytesused;
: A2 e0 }" w9 @& f3 ~ - pthread_cond_broadcast(&pct); P6 t1 K7 B3 k3 ~0 Y+ U7 Y8 O
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
( h0 p% K+ `2 H6 c! ~( h, A - pthread_cond_t pct;3 ]5 c3 ^ e2 X& B. [
- int main(int argc, char* argv[])7 l% k$ |7 P1 J; |
- {0 l& ]/ y! U& p) s! y( U7 _. l3 ~
- .... Y, H8 M% X( s, S% T
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
2 ?& f" `; c2 z! t ^ - pthread_mutex_init(&pmt , NULL);
) r+ x" m) \% h0 I! |9 v - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);4 C: s, s% S& m; x4 D
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
d- O& D7 ^9 h - ...
( }2 I3 ]7 Y4 {! L# O" v: p - while(1)
+ o6 q0 i& | o. e B - {6 n/ y. @$ J% c/ @
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);% B9 Q' Q- X1 C& n" u( S3 r
- ...2 R% S. n" M8 ~ R% [, A& A
- }
) L6 L @. Q6 S! `* | - ...
# J; a7 V4 B- I4 ^4 O8 M* X. k4 L' U - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
( Z3 \- C( _% o' E2 O9 b& o. _" Z; ?0 S - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
& e9 x: x" w( s" K: B. ? - "Server: MJPG-Streamer/0.2\r\n" \+ F- [4 E/ K) }' ?: p; d8 i& ?
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \: G5 R* A, a5 ]( |$ A- u8 F
- "Pragma: no-cache\r\n" \
( U9 d5 `: A8 y( k# \8 b3 V* _; p - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
) @8 i/ D" y( `4 S, [0 n3 g - #define BOUNDARY "boundarydonotcross"8 V* h0 z$ Q! k" g
- printf("preparing header\n");
) a1 R* |4 m X5 | o2 v! y/ y - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
4 [9 G3 ^. L: G! P7 W E+ F2 ?" N - "Access-Control-Allow-Origin: *\r\n" \
* g1 m% e2 r, u6 \ - STD_HEADER \
+ \' o7 c( a* d1 j) c" o - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \" e; t3 g! u, W9 M0 E% ?7 w6 s0 f2 a
- "\r\n" \
/ R9 f! I- e/ f8 J - "--" BOUNDARY "\r\n");% `2 o( `# u' z4 R
- if(write(fd, buffer, strlen(buffer)) < 0)1 v) k8 `, K3 O9 a5 o
- {+ Y# N$ z" W6 { m% t+ I- t
- free(frame);
: m# \+ |1 _6 [/ i( N - return;
2 N- b6 T2 T" }) @! s/ b- | - }
复制代码发送完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" \( j, `3 c6 m V' V2 H$ k% g
- "Content-Length: %d\r\n" \
; A7 s* X3 V) a+ P( q( C1 E6 i - "X-Timestamp: %d.%06d\r\n" \
4 j- S* `9 d( ~4 J8 }0 i - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
+ K0 }; E4 M3 o: u$ ?8 f% C' b - printf("sending intemdiate header\n");/ P, ]! w, e) i7 J3 p2 @& ]9 H" X
- if(write(fd, buffer, strlen(buffer)) < 0); P; F5 O: i3 N; `
- break;
4 I6 e, l( V5 G5 T) l6 Z3 F - printf("sending frame\n");$ S" V8 `2 t% u
- if(write(fd, frame, frame_size) < 0)
/ r$ ~9 X; v6 J, u) c. y - break;
- Y: f4 T- Y! a2 ? - printf("sending boundary\n");
- n6 b9 @4 L7 d" W* x" f9 w0 v N$ E - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");( G! F- V9 b% E; I8 f1 _* T
- if(write(fd, buffer, strlen(buffer)) < 0)
3 ^1 |$ K$ ~+ V( X7 t' i, u; k - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  / k9 b4 g2 L: a

9 @6 H$ b* D: U I0 n* e+ x二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
2 N; E& ^. D8 m) d- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)5 D! P1 Z9 \9 I! t9 ]
- {4 _ N+ U/ H" P$ r; Y4 d: e7 w
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);0 {. b3 v& A, K% u& r2 e0 m, d
- if(*socket_found == (~0))# b) \; c+ J' B0 `& R
- {2 t" ^9 M+ @7 |; T
- printf("Create udp send socket failed!\n");9 C% q' P: R6 J. G) ?# P& p3 K3 S
- return -1;( c" [8 r3 A# W: z+ F6 l. Z, f8 [
- }
6 g2 h- T# n1 r+ J5 [& h8 O - addr->sin_family = AF_INET;
1 f& j4 }" ]; w% Q5 x6 n - addr->sin_addr.s_addr = inet_addr(ip);/ g. L, M {5 L4 G& R2 P
- addr->sin_port = htons(port);
$ e3 o% p+ [9 X' E9 r, E; T3 e - memset(addr->sin_zero, 0, 8);- T0 F, W) E- T/ o+ ?0 U1 n3 v8 ]
- return 0;
! M( e2 @6 g7 R. t/ Z6 e. Q - }
复制代码
) D2 t, b }. N/ @( n7 K/ I7 G5 w, K
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
% e" R8 E, b* g6 O `
+ @/ E/ J- `# {
4 {# ^# P1 I+ _- while(fend > 0)) }, n1 D* e- a( N! D5 p' L+ B) I% O: `
- {
9 Q1 w+ Q& |; t: [/ n- ? - memset(picture.data , 0 , sizeof(picture.data));
3 c% [0 n1 p5 ^0 ~ - fread(picture.data , UDP_FRAME_LEN , 1, fp);% }' g3 x) a3 N) [. W* V, n0 y
- if(fend >= UDP_FRAME_LEN); q* U/ ^! L0 Q- `8 V
- {
' x# T# D" ]" H0 M& H f - picture.length = UDP_FRAME_LEN;8 |/ f4 T0 C+ e* a
- picture.fin = 0;
8 v% e( _# g6 @- V& O: k" z - }$ H7 i; Q3 Z" @; G3 U/ K
- else
) T# A. ~: Q R - {& H% j0 p& { j! z% H' G* N
- picture.length = fend;3 ^+ i1 u, N1 g* T* i
- picture.fin = 1;) C: Z* q5 p8 ]( B
- }
Z- ^/ ~; W5 m - //printf("sendbytes = %d \n",sendbytes);
# S$ ]/ H8 T/ K9 i! T! E9 [ - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);" f- x5 p! n- e5 z H* r
- if(sendbytes == -1)
8 L2 x9 `4 l! H" [' m& R - {8 g8 N# i; @$ ^% `: Y
- printf("Send Picture Failed!d\n");
- T, v' c! `1 b3 O+ o( o - return -1;: f. S$ I! w2 n0 b6 C$ e0 g
- }8 ^0 P" @% }5 r' C' C! U& Q
- else. R$ s( g3 {% h- f! x9 x& R
- { N$ F+ v/ B ~ \/ a3 f
- fend -= UDP_FRAME_LEN;( | r* j" g: M5 m5 ^
- }
# l+ T) \; T- i# n - }
复制代码
- H/ b! f. x1 l1 D; Y7 `- `* C# x5 L- t0 [- q! n+ y
 ) x+ e; r9 m& k8 B Y9 X
0 r4 z" P8 y; @* Y4 I4 riMX8MPlus 核心板: https://www.forlinx.com/product/136.html |