本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
; m& \* w8 F3 b r8 v9 D
8 p: X( L/ K! d2 K$ O6 W4 c
0 ~9 V) d& Z* k7 A7 k+ v作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html5 |! J* ^' {& l& `* S4 f
% F$ g2 j$ N' G, [; P! {6 |" i
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
/ s2 \) _# e& u) z. q$ Y 5 G7 x# w8 {- k- `) U
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 0 n& u* u( H1 u5 X- k
一、HTTP网页服务器% i' ^1 z2 f$ Q9 }0 t
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
1 e& p- S, H: j+ m, Z - {
+ K. t5 s2 A8 O - struct sockaddr_in servaddr;# X, Z t! x) n6 w& g5 G4 x& y' p
- socklen_t addrsize = sizeof(struct sockaddr);
- [, Q# e! N5 O( ?1 t# E; l3 f( r) _8 o - bzero(&servaddr , sizeof(servaddr));6 [% W" J4 P- w3 V7 x9 z
- servaddr.sin_family = AF_INET;
5 T+ K* j8 h/ Y5 D" a) T! x - servaddr.sin_addr.s_addr = inet_addr(ip);0 l+ @* w) \! O' X+ Y! q
- servaddr.sin_port = htons(port);# ~/ i" X2 w n% p+ t( }% O
- int ret;
! ]/ u2 W2 b3 Z; u9 p$ t - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)# w7 L k& x. e5 D
- {
7 a( A |2 W8 [1 L( ^! ]. M# D; m) ~1 O - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);3 L% Y# V; i/ t5 |& b& c& q' i! B% ~0 J
- return -1;- Y' _# Z& f/ G
- }
$ D( |( ^8 H0 Y! {& O; k$ m - int on = 1;! Q7 Z7 D+ E x; |' ?5 {/ p
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)% U" A* B+ q1 U/ C2 v! y U! j# w5 p
- {1 U. s$ {/ x& x A1 [& U1 i8 ?
- printf("setsockopt error\n");
8 C( l" a$ ]; ?! w3 T8 a% D3 ? - }" g/ F3 \! U5 k2 r/ w
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);& {" p. I% K1 x, K. m; t8 M
- if(ret == -1)
+ p6 ^+ c; C. o, ?4 M - {
! [1 [% B+ [9 C8 `, d - printf("Tcp bind faiLED!\n");
$ ^1 G2 C5 M( d$ W4 J. m - return -1;) z U/ k' q; o! w, \, _& K
- }5 O, R& m2 C% }/ h. z) T
- if(listen(*socket_found , 5) == -1)
' J. e" |9 B. M - {& C1 M; l, k, ^' `' O' c# {
- printf("Listen failed!\n");
7 n. a3 ^! I$ [ - return -1;' G9 P w. @* }3 e a
- }+ {, m, U7 ?! N6 p
- return 0;5 w0 E6 h" o& O# 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);0 {& F, P0 l1 S. A8 k: w8 I
- void * Thread_TCP_Web_Recv(void *arg)* u" i# I" J& K7 I8 f
- {; O, Z; l( K9 Y
- 。。。
F$ _# d" e( R8 T( v. i3 h - while(1)
+ s; D& U1 r7 ^8 T$ G - {
1 }! D S0 j9 g9 c; |, H - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);; i/ Y e: F) \; F8 }3 M1 e
- printf("fd_socket_conn = accept()\n");- E$ Q; q) z: m- i+ J+ u% F' R$ ~
- 。。。
- n2 |+ z: o0 \1 L# y2 v: q - recv(fd_socket_conn , recvbuf , 1000 , 0);/ I- e+ M2 K" T1 g) o* S
- }
+ E. i/ ^ L* z' m" C$ M k - 。。。
+ }9 @( r9 u( ` - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);' v+ @! w6 L5 S
- pic_tmpbuffer = pic.tmpbuffer;
! b/ o1 z7 d p$ v% I5 N - pic.tmpbytesused = buff.bytesused;
5 \! n7 @) Z5 r) u: R3 [ - pic_tmpbytesused = pic.tmpbytesused;5 H+ D8 ?/ f! Y, U
- pthread_cond_broadcast(&pct);8 ?1 d% {: s# b! _
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
; v5 l! \6 e- D/ g& ` - pthread_cond_t pct;
1 y% q7 e6 z4 B3 g6 X7 k: p - int main(int argc, char* argv[])- I9 ]& Y% ?% |/ M
- {4 F8 I3 _$ |. U0 e% F
- ...% r" N& J, q- J# _. j
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);6 p0 e( y" g, P0 Q" b
- pthread_mutex_init(&pmt , NULL);! W* A+ Y- J$ u- K5 ~* G
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);0 s! P7 u3 n/ @/ K! T# {+ P4 n. Z* }- h, M
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
3 V# _' E. I4 r$ ]# M& S( k1 R - ...+ P' m9 L5 k c# M2 r5 l, f6 e
- while(1)
' V7 B* r7 t( E: f; s - {
) ?; W" k6 L' V' o5 c - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);# @8 q+ |$ ^! D D! k& O) R
- ...
8 ?9 J& e$ O$ C - }
. Y& }7 x* R/ r5 c* x- i - .../ Y3 p) v9 Z t: \8 p" h! M: a
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
. m7 _4 ?, z! I5 b+ [6 l, L7 ^ - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \( P9 X8 f- a7 Q# f$ @! f# Y8 n
- "Server: MJPG-Streamer/0.2\r\n" \
4 L- A$ j/ p. F+ S - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
. w4 Y W1 w' `. E9 \) x - "Pragma: no-cache\r\n" \) v7 ?1 N! `- _7 L
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
6 n- G- Z6 Y; c/ p1 ~ - #define BOUNDARY "boundarydonotcross"2 \' B9 R# t0 P, o- \
- printf("preparing header\n");
# H- o# W r& ^" f0 ^ ~' f5 J0 j' x - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
$ z' F, d' y; w- @( f7 H+ X& p4 k - "Access-Control-Allow-Origin: *\r\n" \
! \0 m+ U. k- [/ O; u6 _9 ] - STD_HEADER \; D! d. \5 n# P) J) {
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
2 c4 k( K9 k4 E& C+ p - "\r\n" \: K' x& G" @, G9 L) i4 ~* I& w
- "--" BOUNDARY "\r\n");8 j: r' {5 F( ^1 ^! V
- if(write(fd, buffer, strlen(buffer)) < 0)
& ~% T2 E: A" q! I) { - {
0 X, L8 e: p( g3 A# o+ V/ f9 y1 W - free(frame);
# Q5 L- @) _2 Q - return;
' G6 M' X- a7 J2 ~& S& l - }
复制代码发送完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" \
# _# r# p V% Q - "Content-Length: %d\r\n" \: C: c0 E( x0 [) B2 \* ^2 K+ ^
- "X-Timestamp: %d.%06d\r\n" \: |6 ^! W1 P0 L
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
! ^5 t% R( w3 ` I$ x$ p - printf("sending intemdiate header\n");' ~5 N* B9 e l' i0 Y
- if(write(fd, buffer, strlen(buffer)) < 0)- r- o) `8 v# ^3 c, s1 T
- break;
0 `4 w X: E! x; b0 h; G - printf("sending frame\n");5 v- Y( D' ^0 i1 ?
- if(write(fd, frame, frame_size) < 0)+ A2 d, L! ]) p% E* x
- break;
4 c" X. x; i( [! n# B& B0 C - printf("sending boundary\n");$ D) Z [5 V0 U8 {' `. @
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");/ t: ~7 k/ V, `5 I
- if(write(fd, buffer, strlen(buffer)) < 0)
, y7 C& R1 R/ s# [ - 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 @% n7 J# Q$ o. `
 % b8 a+ {2 j9 b! H( @# }
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
6 t2 s. b- q) i& i# P" f; V- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)$ S) f/ _6 L, F2 h- C. T
- {, o U( X9 A9 |; x! F1 B
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
: w& j. A" R+ A# p* C+ l1 R - if(*socket_found == (~0))" H5 U0 b( D8 T/ @
- {
; R! [3 c. I8 H - printf("Create udp send socket failed!\n");* t* a+ M" W( z. v( _9 X, N
- return -1;
; K$ p# ^: U5 ^+ o" A - }
, }- N# v' t+ f - addr->sin_family = AF_INET;9 u0 A- g0 l. c+ q* Y5 ]8 u+ c; M& _
- addr->sin_addr.s_addr = inet_addr(ip);/ q: x+ a. a2 \
- addr->sin_port = htons(port);
$ q9 [9 x9 H( F" l$ T1 | - memset(addr->sin_zero, 0, 8);3 X' C4 l, l2 ]- f4 d- { s! ?3 u
- return 0; P( D( b8 `6 K
- }
复制代码 & h! _1 W- T9 D- h* N" J, v
1 _+ B9 B# q2 r: K
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
# ^. x5 H7 F( j5 r* C
& D6 |; T0 [% C. h& A5 Q4 h# W0 O- ]0 ~
- while(fend > 0)
. X$ L5 S% W& \* P - {
& U) i4 K, F$ F9 e8 W - memset(picture.data , 0 , sizeof(picture.data));. z* E* c) Q" \, r ?* p/ Z
- fread(picture.data , UDP_FRAME_LEN , 1, fp);
* t$ o% p( s# _3 v! C* ` - if(fend >= UDP_FRAME_LEN)
' L4 k, s# L( i6 z6 [ - {- b+ [2 \. {. y" {
- picture.length = UDP_FRAME_LEN;
. B0 @% n- O% |% s2 C. ^ - picture.fin = 0;
$ l! K2 T+ T+ b$ O - }! Z* ?" P+ W. l8 j6 ~3 N; R
- else
z6 V& I/ U; Z1 A8 n! j - {! `2 U8 G( v- l+ a
- picture.length = fend;
- k* k# q5 |5 _0 I - picture.fin = 1;4 E" j6 k4 x- _6 q5 d
- }
" y0 P5 I" e: w8 W* R* v8 D - //printf("sendbytes = %d \n",sendbytes);
" H9 s; |/ \" h7 i. X8 d - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);+ y5 c6 K, g2 s% Y1 Q$ H9 @
- if(sendbytes == -1)
: j( S0 j! U! P5 [: i2 K& Z, F - {; Q* x% m# {6 n7 f2 c; r
- printf("Send Picture Failed!d\n");) m& U3 a5 x- u% I
- return -1;
0 d* k, e" @6 k0 N - }
- F$ B2 h* W0 P5 d' r* B - else/ j* E; F8 J" T, }% R; R9 S
- {
- q4 ?/ t$ g1 A7 u M; ~ - fend -= UDP_FRAME_LEN;2 H/ T, H) P( F
- }7 j6 a) `2 r0 Q- E& Z8 n
- }
复制代码 & Z: ?7 R f" c6 \- F4 l
$ n+ D3 M$ N' i1 u* a* B6 w2 }

. N6 W% U) p" I+ n
* V/ ~4 z1 y# P* kiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |