本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
3 _, r1 y7 w; e+ G' _' x/ E- N! [( Y

* i3 h9 v$ d* t L; U2 {作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html+ ?* r# a% ^) N" |$ V
4 a& a; L" v) |- Z
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
5 C0 f: y2 H8 U/ V
' `% j+ ?& r0 A6 b: _$ c! c6 g& J" `两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 8 e9 X: [ x) }0 b/ A
一、HTTP网页服务器
) }& l0 l7 ]7 M- J6 L先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
! C- `+ K( V9 }! Z - {2 r/ S- C: q. l5 i! |0 |5 h$ B
- struct sockaddr_in servaddr;
6 n }/ S1 P: p% G: M5 T - socklen_t addrsize = sizeof(struct sockaddr);! X4 J S. \: |' P$ @3 x
- bzero(&servaddr , sizeof(servaddr));- l; |& d" v7 Z3 D2 W; m' c# k
- servaddr.sin_family = AF_INET;
. p+ K) |* Y" Y - servaddr.sin_addr.s_addr = inet_addr(ip);: T4 }/ |4 A7 e) I
- servaddr.sin_port = htons(port);: \ v& z' D3 Z ~
- int ret;2 {5 j7 K& u3 j! a% G8 s& _* s
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
5 m0 n ~/ I* j" I! r - {
$ y4 e) n# a8 w& o: F% B/ L" s - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
b V2 i/ w" P) U5 T& R - return -1;2 {4 {* p! Q( Y" |1 ~
- }" ]4 _* g* S8 N2 s% J- _
- int on = 1;
! L# \5 t. _, n7 z - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)9 n$ u$ Z% v3 q# _; \3 s' {, t
- {7 z* k% v# {& H6 V3 s
- printf("setsockopt error\n");
( P. F0 X" p7 ~ - }
; S E0 r0 n, G; x( V8 k - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
. U) {: {: R v1 J2 ]- ~- v4 X* L - if(ret == -1)
' c) K- J( O4 e9 P2 j- q - {
; t5 |) p, p7 b - printf("Tcp bind faiLED!\n");/ t; h4 {" a- W: E3 ^
- return -1;6 {0 B- Q1 u1 |8 K' D
- }3 r1 Q& [! C; o
- if(listen(*socket_found , 5) == -1)
( o: `3 ]# G# C - {
2 g- G0 x, A- A! N" L! e4 t& X2 K - printf("Listen failed!\n");7 O- z9 C2 q9 U4 `8 p9 O
- return -1;5 f3 x- V$ H; x/ N# `& O
- }* t, z8 r7 n, c9 @9 L0 @
- return 0;
; g% z' _- \4 X2 r0 `) b- N6 I! | - }
复制代码其中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);
2 n6 t) B0 r7 ~6 x - void * Thread_TCP_Web_Recv(void *arg)
9 r# s3 v/ u; K5 Q* K- Q - {! L; Y; P- ^8 P
- 。。。
j- X6 U1 I8 e. g7 n - while(1)8 S5 b* I0 @$ D; }* Y- l r
- {0 A( x9 v7 `3 s J. w d5 R8 d8 X
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
( q- R0 w j9 {& N+ U1 u+ p - printf("fd_socket_conn = accept()\n");$ f0 O0 K! X9 U1 c
- 。。。* u3 n" K* }3 g6 K$ ~. ^4 S
- recv(fd_socket_conn , recvbuf , 1000 , 0);: ?6 T+ Q; P. |1 M. Z
- }! `" G6 k6 P6 O8 _! h; Y
- 。。。
8 s- F/ l/ t8 s% e: T* x - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);% P; B( s2 W. B1 U) y1 x
- pic_tmpbuffer = pic.tmpbuffer;
7 ?$ A+ s2 r; ~; H1 p5 q$ m0 [0 p - pic.tmpbytesused = buff.bytesused;3 t) {7 n' t% n7 b: q. c
- pic_tmpbytesused = pic.tmpbytesused;6 ~" d/ J1 @) ?9 q% \9 J. M& _
- pthread_cond_broadcast(&pct);
4 R( H$ W) C# A. F% v - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;) c8 B+ ]( \1 w8 C
- pthread_cond_t pct;1 y$ E0 ?- ]: ^$ _; [
- int main(int argc, char* argv[])9 L3 C# V, x$ N5 h: L6 k
- {; L% e1 w9 L. h; `& C7 W6 ~# Q# E
- ...
2 q" k/ [4 }: v; U+ k - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
4 Y! F: e: c: J0 y, U5 q - pthread_mutex_init(&pmt , NULL);& q6 P1 S8 i9 v) h
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
7 W* L' g# O2 ~- @ - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
& ]: g6 W: c( P4 s - ...
3 g/ ], h& `8 ^2 p4 l) h4 L3 W - while(1)
$ t7 H k2 P( B% U. J; h - {: l* A4 J+ ^5 H( A+ `6 f, k: m: z6 e% k
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);+ |, V- c) o% r+ l4 w
- ...
9 ? D( ?! w1 W% `* {5 v2 r - }
4 d7 |! Q6 j+ h9 m+ d j& l - ..." R f9 ?6 T# q
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">% e a! y0 S6 g7 P& R+ d& k( z
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \9 H0 T( L4 u# ~" m" ^* z
- "Server: MJPG-Streamer/0.2\r\n" \- j! X$ i* G# t! q5 O
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \$ U! b) q2 |: }4 ~
- "Pragma: no-cache\r\n" \1 [ W) x: X r6 Y" I J
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"7 \( U' H% f! d
- #define BOUNDARY "boundarydonotcross"
) w& p3 d M Z& y - printf("preparing header\n");
" o- F5 V- f) ^- n! i - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \8 J, K& j/ y9 v0 z$ k
- "Access-Control-Allow-Origin: *\r\n" \
) o5 i1 S/ i# j( I' r - STD_HEADER \8 l% S% }1 {. U
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \# s1 s& s, J$ R" Z
- "\r\n" \' |, A7 o7 v3 i& B" v# D0 p8 k
- "--" BOUNDARY "\r\n");( x- W8 r! V7 H% H# f% ?" e* e" @
- if(write(fd, buffer, strlen(buffer)) < 0)5 j+ c3 Y! c/ e- K6 Z
- {
% k- K' [- u: l' H* C% r' P! d - free(frame);) h3 C4 e: P6 F, H" k
- return;' ?, N/ A! B" A" _- q3 f4 k
- }
复制代码发送完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" \
: A3 P1 n+ y3 U3 A/ h" s" s - "Content-Length: %d\r\n" \
% A5 p& w. S! E+ G2 ]* A$ c% Q- k3 U - "X-Timestamp: %d.%06d\r\n" \
* C# a1 B, b0 X1 { - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
+ P; B. d1 k' M: s \! Z - printf("sending intemdiate header\n");
?" R; V8 V A: N - if(write(fd, buffer, strlen(buffer)) < 0)4 A, x, ?7 {; S- `6 Z7 [+ @
- break;0 V) E! S, k8 z* J
- printf("sending frame\n");
0 T+ h, x# _4 G$ E - if(write(fd, frame, frame_size) < 0)
& \" P1 U3 ]) y. i - break;; c% X: Z) y) o( o$ O' @7 Y7 Z
- printf("sending boundary\n");
' t9 k; |3 n8 R+ M+ |: F4 U - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
. ? _8 q9 L: O& z3 W ]; b - if(write(fd, buffer, strlen(buffer)) < 0)
) K# o% J: U' s5 \( H$ b; G% | - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  / {1 r7 T" [. u/ e( t
 ) o& N+ x5 A6 |! h* ^' D! p
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:6 W% @: m0 L4 B9 X& v) N
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
2 C p3 _# h B* _9 P2 W. h - {
1 u/ p# B" O/ L9 e9 g: i - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
( t+ J6 K$ ~& b! K6 g+ m7 M - if(*socket_found == (~0))
$ w0 J4 M9 h4 ]% R; B! G9 i - {
U+ c5 I; p9 x# Y2 z( n - printf("Create udp send socket failed!\n");
3 G, {, l3 Q" @/ p- Y2 E6 T - return -1;
w0 L3 Q( D6 {( L - }, o# b' r! X( m, B" Y8 o* [9 w( y" I( s
- addr->sin_family = AF_INET;
! r! \, e6 s2 U, N# D+ V, } - addr->sin_addr.s_addr = inet_addr(ip);& r9 W' \# H% u& T1 P
- addr->sin_port = htons(port);+ X( T5 X7 X: H0 ~, j3 R2 S
- memset(addr->sin_zero, 0, 8);
& r5 i' V' \1 C+ ^) Q - return 0;
8 P( I) Z( U. ]6 F - }
复制代码 9 D# m) R% P! O$ m
9 z9 W( g! ?7 p; K8 d' o
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
1 r; B" ~9 f X5 k+ s4 I
& V0 t/ J8 r( t- ^# o& i
- R5 Z3 x B# o+ p T! b" e& R- while(fend > 0)
V* Q" x6 C$ \& v0 M( z - {& E" ~3 A( R# F7 e+ I/ ], j
- memset(picture.data , 0 , sizeof(picture.data));
$ ^( K0 E' B- V, D( d1 T% G - fread(picture.data , UDP_FRAME_LEN , 1, fp);' h/ B* k- m* T+ u# d
- if(fend >= UDP_FRAME_LEN)$ }: m) l" q+ r+ N
- {1 v v3 Y$ l5 @/ y- G
- picture.length = UDP_FRAME_LEN;4 w) y4 T( u7 J- g. {
- picture.fin = 0;) ]& V. X# H! r6 ]: ?
- }
) h0 s) j9 Y( ~2 T- L- F1 p- s4 J) j7 T - else
9 m# x& o6 b$ \) i: ?, K7 z" ~ - {
- I7 P( R! ] t! |1 T - picture.length = fend;* g1 Z5 c' [2 }/ ~- B: x7 U3 a
- picture.fin = 1;
" I/ n0 _3 s3 y" F6 D5 h - }; i; t% U- u! b7 M# w' f/ \
- //printf("sendbytes = %d \n",sendbytes);
& Q: M5 G+ u/ n) {* ~ - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);( |1 _2 g" X% R, u5 L
- if(sendbytes == -1)
+ L* B: e9 H4 U2 w' F - {
1 N' Q. S% m8 g6 j$ i& ?- O - printf("Send Picture Failed!d\n");1 [6 T# N% \) Y1 E
- return -1;
@! n, K- D( m - }
5 C; `; `; z0 }7 y0 | - else4 v* i6 C8 Z }9 I. U
- {
3 @: X$ v: [$ F3 M$ p5 j3 r - fend -= UDP_FRAME_LEN;8 g2 i: { T4 i$ I3 v1 I, P. Y- U
- }
' j" e6 e! y# E - }
复制代码 # |5 S- n" T3 r8 \1 D
1 x/ G6 T) O/ W& n$ ]' @+ b9 I
 ) l+ Z: p6 g- g5 P. k m
8 C) Z! z6 H: a+ U0 QiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |