本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 8 U$ W& ~# L( Q9 h: h& W
/ n0 Q) _' h2 a
& D& a9 h) \# S k- b' R作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
o+ x* d% [; S# z! T' y
, ^; Y% C, R8 t6 P" Z: r$ g本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 9 b1 E: Y: ]9 }- f
 9 p$ A. A+ K: ^, @3 W1 z
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 6 [' i6 Y5 h4 _1 m. P
一、HTTP网页服务器4 u) Z% D* E5 ` s2 M: c2 f {5 g
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
7 T% O1 ^7 X& Y9 a - {) {9 T9 V7 N, G% q
- struct sockaddr_in servaddr;6 s, x A' @/ |3 h% h3 F* Q2 m
- socklen_t addrsize = sizeof(struct sockaddr);+ I8 d! C q) w7 U
- bzero(&servaddr , sizeof(servaddr));
- G1 Q, ~$ i+ A1 x# E8 ~) w - servaddr.sin_family = AF_INET;
% t4 N W5 U; P. u7 H - servaddr.sin_addr.s_addr = inet_addr(ip);% b3 l* D. E# |) m. I$ V
- servaddr.sin_port = htons(port);, Y4 A3 w9 {7 f! t9 n) c- t
- int ret;) G) M, {5 [! C. L7 a( K
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)0 R" u5 a- b2 |3 ~2 k% A7 ~
- {# H% q3 |& Q. m0 K! E W! S( Y# W: `; l
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);3 R9 v* A# j5 D2 f# z& p
- return -1;1 l9 v: r3 y8 d( \
- }# ^- w% L7 `1 T. t6 u
- int on = 1;$ W$ ` s4 B/ `3 f$ f4 U" U
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0): w9 q- F( ~7 q
- {2 m* a/ z' _# ?" G2 b
- printf("setsockopt error\n");
8 s8 M4 s& r' `7 m( E - }0 w% T* _ h7 m- w9 C8 ^& p- g, v& R
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);# M, E6 w% |, K( b) ?. V; z# l
- if(ret == -1), T6 \4 _3 Z) | C! O# f
- {: S; o& k7 @% e1 m# H7 }( S
- printf("Tcp bind faiLED!\n");
" l+ @9 N8 i6 C# J - return -1;& K/ l' \3 e0 y: m- e; F$ A
- }# b$ O" K+ ~, H4 r' M- K4 \6 `- [
- if(listen(*socket_found , 5) == -1)( y0 N7 R0 }; X
- {
4 Z0 f5 d/ f9 P! B1 c6 ? - printf("Listen failed!\n");! R t$ i0 n; I( F2 P# Z1 `0 U8 f
- return -1;0 x( Z7 q2 I+ @( _1 s9 Y f
- }
7 o$ k- P, x: W& q2 i7 L. F - return 0;
2 t7 o- t) S+ L, m% u - }
复制代码其中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);; B' J# F2 ]. Q& q# r* R2 f) E" L* n
- void * Thread_TCP_Web_Recv(void *arg)
0 N9 j! X$ r" T0 `" h0 k7 e - {) H2 q( D5 i4 v( |+ M5 Y* ~9 c
- 。。。' H* }$ m5 c/ l: p2 Y% C
- while(1)
1 x2 [. u$ y6 o7 d4 p; ~ - {1 l; m" G- i: V" | o5 t
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);) |* m9 l' h6 G" w0 y
- printf("fd_socket_conn = accept()\n");* }2 w2 P3 k4 F# Z' L0 o" g
- 。。。
) Q; S1 l4 Z1 t$ ^3 { - recv(fd_socket_conn , recvbuf , 1000 , 0);
1 v n5 `0 D5 j - }
' l2 v, y( y) f - 。。。+ g* ^4 \# `, Z X5 u6 Y- E9 P
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);3 J/ p; K, m$ t& i# b
- pic_tmpbuffer = pic.tmpbuffer;3 H: P: ]' u! j
- pic.tmpbytesused = buff.bytesused;
$ |3 y7 W: V% a! F8 R - pic_tmpbytesused = pic.tmpbytesused;
2 \; e) \ g/ E - pthread_cond_broadcast(&pct);
[! i. J$ I" S" y6 `; A3 o& g" h7 u - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;' P. o& C5 p5 y7 X
- pthread_cond_t pct;& w9 f+ g) x3 H1 l: h
- int main(int argc, char* argv[]): b8 F/ `/ k$ I3 ?
- {
5 u8 K: a7 G* i# h5 | - ...
, a& p& G# Q! H2 q7 B. _ - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);1 i) n, B# w v9 K- D
- pthread_mutex_init(&pmt , NULL);/ X) c6 \" F! X s, B& U
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);6 n: U' t; g2 Z5 h9 _
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
7 u0 `' f6 e1 P7 ?% K/ X - ...
" h9 b/ O& f$ E - while(1)
/ z/ Z/ k: b5 d# ]9 V! t2 F - {
9 [9 B) y0 B, {& A7 T - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);: s9 a+ L$ S3 {7 P6 U
- ...
4 y- w6 H: C9 T! T+ \+ f - }0 b. f! A* D6 n0 i! S5 m% T, C
- ...0 A; A3 `$ ~: n% ]' U* e* V0 F
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
: Z$ _2 G, ^/ c8 M - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \; p7 J+ U U6 w: c, ~
- "Server: MJPG-Streamer/0.2\r\n" \* [4 W% k/ L, r3 o. v
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \7 J# B' V( a, V4 H2 U
- "Pragma: no-cache\r\n" \
& @3 E! t9 r! j, N9 V0 D3 W2 N' @ - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"0 }) B' X4 N M# T1 `: C v
- #define BOUNDARY "boundarydonotcross"
2 K# H( m. B) z/ t+ F - printf("preparing header\n");
$ x' ^5 ^- X G1 z. U7 q - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
$ p+ K! d: Y: S4 K - "Access-Control-Allow-Origin: *\r\n" \
9 u1 C& I8 D' V* n1 Y! X* G- p3 c* Z - STD_HEADER \
/ m% s. S4 o) V1 n. D8 R - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
! p' e ^+ {4 ^. V4 R% h/ S9 O - "\r\n" \
# S6 d2 z2 w5 y( Y - "--" BOUNDARY "\r\n");$ ]0 Y) x) q- y
- if(write(fd, buffer, strlen(buffer)) < 0)! P2 b3 f$ K; ~ H% j4 U
- {
8 d& m8 q; R4 d. ^4 q$ e& |9 h - free(frame);0 |, Y# O! Q$ v7 F
- return;* d0 K, ~6 r# j* g/ b
- }
复制代码发送完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" \
$ {& [; e! c% t7 Y. @ - "Content-Length: %d\r\n" \- J6 [+ n( d/ H4 X4 q' y! p% o
- "X-Timestamp: %d.%06d\r\n" \
5 t( m) V) x# p% H - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
/ {, c# N+ S( c7 o - printf("sending intemdiate header\n");- T( L% ]4 f0 H2 [! l6 ?
- if(write(fd, buffer, strlen(buffer)) < 0)1 G0 u" k9 c4 `
- break;
" |- y! u: j9 G3 m5 c9 G1 ~ - printf("sending frame\n");
0 p* a2 Y; h' A' U" L5 B1 \5 h - if(write(fd, frame, frame_size) < 0)
e5 i/ _7 v: p( A! O. q - break;
9 R! x& Q. Q1 O# S5 U - printf("sending boundary\n"); y. @! T# i4 [$ z
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");* P J3 M7 s8 y
- if(write(fd, buffer, strlen(buffer)) < 0)
4 v* J ?9 z F- R* o4 F - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  ( M. W8 r" j( _7 @

' k* v8 e: r2 H5 w5 Z9 ~5 g. d' `% \5 R二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
9 w( q! c ^, ?1 a x- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
7 u: H6 L# B. q' V: Y: @' z; s' b - {
% m# W9 t2 n; O0 `* t R8 Z - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
( Q' {5 L% `2 a! c, V. q2 } - if(*socket_found == (~0))6 s# P9 }% e5 b3 Z& e: P
- {6 j: H8 c) r, A& n
- printf("Create udp send socket failed!\n");
' ?. W2 p& l' Y m - return -1;6 N5 O$ E" f1 z) ~3 H6 k
- }& O. d* O6 F5 a- Q( f
- addr->sin_family = AF_INET;
+ j- D' K( i! i7 ~& o& t - addr->sin_addr.s_addr = inet_addr(ip);8 Y4 x# p' J. {; Q+ L$ |3 ]7 y
- addr->sin_port = htons(port);% C# K& j+ d# x3 G
- memset(addr->sin_zero, 0, 8);) M! A: c3 X( h* j' ~
- return 0;, G! C9 H- i6 u# y- {8 F
- }
复制代码
! w: Y* |; x6 x! J: c# k% ?% V( u% L% G% G Z- G/ y0 A3 ^
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:* R' z" w/ Y, c. | g8 I; z# `6 R
, C" Q$ i- s. o3 g% h2 X# @- F
2 i" R+ F0 Y$ ~& J- while(fend > 0)) Q. ^$ v& ^8 }# [2 u2 J0 r
- {5 [, j4 b8 K1 X$ E; H
- memset(picture.data , 0 , sizeof(picture.data));
9 f& w; ?# [7 d% Z$ q - fread(picture.data , UDP_FRAME_LEN , 1, fp);
8 J2 j4 ]4 @: b I8 z - if(fend >= UDP_FRAME_LEN)
8 D; n& @& b9 Z. l1 c - {: H4 t- w! T9 y5 p; E z7 f7 k
- picture.length = UDP_FRAME_LEN;
& B S" B. R: u/ h7 r! }$ s) k - picture.fin = 0;
r- x; w- }$ M6 J; w1 { - }4 u; T- ]5 |/ y6 w( ~; G' W* [
- else
# h( S# B, k1 [/ f8 R - {
$ Z0 E4 e \3 @! L2 E - picture.length = fend;! v9 L* @7 s) q6 S: G# K
- picture.fin = 1;9 ~3 X: l3 e9 M4 Y
- }7 g+ l; g) e3 \& T% \
- //printf("sendbytes = %d \n",sendbytes);3 q, [% H% E3 p0 ~
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);/ u; {8 @' S) I' e" D
- if(sendbytes == -1)
* V3 g9 u$ `1 O+ y" m - {- f. Z, e$ h( Y6 T, {/ u* m0 a
- printf("Send Picture Failed!d\n");
9 _9 d" L$ t) r- { - return -1;9 K# b8 C) R+ Q/ _6 Q1 L6 V1 U
- }
0 d7 G8 D- @) n/ g; |+ A G - else
' G4 S3 p9 d4 W/ N - {' o6 L2 \1 W& c; T% q
- fend -= UDP_FRAME_LEN;; [: {9 ^- U# D5 {6 {
- }
; k( d3 i. J4 o$ j - }
复制代码
E+ C6 T- r, o. Q6 H9 A& Y$ m/ b' u. s. F: b

F9 U2 w/ p( V0 Q5 o- J( j0 T3 Q2 ~2 }; S0 w) A
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |