本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
* k* d5 e: V4 y) j
" ?# G5 G* L( _6 |& I 2 W6 Q- u5 l) ^; Q# p
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
, Z$ h: |: C0 w6 H
4 p- a! L& ?! a0 L本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 ) t" }! r2 @5 l9 D

( H- q E, n" m, {. J, v& {两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
: ~5 h/ j _/ I, W一、HTTP网页服务器4 s: O, D v% M# C
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)8 B9 u- y1 y- w( Z* G! V' r
- {9 o- n2 u/ N/ Y& A
- struct sockaddr_in servaddr;
; C& C5 Y) f& _* c3 j; D - socklen_t addrsize = sizeof(struct sockaddr);* W1 `. G% I9 |/ A' m& @
- bzero(&servaddr , sizeof(servaddr));1 \" s5 K' k, J( l: \
- servaddr.sin_family = AF_INET;
$ L9 z2 N& u! l - servaddr.sin_addr.s_addr = inet_addr(ip);0 `& c9 V% e. W1 e
- servaddr.sin_port = htons(port);7 p) E2 X% J* m* g. F& M' \
- int ret;6 L* _4 p8 j' e" f
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)$ ]! o, n- }7 v! |; P3 X% p
- {! U, H; q! y0 z7 q' M
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);4 m8 Q5 t; F/ t4 P
- return -1;
3 B# i2 P! c! e+ ], S2 T/ s - }9 v5 W- M, x+ P/ v) z }
- int on = 1;
8 @- V! s, M0 X4 X3 @' ~ - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
, y7 Y* Q- T; m1 g - {
& O9 Y1 b! P6 d9 D! J; ^. _ ^ - printf("setsockopt error\n");3 T, K) N; j3 _& Y0 Z. L' r$ n
- }
# B$ I0 N3 c) X7 F o - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);4 q* `: C: t y2 B; b
- if(ret == -1)
4 n$ x) T R. r2 H' M* m6 l3 Z/ r$ h - {+ Y$ N5 B: y/ Q s, v- O
- printf("Tcp bind faiLED!\n");& G& k( o9 x( V% Y" ]
- return -1;4 L# L7 G) ?0 B0 x7 B' D6 K
- }$ ^3 B/ b% e s: P& I4 c& [
- if(listen(*socket_found , 5) == -1)+ D8 d5 t% d9 T& j0 t/ h S2 I
- {
0 P0 I3 e2 W/ f: w7 u - printf("Listen failed!\n");
, {6 J! G# C) ?1 s - return -1;
; U1 ?6 V: j B+ L+ Z) s# Z/ n - }7 W' ~$ H' R# k! h, o. }( V; y
- return 0;/ P5 q! ^* w. h' j4 M$ Y. H" e
- }
复制代码其中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 r6 ^7 \3 ]+ l7 M6 a+ k$ n% T: F - void * Thread_TCP_Web_Recv(void *arg)/ f/ f2 k8 j9 @6 w% A! |0 R
- {9 b4 q2 h5 Q% X5 P
- 。。。- r }' N1 |/ \1 L4 ~9 v: p7 {$ ?
- while(1)
0 ^ r& }( r$ i/ j7 y, e+ p0 i- t - {
1 B- o) \/ l" q P$ ]4 B4 m - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
1 @0 i$ ]7 y5 ` - printf("fd_socket_conn = accept()\n");
% U! ]2 k5 |+ y& D |. C% o - 。。。* R2 P; a* w2 n! j F
- recv(fd_socket_conn , recvbuf , 1000 , 0);! J9 j- a9 E g `. \' y5 _# Z: V
- }* I4 Z6 m8 A% k8 X
- 。。。7 r5 u) M9 C& L# t' V3 B5 `3 x
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
4 r% L* ~3 o; D8 p6 h - pic_tmpbuffer = pic.tmpbuffer;6 T7 P' d& J3 Z, ^- W5 I, z
- pic.tmpbytesused = buff.bytesused;% @; v4 G6 W' Y6 k
- pic_tmpbytesused = pic.tmpbytesused;4 x, X8 ^/ w% w# J& }5 p/ m
- pthread_cond_broadcast(&pct);
m- K, D0 J# \! {- l& D9 r - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
! `! Q2 a* v# {7 W7 K - pthread_cond_t pct;
% Z% |0 @" C* n' Q1 u - int main(int argc, char* argv[]). v2 w: `9 z. e4 G
- {( T+ p2 F: U( d# J" G- D5 p# S
- ...3 r4 |! d( g5 I3 }+ L( q
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
7 d3 C1 I/ e6 N1 ?6 z - pthread_mutex_init(&pmt , NULL);/ I9 m& F1 q: ~; X9 s: h' J* i2 ]
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
! r- J7 M( b9 _3 C+ e) Q# } - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
6 d+ L( E. C f6 l! s+ l - ...( s" E4 w! ^) d; N5 n
- while(1)" N0 Q* H4 f1 a, Q& K
- {- Q4 n3 Z+ _( u7 l7 G, d
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);) y7 n# {9 @ R$ l
- ...# W" }, B$ b7 J" s) o, u% \' V
- }7 y4 x. l8 j( o, n
- ...( c j2 t' F, } x/ \2 u' s3 x
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
2 P E7 Y5 V( z: F; Y - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \* p' a! r' G4 x4 s' u8 ~
- "Server: MJPG-Streamer/0.2\r\n" \
, u: o5 M3 F$ K1 I5 l - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
* D: i. |% T$ X: g - "Pragma: no-cache\r\n" \
# E* |* K; L' c+ d - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
@8 n" M1 w1 n- R$ R - #define BOUNDARY "boundarydonotcross"
2 r9 O9 E; [+ `: d - printf("preparing header\n");
7 @1 s3 t$ A/ l, S! z4 U - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \; z- A- u" `6 k* {* z) W8 c. g1 B
- "Access-Control-Allow-Origin: *\r\n" \
& B1 O& U1 D, m% {/ |) L - STD_HEADER \
5 |' T. k- N6 G! Q! o4 y - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \1 R$ e; \( \& w, U1 @& a1 b
- "\r\n" \. \$ @" |: V* t" H6 c" M
- "--" BOUNDARY "\r\n");
; t, v2 O3 m+ U6 B8 D2 C - if(write(fd, buffer, strlen(buffer)) < 0)2 b- d5 H- Q& U8 R
- {, Q5 }' j8 w" |7 ]2 |5 ~
- free(frame);4 x5 c; k [: h. H& h5 q" l
- return;
& f- }7 z# E' D - }
复制代码发送完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" \) m* V7 w' N1 F* ^8 \/ t$ j- p
- "Content-Length: %d\r\n" \
Z! M' I) ]) e4 |/ K& O" E - "X-Timestamp: %d.%06d\r\n" \+ l: E: y7 {1 \+ j6 {0 o4 Z
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
' R( q3 R6 ~5 x9 C5 P. o# h - printf("sending intemdiate header\n");7 D5 S$ `# c5 h, y
- if(write(fd, buffer, strlen(buffer)) < 0)
7 u4 p! j0 M$ R5 G6 { - break;* _ k- k+ Z: _' v1 |% [
- printf("sending frame\n");/ e% n7 a1 `3 M/ I5 ]5 k" e3 v( E
- if(write(fd, frame, frame_size) < 0), H. f6 P1 h6 ^7 P$ n
- break;$ D. R! P" N' s1 Q) J% Q$ ~/ G
- printf("sending boundary\n");! F" }; d [ Q1 j3 ]- w
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
6 x$ b. Y, l0 j. T. p5 N - if(write(fd, buffer, strlen(buffer)) < 0)
1 {; T( @) d9 I - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
* A# K3 @. x. H / b m. U2 j! a; J, Y3 p# }
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
" S# z' N4 e1 p! Q B- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)# L+ U6 m, J7 s: @
- {
% g6 |: c t2 H# u% ~5 p - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
# P" C* C1 l! ^, E5 k4 L$ H4 p% G - if(*socket_found == (~0))
! E b" X1 _/ ?/ ?" ^ - {
; ?1 O8 H' i! u8 I0 W5 E - printf("Create udp send socket failed!\n"); d7 l) y& e8 l7 o, q
- return -1;
# N) N# z4 e% k! b - }4 R M) S9 R. \8 \: T
- addr->sin_family = AF_INET;
' F# a( x6 \. @. j/ e! a - addr->sin_addr.s_addr = inet_addr(ip);! `+ X( z: y6 I5 G! B
- addr->sin_port = htons(port);$ G) l1 [* w( W$ f; ]* r5 U
- memset(addr->sin_zero, 0, 8);5 w# H ]0 Q! x9 U# M/ Q
- return 0;
, a" c* D" O; t* z - }
复制代码
6 x6 F' W6 y( } s. @5 _' Z
}) t8 A) \/ _而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
+ e8 R4 a4 @4 B% ~9 m+ Q/ R
, E9 s+ C/ x3 b' D
' Q2 U+ {' c6 O* F) u- while(fend > 0)
2 y" F3 b; h; C- p6 l - {& e! V; Q/ w% _$ K& f
- memset(picture.data , 0 , sizeof(picture.data));2 p5 ?* m7 r/ P( |( a
- fread(picture.data , UDP_FRAME_LEN , 1, fp);% ? _' J" z. R# x1 i1 e4 u2 P
- if(fend >= UDP_FRAME_LEN)
& D4 B* n2 R( y2 O! \0 y - {$ o- M3 j2 G! I/ K& S4 K
- picture.length = UDP_FRAME_LEN;4 c" c, R8 a8 G
- picture.fin = 0;+ d$ N1 n5 j& s
- }
( U% h0 H* C$ @! ?# A" i* g - else
. K e$ }9 r5 m- E; ^ - { z, e( c% d+ b; C6 B; o
- picture.length = fend;" L% y+ O0 T1 b: R) m
- picture.fin = 1;
0 G) q0 [6 U( a" T5 @2 ?' c - }
% s6 |/ u S- @, e$ U. u; p' V) y/ ] - //printf("sendbytes = %d \n",sendbytes);
* F2 _" W. s, z/ c6 }8 p - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
* ^1 Y8 Q, E$ @4 N - if(sendbytes == -1)# z3 G9 n/ e2 s0 [; F% w0 l
- {) |* Y) k) j* |
- printf("Send Picture Failed!d\n");( v" u6 u% c5 Q- m8 C! Q5 }
- return -1;
" x4 s3 ~# {8 I' F" V - }! O2 m- D) u+ V: @
- else
( i* N- U8 D% m+ L; q, f9 ? - {
3 z+ D+ {7 n; h J- G$ K7 ~% [+ y - fend -= UDP_FRAME_LEN;. J I" h6 ~* Z: c* b
- }2 W0 F7 D" z" e$ G2 A1 g
- }
复制代码
; w6 m$ `( m! k9 Q6 _" p; ]4 ^( S, }* s8 n/ i; u6 a9 d3 ]
 8 B9 w$ C* G2 v# o# a2 j
, @ X( N: A0 N. W: Q& ]. giMX8MPlus 核心板: https://www.forlinx.com/product/136.html |