本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
. H) x- v* Q! d: |4 P8 {9 ^ ?0 g) D! `
 * T& A2 A, T5 ^4 O* T
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html& c5 e% C5 i1 ~6 C# p
, a* i; j. k) |8 [7 p+ ?
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
0 o3 X# g- l8 w4 O( C' f: q1 T! X ) R: J' i" n! @/ h
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
0 p0 d4 h6 a- C一、HTTP网页服务器7 h6 {" L) d4 z) W7 o
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
; |7 l6 G9 J( U7 `; h; g% ~ - {6 k$ Y( t" v8 e% _
- struct sockaddr_in servaddr;
8 u9 w8 w, H* O/ @) [ - socklen_t addrsize = sizeof(struct sockaddr);; A) A% t4 E! }8 _; f3 a0 a4 l
- bzero(&servaddr , sizeof(servaddr));, V2 Z) j2 D! {# q* H3 E g
- servaddr.sin_family = AF_INET;( o; v3 {& z, i
- servaddr.sin_addr.s_addr = inet_addr(ip); v% l( I* ^8 P: W. F' o2 L \
- servaddr.sin_port = htons(port);* v, _+ N) B2 n- X$ F# p. U, V
- int ret;- K7 F3 p6 p* N
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)2 `$ p( p. _! G1 {: ^0 N/ `
- {
, ^# i }( o/ @ - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
7 k! w: t6 o' U/ G1 H4 q/ G - return -1;" }0 @ S2 }6 P- B
- }& j, I8 L2 G! k8 R5 D* T
- int on = 1;, A P' C* Q! @8 `
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
7 k5 Y" [; C" O - {9 n8 o5 e! W3 }9 G W
- printf("setsockopt error\n");
' J) q3 j. }% w4 \+ y - }
, Q4 k; [- _7 i) o) m* v8 d - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
1 l/ _+ Q, E# ~1 | X$ E - if(ret == -1)% [" A! ]0 T- z8 o1 W
- {
1 E" i- u$ ^! q: S- E$ C - printf("Tcp bind faiLED!\n");; M% ?6 V/ n8 l$ @! T. p/ k
- return -1;9 H9 n' u! n, |; P- h' S/ x6 g
- }
' k" }( z! T) |4 l e" P, c5 K/ y1 F - if(listen(*socket_found , 5) == -1)
& q/ H y( a8 C/ J% B5 L$ U - {
# W4 x8 F+ ~! T# \2 N - printf("Listen failed!\n");
8 N: H* S( T% D t/ K6 | - return -1;( l; {, j( p6 e
- }8 l3 l9 D% ?# {5 {: G7 b! e
- return 0;. C/ i! Z$ W2 n2 S" \
- }
复制代码其中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);/ j+ D' w2 L& `1 a9 O& E1 {" w
- void * Thread_TCP_Web_Recv(void *arg)0 P5 P7 S# A4 y }! m( E6 Q8 k
- {- K( j2 U {+ {$ n& N9 d
- 。。。
- a0 f3 j! Y6 ~9 H2 w/ A; _6 l8 L2 x - while(1)$ s: z; h+ e0 I) Y( g3 P
- {
% z' u J% K) { - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);9 O% [) K" G" [8 L. `, a! {
- printf("fd_socket_conn = accept()\n");
2 k4 k, _3 y& @+ ^0 P - 。。。
( s) M- z( H- P4 r0 c - recv(fd_socket_conn , recvbuf , 1000 , 0);& _3 M! {% V; w. _# w
- }
6 s% l+ u3 f& p# M - 。。。
0 {6 r/ |: o; T! C8 Z7 k - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
+ h& v0 T0 K/ | - pic_tmpbuffer = pic.tmpbuffer;
9 R# _% e1 _( c2 X( P8 b; Z - pic.tmpbytesused = buff.bytesused;
! B# o2 t% ~5 [1 D+ e - pic_tmpbytesused = pic.tmpbytesused;. T" x: c( y) K1 `5 _. N
- pthread_cond_broadcast(&pct);
$ y" G1 _+ L; j0 k" ~% k - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;$ W0 T" t% p( W
- pthread_cond_t pct;
) v# t# w) \. R - int main(int argc, char* argv[])( b6 l2 L" E5 Z% l% M
- {: s- _" {/ R3 ~9 }: [# S, e
- ...7 l# ?8 C0 m; m# b1 j x! f
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);4 f; a1 U9 L/ t6 F5 y# d% I
- pthread_mutex_init(&pmt , NULL);; a) ?* D8 s0 B6 B
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
3 `& Q* ^9 i) |' P$ `* a, @. e8 v - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
& Q4 C' x# v ]0 L: B1 r - ...
5 g- x$ q0 ]- c/ K, d* O) a - while(1)9 x1 C; x9 t$ V0 a# g9 h
- {
- H: b& j( @+ i - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
+ o1 c7 ^1 o; j8 l) C - ...
0 v7 H, @4 _/ x& z6 l7 { - }3 t2 L& `6 q% D5 e Q+ T9 Q
- ...
! c! m e; P% n( t# P2 n% | - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
0 G7 ^ ?8 z, x& l4 g! ~ - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
* c" n6 b( u# ^! @1 L/ I4 D9 J) @ - "Server: MJPG-Streamer/0.2\r\n" \9 N% A% d5 Q" [& a7 l5 {
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
- _" N5 f3 J: b - "Pragma: no-cache\r\n" \
/ ]" {* R1 h# I" e) J - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
; l* X+ `7 ` Z/ P) F - #define BOUNDARY "boundarydonotcross"
2 V% \7 o$ S- U' f - printf("preparing header\n");
: d. I r1 h! g9 C0 s1 G - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
# L3 ]# B/ ~( x0 b+ ~ - "Access-Control-Allow-Origin: *\r\n" \) |2 P+ S4 w% M3 N3 z6 \3 ]
- STD_HEADER \
) U, V6 J9 F m# R# t8 Q4 a. w - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
) I/ |) q7 ] P% |) @# g2 `/ c - "\r\n" \/ ?3 u6 q3 D, O; @2 y: G, U. e
- "--" BOUNDARY "\r\n");
1 m- Y/ P7 _% n5 x5 R - if(write(fd, buffer, strlen(buffer)) < 0)
. X: L# ^+ ?3 w- f! [ - {8 B3 `) d' g, G" {' Y2 A3 b
- free(frame);
' E- L6 Z4 m2 p7 X: d7 [4 t' O - return;2 S# y5 S- a+ j5 k; G7 r
- }
复制代码发送完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" \
, q' }2 [5 \ O+ B0 L1 o) h - "Content-Length: %d\r\n" \$ Z0 y# }; X/ g8 x% ~
- "X-Timestamp: %d.%06d\r\n" \( X4 ?0 r+ T9 w% j! z3 C7 V; }; i" f
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);2 |0 T, N; a1 S0 i
- printf("sending intemdiate header\n");1 } o8 P# T9 F/ ?# @
- if(write(fd, buffer, strlen(buffer)) < 0)
8 x' t$ _. ^$ B4 W$ Y9 v4 ~ - break;4 _* x/ x% H6 D6 M2 o
- printf("sending frame\n");
- ?* Y5 N" v& J2 I/ E6 r - if(write(fd, frame, frame_size) < 0) w3 n E9 ^, d3 H. G* t, N3 X3 t
- break;
- L7 n, M" h5 K' ^4 e2 c8 Z. L - printf("sending boundary\n");0 N& R4 p! Z1 T$ B" q
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");9 v4 D$ m% X* v5 P; t
- if(write(fd, buffer, strlen(buffer)) < 0)2 c. s: K0 F. T' ?% t8 S3 ^: q+ N* y
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  # i# ]1 A' P* k
 , d" O* f8 J/ |( F3 ~% d
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
- j8 a* Q3 x' K0 d- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)6 @* A- }" k* [$ t# L
- {
a8 G. ^; R& h3 x - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);" n/ j1 F6 V6 V/ B1 _9 u9 J/ s& a
- if(*socket_found == (~0))+ D7 c. ^; y) q5 n+ g% C) d
- {
L9 d3 T1 i% F$ J* \ - printf("Create udp send socket failed!\n");, v* d! c9 j* X# M: ~) v$ M4 V
- return -1;
; @; ?2 W( ^! a4 L4 O - }
1 C- L' D0 r P; Z } - addr->sin_family = AF_INET;
/ h0 q: v, k7 Y: K, M7 I9 m# V - addr->sin_addr.s_addr = inet_addr(ip);5 h+ c* I! M* d0 Z1 n
- addr->sin_port = htons(port);
6 J$ r9 c. b$ `' y% B# s - memset(addr->sin_zero, 0, 8);; z7 L I1 l5 M2 H A8 I$ ^* w2 j
- return 0;9 ?+ t8 R x- q
- }
复制代码 - s% s7 R& E: d, \8 `0 O6 d( N$ @
3 e8 b( k8 W& b. s% o- u
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
" q9 H3 ]2 a) @/ V; K) Y) ~4 G
# N0 H, p( e. D; _
2 n% ^" _7 i1 N0 X- while(fend > 0)
) F& b( S6 v3 A; {5 k - {$ Z$ @" u: B- b- l! Z. I5 F
- memset(picture.data , 0 , sizeof(picture.data));5 S) n( T. v2 y0 p' W4 ?( S2 k
- fread(picture.data , UDP_FRAME_LEN , 1, fp);1 k6 K, F3 C3 y M" K; L& l0 R
- if(fend >= UDP_FRAME_LEN)" ~8 f( ]) d# B7 m* V$ F
- {
$ m+ w& @' F# {5 E& i - picture.length = UDP_FRAME_LEN;
5 I' A6 ~& E1 s" R- \6 v3 ]4 E - picture.fin = 0;
& ]+ l4 J o/ P% Z5 K - }
$ ?- M7 L0 F+ T5 J2 h# W3 l - else$ a0 s3 q* v! k7 g/ [1 }! U
- {
/ f7 v7 z* r1 x: A+ A0 H - picture.length = fend;% A7 J/ _4 m$ G+ o- i, U
- picture.fin = 1;
- B4 d( Y' [. w( s9 l8 G, M - }
- p8 F6 G" ?4 e9 L1 P - //printf("sendbytes = %d \n",sendbytes);
9 r" v+ d% q9 ^+ f - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);, q1 W3 S K J6 c
- if(sendbytes == -1)8 |* p N' ~2 c/ m5 _& C
- {% w) B7 T' f& q& Q; ^ Q; r' H
- printf("Send Picture Failed!d\n");1 ~* E! Q9 K2 a# a9 Z Y4 J: D
- return -1;
) Z+ M. p2 c- O% s6 x - }9 m0 A, y% h& b
- else4 X0 N0 u- k: b$ {- d, m
- {. k- c0 K& o+ p
- fend -= UDP_FRAME_LEN;
9 @7 y+ Z/ B2 H7 ]7 _. c - }6 j* {( N5 M9 c: S/ I: f
- }
复制代码 . V* M" p" Q3 Y" H( d
4 f! s) e% R" l0 T7 O

: N$ r0 p$ i, a- Q, t$ L+ i
* F: \3 }4 X) diMX8MPlus 核心板: https://www.forlinx.com/product/136.html |