本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
! u( u* f1 A# [5 l* f* g2 u. t# Y/ N9 q9 {. W

- d6 k" k5 L b2 N( t8 c作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html4 ^: N, w* I' Y- o1 u* ~( r( J* j+ L
0 M3 M3 Z2 f$ |* j1 J
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。 % n" ?4 S2 f. r1 _" I) o
 2 `4 a: T; i* p& e, y2 R* t
两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。
' U6 X7 ~- t- U4 t9 O6 B5 U5 t; r一、HTTP网页服务器4 O; ~* x6 A* O6 |
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)- ^1 K) u! F' N9 _1 P& y# B
- {
7 K2 E5 J7 V0 b, \5 L6 ]' u5 ? - struct sockaddr_in servaddr;( a6 A4 R' u8 A$ J, |7 n) B
- socklen_t addrsize = sizeof(struct sockaddr);! v- J: x6 P' M
- bzero(&servaddr , sizeof(servaddr));5 x9 U) p2 t0 B0 e) p# Y5 X+ F5 w/ B
- servaddr.sin_family = AF_INET;
( \! I" T, }# f" l+ w" Q" G - servaddr.sin_addr.s_addr = inet_addr(ip);, ~& ] {; o* O3 K3 `' h8 v
- servaddr.sin_port = htons(port);+ I3 b( ^1 Z/ ]7 g F
- int ret;
7 G: n7 I' R7 J; y+ _* n7 {: [ G - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
- P8 A. a c* G - {
* @5 g5 G4 a9 @, R - printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
6 X& U% W: T% |. F! f - return -1;
! ^- N) g# v; S/ a3 Z- `( u% N - }8 P, F" V- v* z# b
- int on = 1;
, ^) @ c. F2 b - if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
9 d! b) H+ V& W; q3 v s - {9 p( z- Q$ ?' w# \
- printf("setsockopt error\n");8 T# F. Y" h2 P1 j( ~ G( U& e' Z) w
- }! L4 y) w+ _: Z; @. I, l% w
- ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);: W7 j+ v$ i; r1 o& D) [
- if(ret == -1)6 U+ g; X- W( U) b" k0 y
- {8 y: B& x5 `' F
- printf("Tcp bind faiLED!\n");
7 ]5 ~* o: I# w+ i I/ | - return -1;; ^. S! y F! Y+ ^- S0 N
- }
& S, D5 f4 B i" W9 m# x: I - if(listen(*socket_found , 5) == -1)
1 G% B5 ~- H* b/ O$ u - {# w; _* B% A" z1 X
- printf("Listen failed!\n");
( n# r& o) J2 i. r - return -1;; u% f, b# R( u% H( Z
- }
- U& y5 p6 x7 Y. t& v) A' S0 t - return 0;
8 [5 z& _& z) Z3 }3 @ - }
复制代码其中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);' g$ e. k5 y. s) Q# w8 e
- void * Thread_TCP_Web_Recv(void *arg)
& b' Z& {" V7 ?# l - {. ^5 ?% I! R+ ~6 \) e4 @5 U" Q& q7 R
- 。。。$ T. G! M7 E) _; n8 T" O4 o
- while(1)
" h9 s: _" L$ M& U8 } - {
5 \) B$ ], d- u% ] - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);% Z. b& k+ X( g
- printf("fd_socket_conn = accept()\n");4 V) @) T1 f" z4 E) L! H
- 。。。
0 \1 f1 P K9 i& a( ? - recv(fd_socket_conn , recvbuf , 1000 , 0);
- f6 R" R! `1 e3 [6 X& k. i - }3 n, H: R# \# p1 ^+ V% Z
- 。。。' b0 Q: {9 o; [+ d1 |6 B
- }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);
4 S/ v. o! S" v! U6 V) {% @1 r - pic_tmpbuffer = pic.tmpbuffer;
6 ?" M8 X1 T% b; Z* h - pic.tmpbytesused = buff.bytesused;
& O: E4 x2 a& C& r- J4 H) F: G - pic_tmpbytesused = pic.tmpbytesused;
5 g! f( I+ j3 Y5 I - pthread_cond_broadcast(&pct);* O- D" Y* N) Q7 O5 s0 D E4 u
- pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
& W: S* a7 f) v- ~3 `( ^ - pthread_cond_t pct;
: A! h% v+ Z& Z+ y6 V% G5 W. V - int main(int argc, char* argv[])( p, t0 d, x7 Y' N4 @; F
- {
3 Q* P- N9 V% v7 a1 d! m - ...
* f. f% S: k/ l: P - TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
5 U4 ^9 K6 n# n) W% B - pthread_mutex_init(&pmt , NULL);6 A/ P2 ]5 ] `# X
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);; m: F. i6 N8 c1 b: {% L* V& j
- pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
9 J# c( K; Q1 ~) h+ ] - ...
$ \- v, w$ J1 O2 e) U - while(1)5 W+ o# J8 p8 O' Z5 U. P
- {
p5 e' Q5 w' W - V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
f/ V" s# y( x, S - ...
; h- r4 l' }: k - }' K* [ O9 W0 q. Y3 g/ l r
- ...) V3 `6 w4 b4 u
- }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
) S r& b& T. b/ Q& L; J' i. O2 Z - </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \7 u9 e. z3 `* u& p" R. f
- "Server: MJPG-Streamer/0.2\r\n" \
% I* u& u( c) \# O' P' ?' z - "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
7 R( f; M3 E& `# h: z - "Pragma: no-cache\r\n" \
$ |! G5 g/ A" y. R% F% z8 k - "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
! B! X! X1 V, A/ |2 B - #define BOUNDARY "boundarydonotcross"
+ [2 A7 [2 x7 f+ `& u6 R7 u - printf("preparing header\n"); u* p: {6 u& W
- sprintf(buffer, "HTTP/1.0 200 OK\r\n" \- o6 V# t4 K8 _2 h4 V: a) |
- "Access-Control-Allow-Origin: *\r\n" \7 i. w4 E' K; n# y. H; E) i
- STD_HEADER \" m+ x# E$ J Q6 ^2 r+ x
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \; N4 p1 ]9 j0 s! H) b! X
- "\r\n" \
7 ?/ @0 H& T6 j- |/ ^ - "--" BOUNDARY "\r\n");
( u u) e# K9 P4 q- v' k6 e - if(write(fd, buffer, strlen(buffer)) < 0): }# Y- G9 a- @% a
- {
$ O) U5 r! V4 l) `' ^ - free(frame);
. H0 K* `, A" Y - return;+ \+ m( ^2 U/ O2 A
- }
复制代码发送完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" \
- z; ~/ ?/ P% h! I6 k. u - "Content-Length: %d\r\n" \
7 G' O) u" S y) L. h - "X-Timestamp: %d.%06d\r\n" \: J0 f1 s% T1 m5 m% K- f) M7 h
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);$ {, C2 \' n v4 G
- printf("sending intemdiate header\n");$ s2 J" y1 R' Z) \, Z! |
- if(write(fd, buffer, strlen(buffer)) < 0)2 g; M& E! G9 H$ \: o: J
- break;
1 }# N. F9 X! E - printf("sending frame\n");1 N4 r' T% m* v; S, I: P
- if(write(fd, frame, frame_size) < 0)# O6 t- k1 n/ i$ G% W! ?
- break;" w7 w8 b+ |- ~2 y- L
- printf("sending boundary\n");
0 v9 W5 w9 e1 e# H" M* ^# g - sprintf(buffer, "\r\n--" BOUNDARY "\r\n");( p1 X& w- p" a2 f# a4 g
- if(write(fd, buffer, strlen(buffer)) < 0)
- j" S8 q# @! X' B3 l6 j - 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指令,从客户端使用者角度来看的效果就是网页一直在等待。  3 w+ f& v7 H: ]+ m' w, o' X+ Y
 5 v5 Y0 ]2 `$ R& p; \+ _- [- m
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
: _) H& W- M7 M3 C/ I- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)6 n& y4 F+ ?+ m; E
- {
1 x |) ?& k3 J* l: z( f% W" Q - *socket_found = socket(AF_INET, SOCK_DGRAM, 0);9 R) E* g! s: C7 I0 B9 Z
- if(*socket_found == (~0))' X" P6 u! Y: C
- {# h6 E$ }6 z+ t! _$ q \2 O* _( Y$ Q9 H% V
- printf("Create udp send socket failed!\n");/ n/ R2 G7 s) X+ i7 g5 w7 q8 M
- return -1;$ P9 A, K/ x, l
- }. m& z/ V V) B- ]
- addr->sin_family = AF_INET;' `; v8 Y z6 w2 r# C9 P, R7 S
- addr->sin_addr.s_addr = inet_addr(ip);2 e* }& Z% F, q$ }
- addr->sin_port = htons(port);
/ q8 f7 g, }8 d/ u5 M - memset(addr->sin_zero, 0, 8);
8 \1 e' _# k* L3 f# ` - return 0;
, s; W4 F5 t. a$ r - }
复制代码 . Z( M! E. \5 r1 k
% l* N. ~# E G3 S2 w6 n8 s而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
. q$ [1 \0 H0 |3 ^. R6 E$ w. \0 i0 @4 R& {1 H
) g' W) D- G0 l C- while(fend > 0)
7 \; v q4 ~( H* I& L2 V. l - {& ]5 T) g8 E# p* l$ b
- memset(picture.data , 0 , sizeof(picture.data));
9 Q' n5 S+ _9 K# \, c/ r - fread(picture.data , UDP_FRAME_LEN , 1, fp);, W, @* y9 z: A3 R @6 V' x
- if(fend >= UDP_FRAME_LEN)6 q/ Z: ]- J- {/ Q' W: ^6 N' G i
- {1 x F: i* X5 x! X1 T' Q: u8 W
- picture.length = UDP_FRAME_LEN;, z( Z2 \& {9 P }* L$ i
- picture.fin = 0;
! E4 V8 h$ {3 |3 L. J# B - }9 R! q- x5 v8 d: ~; ~: j/ Y
- else( u: e! A7 Z3 \4 {! @# v& J% ]
- {
9 b/ `& K/ @: y - picture.length = fend;
* ^ p; C3 o* e6 \( [8 k/ C - picture.fin = 1;4 _6 o+ S# c* C
- }! g# Q1 r9 C, b2 Z. O% S& Y" q
- //printf("sendbytes = %d \n",sendbytes);8 s+ y& ]9 t$ D) Z
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
- q3 l; a( h6 |5 q I6 L - if(sendbytes == -1)
# m" c0 S5 X6 E% G& X; A - {
! D; F& r+ X; H$ X# @ - printf("Send Picture Failed!d\n");( `. O! V- i: y$ G9 {( T
- return -1;
% N8 g: Z( L) D6 d. c- c& v - }
0 Y0 Z" |* F' t. ?' k% B4 g1 W - else
* w) _4 L* o+ J/ ~+ @6 A - {" I) J( M# p# x
- fend -= UDP_FRAME_LEN;
3 m; G6 ] R! w* L2 q, ~ - }- R2 F1 W' m( q
- }
复制代码 : A0 @' Y; R5 |' O2 R
8 f0 G v1 x0 U8 {2 o: |: e
 3 f3 N* l; F# P
2 V6 f2 r0 U: S& r" m Z g* J: ?
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |