本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 " j' s, n/ i3 d9 Y
/ n; G" n+ ]+ W# W+ \5 P

; ?' k8 z' A: a6 h, i" Z$ k作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html- r) j1 B) q4 ^ B6 M$ i
3 Z0 |4 n4 j% u0 s8 z. j# {) V6 F! ]
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
3 g i& o3 V" R% w, d: | $ Z- Q5 N) V( |7 ` h$ l$ O
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
. t9 i! E6 l0 G: T: N2 a5 U. Z一、HTTP网页服务器5 \5 M1 s: y. \, O
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)' k' L7 p3 }3 j3 m4 p6 u1 [
- {
: g% }* r! ]9 ^ n( {& y: \, N$ I" B) J - struct sockaddr_in servaddr;6 Y; _3 s$ Y9 Y# l- |" `1 z. h
- socklen_t addrsize = sizeof(struct sockaddr);1 n3 M; d- B% L
- bzero(&servaddr , sizeof(servaddr));
# C$ v2 A& p2 F! K8 n% d! ] - servaddr.sin_family = AF_INET;. D$ @ A9 F' D; q* Y
- servaddr.sin_addr.s_addr = inet_addr(ip);
- R( A G$ ]7 R( R, L5 `9 t - servaddr.sin_port = htons(port);* }% D9 ]. @! @
- int ret;" r3 G' z2 a/ X8 k8 r6 b% N
- IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
9 D! m, ]; l: a* }) q9 D& n9 N! H - {! ^" g. ?/ M# ^' b' ^( P- i
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);6 X6 B8 o, Y6 W8 e
- return -1;
a0 x U0 W$ I$ _6 z# |' | - }
" ~- ^ P" m1 _ - int on = 1;7 S# |9 X1 {, A
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
8 Q+ H9 U1 [! ?9 W - {
0 D0 v5 g/ _+ c' H; @ - printf("setsockopt error\n");
0 A# V- h$ h) ]; ^: o - }
7 y* m& M* O. u4 E& g S8 g - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
% J `. Z" U% [ - if(ret == -1)
- a. S, ^/ X1 ^: Z+ |) s+ }1 T: w6 I+ O - {# v8 E$ h& _; s6 W" G
- printf("Tcp bind faiLED!\n");
: o$ `: \4 z+ z3 i - return -1;8 c: w4 i) v4 H+ O5 h! F5 q
- }
* I- Y% K9 j3 w! p - if(listen(*socket_found , 5) == -1)0 W. J4 V, F# D
- {
. ^: f" j/ ~+ p$ q) b a/ }- a: E - printf("Listen failed!\n");! d3 h, R. C7 R5 ?/ w
- return -1;$ O- y2 L- [' L# ?6 r! J
- }3 e: U1 H+ h, o! a: ~
- return 0;
6 B" U! Y: m! K3 p0 Z8 y - }
复制代码其中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);
( s+ H* A! p; _5 [; I; q - void * Thread_TCP_Web_Recv(void *arg)0 n1 t' I3 {: a4 j' q( K* m( F
- {
: f$ J! L' |) `4 E r7 e - 。。。
5 { I% Q/ c$ q - while(1); J( V- l% D3 P7 u/ l6 f; s
- {
/ M, U r3 r0 X5 K0 P# t5 b" A8 M1 T - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
- O+ x) X9 m3 H; c4 [' V3 r+ J9 Q - printf("fd_socket_conn = accept()\n");
" T7 h2 ?. }* I4 T' t0 x0 ~' w1 x( F - 。。。
5 r* |! i; }( b - recv(fd_socket_conn , recvbuf , 1000 , 0);- R' C+ Y, m# h# g5 P7 D5 p" a( K
- }
$ r- d1 R' n1 |% j - 。。。
" _0 g5 C4 R! [6 W) C - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);) R1 d; q& Z5 K' g5 e
- pic_tmpbuffer = pic.tmpbuffer;
& f1 ~9 d+ l4 ]4 v) t- h - pic.tmpbytesused = buff.bytesused;
' ]- e' _; X( b! q, j6 {8 N - pic_tmpbytesused = pic.tmpbytesused;
7 [# N3 N# @# `! ` - pthread_cond_broadcast(&pct);9 a* u! a( X+ ]# E9 |2 E
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
+ m' x( E# h7 Z: @ - pthread_cond_t pct;
' w. Y; Y! k. |8 `3 S) \ - int main(int argc, char* argv[])
3 v. _+ f" L! r# E3 z0 c. d) y - {* x" S. E1 F! h/ J4 Z
- ...6 q- T; ~% I3 B2 S# R8 E! q {
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
7 ]- j. R& @4 X7 u; t - pthread_mutex_init(&pmt , NULL);. V6 w. l" y9 D/ h
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
, x) X& S9 b: {# T - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
+ X8 J7 t @4 I' j; F- x - ...
% P0 S- R6 Y h4 ~& `& @ - while(1)$ Z- x! `3 ^$ D6 l% j9 N+ t% Z) _
- {0 j% Z$ r& H2 F. B% w, Y
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
# N) h+ ]# ?& W% B1 n7 t; G - ...0 }( r) e* v7 M5 A
- }
/ k$ ]" u/ ^" O- X5 t3 \. G - ...8 W1 A! @7 j6 _: [( |- K" I8 ?- K6 G5 t! o
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">1 s ~# K* B" E' W6 ~
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \8 ]/ @; `( `/ U
- "Server: MJPG-Streamer/0.2\r\n" \
& H: _9 S1 J5 K1 b+ C' B! q) x - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
4 ]" u) z. {5 k) @) Y0 C3 \ - "Pragma: no-cache\r\n" \% N3 N% _* V- U
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"0 ]) t `- j% C6 H' r
- #define BOUNDARY "boundarydonotcross") z9 N3 _9 n; t$ d( ?/ c6 L( |
- printf("preparing header\n");
u+ ]/ p; X) r1 E9 R% G$ a0 B0 q - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
7 u$ r/ y; x1 ^; q - "Access-Control-Allow-Origin: *\r\n" \
8 W. ~4 H/ E# _; G9 z& s ` - STD_HEADER \
' X( R! [* x" o- j; c - "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \/ d, n4 Y4 w2 x$ o' u+ X: \. z
- "\r\n" \
2 \, N* O. X/ K8 ~8 w- f- [- P, B - "--" BOUNDARY "\r\n");$ Z9 B- ^3 x6 q% p5 P
- if(write(fd, buffer, strlen(buffer)) < 0)
5 ]1 S( U* O, z3 c4 r1 d3 l - {
7 O, B& F: T/ B9 Q7 r& e' Q - free(frame);
* |1 P. U* T# y. T) j - return;' U! @% @7 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 U- i) }, p8 D
- "Content-Length: %d\r\n" \; m2 o9 p( G1 D/ G
- "X-Timestamp: %d.%06d\r\n" \
7 o( ^7 F: y9 |: P8 g/ T; T! \ - "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);( G- a0 T* |0 F4 g1 t$ I
- printf("sending intemdiate header\n");
& q) J7 o2 e- I9 c - if(write(fd, buffer, strlen(buffer)) < 0)+ C9 a: ?4 }$ Q2 j
- break;. p2 z6 q; \" [- [1 d
- printf("sending frame\n");! g5 \6 l1 a; d( M. d) S$ X8 F/ I
- if(write(fd, frame, frame_size) < 0)1 ^) t7 G! u. w( e0 K
- break;
& U- L. R2 [$ @- Z% p1 ^% l# h4 x - printf("sending boundary\n");
0 k3 |9 y" R6 Z# s9 a ] - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");/ q C1 \* S0 h& ~
- if(write(fd, buffer, strlen(buffer)) < 0)) P) J3 O1 ^8 t, |# R5 ]
- 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
/ b7 H" h( R& g q& }3 R" U* n: R8 O
s' f/ M5 d; M4 [/ x二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:! ~; R. h# ]8 G& {5 c& x, B1 p
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
" V" B. d9 B& q9 @) t- w4 K - {# O u! W% r# \1 Y" {+ U- y
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
( \5 R9 V* F" e. F - if(*socket_found == (~0))# }$ W$ R4 P9 Y* F5 s# k
- {
9 f9 p0 R/ b; @. W8 ^: X4 o - printf("Create udp send socket failed!\n");4 e, \& }+ m7 v9 p! U* `1 g
- return -1;( p l) Q; R! Y! P- ]) B4 \; m
- }
; S' y; Y# \1 v2 c$ \ - addr->sin_family = AF_INET;! v) H$ r( n( T& a& q! o
- addr->sin_addr.s_addr = inet_addr(ip);
. z! h+ {- Z3 V - addr->sin_port = htons(port);
4 U, b: l/ t# K+ X3 g' o8 s - memset(addr->sin_zero, 0, 8);
- i; X1 a7 b8 ]! E - return 0;
; N7 G" z) g$ l% z$ |, Q - }
复制代码
3 I$ y& B1 }# r4 R, _0 D" Q) ?# b* h# q6 Q3 t+ a3 t! c: K$ X
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
3 D5 G4 R8 a/ t! |% c* W3 u
" c, y z9 @5 c5 t3 |; l$ h& S7 G0 M2 Z$ }( X! R+ W
- while(fend > 0)9 ]/ u* h( g E1 \% [6 X
- { Q& B, [6 p) M0 H) F
- memset(picture.data , 0 , sizeof(picture.data));
" U8 Z. X) [! z0 I - fread(picture.data , UDP_FRAME_LEN , 1, fp);
S0 J" ?8 ?3 H+ Y; ?" e - if(fend >= UDP_FRAME_LEN)3 d8 @; f8 H0 f
- { B3 ^/ T% G0 V0 w' J
- picture.length = UDP_FRAME_LEN; x9 A. E, i! _7 P
- picture.fin = 0;
& p8 A- n- F k4 c" { - }
0 T( c/ i' D3 Z - else
/ g9 B! p# \* L w$ z8 }7 `. c - {
6 K& A, t6 l) [/ B8 S9 M - picture.length = fend;
& b1 Y+ w+ V' m- @ - picture.fin = 1;
& ~2 Y/ h R- r/ i7 s& L - }/ Q7 i* v [; g1 \* J/ M& C
- //printf("sendbytes = %d \n",sendbytes);
' n4 ]& e6 Z* \ - sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
" y' E! H1 R8 S! ^0 G - if(sendbytes == -1)$ I1 O, K+ h) Y+ e
- {
: S2 @6 `! v; h R7 n- Y d - printf("Send Picture Failed!d\n");
' @& U; j( ]9 s( _( l0 ?0 V - return -1;0 H4 e8 V# V1 q* n" A
- }( J% Q5 Q/ J8 M5 z" J7 F
- else+ N% k6 H' g* A/ Q- G* D, S v
- {+ R# A8 U6 n7 V9 v; D8 ?
- fend -= UDP_FRAME_LEN;/ R" A7 H' `5 Q4 ^' D, V( f
- }# `6 Z" S1 s9 n) p6 N
- }
复制代码 ) n1 R$ d1 o7 {/ B0 m
8 M+ \; y" s# t2 S* Y% `. \$ q : S( U1 ?: }4 P; G1 e
+ P i5 O) n7 U8 aiMX8MPlus 核心板: https://www.forlinx.com/product/136.html |