本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 # p5 o' i$ [* I6 y2 n( A: i4 S
0 g- ~; S; E. E1 ~# L$ V+ j" i) P
; p- e) ] f/ u+ n6 F作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html4 {0 S, w A( u( [' K
5 u$ i9 `: }3 _ x/ @7 F6 X7 E4 w0 `. W本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
" E$ c. A) j9 \' m* \' w, ^ 9 v, o5 g& i3 R
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
5 _" Y5 W9 O. d7 m2 |( p1 E一、HTTP网页服务器
+ L7 f d4 Q( F9 L5 |* ?& S先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
# u; m6 Y3 e& K* z2 a - {
) I, e2 p {, m - struct sockaddr_in servaddr;, Q! Y0 [% m0 E3 E% D$ A2 j
- socklen_t addrsize = sizeof(struct sockaddr);
i% t/ C7 ?& p& g - bzero(&servaddr , sizeof(servaddr));% b1 h9 `& i$ W% V# W; R' V
- servaddr.sin_family = AF_INET;- I$ q3 ~: f+ p" s
- servaddr.sin_addr.s_addr = inet_addr(ip);
7 A7 m4 \3 Y: G: e! s% U) m - servaddr.sin_port = htons(port);
, b6 t) m' |4 P6 u0 r' M' W - int ret;9 O; |. `; x! v5 ?, `9 [
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
y; T9 _/ |9 c9 l - {, U! ]# B' `' Z, u! t
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);0 k Q L1 E6 B* a
- return -1;4 _: |$ H) f0 A9 {
- }; p* O$ O, L* d, l* k" H
- int on = 1;
, G6 }0 I; D5 F& e" A ~6 m6 c4 S$ { - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)* o. ]- W% s' e
- {
0 V, k0 Q+ F0 b( I2 T - printf("setsockopt error\n");
' I4 m5 Q' {# @% w5 o - }
! W5 D* W3 m9 J4 a9 P8 ~1 S - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
) r# T0 o+ [. w: I3 g2 d) v0 y - if(ret == -1)$ M3 @& q% U4 {, E8 |$ A; o
- {
: p# k" _9 x5 R% @ t1 g( n9 g7 i6 M - printf("Tcp bind faiLED!\n");+ t2 B+ N5 w5 N$ }' k* E
- return -1;
3 c" w* r; G$ C1 ]' h! Q- @ - }
3 ]- O, Y" Z) k& v! V- F$ m' A( c - if(listen(*socket_found , 5) == -1) @3 Y: q# [/ `
- {- P* p8 q! W: @4 w- y8 V+ Z' d V
- printf("Listen failed!\n");
5 x' b2 a8 M, @' Q% u: A* E - return -1;
: ^3 l- o- J" O8 R: u, f; l+ c - }
7 v! N. X P( M2 I: x R/ B ? - return 0;
' k9 k+ a* U( { _2 J; c% ] - }
复制代码其中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 p% V0 K5 s9 E
- void * Thread_TCP_Web_Recv(void *arg)0 a3 G; }& v* s% J
- {( o# n: {" l T( _
- 。。。0 a9 a' h0 W" V) g
- while(1)
; q( Q' J- `+ m7 H% v0 d* n - {
, ~/ k% ^, x: \; D - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
6 n, i# M! V X/ q3 _1 x- j - printf("fd_socket_conn = accept()\n");
! J/ w, i& {1 G - 。。。
( w6 H! J8 x, B2 K4 X - recv(fd_socket_conn , recvbuf , 1000 , 0);( l. Y% G2 G* T5 |7 Y& {5 b- P) F0 l
- }* w* d0 z, Q* Q5 }" U `8 h+ J8 k
- 。。。
1 q5 k3 |* f4 l: ]$ X! w8 [) b - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);: {7 {2 \* Y% Z" r$ G
- pic_tmpbuffer = pic.tmpbuffer;; R7 w3 Y3 l- i2 s' F( t
- pic.tmpbytesused = buff.bytesused;
' t2 e4 O! G+ o) T' X6 Q - pic_tmpbytesused = pic.tmpbytesused;& P. F+ n m% s) \5 _ s5 B
- pthread_cond_broadcast(&pct);
' X9 |( E) C( O0 [ - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;# I' x9 O' a5 k$ ^3 s. b
- pthread_cond_t pct;* P1 T6 z! R# C7 H
- int main(int argc, char* argv[]): ^( M; g: u# X. f5 d
- {
3 u+ s" t3 e4 Z: b( f - ...
! T8 O7 w# C( W - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
& b8 k. p* w d- u) d$ F' I - pthread_mutex_init(&pmt , NULL);
X, A4 M [8 P* a8 {) R - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);4 H; @3 ]+ z$ z& U5 m/ a/ [7 e
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
2 K: G+ S8 O5 y. ~9 h* Z+ ~- H- | - ...* ?& W2 r% z4 J/ v8 ~
- while(1)
4 \: C( B; |& _3 W6 x9 i3 F7 L( i - {" h+ ~+ b1 R! w' \4 d( b
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);8 |" H( V2 v5 m- e
- ...
7 G) S1 ]7 i' P. j8 |, Z; f" ~7 H4 M - }7 L9 j% ?1 q6 |; z3 p
- .... Q: @- ^- g/ `6 O4 W( T0 J" @, O
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">- P; T" y4 }. G' j* X
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
r, k2 I H- y# y5 `1 k$ I6 O# D - "Server: MJPG-Streamer/0.2\r\n" \
* z% ?; J( \) U! U& _, ] - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \/ V) ^8 r( h% b1 j
- "Pragma: no-cache\r\n" \
4 w4 o7 K4 M0 n" k$ K7 ]6 ~1 ] - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n") b6 F" ^4 s3 c% H8 g
- #define BOUNDARY "boundarydonotcross": }0 B' Y6 O7 r" \& I5 k5 ~
- printf("preparing header\n");& B# Q( I: {4 \( H
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \$ E- S: m, e8 [: T4 C9 e, V
- "Access-Control-Allow-Origin: *\r\n" \
& n" X0 c3 ?) s. Q' s! h! y - STD_HEADER \
% ?; i" @# F7 o% k - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
. z# `1 l% }" Z0 o3 ? - "\r\n" \
! \6 Y2 E: z) g4 j - "--" BOUNDARY "\r\n");9 j4 e% o$ G* D0 R3 P2 T& ?
- if(write(fd, buffer, strlen(buffer)) < 0)
3 C6 }1 y5 n8 U7 m& V/ w; n - {
1 E5 p& m8 `( n; ]9 R - free(frame);
6 i s" Y6 ~& Q# A4 E - return;9 B; W! q) l- }6 c5 y0 _" Y, r
- }
复制代码发送完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" \+ V* Q5 E( e2 l7 [+ u! O! D
- "Content-Length: %d\r\n" \
! o$ c% x: F* F6 d - "X-Timestamp: %d.%06d\r\n" \
$ {+ V0 j B* F - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
* V6 A; E1 X1 h, l# q9 q f; j - printf("sending intemdiate header\n");
# m, M2 K3 ^0 J - if(write(fd, buffer, strlen(buffer)) < 0) z" X5 z @$ ?
- break;
4 ^/ O; n% [) l! J0 b0 U( D, H0 o - printf("sending frame\n");
) a8 N: u, X9 x3 Q% |$ b. d- k - if(write(fd, frame, frame_size) < 0)( {: _; A& t" {( d# ]
- break;, J4 w- Q, g' g
- printf("sending boundary\n");0 h" n+ P& r# |: ~' j' w4 t
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");( ^6 C/ z% E1 q8 Y- C
- if(write(fd, buffer, strlen(buffer)) < 0) U. o; u% d: L# v3 i
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  3 s# v8 o r* L* `0 A: p! L

1 ]0 C0 P" Z. m% o二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
, T4 M6 I8 Y* e: ` {0 k5 z) ?, F# t- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
$ y0 [8 d) t5 Z3 M' N - {* z' W. D) P3 m: z& e
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
$ ~" ?5 ]3 n$ c* b8 u - if(*socket_found == (~0))
C+ f% |+ D* q$ w5 g: j5 @6 d; W - {
1 Y' I4 Q1 n" w s6 `- i# K - printf("Create udp send socket failed!\n");
$ Z$ G6 V5 r X& i0 }% c - return -1;
3 _- v, A' n3 E* U1 ^+ ~- c i - }
) l8 f( r8 I6 u+ Z2 P; T - addr->sin_family = AF_INET;
; U3 I3 o; l1 `+ w/ f0 O! K - addr->sin_addr.s_addr = inet_addr(ip);
/ j9 p' u( G- Q; t& ~3 D - addr->sin_port = htons(port);
" {7 k- T1 g/ T- \9 r - memset(addr->sin_zero, 0, 8);7 F _6 |4 l1 X( E) ?/ t! x
- return 0;
( o" L9 c& s3 C( p - }
复制代码
' \ A9 M9 v: B5 C. q6 @/ C# o( w7 ]
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:3 I. C# ^1 N/ T5 s. H1 w
" |/ c; W& }2 e+ F' Z! Q; Z) m* J! P
& V# |! O% F9 H8 h7 Y3 Z- while(fend > 0)
) B6 H4 |) z% }2 O8 P/ q$ {) Q u - {
7 d# S0 G! c# [3 u/ S - memset(picture.data , 0 , sizeof(picture.data));" w0 N( M" {4 Y- [5 M' F
- fread(picture.data , UDP_FRAME_LEN , 1, fp);2 r2 j' N2 }4 h/ m
- if(fend >= UDP_FRAME_LEN)! f& K( i. B5 q, t; J" Q
- {
1 Y/ X2 l+ L0 s1 m - picture.length = UDP_FRAME_LEN;: {) ]7 F; n+ _+ g6 e. V% k4 C
- picture.fin = 0;6 c# S% l: J$ o; |8 {
- }0 a5 `5 a$ l- F& S+ U
- else
8 H+ \& t2 h. C0 e - {6 t N$ q- V' V2 g) @" c; N% j# }* F
- picture.length = fend;. a6 |: Y8 U( ]) ^, F. O9 A
- picture.fin = 1;
2 }+ z+ `# b. I+ w$ P$ k - }
0 {! d; z/ t5 t% h" s" p) v - //printf("sendbytes = %d \n",sendbytes);9 c! `5 f# R- u1 T4 V" V( x! D
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
4 w& k. p; W& Z& O& w7 [4 o+ A - if(sendbytes == -1)
9 R! e8 N& S2 a9 E - {+ b# L/ i3 s8 n9 G
- printf("Send Picture Failed!d\n");/ C0 ~: a$ N$ s; Z
- return -1;* g: y& e. u% `! }7 j$ @; h" R! Q' q" E
- }% w# b6 s: K, |# K
- else
3 c6 ~! ~+ |/ Z, }, Q" n! J) X - {
/ b! W" R) f" C' J6 ^/ g - fend -= UDP_FRAME_LEN;
3 J: t6 N; M7 `5 t2 v6 g0 r - }0 Y2 |/ f: w7 i, b8 m- R1 S2 L* g
- }
复制代码 $ C# i( L' o( ]+ O- [. {
" r( _7 l5 m% f

- i% p2 q* Q6 m# M. v6 j
8 R' l! u, { T7 a. x* }' f. viMX8MPlus 核心板: https://www.forlinx.com/product/136.html |