本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 . I& V0 z. H) C7 B
7 v8 Z2 Z- V" P y# |' ^4 z
6 ^7 ]2 ~) i$ }# @/ R+ d作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html/ D, M" w" A Q( ~) S
: y0 p# z# C* V4 ?# v2 P1 g3 o本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 : i# h6 u9 S0 Y- `! J
 - v! A: M2 O1 l1 g
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 + N5 D( T! ?* i4 i9 f* O
一、HTTP网页服务器* D/ {1 o2 [. i4 J% \
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)1 d2 b/ B; Z% I( K* i2 r
- {
# _" ^4 z" W; _8 |0 D4 u! N - struct sockaddr_in servaddr;0 B$ I' u( H' A$ E! D' t/ S
- socklen_t addrsize = sizeof(struct sockaddr);
2 w* q! l+ @$ }6 `) A. g; S - bzero(&servaddr , sizeof(servaddr));+ C; ]3 ]4 X' Z+ c% h: `4 R' t/ I
- servaddr.sin_family = AF_INET;
& m) e9 N3 {0 K9 `: [ - servaddr.sin_addr.s_addr = inet_addr(ip);
$ _( K; [- d8 o$ B5 j: } - servaddr.sin_port = htons(port);
7 F) j; F3 m8 T9 j7 \* B - int ret;
3 l3 z \* }( x" i! f, z e - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
/ [# b5 r4 R, s! h - {
5 M. b( O9 _0 `" Y/ M - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);' X/ R0 a6 u) y! j0 n r+ y
- return -1;
( X% {7 j* B+ }! [2 j" p( x; R - }6 ?7 Z/ @8 Z$ ?
- int on = 1;! c) H" P# K ]- V8 {% P
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)# N5 T+ {5 P' Y) D
- {, ~, b3 F8 C# m6 O
- printf("setsockopt error\n");
' u* e9 y- G. s7 w - }
: P0 m" }+ m |2 A& J- {! Y/ O - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
( H" M' c1 x8 w& S, H5 o; q0 t - if(ret == -1)6 v& t& @0 n! a/ C
- {$ n2 Z9 q; R. B6 U
- printf("Tcp bind faiLED!\n");
7 _: |% U9 a) s - return -1;
& Q K* _- i$ f2 B" C - }
5 \% E# G/ Q$ ] - if(listen(*socket_found , 5) == -1) d3 o% W+ q$ g1 \+ S$ ?# U
- {5 a) ?5 l" n# o, n
- printf("Listen failed!\n");7 P+ M7 p3 [3 r2 u5 G2 Q6 E
- return -1;
9 Z/ n: E7 d# z9 Q3 A. k7 ^1 {+ o - }
; e- S+ {. }3 G h, b( W - return 0;& t( X" i& I, ^2 C# J& 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);* M8 k1 U& R$ c4 [
- void * Thread_TCP_Web_Recv(void *arg)3 Z( j m) o; W) _$ I$ _ A! M
- {" ?* r: T t2 l- {. I
- 。。。( ^, k# U/ `, m/ c$ P/ U
- while(1)6 _" s3 Q+ }5 d; ]) {( G! F+ U
- { ?8 k+ N7 t C# r. S* Z e5 v. S
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
: W0 f7 c: x$ `" m; c - printf("fd_socket_conn = accept()\n");0 R1 \. U0 n/ v- D, u/ |* G
- 。。。' F% V& J& N3 T6 v s; C- _6 f
- recv(fd_socket_conn , recvbuf , 1000 , 0);
- C0 _$ ~+ P5 Z" m - }8 z0 s6 N/ e+ x; z5 n" d1 u- d5 _% ~2 S
- 。。。
3 W7 a. ]' a: c - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
3 D4 q8 q1 @; c: v3 C( Z, y9 m - pic_tmpbuffer = pic.tmpbuffer;
% K' o' g1 F4 |3 ]- S* [+ h! }4 u - pic.tmpbytesused = buff.bytesused;2 f3 x9 X7 e9 N) h/ v
- pic_tmpbytesused = pic.tmpbytesused;) `! r5 g) n6 Z" F8 X3 A
- pthread_cond_broadcast(&pct);
) W z2 O s+ L. T) V5 o - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
& a0 q; U0 y+ w% j5 V; V/ { - pthread_cond_t pct;
( R. M1 H- j4 n - int main(int argc, char* argv[])
: x$ U/ E I1 {( G4 W$ \ - {
, x! |8 e- j0 l+ y# | - ...
2 E d5 K5 `- [1 ?* Z - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
3 o M4 R1 X2 ~4 [ - pthread_mutex_init(&pmt , NULL);
- r5 j% v3 S& ]/ T+ L, Q$ g - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
" v! D( o- k0 d% _$ f - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);0 c$ ^* |; r I; T5 J7 \
- ..." p4 F$ Z! `& j' X8 W0 Y
- while(1)5 m3 g: _: y6 F/ y4 q% {
- {
- F: i; O2 o) }8 O- Z* h1 } - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME); a2 `+ a$ l: K- [
- ...; v( H, p7 s& |* H$ l) }' Z- v: |
- }# k9 B2 N/ g; }: | Q
- ...
" a, W- o& n; C4 r) Y% @9 U - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">( L( b' E" u+ V2 R2 f& 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" \
2 O; ]# j, g7 ~9 j& e8 _ - "Server: MJPG-Streamer/0.2\r\n" \& _8 S c' s9 I
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \0 s$ T1 m$ {! k9 G" `" E
- "Pragma: no-cache\r\n" \
. b5 ]& F1 z. n3 T, z% e, w3 ^# @ - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
4 Z$ {# X7 W1 B' V$ _7 V8 } - #define BOUNDARY "boundarydonotcross"
+ n1 B2 f' ^( t; ` B5 \9 v - printf("preparing header\n");8 S. F }% @) U# K- v0 I
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \+ K( L" z. H5 V7 k3 m! b. Z
- "Access-Control-Allow-Origin: *\r\n" \0 {# a+ o% m4 j* k3 ?+ ~6 P# T! |
- STD_HEADER \
# M! e" R& x' e8 C0 n' | - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \" }9 l8 E2 R" g7 c8 D2 m
- "\r\n" \% Q4 l. L+ Z, C
- "--" BOUNDARY "\r\n");8 J* c0 t3 L- W4 n. C k
- if(write(fd, buffer, strlen(buffer)) < 0); c h' Z8 \. V m$ p3 V
- {, I4 b8 X [7 z9 u" o" i
- free(frame);! O ?4 W" m( w K8 O* W" K
- return;) h8 o$ a7 d$ h. Z, w% L, i6 l
- }
复制代码发送完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 f/ Z/ V( Q4 ?- j7 \4 F# W; E: \ - "Content-Length: %d\r\n" \# W6 Q; s" J P
- "X-Timestamp: %d.%06d\r\n" \
! ^6 Z3 p+ d* N( A - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);8 w: {9 Z L& }% K
- printf("sending intemdiate header\n");
6 ]. g$ @1 o" |4 c - if(write(fd, buffer, strlen(buffer)) < 0)% B- [- T4 E1 W
- break;
" v7 k! {! z ]- g) z9 n - printf("sending frame\n");
# c( X& v! x: W8 ^3 D# i" _# v9 p - if(write(fd, frame, frame_size) < 0)$ g) ]/ P* ]; d
- break;
- h$ `% s& d0 g' L/ ]0 I - printf("sending boundary\n");
8 ~9 Y4 P4 d) t% M3 e' ^9 Z% i - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");6 t4 G/ J" n ^% N* K9 y
- if(write(fd, buffer, strlen(buffer)) < 0)# Q" P. _' _/ U: e/ ~/ q& `
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
* P# H6 U# h# [* N% i7 D' @ & |1 W6 F4 ?+ ]) G- H: @# s
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:* w( P. `$ S: e+ x
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port), b- r) S! h/ e0 X F
- {* P" b$ i3 P' V
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);% X0 P$ S3 N. D+ V1 o7 ~
- if(*socket_found == (~0))
& D8 t: h/ Y8 T) p - {3 j4 d) u" Y9 B# F
- printf("Create udp send socket failed!\n");/ N) \+ S) x6 T) \" {0 u
- return -1;
W6 Q! \: f' ~# B( a - }1 T, s Z3 T7 L( h+ b
- addr->sin_family = AF_INET;4 a7 T) F8 ~. e
- addr->sin_addr.s_addr = inet_addr(ip);/ e6 H) S: b+ ~+ J4 L9 m; E+ i8 @, D5 v
- addr->sin_port = htons(port);
, m3 Y- {& O: G - memset(addr->sin_zero, 0, 8);2 ? n4 |2 D$ F& w
- return 0;
. u/ e3 I: P# k2 R+ C/ `( V) I! C - }
复制代码
8 r" }. y; k' F6 d+ t( D7 e9 ^/ o4 l# x0 m
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:( m+ I. l" ]0 T
0 ]0 E6 Q, O; g+ F/ u) l; X" Z9 \; A; o/ `" k
- while(fend > 0)
) `* \! F' x( ^# ]1 c3 R - {
) Z+ U. O+ k( H3 C' T. \) S: ` - memset(picture.data , 0 , sizeof(picture.data));
h6 c/ q8 u& J6 ]/ C$ i2 _ - fread(picture.data , UDP_FRAME_LEN , 1, fp);
; a+ x6 i. `( k8 y. I - if(fend >= UDP_FRAME_LEN) \+ W# k) e+ V& g: b1 e
- {: M+ l2 Z1 g; c6 f. c0 W. ^+ s
- picture.length = UDP_FRAME_LEN;( V/ Y7 g8 A, @% `0 c" ~; L
- picture.fin = 0;
: k' E( X- ], b d - }$ J* U. ]: u4 s# x4 L0 C3 J
- else
' ?' [" R3 S0 f5 h6 ^7 g - {
# p7 a/ S" Z$ f8 |! D - picture.length = fend;
/ ]2 K5 o% Q2 p - picture.fin = 1;
( C( f, u7 m8 V9 V - }0 G0 Q$ ^9 v9 D6 w) i1 N
- //printf("sendbytes = %d \n",sendbytes);# L( W, z2 X* z' q2 t' r( [# A
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);! z( u# E2 I* @3 o) W" X3 C. P6 M/ a5 f
- if(sendbytes == -1)
, E. c/ K/ h E4 s0 \! O# u - {3 {: @ x/ D2 d6 N0 ?, ] c. E
- printf("Send Picture Failed!d\n");
: x" f& E+ L, E - return -1;
: S1 z3 W; P+ j1 ]. A* q! J - }
5 h; B) V( i5 }+ r" D - else
* C6 W8 g( ?9 c+ s' v: C, o - {
4 c) v7 N) E; l) S2 l' ~9 h+ O1 h- R - fend -= UDP_FRAME_LEN;
* G8 d. g; H7 e: \ o1 H - }+ I: q( r5 |- R; H6 n' p
- }
复制代码 % G! _! p/ j/ t: v
6 Y/ G* |# N* T1 ? ' V V3 x; ]3 {- ?3 t) K0 t
6 e' t* M; ~7 f9 F# J% V* C) j x6 ZiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |