本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 9 y2 I, y9 y7 L( h2 G2 y7 ?( y" o9 h
6 \- k: _3 S2 n' T

; F: A" b& g) a; ~; S1 a, V作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
0 N5 @) x6 ^) Q2 N# t. ~$ m% K1 |. K- H& ~, G7 Y! H
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 1 S; [7 Z% l1 I) p

7 d2 ~, d) E' F% O- W& b+ Y) m两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
1 o' W" ?( r7 _4 {# @一、HTTP网页服务器- D- a) ?" i6 F# L
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)8 F% d: k8 H' y4 I3 B( K
- {
" j+ I8 A3 Y8 S: L6 U, O9 k7 p, \1 g. ^ - struct sockaddr_in servaddr;
, v% q) Y f6 @8 t# E - socklen_t addrsize = sizeof(struct sockaddr);
7 O/ \7 c | k - bzero(&servaddr , sizeof(servaddr));" B( L: `/ \! P9 v1 a9 \: t& K
- servaddr.sin_family = AF_INET;
# a' d" D( E3 V7 c' _ - servaddr.sin_addr.s_addr = inet_addr(ip);
: h! S$ p6 p* V; S3 ~5 T - servaddr.sin_port = htons(port);5 r: I+ L0 d# J" Q. L; H
- int ret;: m6 L+ ? [0 q1 {# Y3 u v
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
8 ~% s: ~0 L' G/ _ `5 O; w - {
0 V/ H2 L! Y" U! S$ a& n, {$ @ - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);- m* T; T d" U% @
- return -1;
5 R. h! z: N# T% w% o5 D - }
5 K+ H0 ]7 [$ `; K) S$ j3 i - int on = 1;+ u# G1 `+ v* n% I) U+ b ?
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
8 k1 X3 N% M* \5 `# r - {, x8 Y5 E+ N+ ?/ |# E' g8 V' l: z
- printf("setsockopt error\n");
3 y9 @% F& }( R7 y; L - }( ?' v% w+ T; N( D
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);5 V% Q6 Z4 k! |; z
- if(ret == -1)8 _, s, h+ a1 ^* A/ ]; U" H
- {& {: c2 C- `6 {5 |; j
- printf("Tcp bind faiLED!\n");9 |, n; G$ t' i* ?9 u5 m/ H2 Q
- return -1; E# s7 h) s, Y# w% r1 v
- }
O- {3 O* ]% [8 @ - if(listen(*socket_found , 5) == -1)2 B, M* ^5 Q. ^7 l* a" @1 M- N; _/ g
- {
; l H" d- h, ?) K - printf("Listen failed!\n");
& U3 G h- B: g5 i% g - return -1;6 W. R% Z; h2 \
- }1 ^, y) N$ c% ^# I ~
- return 0;: x, a: f1 p6 b
- }
复制代码其中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);
6 f+ ]6 D- d; B - void * Thread_TCP_Web_Recv(void *arg)$ b* G3 X! F! N7 h
- {9 Q. V* Y5 N4 D; q5 [( v0 |
- 。。。
8 K% _1 j$ u* X9 ~2 V. S1 G1 ] - while(1)
0 u" t& F9 l# M - {
9 c- h w# y' i0 i! P0 {. ` - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
0 d+ x$ [# B* S" X7 `5 P1 k - printf("fd_socket_conn = accept()\n");
( E% B2 w. T/ M9 L: {8 q, p0 R% H& | - 。。。
0 W1 ~- I& x! y( w - recv(fd_socket_conn , recvbuf , 1000 , 0);
. P1 h" [7 M6 D0 Q( m$ x1 z( N: i - }
+ {* O$ L7 p( {0 [, C% ~ - 。。。& S* A* A1 [0 X1 m) S& ?# R1 t5 n
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
9 f; z, u2 a* k3 d- s# a* J# i - pic_tmpbuffer = pic.tmpbuffer;
$ d+ L. H- |& N0 P - pic.tmpbytesused = buff.bytesused;$ l% P6 K) I7 S, P2 I: u5 F2 b/ t
- pic_tmpbytesused = pic.tmpbytesused;+ A# Q0 e# O6 s
- pthread_cond_broadcast(&pct);
2 n" x5 r% ^' v - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;) y5 Z- A. N2 {: l$ m" A
- pthread_cond_t pct;
J" S6 ~$ o9 l - int main(int argc, char* argv[])/ Y! `. |$ n7 |2 v* A( n
- {
0 ~3 ^0 H* v) A8 u - ...
5 M: c& s1 Y% {* f0 O" k4 b) D - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);: B, O5 i( h5 l' x* R2 R& @* f% l
- pthread_mutex_init(&pmt , NULL);
0 ^ W! v* p' X& I% h - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);. h2 ~2 P% P# Z$ g
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL); e2 t; Q L( @6 O0 `* O1 j) u- Z
- ...& D; m6 ~2 h \
- while(1)
" V& H7 E" g5 x. v2 | - {
& \/ }# K6 w; \6 d$ M - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);7 A( L, Z0 O2 \
- ...% O d% S d P& | a$ `: h
- }0 T! U- t6 _' v9 g& ~* S( n
- .../ N* }% x. [% H1 D
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
- z$ _' g5 _; ?0 {$ j/ `; n - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
# a g( @ _7 T0 ?* U" K! Z - "Server: MJPG-Streamer/0.2\r\n" \7 t0 M* _" k6 N( j8 B! `! i c4 D
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
4 X4 U3 e3 l2 d" S, y- { - "Pragma: no-cache\r\n" \
. n+ @6 l' x# T( Y j7 I( a - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
% e! i/ }$ |; `/ } - #define BOUNDARY "boundarydonotcross" I) }# k3 D3 k5 T( g
- printf("preparing header\n");" a$ i0 t7 {1 W O! ^
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \) {& z5 M- X/ {+ ~+ p1 x! {
- "Access-Control-Allow-Origin: *\r\n" \3 D; L' d3 B( p8 ?5 e9 _2 j3 `
- STD_HEADER \' Z/ b: z4 @9 Y; w3 B7 p0 S
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
7 x( m/ S' t& j - "\r\n" \
$ w2 Q. r7 f: a) K J+ _ - "--" BOUNDARY "\r\n");; U# q& j) w( C1 T) Y& q7 r* q( b' f
- if(write(fd, buffer, strlen(buffer)) < 0)
U Q: i' T4 Y; r9 ~9 b& v - {" Z4 L# s( h7 w- o
- free(frame);. Q$ J f: j3 _ Q7 E% i
- return;
" G4 y, b1 ~7 W" A' K! A* 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" \5 @9 ]# |1 X- w7 `5 c& c2 ?, a- N) {
- "Content-Length: %d\r\n" \, z( G4 e0 E# k; g0 S
- "X-Timestamp: %d.%06d\r\n" \* e/ C6 g& X2 k) j6 _ D4 m
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
% c* f9 X# B9 j! K - printf("sending intemdiate header\n");
1 E" L# W, g8 z! e) o - if(write(fd, buffer, strlen(buffer)) < 0)
$ ?; g/ J' G5 l+ k* S' { - break; P( c1 a$ H8 |& e( U& d' ~: C: M" P* v
- printf("sending frame\n");
/ _) t+ L6 w, Z7 P - if(write(fd, frame, frame_size) < 0)
! J1 a! z4 D1 U7 g - break;
T# }2 [: C0 f1 A. ?$ J$ y - printf("sending boundary\n");
h( l, ]8 T9 g - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");7 j7 f! F# H; o( h! I3 a" o( k
- if(write(fd, buffer, strlen(buffer)) < 0)
( \$ K) @+ c5 f9 \ - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  # F1 i% L c4 L ~3 c5 N0 |

4 |5 g& u1 Z4 g$ L, W! |6 [3 n8 ?二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
) K# U7 g0 B5 ]# L" w- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)/ V- [; @+ X$ h; G' X3 o* i
- {( }# O7 ?7 m Y0 r
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
& V1 W- i* @9 U2 C. a - if(*socket_found == (~0))
* C+ J6 e$ s( j6 `0 W - {
9 u4 h% ]' I1 e! X% m6 x - printf("Create udp send socket failed!\n");) p: k2 P' A# n' F( Y
- return -1;
: b$ F& Q! h! g2 m2 ^: {+ Y - }
- b/ r P( |; S' [ - addr->sin_family = AF_INET;
/ v$ O7 K, {, M+ G5 o: @( Z - addr->sin_addr.s_addr = inet_addr(ip);
6 }* I, g) i; @5 C+ _ - addr->sin_port = htons(port);; C s. s& g- u" ?4 H D' k% |
- memset(addr->sin_zero, 0, 8);
3 W* u) ?4 T- @9 `+ W2 s - return 0;9 C. i) B2 l9 {0 V" {
- }
复制代码 , v7 O# X9 e# q
- b7 c6 g ^$ Z9 h/ J
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:( ^: a# B2 m7 T& b% ~/ v) z
4 _; m0 @, F. C6 H1 [
! A+ n4 q' H( q% p+ o& C- while(fend > 0)" C# x O7 n7 Y% M& I' j
- {
" ]2 e4 S8 K* _8 |) c% j - memset(picture.data , 0 , sizeof(picture.data));6 L% E: O0 s0 X+ U9 G" E% K4 S
- fread(picture.data , UDP_FRAME_LEN , 1, fp);
' S, `6 j7 L0 V; p2 m; r - if(fend >= UDP_FRAME_LEN)
$ ]; p- G) e, |$ m6 M - {
4 h( }5 F' b4 b - picture.length = UDP_FRAME_LEN;5 |2 J+ g2 K2 H: Q, R
- picture.fin = 0;
# ?( u" Q H' Z7 ~: E3 @* ?# _, h' | - }1 k9 }- W' |6 C" W4 s' N$ E
- else3 d2 U0 o: d! u6 \- X: u0 g
- {: u5 d8 c. a, V6 p
- picture.length = fend;
+ P/ _# d h# `& d3 k; g - picture.fin = 1;0 R' C5 ?* ]2 x: F' y2 G) H$ C
- }8 O% `7 P4 L5 I4 j
- //printf("sendbytes = %d \n",sendbytes);2 M4 ?+ s8 U, p4 k
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);3 P# D8 H0 O! y9 }2 i* t- D
- if(sendbytes == -1)( {, K$ v& l1 A7 k6 h+ R& m8 U
- {
2 N4 [! g0 J) e8 V - printf("Send Picture Failed!d\n");2 x8 O; n1 c$ e: m
- return -1;: V6 f% r3 G0 I) `# d" f
- }7 a3 s; z6 b( b* B. h6 f# O! O. A# v
- else
]4 W# m4 w0 j! x: Z+ J* a$ y) ^/ | - {
1 C5 h4 R4 I% j; | - fend -= UDP_FRAME_LEN;
* ~- A k, ^" \+ l- ~ - }: A* y; v4 c- g+ A
- }
复制代码
( U. n! C$ a6 V' o& O6 f) A. b; m \$ d8 {1 E @
 # E3 u" w. G- }/ p: E. f6 O* v
5 o8 n3 D( |9 B5 J) ?: NiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |