本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 : t# U% T1 X" w. ^5 }
# u. b" w) G7 n' B j0 e! c% u
 8 O0 j: ^. P+ X" ~) Y
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html9 f# n& D1 ]' Q2 h) d
0 m, B) L; u" W7 h; o5 j {- ?9 ~
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 + o% P. R! O0 l
 2 H& F0 i: B7 h: x, l q9 \' v
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 " O! H' j- H! L+ B/ ^
一、HTTP网页服务器
7 f$ F4 ]' d) Z先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
$ G* x$ h" x3 R3 A' ` - {
3 `) x% Y- e3 ` - struct sockaddr_in servaddr;
' q" `' G$ K/ e l0 C9 U' e4 z/ ] - socklen_t addrsize = sizeof(struct sockaddr);' x9 Q; k2 D* ^ K9 L3 O
- bzero(&servaddr , sizeof(servaddr));
! g! ^; m2 Z% R8 u5 w) w - servaddr.sin_family = AF_INET;$ M6 \1 s8 i7 @' }
- servaddr.sin_addr.s_addr = inet_addr(ip);" u7 O- g- i/ P; U4 [! c
- servaddr.sin_port = htons(port);
3 w. W2 \8 r) w - int ret;- y; B& @4 X {# J" m
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)4 k( J1 N* H' [" n" P
- {9 ~2 {5 }0 y( ]4 ~8 i% F7 W" x$ m
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
1 q# H9 N! y0 u4 O/ o - return -1;
! e) h' ?5 _+ N( O$ b$ R - }1 i5 Z; c3 g" j( z+ i- w
- int on = 1;3 n1 k: |; W- ]% _; Y- D( \/ u" s9 K
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
0 F$ ^- b/ s' K B; W. } - {
0 u* M! `% V! z+ \+ q/ I" Q - printf("setsockopt error\n");1 r3 X" B2 d, q6 [
- }. U7 `* F6 X/ }% ~; q4 f% B2 T
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);3 }2 U" j# X3 ^& k! k, s
- if(ret == -1)
$ i5 g2 Q L( g - {
( i8 {* k3 R) ^5 V' F" s9 K - printf("Tcp bind faiLED!\n");
) m" l7 ?( u! |9 l - return -1;
! a4 N% F4 f+ s& S ]; ]( t - }
' A& m9 O9 S% ]/ U" |6 ^ - if(listen(*socket_found , 5) == -1)
# |' u# G ~ O; N( k# t - {
! f/ t. O& H$ v. b" z2 I3 n - printf("Listen failed!\n");
$ u% p. R! n- K+ w - return -1;
0 I6 C$ L4 Z! ^; i - }, S4 F. P1 g- Z& K: W; w8 P
- return 0;) M: w9 ]2 a( N# u# F
- }
复制代码其中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);
, ?* E" @, o. L3 H* _+ j' x! o - void * Thread_TCP_Web_Recv(void *arg). C1 n3 Y, A8 K4 M F6 u3 ]) }+ D
- {
k {, \+ P5 R' V6 d, \8 ^( l6 \ - 。。。
# L# R8 N$ \6 a2 B# w - while(1)9 ~3 {3 J* \* P) {0 c/ p5 b* O5 v; F
- {
) u7 p: C9 y8 N' e - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
k! F- j& X8 S Z1 j3 B - printf("fd_socket_conn = accept()\n");
. r. @7 W1 n& X; V5 S - 。。。; Z |6 K4 K( M4 \* x
- recv(fd_socket_conn , recvbuf , 1000 , 0);4 O! a) P" Y; ?1 d, B0 d
- }" n8 E% N% B( t) |
- 。。。6 Q. h% {: V: A9 T7 l
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
0 r% Q3 ^: H) } ^- `5 m% \% w - pic_tmpbuffer = pic.tmpbuffer;
9 W( h- {; Y1 `' v5 @3 a& B4 P - pic.tmpbytesused = buff.bytesused;0 E6 o. w& b( u: L$ V3 z, `
- pic_tmpbytesused = pic.tmpbytesused;# b5 X) F# _/ p# W9 G8 H
- pthread_cond_broadcast(&pct);! D9 N C3 ~2 r/ p2 t( X
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
5 O' E5 H% i5 R% Y0 W M6 c - pthread_cond_t pct;
, T; M: W* D- o6 r+ T - int main(int argc, char* argv[])
) d; x o% d4 Z( Y - {( E! ^% R: |0 D
- ...
8 r% h' j2 w/ i* X! [: [9 U - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
3 @1 m: l( ~( R* { - pthread_mutex_init(&pmt , NULL);% a* j- h: a( a+ D
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);3 _- {; @+ A$ c( T% j; S
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
5 U* e4 J d. |; q5 y- j, w - ...
5 A" A8 C4 B* |* e/ F - while(1): _; z {& c4 q# k) c! R
- {- W" U7 [0 D& X8 F. W: V
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
5 U; x$ S# a+ m- N8 r" s, t - ...
2 r* x- z& m/ G8 U - }( Q5 G- |, g1 D/ K9 d
- ...
) [; R: I" a" N1 _# A! X. ]- o - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">9 [6 M3 T% }$ p5 M s/ @( Y3 _7 a! _
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
! y0 q+ m2 J- O! k - "Server: MJPG-Streamer/0.2\r\n" \# ~# Z+ Q1 f# u- S) E [6 F
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \' z; B6 ~/ \6 w3 Q% z/ K- t
- "Pragma: no-cache\r\n" \+ d& O" J/ V5 Q; V# v
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"! \) W" q) v$ p2 i1 {( y6 J
- #define BOUNDARY "boundarydonotcross"2 d! {" S1 F7 V) `% F' {( m+ e
- printf("preparing header\n");3 z5 c0 _' H$ Z* |: n _ _6 R3 @
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
! j6 \& B& F! U1 M* P# O - "Access-Control-Allow-Origin: *\r\n" \* I3 F7 ?* z, q& E$ d
- STD_HEADER \
, e& P2 k' l# g - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \- K% ?; W& h2 ]! d/ p
- "\r\n" \# D8 r; R. h2 \' x
- "--" BOUNDARY "\r\n");- N% X' ?+ V( p. j6 e1 ^. H! A
- if(write(fd, buffer, strlen(buffer)) < 0)( F, ^3 g# z, p. B& W5 h
- {
g+ |" C- U0 `+ m; ] - free(frame);/ Y* j2 o7 y( G" O: O* L
- return;, g* ]5 O4 n* K1 N0 _7 k& v" O8 @( `
- }
复制代码发送完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" \) I% e* a+ G" u5 r$ b6 S
- "Content-Length: %d\r\n" \
7 N# H1 Q2 ?- z: d# ]& C8 P6 ~ - "X-Timestamp: %d.%06d\r\n" \, l0 J) a- W! S1 [/ K
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
. Q; o+ {; [5 c3 ]+ u6 P4 ?9 P2 M$ P - printf("sending intemdiate header\n");
) X) f H% p( h/ a# H - if(write(fd, buffer, strlen(buffer)) < 0)0 g/ E; j# X0 Z
- break;& D4 d1 m h4 k6 K. R/ p( P9 M
- printf("sending frame\n");3 O8 N; ]$ [, `6 Z+ S* ^4 C
- if(write(fd, frame, frame_size) < 0)
: K/ I/ _+ p+ G0 C, e$ m+ J - break;# C9 K# X% D8 s- u$ y: F& F' Y
- printf("sending boundary\n");$ }0 v2 R) k$ M- @ C9 \
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");" l: W; b- ?' b2 V4 ^7 q$ |
- if(write(fd, buffer, strlen(buffer)) < 0)3 Z/ U% @2 ^$ r! N
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
% t3 b4 v6 g7 T2 y- j , b: ~% M/ N7 o6 p5 e7 q$ {, S
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
1 s V4 y- T0 h( S- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
9 c" O* o) X: ]# A - {8 r4 f: W' Q) p
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
4 U$ B, }- K2 I: P% w6 [ - if(*socket_found == (~0)) H' a# F- o+ d" E5 D9 r" a, m( _
- {9 X8 c$ M* ?$ j6 N
- printf("Create udp send socket failed!\n");$ ?# W* u, o5 \. t7 \* {1 V
- return -1;' X0 H0 j* L2 f$ U- k
- }" f0 L! x5 _9 z# h
- addr->sin_family = AF_INET;4 @% `& q+ u, ?" X5 x
- addr->sin_addr.s_addr = inet_addr(ip);
q% u5 b T( Q$ N1 Z; ^( J) l# V* j - addr->sin_port = htons(port);
+ K& I7 H- p. E" Y - memset(addr->sin_zero, 0, 8);+ x0 f1 C+ q1 @/ ~, M! K2 r* P0 K
- return 0;
7 i- @7 B- Z/ ?- E8 {3 ~8 C6 w0 q - }
复制代码 ) D O' J. B( H( t P( i
; \0 l2 b. f5 L
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
; q, q& y, _1 i, m- f7 A& j5 u
0 Z3 t: f, z% v4 w$ y9 _5 i" x! O7 x; I' F I( v
- while(fend > 0)7 @' `3 L6 i6 d. H$ g4 G
- {
5 M. y/ n5 U' H - memset(picture.data , 0 , sizeof(picture.data));
) u U: t* d8 v ?2 p - fread(picture.data , UDP_FRAME_LEN , 1, fp);, i$ }& f" Q& T7 X) K
- if(fend >= UDP_FRAME_LEN)& ?% E% ^1 S- `. |! [7 F8 N( C
- {
- ^: M: s& T9 c0 q& | - picture.length = UDP_FRAME_LEN; X! o* ~0 m3 i4 u7 p
- picture.fin = 0;
4 A. G7 ?7 g6 j; e0 |) N+ U - }+ {5 b9 O& L* l
- else& I* m3 F+ h: u5 Z
- {
: u$ f9 t5 B' ]" n0 k - picture.length = fend;
" v! r6 n. e# J2 x k8 ~. O* Y- g - picture.fin = 1;! @8 `: ?. ]4 }* s1 N' _1 @( [
- }* S" Z, R, ?) ?0 q
- //printf("sendbytes = %d \n",sendbytes);
" C/ P2 p7 y) @& B1 u) R( s, {1 ` - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
6 P9 o8 C: V2 b+ N7 O - if(sendbytes == -1)
! w/ p% A: C8 i. m - {$ n8 t" o4 T; \; A
- printf("Send Picture Failed!d\n");' L! \& k+ n2 w! \; a+ G
- return -1;4 G* B0 t2 v! L0 O" ?8 G g$ j" W
- }
. z6 O* @" c; d3 O! Y* _1 A - else
" `% Q* J: f& l - {
1 a, d. C& o* |1 C - fend -= UDP_FRAME_LEN;
6 d9 i1 f: O4 P( t* U - }
; X5 O6 ^' z9 i* C% S - }
复制代码 ! o1 r2 j& c5 Z8 G4 s: `+ q
# B0 F# T: Q! T! H! s7 p6 t

1 ?4 V4 B, S- q
1 p# H) v. b% Q) O9 g% FiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |