本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 - s) z* V" _0 {
- o9 B- K0 R# l! z
2 i. {( v. p$ l8 p2 E7 ^作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html% ?7 ^: {0 F+ N0 Z4 G N
# I/ z& Y! w. \: u
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 4 ^/ k- g& ]" B+ c. R$ i0 }
 % a0 \- b( j& D3 @+ e- l
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
. s9 i8 Q1 V% r( P一、HTTP网页服务器: V7 H4 Z- N7 d
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
* W! C( m+ \- W% [ - {
. o5 T8 w3 B9 c w - struct sockaddr_in servaddr;
: p. M+ F4 \4 M8 Y* J% g& ^ - socklen_t addrsize = sizeof(struct sockaddr);, S5 w0 B7 [7 T
- bzero(&servaddr , sizeof(servaddr));& t% J: M* H# i8 F9 B" y* H
- servaddr.sin_family = AF_INET;
% L' R* [5 v7 p6 C9 Y- [4 Y6 Z - servaddr.sin_addr.s_addr = inet_addr(ip);
3 H8 D, I$ _ J6 K. _. g* |+ s! v - servaddr.sin_port = htons(port);( E% b" J! q0 {. U9 F) C
- int ret;3 N% V; s% t5 {8 p1 U2 M
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)( h3 e. V( `2 J" w; X8 g2 \+ H
- {
4 w7 N: H+ X* q g: {% f - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);6 H3 Z+ g5 o/ N# C1 B
- return -1;% ^$ d) D6 ]0 A6 i1 Q, d; e/ T
- }
% ^$ n. Z: t: x# O, S4 f9 ~ - int on = 1;/ l& V* u; `* q4 } U/ ?* y
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
* \7 B# g1 h1 Q) | - {
$ O# V- x) S2 o' B' p- o+ z" ? - printf("setsockopt error\n");6 @* ?! C" G% B6 D4 X1 `; v! J
- }
+ G3 G7 a/ h4 B! o+ v) ]2 \ - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);" q$ e5 s( {9 p' u
- if(ret == -1)
" s5 s! p* p# {% p) k6 q - {+ B. J3 O8 ?! M5 Q
- printf("Tcp bind faiLED!\n");3 S* l; O* X6 Z
- return -1;
5 `& c& f0 ^, S& f - }
. ^* e# [, a. _ - if(listen(*socket_found , 5) == -1)* i2 r* |' `; R, A9 p# K0 R. O( @/ j
- {
; J2 `' b% N7 M0 { - printf("Listen failed!\n");) u$ _/ ^ Q/ X5 g! i! P
- return -1;) H, h& C+ |- k% M7 t; i
- }# x# _& w I6 L" `& R) m8 e* S
- return 0;
/ j) ?) d+ b1 C- C: D - }
复制代码其中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);
, O& [! F- v4 h: n5 A& |' Q" Z+ a! F( D - void * Thread_TCP_Web_Recv(void *arg)
, N8 p$ t9 U3 X6 ?& \ c - {' d2 {) K( d. y
- 。。。$ ?6 ^5 A9 V- B3 l! G
- while(1) N u# i d( t$ h
- {4 s, l5 O/ g: X: ~8 ^: m
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);, {0 q# H9 o+ u$ @+ V7 C* H9 {( e
- printf("fd_socket_conn = accept()\n");2 ]( Z% G: f; P D3 Y% m
- 。。。
W7 Z' y7 d0 \! L" c" k! Q - recv(fd_socket_conn , recvbuf , 1000 , 0);
; M# J8 Z/ ]( j2 {$ K3 l& B1 p - }+ Z4 r2 v! f+ Z# p3 y
- 。。。' g ?4 j) {4 k7 m4 D4 m$ ~% T) [
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
4 Y5 ~3 M! y7 F# c0 X' G1 t0 V' w - pic_tmpbuffer = pic.tmpbuffer;2 l4 o, t4 q" U+ l
- pic.tmpbytesused = buff.bytesused;
2 l8 y! P. W4 r+ Z2 F - pic_tmpbytesused = pic.tmpbytesused;
5 L; t* u1 f3 F* n6 i3 Q - pthread_cond_broadcast(&pct);
* `5 |! m3 ? R - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
+ i! o' q$ p( b" j# f6 l - pthread_cond_t pct;# g1 r! @# i8 V) p! y
- int main(int argc, char* argv[])
' u5 h: C$ b% ^: b1 ] - {, G* }# U9 ^; N+ u8 `
- ...
6 S9 _; H' c6 Z9 R" q2 c - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);0 x" @3 R8 k+ [; u1 @
- pthread_mutex_init(&pmt , NULL);; k, I. m2 V4 g4 c
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
1 K) T R8 ]0 l: g f4 { - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
0 Q5 _: [" B: x1 w3 r7 z. r( \ - ...- y( d: G( n7 t' V4 ~. x) R) v" ?2 ]$ ?
- while(1)9 v' w: d8 U: J% [' E6 P* g, ~8 s
- {
6 S% f8 V8 U" _# p4 T - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
9 E' ~# X2 N3 N0 r" x# I; X - ...! ?; H5 {8 |" o/ N8 c
- }8 j0 a+ V( C0 x$ h, k: c2 F
- ...
0 r/ K) h. \8 F3 ~' G - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">' f; ?' ^+ E& l4 ?) C# 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" \
+ G, X% B6 _4 n6 ?7 P6 [) B+ y - "Server: MJPG-Streamer/0.2\r\n" \! K- U* _$ \1 ~4 b S8 x t0 ~5 u
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \0 e5 r! G1 q# S2 B
- "Pragma: no-cache\r\n" \
( X3 |1 w( [$ }. q - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
2 e' G: B& T' ?- n - #define BOUNDARY "boundarydonotcross"! g$ V! S) |( R/ F* c
- printf("preparing header\n");
' X1 K, P m7 M `2 p- V9 ^ - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \* o7 ~% L% ~: J5 g5 ^( v
- "Access-Control-Allow-Origin: *\r\n" \- J& U) ~2 v Y- ?
- STD_HEADER \6 i$ r( F. t8 ~0 z
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
0 m7 E5 c" ~9 [& I - "\r\n" \' }$ w% i2 V+ y7 [% P" s% C. c
- "--" BOUNDARY "\r\n");9 B1 f: B* I% x
- if(write(fd, buffer, strlen(buffer)) < 0)
) ^- y- q, k' f3 H - {& F( f# K3 N; }$ d4 H4 H6 {$ `# u) b
- free(frame);
! v2 g9 Z* \$ \, V5 t) D - return; s! T9 s8 A" }. 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" \
* m7 @, n8 J7 G X; D% ?/ ?- u1 H! E - "Content-Length: %d\r\n" \& k! x: \ h& J5 G
- "X-Timestamp: %d.%06d\r\n" \
Y0 x3 s5 V5 U% q* {# g1 | - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);& y0 y% Z& A. _' ^, `5 a) G
- printf("sending intemdiate header\n");5 C. {. }. ~1 _
- if(write(fd, buffer, strlen(buffer)) < 0)
) O9 S) V }* `3 ?4 Q - break;
$ W" D T- ~- k7 f& Z R' m/ Q - printf("sending frame\n");4 x" h. F& N2 B$ x) ]" v
- if(write(fd, frame, frame_size) < 0)0 K; z. p- L3 P" i7 N
- break;
5 O4 u3 V$ E1 E7 K! o' } - printf("sending boundary\n");
5 }: X1 t; T2 ~7 {% h - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
3 t5 w# s4 L" |* M+ w - if(write(fd, buffer, strlen(buffer)) < 0)
. y: e p4 k* `5 X8 j - 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 s* d, j- S7 E- X

. N# j& F0 @5 f二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:1 B2 c% [* u z' U$ O0 _8 Q2 W
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
8 [( p7 D, i) c" S5 K7 V2 \$ K( u/ w - { a% i7 k% W" `7 f; N/ ]
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);, \% B7 [" ~) u* O2 M& L0 O7 {0 W4 b
- if(*socket_found == (~0))* c* U& K+ |9 b! q4 t7 H! P2 b
- {& {" S6 D; [# B/ K! r
- printf("Create udp send socket failed!\n");
% }; p, X9 w; K/ ] - return -1;
3 X8 }" E; G0 j8 w2 V - }
) B; I/ S4 t$ C - addr->sin_family = AF_INET;
) W1 @, D$ l$ t7 I1 _+ p - addr->sin_addr.s_addr = inet_addr(ip);- ^4 S" J0 p; q4 I+ j2 F# K
- addr->sin_port = htons(port);
6 u3 v d( k$ ^ - memset(addr->sin_zero, 0, 8);3 n5 N* r: X# \/ d5 E6 ^3 V+ [/ r
- return 0;( y3 [# z' a& W# O% l, `" z
- }
复制代码
! M/ X: k& x5 z' q4 x4 Y5 U& U
. {" t# r$ S- t( ?& }而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
& M; z$ A/ K9 v+ F/ \
+ K5 a, b0 R( U" Y6 D" R0 G, t0 ~. r3 j2 P! m/ ^# v
- while(fend > 0)7 _. [% C, ~8 l* d1 S
- {
4 z' Z0 L# ~2 s5 ^7 U1 F - memset(picture.data , 0 , sizeof(picture.data));( P7 g A; b* x \. A0 c, b
- fread(picture.data , UDP_FRAME_LEN , 1, fp);
* y# D2 Q7 U- n: c7 K# x! F - if(fend >= UDP_FRAME_LEN)3 Y5 M9 _5 T) `7 G6 n- ]; M# x
- {
( c0 t3 i% ?& C - picture.length = UDP_FRAME_LEN;
. ]& v1 ^8 V3 H4 D7 u - picture.fin = 0;
! A- n; c$ ?: D6 B2 f - }
- A! n y7 \. e - else
$ e+ v* i* I( m, y$ K$ I: x- X3 L' ` - {8 V( A- R$ O* B" W- d
- picture.length = fend;) B! E2 |$ D6 n2 H" R
- picture.fin = 1;0 e7 ?) M5 |( n5 D
- }
. N- m( O6 m. Z; ]8 |; F, B1 p - //printf("sendbytes = %d \n",sendbytes);
! c3 Y, v* F! q8 J3 }) p5 I - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
5 U1 v r- s0 I5 w( b - if(sendbytes == -1)4 d2 S1 d7 x% }! E9 b
- {2 K, r, v/ ?! z# D! o
- printf("Send Picture Failed!d\n");8 d1 x( X4 j) m) S' h: l
- return -1;
1 l i, U0 ]2 V9 h w - }* L5 K0 d0 ~, g8 h5 }# ]; M# n, S
- else
( A6 Y1 T1 |$ y. l' A1 J4 c - {
2 U T* s: U9 f; R3 Q, [% M - fend -= UDP_FRAME_LEN;
! M8 m" X, Q- n9 K% {- j! h0 F) E - }: ~, P8 ?6 { [4 x/ t
- }
复制代码 " t" T1 l/ \# j" K* D0 A/ U
, O2 V* Z3 N) r5 A* I% K6 j
& f% `5 ]2 |, ]" L6 d" C. @
4 E7 K" ?( c9 e( S. HiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |