本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
% }: h3 Y/ S! G, C2 w% b; q. U) ]1 L
' D& g( V* N; |% P+ x( S5 S
" f/ L: p7 B6 \: o7 v' v; ~$ x作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
* v# B: h) Q- E7 V% I' v" s7 S7 Y
( o' o g2 A7 r7 v! c# D本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
, e# {, X# X6 J ' U# U, `9 m! ?# k) d
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
/ P# \7 K% H) @0 |& \# \一、HTTP网页服务器
( x0 ~5 e9 C4 N1 E( s3 t先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)8 G# @6 W1 M4 |% h
- {
4 `* ~1 h( r* k$ X9 t - struct sockaddr_in servaddr;- R: j5 `4 w# t6 _
- socklen_t addrsize = sizeof(struct sockaddr);
# m* ^# W) E- r$ e - bzero(&servaddr , sizeof(servaddr));7 ~ a- r" l* }2 s+ a9 H9 D7 x
- servaddr.sin_family = AF_INET;9 J3 |4 y4 ]* c
- servaddr.sin_addr.s_addr = inet_addr(ip);
* o! M! |& b8 Z: t4 x - servaddr.sin_port = htons(port);' p' e! G" E. H% y* b1 I5 D/ w
- int ret;
% n8 ]) _8 _, \1 I - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)! _( B# _2 X- m4 v: z# v
- {
9 q" H, G/ O3 ? o S4 m/ w* e4 I - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
1 v8 W1 }$ F( w# F - return -1;& }, r/ [. J# g% E. u& y% X) V
- }
( r7 E+ V/ d: s. Q - int on = 1;
1 w5 ]; F4 F2 G% I1 | - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0), v6 Q' }/ N" Y/ E' \7 |( q
- {
. ?2 A: t( Y! ?0 N' \) G - printf("setsockopt error\n");
( ^0 I( ^# P0 }' o! G# i1 @ - }
$ H- u2 D, w8 m5 U) T - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
* }( O* ]# x9 a! w - if(ret == -1)
, ?$ s1 ^! @) X5 s! \/ Q - {- v3 m2 }# p2 ]! S3 L2 m
- printf("Tcp bind faiLED!\n");3 `2 m! D7 `' c5 R* \
- return -1;
v$ d0 Z. d) P6 h* m- _; | - }
- j& W- g5 R# Z9 y0 |( f - if(listen(*socket_found , 5) == -1)! w& m; p4 M2 G* o9 T9 i! U
- {5 Y6 A! F4 C# p% n$ D
- printf("Listen failed!\n");
1 g8 y. s; f4 A% s- E9 c! F+ q - return -1;
9 Z0 N# o9 I0 n8 A: ` - }6 r0 L' \4 r$ H
- return 0;
/ u2 j; X! W; m6 ?. S) 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);
4 i6 M+ k7 d% q3 o# L+ _ x - void * Thread_TCP_Web_Recv(void *arg)
! }9 V8 E! n. H: c3 ^, U - {
1 @ d5 {$ o+ s, L! @- \$ e - 。。。' g' G; [% q$ f3 M
- while(1)
$ o1 h4 C& O7 y* P - {: j/ d0 V+ B/ t
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);( k$ r- Z: @$ I" g* y: l! j
- printf("fd_socket_conn = accept()\n");( i1 f% F% k" U2 q9 x+ I0 t. T; Y
- 。。。
4 r* Y! A" Z3 ]5 V! l& M' v - recv(fd_socket_conn , recvbuf , 1000 , 0);- k" b2 |* Y$ Q- f6 T
- }
2 m! F2 |$ v" l& z - 。。。; j# f' b; e- J% b- h0 n
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);, ?) M! k& g w! }- H2 |
- pic_tmpbuffer = pic.tmpbuffer;
7 c+ J: m0 {4 Y; v8 w! ?! |) a - pic.tmpbytesused = buff.bytesused;
" f1 w+ [7 d* t! X' n7 S+ ] - pic_tmpbytesused = pic.tmpbytesused;/ X! N; c: \& ~. P# h: a
- pthread_cond_broadcast(&pct);7 V0 b! K, Y$ V# L) Y7 J) {5 \! G1 z
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;; t0 X0 v+ f" h& a, K# K- C( |
- pthread_cond_t pct;$ V9 E3 Y3 Y' y+ Y
- int main(int argc, char* argv[])
8 C/ f/ F* X4 X4 L# G3 N - {7 _8 {! m/ G7 R) `8 g q. N; P
- ...
! m* x' Z7 {) o, t4 b/ @& R. d - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
8 ?) q+ \2 ~( T% ]! s% P - pthread_mutex_init(&pmt , NULL);, O6 x- ]6 k5 l* f
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);9 K3 D6 ~ r' S1 R3 K
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
1 ]8 ?0 x; [( z, I% F# O - ...
4 l; k. J! A3 {( s7 {2 l. \ - while(1)
$ V% Z* I: E& s% E: r - {
7 q9 {8 E3 L( o- X0 @3 m - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
0 n. M, Z. _- {& C8 o7 F; d x# X( s$ I+ G - ... b3 e- u% B; h! }7 m* z. F, G
- }
, y* V, d% m, z- p6 n' G1 d ^ - ...+ k8 i } y7 `+ S, s
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">/ K- ^' J; l; Y' I. N$ E
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \7 y( z# x$ r& d7 k
- "Server: MJPG-Streamer/0.2\r\n" \
( h$ C, O. ]; Z& |! e$ [2 b# t) f - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
: f/ S A& A" r5 f5 M( \ - "Pragma: no-cache\r\n" \' ]6 p" D: M; Z: x% c/ X9 d
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
5 X) D& R. l* i( | - #define BOUNDARY "boundarydonotcross"
, ?4 L& p+ \' q: p+ q3 t4 a - printf("preparing header\n");5 I/ i+ H1 {+ X/ P0 W
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
& S* _: \. u3 e0 ?# b" [ - "Access-Control-Allow-Origin: *\r\n" \
8 B R% \: I3 |$ |: _; I0 _ - STD_HEADER \& [# \7 s5 Z, Z& M4 L' [% Z* e: c& Y0 G
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
: g; S( l9 y. b/ n# |/ z9 ] - "\r\n" \. v3 `: h1 w; C5 ?, @1 Q A
- "--" BOUNDARY "\r\n");- R" o. n0 U" a
- if(write(fd, buffer, strlen(buffer)) < 0). U' r' C' F. n+ t. H( S6 q( s: n! R
- {' B- F" u; u& ?7 g4 q$ o1 X
- free(frame);
+ o2 P) O7 M$ @( T3 s5 `, G" g - return;
% X" o9 Z1 R6 Q - }
复制代码发送完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" \1 n3 v/ A. _, \
- "Content-Length: %d\r\n" \' A8 D- K8 {8 m" }, u3 u
- "X-Timestamp: %d.%06d\r\n" \
5 o/ A5 ^7 z- F& h - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
" C6 N3 F! K$ T - printf("sending intemdiate header\n");
5 p6 J/ ~, e7 F+ M - if(write(fd, buffer, strlen(buffer)) < 0)
1 K% n9 N. g Z- {0 j( z - break; G5 ]) J6 |0 n& `1 w! W; `2 D
- printf("sending frame\n");+ |5 k; ~* D) l! B% g, I3 m
- if(write(fd, frame, frame_size) < 0)
+ `$ |5 {# U4 f) P$ ?+ U - break;
; X+ P! S) {4 A6 o$ s$ _/ U& L - printf("sending boundary\n"); \, y' h1 o# {, N8 M. e& Y9 @! ~ m
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
! ^6 _6 [) L4 B7 f4 Y/ A - if(write(fd, buffer, strlen(buffer)) < 0)
# X5 _% a( w) N4 q1 @ - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  ' z! j) S6 O1 ]8 v3 o; F
 & ]" ]0 r; Q- g+ p0 J# ]* o$ @9 X
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
$ l; t% O1 X) v" {* o. a1 k5 U- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)- f# a5 x9 M+ C# o1 P: ]
- {
2 |7 f4 v- j& y& l" I8 g4 a - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);" [9 u4 C% |2 ^" z9 ~
- if(*socket_found == (~0))
3 ~- t) \) O J& d - {
2 U8 W2 S% ^; w+ j- ~# G$ u: L - printf("Create udp send socket failed!\n");
$ \) [) r- l/ e' w0 }' u - return -1;, t/ K. t- |% P/ U2 C6 ]
- }* t: v5 Q0 y5 |2 R! m$ X
- addr->sin_family = AF_INET;
8 D2 i: A; |1 y0 ?: d - addr->sin_addr.s_addr = inet_addr(ip);
! z' w/ x# u0 c1 D. `$ k - addr->sin_port = htons(port);* K9 p! G/ S+ ~4 P; `3 |2 ^( T
- memset(addr->sin_zero, 0, 8);) S# K9 \+ Z7 i) f
- return 0;
8 d% A$ R! g7 g# c& Z q - }
复制代码
- u; D3 i# V8 q" ]8 l8 _0 P5 M' R1 C' a- x
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:7 I+ u0 P! G9 d( P# h' c- g6 N& M
1 l: G: W/ X+ h( p: |- X- g7 E7 P$ a! P; [0 [ b; a
- while(fend > 0)
% h. @" W9 D: ]6 R& [ - {
& i2 w5 l: p( D6 x - memset(picture.data , 0 , sizeof(picture.data));
! K1 z) B0 h+ }) X" \& t - fread(picture.data , UDP_FRAME_LEN , 1, fp);) J9 Y% h2 Y0 a+ r
- if(fend >= UDP_FRAME_LEN)
3 `; P! X l; N3 f - {: | k9 e: v$ H3 u
- picture.length = UDP_FRAME_LEN;$ o) _7 y8 W/ f4 B1 ?
- picture.fin = 0;+ f# E0 X# Y; H# G9 ^3 C* X! c
- }% z5 i. K |" z4 K- a/ h
- else. V3 _" L- H7 D! R( [6 K
- {
5 v$ ?$ C6 {5 G4 g8 w7 b - picture.length = fend;
: N0 w+ A# P$ p2 u! u) E& g - picture.fin = 1;
# g2 H- b7 @7 Q( K - }
" a3 J0 a, D1 f" h' P - //printf("sendbytes = %d \n",sendbytes);
0 s4 Y7 Z, ?0 M4 D @ - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);+ [2 l1 G- e' Z' c' `' Y2 e8 X
- if(sendbytes == -1)
+ }! N- ^2 O( R - {8 n' J" e R: M% o% y$ x: T
- printf("Send Picture Failed!d\n");
: ?% m" r0 F* q1 x) Q X - return -1;
: d4 ?0 ]( A0 d1 z* m/ @" ^ - }
4 q6 W" W2 _" T/ \ - else1 \7 w# Q1 ~7 t0 D: B8 i
- {) F9 _% s5 R2 X( A0 |9 z/ G4 _
- fend -= UDP_FRAME_LEN;9 \5 _: D7 |& z, E
- }
# c3 g0 ~2 P2 k n - }
复制代码 9 d; C2 n* |8 ~( d* V9 `
3 R% x* ~% F) m$ m* T0 R

/ n9 H- O/ c+ r/ E4 K7 z0 k3 `3 R. I) u* [- ~9 X; _( u l @
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |