本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
4 ]4 h' P5 C4 b6 _$ M# Y' i7 o2 `4 }) J; k8 N; O

. i; B( d/ N: U作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html2 Q; O3 m" b7 r
k) l: T) G, N+ ~9 r$ m本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
' ?0 @+ p* r, `- G5 r; G# I6 C- m
% u5 O7 S% J8 j1 Q8 H0 O两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
1 t% U3 m* {5 I( t$ T一、HTTP网页服务器
5 U0 y; c* j( x0 X% }先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)& L. G2 L* b* s! }: S6 [7 L
- {
. m* k: j, ~# S9 T2 G2 P7 y - struct sockaddr_in servaddr;1 R- A) t7 {( G$ U& F
- socklen_t addrsize = sizeof(struct sockaddr);9 v! @+ y0 e1 c( n2 v
- bzero(&servaddr , sizeof(servaddr));6 c$ d8 f# ~9 e* \3 Z. i9 U
- servaddr.sin_family = AF_INET;+ Q8 ~$ y: K$ }
- servaddr.sin_addr.s_addr = inet_addr(ip);+ {- T7 n7 b3 N: `, w- W5 e
- servaddr.sin_port = htons(port);
5 c- y' ]( H6 {% ?% }' c* T: H9 f - int ret;
! w& s* H! s& Q9 B6 u( ? - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
' ^' m- T2 h4 s& v n0 U; t& g# U) K$ e. a - {
+ B, f' @" } @/ p! U) b9 w" {' F" k - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);3 ?( k: V* c5 X2 b. X5 f/ A
- return -1;3 s2 _5 p I6 | ~! R
- }
+ D" u2 S" _/ J3 i( Y0 _ - int on = 1;
: ^3 L, M( n% f) \9 m" }( h - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
, v2 P& J% M9 U3 m% I - {
D& o: ~4 V9 R! D - printf("setsockopt error\n");" o# x$ O( F1 W# K2 m3 S8 E
- }
" q3 C$ P _& B7 I- O! A - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
4 ?7 O; P' O- P% V - if(ret == -1)
" z, |2 b- O; X) Z" x - {, V. R( x* A6 D7 u! d: H
- printf("Tcp bind faiLED!\n");$ Z ~1 @# n# p3 N! R
- return -1;
9 E. @5 b) e* q, U* X - }' V+ u! l' x% P- s" g) c
- if(listen(*socket_found , 5) == -1)9 ?0 a# q y# ?0 A$ d. @" E
- {5 o! d6 n: \# c5 h' g4 Z' y3 g
- printf("Listen failed!\n");- a4 q- A" B) w' U
- return -1;& o" I G5 r, Q4 m) s. r
- }
% R+ x1 D6 I+ s. z$ Y - return 0;
, L7 o6 x- [. d; { - }
复制代码其中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);
. u* g5 Q, Y- o7 X - void * Thread_TCP_Web_Recv(void *arg)
. h/ z, D1 `( a - {* S3 a1 B, E% r! k
- 。。。1 F& m8 n( l2 n# b Z3 W
- while(1)
, m- K7 F1 u/ h! n$ ~ - {
. ]: X4 J6 k' o, |$ f% E4 @8 q - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);/ r6 \+ ` e; y0 s8 M; p, l- u
- printf("fd_socket_conn = accept()\n");
8 M/ P$ b- J) s) _' y" ^. ~( u7 x( C - 。。。& D: {& p9 ^3 d: o7 d2 y. {& P
- recv(fd_socket_conn , recvbuf , 1000 , 0);
. J( h% d! L& y5 D - }0 l+ p/ @, H; G8 s+ S# {
- 。。。
7 @$ s) A9 U( \ - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
5 h- Z8 e4 d m, l. M) ~! v - pic_tmpbuffer = pic.tmpbuffer;
# e$ a! i+ O- A1 ]* v3 D9 P+ }& K - pic.tmpbytesused = buff.bytesused;
* s6 Q& F$ B! f% U - pic_tmpbytesused = pic.tmpbytesused;5 ^" C5 ~& d8 Y0 R* |
- pthread_cond_broadcast(&pct);) q- M- T& R, |1 J4 g& f3 @
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
7 d9 X/ P( t- V+ g. R9 |" N1 T' d - pthread_cond_t pct;; Q: f0 b C1 |- H4 Z
- int main(int argc, char* argv[])
% n1 j( E+ `5 v, V* n - {! X1 N7 |* F% h- D! r& o# k# s4 Y
- ...: `6 N4 {- b$ z$ [" a3 A5 g
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
$ Y/ g: B/ h J8 z/ ]/ k) S - pthread_mutex_init(&pmt , NULL);+ P8 p% t, q# _. e+ W4 g6 n7 c6 Q
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
! \- L$ O; _+ W, \7 F: V Z - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);- S Y* F5 j, m5 m. o# i2 O& U
- ...7 X; {* l8 ]2 j+ O! t: ^
- while(1)
# B* X, v4 m3 l8 D8 K. i - {
$ w. w0 \$ Z- Y8 ~/ Z, r# P0 ? - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);0 w R n, q+ r! w; K- [, g
- ...
7 E6 w6 H" |) P7 R' l; f - }
/ _8 Z \ i& j, a8 y' i1 W$ j5 Y - ...
1 O1 i. G9 S5 q9 u2 k9 f - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
# A( F$ ~7 H2 Y/ s9 A5 ~0 }2 D2 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" \0 Z2 D" Q4 N5 i
- "Server: MJPG-Streamer/0.2\r\n" \+ S/ m2 S, u7 p3 b, s1 M& Y
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \: L) ^9 h0 Y- y/ y t; G) n
- "Pragma: no-cache\r\n" \
& }2 @: X1 D2 H2 H - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
% ~+ N U( E: G, m6 e8 s- b3 P! D - #define BOUNDARY "boundarydonotcross"
' o' f0 a2 h4 b, H# d3 u - printf("preparing header\n");: Y! N" M% |/ o: L3 f3 p
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
3 ]( r: S4 b* s6 c5 }% [% e/ r - "Access-Control-Allow-Origin: *\r\n" \+ [6 u1 B5 `; A, b8 }8 o
- STD_HEADER \# |' b) W% e2 M3 f$ d
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \1 |* c& T6 K1 y( E
- "\r\n" \
# G+ G2 K6 v+ K! ~ q( L - "--" BOUNDARY "\r\n");: N: G, Q$ Y1 n* A
- if(write(fd, buffer, strlen(buffer)) < 0)1 a- h& X* n! P- H
- {
- Q( F& R5 K' L, B4 { - free(frame);: [9 {5 A* R* ~% L2 o
- return;2 g. Q3 p5 Q- u6 j. m$ p# t. m
- }
复制代码发送完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" \
0 \' q. e a2 r6 h" b - "Content-Length: %d\r\n" \
0 t' q& ~0 s9 |- Q' r - "X-Timestamp: %d.%06d\r\n" \/ \6 v) n, y. L4 V$ F# n3 ], x
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
5 ]' F2 \$ t4 P$ l r |8 C# n - printf("sending intemdiate header\n");
! V; }4 [0 A, e, d% y9 t: \ - if(write(fd, buffer, strlen(buffer)) < 0). H, P6 A) |2 u, I& W* S
- break;
2 y6 C8 Y* E' Y, @. u( v& X1 c& `1 S - printf("sending frame\n");
7 i1 x O! t1 \+ t$ c - if(write(fd, frame, frame_size) < 0)1 _4 R; N# U8 d9 c
- break;- J# f1 x1 c6 K) w1 e0 T" D
- printf("sending boundary\n");: \# H5 e3 i1 _9 ?, a" U
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");& V( r0 R& ]( g. G: c# O' C' ]
- if(write(fd, buffer, strlen(buffer)) < 0)
1 M/ ~/ W, R( i' A. k m6 l - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
O- O3 I- O) Q. L5 S3 R; X
% A$ l2 y4 g7 Z* [6 q( k6 z& g% J4 \二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:9 D9 n/ F+ r. M* j
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)# N: o* s. L& o3 R* q
- {; }3 G; X2 ]/ f X
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);1 u8 J2 U/ X! S, \. m7 s
- if(*socket_found == (~0))
" |) ]1 p+ S; R& c2 Y" @ - {
3 K) V& _/ G# y1 S - printf("Create udp send socket failed!\n");% j; \1 p3 C9 K5 M2 c: Z2 i9 @# w$ S) ?
- return -1;
7 I) K! V' s- v - }1 ]4 u: W% V3 r1 ]/ L5 x
- addr->sin_family = AF_INET;# Q4 e* Z+ D+ @7 {# `+ |- q" ^( Q
- addr->sin_addr.s_addr = inet_addr(ip);8 y0 M/ M$ ?& n W+ d
- addr->sin_port = htons(port);# R3 K2 D; d: I. A
- memset(addr->sin_zero, 0, 8);6 z% A" J3 }$ {) P
- return 0;5 G# [8 i1 j- f% x0 j/ ]7 a
- }
复制代码
7 N" C8 P, [0 ]5 Y& ]3 G9 Q# l$ ~3 M/ p' m: j6 ]' [9 n2 Z
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
6 h$ x/ o6 w8 J- [2 A/ j& m6 \( z
5 ?) }. Z% f6 g2 n! d" Q
3 C0 {$ m, Y# v- while(fend > 0)
" b! `% F2 f5 {3 X: Z - {( f: W* G7 W1 q" b/ j* V2 h; `
- memset(picture.data , 0 , sizeof(picture.data));
% d$ a" K( B) H. f. B% m - fread(picture.data , UDP_FRAME_LEN , 1, fp);
9 Y4 [. }, y5 k2 L- q7 P4 p - if(fend >= UDP_FRAME_LEN)
: ^0 I7 \5 u# E$ R - {
5 y3 ]# ]9 H% q# h7 @8 n - picture.length = UDP_FRAME_LEN;& b4 q# g$ v5 i' [
- picture.fin = 0;
4 j) b- |" r9 x v( {1 G: h2 c) X - }
# F' m% u6 E0 c9 l7 q - else) v5 l0 ]! Y3 H6 g( H
- {
I4 j% z" w, v! N5 h - picture.length = fend;
" L( \1 W. ^4 ?0 B. Y+ | - picture.fin = 1;
* r1 Q2 f/ O. M: G: w6 } - }
. M' v" s! ~ ^7 S3 m - //printf("sendbytes = %d \n",sendbytes);
- v7 t- M6 |( _$ i! V - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);7 e6 p& i( d8 k$ _/ [! V
- if(sendbytes == -1)
/ s! ]/ X8 @# F/ K4 j - {3 S8 X3 [* l( d$ T" |
- printf("Send Picture Failed!d\n");6 P5 I+ B0 p# j; Q
- return -1;
- F" H* ^, y2 ]+ v4 P- [ - }* L ]: T" Z e' ?
- else
6 X' Y) R, t/ T q1 t0 m& P' g - {) B, {6 e" [5 |2 L( v$ I2 ^2 X6 i
- fend -= UDP_FRAME_LEN;
, S; r2 C2 C8 T3 ?9 M+ a - }! `; y- s: ^1 s& {
- }
复制代码
0 n0 s/ u7 I4 K( |1 h `7 @) L
; z" a9 I3 V. z% D& M V) X6 |' A* [2 B/ B/ c5 E8 n
% b$ }/ {1 [2 t4 GiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |