本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 ' d6 p8 o4 p+ l
, R& [. ~% n& \5 o0 y
/ P4 ?8 I( u" K, E6 ^1 O作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
4 B3 h$ S! x3 U4 Q/ B; h
5 Y) T* q2 b# j* [本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
0 t7 q: ?1 J: H9 ~% \; r# E 1 \) d% J+ d; C1 R
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 ( B, S# v2 |+ ?; N d K! T% q
一、HTTP网页服务器' w* Y$ y) ~! G6 W4 v* I
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port), A6 @/ j9 H9 Y3 S6 f
- {- M5 f9 N8 [; _- V% Q9 D* B
- struct sockaddr_in servaddr;% H& {/ Y* S2 t5 m7 G) n X: H
- socklen_t addrsize = sizeof(struct sockaddr);! M) i& V1 [9 B( T) { @, n
- bzero(&servaddr , sizeof(servaddr));; S" h: `7 M6 F1 {" k( R3 a$ s
- servaddr.sin_family = AF_INET;
$ I, [/ Q/ j% \ - servaddr.sin_addr.s_addr = inet_addr(ip);
9 G# u, g6 Q) G/ Q0 A8 w; V, R - servaddr.sin_port = htons(port);0 @; \, O! H( r" g: H4 L! H
- int ret;3 B6 u3 i+ x! f0 @" ^. T2 I, r7 L
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
+ u1 @/ k0 e5 L( F, d+ Y - {; a+ T9 p2 d& w/ r
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
) d' n/ y+ {! @1 n - return -1;& C5 h% \" E$ I9 [' V; P+ L
- }
, [0 {9 d% k. O) g6 g - int on = 1;
4 F: e: P6 R9 W - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)0 s O1 q% @. F0 a% D+ I
- {
' a% j: N: e8 Y( R3 x - printf("setsockopt error\n");/ X' [9 h! B! G# F8 w# \3 j/ m. }
- }
2 T% M8 e5 S8 v: q4 H3 g# f% j. D - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
) T, h% g- D; L3 O$ y7 g - if(ret == -1)
- M6 r* y/ h. F. B- L @% D - {
8 i& V# r( a6 ]4 `. f. T - printf("Tcp bind faiLED!\n");( \, V' H% l; G
- return -1;# J- E' X" _: c: y1 D4 X( s
- }$ L7 K% O: T0 K9 v7 |. Y7 s
- if(listen(*socket_found , 5) == -1)
: `# J/ o* X+ ?! G+ B$ Z* {, | - {
% r6 a2 {8 ~, t* G( ? - printf("Listen failed!\n");% h: G. L( W& b2 n! F* s5 F) h
- return -1;/ D# f8 J, B" U9 n/ {) T- m* [
- }
+ k9 t8 r! P' G1 K7 o; t - return 0;$ _+ Z' I; W% u7 ^ H% H
- }
复制代码其中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);! V. R: R( t h! U& w1 W( F5 |& S3 W
- void * Thread_TCP_Web_Recv(void *arg)- Y! i R0 L& ?
- {1 W% o3 z4 v, s- \1 X) L
- 。。。. _" d2 c) w# F) h7 M7 ]" V
- while(1)
% i/ V$ a& y/ i. {' Y; O- X/ w) @7 y - {9 b3 M1 O" m3 b* e( \( Q
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
. B- f+ p) R' |1 p6 `. L - printf("fd_socket_conn = accept()\n");
' ^- b" C0 J+ I; i Q9 R. }* G4 L - 。。。
& t4 W0 y: c e$ D5 M% }; Y) g - recv(fd_socket_conn , recvbuf , 1000 , 0);( Q) E* {1 m8 o* x. W
- }
" P6 K$ Y+ l* D- U5 e - 。。。' F* Q# z+ R% D
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
' B$ x- P% }$ Q - pic_tmpbuffer = pic.tmpbuffer;' Z8 ~/ `6 y' \5 e$ x
- pic.tmpbytesused = buff.bytesused;
" ^1 U5 l0 `" s8 E2 K" |9 @! q - pic_tmpbytesused = pic.tmpbytesused;
9 V: t2 l9 M6 G$ r6 S8 b - pthread_cond_broadcast(&pct);0 F6 Z% E" B7 I+ f8 u2 D( y
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;/ W, k, P# Q: R) h( l! P
- pthread_cond_t pct;& J7 E& t, L" ~' O5 L) J
- int main(int argc, char* argv[])- G, w# t' y5 B# @6 J$ l' N
- {: U9 s5 R. w7 R7 v& d! |
- ...
& }: g% O- B0 N - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);' d$ D+ ]4 m0 o( H3 z4 n6 s
- pthread_mutex_init(&pmt , NULL);) Y3 l. q% _+ u7 u5 k# E2 X
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
; H( [2 R3 s# ?) }4 B3 g - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);. ?. d# Q6 ~; [( [2 ]
- ...
3 d. L$ I5 C& x: q0 O - while(1)& o9 t o$ ]; f% W8 m6 e* r
- {0 u8 w: S; q% f0 P: E* T0 l
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
3 l. h5 T6 H* u# V( U; ? - ...; X1 n) k" h: F, k
- }/ s+ W+ V7 o' p- U" m
- ...
8 U" c; \# v, K# b% O, A9 i - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
+ ]% m2 h5 k$ ?+ H2 [; C( @ - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
$ L6 t# a3 z# H - "Server: MJPG-Streamer/0.2\r\n" \0 p, T# M1 |1 ]; X X5 I* O* ]
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
?/ W+ l8 V, u8 Z: x/ K8 E, Q - "Pragma: no-cache\r\n" \! S9 D6 B0 }- U3 ~, j; S5 k
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
$ [5 r# F2 E6 D+ |& i4 m - #define BOUNDARY "boundarydonotcross"
! y+ h) U4 d) A) v4 B - printf("preparing header\n");
" ^4 n1 r) i( s7 f Q7 A - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
5 U; O6 V o5 S$ L, u - "Access-Control-Allow-Origin: *\r\n" \* i9 P/ K+ T% Z: t, f$ R
- STD_HEADER \9 H3 e1 D5 C7 |7 U' C1 {
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
6 C- _- G, y0 j3 g0 s - "\r\n" \+ x/ i" O$ W, N( y& y, u$ d9 ^
- "--" BOUNDARY "\r\n");
' X4 k3 n. }8 ~' A7 o - if(write(fd, buffer, strlen(buffer)) < 0)! t* e$ v3 J2 r: U; ]0 \( s
- {
$ [$ M$ M& G7 T, p6 L4 [. l - free(frame);" O. x- A4 o- z6 w1 x& _
- return;
; }9 V4 ^7 r( K; A6 V u - }
复制代码发送完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" \
' o( @0 C$ R* A6 R( v; F6 d. E - "Content-Length: %d\r\n" \7 \. s, I( m5 K C T. e0 E, l
- "X-Timestamp: %d.%06d\r\n" \
7 v- n& [% i6 }7 G - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);- L. [- b$ G5 E5 B* X
- printf("sending intemdiate header\n");+ R; S# Y j! b/ J
- if(write(fd, buffer, strlen(buffer)) < 0)
. Z+ C7 Z6 Z+ h: l O8 y$ y) _4 A - break;
$ x) p; l. v+ |8 J9 z' \ - printf("sending frame\n");
5 v6 Y: L: ]/ ]/ b% j* o5 i - if(write(fd, frame, frame_size) < 0)& ^) p+ u1 J: ?6 D f7 J
- break;% X$ }* l) `) ^
- printf("sending boundary\n");5 g, G/ N ^% K& O: D- I( g
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n"); M) I) O0 r" d7 P
- if(write(fd, buffer, strlen(buffer)) < 0)4 t8 _: ?# a) h/ Q1 s' l. g$ x/ a% 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  ( q' n& h" q+ R/ K! ~6 \& L+ D
 2 Z2 c4 w3 t) D/ V% X( q& H
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
% S. U- O: @6 a. n+ c9 }- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
+ M" `; Z! g. ` - {
Z: w- _! B4 n4 L# @% Z - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);8 r! N+ C: O, t& \; s
- if(*socket_found == (~0))6 t- Y, F v( e, Y! W6 L
- {$ D1 U% m5 g$ q+ i
- printf("Create udp send socket failed!\n");% Q" _4 }6 D5 h, h% O* Q
- return -1;7 T) F7 n4 }: x
- }6 j3 R% I. D, [: ^1 S/ X
- addr->sin_family = AF_INET;
% T' T2 \1 `* u9 l - addr->sin_addr.s_addr = inet_addr(ip);( b* C5 p! Z; Y7 Y9 Q
- addr->sin_port = htons(port);* u; K9 w5 q: V) l- q% }
- memset(addr->sin_zero, 0, 8);( p- j5 t( w) P. `
- return 0;
4 ?" @9 G0 c) {. A: s5 Q; P7 q - }
复制代码
( M* g2 ^/ x# W7 K5 N ^% ~& @7 s& |/ q0 `! Q. I
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
5 p2 z _) `1 i; q9 [3 U
! g2 v# W; O- b5 V+ n! k" y) u. a
7 s H4 M; k* e; B3 c9 t* `- while(fend > 0)
9 `% {; B1 q6 i& R$ \ - {: }3 b1 }/ _" y r& D; u7 a+ c* `
- memset(picture.data , 0 , sizeof(picture.data));
X, \: @% L) j! z9 |. N! p& N - fread(picture.data , UDP_FRAME_LEN , 1, fp);
5 }3 G* T- a, n4 U5 [ - if(fend >= UDP_FRAME_LEN)9 Z4 j1 @) s( ?. ^( I
- {
+ [$ K- a. D/ L! ]1 u8 s$ O$ c7 o - picture.length = UDP_FRAME_LEN;
6 H+ U0 `0 s W+ @, I - picture.fin = 0;
0 z$ F0 n: y) u- W" ?2 Q0 g0 A ^ - }
n& f" s( S6 T) | - else
5 M+ f% T- ?- Q6 A& i - {
8 h: a6 i+ Q+ ^$ \2 y4 N - picture.length = fend;
- S0 F) F _- U- q - picture.fin = 1;& L1 y3 M( D6 d0 D
- }
1 f% s7 {4 p, N7 g - //printf("sendbytes = %d \n",sendbytes);6 A1 G: P, o$ W/ o. j. B
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);9 o, V8 e; O) D5 [, c, `
- if(sendbytes == -1)
" { U) T2 M/ n - {
0 p( r3 V9 c6 Z& s% t6 Z - printf("Send Picture Failed!d\n");! U: s! P$ j* M$ U( ^" B4 X
- return -1;
0 a+ G5 Q/ j$ h& {; S - }
$ ` Z, S+ X) t- n7 d* J* w: x) D - else
, h) ^% W- h& q5 T) ~ - {
% I% m6 X- [* ^# R1 o - fend -= UDP_FRAME_LEN;+ v" ~! T' K3 w1 G( O
- }0 _4 T a5 S. N6 `( {' P
- }
复制代码
9 }# `% x) ~- u
4 x* s! r' Z8 L' B8 D3 S
' r+ d- @$ l& t5 _# P: _- c5 A& Q1 U
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |