本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
; w, h% H. e& O/ {3 Q5 R
4 w, \. D+ V9 @+ {: _# H
9 [( q3 @3 l8 [作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html0 n( H0 m7 ~! x" L. J5 I
8 \% _% r( s& K/ g+ E- s9 {3 U本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
4 ]* L) T7 ?' g; r
" N1 f3 X* P: v两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
5 Q6 K! i% j( b/ d- k. K一、HTTP网页服务器
7 F: W- r, K, v: E9 h6 d/ R先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
7 v+ ~, w k z2 o; t* X6 X9 m( u - {
$ M2 K6 s$ o% S2 `4 h8 T - struct sockaddr_in servaddr;
[ M- u+ s$ b8 [' @6 n ]( [ - socklen_t addrsize = sizeof(struct sockaddr);% i+ w. C. A$ z4 Z
- bzero(&servaddr , sizeof(servaddr));; _2 V: x3 P1 H+ Y' Z1 r8 e9 A
- servaddr.sin_family = AF_INET;% Q2 R# d- A$ q6 ]) `, H
- servaddr.sin_addr.s_addr = inet_addr(ip);
( w( U* ?8 V3 x - servaddr.sin_port = htons(port);
4 q6 n5 r9 R8 f: }! W - int ret;2 s9 B" N. G7 x; p9 W; x
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)* N' |& b. r# C/ G8 Q: L# \
- {( e8 C% K9 b! i& K
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
" A9 Z5 I% \( h1 S. d. F3 J$ E - return -1;4 e; {. l$ @0 e/ ?& g; ]
- }
) k f( t$ P a" o& c - int on = 1;
1 ?+ k1 M, K5 R7 K! V: h4 i S - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)3 ~* @( {! g+ b) e
- {4 P- O9 }# S% G7 S |2 i `
- printf("setsockopt error\n");
# A. L5 R$ _ h( u: y - }
3 @0 L2 N3 c0 Z9 _; M/ W" A2 m - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);, y- K- E" A# u: K9 H
- if(ret == -1)
: I8 ~* V N+ M6 n( @/ n i V - {$ E" j+ ?; J8 |1 J6 V
- printf("Tcp bind faiLED!\n");6 h8 o; Y' f* `2 t
- return -1;
! T/ k2 o* p8 y( a0 F - }
3 J3 Q% X1 S n. f& u9 m - if(listen(*socket_found , 5) == -1)
: _4 T/ @! w/ i$ j9 o) q# T' g - {2 u/ w( y' X% v3 X" K# M: L# P
- printf("Listen failed!\n");
; T7 Z( |% O4 C3 ] ~ - return -1;
5 t* U$ F( E3 o0 ?3 N - }! x6 w- R, j$ R( c
- return 0;# }( T% C4 r; }
- }
复制代码其中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 t0 G8 d) C3 P g( R; z - void * Thread_TCP_Web_Recv(void *arg)
. m8 [4 [* p/ K; a) d - {6 a$ j8 M" B% {# n2 v2 s* w& J2 k: H
- 。。。
6 w7 |$ ?! y8 o; c& U4 [ - while(1)
1 g3 j3 ]$ p2 f- h$ A2 s' s ` - {
0 N3 N) w. a, q, k/ v: _1 E - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
% J& P6 }' t9 P- p- Z; ]7 l1 H - printf("fd_socket_conn = accept()\n"); g5 ?/ Y2 m( v( L
- 。。。# N- a/ ]$ j7 Q( n" ?9 F
- recv(fd_socket_conn , recvbuf , 1000 , 0);. M* U" }# o9 l3 V) e: s
- }3 g2 }7 ` o1 r0 e& _1 l# l
- 。。。" [. }* K* r, D- L1 D
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);( M! A! _$ s8 I' c9 @) n9 y$ J
- pic_tmpbuffer = pic.tmpbuffer;
- ^+ C% _- ?0 I7 O! M; q0 G7 Y9 K* ^ - pic.tmpbytesused = buff.bytesused;* O( u: _7 x, I& B
- pic_tmpbytesused = pic.tmpbytesused;
4 Q8 ^/ R6 Y& c1 g% K; W - pthread_cond_broadcast(&pct);
2 U R( u# y3 J% S% N - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
7 X$ g% E; M _% {/ ] s1 s - pthread_cond_t pct;8 D5 E+ ^ N6 M" N, v# e6 |2 A
- int main(int argc, char* argv[]) I' W) `8 g+ S4 V0 n. v7 J* u
- {
+ X0 C9 g7 ^9 H; o; S; ^3 E# u - ...
+ Y9 s5 k1 p6 A: a! ]* ? - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);% { R, `' s" e3 p/ B0 z( a5 o
- pthread_mutex_init(&pmt , NULL);! }% B2 ` z, e& X: [
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
+ s6 s- X, n% Z/ x! R" [1 Z - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL); ` a$ c' h+ r5 N4 R1 K
- ...
- C6 |: o' Z- A - while(1)
3 K. F' J6 f( P% V R - {
; a, d7 E4 @7 O4 P, m/ w - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
/ m- Z" j3 S8 Y( U7 t; k3 r - ...' ?' ]6 b: V$ J' }
- }, G6 p4 Q* t" j% n1 w/ `) m
- ...
. O5 T3 X0 p1 S/ ^5 K& y - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
9 o3 i* R, J- y' ^8 x; ^ ~/ B - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \9 o8 ]9 R0 l8 J0 W8 |8 R6 V1 @) f
- "Server: MJPG-Streamer/0.2\r\n" \( B7 a5 z; P% {7 z/ d$ \3 R; F3 T
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
- ]( m$ B7 U: z9 f2 H - "Pragma: no-cache\r\n" \4 n8 }7 T/ |* _* B2 k3 n/ P
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
2 G2 p6 ^& M. F' E7 z! g8 P - #define BOUNDARY "boundarydonotcross"7 W2 f3 }! L9 U, p% X3 E. D
- printf("preparing header\n");
8 ]3 z9 u2 [- x$ l - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \2 Y, S: K3 ]! }+ k; D N7 h
- "Access-Control-Allow-Origin: *\r\n" \
. M9 |0 \9 p9 o2 j2 P/ w' P& y; g - STD_HEADER \
- h" I! |4 H! h, T+ c! C: N! P - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
; X; v9 ~ G- n/ P; n& D - "\r\n" \" J2 b1 E. T+ J6 S+ O; C4 }, a) P* {
- "--" BOUNDARY "\r\n");
/ _4 A! ~3 r( c, ~5 ?. i - if(write(fd, buffer, strlen(buffer)) < 0)! ?, n( ~/ _; V6 i/ n) U& j
- {
3 O1 B! ^! k( E+ g4 U - free(frame);2 D; W) a! ]1 g5 f3 x) B
- return;! O# n2 Z- }8 r/ s1 D4 T
- }
复制代码发送完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" \
( s8 ^ z6 y; d - "Content-Length: %d\r\n" \
4 o* R: W4 g5 @5 o) b - "X-Timestamp: %d.%06d\r\n" \0 o; n4 Z7 }6 {8 p, b7 y8 H
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
% O, }8 r8 m% ?6 w, x - printf("sending intemdiate header\n");: g1 ?) Q v% Y% [0 `
- if(write(fd, buffer, strlen(buffer)) < 0)3 ]; ^. M* ?7 u* R9 \2 |# ?) D
- break;8 B; `& I) m# K) F; I: [
- printf("sending frame\n");7 i) e& f9 H: d$ P1 t( J
- if(write(fd, frame, frame_size) < 0)
+ `, \- Q& h/ l! W7 y8 c - break;
" ^3 Y5 K0 H( y - printf("sending boundary\n");
9 u3 n7 Z( P( D2 m& M - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");6 F" k0 x- P. }8 h0 R
- if(write(fd, buffer, strlen(buffer)) < 0)& m* F& c' Q3 E$ }
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  + {+ b4 T# x1 W5 g( L/ X$ Y
 ' K2 L9 f" p* D/ b. Y
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:5 U9 h3 S7 f! i) t9 y0 u
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port) @8 a* ^. I$ Q# \1 R# M
- {
4 G0 A' f; K- G4 w! y/ Z - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
+ K$ P: ^. [( G7 w( ~ - if(*socket_found == (~0))) B/ f9 o0 g2 M4 w* i
- {
v; \& p$ w; J- G - printf("Create udp send socket failed!\n");0 u' z+ H) Y n9 l
- return -1;+ q- v* [9 l9 G3 q9 c" }1 J) k
- }1 r) Z0 T' B S5 f7 C/ ~
- addr->sin_family = AF_INET;
# y4 e+ K% Y) H4 P- { - addr->sin_addr.s_addr = inet_addr(ip);+ c, \1 T9 H# q! D' d
- addr->sin_port = htons(port);
8 H( R7 b4 E% x/ W - memset(addr->sin_zero, 0, 8);, F9 R3 m3 D- n, W* A1 D O
- return 0;
4 F7 w9 b: w; w- i5 K- z5 I - }
复制代码
; a) p! a1 a7 `+ u- j1 g/ g
+ F5 N& ?. N+ o1 D4 g而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:: U& `* N6 t; F/ A6 \
7 g4 i Q- J) Q g6 a. \( h! B& W; N' @9 r6 k; e) H
- while(fend > 0)
1 m' {3 Z- G- I! a, i& n' Z0 G8 r - {
; E% t( t1 @2 c; G- T3 | - memset(picture.data , 0 , sizeof(picture.data));3 h2 ` J# k/ D6 P8 L% g4 O2 p! Z
- fread(picture.data , UDP_FRAME_LEN , 1, fp);. X7 h# h! Q( S9 M
- if(fend >= UDP_FRAME_LEN)
/ Z' t$ F; t) d; x - {* P, J, G D X' a% m
- picture.length = UDP_FRAME_LEN;/ z% ^+ W3 _5 z% L& ^9 ?) w; y0 C
- picture.fin = 0;
. j9 i" Y$ h. D6 s7 K+ u - }
; {$ x: y- e- t0 H - else
0 S/ T3 P! P4 I - {
$ X% S n; X, J) x+ J - picture.length = fend;, A( ^+ @3 l" l: g
- picture.fin = 1;/ F4 {' P( b& ~! w% Q. o r
- }* w7 j) Y; s- l5 @
- //printf("sendbytes = %d \n",sendbytes); w, X ], l! H; H5 @( D9 ~
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
7 F0 j& R: v; Q: A! t - if(sendbytes == -1)
- z% j3 U- z( P6 \7 G! V/ } - {
* l1 s4 r* x N7 O# P - printf("Send Picture Failed!d\n");" B1 x) R: V4 S' i9 p. W# g% A
- return -1;4 N4 i1 H5 C) S
- }1 l: \- B# Y; ^+ V
- else
! p3 H/ j2 N6 ^# v( S - {
# r" [1 q$ J4 i# x# x - fend -= UDP_FRAME_LEN;8 L) h; a( a( o* \ V. R
- }
- v0 P t: O8 b - }
复制代码
8 v( l$ ~2 p3 R* m# }
8 ^+ ~4 k$ U6 M5 m o& y9 L+ ]/ a' M9 s 8 I: X- m2 T! O$ h9 ~
9 @7 e* S, m2 M& P& h" M4 L; RiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |