本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
( d* a& k* s9 _ t; G3 d Y' s, H7 j) N7 d0 O. f
 7 _3 _- R* m* W. y/ `3 q7 v
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
+ g. N# w3 E$ I
; S' l" u& y' q; f6 Y" x$ G0 {) k本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
3 V* t& e' q. v2 m4 N& f6 k# y$ I 7 R. P# `" w1 D& d! `! @
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 2 N8 E1 E, b% k0 a
一、HTTP网页服务器
; L6 u7 P1 n/ \; i+ U$ V先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
% E X: v. E8 m. E& Q9 x6 m6 L - {0 X9 @" {4 B1 G, G/ q/ ]
- struct sockaddr_in servaddr;
7 s) n* g$ I# L: Z9 _9 F2 P# S - socklen_t addrsize = sizeof(struct sockaddr);: {. V# f1 a# }- ]
- bzero(&servaddr , sizeof(servaddr));* r" x6 M: Q1 B( B- s% G+ N
- servaddr.sin_family = AF_INET;
" h& s; k8 ~: c, m N, e. z t2 v - servaddr.sin_addr.s_addr = inet_addr(ip);
+ T" c, c0 ?6 h( f - servaddr.sin_port = htons(port);
# p% j. o1 c8 V) w - int ret;
% k% p# ?3 K) [ C# [( c - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)$ k7 N; \( L4 F" t6 x& v6 k( J
- {% S5 r0 d2 K/ c; ^; }
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);+ X5 b; z5 \: L2 p# z
- return -1;
/ V4 O5 v9 n% p+ K% P1 f9 b" c, @ - }
0 F$ V" z# g) A8 ?" v! S3 P - int on = 1;* r O8 Z2 ?+ l9 r0 E
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0): B; [" u( _( i+ S# \" D1 B7 l
- {
9 g) O2 [% v9 \. Z - printf("setsockopt error\n");0 r) b) K* I4 D& h/ F6 ~& j
- }- I; y' N/ P$ U w6 u( U( `
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);$ F1 Y* g9 z/ \# [- c1 j& _
- if(ret == -1)9 O& U& X* Y- ^
- {
0 c# B3 M0 \$ [! U$ }( E - printf("Tcp bind faiLED!\n");# e c" [2 G1 m$ K
- return -1;
: M- X0 H" }4 M7 f. y2 g/ a+ k - } S% ]( g4 r* p; Y1 g
- if(listen(*socket_found , 5) == -1)0 x' ^, `& D9 {! I; d3 _
- {2 S! `7 y3 r8 U9 l& S
- printf("Listen failed!\n");7 J' v) P. [: R; C7 K6 ^
- return -1;
0 T# H; m' S h% S9 [: ^- v- w - }
7 G9 {0 w$ R& f- z+ X& d) G, X - return 0;
6 E& K: }/ r0 l5 Z4 U! b$ R - }
复制代码其中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. c; |5 N; {+ `9 E' k5 u
- void * Thread_TCP_Web_Recv(void *arg)8 p# ?$ {& ], |$ ?
- {' l* t0 B- O) F5 x
- 。。。/ }( o0 S* O' X- M
- while(1). M& z% s: `. c' x7 ^
- {
# B+ V ~* A2 g/ e& f( A- ]/ `! v - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);/ x! F$ G4 e( r$ n" b
- printf("fd_socket_conn = accept()\n");2 e- }9 c+ x8 H6 E/ T( {" K
- 。。。& q$ K% {3 @) G+ Y5 V$ D* F" e
- recv(fd_socket_conn , recvbuf , 1000 , 0);
* E% W% L' ?2 s( X6 M, \ - }
# |" y" w* K# o& B( ^, O: T - 。。。
; ]1 W t4 Y/ `3 J6 n - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
' L: b* b0 d/ K/ V C: m% ^ - pic_tmpbuffer = pic.tmpbuffer;
) N; P% }/ x5 E$ T. s* @ - pic.tmpbytesused = buff.bytesused;
- a$ T u* w: R+ ] - pic_tmpbytesused = pic.tmpbytesused;! ~9 F) M9 M' L* [
- pthread_cond_broadcast(&pct);
3 P* M8 i& S1 @& S- G - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;4 z& Q' D5 v# V" s2 Z
- pthread_cond_t pct;% Q: V/ p {) S' S; p. N3 f6 [: K; ~
- int main(int argc, char* argv[])' ]0 D' f, X/ M) R3 a
- {4 ~# \2 \, A* Z* i4 g7 s! m
- ...
5 b: }1 I) g+ E; G - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);0 c: h. m# Y# }7 y- D5 k+ F$ x
- pthread_mutex_init(&pmt , NULL);
. G% N) Y% R+ Z; h - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
/ Z" S r! |2 x# u' [3 w& W - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);5 u& ~* f X r; z" f/ ~% B( Y; D8 \
- ...
. h2 i/ `7 ^: K, r1 w - while(1): N5 g: {5 z5 j# n" d3 a
- {" ^6 @3 Y# d/ ?4 u9 C
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
" D1 f' y, \7 q: Y6 ` - ...1 `* T/ `( \1 W8 ]- ?! w1 ~
- }+ l2 D9 ?" w% v- C5 u5 H, `
- ...6 \$ _3 B. g8 n9 O$ H+ _& G& v
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">! u9 O$ @. ~' X. `+ ^
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
$ [( X, |6 O) J; s& ? - "Server: MJPG-Streamer/0.2\r\n" \* J% L+ m1 U: O' L' i7 T: N
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
( ^ T6 M& f' q9 ?8 \9 s4 ~5 \! \0 b - "Pragma: no-cache\r\n" \3 ~9 E" |9 k# j0 l. {
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
1 q& g' z5 |' _% m - #define BOUNDARY "boundarydonotcross"
) r5 h* X( z5 @) I3 I) Q5 g3 u" G4 c9 t - printf("preparing header\n");4 p x7 J+ E& Q2 k3 m! c" U; g/ u1 M
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
6 G0 R4 D% f* K2 ?6 [ |$ t - "Access-Control-Allow-Origin: *\r\n" \
% Y1 \' c( k+ ~- u; a - STD_HEADER \
: T$ [+ d6 e6 l }; Q - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \6 ]& D6 r) n8 M0 M
- "\r\n" \
5 B! ]+ U, K' q) o) x3 m9 q - "--" BOUNDARY "\r\n");
9 K+ l4 f; u$ I' i O - if(write(fd, buffer, strlen(buffer)) < 0)
! ?* D- A! r# s8 B% g - {
. E! B4 u+ Y/ E0 J& o) |! Y- h, N( J - free(frame);# G3 m0 d+ s0 t8 Q% f3 z* E
- return;
9 P! t3 F: @! t% L. }! U - }
复制代码发送完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" \
! b; s- s( L# v2 r5 w+ C2 } - "Content-Length: %d\r\n" \
5 k; p. U9 J5 Q# i2 m, @ - "X-Timestamp: %d.%06d\r\n" \3 N: {4 s* E9 ~$ r& ~
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);* m4 r" [' f% ]# a
- printf("sending intemdiate header\n");
# M% o! D* a8 M+ S - if(write(fd, buffer, strlen(buffer)) < 0)* x F% m% _+ f, q, q( C
- break;
. o( B2 b" I1 g0 V - printf("sending frame\n");9 i& E' `* i! \% D' Z+ t& {/ t
- if(write(fd, frame, frame_size) < 0)( U1 b: I7 i$ m" y+ w @
- break;+ n6 w" ]/ b1 {' u5 ^
- printf("sending boundary\n");
9 @0 O: @9 F* Q9 A$ p - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
/ h6 L5 g8 O/ p$ m - if(write(fd, buffer, strlen(buffer)) < 0)
' e. g1 g* j4 N# B! ^ - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  % k: ]4 S, Q) q& N
 8 ^* `" J6 Z/ X7 J
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:; A( X3 c L# P) U* s
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
. B; p$ I: ~* }5 ~ - {1 u, g9 r9 Z6 E& P3 Z( n
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
& z+ H# g y& ] B" ~, q - if(*socket_found == (~0))! u# M: \* M- @% i" T: \% u
- {
! U& ]0 e' V/ Z! Z - printf("Create udp send socket failed!\n");6 m3 C3 ^; l$ I
- return -1;- i8 W/ C* u5 n- T! e/ f, G
- }' B/ c# {$ X; R2 L: B5 S! ?5 t
- addr->sin_family = AF_INET;8 x# T$ n- I* K" }) ]
- addr->sin_addr.s_addr = inet_addr(ip);
4 k% H/ e& }9 K I. v - addr->sin_port = htons(port);
) T) v/ [% S" T7 m6 b- B9 z - memset(addr->sin_zero, 0, 8); N& T2 T9 I1 L& O. i
- return 0;
5 W$ W( a: o2 E - }
复制代码
( g5 Y+ X8 n% H# a1 z$ e
1 k8 M6 X& Z9 M& ~5 S) U1 q& j而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:+ r( @9 g( v' a& o
" l5 w& R. i. `- i5 r8 c$ \; L6 j8 y
- while(fend > 0)
6 A; T+ C5 u& E0 m1 Y1 V0 Z - {
% [2 ~- D4 j: w$ u- q0 c# w, H - memset(picture.data , 0 , sizeof(picture.data));
; B" K. J" S B5 L - fread(picture.data , UDP_FRAME_LEN , 1, fp);
# ?2 k- T4 t8 I, \" K' _% m/ i - if(fend >= UDP_FRAME_LEN)
; W0 X7 k% [4 L: }. f - {
1 C8 g8 D/ i; b) }+ Z0 n - picture.length = UDP_FRAME_LEN;# C9 Y2 t( ]3 _: `
- picture.fin = 0;1 U; r# k! l$ I, K" h
- }- C V K$ e( Q. [$ w" L
- else; i' n/ e8 d# V( q" n; r
- {0 L4 a! k6 g: g; B& f: s
- picture.length = fend;! \3 U) L R! ]/ H
- picture.fin = 1;/ B* g+ K2 D+ f, E# S ^
- }# ~7 l6 M. R- [
- //printf("sendbytes = %d \n",sendbytes);& K4 i& e$ A% b
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
; b! D& I3 X% p1 N - if(sendbytes == -1)
P2 ^7 Y! r- \% Q% ]" z) ? - {, u% S" x" H; t
- printf("Send Picture Failed!d\n");
6 h# \/ }# z3 p" L - return -1;
9 n, g6 G% Q8 o1 h2 Y - }
5 W' g6 e0 K; p" E) u - else
( d) l/ V+ M& A! f2 H, ^ - {1 N; d5 k v! m
- fend -= UDP_FRAME_LEN;
7 P/ X& S. e3 }. j- j$ s& j - }3 M2 A% t- B4 Z7 N
- }
复制代码 : y( a& x; v$ x" }# V0 ^
( N8 @: ^9 p9 k; D$ ]

4 A6 z. g s! j) ]6 R2 z4 z+ i3 {- P! f+ w% g
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |