本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
! _& E C( X: J( `, g/ O) g6 p- ?% w

/ ]; Q/ L$ [0 c% J作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html8 _$ e2 A: a3 M" N
; n9 l* K4 Y) ]7 E$ F6 B本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 5 y* _: R* B; F; o% j2 G! \# b) y

* {7 V. c1 `+ ]& w5 L4 X# Z两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
3 Z( l- Q5 A6 n一、HTTP网页服务器5 U+ ~+ _% C, w
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
" S; _( J& b7 z9 S# D9 {; O6 B, t - {
3 S& j: o0 N X4 ?' I; k- v( O - struct sockaddr_in servaddr;
0 C8 `* i u; i. W. k - socklen_t addrsize = sizeof(struct sockaddr);0 G% _6 r) y! W
- bzero(&servaddr , sizeof(servaddr));/ T* t% T* V V- ~7 m$ H' R( h
- servaddr.sin_family = AF_INET;& c' [9 B/ w$ `) }
- servaddr.sin_addr.s_addr = inet_addr(ip);
& F& U0 M0 J) q, q9 H0 e+ |, v7 J+ ` - servaddr.sin_port = htons(port);1 K6 ^" d( [8 r$ y( \
- int ret;
1 O. X1 M' O. m4 d. u, | - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
% I: z6 y" ]4 ~! ~% u+ L - {
6 Z# Y3 Y6 a) _9 @+ H3 S - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);2 A: [5 H, E. f% L+ B: u
- return -1;
3 A9 D7 n: H7 H+ d- z9 a - }! q N+ {$ k a" ^% p1 k
- int on = 1;
. m. l: M/ P. g, S - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
8 O6 M3 K6 W, }+ ` - {" q, T+ k; J. s& d; `
- printf("setsockopt error\n");
. z% Z! e' \/ |) a; |2 p/ [ - }
" b5 v4 a& h" d/ i$ @ D - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
+ E( f5 Q$ F7 Y8 t0 K0 U - if(ret == -1)
9 I. ]. f$ d) Q+ l - {" E( L* Z- e5 N( h
- printf("Tcp bind faiLED!\n");
5 t- A; e3 r6 ~0 K7 ]' h - return -1;1 {9 D1 q) O6 @
- }6 G% e6 R0 }7 U# A' s2 w( p) k
- if(listen(*socket_found , 5) == -1)) b# Q5 |& X. I) U5 f
- {/ N( R" _. a7 l: ~+ [) t
- printf("Listen failed!\n"); W- B, h5 F6 t* h/ j0 ?! o+ M
- return -1;
- ~7 o. e& ]6 z4 V" _% m - }
! N$ Q& h, u7 v2 J - return 0;
! K: O3 ^/ m& A, K - }
复制代码其中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);
- n7 P. X; b) c$ M$ O) C+ U) L: e" g2 B - void * Thread_TCP_Web_Recv(void *arg)
k5 d* {; `3 y( J" H& K, P - {
& s& W2 X6 Z8 c7 K - 。。。8 \& V3 s8 t, Y0 L. _
- while(1)1 K0 j+ J4 f" Q9 E+ {
- {
8 M. X1 ~" o8 R0 n - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize); w$ ~) Z4 r& ?0 {: ^: m; r5 G# h
- printf("fd_socket_conn = accept()\n");
$ G$ O% P- A( Q, Z - 。。。0 o' P5 z- k- Z0 _
- recv(fd_socket_conn , recvbuf , 1000 , 0);
9 p1 Q. z4 v! h; K. X - }
! m1 w9 M# C9 a4 p! i4 M - 。。。0 b" p+ N' g; k% Q- @3 j5 z
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
- b/ @% P1 U3 S6 P, [: ^. i - pic_tmpbuffer = pic.tmpbuffer;
( U! i5 f1 h E1 E - pic.tmpbytesused = buff.bytesused;
+ p9 \7 r, p( K: i - pic_tmpbytesused = pic.tmpbytesused;+ `/ M# Z$ J% F, g$ P
- pthread_cond_broadcast(&pct);2 ~+ ]% X, w5 Y
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;: R: |. ~ Z/ M3 ?/ f
- pthread_cond_t pct;! n4 P8 B% R+ Z% Q* D) [/ y# Z
- int main(int argc, char* argv[])9 I! f/ R9 m' f
- {
4 H' E, T% [9 h: z3 E - ...+ d) Q8 u. p. W( ~4 t, N
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);4 x+ M: X E# O! d1 B6 t- Q
- pthread_mutex_init(&pmt , NULL);
4 m( @* W' r% ^: y' o$ G - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
9 ] p( b3 s+ N2 h0 k# d - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);8 T# j7 D$ a% F9 w
- ...
1 j! g6 e; j4 t' V2 a% Z% J - while(1). D' H' Y2 L& ?/ ?2 E) p
- {# `, W" }# a/ z/ z% | D. \
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);" O* }, c: h4 _- W0 R
- ...( f# y) u0 H( G
- }" v ?* \" p9 W1 o
- ...
7 O- ]" `& j, c& Z - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
/ B; ^) o! \& z0 y5 A - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \; I* _8 l& c$ w
- "Server: MJPG-Streamer/0.2\r\n" \4 {2 R* t1 A* x. R& o
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
; A; A; N+ Y# N2 g4 n - "Pragma: no-cache\r\n" \
$ Z9 W4 V2 L5 Q: k- W8 X3 Z - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
( U; Y+ R( M, `# i1 D+ v2 Y8 \ - #define BOUNDARY "boundarydonotcross"
' p9 E" {) v8 F& S( r - printf("preparing header\n");
/ \+ B, W& Q1 `7 f - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \0 i6 M; ?, ^/ U* l
- "Access-Control-Allow-Origin: *\r\n" \
' S7 N+ P. U0 C; \7 Q* m& N - STD_HEADER \
6 ~/ K2 {# u. e) | G; b - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \. b! x8 K9 j2 ^2 \. a0 t' b: U
- "\r\n" \
2 i" ]4 `, |9 J4 } - "--" BOUNDARY "\r\n");
7 q+ ]: o" U( N6 H# W) {7 v - if(write(fd, buffer, strlen(buffer)) < 0)
" z5 t6 k( h( L* a - {
( L+ ]7 b( z! x: ^6 g5 k3 t - free(frame);
& X) w7 P' C* q( W. U - return;
0 c3 u1 x9 x1 B& C# H. Q5 k7 Z - }
复制代码发送完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" \
4 l9 }9 f) E* ~2 } - "Content-Length: %d\r\n" \
7 Y' _! V$ F2 g, S- _- H - "X-Timestamp: %d.%06d\r\n" \
: }2 i9 O1 B" b: ^2 ]6 f9 [5 U+ d0 j/ m - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
$ S9 }9 A# L7 j* ^9 S. R( C - printf("sending intemdiate header\n");
- d$ Y7 R6 u, y g - if(write(fd, buffer, strlen(buffer)) < 0)
" L B% {' ? @ - break;* \1 S' k B: w3 E" `: e, u
- printf("sending frame\n");* |5 q& o6 x1 s0 Y1 n
- if(write(fd, frame, frame_size) < 0)
( T4 p+ Y ?4 u& b - break;' }6 ?1 x: q; r' g$ T: d( x$ F" T. t
- printf("sending boundary\n");
" B O, x: M, ]: j, H - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");7 G/ M: q+ o$ f7 Z
- if(write(fd, buffer, strlen(buffer)) < 0)
5 d1 U1 L! Y0 R0 I; J8 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  2 t8 p8 K* a" U3 x8 z
 ! O0 }1 {* u# \, z) D9 d. a
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:( S# j3 @6 j6 H. q- h* [
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
- M0 w( R |2 C: f$ Z4 I9 h% `& w - {
( O5 ~% S/ f1 f% d8 c8 O7 i7 } - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
/ O# y& d& x4 ~" ^4 V - if(*socket_found == (~0))
, U! L$ x' C) j/ l$ k- ^ - {
% l' z! f& w* ^$ B! [/ t - printf("Create udp send socket failed!\n"); @- R5 W& U Z6 a
- return -1;# c) y) w8 w% N& r6 X Y4 }
- }/ {$ U8 u$ X ?, j: Z& ~( }
- addr->sin_family = AF_INET;
4 l( k$ N: J) x( V - addr->sin_addr.s_addr = inet_addr(ip);* L& M: O) h) X3 F% D. d
- addr->sin_port = htons(port);# p* s D# t1 V6 L) f9 F+ v
- memset(addr->sin_zero, 0, 8);' U) C: W6 N' s H6 N1 u% N
- return 0;
$ d9 I* G8 o6 @3 m% @/ w/ x) a - }
复制代码 4 n" U( L) U% H/ G
- O4 p2 Q. F! ~3 {% \1 k
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:* s, y3 m _. E5 w( b
" Q/ j( Y! V- K( f7 z2 Y |! m$ `9 h! C, U# X( j D. f+ {
- while(fend > 0)1 U7 A0 v8 K2 v% _* T3 b# g5 L
- {
2 n; z" [- M9 e - memset(picture.data , 0 , sizeof(picture.data));
4 p( |/ B, O8 B1 K' }8 `* r - fread(picture.data , UDP_FRAME_LEN , 1, fp);
+ r5 j! \3 P4 W% Z6 ?; N+ d1 a - if(fend >= UDP_FRAME_LEN)
5 l! z/ h- [1 Y) V3 I1 w3 q - {
* ]1 H% _: W/ y6 |* A/ ^% o. b - picture.length = UDP_FRAME_LEN;4 o0 Q. x& B" b3 e0 b/ n: J3 D
- picture.fin = 0;' R# |/ r* X' F& }: b" n& I* @2 Q* w
- }
) V+ F/ e) L' t3 |' R - else) I3 T, Q! `3 b7 K0 W" l) F
- {
! B* l( D) E7 \. S" p - picture.length = fend;! U9 u& n0 M" Z* C
- picture.fin = 1;
1 G! i k, T, X% X) b7 w - }
# l- G$ M- G* C2 A - //printf("sendbytes = %d \n",sendbytes);! d. G; @' w K/ N; P4 P8 j
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
Z; r/ N4 ^ h7 n& P8 t. k - if(sendbytes == -1)# {/ v5 a% K5 n; l# D @/ g: |
- { T& X, |) @" u. v" b# {
- printf("Send Picture Failed!d\n");6 A7 e( M4 P7 s; j% a
- return -1;8 Q, s4 I% s) L/ W. }6 `
- }2 e: T$ ?# ^. ]' k2 B& K
- else* n7 W7 F& m2 j+ n- d
- {; X; E# a+ `6 ?! H' M
- fend -= UDP_FRAME_LEN;
, \+ Y. w$ x3 Y3 }+ i* P g9 l - }$ z5 m$ C; a2 g% J# c0 R
- }
复制代码
( B% I! w$ U8 m. |
1 C: h3 {3 l5 T+ n3 y w( z& m2 r' `% a ; C1 Y/ {7 z* f& K
1 |8 {9 \; L+ Y5 [
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |