本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
: H; A) v8 M4 g3 A* e/ ~! A/ ~0 m2 {% g; I& w

6 h+ B5 Y' k4 y2 P% v& V I- T/ y作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
s0 f. q- p8 B/ m3 m) Q' W b! l+ B
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
% d4 G& t7 e# q }$ a4 Y5 e" m : c0 V$ \4 U" B1 S- E) L6 Z
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
" m! E v( ~% G: }* j- I) [# E, W一、HTTP网页服务器
% H2 A& p, M+ B( l4 R先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)9 `* Q% X- L- p8 ]; d: E
- {6 w1 Z# [. B; M$ R
- struct sockaddr_in servaddr;" i0 q& {; E* T% V3 {
- socklen_t addrsize = sizeof(struct sockaddr);% i" e; Z: b& q7 q
- bzero(&servaddr , sizeof(servaddr));& f9 b- G* ^8 W* v9 P: M9 h3 U7 B
- servaddr.sin_family = AF_INET;+ z- @; f) S9 x+ u- {
- servaddr.sin_addr.s_addr = inet_addr(ip);/ }' E9 [4 k" E$ ]2 U4 r
- servaddr.sin_port = htons(port);
5 V- Y6 I+ O* E$ ~. z- U9 @7 o8 p - int ret;
! C) l$ f1 Y4 O# [! t' I& L+ n - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
5 y; p( ^( C! Q) ]) [8 v - {7 ?5 R' F3 | g! Q7 A1 ^1 m
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);; q( K. j; }2 L# D
- return -1;3 u$ l% d x. q
- }
* M. c! S4 |8 T& h, r - int on = 1;
" o. q" D- T' _4 k% _( w" \ - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)% m% e# @0 t$ w6 P* }/ O
- {( r8 p& M& u% {+ f" J1 L
- printf("setsockopt error\n");; F9 B u' _6 h& o( [
- }
[' b/ H4 b& O# o - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
. } s( k4 q. |7 P: \1 o% Z1 B1 z: `* h - if(ret == -1)8 t$ _ H- I5 g0 ?9 r
- {
% `. n) U* \6 c- n3 g* f z. S6 N - printf("Tcp bind faiLED!\n"); T, y8 }) o" r
- return -1;, O6 q1 H' W* B p4 A
- } R( m/ _* y* g8 ~0 V
- if(listen(*socket_found , 5) == -1)
7 E1 E0 l3 ~( w* q4 X- C5 I - {
9 L7 x/ o1 p4 }" H" S% j - printf("Listen failed!\n");) N! Q/ k' r& @3 x" M
- return -1;
8 v$ `; O8 {* W% a& b - }
; y" U/ ^! x! {4 M& c, b - return 0;
* G1 d0 }; F' O0 ], z7 y - }
复制代码其中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);$ r6 ^/ T+ w+ _
- void * Thread_TCP_Web_Recv(void *arg)
. F! L2 O1 N. M- C. ^ - {' L# M; _- s! a; h" a; u' E7 n- u: Z8 y
- 。。。
2 e0 a5 B; O* n - while(1)
4 h+ I: {# |' V. q2 ?0 N5 C - {8 t( ?: b. O) P/ ~$ B
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);( f! c( K, m( _% Q+ p
- printf("fd_socket_conn = accept()\n");) p9 g6 D5 Q; R0 t
- 。。。3 [8 B7 C5 n0 |6 j% t% ?1 V; d
- recv(fd_socket_conn , recvbuf , 1000 , 0);
; }$ n! C( [3 R7 d: u/ H2 F - }) _; F7 X" V. Y2 r7 K: r5 {
- 。。。/ ]6 P9 O$ \: Z2 _& I
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);6 H0 j! W8 p r1 \6 R6 \. u2 j
- pic_tmpbuffer = pic.tmpbuffer;
[5 `$ `. Q% x# I6 t6 S. m - pic.tmpbytesused = buff.bytesused;3 |7 v* |) u- o# V- A$ h4 N
- pic_tmpbytesused = pic.tmpbytesused;9 P% Z% D. p5 Q$ n5 W) b' K
- pthread_cond_broadcast(&pct);" L# d3 g' N# E
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
% I* y3 s8 {- A% g# C( N( _ - pthread_cond_t pct;
1 ?, j8 l$ R) {& E - int main(int argc, char* argv[])
5 ]) x9 D. C) x* G J" f( C% u - {
. a2 _" V; f1 L! x* u7 d, S - ...) N7 i3 ^& q' g& t; j9 e. n& s
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
4 C0 }: y. d) Z - pthread_mutex_init(&pmt , NULL);
3 W5 f' k- R5 n- [1 V. g7 e: e - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
$ B: o/ \7 k* L: H1 N X: b - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
1 H8 T( w5 W' i, { - ...( {; x, Z% y% w7 ] X, d- r
- while(1)+ m+ S1 E# Y) J
- {$ N( f% q0 |% E: f, }( p
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);4 y# _9 a6 B" T9 Z( L* f
- ...
1 {' F1 F$ `% f7 M - }. {. E# U( z/ l
- ..." ~$ k9 _! ^/ j5 A' p. B" w5 o
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
, T7 V) D7 ?8 S+ q - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \* g6 z- w3 s+ Q( z
- "Server: MJPG-Streamer/0.2\r\n" \
; P2 ^1 x- {; B% J( B* Z, p - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
0 H: v. I3 h9 s - "Pragma: no-cache\r\n" \
, Y) B k& K- ^ - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
: i( I6 O8 f) D - #define BOUNDARY "boundarydonotcross"
4 \6 Y6 Q M: D - printf("preparing header\n");
! W9 G, N7 {# [6 u) [8 s; D - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \# ^4 Q6 l" H; {: ^' I* I) Z
- "Access-Control-Allow-Origin: *\r\n" \8 b7 S/ H k2 ^3 v- p0 N
- STD_HEADER \9 M" n' @0 z- a1 P3 M# ?
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \7 _4 F/ z/ P" p0 T: w1 z' b! q
- "\r\n" \
# W, i0 _$ x+ [% w% F0 r( t - "--" BOUNDARY "\r\n");
+ }- D% l1 a$ |# }% Y - if(write(fd, buffer, strlen(buffer)) < 0)
$ N% s9 X, _' ^/ E% P+ b8 a+ O6 X2 l8 r - {
D% e, B& ~8 A! Z9 p$ n" K `/ X- B' r - free(frame);
* K# w6 i7 w$ |, T# t0 v2 N9 G - return;
3 F4 n4 o8 M* G' e( j: E. _+ u - }
复制代码发送完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" \
! K* X) @; y: k - "Content-Length: %d\r\n" \
+ j, I7 [! l$ Y3 {. f - "X-Timestamp: %d.%06d\r\n" \
* U) M2 Y! C3 e) }, ?2 [ - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);5 x% } f" J# Q# b5 `% _/ K4 h
- printf("sending intemdiate header\n");
( p- |6 T0 U7 r) \/ V8 R. S) g0 W - if(write(fd, buffer, strlen(buffer)) < 0)( b7 j/ Y6 C# b/ ?. ^3 ?( ^
- break;4 `& \" s: K) V3 O
- printf("sending frame\n");
* H# x* D) E/ Q7 r9 ` - if(write(fd, frame, frame_size) < 0)
, ~# k' K, R' a - break;
/ w5 ^* R h) _& z8 ~; Z - printf("sending boundary\n");! u6 S3 |. b5 I: ~4 e6 I, J1 t0 e
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");2 G7 X, v0 Y7 u+ O% O3 L
- if(write(fd, buffer, strlen(buffer)) < 0)
% m: b9 g$ g0 j% r6 S/ _ - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  7 g" p* f/ |9 P3 J; N: H) G. {

- a7 r7 ]* N, K7 A: h$ m! e0 C& ]) X* b二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:* x) S0 f# ?7 B4 }2 s! L% _
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)- q+ \. {$ o; D4 _! ~2 O
- {; u8 C ^, R# q* w
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);& q# X$ Z' p+ K, R( T8 I; N- v; M
- if(*socket_found == (~0))! c: R' ]. D: g( R
- {; o$ z- f2 M$ c z1 B
- printf("Create udp send socket failed!\n");
& t% ` J/ H; a8 m- G! y - return -1;$ T# x3 b8 i3 j7 c& T! u
- }
3 l# `0 B) Y) a$ O" ` - addr->sin_family = AF_INET;
/ J: ^' a m% o/ _6 `7 R6 T* t5 F - addr->sin_addr.s_addr = inet_addr(ip);/ D/ G( t/ n" X( Q/ e5 \
- addr->sin_port = htons(port);
. A% W8 r }0 m% c - memset(addr->sin_zero, 0, 8); G+ y d5 x6 ?0 p9 \: F
- return 0;/ K! B! v0 ]( I! E9 A
- }
复制代码 3 B, G9 g: A, R: L1 s5 Q) ~! h
: ^ i* M4 D- I7 F \2 F9 N, ^而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
7 q2 T0 n* Q* j$ m: W: _! h$ P" X9 f& P5 N6 v6 X8 d/ w. D" S! ~
1 b) w! ?+ q( R5 Q* [$ _" L3 Z- [* m
- while(fend > 0)1 s+ j! j* ?( ^& ?2 r9 ]
- {
' D5 \4 |& Z& B+ T9 K% [ - memset(picture.data , 0 , sizeof(picture.data));
! V& E+ _" R% ~2 e$ f - fread(picture.data , UDP_FRAME_LEN , 1, fp);. P7 V# l5 \ [$ i; |: E, Q
- if(fend >= UDP_FRAME_LEN)
! p. w- H# k' R* p, ^) V# d5 Q - {
8 J* | v& a4 p/ P: w - picture.length = UDP_FRAME_LEN;
+ c9 N7 P$ `( W2 i8 G - picture.fin = 0;, N8 l6 X# a7 `6 f: a7 A4 q" T( z, }: {
- }
$ C! z, d( `5 B q* n/ |% C& @9 {" R - else
; s9 X0 b: [5 E. C0 ^4 U - {2 r$ `) x$ Y2 J. p$ u" `" U5 n A' k
- picture.length = fend;9 |( L+ A) T' L+ |7 |. E# F
- picture.fin = 1;! g* L" X% H, w4 D6 j" x
- }2 D- p/ {' H* a' ?' v3 S
- //printf("sendbytes = %d \n",sendbytes);
+ O- v5 [$ x+ q6 C - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);0 m3 z2 y. a( B
- if(sendbytes == -1)
; |. u' Q7 d- |4 \/ R- r* E. P* n - {
( m. b2 B# W6 v$ G } - printf("Send Picture Failed!d\n");
. ? m/ @/ b9 n/ \( l- {) v- G3 |5 j - return -1; W; K! {4 v* e" B) ^; i
- }" W: m8 H4 O F2 ~& K* g- y* Q5 P
- else1 A2 P5 L* H2 u7 w+ r( N' L
- {
4 z) I; y( M1 \& i7 z- J4 D - fend -= UDP_FRAME_LEN;
4 d* k$ `6 l2 w* ? - }
- t# F( |: @( K: V" d1 }3 c - }
复制代码 ) b* A! P8 a0 ]+ m, u% t
: L( I$ g9 Q+ o* r; c+ _9 n* x

9 v" G: h; i; N: F8 b2 P2 z' |& x& b0 i" z( I
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |