本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
! a! V; B! ?' k( t7 k1 h- W. k# D$ C: v3 t/ d* e1 E0 W

1 a( O3 s# t* f6 A作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html' |4 L" V A& X" y
8 T2 g4 l S+ c! e* U& U
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 / o: R1 I1 @* O1 L7 i4 T

7 }0 p2 V+ x5 P3 i5 z两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 3 d Q! h' ]1 M) Y+ p8 G- u
一、HTTP网页服务器 g5 ?, q y5 |( S5 e) V
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
, d. B! M( N0 e: y - {0 f; z& u- k2 k2 F
- struct sockaddr_in servaddr;! B) m! S; V8 ]! }6 [
- socklen_t addrsize = sizeof(struct sockaddr);6 {. o& a9 J6 J N$ s. `4 X- h
- bzero(&servaddr , sizeof(servaddr));
1 ?6 }# m4 H; t* h - servaddr.sin_family = AF_INET;
- Y4 R$ z' M! S) z: q- N/ J$ O - servaddr.sin_addr.s_addr = inet_addr(ip);: b6 D/ _: p- p* f0 Y2 P- o- s: i! {
- servaddr.sin_port = htons(port);
2 [( l: U, K( @" g" Y' ] - int ret;
3 d1 V# G# b$ v- n) E6 ] - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
1 l( y% i' Q ~! s - {
4 i8 j9 @# H2 U" R& j. Z& P - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);8 C4 E% q( |3 `! q3 v" D! n; p
- return -1;
; g8 T5 U3 \, [) [6 W - }
- I3 o5 J& M/ C: J1 S - int on = 1;
# w# ]5 x( T. t. @" i0 ? - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
( H3 a1 N' R5 C - {
/ c9 D3 y6 V0 @! M - printf("setsockopt error\n");) c- J' J! h, Z% O: q6 `( V
- }- ^% @) X- N$ H% P1 s; F
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
' U9 p" l6 c9 R' X& ~/ @9 }+ G4 k& u% m - if(ret == -1)' S# j6 K) h! B+ q* \! \, t6 I
- {
6 T, I+ c9 B* i( M8 k8 y: G - printf("Tcp bind faiLED!\n");
2 m+ o/ Q* X+ R; }1 r$ p3 @ - return -1;
/ D9 c- B. h+ } m4 r W - }
+ M# z w! C Z - if(listen(*socket_found , 5) == -1)7 J, o; W: W) h, c% a- s( I
- {
' {$ D+ V! i0 k! O* [ - printf("Listen failed!\n");* D% ^8 F, T1 l3 u& K+ I& V' j( ~
- return -1;9 | ]4 ^6 j( R8 o$ a4 X
- }/ ~/ ^8 u% |! k
- return 0;0 X& t/ V5 u% d; u d0 [: K
- }
复制代码其中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);8 J7 K$ \5 @: Y8 E! X
- void * Thread_TCP_Web_Recv(void *arg)
+ B: u2 F3 L5 d% I' O' y - {* @9 M9 u0 u `1 X/ G% W6 [6 e
- 。。。" P- [& Q- j' `# T, w; h* j
- while(1)
; l7 C6 Y% e/ P. b, y, R) w - {
: D; Q1 @0 v, O/ d! F ]1 A - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
% f9 t. p1 b; T8 y ]4 U - printf("fd_socket_conn = accept()\n");
# m+ C1 g( W7 B - 。。。! u0 @. Z8 c8 h" C( `+ m
- recv(fd_socket_conn , recvbuf , 1000 , 0);
; P% S/ D2 [0 `- ? - }
) w# G7 c" R3 f) R - 。。。& S9 ]: ?& L& a' C9 P+ ^
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
+ Q8 ?. Y( y/ t. @$ Y# H - pic_tmpbuffer = pic.tmpbuffer;
1 E% d+ c7 j, B9 v4 I3 Y - pic.tmpbytesused = buff.bytesused;
: ?' k$ C5 o' e, @5 R9 ?7 H R - pic_tmpbytesused = pic.tmpbytesused;
0 L1 v: c/ |3 b. l - pthread_cond_broadcast(&pct);
+ I" o3 r' d* s' N3 P: K' Z3 m - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;2 R4 l* V! ] k! O
- pthread_cond_t pct;
! y% l- u h' L0 w( R% f - int main(int argc, char* argv[])& ^1 Q; e" b, x i2 w/ A
- {' P3 K% i' _" L2 q
- ...
' H) q: m" K/ P2 [ - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
2 C5 E4 j! d$ Y" T5 A/ t - pthread_mutex_init(&pmt , NULL);
. c) Z' a7 B6 i" \- `1 @ - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);, @. v7 I- T' a/ C8 ]' I8 b @
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);4 c- m H2 z8 {
- .../ P0 j2 h& @' B$ x5 u7 \! i
- while(1)2 j3 m" t0 @: M2 Q2 ~
- {6 d$ h6 b: O7 T2 y+ |
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);, b; R$ _3 b+ r% y: n* T
- ...2 n0 n& E2 X0 S
- }$ {# i" a+ L4 r' V9 J
- ...
; ?+ \8 J+ n4 I4 S* F - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
9 h: r9 E# c+ q" n1 k# ? - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
* j$ T* j1 O0 B# H! v - "Server: MJPG-Streamer/0.2\r\n" \
/ ]# z: }+ Z4 `1 ] - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \% h, I A5 g, O2 a& j* c# f
- "Pragma: no-cache\r\n" \; `, Q! V* m: c4 j
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"" Y0 [/ [" `/ a# n8 x1 `
- #define BOUNDARY "boundarydonotcross"
+ L2 p* j0 O. i$ F- ? - printf("preparing header\n");
4 O5 o! A q6 I+ s, @' k - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
' _& c- y2 `$ ]' I, A0 j0 C - "Access-Control-Allow-Origin: *\r\n" \' R# Y* T5 T+ \. @/ S* i
- STD_HEADER \
3 b% \* w& p' H }# R4 _4 u - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
0 G1 k1 p% A. T" V - "\r\n" \
8 q4 u4 Q* Q9 t: d. | - "--" BOUNDARY "\r\n");6 `6 B; a7 y2 R! G5 D2 I+ m
- if(write(fd, buffer, strlen(buffer)) < 0)6 m. J4 O* X( z a, P
- {
) l& y' o/ p. K2 o4 J2 Y# D2 Y5 I- X- ] - free(frame);
& Q9 i |9 |3 { - return;2 B/ Q; a9 t/ D. e7 [9 C4 d
- }
复制代码发送完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" \
8 k y, m0 e. e# K7 p' i. \ - "Content-Length: %d\r\n" \
. G0 c$ R$ P% e# @3 ]# i - "X-Timestamp: %d.%06d\r\n" \. H% K0 S% c: q9 j# ~% K3 I$ @
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);. y8 @0 R, b# b& t0 S
- printf("sending intemdiate header\n");+ u0 ?6 @! }7 Z) A- {2 k- V
- if(write(fd, buffer, strlen(buffer)) < 0)
+ i3 w4 s& ^" q# u' b: o - break;" {1 [" \- a# v; _( a3 o
- printf("sending frame\n");& b6 {3 ]' H0 a# m
- if(write(fd, frame, frame_size) < 0)
, R3 ]" c; Y; C, r6 M0 X - break;- L9 p# n$ ]8 w( A# c
- printf("sending boundary\n");# d- C8 s& f4 A6 [& _: |# I
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");0 v$ F9 s' V6 C: K) A4 V
- if(write(fd, buffer, strlen(buffer)) < 0)
- ~% `5 F# t. f7 E1 { - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
: K2 s" q" \; _7 m/ Q' D; \ A% p
0 @! k) P0 U4 @* |' Q8 T0 h, t二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
8 N, A1 Z+ f3 L& ~& t7 O7 F- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
0 ^" _) ~; |+ [3 _ s, o( n - { l$ B, ^, G w: m) S& M2 I
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
! Q/ t4 d: \0 d: j' Y - if(*socket_found == (~0))4 |# x9 {% X' h& P
- {5 o0 A* B( A! Z' Y
- printf("Create udp send socket failed!\n");$ r6 G: E" ~3 k4 R. H G
- return -1;
% P) B; X' n5 s3 C, C3 f - }
2 s3 P3 H( b4 Y5 }) u& }9 S7 d - addr->sin_family = AF_INET;. D, \; |# S) Q' A$ \5 ]) ~
- addr->sin_addr.s_addr = inet_addr(ip);
, @: @7 v) L: f P" U9 y+ i3 j; I" j' N - addr->sin_port = htons(port);
* J8 ~* Z) j$ D z S7 T$ s - memset(addr->sin_zero, 0, 8);
/ R2 a1 F4 G0 O5 o/ y - return 0;; h" C0 ~+ @0 s. U) V. E5 D: S
- }
复制代码
, a3 \3 n& w4 B# O F# n. {& W9 F- V1 J2 Y
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:, h& m( X9 F0 m4 `1 d: Q* q( X
" i8 P: R. k) l+ M
" ^$ H( v' R& ?% L, M8 f- while(fend > 0)
& P7 B, V( [7 f4 S. c/ J - {3 |+ E M; ?% d( o4 W6 Q% E1 q
- memset(picture.data , 0 , sizeof(picture.data));
& U! B6 m+ n. h - fread(picture.data , UDP_FRAME_LEN , 1, fp);
% v6 A1 V, z- W# C3 d* ` - if(fend >= UDP_FRAME_LEN)
* X2 ?; K1 N) {8 q3 q; z5 \3 s% P3 ]( A - {
4 [$ G3 I" i: l: L0 K! k - picture.length = UDP_FRAME_LEN;: ]& u* V0 m3 g' ~) q- [
- picture.fin = 0;4 }2 S4 _& n F* I+ y
- }
5 [0 t' q" G/ Z. w+ T. V1 F. t - else9 b" N* n& y2 b6 e' g2 k
- {% O: N7 {. |/ N) G' ?
- picture.length = fend;2 c% ^# B0 C. A" R
- picture.fin = 1;
+ d/ ~/ S* F4 n3 N' A$ l1 R5 H+ f; b - }5 B- K; F, o1 d. L! b! t
- //printf("sendbytes = %d \n",sendbytes);
: n4 t3 n6 ^: ^ - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
: U7 V% J1 f+ e. s - if(sendbytes == -1)/ B, \9 {$ L$ ^
- {% M2 q& ~1 a; ?- e
- printf("Send Picture Failed!d\n");+ M1 @, G2 t, x9 A( K& K4 S9 R
- return -1;
: `3 r; R% ?! J c3 e( g4 b - } J" O, S# N: q' |0 ]9 e, Y6 e
- else
$ E( } u; }; L* q8 X* p - {0 V' Y* b$ }- j7 E
- fend -= UDP_FRAME_LEN;8 a. c/ w5 k& _5 H" q) d" X# h( G
- }
2 b+ g' V/ a$ e0 r. l$ \& V8 S - }
复制代码
0 o ?' u$ V. b
1 S" I; Q! [' a9 G) N! f3 A
; a4 Z y& i5 J% [9 ?
6 o6 z, V9 J5 K! D5 giMX8MPlus 核心板: https://www.forlinx.com/product/136.html |