本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
7 E. |, x# L$ l0 I- W5 v& c: U x1 i3 d3 s2 T0 k6 u2 e
 7 x6 U' B( c9 C' t
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
2 N6 A# C" E# A: D5 B9 k: S: i: `, L Y/ Z. k8 t
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
1 G: m! L6 z d7 Z! V. i! b! O
5 w) z3 p3 w. d两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 . ?$ n0 [ a# u0 B2 o! y$ h
一、HTTP网页服务器+ v. s) C( e! w2 A+ }; f
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)9 I6 ^0 x w4 j9 P- E5 Y# G k+ i
- {0 O$ S2 C$ g, r1 b
- struct sockaddr_in servaddr;
3 `: c0 `5 @# U' G8 Y8 b5 i - socklen_t addrsize = sizeof(struct sockaddr);0 `/ H4 |9 A' Q
- bzero(&servaddr , sizeof(servaddr));& e$ |! _! r" R4 k2 N/ U5 h
- servaddr.sin_family = AF_INET;
+ J$ V( q2 _/ q0 X t. L - servaddr.sin_addr.s_addr = inet_addr(ip);. w+ n& z6 H* ?# Z7 ?
- servaddr.sin_port = htons(port);
( |8 q$ X- C* M - int ret;' t k! y7 p/ T* C
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
% [; K2 w, M7 ?" D% L3 T& Y - {
# U" c9 E9 b0 t- B - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);# V3 m8 m Q, g1 W
- return -1;8 s }2 r) k3 X7 W1 Q- t
- }
( L/ s, T! Y6 w1 c p0 a, N% Q - int on = 1;1 ^ j% L( V% W( ~) H" U
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0); ^$ W( \$ a4 [7 e3 t, P
- { p+ x: E5 o# Q9 v& O6 y
- printf("setsockopt error\n");
& ~6 l$ V" v1 {! G - }5 g/ N+ g" ?2 f D: L
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
! l' ?3 H& w) I4 L1 u! w$ V - if(ret == -1)
: @; Q3 g; O1 i) {. X8 s& @; P - {
# W/ b" M l7 S! t$ t4 M - printf("Tcp bind faiLED!\n");+ w V8 N- j9 q k2 C7 L$ T
- return -1;/ K, `2 d% j8 E! R* b7 z9 v) A$ Q
- }5 G- u; V; Q/ Y
- if(listen(*socket_found , 5) == -1)4 U% n5 f- S* U
- {& o) w2 Y9 O0 x4 w _
- printf("Listen failed!\n");
4 ~2 g2 t4 }) }/ }* s' g - return -1;
% H2 O0 c& x- I G1 k; }5 Z - }
# S. e' T# Y- u8 {/ N( ^* `; v7 } - return 0;, A" I6 L4 n- V7 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);' x% ?9 o8 b7 I: v+ }! x
- void * Thread_TCP_Web_Recv(void *arg)0 y: M% o: M/ v. a1 Z
- {
, n; q4 I! p3 f* Z5 s6 b# R - 。。。7 [* p6 J$ X: b' y5 U
- while(1)7 M: C( E5 ?' f8 G
- {) w8 M- m. s- R6 p& |* |" ?4 C$ J. f
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);, f5 a! X+ g# x! Y7 m2 y7 X3 N
- printf("fd_socket_conn = accept()\n");" F( _- |% {" C' e
- 。。。7 W4 p6 l- k1 C
- recv(fd_socket_conn , recvbuf , 1000 , 0);7 a7 j; C! K2 Y3 [ k7 k
- }& t# N5 X! H2 W8 e4 D
- 。。。
+ C. N6 ? n# q% e9 t - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);( {4 ?/ ]3 x0 I1 \. D( q4 W
- pic_tmpbuffer = pic.tmpbuffer;, v4 }0 N% V( ~
- pic.tmpbytesused = buff.bytesused;
( l0 c+ ^: A: g0 V6 t* N, M4 | - pic_tmpbytesused = pic.tmpbytesused;: ^2 B0 D( l8 O0 ? h* G* j& A" O H
- pthread_cond_broadcast(&pct);" @- e$ \0 e4 L0 g4 {4 U
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
$ n( Z. |% _% ?7 L- x - pthread_cond_t pct;1 D9 F' f! _. b! j. c r8 N! k
- int main(int argc, char* argv[])$ k4 Q0 Y6 B3 [; u
- {" v: Q7 P' R; V$ L
- ...6 q2 A5 p: l7 q. E8 ^
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);+ K/ i; s% c: U0 |* ^' T
- pthread_mutex_init(&pmt , NULL);
' @: h" j# x" j" p! ] - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL); n. Q; J( a7 |8 x& s: t f, P* v& }* S
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);2 Y6 Z3 A% ?( K; @3 V% G1 ? P
- ...
1 e: l- B9 F r% r6 }3 j D: B - while(1)
# h- B! j9 K% U( D2 n" w - {* Z0 ?1 R* b" d* _6 w6 j) t3 V3 p$ ?
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
" k' Z, Z7 I& P - ...; W) W* v, c/ a m/ D0 l
- }
. ^) z7 m; ?) [$ t+ ^+ v - ...1 d$ {' P7 E" { V* p/ E
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">5 V- _1 k" n0 ]4 w8 Y+ K" 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" \$ ]" M; {: \; | x" N& Q
- "Server: MJPG-Streamer/0.2\r\n" \
1 T: t( F1 D) b( c+ J - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
* T! ~0 X2 [9 ~1 R - "Pragma: no-cache\r\n" \
. ?3 D* }9 g2 S0 ~ - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
. o7 J0 h& t0 O4 N0 G - #define BOUNDARY "boundarydonotcross"2 Y: G) M' g+ U
- printf("preparing header\n");
" I+ b* m2 t; u5 R% j - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
( d- {9 n+ _3 p! v; f - "Access-Control-Allow-Origin: *\r\n" \
; H9 h8 I% N/ z* W - STD_HEADER \
4 `5 A# t5 V+ ~ - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
( F; R4 _# V* Z3 m8 [0 | - "\r\n" \
( h7 f# V% o; B$ Y - "--" BOUNDARY "\r\n");6 l8 v+ Q) {8 q+ W, D" ]! q$ \
- if(write(fd, buffer, strlen(buffer)) < 0)
5 O/ U# b2 t4 L - {
. I* N A8 p F0 x9 S% i1 @1 J - free(frame);
4 ^) o) x. G2 k: ~8 d2 |$ q- `. m - return;
: g$ W" Z. ]0 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" \6 L! F i3 E8 }% A [7 ?; d
- "Content-Length: %d\r\n" \" i* m6 \8 r/ m7 y
- "X-Timestamp: %d.%06d\r\n" \
- Y. m/ A8 a) ~% y1 [ - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
9 N/ z( B) X! H7 |6 l - printf("sending intemdiate header\n");
, v: ?8 ]: R7 z; {6 O/ y - if(write(fd, buffer, strlen(buffer)) < 0)
2 s# y" B j* x+ j - break;2 f/ o; f' F3 L7 O! e1 x# j
- printf("sending frame\n");
1 I; J( Y. l! y7 d4 d, H8 r0 |+ } - if(write(fd, frame, frame_size) < 0)/ V/ t! k7 ]( G5 W- }6 [- I* A6 Q
- break;
k+ e) I5 `; o3 M - printf("sending boundary\n");
) p: U/ i& `4 y m. z$ Z- o4 Z o - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");: h) f( A* }* p- [/ ~
- if(write(fd, buffer, strlen(buffer)) < 0)2 F9 P$ o6 d# n; t: O
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  " C4 r" v% s5 I$ E6 b

) T7 A$ g' X8 D4 O# c二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:- W4 X- C! K$ O) H# ~- t* Z
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
0 A& o! q) r" r2 \ - {% v; ]; m5 k' ?; P
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);; b5 C2 c2 J0 @2 [' ^: x
- if(*socket_found == (~0))' k( s$ [1 s2 S2 o
- {
3 O8 n, d: z$ e& g6 d - printf("Create udp send socket failed!\n");0 l# H2 [- k9 O+ K3 y
- return -1;2 A' }* U7 B# S/ ?9 j6 [" T
- }) M* d5 |, S% P4 O1 }
- addr->sin_family = AF_INET;
& b3 T9 r9 |& g$ [$ T0 U - addr->sin_addr.s_addr = inet_addr(ip);
, O5 D$ Q4 C7 K9 E0 t9 }; H1 ~( @ - addr->sin_port = htons(port);% t+ R! U, c4 h/ k7 k- L$ \
- memset(addr->sin_zero, 0, 8);
& R* `5 y6 {& s# Y4 k: v% \ - return 0;- R/ L+ Y9 ]2 ?7 j- S: w6 t$ M$ C
- }
复制代码
; e$ ]9 [4 x$ o S) L6 m
& `. v k2 s0 p( T9 ^+ x8 }% c而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
9 B7 Z4 M" d7 e% I$ }
. a4 Q$ k$ W( f) s) G
( e T) p; K3 Y/ z3 E- while(fend > 0)' v" l' A* w0 }; t2 }
- {
3 C+ C" v8 W; u: W W& D9 N - memset(picture.data , 0 , sizeof(picture.data));
& {$ P0 T" b, W/ h" E7 ~ - fread(picture.data , UDP_FRAME_LEN , 1, fp);
* _7 E: `& o0 L/ |4 M - if(fend >= UDP_FRAME_LEN)
7 N; B2 p" G+ \; R* O: ] - {
) s3 _+ T( n* ~, n( i3 i3 p - picture.length = UDP_FRAME_LEN;! F% ~; p" o; J/ R$ F
- picture.fin = 0;6 X5 a6 G% I" y8 H' J+ g
- }
2 H; Z5 |" a# w - else# G, d% Q1 o c# Y# p
- {' @; X4 F ~( d; E
- picture.length = fend;
6 ?6 H& _6 O7 b( {% K' g - picture.fin = 1;) `, F7 N: g( i0 e% | a7 x
- }
! u6 t- X- B& w/ |5 Y6 K' G3 j - //printf("sendbytes = %d \n",sendbytes);
* a$ c7 P% U' F( a - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
2 y# S- U% p7 N: C4 Z4 u; o - if(sendbytes == -1)
& S! B7 _: ]8 ]) k" j - {
7 v' |% u# Z2 D0 T* l: E2 M - printf("Send Picture Failed!d\n");
, g; [1 k, @! Y - return -1;) u( A6 U- M b8 I* e# {$ j
- }
, {- Q1 }# ?1 }' ~6 } - else
# i, \4 n( P7 o4 w) K3 G( l - {
$ v# c4 q; r. I9 Y5 q; @" R, ], o1 L+ D - fend -= UDP_FRAME_LEN;/ v" N3 O3 p" H, |6 n& G
- }
" t$ ~1 W0 ?5 G& X0 U) Y& ]% m - }
复制代码 , b3 V2 U; R" l1 x. n
8 x! e2 L) c: Q) ^1 U

/ g' q* J# }7 N. b* P1 z7 y4 d
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |