本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 / @; C( h7 I `3 X0 V2 q8 r+ @
7 m/ }3 v6 ~) T1 c F: n8 @+ \6 G3 @; @6 [
作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html0 {& J- c9 [8 L7 ~ |! C
4 z& v: @1 ~0 E7 D2 J# R本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 & I( d: m7 i/ u* X$ w+ S) W

_: n# o+ D% L! I$ z- U3 z- U, `两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 . I) b; h! [2 w9 O
一、HTTP网页服务器3 }3 H0 |/ b' p( c( N
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
6 ~5 g l/ d) g - {
" ?' Q6 h) i4 b# Q% E" X - struct sockaddr_in servaddr;
- ] N9 f9 S( q - socklen_t addrsize = sizeof(struct sockaddr);
: m+ l3 M2 p/ h; i7 T - bzero(&servaddr , sizeof(servaddr));
# h$ n* Z" Q! ]0 [' N% a. h3 T - servaddr.sin_family = AF_INET;
( ?5 f E+ s7 ] - servaddr.sin_addr.s_addr = inet_addr(ip);2 W/ Z! h# ^2 N% }' K
- servaddr.sin_port = htons(port);
+ l! y0 r# R! h9 m/ n( Q - int ret;/ H, i8 i4 m. C" }5 s3 o& @5 _
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)6 f! |$ q; y( J' d5 o
- {
" l" X$ t! F" y0 @$ K6 a" o - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
* h! I5 m/ p9 e5 T! d7 C+ x( z - return -1;
9 d5 R* G+ l3 W+ _, G' O% J1 P* e - }5 V, k# _- {; Z5 |6 D, ]0 R
- int on = 1;- O; j9 i, j. J3 u' l
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
& G0 u7 Z+ @" z8 Y8 Z. S( E - {
% z% z8 B( A7 L; R# B - printf("setsockopt error\n");
& B) W; `8 k4 k: j- L& o( V6 [ - }8 O. d$ x+ D3 `0 B& c& A' j- V5 [* F
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
9 W5 s2 p, p' F% |# S - if(ret == -1)$ Z$ `/ f7 W0 @% E3 t
- {$ u5 }% a' v% T" u
- printf("Tcp bind faiLED!\n");/ o6 ]; \* U4 k$ a8 a
- return -1;
3 r8 R: n, O% V) G: F; k [ - }
8 H: \/ a0 M4 Q( F3 c4 ` - if(listen(*socket_found , 5) == -1)' ]4 d& f j* B" T0 i* A
- {7 ?; }0 @- @% v; f* m
- printf("Listen failed!\n");6 `4 f+ u; B% B! w
- return -1;
2 h" t1 c, D+ P& y% M# `6 r- f - }$ Q1 O2 M5 u; m% s6 o6 Q
- return 0;0 h( q" c4 x! @/ k- o& C
- }
复制代码其中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);, w( G% g' T* m7 I
- void * Thread_TCP_Web_Recv(void *arg): z9 M6 x# X& f
- {5 a, n: S: y$ E7 V
- 。。。
& C7 i# O- C G) I - while(1)
2 w! o; r+ i ]/ c) M# w d - {, ^( _# o8 p7 P7 j; _
- fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
5 x3 t! l# f0 k( ~; ?5 @8 ?( i4 t - printf("fd_socket_conn = accept()\n");7 z$ g3 r8 u7 E7 t; g/ h
- 。。。8 r4 q, e0 O2 {- l4 G U0 u
- recv(fd_socket_conn , recvbuf , 1000 , 0);
& l6 H4 f; Q# c. z3 f) u9 ^ - }
4 Z5 C* {" m. a2 b - 。。。2 \# J m3 Y6 {8 N- e4 G! H8 l2 I+ |! W9 d
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);6 N$ i/ M2 W- F6 y& D0 o
- pic_tmpbuffer = pic.tmpbuffer;9 h9 {0 {, p8 T' B/ X
- pic.tmpbytesused = buff.bytesused;' d$ h+ W8 r! B0 R. n X+ Q8 y2 W
- pic_tmpbytesused = pic.tmpbytesused;
% g- n6 P1 @0 M$ _ - pthread_cond_broadcast(&pct);7 ?1 d8 G6 G% e- y7 ?/ b; K" h/ z c
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;; F4 ^" \: E/ M V" y
- pthread_cond_t pct;, n+ Z3 H2 A6 Q. _, O+ y. m
- int main(int argc, char* argv[])
8 f4 ^, |' ]! k6 x% x3 i& e) ^: n - {
, t. I$ v/ H, \- A$ \1 {2 H - ...- r4 x$ n5 z/ U! e1 y! d# B
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
# V! V2 [: A$ c# G8 r- Q4 P - pthread_mutex_init(&pmt , NULL);* C6 ~* F u& J9 L" k9 G
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);( K1 K$ n% y: i- p V
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);1 D$ H! \7 A) |
- ...) P$ o. |' R6 z" O7 b% s
- while(1)/ t' ~5 w2 Z1 ^8 c+ E
- {
2 E# S3 s3 o% P. y" ` - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);& Q9 X* ^% s. L O3 H
- ...
' L% M2 K, D M' ?6 G9 C6 V - }( T& g B( M% h5 K
- ...2 @& |- C* Z+ C. _6 [) m
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">4 k# N+ k- P! |" k$ E# {4 g7 S( U: x
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \, g+ L' B0 L: t& E, E0 t
- "Server: MJPG-Streamer/0.2\r\n" \
, t2 M- |- k% X - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \: |5 u! Z: e0 M8 t
- "Pragma: no-cache\r\n" \
# k8 M3 l; K8 k# Z F - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"! T/ F% T& m8 W& R* d* @. C. J9 m
- #define BOUNDARY "boundarydonotcross"
' W+ K( o4 e- t - printf("preparing header\n");% A) J% ?" P5 `" `' L, g$ b! \
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
' G @1 ~ W5 d" W1 G6 I7 V& E, ? - "Access-Control-Allow-Origin: *\r\n" \% u4 {- I; ~7 Z; A
- STD_HEADER \
; Z6 O8 E6 G8 v1 j4 H& f; D - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \ U4 m2 D, _' |, u
- "\r\n" \
1 b% O1 ?* z( Q - "--" BOUNDARY "\r\n");
4 O; D% r5 h" J Z2 { - if(write(fd, buffer, strlen(buffer)) < 0), u0 R6 _; G3 T; Z6 w& E4 V7 _' \
- {
" t# [$ `0 D2 G. u' J8 ?# j( l4 B. j - free(frame);
& p# l6 w. D5 k0 y/ G$ U - return;* l G: M$ S8 f+ f1 v, k
- }
复制代码发送完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" \
/ _7 W V# H; l: r - "Content-Length: %d\r\n" \, Q: s1 i2 T" ~+ X
- "X-Timestamp: %d.%06d\r\n" \6 \' t+ T$ w2 {+ F, p! r) I
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
0 I7 n) s) D% q; t0 t - printf("sending intemdiate header\n");
4 _& e' U& |+ E; O$ B {* _ - if(write(fd, buffer, strlen(buffer)) < 0)
! d5 J0 V4 y* j, u - break;
4 L `) Q! n$ f- ?2 p0 b3 r3 c - printf("sending frame\n");1 ]' V2 s2 u6 M* z2 c
- if(write(fd, frame, frame_size) < 0): a$ r w: s5 d4 y/ g4 x
- break;' `8 w i3 w. N, Z S3 P# d, o
- printf("sending boundary\n");
1 v) O% E! S& X1 L9 l' } - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
! b# | q. K5 q. }9 R - if(write(fd, buffer, strlen(buffer)) < 0)
% v) P" G8 p) Q. r5 A" X/ _; H" h - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  9 L- |# J( Y) N; J

6 X5 c5 t$ ^2 O& c1 E$ R# H二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:) f1 y# u% F6 |1 x; M0 C9 C
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)& M$ T, ?6 [) f7 X& P
- {; n( `+ ?+ o; B Y5 B, H
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);3 e% @; P2 P9 ?0 f8 a, O3 _" X$ P
- if(*socket_found == (~0))
& i$ |3 k- S3 k4 U9 e - {" q+ O- S9 b1 ^/ `, C. c
- printf("Create udp send socket failed!\n");2 u; W. D. C/ c
- return -1;. s7 u" X9 u: H, S) s
- }' J2 L2 K/ n0 m/ {
- addr->sin_family = AF_INET;
8 J6 b( H4 U0 t - addr->sin_addr.s_addr = inet_addr(ip);1 m( s* Z9 M. P1 P: ^3 X1 v& d
- addr->sin_port = htons(port);
3 v3 L; M( y5 }/ L$ P6 W, ] - memset(addr->sin_zero, 0, 8);
7 g P u1 u: j) F) | - return 0;
! k* C3 z1 L$ O - }
复制代码
2 m) f3 K; x4 k' T4 y
+ P" k' `* @+ r而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:. O' A. G2 w- g& R7 a4 ~6 W
# T. o% s5 H# e5 |' H8 L: {2 M* G! E# _
- while(fend > 0)
. H( p) B# {0 h, l4 u - {) x8 z1 L+ U# h4 G& a6 n( _
- memset(picture.data , 0 , sizeof(picture.data));
: @+ C8 m; n4 R6 ^" R; ^9 ~ - fread(picture.data , UDP_FRAME_LEN , 1, fp);
2 y& i/ X1 T! c# n - if(fend >= UDP_FRAME_LEN)0 C, a Z; W# O% Q6 X
- {
- n% |1 y. \# f" I8 ~8 m - picture.length = UDP_FRAME_LEN;% D; L1 c: D( G [
- picture.fin = 0;9 H8 h6 {; n" E( a. D9 ^6 q
- }# Q1 D) C7 c5 m. s" V; v) a5 j
- else5 y2 k! i8 Q, ?# S
- {
( l ^3 s2 a9 R/ z$ b* g - picture.length = fend;- g1 D, P8 ]6 }$ t+ q5 f/ \. @
- picture.fin = 1;
& [7 X( Z' x( [" N+ ~+ Y. v - }
x: j" @8 |5 ?* X5 d7 Z6 F! G! x - //printf("sendbytes = %d \n",sendbytes);
$ ^, ]: N: S0 ? W - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
+ Q9 u0 X: h1 D& ?& C0 L- F. r - if(sendbytes == -1)
8 x% Y* y1 i) t F - {
7 e \' \/ p3 {, A - printf("Send Picture Failed!d\n");: X" q. U& E! N, c, e! s+ n
- return -1;
U0 [$ L/ F% j* Y. R6 z( Y - }
+ I- k' B% l6 ?+ I - else
4 r+ B( \+ a: x" O5 S+ t) { - {' `! `3 a# r# m! W8 n
- fend -= UDP_FRAME_LEN;, `8 j" S* s8 V0 ?
- }9 k7 X; i; i* I( h' a3 C
- }
复制代码
$ Z+ B5 `. w1 k- `2 |& A/ h3 C; e$ {/ D
 ) Q. L! F& \ {. s* T* o: s6 ~' {& n
6 e6 {3 S! [; M1 l5 N" DiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |