本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
x4 B+ z4 ?6 r& k j3 \! y) t7 C6 i/ E' `" Y

* {* c$ [1 m0 P% f5 Q+ c作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
! { y: l2 T: ]2 x; e8 Y
. ?. O: b. [+ k) e3 C0 v9 T1 K本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
" D" X; [/ U, L x2 z+ N ! J) B3 E: W9 ]: v
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 9 Q6 f( @; E( N+ c" [9 c2 l7 E K" G
一、HTTP网页服务器- B" E- ?1 w; f0 |
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
$ p" v# |. l& V9 d$ H - {
$ A0 c3 E7 P% E1 V' \% D- T O - struct sockaddr_in servaddr;$ T {6 }& R `$ c# ]7 u7 O+ \, z
- socklen_t addrsize = sizeof(struct sockaddr);
1 i- v5 o* l+ q6 n0 L3 ]: i# k: ] - bzero(&servaddr , sizeof(servaddr));. U8 g1 V. Z! _
- servaddr.sin_family = AF_INET;
+ R# O5 Z' w9 `4 u0 R! i - servaddr.sin_addr.s_addr = inet_addr(ip);# g/ y9 k$ I6 Y1 x$ p
- servaddr.sin_port = htons(port);
V2 V4 `% a/ Y - int ret;+ ]6 P* @- D3 |1 d' N
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)" h/ g# N( ^" X: N
- {; x/ u8 b2 L# u% ^: N/ B( h# ^. O
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);6 i7 ~' t3 {' u/ C
- return -1;( W7 O% S; Z4 E. D9 ^+ p: W. Z1 G0 N, R- x
- }
# c6 d* s5 H- x& M, Q- o" } - int on = 1;4 w# h5 @- H% {1 D% j2 n' p D
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
F X( p# o* T5 c& M4 c# k - {
& y* ~7 S3 f" y3 `- h, P2 E - printf("setsockopt error\n");7 ~8 ?% x F( U. }* R9 u
- }
& J- y8 ^" s: d" T0 D - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);8 ` p. E& F$ k) ~: u$ p- H8 y
- if(ret == -1)& D) l* ?# c5 |6 y/ w1 b
- {1 M% v, Y) L# X) ?
- printf("Tcp bind faiLED!\n");
: ?9 Y; Z [( U0 a* K) x2 v+ j% p4 { - return -1;
0 t& o8 X# X4 \- ^ - }
9 Q: _2 w2 O; \: y, `( t n ` - if(listen(*socket_found , 5) == -1)
, Z. v0 Y7 f3 j - {
; d, _; k, g5 y- Q! y' |9 H - printf("Listen failed!\n");
. G! s, L' z5 J- L - return -1;
t; i. l9 w8 E1 d* [ - }# Z. N$ V% k3 E. @) d+ {
- return 0;5 ]) b5 |1 r% l' j
- }
复制代码其中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);9 @. q @6 t& V: \* K* {; X
- void * Thread_TCP_Web_Recv(void *arg). q/ r! Z) j# ~( p" l
- {
4 q$ [9 j0 K; `8 c - 。。。
2 c! g8 M/ X0 }$ C - while(1)$ y$ F% u. x9 y$ m
- {* B$ @9 A$ t% L1 H+ c
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);+ `' Q( N9 ?* @/ m
- printf("fd_socket_conn = accept()\n");
G+ E u# Y+ N3 G% t2 P - 。。。
& X* V) G( d0 J. d$ \4 m - recv(fd_socket_conn , recvbuf , 1000 , 0);( O4 w' N( }$ c/ a8 g
- }% A- Y2 G6 c0 \+ t: @/ u4 S) ~9 v
- 。。。$ R7 e. y D4 F7 K* c
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);- ~* T- Y [8 B a& e
- pic_tmpbuffer = pic.tmpbuffer;" h5 g4 r* X4 |1 P* P6 j% A$ m
- pic.tmpbytesused = buff.bytesused;0 }% G+ B& |8 p: R3 z4 @
- pic_tmpbytesused = pic.tmpbytesused;- P5 G, O" ]( v$ Y5 V
- pthread_cond_broadcast(&pct);+ v$ h2 ?- C* T0 X
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;8 h9 S# }6 H @# r
- pthread_cond_t pct;
7 _" B% |! P! J$ X: Y9 Y' O, c - int main(int argc, char* argv[])3 l4 [6 W! Z$ G+ u; X: q/ ]
- {
' [+ }$ A4 ]. c$ j, u- t8 C$ f - ...0 c* e% R3 U0 m) d
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
2 R( N4 I! L7 s$ ?' `5 g - pthread_mutex_init(&pmt , NULL);
7 L& e/ w& [+ i) l) o - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);4 k% Z( N: u9 [* F( x* H" l
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
5 Y+ ]0 _( `# g2 b - ...
/ N3 o3 F- X9 e: [ - while(1). Z8 [% x4 a: e' c' O* P+ M
- {
0 h+ @, {& W1 L- A3 D; T - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
: s/ J0 R& K6 o, U4 d7 F - ...+ `5 m; V+ ^0 L9 ]
- }
1 P2 j8 u' K2 X' |! E - ...
) J) Y+ F; z' k' `$ f- f - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
6 R& [( H& {( C* ^" s- h# o0 u( M" C - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \. d/ ?* B% ]9 Q4 y( }6 j8 L
- "Server: MJPG-Streamer/0.2\r\n" \
3 K# t) N; ]) X8 X1 w- p7 m; S - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \0 T C# y) o" R# k' J; N
- "Pragma: no-cache\r\n" \
7 `% y$ n8 e; ^6 j0 _ - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
$ q+ {) f0 [! f, ], L - #define BOUNDARY "boundarydonotcross"
+ H6 e" K" S" }; [( o! Z - printf("preparing header\n");4 C N: p! g5 m$ R7 }+ T' k
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
6 d, I( C Z& S/ `' U4 h - "Access-Control-Allow-Origin: *\r\n" \
4 x7 J3 g2 q' @4 \ - STD_HEADER \: d" c) J: X7 V2 V7 i3 S r0 j
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \ i" i! v# k& n; E- O1 y
- "\r\n" \
: E7 X5 n1 x8 l7 o: y* L - "--" BOUNDARY "\r\n");
7 b% F( d7 Y# f: h - if(write(fd, buffer, strlen(buffer)) < 0): v& q+ @1 p0 c
- {
8 ~# k; _' c3 F& r' ~+ K) [ - free(frame);
# k( d3 s' U; b3 V3 w: v - return;
& }0 P: u H6 x3 n8 S3 T$ Z. 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" \8 a" e' S- G# l/ ~4 w1 m+ K
- "Content-Length: %d\r\n" \5 j; u. T( o9 E; E7 F
- "X-Timestamp: %d.%06d\r\n" \1 n& g5 c& T8 b7 \, R3 Y
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
4 W3 a: L) U5 t/ D. ` - printf("sending intemdiate header\n");7 ^ Z: g( z+ d. { j& z0 A3 \
- if(write(fd, buffer, strlen(buffer)) < 0)6 J) M! x+ o# w) G, G, A a5 t% p
- break;0 s! i' W- H; B/ c( @! S. C: m: L
- printf("sending frame\n");1 i8 Q: m5 T+ g3 w4 G3 y+ x! _. d
- if(write(fd, frame, frame_size) < 0)# n3 b3 h2 a; ^* Q- F2 p+ u
- break;2 t1 S& h+ U5 ?3 w8 V
- printf("sending boundary\n");
/ X1 t. Y+ i' h" `! t - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
7 h* L) C2 \% ^. ?) u& _; [( @$ s1 j - if(write(fd, buffer, strlen(buffer)) < 0)# b0 K6 T& T; D0 n0 b' g
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  s) W9 w4 x4 q/ [

- L) P5 w% ~ }; h2 o二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:0 r$ x6 g ]3 b" {; X) t q
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
2 r$ Y6 H& M. s# \- @$ x1 b2 y. z - {
2 X( m s2 Y B, N1 q* c - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
3 D! m( F3 d' s: _$ v) q - if(*socket_found == (~0))2 ] e8 t' J9 v- N% n1 D8 m
- {
( Y) _) y3 F5 p - printf("Create udp send socket failed!\n");+ `# c$ P* ~( P6 D+ i S) N
- return -1;
8 X% p: u9 [3 P, V1 b; e - }6 T" H1 }7 e0 z4 }2 Z" k! p% o
- addr->sin_family = AF_INET;8 ?, `0 a2 B( _$ z( f
- addr->sin_addr.s_addr = inet_addr(ip);
j6 Y1 C' a$ [8 R2 d - addr->sin_port = htons(port);
) |- t; c6 N7 R+ {- @7 b. D6 s# T - memset(addr->sin_zero, 0, 8);
8 d, T4 \, C4 F3 C - return 0;
6 @$ ]* h0 l4 [; z - }
复制代码 " q; W1 ]& ]% a) i4 m4 x( H" |. q" {
2 F: ?6 Y. W9 ?7 b0 g+ D$ a, E
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
$ K; I* s6 l7 L+ A8 d5 R4 Q3 Y& V3 W E( l' V W) b/ _
6 y: g0 ]/ h# P0 x
- while(fend > 0)
F0 G9 I: W6 l9 u6 Z) X3 H, K - {5 |1 ^* l) M$ R3 }4 y
- memset(picture.data , 0 , sizeof(picture.data));
' q4 p$ I0 g. a% q2 |# J6 Z; u - fread(picture.data , UDP_FRAME_LEN , 1, fp);
; I8 t3 k5 z" u6 K9 S% G - if(fend >= UDP_FRAME_LEN)
1 r9 o# T/ R8 S8 `0 `+ U% v+ F - {
6 g) Q/ W- p- [ - picture.length = UDP_FRAME_LEN;! W% D0 U7 i% K% ^( Q$ N ]% F9 C
- picture.fin = 0;" o+ C9 i7 N4 }: @8 |. t; r
- }- C1 D& x( U4 `# r
- else
7 Y7 h1 _) v! A - {
; V$ {+ C6 M, ~) `& X1 K( k' y - picture.length = fend;$ o4 A W, u/ s0 M5 S" ?/ S& L
- picture.fin = 1;
6 J% t1 Q2 @* B1 w3 {# f6 U0 x0 ~ - } K1 E# C B6 a) j& x% k
- //printf("sendbytes = %d \n",sendbytes);
0 [# o. Y1 y' U1 _ ?- k - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);# d8 d. V+ i1 C9 A( j! Z
- if(sendbytes == -1)
: u" W/ c- Z @# \0 ` } - {
: n$ r) a) _6 I- V7 V# q# k - printf("Send Picture Failed!d\n");
3 F0 @/ G9 V- c. I9 } w' ?% V( ?4 h - return -1;- ?) M6 a, }7 b
- }/ v: s9 C! w: m* u( W
- else
* \0 \, e7 O+ R3 f - {
+ a1 h X7 A, U) S3 x) n0 S4 x* c' F - fend -= UDP_FRAME_LEN;
! b1 _$ T, G- M+ w4 o6 U( g - }
" H. |; q+ n2 J - }
复制代码
1 o' I; w1 v5 u- I, j c/ z" ~- t i- U

2 B1 ~8 ^$ ]# v( b; N+ Z7 [: Z9 W7 D, ~
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |