本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 & }6 h3 W( g4 g$ D" A$ c$ B
0 \3 S2 O) x+ r6 @+ [9 b% @: z $ V- o0 C! s0 U* I! \+ r6 e8 f0 v
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html# m' U Q" e+ i, J0 r- \' e
4 `7 n1 N- L0 V8 e: `本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 # z! S" L, a/ q

2 b9 _% w+ U. A" }; z2 D两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
7 z1 v6 @, \0 C, |+ `一、HTTP网页服务器
. o$ {1 J* \ C$ T0 C7 M6 ^7 U先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)8 i+ D. \# k* r R5 k
- {0 x5 |$ ^% \5 U
- struct sockaddr_in servaddr;3 V$ t9 W! X& g7 Z( U
- socklen_t addrsize = sizeof(struct sockaddr);
# Z" S7 y# |" T, I/ i - bzero(&servaddr , sizeof(servaddr));
8 K2 o* Y' l" m Q0 p. |3 ` - servaddr.sin_family = AF_INET;
( _" [( O" M* |0 {3 E, m3 u" h - servaddr.sin_addr.s_addr = inet_addr(ip);
Q; C' d: a' Z, Y - servaddr.sin_port = htons(port);, O/ C( Y3 D, A& K
- int ret;
. J" [5 R, d" J& m8 L6 _0 b - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
( @1 A7 m9 z/ U# C# i - {
0 w* c) l) r4 K" C0 m* P. n3 W - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
& [3 v$ N4 T7 N _ - return -1;
9 E5 f+ K- O8 u1 v$ W; v - }+ p6 h& Z! c+ h3 U, K
- int on = 1;
: {+ I$ N _6 C - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)- ~3 _0 Z4 ?1 w( G/ ?
- {
; l! z# Y0 K6 Y - printf("setsockopt error\n");
; P2 l L7 C0 |0 N - }
- g5 M" r4 A* I1 E# A - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);' `& G" B' r! f- v6 b
- if(ret == -1)
+ F( p( {% _ O" @) D9 W$ q - {3 @( h+ x8 p/ s, _7 s
- printf("Tcp bind faiLED!\n");" C8 |# p& r8 E2 z; A+ B" D
- return -1;4 C( l8 {. \; Z4 I, o1 S
- }& a9 e. B% `0 L; p- u" s
- if(listen(*socket_found , 5) == -1)" i- ~+ S8 @( [0 d' K
- { F" O$ }) F+ C4 [( e: J$ L m
- printf("Listen failed!\n");
) }5 t8 J" F6 x, U# p C% C9 P6 x - return -1;
" r4 {. A4 k9 E' U! F# Y8 p - }0 }! H9 \* K6 ]
- return 0;
( b/ x2 P$ I7 P# ~$ [- p; ]. u& W! d - }
复制代码其中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 Z; m2 n( }6 G, f( @) {0 |# u
- void * Thread_TCP_Web_Recv(void *arg)' [% c7 A3 N/ ~' x
- {, ^3 ^; |2 t+ e9 p1 X
- 。。。
1 g0 o1 t! R2 F8 t/ G5 V0 m - while(1); d3 q, o$ M" ~* [1 R1 L/ N6 E. f
- {5 u" }- ?+ z& l5 J* W, y
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);' L, l+ w/ c( m& v1 d
- printf("fd_socket_conn = accept()\n");" M i! ~- d- x! t) L% k, q4 M1 C: Z
- 。。。
$ X i& B% w* k+ Y2 i - recv(fd_socket_conn , recvbuf , 1000 , 0);
# \4 L( @, v4 B1 Q5 D - }
5 S5 D/ m% Q, a* C. g% r - 。。。
% w0 N* g; V$ Q, \. K1 f! ^% I - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
: I0 X& J- u4 {/ F+ U3 ` - pic_tmpbuffer = pic.tmpbuffer;! b& H! \7 U' M4 T" z0 O
- pic.tmpbytesused = buff.bytesused;
2 ^# { C" I+ Y/ ^5 H+ y - pic_tmpbytesused = pic.tmpbytesused;" E3 I/ |" H7 |8 T
- pthread_cond_broadcast(&pct);
* j( z, n) u+ \, r& F" P - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
7 r j& N$ ]: U# c9 ?, x# T - pthread_cond_t pct;8 D8 l+ I1 P4 D" _
- int main(int argc, char* argv[]): `- _6 m- O( E, m! T* O# g6 W+ x
- {9 e4 W) ~ `3 F( H: E! g
- ...
* _& ?2 k( b, Q! m5 y1 y - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
& x/ ^+ G8 V9 }& Z - pthread_mutex_init(&pmt , NULL);
# r7 \! s6 H9 ?* T7 I - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
# g. l. K T( ~) }7 I - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);3 z/ C- h' r* j/ V l
- ...) h: X; T) t( F. V$ p
- while(1)
& S8 W- C2 \" D; d& r/ t - {3 `- O. Q& P' q' B% p* K n
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
0 ^6 a( s a9 ^* h; ]& ` O" O' O - ...
: h% p! d0 ~8 z- s& H: s- x# y" X- j - }
! a8 y$ W9 u+ _3 K* ~/ K* `- X - ...3 L$ d/ b8 ]* q/ S, l) }- j/ y
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
; u, v; m8 Q' G5 z# ~9 N - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \6 n1 \+ [- }! r' b
- "Server: MJPG-Streamer/0.2\r\n" \) t8 o2 x X; J9 e6 F
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
5 j) ]. C. I2 }3 o1 |1 ]& X - "Pragma: no-cache\r\n" \9 C* X, h" M3 v; s. v4 I
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
2 B% u* x( p. O& A - #define BOUNDARY "boundarydonotcross"& e, P3 o2 X# @7 R# t4 R
- printf("preparing header\n");+ `/ L+ z( u; b- j4 R
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \2 z+ H6 b# V/ |
- "Access-Control-Allow-Origin: *\r\n" \
, L+ \0 A4 v9 L( K0 Q - STD_HEADER \' g# c7 p+ B' e7 S/ w( o1 F) J
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \0 j: _( E( D1 O' D" F. j/ }
- "\r\n" \2 V" Q+ P; C7 l, X. A: z$ |0 r
- "--" BOUNDARY "\r\n");
m3 f/ U: `3 P7 r) z9 O" P - if(write(fd, buffer, strlen(buffer)) < 0)
6 H; K* Y$ _! h. P1 G1 | - {
; c6 c: w3 [ g5 { - free(frame);
4 a% Y' u4 B) f3 s - return;/ C# E6 y. k7 b: g2 C5 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" \/ ~# \9 T, B1 `- s: F
- "Content-Length: %d\r\n" \
" k. P: W7 y$ b: W. H) c5 h7 m - "X-Timestamp: %d.%06d\r\n" \) d* F: m. R( t, N
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec); h! [" `5 [- ~4 ]) ^& r
- printf("sending intemdiate header\n");
3 N. G* }$ F& b4 C" ? - if(write(fd, buffer, strlen(buffer)) < 0)
r. Y3 k- r/ e4 E8 v - break;3 A0 c( Q( q7 F* |9 O
- printf("sending frame\n");
+ J' f' Y1 V0 r( y" M- u1 \; z - if(write(fd, frame, frame_size) < 0)! K( _. f* D) P( ~
- break;/ o' a. Z" w: E' E( R+ w
- printf("sending boundary\n");. ~( W- n, i: s6 R
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");7 V) e) ]5 Y5 k% G4 N1 {
- if(write(fd, buffer, strlen(buffer)) < 0) P: ~; p7 t3 C' D( \* \/ N/ n: q
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  ) h0 m% } B( e

6 }4 p4 D" w$ X) V1 R: _二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
/ o# Q( b$ F1 W2 A _ J: ?- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
4 z; {' Z0 H+ b+ E- _5 z+ ^ - {/ B$ d- y& K' s) |$ h
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);* @3 S& \( _" C* B3 z( N
- if(*socket_found == (~0))/ X, B7 Z7 h! h8 [( q. R" ]
- {
e" g3 x4 l: i - printf("Create udp send socket failed!\n");
% M, j) q# d+ n1 u5 V8 t9 W8 q - return -1;
1 t. X2 L0 W5 E4 R4 v7 W& Q+ j - }9 X% `8 a: G6 ~8 U F" K9 _, I
- addr->sin_family = AF_INET;* z5 j( t4 O& @, C8 V, m
- addr->sin_addr.s_addr = inet_addr(ip);
. Z+ b) j8 y5 a1 N - addr->sin_port = htons(port);
0 c8 L* Q- `- x - memset(addr->sin_zero, 0, 8);
/ {- }& o& s. ? - return 0;2 c2 h J9 T z8 v' a, Y
- }
复制代码 2 K' Z) R, }/ }; S( c
3 Z8 q# [! z: R' F而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:: m" W- p; {5 q1 R/ W7 Y
7 D$ v8 `) A3 q5 `
/ k8 \5 F2 z# u: H- while(fend > 0)3 _) |; q1 i) X: \
- {% l v' f9 h* C8 }
- memset(picture.data , 0 , sizeof(picture.data));
. j8 I, D) w" g q8 Q# P( I2 q: Z+ e - fread(picture.data , UDP_FRAME_LEN , 1, fp);
' d' H- F8 j0 I, N4 Q - if(fend >= UDP_FRAME_LEN)
; C* F. G5 A! m* _( K4 Q - {) X: o3 x( N7 i' l5 ?
- picture.length = UDP_FRAME_LEN;
& B3 R0 P; j5 W4 @ - picture.fin = 0;
# e$ S1 S o1 b G; T - }. {* y+ o6 [7 x+ ^$ p9 A% W& H
- else
, {8 j/ E" Z# I& T' O7 \, i( I - {
4 A! N4 t2 V5 |1 y- x5 b I6 ~ - picture.length = fend;
; K9 l6 k& Y1 r% } - picture.fin = 1;
* R3 @5 M9 p" f' L% l - }
; Q9 K$ k$ J( @3 u9 R - //printf("sendbytes = %d \n",sendbytes);
4 X8 @8 H+ a! @7 H9 T - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
! x: P0 Z8 P+ D! C) R: O; M7 f - if(sendbytes == -1)/ U! j5 B; p5 g# |* K+ T
- {
' c! ~% ^8 A. i1 _& z5 \8 ? - printf("Send Picture Failed!d\n");" d2 I! X! E% W9 _
- return -1;
; W' P% y: I5 z' [. A - }. \1 A& \$ J$ B6 m' {! P
- else
5 C, ]) u- f6 @. _4 ~7 q5 g& X k( J2 X% d - {
% A$ w$ y( i" l2 e) Q' P v - fend -= UDP_FRAME_LEN;
: F. d5 X8 K6 S - }
. J5 ?2 D& h% g2 g - }
复制代码 % n" [2 R7 f5 A9 k, N
) f* Q5 k! V0 }8 ^, p

0 K, d; Z( W' X" f4 {9 K* |" Q# L. k0 e1 U5 f3 m# j
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |