本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 ! I8 _) O' T6 n! M8 ?% ?% I& v/ q" ~
4 _* s( ?+ G, s, H 5 R: ]7 f% r( P& J8 b/ b b
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
8 I' d( F2 @( E4 s5 o, Z$ L+ Q
9 U3 e4 P' V2 s4 w5 p本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
$ N4 J3 q0 F9 k: N- M4 a; s 8 K- S7 z: f/ a7 ~& ?
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
% ^0 t3 l; {! n: x4 O( n4 }- m一、HTTP网页服务器/ y7 L: Y% o/ W3 _2 K4 b4 Q
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)5 ?, u4 ?$ L$ g+ r4 z: I
- {4 W# ]$ @, E* i2 ]$ U A
- struct sockaddr_in servaddr;
/ Y. k% h3 h0 l# o7 {8 \ - socklen_t addrsize = sizeof(struct sockaddr);+ Q) x0 j5 [7 i2 \- {
- bzero(&servaddr , sizeof(servaddr));
3 t" Y {4 L- r% h- z - servaddr.sin_family = AF_INET;7 a' ^5 M- r. j9 k; s
- servaddr.sin_addr.s_addr = inet_addr(ip);
3 q/ k0 g- R: T9 N9 g8 f* ]; D - servaddr.sin_port = htons(port);, f; ~5 g% H% C' l c7 M
- int ret;1 w& h$ ~' s5 G( z( a9 [ n: d
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
8 p: B: i2 q6 u) ` - {
5 d; S3 b4 v9 h0 [% i$ E - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);9 m8 ^+ T2 T# L
- return -1;
! K0 j3 n' |9 |, ? - }4 U; [- n4 @: v% n" P8 {
- int on = 1;
, X5 _& d3 z: Y1 l4 a2 Q - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)3 H& I5 t3 N; \9 i% c: l- V5 s
- {
5 [ S! f7 h8 b% e' a6 O - printf("setsockopt error\n");* j% n2 s! |" s7 A9 `. z+ n w
- }7 I3 H. c6 L3 p; v* \4 K4 s
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);8 p) \& V$ r/ g8 c5 m
- if(ret == -1)1 X |1 t& @1 L. ~. z
- {' o {) K+ j; B8 p$ f& s0 |+ ]
- printf("Tcp bind faiLED!\n");/ s3 q" E8 t7 B: i4 N1 y: f
- return -1;/ ^- ~. ~8 O; x8 \9 |, z, H8 J. r/ H
- }
! I2 j0 z6 |8 ~ |1 r - if(listen(*socket_found , 5) == -1)
# S o( Y* O+ K& h! ?8 D - {
8 ]. R% A- F7 a# t: ~ - printf("Listen failed!\n");
' f, g5 M6 Q3 y! q6 _ - return -1;
' ~5 d, S: i1 a5 [8 N! J) b8 z- t - }, F1 D$ K( }, w4 C, r& k
- return 0;
& S! q% n+ U& g( o, v3 O# G3 m, \ - }
复制代码其中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);% o4 f3 P6 i- {' \8 g4 p
- void * Thread_TCP_Web_Recv(void *arg)
2 x. X) i6 z0 |) N4 _ - {
+ B( q a8 H1 S! S$ E - 。。。
! ?$ k+ L2 F& Z+ {# j7 U+ t - while(1)0 R+ I2 Z" Z, z# w2 ]+ m
- {. Z1 O: }* s% _2 w
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
% x3 @& U9 l+ Q% ~ - printf("fd_socket_conn = accept()\n");! g X' x4 F' B: M6 R% K7 ?
- 。。。& L5 X+ h; y9 I) ?/ u
- recv(fd_socket_conn , recvbuf , 1000 , 0);
) v5 p1 z% i3 c ]- R+ X% E - }
2 W: |. o. R0 x - 。。。- Z8 t( T, @9 |4 @
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
/ e T" w9 x- }6 `% V) ~/ n/ R - pic_tmpbuffer = pic.tmpbuffer;& A3 u' P: M, c( A& G) E
- pic.tmpbytesused = buff.bytesused;4 a2 N6 l" k+ ?2 W) Q2 l9 ]( O7 Y
- pic_tmpbytesused = pic.tmpbytesused;
0 E8 `. F5 Z( w5 n4 x0 p6 h - pthread_cond_broadcast(&pct);
+ ]: I4 Z% r. D" N - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
; o7 b" ]. |+ b1 }; V' g - pthread_cond_t pct;
% f$ q! v' w7 [: X+ k - int main(int argc, char* argv[])6 s# v9 b9 W5 s n
- {% s# w% b6 e. K- E
- ...( z, o5 k' l7 t
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
% W. ~2 c# Y, M4 c' E, X - pthread_mutex_init(&pmt , NULL);
2 ^2 o8 P' M6 l3 d3 M, g5 c - pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
1 e. J" C$ i+ H8 S; j - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);9 [2 y, R9 d* ~. v
- ...
9 M+ K( j2 v0 D7 W% @# W+ d8 T - while(1)
9 u. D$ a: k- O1 N: b2 v" d$ i - {: ^& i6 R# F# F( |
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
( ]- v; v) V4 _2 B - ...
/ j' A, i! D' }0 i0 Q' b' B) C - }. y- |. ?. U- j" L
- ...
& U, c+ t# R9 M/ C" @/ U( K - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
9 c) i. h+ ^' k/ [; d- @; ^- C - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \4 G5 x2 O, N1 s# S5 @% p
- "Server: MJPG-Streamer/0.2\r\n" \+ v* W- F: A& l9 e h0 i7 f5 ?* F
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
0 K- J3 O, ~' D6 H% a8 z) e - "Pragma: no-cache\r\n" \
+ f* o }$ H% V" P - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
' r% ?$ s" f8 r9 ^; P; e; N - #define BOUNDARY "boundarydonotcross". Y( ^, `! s! R
- printf("preparing header\n");
1 C6 x5 L$ o, f0 h - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \2 n& z6 Y' c# ?7 _4 R0 U
- "Access-Control-Allow-Origin: *\r\n" \
. C- T- P- P, k! |: f% ~7 Y - STD_HEADER \
+ d- X+ E& L( a3 V - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
2 _$ l- g$ q9 [% |6 j. q/ o: { - "\r\n" \
7 N: v2 j5 g0 u6 l% H% H - "--" BOUNDARY "\r\n");
( K9 K' b4 V* o! G/ N+ Z - if(write(fd, buffer, strlen(buffer)) < 0): I; J+ a! u d; ^# \% k% a
- {- @5 D, {) J4 V# i" I
- free(frame);2 a" P% F9 g: H
- return;
; X$ U P' c* ? - }
复制代码发送完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" \
# R( W9 [2 c) U& G4 J - "Content-Length: %d\r\n" \5 H+ }, [9 w1 _5 e+ T8 B# o
- "X-Timestamp: %d.%06d\r\n" \1 L) Z) Y" F# R- i
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
: E( t/ X6 F# T7 [$ z2 n! r( k% L - printf("sending intemdiate header\n"); i' @3 V' p" p# q. N+ L
- if(write(fd, buffer, strlen(buffer)) < 0)
! O& {$ `8 w7 T! ^7 a6 }/ ]5 N6 R - break;" J3 S: s8 ^* N% N: {" s$ @. x
- printf("sending frame\n");. [$ @9 L7 `) w8 v
- if(write(fd, frame, frame_size) < 0)/ z5 ~+ q: `) V$ a% F" v6 l7 [
- break;
1 U+ J4 c, [% f" z- U - printf("sending boundary\n");; o$ [% H/ k, J# D7 \. Z- ]
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
; v: |" ~" W' y" } - if(write(fd, buffer, strlen(buffer)) < 0)
; I5 x% T1 z# K* O - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  0 o! Z9 ^; {, j D: P
 + P* h& }/ T9 M& H- i; r
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:! d2 ]# G2 Z; q
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)4 @$ k. H |& Q% e
- {
/ e5 i5 X, i5 Q" }* R% B - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);9 l6 @, S8 R4 T* E4 v0 O
- if(*socket_found == (~0))0 K2 I; w+ [5 U5 O. @
- {4 R) l; X _1 ~' U
- printf("Create udp send socket failed!\n");
9 O) M4 d9 X2 C2 L, s - return -1;
) i2 U. q% Y, ~1 q) I7 S* v$ }3 Q - }
: K7 Q8 t, n( Q! }' p3 p, v - addr->sin_family = AF_INET;
l( h: ]9 m3 I+ O - addr->sin_addr.s_addr = inet_addr(ip);. C3 p3 O4 \% x' e9 {, E1 [
- addr->sin_port = htons(port);
2 w, C P9 ~2 {) D0 `- t8 ] - memset(addr->sin_zero, 0, 8);
( W2 g7 s, C1 g( D5 B* A - return 0;5 k9 f1 V: T. S6 [( {. Q+ Y* E
- }
复制代码
: Y5 ^) z$ I+ R; j+ g( e! P% G6 e' h; o6 g* i+ C0 l3 W( E
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:7 p+ `& M$ ?" N0 y# v2 b
' P6 B/ S' w4 h* x/ P* `/ y. D' V3 O
y, F- x& ~8 n* V7 b7 H$ C
- while(fend > 0)
7 _& u6 d% @, s* ]4 d - {, T g6 v# ^$ ^
- memset(picture.data , 0 , sizeof(picture.data));: n. h. F1 |( c0 p+ {, w( ]; v
- fread(picture.data , UDP_FRAME_LEN , 1, fp);2 W/ c+ Z x+ N! m
- if(fend >= UDP_FRAME_LEN)
5 [* a, N" E1 Y" R3 y6 Y7 N% f - {
& G: c7 ?: P8 S: e. p0 ^4 ] - picture.length = UDP_FRAME_LEN;6 u/ D% ?* k; l, C( z' [
- picture.fin = 0;/ Y$ y8 f- J' u) j
- }$ O3 Q; h2 l% D ]9 u: C8 h
- else% a1 P/ y! E! X/ ^* O/ g
- {
/ f* q* N o8 ?* @0 w - picture.length = fend;( C( E$ N$ N% Q0 l! |
- picture.fin = 1;8 H6 n/ R1 @# e' H& b4 D7 v; e
- }
; @! _5 d) ^/ t7 I - //printf("sendbytes = %d \n",sendbytes);
+ R8 [' z2 k: A) R5 _; e - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);! ]; q# w8 T' {, Q. w+ J, _8 D: L
- if(sendbytes == -1)
2 W$ d8 l, @7 h$ n/ [# V - {! _8 s5 p& l# N3 D
- printf("Send Picture Failed!d\n");( Z6 ~) ]$ M& G* d* ?& j) P% I: R0 R
- return -1;8 m. c5 S5 s6 L n% Z6 t2 D: I( H
- }- a3 Y3 u' v6 @( \; ]% z
- else
3 u* Z4 X( N3 @( @ - {0 ?6 b" R9 b. n
- fend -= UDP_FRAME_LEN;
}9 Q+ B3 ^& Y9 \ - }/ O) j+ K a v4 ~" |! v
- }
复制代码 ! s3 F, d& P* @3 N0 |
; z: V# I7 W4 d* D+ B: b5 @: r! C4 Q : }, H- t+ _: t$ h1 D* D8 W. l
+ |! s0 }! i, v4 \( A% v$ r; m& S+ Y
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |