本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
! v, H+ h( K2 r b! o
; c" e% F, T v1 W' ^
1 n0 Q( K x. y3 Q0 J& w; S- A作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html# r% @) ?! C* H) h3 B: ]5 {" b
$ ~ q5 R O# A( P4 q# J0 F
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
/ M& r! s6 t; U' t0 S
6 c1 P" Q$ g( C* k4 B两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 4 V+ \9 W& Z n T( F# }
一、HTTP网页服务器1 [3 e4 p) V2 s/ A! @0 {
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)! Z1 T( W+ t! _6 S3 I4 k& @( _
- {
- _# U5 p1 i' e9 f% V* X - struct sockaddr_in servaddr;
* b6 r2 r4 k/ W3 q; P( ]' P( r - socklen_t addrsize = sizeof(struct sockaddr);
& @9 E& ]( x) G& u3 _* t: N3 P! }7 l2 U* l - bzero(&servaddr , sizeof(servaddr));
8 q* P- z8 Y8 N. F+ p8 ` - servaddr.sin_family = AF_INET;+ q/ \1 e- r& ^4 x5 I* A( c* {
- servaddr.sin_addr.s_addr = inet_addr(ip);
. |; C, q9 ~" U; Z - servaddr.sin_port = htons(port);
: {0 @! \7 }1 m' ? - int ret;' t: N# V3 \0 s
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)* ^& w; H- E9 o: K3 {; @8 A/ F# |
- {, F$ ~5 Z' ~, N' l2 [) W9 n- T
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
! _( T' t/ S9 J0 x& c5 _7 s2 c8 [* m - return -1;/ \3 y* C9 `1 j: b0 J
- }* b3 I/ d2 x& N" d
- int on = 1;+ s( Q# T; C. `# |8 V
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
) r8 n) l9 _: i( I7 H6 w - {
. B( z+ N) K- m; C - printf("setsockopt error\n");/ D; q7 h& r5 f5 R( @/ [" c
- }2 |! r2 Y( Q1 Q5 q- S) ^& Z1 f
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
- s+ b' j+ A. w1 d - if(ret == -1)5 L- C+ }0 q/ V1 V& L& c. G
- {
# U; c. O. @! a4 n/ R - printf("Tcp bind faiLED!\n");1 `. q( e! |6 a% l& _
- return -1;
+ V! H% N1 k* ]7 _4 I# x - }
9 v5 {6 c) f4 @ - if(listen(*socket_found , 5) == -1)
1 d2 |/ [0 U& |. P/ ~ - {
( @/ G: k0 `8 v( c& Z2 } - printf("Listen failed!\n");, y2 Q" H6 V/ e+ |4 x# \( x* D
- return -1;
) u( i; o; Z' o! ]' T - }: s2 c6 y9 z& E4 A s; [: A/ m$ V
- return 0;2 b& h" a' K( 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);2 r( ? l3 i d) j$ B% f
- void * Thread_TCP_Web_Recv(void *arg)
) n8 Q E* \8 a0 ^$ W - { K3 n4 X# w8 L5 D# `% [
- 。。。
2 N2 J& g* f3 V4 } - while(1)1 w& w8 r0 k; ]( P
- {$ b3 L1 T/ s; a+ X" L- e1 J% @; A+ _
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
; R' X5 S/ V. R; I3 }* Z2 ` - printf("fd_socket_conn = accept()\n");
8 p9 Y, X g( h9 J! V - 。。。
2 `7 ]. g8 M9 [* H& Y - recv(fd_socket_conn , recvbuf , 1000 , 0);3 i( w6 ~0 R6 Y
- }
$ K$ k5 T: M% t' x' [# a' p0 d, W - 。。。
: q4 _& m+ d( Z! w `* D2 j |- n - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);$ g$ w. ~/ g5 Z- P
- pic_tmpbuffer = pic.tmpbuffer;
7 N9 F, C3 A* T0 l2 c2 W' W - pic.tmpbytesused = buff.bytesused;: |# v1 i7 L8 r1 a, @! e* U
- pic_tmpbytesused = pic.tmpbytesused;" I1 {* i- M2 b i+ A
- pthread_cond_broadcast(&pct);2 C4 b, ?5 w9 R% m4 v2 l: \
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;' @# g- `, x6 u
- pthread_cond_t pct;
& M. W6 |' G+ Y. P. d - int main(int argc, char* argv[])
3 u+ k9 G3 F8 S2 e; U0 `6 F - {
; j# d7 p3 ^( X6 _ - ...
$ S7 E" ]! A y" k - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
0 O0 n3 ?5 q$ E% V7 x! u - pthread_mutex_init(&pmt , NULL);' U: I" n8 T1 D; n, t! t1 X1 m/ z
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);6 J/ ]/ s/ o1 H! @+ @
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
2 d! d& @4 u! \$ ~1 W; E& j - ...
1 @; t! N& j4 i% I' ] - while(1)
- N, N0 @2 z* w - {
. ]5 M$ k7 ?3 {1 X$ x4 C - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
5 X- I( G9 F+ t- g' E, N0 u - ...
. D G t% m" q9 \* p' O, V6 J - }' D7 ? Q" h/ ]! S; E8 ?% H" x
- ...
; n7 u) r6 i# ^! K* | - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">! q( M, x) p' D/ ]6 h6 f( i
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
! T& `$ _% Q' K - "Server: MJPG-Streamer/0.2\r\n" \) ?* a, A+ M/ m; M; `% ~, ]
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \$ L6 X; ]; H. d$ j3 v" [) P' w
- "Pragma: no-cache\r\n" \5 @2 y4 W! c" U- T6 R5 O
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
2 M1 X. ^4 _0 c9 [ y3 X' ^# c6 v - #define BOUNDARY "boundarydonotcross"
! J& n/ j4 G! e5 C2 b2 X$ q - printf("preparing header\n");
: \( |! h4 W$ d4 v - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
. f4 ^8 o9 V# U' F! I - "Access-Control-Allow-Origin: *\r\n" \
1 J/ i; e$ b$ {$ K - STD_HEADER \
7 k4 l, e, x2 i - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \- F" ?7 w; w$ c: L0 | n+ k
- "\r\n" \
! T4 n% @1 q8 ]& E - "--" BOUNDARY "\r\n");3 A8 W" {, i" n9 V
- if(write(fd, buffer, strlen(buffer)) < 0)$ }6 x; M4 r0 L
- {
9 ` C1 H# l* s- P$ h0 ^5 T- } - free(frame);+ l- a" h9 F# e) g' X
- return;
: k( q8 u, I' z; R8 L( @ - }
复制代码发送完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" \
: g* ^8 a6 V0 T2 `$ L, Q - "Content-Length: %d\r\n" \& J" A. S1 y+ B5 r& A
- "X-Timestamp: %d.%06d\r\n" \
5 q6 z% E% U: c6 k6 F6 a - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);! _, @: |1 k" a {+ L& R' ]
- printf("sending intemdiate header\n");# o! {0 [( Z$ V7 K
- if(write(fd, buffer, strlen(buffer)) < 0)) ^$ a% m3 z- \; m+ m. q
- break;
) O8 z' y5 n/ g: m2 B( b5 F5 U- U' M - printf("sending frame\n");
6 s/ h2 j' g. {1 R* @- q( L/ M - if(write(fd, frame, frame_size) < 0)
/ @ y: J& T8 |) F - break;
/ ?0 I l8 t4 f( W) j( Q - printf("sending boundary\n");
4 U; j _8 k' K8 _ - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");, ?$ j- i( |, X* w$ I
- if(write(fd, buffer, strlen(buffer)) < 0)
9 `# c: c$ ?& `* q$ F9 k3 X5 }" 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
) E9 X2 t4 i- U
, B8 h) Q1 {) a$ \; V3 @二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
6 n% O2 X& A% I k- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
y6 Y6 C, [0 }2 i - {
# X2 Q5 n6 a2 k% j* C - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
: C& ^( f( d- }' U+ G1 O - if(*socket_found == (~0))2 U; @# } y$ n* U5 T7 Y+ r
- {
+ z" S$ y% k1 G- F$ r! Z - printf("Create udp send socket failed!\n");7 [; @' ^) [* ]& z4 O- j
- return -1;/ X _$ F% U* b: T# _+ R3 b/ ?7 W8 A9 S
- }8 ^- e* r8 F! H. C& L
- addr->sin_family = AF_INET;
' h, H3 {% p9 S! b - addr->sin_addr.s_addr = inet_addr(ip);
+ H7 W% Q: w! j: |* d5 o. J - addr->sin_port = htons(port);: ^; [/ E- u- c6 g L
- memset(addr->sin_zero, 0, 8);
. c. j6 u1 ] X - return 0;) Y: y8 V5 @+ S3 ?
- }
复制代码 " P9 ~, v7 r3 y+ u% t& q2 E# u
+ J$ T( `+ k# S1 n/ Y
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:+ u2 ?& L( e2 P" U" N
5 ?. o/ z9 d4 t
% K5 X% Y1 z7 Y+ J' ~( X+ ?0 W
- while(fend > 0)0 s" b( Y% `" h% C
- {
4 v! p, }4 |' B* B1 q - memset(picture.data , 0 , sizeof(picture.data));$ T& H4 W0 o- O M& ?. z
- fread(picture.data , UDP_FRAME_LEN , 1, fp);
# }$ X6 p2 r0 u% Y% Z$ ] - if(fend >= UDP_FRAME_LEN)
1 P4 J; P" k; o. O7 l - {
* v/ e; P9 q) i3 O$ F: F5 F - picture.length = UDP_FRAME_LEN;
5 M V7 f U: n6 \: R* a3 a8 f - picture.fin = 0;
+ \7 q) e3 j4 Y6 d; m - }, N, H+ i+ x( v. g4 k% h
- else: q W+ C x& o$ Q3 c" p( I
- {
( E" }' o: @/ u& w0 g1 L - picture.length = fend;! I9 O( T1 y1 \$ w
- picture.fin = 1;2 {: {( H5 I( ~2 U: e9 k
- }# A! T8 Y5 H1 Y. J' p" g4 p
- //printf("sendbytes = %d \n",sendbytes);3 M6 g& d0 F* f* {
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);/ d1 t& y1 ]7 _
- if(sendbytes == -1)
+ k, N2 L: ~' q6 n/ M: B4 ^- u - {
2 W. H/ X4 q8 ^& L. T. @ - printf("Send Picture Failed!d\n");
$ z0 T0 ]. Q- e" w- E( ^ - return -1;
' ]$ _( j N3 d3 d - }0 S1 v, n' E/ L4 F( ~. e! w
- else
& E% @' D( |' |6 { - {
0 l% u# B) }& g( J - fend -= UDP_FRAME_LEN;
7 }: K" h8 ~8 y5 j& a% `4 n - }+ @- Z- M, |6 [
- }
复制代码
7 V p; R; n: n- e' q6 i$ v
) G1 l" ^7 j' {% r- @2 y( [, I ; p+ {: T. @; N1 h. I0 O0 k# ~5 a
, R' c- b% O; @: Q1 S' LiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |