本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 . N# m* h! h4 [3 G8 a8 v# L
1 I4 P7 ~ C. t J, W3 J
6 A1 M' N' h5 Q! k2 l作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
1 Z3 }! B: r# Z, t5 f K! ]" p) Z
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 7 k( C3 V$ f0 Q; R3 D) g4 U+ @6 M8 ]
 / b* o- C }# v& k! @- x- ]) u. r
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 1 e8 ]2 @: }( z; \' z; E! j* P
一、HTTP网页服务器/ T0 L2 j% Z( p
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)6 ]8 v- W' i6 a* u
- {
3 i3 ?/ N: W4 n2 P - struct sockaddr_in servaddr;" j, Z, C# y$ a6 b
- socklen_t addrsize = sizeof(struct sockaddr);- ]( E W; S, V6 T3 I2 ~5 u1 g
- bzero(&servaddr , sizeof(servaddr));1 S0 o8 s* r6 j# Q
- servaddr.sin_family = AF_INET;
& P( P9 }. H4 B - servaddr.sin_addr.s_addr = inet_addr(ip);) [. r& b0 X; W! W3 P4 ~0 Q( E
- servaddr.sin_port = htons(port);0 k+ A: C$ k0 ?0 C% h( z+ O" d
- int ret;% }# E) z' p! j/ z% Y' ?
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)! _5 u; w" J+ i0 f, y( w
- {9 w) n5 ?, y4 Z4 R% N; S" Y
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
$ T8 C/ J2 |4 G" l+ P - return -1;
9 p- L7 {7 X: z# R- `9 r - }# Q! \) l0 i. \; Q5 B- {, W
- int on = 1;
6 |( b2 \. J+ t8 g - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
c/ K' u4 a0 d/ G6 V( {- \ - {
8 V( n8 H: S7 h+ f# d - printf("setsockopt error\n");
1 Z) b' J+ \ w2 h1 k1 | - }
+ J, T) C2 S0 N, ]+ ? - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
8 b ^5 L# J7 l* e6 T - if(ret == -1)
L4 g) e, E+ |9 ]) p& e1 s+ p3 D - {% E& o8 n4 Q. K. p& y
- printf("Tcp bind faiLED!\n"); ]* _1 y) B) H7 }! ~
- return -1;0 b8 `& M% b3 v
- }
: O8 j; x! A: k/ }1 d/ ~ - if(listen(*socket_found , 5) == -1)( C# L2 z3 E0 D2 v" e: C3 r( Q) N
- {. H* X2 q7 P, e
- printf("Listen failed!\n");
, W4 j' @: @2 z5 D" z - return -1;
. X0 P7 `; e0 G4 Z! c - }& K/ e; n* q: \! g6 S( O8 w8 ]
- return 0;5 S/ d' b3 o5 z4 Y/ a
- }
复制代码其中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 [& U% H3 l5 ^ - void * Thread_TCP_Web_Recv(void *arg)2 W! v- `% i( [% X" Z& a" x
- {2 }7 l) i1 @9 v! Y
- 。。。
* }, E- [4 T5 j3 h - while(1)7 d" D: M$ {7 i% Y
- {
, V; _+ r9 J1 z - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
7 j% J: h" m! j, Q - printf("fd_socket_conn = accept()\n");
4 \( W" y. }5 ]2 L( V0 ] - 。。。. m4 q& \; h, X( j9 _1 e/ N
- recv(fd_socket_conn , recvbuf , 1000 , 0);
0 _ x% N+ M. {0 k& m# o - }% X4 C! q' |& M( Z+ R
- 。。。
- m( |/ r) G* e) U1 \& h4 p8 L* G - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);& b4 ]7 P4 L; ]
- pic_tmpbuffer = pic.tmpbuffer;6 q& a$ F) F! p# i1 c* k) ?
- pic.tmpbytesused = buff.bytesused;5 t! j, m( [& [: H
- pic_tmpbytesused = pic.tmpbytesused;( I+ C+ y2 d! U# ^, c2 L
- pthread_cond_broadcast(&pct);. _) x: ]) q! S' g; L
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;$ k: [) m& `1 W
- pthread_cond_t pct;
* B$ q0 n9 ~6 T" s - int main(int argc, char* argv[])
9 e- V- f6 C8 M8 s8 m V - {' j% u1 {' X7 Z. I" s* j
- ...
3 x( _" W( {/ |& ]/ ? - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
; D) x4 |/ k$ a* D - pthread_mutex_init(&pmt , NULL);
) V x [4 J) M# A. p - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
- z1 n' ^! M& W8 c$ j - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);3 v6 ~4 i9 \1 K* s
- ...
q" }7 C c, ]' g$ N - while(1)
9 w$ p8 G' v$ ? - {' u& b8 D; r$ V9 r# m- H
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
6 [8 g* F! W9 m1 Z3 D. S - ...) V# W" }( d) h( o) }
- }' X( y; w2 E4 k4 O' t4 M* A
- ...' w& w- T7 {6 V1 n( ^
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
( M5 Z+ x- d' X0 e; Y4 h! s! s - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \2 H8 A6 f W" Z$ ~: @( q, _; L5 U
- "Server: MJPG-Streamer/0.2\r\n" \
/ ?+ a& w2 ?5 r/ a; H- p2 J - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
- p( E& g$ x' t; \# P3 m& G1 b! X - "Pragma: no-cache\r\n" \2 d# r+ _. j0 q7 Y% N
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
1 f/ U2 o, u: g$ V - #define BOUNDARY "boundarydonotcross"
2 O" o6 d& i1 Z9 j - printf("preparing header\n");
`7 S7 ~- }* v4 a$ _! K - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \# t& W/ x' [$ ]3 M. i% h" D% s
- "Access-Control-Allow-Origin: *\r\n" \
% U3 s. s/ U) [% K% M - STD_HEADER \
2 P, j7 `; B' [5 ?& ] - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \+ P7 p' Q" ]0 |+ `, @7 U
- "\r\n" \
) ~" n2 h P: U3 h: O! O6 y - "--" BOUNDARY "\r\n");# u d; M! @# K' \. y: k
- if(write(fd, buffer, strlen(buffer)) < 0)
$ ?7 _/ Z! p0 ]1 M - {
' X4 n) N8 t9 p& R# `0 w - free(frame);
% v* w! X/ t( P/ w7 q - return;
& l! v! T, ?1 S - }
复制代码发送完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 q0 N) l/ I2 `0 i. {/ o$ c. C! {9 { - "Content-Length: %d\r\n" \) q3 e2 Y/ y" e. t2 c" F
- "X-Timestamp: %d.%06d\r\n" \7 a! R8 B' H$ V$ q+ p4 K6 x
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
5 ~+ i( P1 b0 \" D4 g - printf("sending intemdiate header\n");
% @' ^9 g$ M6 Y1 k; a4 @ - if(write(fd, buffer, strlen(buffer)) < 0)- h9 |3 g; B6 `3 I2 e! s
- break;6 t4 ^9 a' _/ W6 ^. C
- printf("sending frame\n");3 |" d( N2 S" a* R' N* b4 u
- if(write(fd, frame, frame_size) < 0)
8 j* T. L" w# d - break;2 ~1 O" Z$ Y( r4 i/ X
- printf("sending boundary\n");
; I4 F( M: Z( e - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");/ q/ P2 j! U2 b k. x2 i
- if(write(fd, buffer, strlen(buffer)) < 0)
, a2 t+ R, F x7 o q5 Z/ @$ V - 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/ \! x0 T9 j f' m
) s! i4 T. v. n0 k二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
$ t4 D( I2 F5 o/ \$ {* L. R& y) c- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
5 v; h- D. A u5 f, B - {
4 m5 x& b/ J8 j% z: a0 I) L - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
1 w- \* P H1 ^/ e8 _% U( U: h - if(*socket_found == (~0))" _( f3 s Z& ~. D2 N v, R
- { m; C7 _3 T1 N) c; Z1 Z$ q
- printf("Create udp send socket failed!\n");
5 K/ D3 j6 D1 ~9 a: u, z9 N - return -1;( ~6 ]4 `+ Y2 p6 j. O
- }
2 e, j! I2 ^) s2 q% O1 i6 N - addr->sin_family = AF_INET;( G7 y1 n1 I7 w9 s* K- d! h( H
- addr->sin_addr.s_addr = inet_addr(ip);
. {9 D5 |7 r3 {+ o+ L - addr->sin_port = htons(port); c& W2 h0 O. x) g7 {$ e
- memset(addr->sin_zero, 0, 8);
( b5 |3 `( Q' v4 i" k1 P - return 0;) D6 ~% p3 \3 D
- }
复制代码 1 g6 V7 l& r* K6 ?6 J6 K& A* m c
) u. o0 q* W1 s7 w而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
0 w/ G7 Y: k, t0 f* A1 _" ~! {
, M9 z, g- v; t+ z1 L" q
8 }4 \6 s# p) d, u- K7 O- while(fend > 0)9 l" F; g+ M5 n+ Y
- {; J5 B' I7 V$ v2 P" {
- memset(picture.data , 0 , sizeof(picture.data));: C! T% y1 e- I2 a2 R& S4 o+ _
- fread(picture.data , UDP_FRAME_LEN , 1, fp);) n. |2 k* ]1 b1 y$ l- \) {
- if(fend >= UDP_FRAME_LEN)
M# _* V9 Z0 u3 x# b - {( T# ^3 k; Q/ G O5 H4 I2 y t
- picture.length = UDP_FRAME_LEN;
, X1 a) ?# Y: C( I - picture.fin = 0;( v( J* ^9 ?7 S& r
- }& X# ^: Q* K2 J8 U) O8 Y. p
- else
/ k, h2 a- J' _# s& T) Z8 n - {
; Q; Y; V( l% z - picture.length = fend;* x6 |4 A, ]$ {# K, W# a
- picture.fin = 1;
# I' ^8 e8 m! [ - }
) ^" r: f# q2 h) D - //printf("sendbytes = %d \n",sendbytes);
* f4 x7 R- W0 G) q! p6 u - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);2 r7 e/ M: R) R) m/ ]4 e
- if(sendbytes == -1)* {- H1 n. V) R3 d. Q
- {8 k; M6 \) T, y
- printf("Send Picture Failed!d\n");
2 a6 f/ n% { V - return -1;
: C$ O4 s& ?9 L& Z7 ~( G0 L - }" w8 a0 B. `! t; _
- else. _8 M4 k" j4 P, t
- {% [, L% P1 b; j, |3 \7 w
- fend -= UDP_FRAME_LEN;
" c! D' }/ |) z - }
! L6 c4 M1 `: M4 N; P% {( l - }
复制代码 " K* H9 P1 ^3 T- a0 |
g! T3 [( z6 v! T" L . r: l P/ v- f! \
+ N7 a) u9 c1 HiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |