本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
4 W6 n5 S6 E" S0 Q" b/ ^, I' P* _. o7 g* o
 , s/ m) S0 i% x* k9 S
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html0 i2 Y% j! F: X2 m" |3 `" i
( k6 ~" G/ g' D本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 6 p& Q" O/ u- n

. \4 \ y7 E! M* |5 A8 {两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 2 b9 f& r. w+ M, B/ Q
一、HTTP网页服务器
& [& c% B! d0 |: J. ]先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)& h3 m- N5 a* u: B% p
- {
5 t4 x3 }1 O5 ]( J - struct sockaddr_in servaddr;1 e& g9 C2 A7 U( m9 [6 ^( I
- socklen_t addrsize = sizeof(struct sockaddr);
# v K2 r/ X' o' [, o- s9 j - bzero(&servaddr , sizeof(servaddr));1 F+ N% v4 D1 B7 N
- servaddr.sin_family = AF_INET;
# j2 g1 }0 @3 s6 z; M/ |1 } - servaddr.sin_addr.s_addr = inet_addr(ip);
6 D L4 H3 M' V8 \/ o - servaddr.sin_port = htons(port);9 S/ P8 Y9 @- L7 j) M% s* S, J, \
- int ret;
- U9 e2 A: B: t& k - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
! A' F, X8 g! u- O5 w - {
) x% H( q7 t" S6 a - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
3 ]8 Y ~. q" l" ~" V; g7 k - return -1;9 |: l$ _: x; U% V
- }
" a2 ]9 `! }, \; @/ P2 ^ - int on = 1;
0 A. W: Z4 _( u6 p1 J. I! v' E' z1 C3 s - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
, U1 s7 Y2 {, I+ k2 d2 Y: B - {3 p# i! ]; A+ {0 R
- printf("setsockopt error\n");
: } ]! S0 x6 x/ _4 U7 w6 I( _ - }
1 w9 P4 y* V) `$ H/ Z" b+ F - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);! n* u4 o# Q: R+ a7 b
- if(ret == -1)+ I4 m. {& N4 W C# |: r
- {
/ i6 U- U6 e& C4 l - printf("Tcp bind faiLED!\n");
5 H. f8 w. M9 Z/ j - return -1;" a! J+ ?) k! D h' l
- }# S. D* Z0 |8 C3 u. w$ [
- if(listen(*socket_found , 5) == -1)
- O% d3 D# N l; j! B! Z - {
7 C) k5 R+ o5 I9 Q0 J f - printf("Listen failed!\n");
% v# n1 b- R6 ^/ Z+ u! a1 c - return -1;$ t( j( O9 R1 P, J
- }
3 r# U3 B& r4 N/ [1 |" E H9 A - return 0;/ i, b6 j! H. U9 [! b" A( S2 ]
- }
复制代码其中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);9 k- @! F' d K5 p: s- \, ^
- void * Thread_TCP_Web_Recv(void *arg)% f7 N* L9 V% c5 {
- {
% }$ m5 i! s9 C' l; m9 h5 V - 。。。
# `0 h% x( L( q - while(1)
( s6 E3 j: @" x - {
! m9 P! v U+ X7 c8 r/ b7 O - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
& ]* n" \" F4 ? - printf("fd_socket_conn = accept()\n");' D- c6 p$ t- _7 ?: k# N
- 。。。6 n/ ~' T6 o7 K! f
- recv(fd_socket_conn , recvbuf , 1000 , 0);
# V; D/ e9 y7 C/ Z0 ~" e - }2 x' I1 ~2 Q: `$ p
- 。。。
* n: x0 z. k$ s0 d3 L) |- v - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);) x, ]/ ~9 k2 c1 _
- pic_tmpbuffer = pic.tmpbuffer;
w" w0 x! v' B5 w+ K: r0 h1 v - pic.tmpbytesused = buff.bytesused;" i( X" r5 V9 Q! g" q0 d' F
- pic_tmpbytesused = pic.tmpbytesused;
; _, A7 V1 I; K2 s5 R9 W - pthread_cond_broadcast(&pct);
+ n2 M7 U( O: N& \8 @$ W+ U - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;6 l4 |/ E2 |1 M9 ?# L) z1 T
- pthread_cond_t pct;
, e: ` }' e/ p - int main(int argc, char* argv[])
! x5 X( `5 X$ t$ ] - {
) Q! O1 y8 V& h1 O - ...
- i: I C4 o9 y' B P - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
/ H6 g% U$ D. Y! W. O - pthread_mutex_init(&pmt , NULL);+ s$ ~$ e- A) {' D8 I, ?, H ^& f' B
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);" _) m& ^( k- e3 ^( P
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
8 D, x9 l* I' \$ w, Y& M - ...
5 x" a; P/ V5 M a - while(1)& o! ~1 I' n! h) [8 G
- {
9 c" N3 o* a2 c7 i" h6 |1 K - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);: d9 X8 k: j1 e: r
- ...: N+ Z% p' R1 c* B
- }1 W' g1 Q: W* h+ U' g
- ...; I5 b$ Y0 L% P4 P) e6 p% K: J5 R
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
: \+ {# m2 N6 U. N. u - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
3 c# H2 j8 O( Q4 z' _6 T; G- D - "Server: MJPG-Streamer/0.2\r\n" \+ A% w) j7 u) q0 G Z$ U- z/ d
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \9 Q) t# v5 w! E& h9 d5 _: b9 D- C
- "Pragma: no-cache\r\n" \: w- k( K1 A8 A( ?# x( k
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
% D- ]. G5 O# ] - #define BOUNDARY "boundarydonotcross"( z7 z5 M6 a* y. {+ X4 e8 X7 g
- printf("preparing header\n");
1 k% ?% k& B* u' A: [ - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
' ~ k; P) V: W' u3 h3 I+ P - "Access-Control-Allow-Origin: *\r\n" \: \3 e2 `: V5 S/ S
- STD_HEADER \
9 D3 ^) i5 |5 m+ }6 C; Y/ v5 a - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \: o! G. ^* q& o( {" t8 P' z1 v& x
- "\r\n" \, R0 X, v! N$ q5 F5 d! v
- "--" BOUNDARY "\r\n");
. O9 p5 @" ^% U( L/ C - if(write(fd, buffer, strlen(buffer)) < 0)& R( }9 n. z5 F" ^4 |
- {
! a0 d: y% i M7 N6 [, E7 k - free(frame);. s8 s- W4 R9 T0 p' y- {2 s, k
- return;) |9 u# ] L A) a
- }
复制代码发送完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" \/ S6 H' |* j6 A
- "Content-Length: %d\r\n" \/ W2 h0 f) Y% ?! n8 k, D
- "X-Timestamp: %d.%06d\r\n" \
" d9 m% T, y3 H9 E% J, E - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);% P& [3 K( H2 q, B
- printf("sending intemdiate header\n");
* S# K% N* P+ P/ g& F, _% a - if(write(fd, buffer, strlen(buffer)) < 0)( ~* p5 {: T! w4 G/ b9 u" D
- break;" v) @+ Y$ X; |9 W) `7 z- M6 x
- printf("sending frame\n");
) R( v8 q5 ^( m2 i. y4 u: Z) ] - if(write(fd, frame, frame_size) < 0)' q0 H3 }" t. h) I( t7 X1 j
- break;6 Y$ f6 w8 C: n: y
- printf("sending boundary\n");# y- A5 D+ w4 f; Z( C1 r* B
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");; p# \0 H+ d6 P$ o2 O
- if(write(fd, buffer, strlen(buffer)) < 0)0 v- T4 F1 _& d) Z' X. Y
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
/ c: l$ q6 {* b& Y) y# J
4 J- v* `( p0 {) Q) I2 e. y二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:" _4 Y6 G+ P0 K
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
! o( t F. C& W( w6 [/ P, j - {8 ?) \: ~3 x! H1 r
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
W- P6 U4 J% n1 G% U8 o - if(*socket_found == (~0))
" _: z1 n2 J% V6 ?0 r! y - {
) b, U& L0 Z: Y+ E' v, h$ K8 ` - printf("Create udp send socket failed!\n");0 u* L3 L- q# x1 S$ N5 ]5 ?9 e; J
- return -1;9 ] |& b2 X5 h$ J+ \" a
- }+ R$ Q) W' i5 k$ P+ i* V
- addr->sin_family = AF_INET;
' P) h" S/ I- e" Y0 }0 |) M! O - addr->sin_addr.s_addr = inet_addr(ip);
8 D' {- c1 ^5 @- T( i - addr->sin_port = htons(port);
* N+ s/ F' q9 R - memset(addr->sin_zero, 0, 8);
& [; Z' k4 u( y. @ - return 0;5 ^% V2 d) h5 _2 J W( h, B
- }
复制代码 ) l* b& X) {! u! O3 O
$ K" `* {; `' ]1 C7 D. R而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
! K9 d2 n; K& f) ]. s! S* p6 n! V7 z
7 N% A& p. K( c- while(fend > 0)
. W# Q, }* u9 Z9 `% [1 Q - {( M. H1 s1 K5 D- u2 d( z- Z. P
- memset(picture.data , 0 , sizeof(picture.data));
9 k: h6 Y1 J6 c* u) z0 W - fread(picture.data , UDP_FRAME_LEN , 1, fp);& `; M' f' W3 [# |
- if(fend >= UDP_FRAME_LEN): h7 \( ?7 j7 Z, j1 m" f( b
- {
/ B0 l6 m+ K+ W/ \$ r - picture.length = UDP_FRAME_LEN;
- w6 ~3 s+ ~& q1 y - picture.fin = 0;- t! U) t( @) n% f- q
- }
' d# X3 P. s9 s$ W# m8 { - else1 e4 @' L" q% o
- {
1 X# r8 e m' R* M4 J0 }) I - picture.length = fend;8 k. q) t2 l* D% E& A
- picture.fin = 1;
0 a) {2 S/ c0 d7 b - }/ U1 ~; b4 T# ?: r- d
- //printf("sendbytes = %d \n",sendbytes);1 T( J/ b# @& A5 h3 n7 Y
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);. O/ z4 k& D. F
- if(sendbytes == -1)
: Z, D7 P' R5 S- [( a2 B! ^$ S4 i# k - {
1 r, ]# ^: ?. a; @- m o, v - printf("Send Picture Failed!d\n");2 J0 G. P: x9 J6 i2 h8 e
- return -1;
& T3 R4 @2 f$ R; Y; Y& P - }
) d h3 j( [8 g, J% o/ |& L* D - else
6 X# J" D2 |3 u9 ~0 C - {$ n" K. {( Z3 B" Q& Y3 Y" s( _
- fend -= UDP_FRAME_LEN;
q# N( B5 a8 p$ Y) J' n - }0 S9 U5 W- c' j6 e
- }
复制代码 4 X# J: u4 O2 P: A, H2 w5 t$ d d
& N0 m! E, G) L) f' e' r

: j' E: e& G3 Y, E+ T! l$ ~9 J5 N' B4 s; }! r# A( _
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |