本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 7 y, t1 @3 P% `5 N" x$ I
6 E0 M7 ~* R2 z! D2 R4 Z
8 L b. T# z+ [. l" f, T) T作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html8 J5 A4 l/ L& u
+ _9 F. S' C3 y" e5 R; I本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 : j6 @; w: g- a2 f. B! `3 w
 $ i& E: k5 x% t
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
1 S1 e4 y% o+ _) h9 A一、HTTP网页服务器0 \. N/ p& \. Y5 z' X7 p
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
. N$ a7 t6 s* d( X# A - {/ o, R6 a/ d7 C- E3 b
- struct sockaddr_in servaddr;
! N9 \ c' p7 ^1 x - socklen_t addrsize = sizeof(struct sockaddr);
+ h4 t! p: T8 j* Y - bzero(&servaddr , sizeof(servaddr));7 x4 k$ T$ l+ p0 k* R, f
- servaddr.sin_family = AF_INET;, w: e) a" V7 G& J S& ~- V
- servaddr.sin_addr.s_addr = inet_addr(ip);% v, H; G1 g! ~/ q2 f2 C
- servaddr.sin_port = htons(port);* P: W* l8 O7 E1 I
- int ret;; G3 @; V* U2 u/ U9 W; B: n' f
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)- l2 l X5 {- K* @/ E3 n; E
- {
9 |' U- h8 E( [! Z. ? - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
7 J( v! G% u3 D( I9 l% D& ~ - return -1;
" S" A1 M, r. E& W0 B' { - }! ? ^ I: }1 [* E# W% I, G- |
- int on = 1;
G( k0 U& G+ R# U: i2 ~! v6 ? - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
# n/ B: ~( b( w: f' I1 c - {0 m, H# Q- b- U
- printf("setsockopt error\n");
! G$ ]1 `7 p% [* a7 D0 ^ - }1 q7 h) e0 g0 r$ k) M( R
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);5 X0 j2 o7 t" t1 n4 c9 g% s
- if(ret == -1)
9 x: V# g H+ u; {9 |' r - {4 K# \# b3 v& P, i/ ?6 a9 e
- printf("Tcp bind faiLED!\n");
& W; I: E( S x" ?6 `9 i - return -1;* T3 R( J$ ^) k% a3 v
- }: W9 d) A8 [$ g: T& P' G
- if(listen(*socket_found , 5) == -1)# s- Z% r9 t2 a3 t9 W+ [8 @
- {$ r' Z+ V6 F- L
- printf("Listen failed!\n");& i; s$ j& @7 z" \$ f( X
- return -1;' K' o% t6 _0 o/ X( m
- }5 U# S9 Z' }4 S% T2 G
- return 0;
- s5 @7 L- _% w - }
复制代码其中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);- `" _3 T7 M! M7 {
- void * Thread_TCP_Web_Recv(void *arg)
1 A7 a) q; b9 Q% Z' O q - {7 _1 d: G* e) }) [( G( ~8 i0 }, S$ V
- 。。。' M) C) A: c& D2 J2 e
- while(1)& N) U7 {5 j, ]
- {& N' N) r* g4 g) k. B% u, O5 F8 F& Q
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);0 }% J# i) P' }
- printf("fd_socket_conn = accept()\n");3 i" t7 Q4 O& t0 N( } W' h3 [
- 。。。
- [6 [, Y8 c0 y0 h - recv(fd_socket_conn , recvbuf , 1000 , 0);+ O/ b( \5 L4 K. n L4 e3 M
- }
: M: w" b) X/ |2 e - 。。。
* k' [ }; T% S3 Q8 Q; y3 Q - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);6 x5 g" j' F# W0 c
- pic_tmpbuffer = pic.tmpbuffer;* D4 R& g# ~5 ~- X* w5 |
- pic.tmpbytesused = buff.bytesused;2 c, Q$ h7 J, U3 ?# M; l
- pic_tmpbytesused = pic.tmpbytesused;; c1 h# f/ t3 O) ], k' F
- pthread_cond_broadcast(&pct);
, O I; |$ I) Q" B, n+ ^' S* j) Z - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
- N8 {# L& b" p - pthread_cond_t pct;0 _6 w* _! z& `6 r
- int main(int argc, char* argv[])
2 g1 ~/ I6 o! K' g - {
4 B! W: A' [0 ~9 Z. z/ s - ...& [. E1 _3 }1 S) g4 e
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);5 Z u. B6 n* t! i
- pthread_mutex_init(&pmt , NULL);8 h ^: z4 a/ X, ?# M( j
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
; w8 R0 k0 |* b' E" v! X; O - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
- [/ h, {2 O7 R( e% C - ...* |% Y3 S4 w5 s, @' F$ y$ N2 g
- while(1)
) P; Q1 e8 b- @- _" D0 K" J - {
+ N$ o' ~3 L" m( W - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
2 g. F5 J4 k2 Z0 r - ...
" z) m6 M9 T! e. l$ s' q - }' V: U( t, G# x+ P. e0 `4 ?
- ...
+ ^4 ?' h i7 V- X% b/ ~ - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
& J& i8 _' q4 `$ U5 ], B8 [% V5 N5 b - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
/ l( O* ], h/ ~+ M7 r6 }( t - "Server: MJPG-Streamer/0.2\r\n" \0 n ^3 S4 ~8 K' d w1 [) Q
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
$ {: E; w3 L! ?6 p9 M2 c - "Pragma: no-cache\r\n" \
7 `; W3 X9 {+ L* D' p) l - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n") E+ j- [+ F$ P8 }
- #define BOUNDARY "boundarydonotcross"
7 [, j) K5 B5 y* ^; J - printf("preparing header\n");: L( @- r. H! L2 y! i5 T2 D' {
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \8 i- J5 c e7 @5 @+ A o
- "Access-Control-Allow-Origin: *\r\n" \+ C1 n0 n$ l3 ]" y3 w; A
- STD_HEADER \( d& F$ v' Y/ n8 y6 W6 ?! z
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \' x6 R C, t( {9 v3 A
- "\r\n" \; ?) @8 {8 i# F# r
- "--" BOUNDARY "\r\n");
/ Q' v+ W: W) ~. \% E8 S3 Q - if(write(fd, buffer, strlen(buffer)) < 0)9 T! ]9 s7 m* E2 w
- {$ R) ^# ~" f8 z7 v% @# H! a
- free(frame);. K2 V! p1 ?- Q+ p7 e
- return;# a9 G$ U, |1 x( M3 K7 f
- }
复制代码发送完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" \- M2 p9 F4 e+ G! h5 J6 N7 A8 [
- "Content-Length: %d\r\n" \
2 K& E/ Z# u; {! c9 [( [8 z - "X-Timestamp: %d.%06d\r\n" \
: F z: g% ?$ n - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);) `' e+ Z. U9 @9 H! a: [% C
- printf("sending intemdiate header\n");
: [5 R( o. w7 k$ A( \: ^ p1 T! m - if(write(fd, buffer, strlen(buffer)) < 0)) I* y o' v, K& d& O
- break;
9 i; ^7 B# P* v* g$ Y - printf("sending frame\n");( Z0 L, b) R7 w3 I5 v! q* n T
- if(write(fd, frame, frame_size) < 0)/ U7 Y. ? x6 D- }1 F8 B
- break;
8 }% l, e3 |# J- h5 c" I - printf("sending boundary\n");
" l4 d* g) @9 U9 Q ~: C0 t% { - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");/ ]$ ^9 c) U9 N* ~' V' j5 `% |9 Q0 B
- if(write(fd, buffer, strlen(buffer)) < 0)
" U1 s& O9 ]4 u/ 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
- c2 y6 }# o. Q t% x; x
( G. P5 K; Q2 K1 P( c二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
: z8 Y" y2 b4 a) }0 O" b- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
, |; b$ K7 J$ [5 m- T. @ - {( p, B4 y; [1 b& t5 A% C3 ]1 X
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
2 Y$ m9 @+ o$ e$ L i; g! D - if(*socket_found == (~0)), Q2 z2 M+ _/ H
- {
& i+ z! y e4 B1 _9 s - printf("Create udp send socket failed!\n"); x3 u* } g" z
- return -1;8 d" e; n; W: {9 |- {; M
- }
3 x2 V6 M' c# U3 f* `; R - addr->sin_family = AF_INET;
& [( U9 }8 Y4 R) r - addr->sin_addr.s_addr = inet_addr(ip);! g! g+ a% D/ S' a
- addr->sin_port = htons(port); ]6 |* g! @5 n: ?/ x4 s6 q
- memset(addr->sin_zero, 0, 8);
; w9 F; q( q- m: N - return 0;
0 b0 }1 w+ [& [$ R$ L - }
复制代码
& s1 @7 n9 R8 i/ v/ e9 n0 T/ K, [5 `& z4 X! n$ B/ N
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
* j, ?1 }5 s+ l% I' h$ l( G3 W) e9 T# t& u4 {$ g
5 k. W* {' u1 P' P1 v5 j2 n' I+ ?- while(fend > 0)
3 N# ^% U$ D3 N) }$ }6 _ - {
9 d+ Z% J+ ^, y; G% i# ?$ Z# } [" s - memset(picture.data , 0 , sizeof(picture.data));
8 u' d- d' @/ o: }% w' T; e - fread(picture.data , UDP_FRAME_LEN , 1, fp);9 t) z( d8 {# {/ |8 M" f. m6 ?
- if(fend >= UDP_FRAME_LEN)
0 x- [* B5 c) v9 B8 t - {
. m4 s! z# W+ i$ U - picture.length = UDP_FRAME_LEN;( p* Z, M* F F: q2 i+ P/ r, G
- picture.fin = 0;% Q: }! ~" k; c# f
- }
$ K- l( C5 V) O$ c/ K3 f1 b - else
, |3 ^1 j: k1 e, \+ i# T$ h: |0 ` - {6 J9 P. S! l3 `) {3 Q; g0 K
- picture.length = fend;2 p! d5 |+ s) K8 g' K
- picture.fin = 1;! D- [+ {8 a, c( Y a1 K
- }
0 ]7 N$ k) L0 j3 v& D8 T0 w' d - //printf("sendbytes = %d \n",sendbytes);1 J2 s% v6 l- X6 F+ m ]
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);. ~4 ]% O' w: J
- if(sendbytes == -1)
) h4 y7 U; D5 v8 t1 b. F% g - {( a# I7 V5 y5 q# y
- printf("Send Picture Failed!d\n");/ N7 Z# |' I( K/ T2 ?0 F
- return -1; p4 O0 x9 U' U/ r- Z
- }
* I. s1 X2 b( }( J0 Q2 {7 r7 X- ^ - else
8 M Y5 y4 |7 ?3 G! F- [2 L' x - {4 }! W9 e- q5 m w! `" e$ a
- fend -= UDP_FRAME_LEN;& _2 A$ f% l! V4 B
- }$ o3 m; V; e* |: h
- }
复制代码
- X# k1 c3 x! p) H! a- D7 k ?* X& i/ @) X
 5 e, {) q, R4 K4 K$ d
7 @$ I; h: k+ L2 C) h+ j
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |