本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
/ m" O$ h+ x) M% ~6 s! N! x! [) s. a% \$ @/ N: A
 9 R& G# u' v- X# ^! b: l5 s9 Q, e% V. z
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html( H5 r! F7 F# | q, s: c* w
3 O3 v/ X" Y' A$ P
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
9 \% s# Q# K: r; v$ E; {. b
& S0 c& E4 i7 j/ n两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 & F4 c( r5 t5 p) r; d
一、HTTP网页服务器( a4 g+ P \: u* e9 K1 X
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)3 _ C) b) {% `/ i! Q
- {
% f0 A: S; _, t" k/ r1 y+ N - struct sockaddr_in servaddr;
; I6 v; @3 p- J3 t - socklen_t addrsize = sizeof(struct sockaddr);! S/ M- b5 @/ v& I2 E1 U3 Z
- bzero(&servaddr , sizeof(servaddr));
@) G# Y4 F5 T3 H% ? - servaddr.sin_family = AF_INET;
9 `0 D, S1 W* Q0 q1 ~ - servaddr.sin_addr.s_addr = inet_addr(ip);+ X. k6 O0 H& ^) p( s" |, e
- servaddr.sin_port = htons(port);
$ e) O( c G3 V - int ret;1 }* s" n4 @# B) x* L7 n g4 \ F
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)5 U) o+ G' O, a$ q
- {& J2 d5 y: J& h3 x* p7 h
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);: o! H. B8 V, J& c. G" {4 T
- return -1;. C+ P" t' n; |9 L
- }% P/ B; o | }9 n9 t8 ~7 o( [' V
- int on = 1;1 _: j d) W+ O# C. D8 x* _/ L0 |
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
& c9 Y5 N: L: u$ [ - {, b$ z2 A( f0 Y% D# _* v
- printf("setsockopt error\n");6 B7 X) N3 O4 R# }8 E# Y
- }% E; T9 _0 X, _" Z Z+ _: U
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);& @/ A2 [ @8 c" s! `- k
- if(ret == -1)7 ]6 Q8 Y( R& L0 Y9 O
- {
. x! W7 h7 F$ b4 I5 z9 v% n4 f' W - printf("Tcp bind faiLED!\n");! [$ Q8 w6 i& _2 G1 J1 n) }5 W
- return -1;
" \$ l$ q; _+ |7 H) f y. ~ - }
) q2 |# k- D3 [, u7 h) D - if(listen(*socket_found , 5) == -1)4 W; }1 C$ w$ I' _9 [
- {( y; {* x' q* i1 L& {" V2 x E
- printf("Listen failed!\n");
' z, V6 M; s: i) A - return -1;
9 X# e" y q7 _6 u# }9 u R - }0 X9 T9 Y+ T9 A% X# {, W% r
- return 0;
7 x7 @' Q1 m, p. N, X - }
复制代码其中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);
0 J! b1 m$ f. a$ Q1 F1 g3 Y, g - void * Thread_TCP_Web_Recv(void *arg)
; ]: d* f7 x: I" b - {
2 V( R- z. ?) G' n, S4 l) [; } - 。。。
4 e( M" a2 S4 {% W9 j! b0 ` m - while(1)
* ~% V) a; [# w6 H4 J6 N- _* l - {
0 Z- Z5 w8 M( e3 H( P- f: K - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);7 k; ?) F; ^7 h# L a5 H/ S: y
- printf("fd_socket_conn = accept()\n");* k0 B: b" o& o, z% d
- 。。。' \2 T0 ^( G6 ]) p/ b
- recv(fd_socket_conn , recvbuf , 1000 , 0);% _' G' \4 v1 N$ C8 z1 b
- }
" I4 S0 i2 I' L3 x( e- i" H - 。。。
/ v J8 A4 f; s - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);2 p0 O! N( {% |' s& }# Z2 ]
- pic_tmpbuffer = pic.tmpbuffer;+ R) C$ e' s$ s4 r) }4 r1 n/ y5 P
- pic.tmpbytesused = buff.bytesused;
; Y/ E% y" b7 f. m# Z5 ` - pic_tmpbytesused = pic.tmpbytesused;
5 v, {; ]+ q; g o: h - pthread_cond_broadcast(&pct);
2 C8 f8 j$ ~* O/ f - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
, i4 Y4 a' H& S# k - pthread_cond_t pct;* B6 ` w+ T$ }" `
- int main(int argc, char* argv[])9 z1 _( F9 u5 f3 G9 j+ x
- {
7 J8 b" S8 ]3 [. u6 @ - ...* f, o Z3 g' d' N0 U
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP); j, q7 M3 }5 u6 A N- a# j
- pthread_mutex_init(&pmt , NULL);1 K; l! p0 [+ j& L
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
6 k8 D( q5 ~& \. z" J* b3 ~9 T& { - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
+ m% }: g' N- Q/ {1 l! e - ...
! |$ P$ e. o# t& k7 a W - while(1)
9 e0 Z# Y3 U, f' ^/ Q - {4 q6 M8 Q8 x' F/ l
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);7 ^9 S) S1 k, m+ \; {4 l1 G
- ...1 a1 x, o8 Z: ]% r
- }
h% s: `! p8 K7 i% g: i - ...
5 t% `% a2 d: x% N5 F' s - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
3 c% ^( U7 ]' P4 n6 I3 O - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
$ |* r1 \& k7 i) p" S( l9 K# k: N: r - "Server: MJPG-Streamer/0.2\r\n" \
0 ?* @2 n0 C5 x, V2 y2 p- _ - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
/ _% R% Q/ C* \2 {# s6 G - "Pragma: no-cache\r\n" \
9 @/ p, _9 _0 | - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"7 `: x# a) i7 e# d7 `; C
- #define BOUNDARY "boundarydonotcross"
& ?$ w. M* {) {7 s! g5 j2 S8 x9 ~ - printf("preparing header\n"); d% \5 i. p: G& k1 }
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \* o6 l" U; y7 r* ]
- "Access-Control-Allow-Origin: *\r\n" \6 y. n7 X1 Y: @" ?/ V
- STD_HEADER \& Q4 B( `8 M% Y4 k# B, A( e
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
8 I" a. R" v: U& O% F, n( ~ - "\r\n" \
6 y5 X1 u+ V( N9 Y: Y - "--" BOUNDARY "\r\n");6 l8 g! j) ~. S( S3 B
- if(write(fd, buffer, strlen(buffer)) < 0)( q/ s. k9 C* {! j: b/ _! s
- {
s0 k2 I3 G/ F1 n' g$ W" z - free(frame);
! `. |2 X3 t( `+ f5 Q# x$ f5 k4 d - return;
6 u( ~1 a% M7 f0 e1 v! N6 L6 t - }
复制代码发送完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" \0 P7 D( w# P9 i- K
- "Content-Length: %d\r\n" \. D# v/ j' w* T1 t }! I
- "X-Timestamp: %d.%06d\r\n" \6 }$ V+ M" d7 p0 i* C
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
L! H x+ i0 I9 l4 U9 E. @ - printf("sending intemdiate header\n");7 j! r; l% \: R8 H+ J$ R
- if(write(fd, buffer, strlen(buffer)) < 0): X5 t6 ], Q, M+ D
- break;2 b! ~" p9 C' K5 o- O' v6 W
- printf("sending frame\n");5 g6 j3 Z9 E/ [
- if(write(fd, frame, frame_size) < 0)
3 k l% M# g& @0 T' d7 \ C - break; a5 _: B" C2 s
- printf("sending boundary\n");9 a* k9 |' U `5 B: S `
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
) h4 X) y' c" s% Z" l$ O- K3 k - if(write(fd, buffer, strlen(buffer)) < 0)) t5 x0 w, |% V
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  + n) s# ` c/ C- ~/ E4 Q' u. P
 2 F! Y/ t1 `, N4 B# O# q7 L% Q
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
: ~3 J6 r% V0 y- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)+ V2 C% z: h: F1 e. a
- {
! t( I7 V7 }' i - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
: I" ]7 A* T( L3 f, W% | - if(*socket_found == (~0))
% n- g4 Y- s/ Z - { c9 f0 B4 q0 m
- printf("Create udp send socket failed!\n");! @ [3 \! l( ~5 R, b! N
- return -1;
$ J3 H) ]5 Y' S7 g1 a/ m - }& k5 F) n6 `4 z7 ]
- addr->sin_family = AF_INET;% F9 _# p) n7 E4 E1 H! W+ o
- addr->sin_addr.s_addr = inet_addr(ip);
4 ~/ a! _7 }( D/ E C5 I - addr->sin_port = htons(port);" }- j A! S6 p( k! L3 ]
- memset(addr->sin_zero, 0, 8);4 b! A6 b: e- x& X& }+ |" P
- return 0;* ?' Z2 L) u2 |# j# c2 M( ]
- }
复制代码 9 \ L( i/ m% L, D c
; a$ N; V! c$ ~6 L8 ]8 f
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
3 _! n3 }+ X" _2 `7 g
+ Y" z" C2 U1 A* E L) d
6 h$ s: _6 d/ R' {0 S0 ~$ c c" I- while(fend > 0)+ f" e3 ?3 N. k. |8 u9 E9 i
- {" n$ H3 R" E( u, b7 @) ~/ ^
- memset(picture.data , 0 , sizeof(picture.data));
! Q0 P7 T! K0 x9 J6 B - fread(picture.data , UDP_FRAME_LEN , 1, fp);0 m/ H$ F) i. S3 x( s3 Y
- if(fend >= UDP_FRAME_LEN)
1 @# g9 r* P5 m* f2 r; N0 }# z; C - {# e# G. U6 F. E, j" F1 L3 T! O
- picture.length = UDP_FRAME_LEN;
: _- p E/ i7 s9 a# w6 T - picture.fin = 0;
M; y7 {. A( a; p9 Y - }0 u# I; D3 ]( U( n3 q/ B; B. U
- else
) }0 x7 P& b9 p- V, M3 X# T) l - {6 X+ A7 b/ Q2 b3 ~. A) S
- picture.length = fend;6 v; g2 U5 j( d9 n* e
- picture.fin = 1;8 @, U: G4 @9 k, s2 @% D
- }8 L& w/ ^6 t* W- h/ R
- //printf("sendbytes = %d \n",sendbytes);! U0 o5 d# F, ~1 {6 ^* k
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
' o* E: A7 Y) @; L O0 [ - if(sendbytes == -1)
5 m: Z4 T% X9 F; t/ O! b1 ^+ C: { - {
* i# k5 q6 J$ [/ A* {# j- B - printf("Send Picture Failed!d\n");2 H) h/ W, e) W! p# L K1 r2 G
- return -1;
% N2 y& a) Q. G8 o# l - }
7 R1 v2 ~3 u# \: y) g- B( H - else' p. M# e5 `( r8 Q; h/ @
- {
" f; }" T. A8 M, _: f - fend -= UDP_FRAME_LEN;
% I6 K% e' g j s0 ^ - }
A4 Q7 j0 q. i" W6 V1 q& ?9 I - }
复制代码
, ?3 Z, E1 g7 v- _4 I+ l
) d* r& q# f( [; g
# F6 ]6 y5 \' n" P- q& ^3 E) ~$ Z: s: P% k4 u! m5 w
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |