本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
: @0 v; z) a% k% P5 j& \. ^5 {3 Q- r4 [9 j& H
 & f4 v1 I$ ~' \4 H4 o4 s, D' d* U) s
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
4 V/ k, `1 G, M6 X
- }& q* d1 g1 G' z. }# p* M本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
9 P' v# t& N+ A0 h0 h - B4 {( C9 D3 s" b. z) [- L- r
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 7 c. S' @2 S: K: ?5 @5 ?; q' ?, o
一、HTTP网页服务器7 u% F1 U2 h/ I9 r% ?0 F w+ f
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)$ Y0 T3 s" O8 u) E, n& O
- {0 O3 v& ~( c( p5 t& q8 e
- struct sockaddr_in servaddr;
5 W% v" c# n: P( j" {# i - socklen_t addrsize = sizeof(struct sockaddr);# S( F* J0 {. O- E
- bzero(&servaddr , sizeof(servaddr));( n5 [, I: P2 q( x. B) ^& s, E
- servaddr.sin_family = AF_INET;) k; u/ f: t6 M9 r; B; V) r5 L* {
- servaddr.sin_addr.s_addr = inet_addr(ip);
8 A% ?, g* L/ m* o - servaddr.sin_port = htons(port);, M: |8 c, c% y" F: k% U
- int ret;
1 ]+ Q$ L* \: K [6 q - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
3 S2 ?6 H. ]2 c - {
8 x$ A& j1 n0 M# r" H: x& ~ - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
& v/ ]% u! z& o" t - return -1;
; l; g# P) l3 a3 G6 i7 L - }
% |/ G( J0 x- S2 I$ Y9 p% r( n - int on = 1;, W( ?) P# T7 S7 r& P8 m
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)5 }) R5 e# k; h9 _. X: j
- {8 V" i v3 f4 k$ V
- printf("setsockopt error\n");1 W/ W7 c( m" D! Y k
- }* o* A* }3 R, s- D B0 V4 J5 ~
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);( z5 j; S8 N, z- W* g5 S# l
- if(ret == -1)
6 r: M2 f2 s! T9 D2 p: J" ^ - {4 {0 \) ~+ E5 F* ?1 ^1 v
- printf("Tcp bind faiLED!\n");) r$ }1 I0 g$ x8 d9 c+ Q5 b" G6 W
- return -1;! t$ o+ o6 p+ ^' J5 ^3 ^6 ~% M! v) u4 y
- }& s; V. B' o) P8 T1 X
- if(listen(*socket_found , 5) == -1)
) C) V! M) @; k1 ~6 p# x - {) e. ?5 _* N8 c3 H5 w1 B
- printf("Listen failed!\n");8 c3 r# l) `; C: m
- return -1; |9 O4 o4 u: T1 k4 p
- }1 I, c6 e( F# @' N$ O" |
- return 0;
0 h$ r- r! t8 t7 h) g. q - }
复制代码其中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);) r. U% g; ^) z) z. _# r
- void * Thread_TCP_Web_Recv(void *arg)
2 ?/ N. N0 F9 |' p- s - {$ i) b; b: w% i$ s. X) d2 w. B! {
- 。。。
9 p3 b* y. r+ [; E6 v - while(1)
4 N8 r9 S7 H K6 K! T7 g - {
9 H% j7 a a, I, u4 i4 i - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
% I' ~8 s7 }" w' r$ k4 x. A - printf("fd_socket_conn = accept()\n");- B/ ?; r" K) }3 w
- 。。。
# W! @3 Q" I# w; ?6 R B - recv(fd_socket_conn , recvbuf , 1000 , 0);
, \( ]. T& ?! D3 w1 R - }- o/ d6 [7 ^" U3 U1 h0 h
- 。。。. P2 C, |. `! Q, @( T4 S7 W/ q' k5 b
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);+ X6 Y3 i# s/ @% m; r* D5 o
- pic_tmpbuffer = pic.tmpbuffer;8 A* H% X0 T6 [) F
- pic.tmpbytesused = buff.bytesused;
+ S8 S+ k+ g. T/ B' H - pic_tmpbytesused = pic.tmpbytesused;
. ^( c. F6 p* t - pthread_cond_broadcast(&pct);
4 w* G, X/ u8 h3 i7 j - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
6 X. ]* i: G) I! f5 M - pthread_cond_t pct;; C0 v9 Z' s; Y
- int main(int argc, char* argv[])
* T& w7 {+ D8 S3 V2 I7 r - {* a7 ?) N7 t2 w2 T# R* `
- ...# _1 k4 i: X# `8 R& S- X! h
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
% Q ]9 K6 ^- M! C' J - pthread_mutex_init(&pmt , NULL);
$ Z+ W* V# T; C# ~+ D% \" x$ e; O1 r - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
, R9 f4 b( p$ `2 h, |) Y( L8 _ - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);) X6 M: d( |* a2 }, c' L+ s
- ...
/ I3 @: G5 G5 p5 `/ s - while(1)1 d6 y8 n! F4 X2 x! f9 \" \
- {
5 p8 \' r% T2 p) `- I, J' g/ X- C' X2 @- | - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);# a5 y* s0 | a+ v
- ...- k- c+ o' A& }$ ]2 G. I* V' ^ [
- }
% p1 S# P4 K) z/ T - .... l( _- @# O2 T( A1 Z7 R/ J; d
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">9 k1 T9 L7 T) b' o# p6 i
- </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 l7 ]: h8 _7 r9 U+ I: R; [ - "Server: MJPG-Streamer/0.2\r\n" \
! H1 Y6 S: B) \+ n$ L% E5 g: B' k - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \4 z6 @( `3 |8 {8 y: w8 X/ n# n
- "Pragma: no-cache\r\n" \. ^: J. B) d2 l
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
' p* H6 Z: S {& } - #define BOUNDARY "boundarydonotcross"% r# y. L% m4 H
- printf("preparing header\n");
) _2 Q& D& h1 T W: U - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \5 I& Q0 p, y4 F8 k- a* r
- "Access-Control-Allow-Origin: *\r\n" \8 L/ o- Y. {2 ` W4 c/ @; H
- STD_HEADER \" l; f. h% [$ \- i) j+ j7 v
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
9 }* R- X. g) W8 Y* l - "\r\n" \* F; e6 U4 |* K
- "--" BOUNDARY "\r\n");
4 O! \( v2 R5 [; ]$ W3 Q0 ]6 Y8 f - if(write(fd, buffer, strlen(buffer)) < 0)/ K$ k- [ \6 s4 t; Y9 B
- {
{4 U- [1 L7 Y% o1 U - free(frame);
& I4 j' R9 v8 n0 j/ K - return;
; D4 R4 l: @* q# n - }
复制代码发送完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" \
/ q9 L4 i8 J! s" D- Y: h - "Content-Length: %d\r\n" \
" T" _/ i8 O; I$ m - "X-Timestamp: %d.%06d\r\n" \
2 H3 U1 @- P) t! @, Z" P - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);: U0 o1 H- _0 B1 n* v4 s! E) n
- printf("sending intemdiate header\n");6 s( \) w' Z; n; i. a# V
- if(write(fd, buffer, strlen(buffer)) < 0)# K* z m! g* Y$ o
- break;
( f9 K. \# ` f% |; u! w - printf("sending frame\n");) J |! C! F* \+ x8 h( V! t- ^
- if(write(fd, frame, frame_size) < 0)# G0 M3 [. ^" P% v5 Y9 P. W
- break;
) e& N4 k9 [; j0 e2 Y( N6 i - printf("sending boundary\n");, R6 J0 {9 ~- e8 q' D& e: V
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");$ }. D% p3 y2 x5 c
- if(write(fd, buffer, strlen(buffer)) < 0)
; i8 b: ]; l" z8 _6 R/ {9 D4 i; z - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
* b+ N, ^7 O2 x3 }$ i! J/ W$ j( ] + O& C/ ^7 U; Y) U
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
5 Y3 [) S* q) S# D3 L, {- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
5 N. R4 n8 D) p; c - {
" p4 F1 x7 A" E - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);1 y3 [$ e7 P, L
- if(*socket_found == (~0))
* m+ \ j- L. p7 F9 e# y) N2 \) a - {* t6 \ {$ U8 w7 J
- printf("Create udp send socket failed!\n");
, k1 y) W. A0 o! H A! D: V7 C9 H - return -1;
5 W8 g% [% s7 e1 ?6 l& ]9 i- z - }
) o) g" c0 o. N - addr->sin_family = AF_INET;! I: g. R( P: e6 ~! h3 F1 d" \
- addr->sin_addr.s_addr = inet_addr(ip);) H" s* w5 K, X; `+ E% Y" G
- addr->sin_port = htons(port);
+ E9 C& G! i& O- f$ p - memset(addr->sin_zero, 0, 8);3 Q* k4 h, b. K/ A
- return 0;* I8 ]7 S0 u0 D/ v& R( V
- }
复制代码 " \+ e' _' Q4 E2 i: v% z) B4 I
3 N$ h4 b" ?$ O而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
+ {/ h3 @. x9 w9 P3 g
6 X: x% Z& t8 W# V0 P( b5 r3 n, {7 I. y# C } b
- while(fend > 0); ]: G* v: t$ p M' s6 d4 ^4 x5 V
- {9 s$ E( Q9 z; m* T- |
- memset(picture.data , 0 , sizeof(picture.data));
( j4 ~1 N% D1 z1 ~' Z& \- M8 x - fread(picture.data , UDP_FRAME_LEN , 1, fp);9 G, j# s7 C* _% }: V4 W/ I
- if(fend >= UDP_FRAME_LEN)) {9 @* u+ |# I% Z2 r; d; E; Y
- {
% R" m' y) F! `4 k) M" t, o( G - picture.length = UDP_FRAME_LEN;$ k; p3 Z/ h" d8 b( t( I D" @
- picture.fin = 0;
6 ?$ i. ]! F2 |# e: E- P - }
2 r3 p4 F R6 X7 u5 t - else
6 W3 _1 g0 {& I) |6 [+ O& ] - {
. B- M4 X3 n- z - picture.length = fend;7 Y/ h' G! U" g% N! m
- picture.fin = 1;
% _4 O5 S w. K! q: G - }
9 e p2 P. [- }. F. M1 E8 k - //printf("sendbytes = %d \n",sendbytes);
( E* x: F: ~8 E6 T F- j* G+ w - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);3 b$ q' k6 n, _8 X/ S
- if(sendbytes == -1), U% C& w* T" q( {# m. r( v# Z4 `3 q
- {
( v; p! q8 ^' A/ V/ g$ ?5 l - printf("Send Picture Failed!d\n");
4 g- O8 C5 A! v - return -1;
) n( n$ k! j, ]2 |& w+ { - }' I M3 ~: O4 |% |" j# ^8 Q
- else1 ~ Q0 P4 |( [" v% v
- {4 d9 A/ D3 a7 s3 R5 q! m
- fend -= UDP_FRAME_LEN;
1 ?# J, b+ L* ~( ] - }7 C9 u* Q# C3 u, L1 r
- }
复制代码
6 e0 b# A$ e6 R8 I6 k# ^
2 p" @0 W% G# e) v g6 q$ ?- M* v ) Q# m( [- r1 W
9 a* C3 l. ^. i/ ~- q* A& x: H- t- Z+ u( y" kiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |