本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
, `& [) g- |7 ]' m; J! @4 o! J
: E; N R* x8 |/ Y* k& \. S1 f( Z
. H' t# p1 ?8 L/ e: p# P( O& K作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html5 `/ d) \: r; E6 `5 K0 R
1 v* k. z2 m- J本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
: Z ?7 }; y6 s3 L+ ` & T2 d; E$ I+ Y U/ z1 F9 `
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
) k5 [" o4 M; \3 ~8 _$ t5 X( i( x: ?一、HTTP网页服务器; f' u2 `9 ~, O4 K! z w
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)7 w% K k @: I8 @* V! W
- {
! K, [- T4 C0 T# d, r - struct sockaddr_in servaddr;
0 [$ g* h* o0 {6 k) _ - socklen_t addrsize = sizeof(struct sockaddr);
: N' [+ q- I3 } - bzero(&servaddr , sizeof(servaddr));
1 S. ~9 p* F$ M7 {6 s- I - servaddr.sin_family = AF_INET;
* `6 b) l+ X- s2 ?7 g0 C+ w& V - servaddr.sin_addr.s_addr = inet_addr(ip);- N7 V5 u$ T6 J; |
- servaddr.sin_port = htons(port);0 F% E( O: X2 f6 B9 Q0 t9 r+ K
- int ret;
. I4 c8 O7 M4 I# L4 |1 A0 C( w - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)7 X6 s* @6 \0 @' W1 k
- { u9 K4 k% ~7 w
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
1 |4 n) N f4 z8 v) h - return -1;0 I4 z6 y9 r6 @6 A: m, b! s6 N/ [
- }0 n8 A. i; G! ] X, f8 O- p* J
- int on = 1;
! h% W4 h7 ?* G- }! v! Y& C - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)) n( A; Y, C2 f& R* e# |4 U# ^
- {
# ]. N% ] ?( s# |& A' b - printf("setsockopt error\n");
0 G0 c: P+ F8 y/ l - }
; \2 |9 v/ e s; o - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);5 F7 F, D' v' P; R" y/ T
- if(ret == -1)+ W' g4 m ?6 W* g1 z
- {
/ J G5 n0 F4 q- I" ? - printf("Tcp bind faiLED!\n");0 V5 s3 E! r# o5 O+ z, M
- return -1;
: H! t" @& N0 N - }
# \! Q9 C' ?+ M* c, K/ N - if(listen(*socket_found , 5) == -1)) b. Q# O ]- S* a- E. C; @, F
- {
3 B) m* R i5 ^" |& R: g - printf("Listen failed!\n");; ^4 f- W- H. r4 V8 ?; c+ t O, \
- return -1;. U! S. X) f) J
- }: [5 g' Z1 G) H
- return 0;" L! [6 n5 i# x/ }/ a2 g! D" V
- }
复制代码其中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);
) T& e' v. X8 |$ y2 l: M - void * Thread_TCP_Web_Recv(void *arg)
! e! R9 j+ L. J" B) e, V - {- R$ M0 z* C0 }1 ^+ s8 z }
- 。。。
6 w u% J) j, C# j, L - while(1)
$ y G3 `, s0 _) D( x - {
a7 K; a: |; Y" g: ? - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
4 d: `. N) u3 z+ N* W! p6 E - printf("fd_socket_conn = accept()\n");% h1 Z& j/ X8 E9 R& U9 k
- 。。。
, F6 P2 X# @4 d+ A - recv(fd_socket_conn , recvbuf , 1000 , 0);( L0 N1 z3 K; K/ B X
- }0 o) I: t- @* g0 c2 j
- 。。。
! a- B* {3 c# U - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
8 j5 U+ v' F9 W - pic_tmpbuffer = pic.tmpbuffer;4 [7 @3 E- b7 M" P' r
- pic.tmpbytesused = buff.bytesused;- }" ?( L' x. M+ ~, L
- pic_tmpbytesused = pic.tmpbytesused;, G2 \' F, ^5 i" h! D' V
- pthread_cond_broadcast(&pct);4 }1 h9 V' J; y; d& |( O1 O
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
) M7 E3 J M- C* w6 l - pthread_cond_t pct;7 O( @3 U( H6 P
- int main(int argc, char* argv[])% F& }* N" t* L* r( f0 \
- {6 ^4 B( k6 ~7 z1 ?: H( {
- ...
# h, p, M* \" z3 i; L - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
: R* D# u" Q: l7 E- S- w0 q" E6 O" { - pthread_mutex_init(&pmt , NULL);! E( Z% ?5 N. }4 y/ |# C/ I
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
( B P9 a0 Z, t5 c) I5 o+ P - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);6 u2 o6 W5 N, `" i, F
- ...: C2 S1 O- E9 ]* v) I; m
- while(1)& P( V" g: R$ C! d" \6 E* M) P. G
- {$ P1 l+ a9 ?) o9 r# t" T
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
& W( R' O( a4 }6 s - ...0 p" d3 S9 J: w# X
- }
0 H4 b7 E: \2 F: j. u - ...
: h! S. J+ K5 M7 R+ Q; j" m; @ - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
: m1 \8 |) U3 J5 i5 `* x0 G, e( z - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \" s5 \$ J* r5 p5 X
- "Server: MJPG-Streamer/0.2\r\n" \
" R V( `, H7 X% K3 }+ w5 z - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \$ [9 @0 z1 F8 Q+ D4 ^7 _/ w
- "Pragma: no-cache\r\n" \7 r3 a% K1 _; n; `8 Y$ `
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n". l' z8 |, @8 B0 [ Q
- #define BOUNDARY "boundarydonotcross"
0 h: `0 _1 H' q" h; f- P' {+ P - printf("preparing header\n");
, y* Z% |& w, t/ \5 K - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
. n" e: ]7 y7 R _! e - "Access-Control-Allow-Origin: *\r\n" \5 _: m) S$ ]4 v# c: J+ ? g) o- p
- STD_HEADER \" P; c4 S- ~4 n$ y3 u
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \* h. _8 Z3 w2 E- |
- "\r\n" \
+ W' h5 Q, Q* x, B, e. c% v - "--" BOUNDARY "\r\n");
0 P4 f, v$ L! S - if(write(fd, buffer, strlen(buffer)) < 0)+ j9 T3 s# P; ~
- {
1 K$ U2 P: O) S# A* N - free(frame); o1 }% ]: I4 q( b6 q
- return;
' ]0 Y* c7 s9 Z ?) Q - }
复制代码发送完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" \; R8 K' s. `- S% D) p% ?+ X
- "Content-Length: %d\r\n" \
+ @2 @9 c d" X9 N - "X-Timestamp: %d.%06d\r\n" \
: d' m5 c% V3 d }& l9 V$ Y - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);3 s5 t0 A' f; j0 x' {, m
- printf("sending intemdiate header\n");
6 F% |4 L3 n/ k0 i - if(write(fd, buffer, strlen(buffer)) < 0): I- [8 z9 i: s3 A& v. U' V# N8 N3 M
- break;1 Z' R* A8 H+ G P3 o4 J
- printf("sending frame\n");- R0 v( K9 p; u5 H5 d
- if(write(fd, frame, frame_size) < 0)" `' I9 ^& J# Q7 h; z& p
- break;5 F }# d! h" k3 j" y# W
- printf("sending boundary\n");2 C; S5 o# }8 C+ T( ?3 Y" s
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");' ~# b8 \4 `! ?. Q7 U
- if(write(fd, buffer, strlen(buffer)) < 0)3 c: z; i. c& U! P5 ~
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  - {% }" w6 y1 L/ ~4 I$ W

6 b, F- O8 S& I+ i7 |; K' j3 s$ K二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
9 E3 i+ n$ d: K+ O( b1 x; x% d- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port): z+ c& y# H! R3 O- U* M( R, J0 ^5 t
- {
! r6 J t9 ?5 N: P% U6 r - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
* u9 f8 t! R. p9 W" J" H - if(*socket_found == (~0))" A- L9 ~! z$ ]1 h Y1 e+ w$ p
- {# z" c, K6 k7 l( u
- printf("Create udp send socket failed!\n");+ I5 x( A' S1 q ]
- return -1;8 F$ E! Z6 T1 O' [1 ~
- }( k! ]& S$ J+ w- q2 G; T
- addr->sin_family = AF_INET; d0 |3 [" F& c& s) R
- addr->sin_addr.s_addr = inet_addr(ip);( U7 ], y" r& y% y2 ^
- addr->sin_port = htons(port);
* c# t) m7 g2 y% Z! @$ N - memset(addr->sin_zero, 0, 8);
! k/ C! @0 \7 W% \+ [; a2 ` - return 0;
4 W, Y; A- N7 c/ b ^ - }
复制代码 ( t9 {/ m: v* B' Q7 ]
5 R# v: U6 q/ \, W, @* E而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
) \: A/ z7 W% n: f9 D1 }% Z
+ J' N9 M8 e8 J; O* p4 z4 c' g" N
+ P& V! ~! w7 C' }0 P- while(fend > 0)& ^' e A; H. i8 n
- {
' w* e) P; g+ y - memset(picture.data , 0 , sizeof(picture.data));
$ G0 `5 n1 T2 C& f: ` - fread(picture.data , UDP_FRAME_LEN , 1, fp);9 f4 R7 c- q( Y* H2 n! |9 m
- if(fend >= UDP_FRAME_LEN)
' |5 `! \4 H: p+ J - {
l! t0 V& R9 P$ D' Z - picture.length = UDP_FRAME_LEN;
4 \. i6 n( M6 j2 ]* ? - picture.fin = 0;, D& u+ V6 \( t6 b3 x
- }, i( P$ z: \6 {) n5 Z) T
- else
# {$ m2 k" a8 d; w7 V& d: M - {
( [* o' w9 ^8 l - picture.length = fend;7 i9 D0 e+ O- F. l: Z( n* w" x
- picture.fin = 1;5 ?( Z0 Y- Y. A$ r
- }3 }( c4 G7 N% l0 K& P' w, a9 L
- //printf("sendbytes = %d \n",sendbytes);
. A9 Y$ r4 ]. S; h8 i - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
" W) F$ T! Y4 m" l3 H9 U8 m - if(sendbytes == -1)
/ O" B. R$ H' [' F5 m - {4 [+ p* W9 @, N9 h+ v6 u D
- printf("Send Picture Failed!d\n");
$ B7 _: n' J' C# n1 q - return -1;3 {' s& L4 ~0 z1 _
- }8 ]6 S2 Z+ K7 k6 W
- else
% o* e% \* x( k" ~3 j - {; M d1 a3 \, y0 ?: `- K& M
- fend -= UDP_FRAME_LEN;
; Y+ ]/ u: s% @! L( ~6 E! u/ @ B6 Z - }- e, b4 W$ h: b+ y9 j5 w3 @" ~
- }
复制代码 $ l* M3 s { ` @1 b* U8 ?
: ^! P- p' P5 {, `

6 g5 n" D8 D2 |/ Z6 L/ F2 j9 m x$ b' y) R. W" f$ y: \" X
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |