本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 1 b3 E- u5 t& B) {
6 t% V3 N" u4 Z0 |- F' F 2 h( `& }# z6 w; k
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html4 l* f; U/ S# S5 X, o1 K8 @
& _( ?! X$ V+ `4 g% u/ }# E/ D- b
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
+ @" T# O( P" V; o
5 a/ b1 k/ E& t8 n两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 ! G' f+ G- @9 ]7 d
一、HTTP网页服务器
. Z; p0 F8 m5 Q; n- y先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
, g& N& z/ ]0 `( u, ^# ~ - {
% r3 k: w$ `% R - struct sockaddr_in servaddr;4 @: R8 r* ?7 n1 o
- socklen_t addrsize = sizeof(struct sockaddr);
: S+ ? R& o, A2 ? - bzero(&servaddr , sizeof(servaddr));
' b4 n0 T* S" i5 L- P9 f - servaddr.sin_family = AF_INET;+ @) R5 `# S3 B1 H/ i; G) ?
- servaddr.sin_addr.s_addr = inet_addr(ip);/ j) M, d5 e1 r) Q6 V# c
- servaddr.sin_port = htons(port);
3 G* F# N& ^: v: O* P: k. D - int ret;
3 w" ^+ g9 Z- m6 I( s" i6 J! q6 k: l. q - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)/ Q7 U2 i6 p! s% C
- {1 k% b& m3 M- y3 T% U
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
2 m& u3 b9 T7 ^2 b, J - return -1;
% a, z9 j. z5 q. j+ @2 y* [ - }
$ R) D5 w9 ]- x" q4 A8 u6 R - int on = 1;& B6 G4 `6 W/ x# n q9 a8 F
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
9 D& N+ O/ [ U9 X3 }8 p2 k# E - {
- A' g S$ b7 v y$ e3 x - printf("setsockopt error\n");4 y) w) v# R- V3 \: E' a
- }
; n! |. v/ u p$ z) |( W3 { - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);% l- z/ W4 f; R: P0 Q) v' t
- if(ret == -1)# t' |* _) g& P* w' B2 c3 s
- {
% Z; ~4 T! W( `$ L - printf("Tcp bind faiLED!\n");9 Q& e. c8 I1 G
- return -1;' t* J: f; s* [2 g4 ]& X$ t
- }2 b4 u7 x+ |; d# M& R1 R% z
- if(listen(*socket_found , 5) == -1)
; ]! L y; {7 B8 Z9 b) [, z - {
( s, q) W: P p8 q/ `1 w* x, Q - printf("Listen failed!\n");. T4 m' I" X9 `' S
- return -1;
3 N4 ~+ t& @6 U - }$ C% i& \2 M I* }) X. u/ n( ^
- return 0;! {+ N3 _+ U9 H: @1 N, @
- }
复制代码其中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);" T: ^4 T, K9 z) u6 m$ j2 a& N# n
- void * Thread_TCP_Web_Recv(void *arg)
* S3 n, Q# l: |) y. b/ Y' y - {
5 K* ]1 X6 H) d* }4 ~, k - 。。。
* L, b4 n8 h+ D9 Z$ | - while(1)
& x# j+ x* u9 E: q( Q5 M, w. T6 |7 p - {. v. z2 b6 v3 d* l5 x8 h
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);, e" e7 r* A6 d9 v8 t$ S
- printf("fd_socket_conn = accept()\n");
6 |& @! G" o: [' l - 。。。
& K; s- F- x0 Y" Q - recv(fd_socket_conn , recvbuf , 1000 , 0);
+ A s9 }+ w* x- W* U* s - }
: @8 J. Y v! Y! z" E4 ?& | G - 。。。
( |" {. K" S+ ~: u5 { - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);8 \( { l7 g" b* _7 o' T% ]3 A
- pic_tmpbuffer = pic.tmpbuffer;
7 ~1 Q0 p8 P4 P4 f0 O. s - pic.tmpbytesused = buff.bytesused;
- v; ], W( Q4 R8 k- q) C - pic_tmpbytesused = pic.tmpbytesused;& V8 _" C( C6 b, P- N
- pthread_cond_broadcast(&pct);
0 C$ N! D k8 d9 m, }( Z4 ? - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
( n8 ^2 L$ `/ q* D% d0 u - pthread_cond_t pct;
# w ~) @. P# h' z9 l! P - int main(int argc, char* argv[])0 T6 o9 ~+ P9 I& p0 j
- {! u% Q2 W" A: b$ b9 k0 N- x3 t
- ...
- |; n1 Q9 c. s - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
$ [0 f, z( G- x) Q7 u- u - pthread_mutex_init(&pmt , NULL);
- j; k% o) U/ S0 d - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
+ _# E9 z8 ]% S! k2 _ - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
! k( y% {: }- e - ...! Z7 v/ H; w c! o* i7 Q9 }; e" z
- while(1)
8 J* ~. |) ] m% H9 i - {# f, v. I; @7 ~( ~
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);, z; g; N1 t7 p+ x1 f
- ...4 s2 v2 B6 d' n- E
- }
% Y; _* b; y+ L" ~5 T5 P - ...
' g1 e, e, G8 }# J- `9 F; z8 F - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">% Q: k9 A3 x! H7 ]( g2 _
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \% F* W" ?5 {; e6 s+ [2 C
- "Server: MJPG-Streamer/0.2\r\n" \
, V. z" `% j0 X' a5 z+ s - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
8 q$ J2 M, M& o1 j - "Pragma: no-cache\r\n" \
$ `* R; Y$ d7 {( y. _( _- [: m - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
M1 H8 H( p/ h3 R. P: x - #define BOUNDARY "boundarydonotcross"
6 |0 Y. o" S6 x) T3 y8 ? F - printf("preparing header\n");
4 M- A* v3 ~9 r! ~ - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \8 U2 n& ]# d" H! f1 C( q! X
- "Access-Control-Allow-Origin: *\r\n" \
% D/ r0 x9 Z* w3 D9 ?6 [' W - STD_HEADER \
: t0 X( P- U. d5 g0 r9 Z - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \- ?7 ^4 {, |- O% H1 X P o z
- "\r\n" \
$ n0 z6 }) v3 ~5 Z% u( r - "--" BOUNDARY "\r\n");
4 V8 _0 Z; P* m$ u3 y" W - if(write(fd, buffer, strlen(buffer)) < 0)
: _: t3 }- x6 V3 i - {
, j- g, }: U. v* g. ? - free(frame);
8 v" ?2 N$ v& i - return;/ h' \- e3 Y" s7 }% w
- }
复制代码发送完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" \
^8 T1 v) Q3 C' o4 z$ c - "Content-Length: %d\r\n" \* r7 n. \. N- k6 J
- "X-Timestamp: %d.%06d\r\n" \* a. F, ]* I, j6 p; X9 L3 B
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);1 y+ @9 X4 B, A& C# t/ j
- printf("sending intemdiate header\n");# ~! Y+ e* s8 a( C7 P5 P/ X
- if(write(fd, buffer, strlen(buffer)) < 0)
: E/ A6 E$ x; i - break;8 G$ } [3 Q; b% L1 H; l7 P
- printf("sending frame\n");
* l; r- o+ p) ~* o" _ - if(write(fd, frame, frame_size) < 0): ^" G3 o% h2 t' J. n G+ S# q
- break;, W/ r& \' T, P% {' S9 f
- printf("sending boundary\n");
; p( b9 H. p3 k8 `. Q - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");. B3 T/ V7 x# s0 s/ i+ N
- if(write(fd, buffer, strlen(buffer)) < 0)
/ w7 I& R3 V) k) z t% t - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
$ H0 r& B# q4 m: |1 |8 J . B9 X3 z5 a" B5 ~) Z4 n) N
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
! q1 _4 g; n& l* a" y- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
% t; d6 W* d' K( ?1 c* K' W - {
- M. W1 F: v2 y4 G( u5 c - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);# x6 {# ^0 o# |/ u" p. l
- if(*socket_found == (~0))
" @# I4 Q- A& T N - {. S0 Q+ m9 q0 e
- printf("Create udp send socket failed!\n");
, R/ @# u$ |! D& r - return -1;) T6 u) s8 o0 u7 |
- }
: k. B: ?% m! x1 ? - addr->sin_family = AF_INET;- V& h$ p8 h; M& s! z9 G
- addr->sin_addr.s_addr = inet_addr(ip);5 V. `$ d2 H5 H8 D$ [, Y
- addr->sin_port = htons(port);
J6 M7 O; c0 j. X% k9 B - memset(addr->sin_zero, 0, 8);1 Z( O: c& X7 g: y
- return 0;
- n2 o! L5 Z7 ^6 ^# Y - }
复制代码 3 I! a0 T& A8 m. [5 U
" ~. r/ G3 p( A, O) F: w9 V
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:% [# E6 B. i# ?( C; ~0 U
9 c) Z1 S* r+ I- v8 C* w% C
$ h1 f" H" a9 F: {+ B- while(fend > 0)
2 `+ I2 V, _% k9 @ - {
1 N9 `; q- v/ K& j0 O - memset(picture.data , 0 , sizeof(picture.data));
% R6 c+ ^7 U0 r. K; w. A, A0 a" |1 I - fread(picture.data , UDP_FRAME_LEN , 1, fp);
2 g# Y L4 n: U4 W( \ - if(fend >= UDP_FRAME_LEN)
: N8 @ q: M% S) y4 r4 | - {" `2 m3 S- o* x8 G
- picture.length = UDP_FRAME_LEN;
5 R0 V0 O* x* ^& S% ?5 Y' i - picture.fin = 0;; Z% T1 I6 r Z- v Z9 a
- }- W. k+ A: w/ k& X2 m8 x
- else+ K4 z" k& G, i) \2 F1 Y( p& @
- {
) I" g( V7 d+ q9 L( I I! Z - picture.length = fend;- c0 K% b. N+ Q# {& t
- picture.fin = 1;
5 d1 ~8 |: p7 D( J: l6 ]4 f - }
% G- N4 w8 \3 X/ g( U7 z7 a - //printf("sendbytes = %d \n",sendbytes);: j/ D' B- G1 R0 n3 P! ?
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);) d/ M6 K5 J6 y6 X
- if(sendbytes == -1)
1 s! f* W' w6 i d% C: \& j - {7 x- v+ X' h6 f7 V
- printf("Send Picture Failed!d\n");
* h6 B# N! h0 `8 R" a - return -1;8 C2 w; d1 y3 I1 K9 D. f) x
- }
) V: o' k# o& g+ a - else
% |& Y- e2 |; ^' _ - {
- z1 A! e2 Z1 |! @, m4 t - fend -= UDP_FRAME_LEN;6 [3 z: L9 L o/ D% O. x E2 t
- }
( v' a" i9 Z* w4 D3 s0 A - }
复制代码 ! ?$ L1 L& Q( U; {2 e4 @0 |
( i$ [+ m( U7 A$ X% w* ^, \2 G h) q0 _; @8 g5 t/ f: i
# Z+ A" Z$ \* D' a2 K, x
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |