本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
' L8 G0 W6 h& @- }2 b; i3 z( b! X9 d- C) Y; T) \- N$ G/ m+ ^0 L' N1 T
 9 T0 `" l% ?" O( ~" A' w F3 v% F$ C
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
! S3 @+ v+ D+ \& {) r# I
. t8 J3 M) H/ @# O4 X( E本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 ( w8 ~0 M R& L/ S9 O: Y- P& ^

% v3 \& ^- Y+ K0 ^+ g2 S5 V7 l两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
. b1 X0 X, l* X一、HTTP网页服务器& s; O4 X3 a7 A- R+ ~
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
8 g9 }$ C8 J4 U - {
$ J& P6 R/ f1 s, n - struct sockaddr_in servaddr;! ^/ }% F9 c& g3 [! I1 W
- socklen_t addrsize = sizeof(struct sockaddr);
0 l5 x# p0 x2 L7 N q) Z - bzero(&servaddr , sizeof(servaddr));
& F1 R* D: S$ I( B3 r - servaddr.sin_family = AF_INET;
! y$ F4 @6 c; X( X; t! N% x0 L9 s - servaddr.sin_addr.s_addr = inet_addr(ip); L N3 R$ `( ^0 _
- servaddr.sin_port = htons(port);
r* S% Q {4 ^ A \0 i+ M8 R& y - int ret;4 v, K, v- m. Q+ {' g1 o
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)* g- P0 n. Z* P" J
- {# v7 k& S, R- @+ ]4 a F( t5 [: i1 a
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
) q* s: y7 D" t0 ~$ C9 d6 w - return -1;
# _; u4 @/ j" h. m5 C6 f - }
- U5 E: s G6 a1 [ - int on = 1;
5 k# M7 Z4 c: E6 m" k - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)+ j3 j! r% g0 f0 i Y
- {1 i2 f# M9 O7 p
- printf("setsockopt error\n");
' N4 B) F+ T, p% F$ U - }
* i. m ~2 C) R( p! u9 ]4 T - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);7 ?4 Y: ~9 i* X E R: y
- if(ret == -1)7 B) v8 b2 M( o) N, p
- {( \# c9 Z. s% U; }) k$ T
- printf("Tcp bind faiLED!\n");! {5 _' G% I9 }# P. H% h
- return -1;: e2 n0 }& G' K, e
- }% G1 A$ h w' R( ~' F( g/ y
- if(listen(*socket_found , 5) == -1)
6 J" @" @# j3 ?+ a - {: A7 H! M' X3 J2 F4 Z. i
- printf("Listen failed!\n");$ c" F: v( S! s& S& V f
- return -1;
, x% H0 r$ X% |+ l j+ d# e - }
, y# X8 u' \- G# @ I } - return 0;! L. U* H$ M7 t: `, G
- }
复制代码其中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);
1 ^, m9 g0 r! j. l7 i2 \, n, C - void * Thread_TCP_Web_Recv(void *arg): \ }# e0 V) q' L
- {
) T; j) ^# [) X - 。。。
. V" \# h4 m1 h5 n6 c& l - while(1)2 ^+ w8 _! Z, p0 D( U% q
- {
4 l, {1 a: \" B/ N: |3 v - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);$ z& E$ f! [# |& o6 U* U. f& h* S
- printf("fd_socket_conn = accept()\n");1 m; y. i* Z) Z Z/ D3 e1 s
- 。。。9 l. D$ T, u; C9 w: Z( ]
- recv(fd_socket_conn , recvbuf , 1000 , 0);8 I( G- S3 y5 y5 O
- }
( c: [3 K' K9 j. h9 ^- d7 u - 。。。: f, G1 c3 B+ c0 r t/ d5 t# R
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);! i+ ~5 b, ?) \; e& P
- pic_tmpbuffer = pic.tmpbuffer;
$ I! N( K' w7 i - pic.tmpbytesused = buff.bytesused;
; k$ |! ~ G: P - pic_tmpbytesused = pic.tmpbytesused;
+ f+ A1 b4 Y$ Z* n& u' H - pthread_cond_broadcast(&pct);* _8 p! \1 N q) B: m8 p
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;& i# v( |7 F( a6 o# `" ]
- pthread_cond_t pct;
0 g* l5 u( @. p# }9 A: w - int main(int argc, char* argv[])
' F* j+ Q) R+ M. H% n - {
/ n* h4 y7 n" V; C, |- q - ...
" S$ o; E$ P) @* r% x - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
4 S: e& ^& a2 i1 y0 j7 g - pthread_mutex_init(&pmt , NULL);0 [% E- ]1 B6 c6 F7 H9 r% Z9 E' v
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);+ F& \9 X4 g: X. V' b$ p' @
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
. k, Y0 V% t* @6 C- T* T$ |% f - ...% k" _$ {# V! T) U
- while(1)1 c2 o+ H7 m0 a# ?
- {
0 {, u; o! \& {. Q - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);' n! u4 _' x; Y; n
- ...; s8 a/ w9 \, I8 M
- }
b% o- _& E2 D6 O5 ]' V - ...) o' o3 t r) N# i" |# b$ v( s
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
2 p/ C2 [* l2 Z" g - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \) H& d2 r4 p T4 q! P8 Q: N9 p
- "Server: MJPG-Streamer/0.2\r\n" \
" S5 u* i) B( d, K# ]; M5 I U - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \2 ]! ] `! ~; }* p4 Y
- "Pragma: no-cache\r\n" \
+ Q4 M* s( e2 J( |. ^4 D - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"# O$ W. ?, [; s% {$ _& B$ ^3 _. K
- #define BOUNDARY "boundarydonotcross"
4 a' m k5 P- [3 _, n2 ] - printf("preparing header\n");. J8 ~8 d# F* J) `+ Q# V- o8 I. o+ }
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
; H. `; E, m* i& x - "Access-Control-Allow-Origin: *\r\n" \
' _* ?* s- f, b2 P/ C6 }) c5 { - STD_HEADER \. V* k/ l" o9 t r/ f( T1 r( j5 o
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \/ r# _+ Q) |% ?% P& g* L' e
- "\r\n" \& A7 ~3 d: X1 j) P
- "--" BOUNDARY "\r\n");9 X! o: \) f( w6 D; u
- if(write(fd, buffer, strlen(buffer)) < 0)' R$ F* D6 T5 Q5 v( M" ?4 D
- {. x5 F5 C& s. w% P* T
- free(frame);2 [+ H, N& G: \ ?! X& ]
- return;
# U r! P! r% N8 n - }
复制代码发送完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" \
( u- ^8 L& n/ a$ @! H - "Content-Length: %d\r\n" \2 F1 @' N9 y" }/ T
- "X-Timestamp: %d.%06d\r\n" \/ P, O, V: s; P3 [6 b
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);' t6 m9 k& j0 T& r# t5 G8 B) M
- printf("sending intemdiate header\n");
$ }0 _. U+ b6 H2 O/ S8 m5 y - if(write(fd, buffer, strlen(buffer)) < 0)
% {9 i: W! V0 k# r- \8 D - break;
3 u% O) A$ Q' m5 x, G6 y/ {7 w - printf("sending frame\n");# M- U* {2 G6 q$ j2 d$ U* \
- if(write(fd, frame, frame_size) < 0)( M. W9 E5 d2 x7 @& ^, I6 {/ l- d
- break;
. q C+ I* U/ c% t2 f2 _3 f - printf("sending boundary\n");
Y+ B: A& V1 E* j$ [( R+ [ - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
: @, X! D# A5 @. C - if(write(fd, buffer, strlen(buffer)) < 0)
, |3 h8 F% e6 {& s - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  + ]( v4 r2 |# h% R' b5 s! k

) f q' I0 L8 m2 G2 f8 e5 {二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:# i' F. A$ z! ]8 d
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
p3 S; D% V% L" M - {
% E. p0 {% d9 e' \; i - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
$ O: g* ?' t1 h4 k5 c g/ Q( |0 U - if(*socket_found == (~0))
- w1 v. n5 L5 X - {9 d6 `9 V' o7 X9 q( ]/ S
- printf("Create udp send socket failed!\n");* n1 M8 D3 n# u+ I* E) N( M3 S
- return -1;9 y0 U8 s' z- Q2 C7 b6 t
- }
5 C2 h$ P9 D N- \# k - addr->sin_family = AF_INET;
8 k2 w/ h. G3 \9 ]! V" n4 l C - addr->sin_addr.s_addr = inet_addr(ip);1 f! B) t) E$ b
- addr->sin_port = htons(port);
. E% r) E1 n% }3 T+ p( k$ q7 _* T - memset(addr->sin_zero, 0, 8);
0 Y& s/ l. Q) }6 E3 F3 v) W) [ - return 0;# U. [0 G' E+ W% k
- }
复制代码
3 x& ~% G7 t2 ?# t8 T- v9 @) L, K; P
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:0 L0 B+ y9 B) B1 `8 ~7 c
7 ~% P0 ~, _! r! e5 Z j6 f, {' _ `5 w, J7 ]
- while(fend > 0)
$ e& X b: ~: h# o - {
5 r1 }* ~- i& m7 w( \ - memset(picture.data , 0 , sizeof(picture.data));; I9 @3 U0 J2 T
- fread(picture.data , UDP_FRAME_LEN , 1, fp);5 C" Y, x: W6 q
- if(fend >= UDP_FRAME_LEN)$ C; i. a. r; F0 F' b- f
- {
2 ?9 X4 H. z1 }# l# V- j1 z - picture.length = UDP_FRAME_LEN;/ z$ }: h5 G1 {; M4 `
- picture.fin = 0;
) ?) v( k& L7 U3 D - }
: H# G! l: g4 Y4 [+ l - else
) n& u3 {2 R* Y - {# B- W' F( I7 a, u; _: S( C
- picture.length = fend;4 }: j/ ]* A; M' f. M
- picture.fin = 1;
o# d9 Y3 g4 \: { - }
5 }$ U% I7 Y7 b. o - //printf("sendbytes = %d \n",sendbytes);7 c7 Y$ B1 W- C+ F8 N7 p
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);. n% W6 p9 `# V _! W6 R
- if(sendbytes == -1)+ l- `7 ?9 q9 M3 A9 W K
- {
: O- t0 C& |' D; C0 A6 q - printf("Send Picture Failed!d\n");
0 Y5 a8 [, V* I4 y0 Y - return -1;
) j1 y' `. S9 A( i+ V4 C9 y - }
' F, e0 n N7 `# ~7 W. q - else
/ S" l$ q2 s. N* ]* ` - {( m# T3 U, v, a6 W' G
- fend -= UDP_FRAME_LEN;
9 ^4 f1 U) \5 y [$ b1 s. U - }6 O* T1 ]( @; y. P; ^( p8 K' y
- }
复制代码 % n$ k' B$ i1 Y9 x( n
9 n0 g0 u* t1 L
 4 t8 S. h- C& T1 P- F" Z. {+ P5 p( z
+ r7 A: P [- a0 b6 ~iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |