本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 3 D* N8 t% S8 y( `) Z* |
4 H3 `. u2 g. U0 h 8 C+ w, V/ ]# e* E6 W
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html, Y6 L; f6 b+ l
' B/ S& x: B# ^: _" d
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
& g, x+ } `8 {1 E * \) f& N+ E" O% d4 U- H
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 - N) G6 T w5 e# z
一、HTTP网页服务器3 b2 Z, o4 [& v- D1 T" L! k
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)) a& a/ t. [) ~4 [( H3 `# J
- {! p% O1 X# y) F/ p
- struct sockaddr_in servaddr;
3 u9 p' _4 z {' U/ c3 h - socklen_t addrsize = sizeof(struct sockaddr);7 K* [0 [9 ^* r% v$ `& h
- bzero(&servaddr , sizeof(servaddr));2 _8 S0 d# E m( ~
- servaddr.sin_family = AF_INET;
) q/ Y% e$ c/ ~+ H' p2 _ - servaddr.sin_addr.s_addr = inet_addr(ip);
) |4 V' o( ]- t* {) v3 ]# ? - servaddr.sin_port = htons(port);; W+ V" \+ r3 ]" T% o4 ~
- int ret;. c% M" g2 K3 b: G6 U, a
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
- V) Z1 |9 v$ y0 W7 U9 o; G0 ~ - {
. R1 F; p! \: Z! R7 K/ q( _6 x' _ - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);# p4 o* j5 ~) x- R# }6 ] A% W Z
- return -1;- R' j; G* y2 C" o+ g
- }
7 [' Z+ I9 @" N7 S/ P. K+ l - int on = 1;
( U) |8 v7 r1 q4 m2 D3 w9 _ a% t - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
/ Z* U1 I1 K. \* }! _5 D - {( s4 L1 k2 T% g
- printf("setsockopt error\n");1 V" O7 I2 d- G
- }2 ]! {# f, d, O1 N& a& s r" d
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);1 F- ]; r1 b0 \' |0 X/ [; G
- if(ret == -1)8 G# R4 i. @) l1 @
- {/ X0 a0 \! t( P+ L" q9 Q: Y
- printf("Tcp bind faiLED!\n");: }( a) ]3 o' Q5 x9 \( h
- return -1;# b8 Q0 t |. Z* D7 o: f
- }6 ?% S* y: `2 v. `$ ^
- if(listen(*socket_found , 5) == -1)8 E# \+ e$ x I8 m, y# |9 p
- {
: K% |9 l1 I5 m" ~; L1 H - printf("Listen failed!\n");+ ?7 r1 m7 @0 W# w; c& @' \# q
- return -1;+ f( ]+ \" ^3 e9 B$ B
- }
" {% \* H# `" g# E$ U* f. m( N - return 0;
% l3 \( r& |; J$ x - }
复制代码其中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);
# T* O, r! [5 a" f - void * Thread_TCP_Web_Recv(void *arg), y% F4 e8 z0 z
- {9 \7 H8 j! f |0 t
- 。。。8 ~7 l1 s6 s; m' U. U: R7 f6 y7 `
- while(1) r6 `' M: Z+ g) W8 m8 b
- {3 S/ Z6 R3 M# ^9 H+ u* Q) S
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
3 ?' ^% P% B* Y h& ?0 |3 X - printf("fd_socket_conn = accept()\n");' P8 T4 s F b8 j% h- A
- 。。。2 O3 Z" ?2 U( ]# m1 u5 }" i/ j
- recv(fd_socket_conn , recvbuf , 1000 , 0);8 ^+ |# C* j) K- i$ `$ ~8 ?
- }: ]# v8 O1 j5 @) c! B" G
- 。。。
) U# l+ A4 e9 F- Z$ ]1 p0 Z& \ - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);) J$ l& u# U/ E0 A' w
- pic_tmpbuffer = pic.tmpbuffer;# r% }8 N- ]" ]
- pic.tmpbytesused = buff.bytesused;
4 m; M% [ W1 T( |7 ?9 m) T - pic_tmpbytesused = pic.tmpbytesused;
( g$ ]3 f& j+ O- J6 c - pthread_cond_broadcast(&pct);
8 }. j {* \* v$ `4 t! L - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
6 i' @% V5 I! L: o% h1 Y- ?0 s0 M - pthread_cond_t pct;
5 k# D% ^- o, x5 ]' d6 [- E+ z3 K - int main(int argc, char* argv[])# \5 N' u7 l8 i- ]- [3 }, Q4 P
- {/ R# e, O) ^) N4 }# H6 s
- ...! e7 F" ]5 k. N
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);9 {0 k# _0 p/ P w
- pthread_mutex_init(&pmt , NULL);1 r- R6 K e0 `: A( p" q5 h+ N
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
% L4 V2 V% y" P - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
( G$ {, |0 Z+ `9 o$ N& ^ - ...4 b- E, p5 f( G9 s4 H% U
- while(1)
- `) s. H2 U3 B- P0 [" b- g - {
( J R% B0 n9 M% I - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);* V" p1 [! B! g1 ?+ Q) d9 z% r1 [
- ...) p' W$ o3 K0 {2 \1 u
- }4 r$ A/ p3 H4 Z7 Z7 J
- ...
3 b r1 x; H. q, J$ i - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">1 f9 m; S9 o& R/ Z* S; `0 w* m
- </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 y1 Y) v `2 b: V- Y
- "Server: MJPG-Streamer/0.2\r\n" \- X3 k; T9 c5 g# u
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
/ `+ N( [- {3 u) z - "Pragma: no-cache\r\n" \6 y: c6 I7 ]7 \/ C. F9 F% L
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
( T9 i5 A% _" W7 s - #define BOUNDARY "boundarydonotcross"
3 k! ]' H& |+ e$ w' K0 d - printf("preparing header\n");
) @0 ? [) W, b- @ - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
; L" L3 C% w0 v. S - "Access-Control-Allow-Origin: *\r\n" \
: O/ _ H$ h" s& C- |8 E4 g" u - STD_HEADER \
/ A: @3 ]% T* Y; u2 R - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
8 F% A& j( e/ c6 X+ b0 p - "\r\n" \5 A# c$ | ?2 s
- "--" BOUNDARY "\r\n");
# y& p4 T z* m3 w% b - if(write(fd, buffer, strlen(buffer)) < 0)
2 V9 ?2 l/ _/ L7 C9 R7 A8 V8 r - {
: ?+ f' v6 L' I: u& G - free(frame);
* S7 c2 j$ @/ F5 q4 f6 a; A P0 | - return;; S7 U6 e9 ^$ D b& b. p. 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" \, v- q0 x/ g+ z. {/ L
- "Content-Length: %d\r\n" \
5 K) k! ?* r5 \# ]! f# [ - "X-Timestamp: %d.%06d\r\n" \' R" A; u8 b* Z' X6 H
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
! ~) r: { [4 |. x - printf("sending intemdiate header\n");, }7 L2 O6 ~: C/ b3 ]7 F: v. k& G
- if(write(fd, buffer, strlen(buffer)) < 0)
: t% s& n( c9 {- p. j" g - break;% Z. D+ \4 G; h0 o: R
- printf("sending frame\n");
4 A b/ D- ]' j5 a0 | - if(write(fd, frame, frame_size) < 0)3 T1 t& A' [0 u6 J0 ~, H
- break;
+ y) q6 U* H/ M* B4 i }3 } - printf("sending boundary\n");
X" A2 d8 M! V9 C0 |5 H - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
6 \; P7 T" r8 O# p* D8 ]4 l9 S- b - if(write(fd, buffer, strlen(buffer)) < 0)
7 T, C8 h) c" o0 `, Y1 p5 \0 U - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  & `- f" q8 O, S j" R) P
 , T$ E/ ]! }% c' ]( l. v2 a, y5 [
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
8 K& m4 S( P: J4 x9 f5 r' y0 o4 \8 K9 f6 n- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
) J( a$ B; u5 Z6 M - {
" x: {: Y( n' `8 u - *socket_found = socket(AF_INET, SOCK_DGRAM, 0); k3 Z3 m. d1 N1 L5 f3 m3 v, M
- if(*socket_found == (~0))+ O. K1 u) ^1 F- U
- {& E2 B$ B' X3 O5 S
- printf("Create udp send socket failed!\n");
5 d( p5 ` k& K, L: C$ m - return -1;! I# ?! S+ C5 g/ v
- }$ C! {; Q. I& `; o
- addr->sin_family = AF_INET;# G# H/ u4 x! i) o2 q: D9 G
- addr->sin_addr.s_addr = inet_addr(ip);
4 P9 @) b1 n6 J" q5 P- t+ W2 N$ F - addr->sin_port = htons(port);
; L; w! B$ t5 Y/ _1 B. F - memset(addr->sin_zero, 0, 8);
+ l/ P8 y3 J$ f( j+ T2 r( } - return 0;4 O7 O0 b" v. `& n# f/ }' o
- }
复制代码 & H; r7 Z# D3 K6 o* z3 \
1 F) g- S: h1 N+ e" z
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
6 c3 p. S* x# g' T/ E C9 T- ~. X+ |8 M/ E7 w& D5 R* v3 g7 W
0 x5 Q2 L0 u* v1 l- while(fend > 0)+ ~2 {0 M3 `0 s
- {
! ^: J! X% @! t - memset(picture.data , 0 , sizeof(picture.data));
& H3 Y# Q' Y% M |; T - fread(picture.data , UDP_FRAME_LEN , 1, fp);
5 ~7 V( c5 U; B4 ^1 @6 W! h - if(fend >= UDP_FRAME_LEN)
6 D' x+ U3 T# Y/ M3 Q" ^, } - {
1 s% w, m' u2 V7 `, i# D+ b - picture.length = UDP_FRAME_LEN;' m2 Q$ \7 ^) a* y4 k; B* T
- picture.fin = 0;5 A( @! G9 J/ r: R
- }- q! l Z4 ^* u% Z% [# f" `7 K
- else7 r! o4 I' ~1 {
- {
4 P0 F. L1 c( Q1 _3 r7 d) ?( B/ Y - picture.length = fend;
! K6 @4 I# ?8 s$ j - picture.fin = 1;; b+ c0 O% P! ], U+ y6 h) \6 X- s: {
- }9 D' l- T7 S; E) ~+ g' W
- //printf("sendbytes = %d \n",sendbytes);
% \6 U5 Z0 [: e - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
: V0 F' \6 J- o' n+ z - if(sendbytes == -1)# [" W7 Q+ e/ {: U- S
- {% X; D2 A. w, C U" b' k* j
- printf("Send Picture Failed!d\n");
~& ~+ z9 C L! S - return -1;1 g( [5 G2 { e3 M
- }* J, W, F. u# M4 m2 `% `
- else
0 D+ o9 r8 C7 {3 K$ p3 j - {9 |# [2 v1 M# Y
- fend -= UDP_FRAME_LEN;
g3 V; o& ~+ U% Z7 t; y2 |! u - }
0 G2 D9 ?/ @: B0 ^( z - }
复制代码 $ F0 H. {: ?% C) K, h M/ t0 ~ n
9 [' b' \6 d N7 l, \

; x/ h2 i9 T0 x7 \3 ?/ r8 F/ Z( ^# {5 I& P3 x% P; Z8 @
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |