本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
9 t; f2 C( F' e
2 |* { T) s) ] - E& l# R& F% z+ _2 j v4 q, v' p# {
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
" S. a) d+ X- }# m
0 [! P# u9 D4 V2 _5 s3 P$ m本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
* h. j! |9 c1 p* P, s) e2 x$ c
$ T0 B; M+ v- ~% F$ V% G3 @" d两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 $ i0 }) @, D% v1 f+ ]9 W
一、HTTP网页服务器
- v- `, J' y2 E& p7 K7 ?先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
7 ?( z* l, r J3 A - {/ ~& F( v/ b5 K3 p4 \! I- n
- struct sockaddr_in servaddr;) m0 F; Z: t" U. I3 b% ^0 c
- socklen_t addrsize = sizeof(struct sockaddr);. S, ]' O& P- n, ~% C
- bzero(&servaddr , sizeof(servaddr));
' v/ X @9 H) R5 U. i# {# w3 K3 G9 _ - servaddr.sin_family = AF_INET;
. E* F. K! j. _; Z! S" m - servaddr.sin_addr.s_addr = inet_addr(ip);9 Q* R) j* d$ T+ N2 X) ]
- servaddr.sin_port = htons(port);8 Y% x, C8 l$ Q% X2 ?3 w
- int ret;
2 s! a) R% i. j# g% ~ - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
) R- g5 }) Y3 j# v. N( J+ ?1 L# k - {. ]: A# G, G% ]/ H5 f0 N& G
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);8 Q3 e- B) ^; h
- return -1;
5 h R% M( V4 Z& j1 J - }* E3 s& i/ j" [( u% F: ~+ w
- int on = 1;
6 |$ S: I0 S9 _$ k7 H - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
" x( s$ O2 x" I" D e - {
) X9 H& |; W' X( ^+ E - printf("setsockopt error\n");: r& v: u$ E9 f0 C7 ], A* z. _& R# `( S
- }$ ]% L6 e8 U" I0 E. j0 R% S
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);( Z. E. `* y9 [: p0 K4 G! j
- if(ret == -1)+ O; W+ X% P6 Q) _* k, H \1 m4 m
- {
2 f( n7 `% U+ d3 `" f$ b - printf("Tcp bind faiLED!\n");
. A; n" n) y; D, {) F+ R% e3 l8 W - return -1;5 l! Y1 l, M* N( [* Z
- }( W) N4 _: w4 [" @
- if(listen(*socket_found , 5) == -1)
2 E; I: P3 t7 J# Z- U3 Q - {% U% @- }; V% l
- printf("Listen failed!\n");1 }/ L/ `) N/ `& x7 F% d
- return -1;
/ `5 C L" I" F2 h - }
% [; v2 p/ x7 |2 f - return 0;* C1 X& _" Z- _4 c, y) z
- }
复制代码其中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);' p) {: p- V+ i" I% N0 V& p# e
- void * Thread_TCP_Web_Recv(void *arg)1 f. |. y0 j5 F3 n+ h) X
- {4 Z+ g0 _" {% e3 t0 ?6 T$ p0 Y
- 。。。/ k- L) X, C0 Z/ I' b
- while(1)
- V) D+ U( |6 p! a1 R, c3 D - {$ {) f6 D. ~2 {. ~: m
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);3 J) ?' x6 ~) e" C& a( j/ U; `2 Y
- printf("fd_socket_conn = accept()\n");
9 @/ e% Q* }$ F' K* v - 。。。
7 M$ v3 @ t: u1 D) U - recv(fd_socket_conn , recvbuf , 1000 , 0);
+ s& O7 j6 R+ ~ - }
9 V b# m, M' w8 V. s - 。。。4 F6 Z" J) Z6 D6 g8 b6 i
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);: b3 \9 x* N. f2 ?
- pic_tmpbuffer = pic.tmpbuffer;
( ]9 L" B" I, J& x$ ?9 v+ n - pic.tmpbytesused = buff.bytesused;
0 C6 G* ~" {$ t3 U$ e" r4 ^ - pic_tmpbytesused = pic.tmpbytesused;
# O% [) J0 I$ i4 A - pthread_cond_broadcast(&pct);; m! }& E5 Q1 O5 ]5 ?5 M9 o
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
, C& c; u# R' f1 m& u - pthread_cond_t pct;7 [5 ?( L" | P
- int main(int argc, char* argv[])/ d6 `! c2 z) R
- {
! y1 B+ ?% s) H. I5 z, R1 l. E, T - ...% |! j4 T" L! P. L8 R! o! v* b
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
e- s* S6 i' |( t7 T$ k1 W - pthread_mutex_init(&pmt , NULL);
8 {+ G" E$ Q: m; L" o - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
8 p8 f4 z% ^$ [ K - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
5 g8 ~8 M* I5 k' ^! d% P9 T$ f1 V - ...+ U1 z! e- t. |1 A1 ^6 N# }
- while(1)
: Y1 }9 Q# B3 d$ [( _ - {; F% Y! Y6 S- A% l- k# _$ n1 _
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);8 r, K0 m0 n% r" r8 u* W
- ...
6 R; k) Z; Q1 E) S) G; c: P) d - }' b5 Y! {8 \6 c i9 O% O5 b
- ...
/ i; D! P# u* j( @& s; E- T* t - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">5 c! W( L# i3 {' @
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
* _# Q3 j- ~) c9 u - "Server: MJPG-Streamer/0.2\r\n" \" n+ F/ i# Z* ^% ]3 R! y0 J) b
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
! A) I) g' C2 M6 m: d, ^, M6 _# A - "Pragma: no-cache\r\n" \4 p/ E4 x6 ?, D# U, d) a+ m
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
7 ]/ W/ |3 d" ^, G4 q+ D+ O( H - #define BOUNDARY "boundarydonotcross"1 F' M9 M0 x$ g
- printf("preparing header\n");2 Y X# P( m) J6 M
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
/ Z9 q# v6 u' z4 n; j - "Access-Control-Allow-Origin: *\r\n" \
: W3 i$ V6 B- F& S! M2 b5 U - STD_HEADER \
0 M7 C! i' l8 _; e- l Y7 c - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \3 U# u O( {9 n& Y; P$ ]- `
- "\r\n" \6 N8 B9 T# e$ i3 J' }2 q& w
- "--" BOUNDARY "\r\n");% G2 L2 ~0 w9 l; \# G
- if(write(fd, buffer, strlen(buffer)) < 0)
2 g5 e3 Q( I! x" k" z) b- m - {7 l$ ~- A, N" v
- free(frame);, R( X9 S5 t' X! p
- return;
, I- ^9 r, _( c2 d/ b+ Y - }
复制代码发送完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" \
" M8 r1 r# r4 S9 r$ }( K - "Content-Length: %d\r\n" \
4 C& b& U; \. a" C - "X-Timestamp: %d.%06d\r\n" \4 n1 O: z$ I4 x3 ~, Q! ]
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);! [# P1 I' a# w" c) z
- printf("sending intemdiate header\n");& {4 `$ G) I' A9 ^1 x4 b( v8 x
- if(write(fd, buffer, strlen(buffer)) < 0)8 l Z3 y4 w6 l0 b5 |( ~. t
- break;+ i8 r B+ A$ S, ]4 V3 w7 X- C
- printf("sending frame\n");
' h2 I/ b$ k. F - if(write(fd, frame, frame_size) < 0)6 U' o: x q" x
- break;
. T, |. }" U3 \, [# s! h2 K - printf("sending boundary\n");
& [! p' F9 A9 I* p% t; c - sprintf(buffer, "\r\n--" BOUNDARY "\r\n"); O2 f! Q: ~; g, E; {
- if(write(fd, buffer, strlen(buffer)) < 0)
( S j! u1 U' v1 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  ( r6 T$ S9 W: {+ f- U5 J

- h/ Y+ d* T0 `二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:' z3 H3 J i1 ] O9 a% j7 S# z) v4 j$ U
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)) ~# g, _& E5 k4 }0 g( r4 s
- {% |2 M# E+ _; O8 M5 B6 f6 E
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
$ l4 `" E, x- F s5 | - if(*socket_found == (~0))
' y1 U* J+ h3 C - {8 t' F, a, Z/ d2 @3 y
- printf("Create udp send socket failed!\n");1 o$ K/ v0 p; d9 W
- return -1;% j' o. [/ R4 ^6 [/ n8 r
- }% x6 y5 {# s! P, Z
- addr->sin_family = AF_INET;% N+ Y3 X! s% W5 T
- addr->sin_addr.s_addr = inet_addr(ip);' }( k+ f2 v+ @, z* G
- addr->sin_port = htons(port);3 x$ a. a& g7 W: D7 @4 z
- memset(addr->sin_zero, 0, 8);8 w6 L" ^) m, i: N
- return 0;/ L! A% [7 C4 p {
- }
复制代码 ( n' X5 g6 f4 I* }7 N5 r
* s( e# c2 r8 f9 K0 Q而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
8 f1 N$ I# l2 Z! k3 L; p% A; w; @) a3 L' Q* X
- S* y3 m% K6 L$ D' I' x" X8 d4 X- while(fend > 0)- K# j' H. k8 X/ Q& N
- {8 G! Q! l: R) t6 F- W
- memset(picture.data , 0 , sizeof(picture.data));$ [* x. N" z1 e2 d+ L( X- [! W& s( J
- fread(picture.data , UDP_FRAME_LEN , 1, fp);
0 r4 u+ _( u/ l8 j" m - if(fend >= UDP_FRAME_LEN)
" p7 T/ ]" G( ?: w' M9 E - {/ J4 [- x, T1 y7 V8 I& K
- picture.length = UDP_FRAME_LEN;6 @$ c8 I0 y1 X1 A4 q& D7 C
- picture.fin = 0;+ C; y6 i, w+ }- }
- }5 s' `9 j4 |9 q
- else
; F% n& g5 Y* l/ I+ C - {
, E, j: i) Y8 X - picture.length = fend;
; J% l% T5 l- Y3 @) U( e - picture.fin = 1;
- N" G0 D: @- P* K" g7 `8 ? - }
9 R( H V, E+ H) ?! `3 T - //printf("sendbytes = %d \n",sendbytes);
' p: |( @$ [" d' l, b+ g" h4 I1 @: Y - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
; f# P% P: g, _3 b0 x! a - if(sendbytes == -1)
* ~" }' X+ D4 U+ r# {! ] - {. t& T: T, _! R4 K- Z0 t" Q
- printf("Send Picture Failed!d\n");1 S: L: J O" y* J" \+ f; t9 G
- return -1;
4 F# ?/ O' m. J$ E0 S/ n - }' m7 p9 |2 H- ~5 {/ \3 c- s4 f
- else' g7 W) }0 F/ W0 n; r, w
- {! _' q( t i4 K. R
- fend -= UDP_FRAME_LEN;" E* t+ E3 ^! V- v, n. U
- }4 ?7 D3 y/ [: b4 Y6 D- M
- }
复制代码
+ K' V& ]2 r# ^% e
9 `2 P" `: K- N" g/ V7 a 7 S8 S$ u( U8 b5 |- U2 \4 B
: X' } B! L# G9 [" @" _0 X% ?iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |