本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 8 ]2 L [0 d3 u
! J) _6 m% _0 B* T% W& \

t+ w" S8 O/ N* k- J# n+ }作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
t3 ^4 z* p( E: Y0 u% R: F; |" \4 c: H/ t1 s* C
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 * W5 Y0 x7 V2 K3 o5 H1 w6 ?1 s

( Y( u. R. t0 p9 ^" c2 L两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 , W! \% l l; K, B# K, d3 ?
一、HTTP网页服务器/ }$ J7 } d' T7 l, ]$ }: g: \
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
* d- s2 ?6 h) @2 X" u" G) } - {+ f. B# o6 a" q# b
- struct sockaddr_in servaddr;
4 l8 F+ K$ N5 h, J/ z9 U% X - socklen_t addrsize = sizeof(struct sockaddr);
; w/ `) ^( d/ U; F8 b* E6 A2 L - bzero(&servaddr , sizeof(servaddr));+ K0 [$ o$ J8 A2 i
- servaddr.sin_family = AF_INET;2 o2 s( ?! a, d5 c- m0 J8 R
- servaddr.sin_addr.s_addr = inet_addr(ip);7 g3 {0 Z# x1 B; o8 ]
- servaddr.sin_port = htons(port);" a @4 l$ |3 ~, o. g4 R, k% e9 z9 |, a
- int ret;( Q$ [& r5 A- j( b: t2 h$ f
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
3 {' N6 |+ N% _3 ~8 G - {: {/ c) N2 P8 N1 h7 u
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
" s( c ]. L% O9 P% o" H" g - return -1;
4 Z# S0 n; b& P+ [, m, `+ U - }
5 t6 u' _/ B' i1 {6 T% { - int on = 1;
; c! }4 x, o& B, `) N E; H - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)! v" m9 R% Q& Q# `
- {
- a. f0 b* O4 G# s' N - printf("setsockopt error\n"); W5 n" u3 J y' r: W
- }
; T' i& b, o! R% ^ - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
2 {/ u+ x: U u% P2 H+ z - if(ret == -1)' |5 {7 P1 m' k9 K% h' I) T
- {
; D6 \- x! i7 S6 k - printf("Tcp bind faiLED!\n");
% v1 a: R' x8 _ - return -1;
. C3 x) z. J7 L u - }
1 x- C E# t% s6 @' |/ `3 q - if(listen(*socket_found , 5) == -1)% h0 f( L/ t: \* z
- {
- G9 g+ t! N4 Z, r3 u1 M - printf("Listen failed!\n");& A% E) u$ R1 {' T2 L1 d
- return -1;0 P8 ]8 |" z O% Q
- }% `' d; m7 K7 {
- return 0;, ~2 Z' A; p9 X) 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);8 ?- \' e9 F8 o& O" h- q& y6 a
- void * Thread_TCP_Web_Recv(void *arg)
3 \6 ]9 f( F) v: w. A7 I+ y5 c2 u - {
( J0 `; a# D8 J* M* ^8 J - 。。。
5 k, L4 q7 N* e/ m7 R: ~0 W2 ^ - while(1)
& t3 f7 D& \& B4 S4 X - {
. L# ]. k; m2 s - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);7 f+ T$ `7 Q$ n# G+ a+ K
- printf("fd_socket_conn = accept()\n");
4 X Y6 Y- l0 X - 。。。
2 Q' V* r, j5 c6 x - recv(fd_socket_conn , recvbuf , 1000 , 0);
6 N+ x5 ]1 U P! w. ]# _ - }
. b1 B9 ^, D* x3 N2 V4 q* p - 。。。+ O# y8 R5 m) S- Z% e e
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);- T, r2 Z( }$ M
- pic_tmpbuffer = pic.tmpbuffer;4 k0 C! p: t; }. E1 Y: ^# P+ d% c8 j
- pic.tmpbytesused = buff.bytesused;* C7 R+ f6 r4 ^" F1 h% R
- pic_tmpbytesused = pic.tmpbytesused;
+ v, a' `0 c. G. Z - pthread_cond_broadcast(&pct);6 l0 q. U& ?1 J- ?6 X& e: j
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;5 i$ f, X! _, B! H
- pthread_cond_t pct;( N6 M5 X- P T7 l9 I
- int main(int argc, char* argv[])) V9 v: n- ~) e6 c7 D: b
- {+ L$ H# r, _6 N4 m6 f) x
- .../ g V. t1 I. ~! F
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
: k @. a! h$ Z9 f - pthread_mutex_init(&pmt , NULL);
7 S, o' }/ U6 O( \ - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
( U- ^* u2 A6 d8 d |$ n - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
( F" X+ F: ?2 @+ Y3 A7 n2 [4 h - ...
2 @5 s; B5 w |0 S. e - while(1) H9 P. C7 S/ Y
- {1 J: }& [; r+ Z" h) n- d/ B
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
( j. G" x) F& b$ p& E4 C3 ` - ...
! P: w" A# h) B4 n - }+ K( r) z# U# r( k
- ...
! ~) {& R+ j% }& g7 }6 S) K% Y5 a - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
( {/ h. e# N/ o1 @- c# ^# ]! G2 D - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
8 x+ i0 z0 X% F7 [" {& k. ^; F - "Server: MJPG-Streamer/0.2\r\n" \
3 @0 d* {, O! _, L `% T% f( V - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
5 `3 E8 T& G" y - "Pragma: no-cache\r\n" \9 H$ d- E$ j8 I8 c( z$ o: w* z+ N
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"& c m6 ]* ^2 M1 m- z. _( p8 V
- #define BOUNDARY "boundarydonotcross"- Y9 c6 [; A' }# ~
- printf("preparing header\n");2 D+ {* {/ X5 j" K: n; y
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \3 C- e2 P" N9 Y# l ?
- "Access-Control-Allow-Origin: *\r\n" \
. k# P. X, ]& {5 \0 H* q$ k6 J - STD_HEADER \( x) {1 d% U2 M- V$ O4 }
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
) d1 f! Q; @' ]! C1 v - "\r\n" \
6 E* e1 T4 }6 z- q; [9 q7 \: ? - "--" BOUNDARY "\r\n");+ g% N7 N' x# ^
- if(write(fd, buffer, strlen(buffer)) < 0)9 _2 h7 O) [: p- S1 O$ \
- {6 d/ d! I# ^( Q
- free(frame);: H) w' Q: u3 V8 G7 A, n& l# q! g
- return;
! ^9 f" P& D; N% m - }
复制代码发送完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" \3 w! C5 u/ y5 }# f, u8 k+ o
- "Content-Length: %d\r\n" \
6 ^# u L0 i$ m! C - "X-Timestamp: %d.%06d\r\n" \. m3 U e: t% e8 P$ O4 y* P
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
7 v1 C/ L* M/ _# s( {9 b6 l+ f: T# f; j - printf("sending intemdiate header\n");
. Z$ X% ?0 e y0 P6 \ - if(write(fd, buffer, strlen(buffer)) < 0)
* X, i7 e1 T0 E. x3 y6 R! d - break;; z7 k# P8 l& {3 u/ s. t
- printf("sending frame\n");# |# V( R( A3 S* R8 j( n
- if(write(fd, frame, frame_size) < 0)
2 m7 r& [4 P) Q5 |" } - break;
2 G$ k- a( X2 k& b; v% l! G - printf("sending boundary\n");, G4 S8 L+ A8 @" X
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
6 R8 ~ X* n4 J - if(write(fd, buffer, strlen(buffer)) < 0)
+ E3 ^" S8 p. m) L( F - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  $ J |$ Q5 A3 ^% v( j& ?

9 ]9 w1 p1 z6 P5 z! O% G+ t二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
) V" I: `+ Z6 K1 W# w2 t4 Z! A- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
* O8 n1 o# c9 ? - {) j0 ]6 V3 s. W6 W# I+ _$ A
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);% f+ D; ^+ l9 M; s7 N
- if(*socket_found == (~0))
- @- D& p+ V5 Y - {5 i5 a# f* X( [9 g0 w c" `. N
- printf("Create udp send socket failed!\n");
m$ I" B" |: }' U9 E4 B - return -1;* {# @% T1 ^+ l. Z
- }
& |+ B% S0 A) Y2 m( h( V - addr->sin_family = AF_INET;) j6 _6 U& v1 A" n& `, w3 \3 b% ]' x
- addr->sin_addr.s_addr = inet_addr(ip);
# p8 U3 q% }! y) O - addr->sin_port = htons(port);1 V) P* a) ~1 H% w# M
- memset(addr->sin_zero, 0, 8);# F. U% X. Q& ]4 y# Y0 b3 v
- return 0;6 _% w% |, G; O0 K
- }
复制代码 # _) a1 C6 U/ r& k
, F1 W' e# v2 ^3 z7 M. w* d8 G而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:$ a8 h! p: N# d7 z* H
- J; |' c5 N( o8 v o9 F
- e' |% S- ?/ d
- while(fend > 0)2 p5 M: _; ` f6 S
- {7 ^4 o" N. N( g1 O1 e
- memset(picture.data , 0 , sizeof(picture.data));
4 d2 `3 J8 B- D3 E! X/ E$ w0 } - fread(picture.data , UDP_FRAME_LEN , 1, fp);
) x6 `$ I/ w4 ?( B- e4 v - if(fend >= UDP_FRAME_LEN)* E) T3 T" e4 J7 `2 `" L
- {$ Q+ G5 u: x _: B1 j
- picture.length = UDP_FRAME_LEN;' P2 K0 @' N2 W# z$ j. b
- picture.fin = 0;, \; H8 O, s1 j3 k* r
- }
( i, d* x& |2 b - else
6 d/ e, u6 R' V- @( u' t0 _, @ - {8 R8 c; U* A, M3 I( A1 d
- picture.length = fend;2 o% s) J" ? U3 }0 y' S
- picture.fin = 1;, n$ o+ N6 V- ^- V! ^! I) d
- }1 Y$ x' s0 ^# I
- //printf("sendbytes = %d \n",sendbytes);4 ]: F p' Q- _' t2 ]% X! W
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);6 [2 s7 ^: ?/ }3 |# n6 I, p8 a( O
- if(sendbytes == -1)3 r: S- y" I6 f$ r8 Z
- {1 E% t& S3 M$ ~3 m; X
- printf("Send Picture Failed!d\n");
4 m- c: j! m! D5 y' T - return -1;
3 p! m/ ~6 d; F - }
. n; u9 O! a4 w& m, ?5 T( c6 v* I - else8 @& r2 O, J# ]- G. _
- {
( u( }; i* D7 V - fend -= UDP_FRAME_LEN;, x/ Y" s4 R, }, q
- }
& H7 y; r& Y2 B' n6 Y; m0 \* g7 r - }
复制代码
# c ^9 _( _6 v- ]3 {8 F9 |
- h" }4 Q! v% C' l0 ? 2 i1 ^. Y7 C! f
! U) `1 V3 }, [% h$ W, `1 ViMX8MPlus 核心板: https://www.forlinx.com/product/136.html |