本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 " G9 d+ |! b! ~% w! c
- x) N( ~7 H. f; ~# M$ z/ k8 I

6 K9 m) _3 g1 L* n作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html6 W% L0 V v6 w/ h
3 G3 `+ S3 s. {$ p4 w/ G3 }
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
5 @6 R }/ `6 ^7 p & u. {8 \+ P* e
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 / f: `9 D2 L% }; @- A
一、HTTP网页服务器
/ r8 H: a" n! m: w6 T: m/ R5 s先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
' u& A Q& r+ j% H, x' n - {
+ r5 a, N/ ]* [' h5 i+ G) u9 p. j; G - struct sockaddr_in servaddr;
5 P( o1 K; T, E - socklen_t addrsize = sizeof(struct sockaddr);
2 `& G0 ] g4 f" h - bzero(&servaddr , sizeof(servaddr));: i$ P8 L y) K. @' V" A2 W
- servaddr.sin_family = AF_INET;
5 `1 Z/ ^/ x- K - servaddr.sin_addr.s_addr = inet_addr(ip);
& X- ?3 E, {/ \/ B2 {% L+ d - servaddr.sin_port = htons(port);
" E' w% _/ \& p: Y. P% C - int ret;
4 m* |8 G7 K% s - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)7 ^- j7 r- z0 T& L1 c5 i* q
- {
' i1 w0 ~! H( r0 }' I9 L- X - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
e4 Y1 J3 R. D" ~/ n - return -1;
$ T: _: U, K! i, [& S - }
9 }+ q. U4 Z% p1 X5 m k7 I - int on = 1;. S2 `" P7 L& y" j- J
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0). Y6 ]6 ?3 C- q& G+ J/ Q
- {
, G. C6 e3 z0 O/ K; Z2 P9 Z - printf("setsockopt error\n");+ q, a! c0 y) i+ ~
- }! j9 i2 u7 ~" l
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
# ^3 }* m3 |) s1 V+ q - if(ret == -1)
B' l( O, D2 E+ ` Y3 Z - {, ^: M& U7 K3 f: n2 g% h
- printf("Tcp bind faiLED!\n");! S# {" V4 X! ~# q' s5 {$ Z
- return -1;( \" B8 }3 X) U" a
- }+ q4 E( y! c# ]2 ~: z; ^( r; y1 ?
- if(listen(*socket_found , 5) == -1)6 s& {% a" ]8 [5 z( O" X
- {
# s9 a! T# S8 q6 ~0 {" R - printf("Listen failed!\n");
4 N( c& |0 C/ ]- ^7 y. a - return -1;
0 p+ I2 ]2 e. J: v/ e- e2 t - }6 p3 \! C8 a$ s
- return 0;
. J1 W* q1 p$ G/ L0 Z/ n' m - }
复制代码其中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);2 B8 e/ _8 ^/ `2 E2 V) a- p7 b! B& {
- void * Thread_TCP_Web_Recv(void *arg)' |" w* }: ]6 v: e# U$ f- G
- {
/ x: ^8 z) g1 z0 ?% }/ o8 c3 Q - 。。。
2 K$ L, P; I1 \; F - while(1)
# Q# j* t- n1 b( J" H - {7 ?* U5 R+ i1 _# } W- _
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);2 s( V6 V) E$ ^& }
- printf("fd_socket_conn = accept()\n");/ B& ~6 s2 g! b+ b {! `
- 。。。
- S+ t) r8 |! h& v, t# G* @) E - recv(fd_socket_conn , recvbuf , 1000 , 0);
% X% V( ^9 R- U( P+ } - }
. {- z2 P) v( N1 y8 e - 。。。: Z0 p) X2 K: k# }3 q+ k4 O
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
! P6 B$ N; a0 v9 x u7 h7 x# c - pic_tmpbuffer = pic.tmpbuffer;! B* ^. P- ^# i w, L( w
- pic.tmpbytesused = buff.bytesused;
+ u5 I. C9 X3 f- _5 D0 Y - pic_tmpbytesused = pic.tmpbytesused;
5 C6 C' y, u3 m2 k$ l - pthread_cond_broadcast(&pct);. T( p8 b* Z$ d
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;4 b; e: [/ _3 g8 P/ O. N
- pthread_cond_t pct;* T- e! x6 A$ o, d( e
- int main(int argc, char* argv[])
; l" X) [+ m# W, S( ^& g - {: S+ G5 Y' \, e, X4 N
- ... r5 L" F# r d5 \0 h8 x7 S: ~
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);, w0 U" ^9 @+ T
- pthread_mutex_init(&pmt , NULL);8 ], |2 K4 f$ p/ I$ h! D5 ~9 S
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
1 V" X& @% B5 V, j1 y - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
* d( q/ H1 A o* G0 @ q; R6 Q - ...
, _6 @1 r; ?! B/ H - while(1)" J* L; O# K- p. ^5 e* E- v
- {
) r* c v; ^ R$ S - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);) _+ F* } z- E4 r. N
- ...9 O4 q% @, ^" ~% @
- }6 A9 _) R$ p: Y/ |+ P8 n" I+ ^
- ...
( z& o# v, z# W0 x - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">; q) p# P9 y2 H4 ? |4 N
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
1 P! s9 U. v' M3 d- T - "Server: MJPG-Streamer/0.2\r\n" \
1 p7 T3 g: x! ]8 }$ a/ [ - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
' ?5 F7 C/ E5 S6 |0 [1 D J - "Pragma: no-cache\r\n" \
) v, ]# J4 z' ?9 h8 n! m0 t7 S8 \6 i - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
7 J! S4 d$ k4 s: i% J$ O+ A - #define BOUNDARY "boundarydonotcross"
( ?, B5 \, c3 F - printf("preparing header\n");
1 G( \, `# N1 h/ K - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \+ U6 y2 ?7 I3 D. }! v q- L
- "Access-Control-Allow-Origin: *\r\n" \# m0 J2 s4 l' J2 z! f% p
- STD_HEADER \" |7 ?, K. n1 F
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
/ b" r( Q$ }% {- X2 ? - "\r\n" \. p! c. n7 E! Q: l8 f# B" `
- "--" BOUNDARY "\r\n");7 `5 E, |8 e2 F5 |. Y
- if(write(fd, buffer, strlen(buffer)) < 0)* _3 T5 V g1 I- @3 Z' G& M
- {- l. E3 i/ V/ s' J
- free(frame);+ N) V) k8 `' p
- return;
3 d4 I3 U9 g3 P* W& x( b - }
复制代码发送完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" \; Z) i) h- z! N F+ T- b
- "Content-Length: %d\r\n" \0 s; k" t: I1 J1 h/ O
- "X-Timestamp: %d.%06d\r\n" \: Q1 |# Z+ N3 B. @$ N
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
0 C# i! [) x- Q* f0 |8 e - printf("sending intemdiate header\n");/ X) F4 F/ C# p/ G# H
- if(write(fd, buffer, strlen(buffer)) < 0)4 X5 a' s4 |/ p
- break;
]1 _8 u2 K+ [; n/ Y* Y9 a" r - printf("sending frame\n");
4 \$ o! X. N# X* L9 v* |4 e, S - if(write(fd, frame, frame_size) < 0)
" r3 z, q) i, B- n& p3 | - break;
5 Q& a- {! M/ x4 _/ ?* T - printf("sending boundary\n");
4 O) I$ K5 n$ \, N - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");$ b! R/ v/ H. d* |7 a
- if(write(fd, buffer, strlen(buffer)) < 0): Z# Z0 G1 Z1 o) }( I$ i* q6 `
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  $ B2 i6 ]' l! F2 \ o, B/ R

! _" z" p0 n; b1 n6 s6 y9 o二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
' l4 E- z8 i) x, _! J* P) y9 b4 @- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
h6 P- _7 Q* i. k4 _/ H - {. K" s2 k( e3 q, B
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);6 p+ x$ U9 }3 a6 Z8 z3 `
- if(*socket_found == (~0))8 a2 y2 ^2 w$ ?$ D, l M
- {4 V/ H& y G2 J7 w7 o
- printf("Create udp send socket failed!\n");- A8 v- r' R4 r# K
- return -1;
( L; H/ t6 N# w! |" `5 t. {) l5 i - }
( ~/ T; N4 ?( u0 y0 C8 `9 r4 h - addr->sin_family = AF_INET;
3 N: E) p, `5 W - addr->sin_addr.s_addr = inet_addr(ip);
# R; t* O, X' r# m5 { S/ O4 J - addr->sin_port = htons(port);
. b* m$ U, s; Y; f" {8 f# q - memset(addr->sin_zero, 0, 8);0 @% f2 k1 @! X6 u
- return 0;+ P$ P7 t3 G/ r* W
- }
复制代码
V4 p5 e# ~9 w) I- b% v# e/ J# I8 O. q& k. d' c7 z! Q+ ^% b' d! _
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:0 _: M# ]. y# u: v' X( t% \" m
3 V% r; o* s1 u7 @6 i; L/ y
6 U9 L8 V" Y) @. r. _5 |5 t% @
- while(fend > 0)& Z5 f8 [2 a+ L! r
- {! n/ J( }6 w; c3 P$ M9 F) U6 H5 g
- memset(picture.data , 0 , sizeof(picture.data));
0 [; B b* ^) i; e# O0 O - fread(picture.data , UDP_FRAME_LEN , 1, fp);9 _$ t6 G" p% `# D
- if(fend >= UDP_FRAME_LEN)! N3 B S2 t) Y! F6 {- U
- {+ n1 j; Z2 R8 z) G8 [
- picture.length = UDP_FRAME_LEN;
. l! z$ y# ^ _ U2 n - picture.fin = 0;) j: K; d0 V/ s# D- z
- }$ l2 T% S0 x0 i0 z
- else
- Y$ b& v- d- q& Q, y' z. a4 j - {
( j$ q" r7 [* W' } x: N h - picture.length = fend;
! i% |( f' H/ K& N5 Q2 h - picture.fin = 1;
" B9 m1 o' N! z) T. y' L( L4 ]- S - }4 n; V/ _4 e6 p) F: x, c
- //printf("sendbytes = %d \n",sendbytes);
: _- ]) ?& \: L - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);. r; ~; Z* [* e& H9 E: m
- if(sendbytes == -1)+ B) N/ C) S: l \: o
- {: |' U( }6 F& T0 q. ^
- printf("Send Picture Failed!d\n");" w# b9 W& m, v& a
- return -1;, s d" h6 P- A: a, ^
- }
! I+ M& ^$ [& Y/ q6 ?- _3 s - else
) d' B/ R, S( p+ N) r, z - {
L3 D$ r- i+ X) ~ - fend -= UDP_FRAME_LEN;
9 W0 u/ l& g2 j8 a9 E E& Q - }
0 C3 F9 ^5 u. I3 {" T - }
复制代码
. w* f4 b9 G7 |: i5 `/ E/ O8 Q; l& z" d3 I2 Y! K; Z

9 @+ \8 D# |0 n: S" f T
; g+ Q4 N$ S, E) siMX8MPlus 核心板: https://www.forlinx.com/product/136.html |