本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
: O4 e2 g2 x+ B' @" n0 V9 z6 @ G/ L4 {

2 K! B1 a3 r% e T2 k Y0 X6 w: K作者|donatello1996 来源 | 电子发烧友 题图|飞凌嵌入式 iMX8MPlus 核心板: https://www.forlinx.com/product/136.html8 _, ?8 Z! h4 j
" {& @$ C' W) d0 x* H& p, ^5 t3 M
本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。 MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。
. r. L7 m& Q- \; r/ H
1 T/ I- c2 @- c2 h# \* B0 s m0 c! y两者各有优势,对比鲜明,其中: 这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。 0 u; Z( b+ H2 g- n; V) v" y9 f
一、HTTP网页服务器4 l/ [; k) U1 D
先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器: - int TCP_Server_Found(socklen_t* socket_found , char* ip , int port); \' j& d8 x1 o
- {
5 }0 t" c# O% R% T3 S: G - struct sockaddr_in servaddr;
+ w8 y4 O" y; N. k - socklen_t addrsize = sizeof(struct sockaddr);# [+ [& l8 @ f5 t" O7 ]6 O
- bzero(&servaddr , sizeof(servaddr));! _# f$ y, \3 r R
- servaddr.sin_family = AF_INET;, U& b0 m7 [. T8 }
- servaddr.sin_addr.s_addr = inet_addr(ip);* j# Y5 h+ a t) c% q1 d' d3 v
- servaddr.sin_port = htons(port);
% E, d" J& o3 L6 k - int ret;
, e. f9 F: m- L0 v. d. D - IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
- Y' }' ` Q7 v - { W+ A$ J3 a% W8 P- l5 B$ k1 X& @
- printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);$ [. [: i/ J0 s8 g, x- F$ m
- return -1;( T+ \3 g& E3 v. q' e$ I# [
- }
" N( ~8 \7 a j; m! ` - int on = 1;( C0 f, Z6 \% `: \
- if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)3 Q n+ d& [1 Y, c4 k
- {5 I2 z# u. F" {- r
- printf("setsockopt error\n");
) K7 \) O6 @8 w. a/ k. ]8 k5 l/ Y - }
- Y' w; ]) o7 P) h2 [ - ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);5 M7 N2 f6 w& }7 Q; |7 Z
- if(ret == -1)
s% Q8 C. @2 n - {( t4 o7 y l0 W* {6 ~/ h
- printf("Tcp bind faiLED!\n");; U2 z# V1 ?) _! o* [
- return -1;* y6 e5 y! C( F3 m/ G* f3 o: R
- }1 y' c, D; B9 v$ x2 F0 Q
- if(listen(*socket_found , 5) == -1)
& [* N+ V7 |1 C3 d - {
/ v, o0 E5 _6 U0 S ? J8 H - printf("Listen failed!\n");9 F; k6 O! z e w. D
- return -1;
/ J+ b, S% i& M. u' J# q - }
+ X' J" Z' s2 D4 y - return 0;
$ k6 z$ Z% _6 g7 d& y$ w2 ?) m, I - }
复制代码其中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);3 U+ p. t9 s! }1 |; R% o4 ~
- void * Thread_TCP_Web_Recv(void *arg)* C9 g! @1 l4 ~5 y p. f9 O
- {
! z: X& {4 U& G. L3 `2 D2 w - 。。。# ]9 r( @. W% f8 k$ ^
- while(1)4 h, x0 z, V* t2 G0 b7 J# ^
- {
a/ W/ a1 `4 U6 t( f8 \3 v$ i" J - fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize); Y8 M! `% H# e6 h. f# i
- printf("fd_socket_conn = accept()\n");
, h5 [2 l, N4 h/ q - 。。。
7 H6 B& |4 |& r; G: }2 \1 m# T) A - recv(fd_socket_conn , recvbuf , 1000 , 0);
9 f, e# ]: P, n5 K$ S - }
1 a. F' }- M6 G1 i- e3 p! M - 。。。
( l" { |) o8 z$ n" l, j - }
复制代码MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问: - pthread_mutex_lock(&pmt);' J$ l" y( p, d, M/ h3 i
- pic_tmpbuffer = pic.tmpbuffer;2 ^" S* y: N) W6 u/ c- `" V' `
- pic.tmpbytesused = buff.bytesused;0 X: a0 C( {2 W7 b
- pic_tmpbytesused = pic.tmpbytesused;
0 |# W2 P6 R- ^( {7 R# w& b - pthread_cond_broadcast(&pct);
. _3 y" s3 i# V3 q - pthread_mutex_unlock(&pmt);
复制代码线程互斥锁使用之前需要初始化: - pthread_mutex_t pmt;
4 b0 {5 e) y- Z) {' W9 i - pthread_cond_t pct;* h& ~4 a5 e/ d4 i4 i; v! v
- int main(int argc, char* argv[])) m) D" K9 u4 {8 Q" t9 n7 U
- {
0 D- \4 P1 w+ }, Y- S - ...* b2 a z5 g8 ^4 W, t
- TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);- e1 j% U. B2 N1 V2 G: z
- pthread_mutex_init(&pmt , NULL);# n. H0 p: a9 Z7 l. A
- pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
; `; f# q0 g* L) b4 n* C% m6 F3 H - pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);8 j; {$ h E/ Y1 G3 c H% Y2 q% q
- ...
- \; L+ |# U7 s4 s5 D( ^2 d" c - while(1)$ B3 {% J' \& S1 }9 ^. Z8 P
- {1 H J' |; g8 l( f% ~
- V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
( h0 L) [! N- e: } - ...
& |& I6 [9 N7 ]* S# d9 {, s - }- G% u4 U) e/ j/ I e& x# H# q
- ...
, X9 h" M: x5 q6 A5 L+ E+ V0 J - }
复制代码然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路: - <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">2 @7 d- _* P' E2 [
- </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码- #define STD_HEADER "Connection: close\r\n" \
& s% |. p' S0 ?: ]8 P2 K( [ - "Server: MJPG-Streamer/0.2\r\n" \0 }+ O9 J! s3 x1 ?
- "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
1 W$ J; V# c2 T* d' \: r6 r - "Pragma: no-cache\r\n" \6 |" h. B. M3 P9 C% v2 c
- "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n". T, ~' E+ j; Q# b9 Q1 s8 i
- #define BOUNDARY "boundarydonotcross"' s& @( p9 Y; t
- printf("preparing header\n");
. F) R7 ?/ E8 F b: H/ m - sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
" w; i( [+ E" ]* w; M3 H! L - "Access-Control-Allow-Origin: *\r\n" \4 P; d7 V, Y x
- STD_HEADER \- z5 Z2 ]+ m& b* P/ |
- "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \5 b/ e* z; A- c8 g* Y( h
- "\r\n" \
4 z1 F% v2 z V% G - "--" BOUNDARY "\r\n");
0 e) W7 V% {. h7 Z - if(write(fd, buffer, strlen(buffer)) < 0): t( x& M" L; D
- {5 r3 w3 k2 Y! |% B
- free(frame);
& z* ]! E' e ~9 e, D$ [ - return;
) h1 P$ o( f, H6 q4 H' U# |, \ - }
复制代码发送完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" \. g( d* w! u3 X6 n/ Y$ O
- "Content-Length: %d\r\n" \
0 q9 ~8 H4 \" h1 L - "X-Timestamp: %d.%06d\r\n" \8 v5 D5 k/ v% a3 q
- "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
1 ]; A) Q& d q- C, W1 Z - printf("sending intemdiate header\n");
3 f# a- G! E3 [% x, a: n - if(write(fd, buffer, strlen(buffer)) < 0)
+ I6 D# X( V8 p1 w& J- N6 h9 k6 Y - break;
5 q4 \0 L: g3 `3 G% q# K1 g - printf("sending frame\n");4 c- h% R1 D* s2 \# `$ f
- if(write(fd, frame, frame_size) < 0)
- D+ E$ ~0 ]" I+ H; {( _1 w8 q - break;. \9 p- a5 w, j7 t9 U% K5 x/ N& p
- printf("sending boundary\n"); e* s1 X4 Q$ U, \
- sprintf(buffer, "\r\n--" BOUNDARY "\r\n");$ {8 O" I# P1 n. H
- if(write(fd, buffer, strlen(buffer)) < 0)
% v8 P- c! P) K- R+ 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指令,从客户端使用者角度来看的效果就是网页一直在等待。 
# L/ Q; v* e9 n& d
6 s p# \3 U7 z2 u: y. ^: @/ ~二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:" { ]6 P( B' ?" n# |/ j# o( j( ?
- int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)6 P3 w6 ^9 q1 p& l0 Q, T" M0 c
- { _- C9 x8 h; O7 p" N4 @" o
- *socket_found = socket(AF_INET, SOCK_DGRAM, 0);) E% \% F: W+ r7 v7 A5 c1 C
- if(*socket_found == (~0)) t: O4 D# n X' ?9 K7 b
- {& G$ _. t7 t( [1 T& }* m+ l; l" H
- printf("Create udp send socket failed!\n");- F* P, S9 L' k/ K9 w$ K |7 V8 {
- return -1;
' H# ~1 M/ h2 p5 K9 | - }
- g% J! }3 b# y- j, D* |8 l - addr->sin_family = AF_INET;
+ X) z% g- }+ F. p$ F - addr->sin_addr.s_addr = inet_addr(ip);
* `' }* _9 X( S5 i - addr->sin_port = htons(port);
$ N" l3 C9 Y% I, B A$ W+ M - memset(addr->sin_zero, 0, 8);7 n: m/ u; l$ |. ?( n
- return 0;
* e4 b& H2 ?( [+ _5 d& B - }
复制代码 / T/ J( p& U) n1 e" j
$ A) ]. Q* v2 D g4 Q8 v$ Q: r6 r而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
! \& ~! j5 t f( e! {8 h+ S* i1 |, N: r" a# N
7 \- v! E; o% W* L& Z! E
- while(fend > 0) S7 p/ K6 y2 y' M1 q2 ~+ k
- {
6 e; Q5 |' Y4 G1 Q! ~ - memset(picture.data , 0 , sizeof(picture.data));
$ b( @. w4 M/ ~ - fread(picture.data , UDP_FRAME_LEN , 1, fp);2 d8 z0 s7 o9 p+ z0 L8 ?% e# m3 W
- if(fend >= UDP_FRAME_LEN)3 o+ j. _5 R! [5 q- ^+ M# ]
- {- k, T6 ?8 y! J
- picture.length = UDP_FRAME_LEN;
! d# l9 e2 A/ B - picture.fin = 0;. w" S, m, D. s) P! r
- }
6 ]% {: _8 v5 V; i! r - else
8 {' Z9 J% j# F2 y* H - {2 p3 U9 s( t1 q. w% W' k# T
- picture.length = fend;
+ K3 @" U _" K2 O, H# P ]2 t - picture.fin = 1;8 m4 n o' E' [* L$ T0 m
- }# H/ r0 J) \4 R) Z9 t
- //printf("sendbytes = %d \n",sendbytes);( z0 ^. \% t g* C" m, l6 S
- sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
/ k+ T! K6 s8 s( t$ y5 j* z1 L - if(sendbytes == -1)
9 q' U7 l4 v4 u - {! B# G# \/ J5 u& ~1 |* H$ K) U
- printf("Send Picture Failed!d\n");. Q T4 w4 {4 Z8 o9 d
- return -1;
0 E0 a* V; c& O' i8 H8 M9 a - }
+ R: r; r7 x3 Q3 k - else
, i( [! ]% p V/ \/ t% W. { - {( E9 }5 w' P2 P P
- fend -= UDP_FRAME_LEN;
- }% }8 U7 a% N* L E* | - } F# P4 V" E$ ]; ]. `% u1 a
- }
复制代码 . N4 h5 x) |$ ^# z% i. n% h
& |" a* ]' p \ C9 O* C0 ~$ v& z
 7 d1 a! x* f+ Y3 |. A9 ?( X
0 I) v' {) Y1 P- Q
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html |