本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 # I. w0 e" N# b; u8 i
$ b1 q! w0 z9 a1 l
, K \6 R! V# `4 `; ~作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
3 m, K% Q2 I1 q6 W1 G) d6 A6 T7 \5 y, b1 I$ _2 M$ L1 f
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 + F- D5 `; ~4 {# o5 b

5 C' K, Q8 O x0 ^/ Z! D0 C两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 " \8 ]% O4 C; u
一、HTTP网页服务器
- B* N, ~/ e' j2 Q- L先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)! v, y4 i2 b Z
- {: N* C6 c" x. A* z
- struct sockaddr_in servaddr;
, J" K. O; R8 Y* @0 I% ?4 Y% { - socklen_t addrsize = sizeof(struct sockaddr);' K4 ~8 g. ?- a) K) [
- bzero(&servaddr , sizeof(servaddr));2 p5 U8 T7 h& i9 G1 c T# e
- servaddr.sin_family = AF_INET;) z3 _% b8 U9 i Q1 b8 |
- servaddr.sin_addr.s_addr = inet_addr(ip);
% d: S ` i- ?' J. P9 T- D! F - servaddr.sin_port = htons(port);! y+ v5 N3 k) G& U9 f
- int ret;4 d& k+ @0 m. l# ^- v |4 b
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)( m5 M% M J9 _0 A4 ~: F% Z+ _
- {
$ q v+ z$ z: B" V1 K. h; \ - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);. {8 l' D# O* N9 N/ H
- return -1;" B7 \& l3 _5 v" V# j
- }( a. y* j6 J% w j/ Z/ V
- int on = 1;
e* U+ J' A$ S9 M; c) b/ p( c - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)- C/ r8 y7 K: v- C2 Q& m J
- {
m( G! J, ]% ]" f1 P8 }" R- b - printf("setsockopt error\n");+ R7 Y3 T/ j8 q4 W1 I
- }
$ U1 U6 x# F; y- ] - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
) z, c( C9 g/ S - if(ret == -1)
) e9 p- b5 \& S; o, | - {
- P1 m6 Q4 |) c$ h$ _0 G: p/ t - printf("Tcp bind faiLED!\n");4 p( v& _4 Y, u* j
- return -1;
; s+ C( \0 h8 Z" Z6 l0 ] - }) ~* _6 n s6 a6 P, x K
- if(listen(*socket_found , 5) == -1)
: o% O% I [% a. K6 N - {
3 u: }/ Y3 s0 \( o" \ - printf("Listen failed!\n");
% s4 X" ~0 B) s& B8 q - return -1;) H" g9 Z% v/ r% G* A; _2 v
- }2 Y& C( j6 c6 E' T2 {: ]
- return 0;* A7 W+ U* O& ^5 ^2 m+ B
- }
复制代码其中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);
3 J* W2 M2 N, I3 r6 M6 k$ k - void * Thread_TCP_Web_Recv(void *arg)/ P$ M; m. v9 D8 S+ | W( S9 E
- {/ i: B* B7 m) |% i, v7 ` h- e
- 。。。
3 _. S; v3 ^' B - while(1) ?* u+ m& q* _. k) B. ?) ?
- {9 ~8 t# Q- J7 E0 O, \! s% R
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
6 T, P9 P7 @! s% U8 \, Z. n - printf("fd_socket_conn = accept()\n");
+ j0 H7 \& Y5 F1 F- Z# x' |- d" U - 。。。- } k' d6 v8 d5 U
- recv(fd_socket_conn , recvbuf , 1000 , 0);5 F# U. v7 K+ W7 Z
- }5 }: M; x" }' H! K5 I0 N
- 。。。
`/ d( }3 K( H ` - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);( q8 p3 l6 O9 a" W5 y3 f# |# l
- pic_tmpbuffer = pic.tmpbuffer;
3 Y2 N- r# I' S( z) _ - pic.tmpbytesused = buff.bytesused;
, @( D4 D2 h$ \9 U8 O - pic_tmpbytesused = pic.tmpbytesused;
# k4 c; g6 H* w' S - pthread_cond_broadcast(&pct);
; z* U4 l4 Y7 ~5 O - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
' f8 \# f5 V: E. b# V4 L - pthread_cond_t pct;
1 L. x' p; k" ]' q# G - int main(int argc, char* argv[])
4 M2 a& b: W- C0 D: }2 b1 T - {
* W( E5 ^" z5 f) }$ g% o$ [# x9 C - ...
8 Q' ^ W' y& g2 b! |% h - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);; Q2 f1 J. c! k# |' Q3 H0 ?
- pthread_mutex_init(&pmt , NULL);8 ]$ h1 b$ v- c0 c3 J3 F
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
* Q" E( c! P: I8 M( D" { - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);2 Q/ ^0 L: t; ]' ?, O
- ...( N! N( c& x/ k
- while(1)
6 r7 x. j( y' _. B% d$ _ - {2 m6 n3 f+ i- ]! ^1 U
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);% J* |- B% r" R
- ...
2 S4 A* c4 s8 }3 [9 k! ^ - }
7 o# X ~1 q- `2 V; W2 }8 G0 x0 d - ...
1 G. G2 y3 M7 X" I' O - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
5 s9 D: p9 r9 z& ]- t% `1 @7 D9 \0 w - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \, f7 E- P. d5 x6 M" y
- "Server: MJPG-Streamer/0.2\r\n" \8 j! a8 U* E) J: r( A. q1 Z
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
$ n: P9 X& z8 Z) A - "Pragma: no-cache\r\n" \4 |4 O2 }% {( X5 U) n: s
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
1 ?* q" Q/ f9 c! J$ P - #define BOUNDARY "boundarydonotcross"
; b7 b% Y g; V1 a- v R# [% z/ _9 I - printf("preparing header\n");
+ [2 S$ z4 e. L) q: s1 m0 @- J - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
- N7 U5 }& y: c+ D) S4 m$ j1 T# W - "Access-Control-Allow-Origin: *\r\n" \3 }9 i) e7 s! b* w6 [
- STD_HEADER \
* M% z- Y/ X# I' n2 T( `8 Z1 ~ - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
7 }' k- v7 | g; r6 v - "\r\n" \4 I8 C1 l0 P7 ^$ W
- "--" BOUNDARY "\r\n");
+ U) u. A5 M5 K* _, y - if(write(fd, buffer, strlen(buffer)) < 0): w" Q" v3 q$ ^
- {
4 G" f* z- c5 Y. s# {$ x% p( Y - free(frame);
Y! I2 Y @" C, C1 _, H- W* N* _ - return;
3 W0 ^1 r8 U: o6 t - }
复制代码发送完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" \
9 M0 M% Z# m+ p8 z - "Content-Length: %d\r\n" \
7 M) ?! r o$ @& x# Z1 e: g: e - "X-Timestamp: %d.%06d\r\n" \
c+ @$ ~8 l7 E4 ~! ] - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);" u% F4 d* o- a( g4 m( [
- printf("sending intemdiate header\n");
6 }: r5 V. N' \& Y3 J9 q - if(write(fd, buffer, strlen(buffer)) < 0)4 C F7 S* m9 Z# K
- break;5 V1 C z9 E2 J: K0 { k0 O$ t$ o
- printf("sending frame\n");% r0 t- c S/ S+ a' A5 e% z
- if(write(fd, frame, frame_size) < 0)* g1 F9 a& q& n# o
- break; Y8 H& o6 t! P; q0 B( C5 ]
- printf("sending boundary\n");9 D1 r( X+ q) I4 k# C
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
1 b: a* O$ t- W3 Q! a% j - if(write(fd, buffer, strlen(buffer)) < 0)
( }7 T R/ u, p: P! k7 E" R( x - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  7 w9 U9 L9 R- }3 y5 W; ^ a$ A

* k, S* {4 W/ B7 H3 G( t二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:2 P6 L) t5 A( X2 Z) \
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)9 s* Q ?* b/ y" _+ [5 B" `) e1 U
- {
, A' r5 a1 I, H Y - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);# k: D g* J9 x6 G8 A
- if(*socket_found == (~0))
Q1 T* ^7 S2 H$ }7 I3 k - {
) Z* s* f o" u ~5 A4 A9 ` - printf("Create udp send socket failed!\n");
* L$ P4 p1 F$ @8 ? - return -1;
: b$ r; l! ?" k - }
' \) I+ e |8 c: u8 r- a s& a& Y - addr->sin_family = AF_INET;
% [3 b& U: @ a- D# ~ - addr->sin_addr.s_addr = inet_addr(ip);
$ o# Q; I7 W/ J, c - addr->sin_port = htons(port);( B) ^ o( u( E1 C2 n U# P3 g
- memset(addr->sin_zero, 0, 8);8 [9 o9 f8 m6 c O# u" x7 b: m
- return 0;
! a5 U- _* h5 l, d! y - }
复制代码 i, z) [4 O3 k# c
* h+ O0 E: c8 y; F5 C& ^
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
! i& p& A6 P/ l, t8 l g
5 _$ ?( E3 G8 g: g- K3 ?+ V: i# N0 c0 e: I( ^2 J4 q3 W
- while(fend > 0)
3 L0 Y& n$ l# l5 _ - {4 E0 y# }/ u) g+ @2 [: D" E1 |
- memset(picture.data , 0 , sizeof(picture.data));+ m! I8 S: G/ n2 f" f1 i
- fread(picture.data , UDP_FRAME_LEN , 1, fp);
0 T; N+ _+ y6 ? - if(fend >= UDP_FRAME_LEN)
, f4 C+ H0 J3 F) i. }" ^7 W - {
- C# t: V% y3 V - picture.length = UDP_FRAME_LEN;
! ^, J* f. r0 r7 j' j+ T) E - picture.fin = 0;: c( r5 Y! t( K5 p
- }4 [# {* o3 q' z7 a9 y4 P( Z/ f
- else: }# b3 J0 o) G! P& a6 Y/ A
- {( F N. Q& [% a' l6 y
- picture.length = fend;9 K$ |/ x8 M) l7 u, K% F
- picture.fin = 1;
7 L4 D. T" U; M$ E. v' g - }
: h+ q* Y2 S8 ~- [/ J - //printf("sendbytes = %d \n",sendbytes);
0 _* @6 `& W/ s$ X- } - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
" e6 Z: U# }: N. w/ S# I - if(sendbytes == -1)
6 Y1 T. t1 f4 l( i7 I - {: G& I! Y9 c2 ]
- printf("Send Picture Failed!d\n");
. v: H( R) D' k% o5 C$ y3 ~ - return -1;
" W, w9 A0 k( a1 l7 g - }
' Z- @1 o0 D) y2 L( }7 X - else3 ^4 m: P( g0 P2 u4 J! S
- {
7 g# i/ d$ T6 U, p" Y - fend -= UDP_FRAME_LEN;' ~/ Z% P1 O! Q! j. {3 E
- }/ @& x! o, x! F" P: W
- }
复制代码 & r a: T! j& G6 ^1 B. @
' a6 y% [! g+ b: z- e& P& t% R

9 n! I, U) k$ q, B7 c4 r' V' c7 L+ v1 ^* h+ ?; n) p9 S' N; T7 x y
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |