本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 1 O. v) i& i+ ~, S
8 B* f+ o$ o% F- n 9 q# M/ X* S% Y# Q
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
8 C" ^1 Y! a, K9 q1 j A: L7 U$ Q% [
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 % Y5 ~ Q N: y' n+ z3 H3 I3 Z2 C
 9 K! X9 q8 F) `, a
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 . E) J9 B/ V2 t" t" }) I! C0 S
一、HTTP网页服务器* S7 b; q* F* ?
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
$ P# L3 R9 r/ k0 K - {0 I q/ `% j |: X
- struct sockaddr_in servaddr;
8 v9 X; q* m+ T - socklen_t addrsize = sizeof(struct sockaddr);
( P5 W% t4 P% u2 x. h; Y4 W8 m - bzero(&servaddr , sizeof(servaddr));! H2 p! L0 V% p2 p& _
- servaddr.sin_family = AF_INET;
" u( @8 g8 |+ T1 G/ n0 h# ~ - servaddr.sin_addr.s_addr = inet_addr(ip);
- h0 K g% m! f9 r - servaddr.sin_port = htons(port);
- \2 K! @7 ~; K" W3 D; z( e n - int ret;
' t% Z& P# l6 o$ K! x: q/ } - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1), a2 P& Y% \5 U1 P( K0 p
- {# D* r5 L: }( x) Z# |/ [7 Q# C
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);+ J+ S K+ {; z" o P7 F7 g" H+ b
- return -1;
" Q' E: I6 k R! G% v, P6 O - }
7 g* V& S! R) |9 x" S. n6 z - int on = 1;/ N+ e7 J' X. H( N* g! d: @
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0). k7 Q' M+ U, G" [0 \! F
- {4 J% h" W! V0 u& E7 Q$ C
- printf("setsockopt error\n");
% `( E6 A' m9 Y - }0 a4 G8 d' j( N, l: M
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);; c5 N/ u+ c) Y" E) D3 q
- if(ret == -1)9 T( U s2 A7 u- n" I F* v
- {5 {7 G+ ~3 K4 D: e& u- h/ z* i
- printf("Tcp bind faiLED!\n");
( e% Z! F, R0 L; W; H# l, {; @ - return -1;
1 }; ~" ~1 k$ Q2 w9 V I q - }! j {6 Z K6 U# p3 P# K
- if(listen(*socket_found , 5) == -1)
( \% s) d/ x& l! a - {: q9 p5 C( l U c& h
- printf("Listen failed!\n");
8 Q' J, O5 D4 m' k5 a - return -1;
! ^; f* c+ n# C4 [( V+ c - }! ~! a0 h0 y, w- B3 K! R7 s' R
- return 0;5 f' ?& m B# | j; o" Q
- }
复制代码其中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);
( Y* t9 Y5 z/ V) x& J# I0 D - void * Thread_TCP_Web_Recv(void *arg)
, o* E, o) Y f- l5 j/ f - {
+ S3 R$ ?5 G* ^' {( N5 v - 。。。0 B1 |2 J Z$ h/ [+ \ U
- while(1)
1 W1 V( s& n2 C/ b4 z( Z* } - {
) x% c0 B% n( ~/ k( `! T. ? - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);3 O, I7 p* I6 B4 ~* ?% j
- printf("fd_socket_conn = accept()\n");
5 ? a/ X% I9 g1 m - 。。。
5 I' v1 T" [0 l7 s; o& j - recv(fd_socket_conn , recvbuf , 1000 , 0);" `: W& ?* I: {8 x
- }$ P! Q/ ]1 ~# j7 n# \" r" R0 l
- 。。。
% y3 t" A8 ?, d4 A3 h - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);( }2 s) _& Y, E% V0 B& ]9 ^
- pic_tmpbuffer = pic.tmpbuffer;
/ ?/ u! u+ l: x$ V; J, I( ? - pic.tmpbytesused = buff.bytesused;
7 z# V* L' F: h, B1 t5 s - pic_tmpbytesused = pic.tmpbytesused;
, Y, h' M( Q _, M3 c - pthread_cond_broadcast(&pct);
z7 g o3 v8 ?% O' q" H. m - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
1 K/ `* O6 s; M& w - pthread_cond_t pct;
, q$ F5 J ~( {3 \1 M - int main(int argc, char* argv[])6 h# L( r& m! L/ G* x' V. a
- {/ c- ` }7 N m, B3 Y3 a
- ...9 `2 \( E* t+ I& W
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
7 m1 t6 _8 S% z5 B+ i' H, R - pthread_mutex_init(&pmt , NULL);' K+ y3 _$ y- @) a: s
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);* {6 o$ I- Q$ B6 ?$ Q: a
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);7 e5 W/ v8 N& \5 M! X- [- L
- ...& R( u0 x* N) w) l l8 _
- while(1)7 }9 p; j0 b: [- m
- {5 I% p3 x4 Y! T$ T/ v H* _
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);- }3 w. R& g' ]# r! m/ ~5 J U
- ...0 h3 W: d. f; g& z& K% @9 i. v
- }: e' Y5 [( z: R3 b' J- _) I" e, M
- ...
) K# f4 w( Z% `8 Z* W, y - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
7 `, p) S5 |3 Z# C# }* z% 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" \/ Z- @5 S- n3 ]) z- H
- "Server: MJPG-Streamer/0.2\r\n" \ e5 c* M0 [5 Q8 S# J
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \4 W5 B2 z3 z4 n6 S
- "Pragma: no-cache\r\n" \) Q! b1 p, L, v' M$ V
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"9 \7 Y5 M8 C) z) A7 W# i
- #define BOUNDARY "boundarydonotcross"# E3 p, N8 \/ W3 e0 Q" |5 N
- printf("preparing header\n");
, h6 v) y# f* w: g$ h4 _8 | - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
# j0 S- v) h6 ~& J7 q# a3 ?5 c - "Access-Control-Allow-Origin: *\r\n" \
- E3 ~% k# t2 c - STD_HEADER \1 c- Z R; i7 E) F" l; \5 t8 Q
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
6 G& S, j" p* A1 [% k, k( c - "\r\n" \
: x2 N1 u% _$ {( @* y - "--" BOUNDARY "\r\n");
/ ]3 S$ y- j$ N# ` - if(write(fd, buffer, strlen(buffer)) < 0)
. k2 Z# u6 b. |$ O- j, D0 s9 ? - {) v3 ~( L$ b( y0 R1 ?2 O
- free(frame);6 r, k- g5 @4 f5 f4 ?2 W) h: F
- return; U2 _/ k! S3 W s5 M* u$ p; J
- }
复制代码发送完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" \
3 D. \( `* o( t9 { - "Content-Length: %d\r\n" \- Z; M* I* a& D2 S& o1 U
- "X-Timestamp: %d.%06d\r\n" \$ |4 T3 q9 v* N9 E# Q0 y# V
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);7 {1 f4 k+ t4 U) l/ P. O3 c" G1 R9 f/ l
- printf("sending intemdiate header\n");
8 b4 v8 E2 c* s) B( Z% W - if(write(fd, buffer, strlen(buffer)) < 0)# y. g8 f: ?, s; w+ h
- break;' q# h7 Z: w# b: p+ \
- printf("sending frame\n");
& E9 T/ \6 p6 a, |4 t3 o - if(write(fd, frame, frame_size) < 0)4 k' J0 R4 }, C5 ~* A2 e) e" R
- break;+ k3 Q# a/ s7 J4 |- y6 Z" S" r# U5 Z
- printf("sending boundary\n");
" p! @) l0 g h - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");4 |1 y7 S3 c( H+ R
- if(write(fd, buffer, strlen(buffer)) < 0); y- n; l/ P0 F2 r( m5 d
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  & s7 I( @9 m l* f
 6 e7 s5 {8 Y; q( J/ h! w! a/ C
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:( }+ C" |8 e; U4 C
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
) z3 Q& f: A2 `( \" {: ~3 T - {
& a( l: G( _6 `# P7 O; }) { - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
$ V+ x: H* K+ k: X+ _: q - if(*socket_found == (~0))+ F: k: T( F( Q/ C. g
- {
5 {; N" |) i5 T6 g8 E! A - printf("Create udp send socket failed!\n");/ G! y5 k; k3 |" d! I d
- return -1; t6 w4 p5 T; N( @0 L; ?
- }
+ d0 O; n" p( \ - addr->sin_family = AF_INET;
; w2 U/ B$ ], K% s* s - addr->sin_addr.s_addr = inet_addr(ip);
2 ^: F* }( h6 u2 {. ` N - addr->sin_port = htons(port);
; {4 ~4 y9 a f# w& k' J: j - memset(addr->sin_zero, 0, 8);
" E9 Q- x3 g; ~4 }* c+ u - return 0;0 E' \- F. H8 _# B4 ^$ W8 M3 e% B
- }
复制代码
1 P0 v2 x% F4 Z' R0 O
7 R$ c# s! k0 d1 e. x7 x7 ]- A& I而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:6 p/ A6 d8 }6 y( a6 ~) Q+ s- y
# f3 Z' U% D0 w2 b# p6 c8 R/ e! _- _( ^3 f1 Y
- while(fend > 0)
6 d$ Y6 W- L* U+ t: K+ ~ - {
& h/ [& D; H3 ^. `+ D - memset(picture.data , 0 , sizeof(picture.data));
- D" E+ p/ I/ I7 n& P" E - fread(picture.data , UDP_FRAME_LEN , 1, fp);+ T! v: k1 p. K, V8 v3 P
- if(fend >= UDP_FRAME_LEN)
+ _" q% j. C+ c3 t& E4 x - {
: A: B7 r+ W6 t, g& G! r - picture.length = UDP_FRAME_LEN;
/ L3 b8 x: q! r+ c: ~* k - picture.fin = 0;& D4 C! \/ Q' j( F+ f
- }2 k6 x' i: z! C! ?+ }
- else; B0 J$ e" z: J ?0 T7 w0 {) N$ ]
- {
% M* i! g! q. ?" S6 [ N& { - picture.length = fend;
- x* B/ A6 z: h9 @. Y* K8 R) _# {! o$ Y - picture.fin = 1;6 O2 n7 u q- t/ ?8 d, i
- }
* |: {/ W& E( z - //printf("sendbytes = %d \n",sendbytes);% i+ a. Q+ m/ y. `# X
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
1 I% x4 y+ y, V5 E1 T; c7 S6 n' z - if(sendbytes == -1)
! H7 l# E& y0 ?$ p - {
1 a6 O H# G8 J - printf("Send Picture Failed!d\n");
" h* }' @( t. b! T4 ^, B: v - return -1;
) ]1 o; O. Q* t4 ]/ r - }1 L2 I+ ^5 H- a; u ?: e: c3 D
- else
+ U" N$ }" s: t, `& o - {
* A6 s; U+ _9 f8 ?5 g4 g. G - fend -= UDP_FRAME_LEN;
+ o' |' u3 I G - }6 ]( C: L6 d! v% l4 T& B+ x% g
- }
复制代码 ( l- M7 d; t$ f8 L" Q" L
( j3 y) c6 N7 l$ Y! S( G

: D2 P+ ~7 l8 A; q$ Q! M8 S+ o
' u+ h5 k# b% _& S9 ZiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |