嵌入式爱好者

查看: 11665|回复: 1

[帮助] 发烧友实测 | i.MX8MP 基于HTTP网页服务器和UDP上位机的MJPG码流传输(mjpg-steamer)

[复制链接]

48

主题

55

帖子

304

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
1 `& q1 s  L5 m4 Q% V- y* T# W5 e

: H6 j6 u/ ]; M! I+ \) o

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
( x8 G1 k* b% c8 }  }6 o& i1 y) p  {3 [- y

本文采用的硬件板卡为飞凌嵌入式OKMX8MP-C开发板,系统版本Linux5.4.70+Qt5.15.0,主要介绍基于HTTP网页服务器和UDP上位机的MJPG码流传输。

MJPG格式作为一种持续传输的视频码流,在远程监控领域中应用较广,而实现这种远程监控的第三方应用最常见的有两种:浏览器HTTP网页、UDP上位机。

# Z% \! A7 M# K  E3 T0 U+ _0 `. Z


& `  J: u! d) @+ y& \

两者各有优势,对比鲜明,其中:

  • UDP上位机:传输效率高,上位机编写方便。

  • HTTP网页方式:客户端无需安装上位机,只需要一个浏览器应用即可;客户端访问服务器支持跨平台支持,无论是电脑、平板、手机,还是Linux系统、Windows系统及安卓系统都可以,只要有浏览器应用都可访问,而UDP上位机则受限于目标平台,不易移植。

    8 ~* T4 s) N& n, h

这两种应用各有优缺点,对于嵌入式开发者来说,两者都必须掌握。

1 }/ G+ j$ Q: d
一、HTTP网页服务器
: ?4 m& v5 A: }# m  @' B

先说下HTTP网页服务器获取MJPG码流的代码,首先是OKMX8MP-C在开发板端建立TCP服务器:

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    & g% [4 O9 V" `8 j
  2. {5 j2 b' x0 K2 v2 O" d  e
  3.     struct sockaddr_in servaddr;
    & w6 ?; k6 v4 D1 O. _: t3 B+ u! V+ s
  4.     socklen_t addrsize = sizeof(struct sockaddr);5 R! \- x( W8 ]/ w  ^) `# J, B
  5.     bzero(&servaddr , sizeof(servaddr));3 t) {" k7 h5 `+ T
  6.     servaddr.sin_family = AF_INET;# {3 H# r# N& T3 t8 `
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    9 Y. I" Q3 }2 u/ h& `8 f
  8.     servaddr.sin_port = htons(port);, H3 l9 W/ o! \& p# {
  9.     int ret;
    ( g2 n- X/ W6 ?- u% [
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)) s; g0 g5 H9 x. N( b6 m/ {; Y
  11.         {4 r7 x. D7 L0 K6 ]8 _! ?
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    / k" C# y& W! {
  13.             return -1;
    - R0 K) }) m+ V2 K$ L  }
  14.         }
    & R/ A7 _+ j; [
  15.     int on = 1;/ P- R9 n5 V. l: [( q4 @2 a
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)- T2 P0 J6 u2 F  C2 e
  17.     {
    8 u$ T9 ~3 ?, n- B7 f
  18.         printf("setsockopt error\n");9 X. e8 P7 ?9 e
  19.     }+ Z. z% H7 l5 ~0 s9 B* A8 C
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);% q! |& K- ?' p2 J: Y" Z
  21.     if(ret == -1)1 F" m( W& C/ i5 v% K1 ]6 q( Z
  22.     {: S" f9 j- k+ ~" b
  23.             printf("Tcp bind faiLED!\n");
    % b( x2 g1 K0 Z/ D8 E
  24.             return -1;( i1 u$ j1 ?- d" c1 U
  25.     }' ~1 p: V2 M5 i7 G9 p
  26.     if(listen(*socket_found , 5) == -1)
    $ W3 p- h5 u! N' d& h  h/ D% ~
  27.     {. k3 ^; L" E. R3 S7 g5 b
  28.             printf("Listen failed!\n");
    - {! z# d7 g0 a6 Z
  29.             return -1;- U4 F9 Q$ T, s8 u  u
  30.     }! j& ]+ L/ h- t# g
  31.     return 0;
    ( t" V! j5 V7 u; P$ V
  32. }
复制代码

其中setsockopt()函数是可选的,一般只用于规避socket()函数的建立错误。

建立了TCP服务器后,返回的socklen_t型实参在后面的HTTP网页服务器中需要用到。

HTTP网页服务器所属的TCP操作是需要另起轮询线程来让客户端进行accept()握手操作的,accept()之前的listen()倒是只需要执行一次即可,accept()握手操作和recv()接收操作需要创建一个死循环线程:

  1. pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    ' @2 h/ m8 V/ Y/ j
  2. void * Thread_TCP_Web_Recv(void *arg)) x4 o4 A" c8 H6 i) n
  3. {( G9 ?( M, c9 u: w
  4. 。。。( G5 c! a& w4 r! B4 Z' s" B6 d
  5. while(1)2 m# F, }' e. {4 j9 j- k% l  F
  6. {" h5 n6 M# k" n, }. U5 ?. ?% ?8 n
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    $ N- Y; h5 ^( ^9 U* M4 H- k1 ~) R
  8.            printf("fd_socket_conn = accept()\n");/ r3 o7 r, d8 z& ]: V4 z# s+ \- K
  9.     。。。
    - n8 o  M9 \! R% |& p4 Z
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    2 m& }" v& g6 L( B! M  ~# L5 s
  11. }; s, @& S2 S# M4 i
  12. 。。。% u) `/ L% n, Y# Y. q" a2 g
  13. }
复制代码

MJPG帧可以使用Grab操作获取,获取到的MJPG帧需要在TCP线程中读,在Grab操作线程中写,这种被多个线程访问的资源需要加锁防止读写冲突,即资源被Grab操作写入时,需要上锁,不允许其它线程访问,操作完成时需要解锁,允许其它线程访问:

  1. pthread_mutex_lock(&pmt);
    8 P7 L% D6 G4 O; s  Y
  2.     pic_tmpbuffer = pic.tmpbuffer;
    5 V& I* ^9 B5 [' W0 B* t) p
  3.     pic.tmpbytesused = buff.bytesused;
    ' L$ t( v( R3 t
  4.     pic_tmpbytesused = pic.tmpbytesused;( C$ {1 A& u/ \
  5.     pthread_cond_broadcast(&pct);
    ) Q: P1 |- p+ @4 C& p
  6.     pthread_mutex_unlock(&pmt);
复制代码

线程互斥锁使用之前需要初始化:

  1. pthread_mutex_t pmt;/ {% P- W& U( }2 y: H+ U: J! J
  2. pthread_cond_t pct;
    / B3 I1 Y2 i& U: W& L
  3. int main(int argc, char* argv[])9 o5 `7 X  ?! ]8 _/ W) J
  4. {2 I* l8 W1 w7 n' e  l! @# L' d3 |
  5. ...
    ' J0 `4 P4 i1 R6 l$ `2 O$ ^/ ~
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);, v/ H0 X! a' z" E0 F
  7. pthread_mutex_init(&pmt , NULL);0 F  p% V, @8 [4 v# f. g
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    $ H$ X) `8 e) W" ]9 I4 @- F
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);. Z# l+ L4 v) G! N
  10. ...% y' ~6 G1 [. B0 B% O
  11.     while(1)
    8 _0 y! v( M1 Q3 K6 ?; G* m! O
  12.     {3 o- ]& \0 W" Y# F3 B9 r" \
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    & C4 ~( z6 [- l/ M9 \
  14. ...
    & J" Y8 P2 q! y
  15.     }
    7 w( d8 R; p" r4 ]4 a
  16. ...& w$ X6 h' x  f9 S- z3 d
  17. }
复制代码

然后是发送的细节,发送图片文件之前,需要先发送HTTP标准头,这个相当于给发送图片或者其它类型的流数据铺路:

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    - ^1 S5 |% L$ w. D+ B
  2. </span></p><p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"></p>
复制代码
  1. #define STD_HEADER "Connection: close\r\n" \
    ) r: ^' D( Q$ [6 M+ w1 g
  2.     "Server: MJPG-Streamer/0.2\r\n" \2 L& b$ E0 x/ {
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \( c' B, ^& ^6 u' i) ?/ f
  4.     "Pragma: no-cache\r\n" \
    2 W+ I6 r5 q& n. t) D/ f. X
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    0 D- N$ N: d) \) `+ ~
  6. #define BOUNDARY "boundarydonotcross"/ L. ^2 ^! F, Z1 u/ l1 x/ p
  7.     printf("preparing header\n");
    7 G- g4 {4 O2 ]3 v
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \5 l6 K" l( D& O; Y$ A
  9.             "Access-Control-Allow-Origin: *\r\n" \
    6 y0 L* m$ \5 H8 S; o
  10.             STD_HEADER \
    . X; p+ V+ F1 _: i; ~$ J/ }7 |4 T
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \" V! b, [0 O1 u. l
  12.             "\r\n" \
    6 |* ~6 d7 X  A) V5 h4 O
  13.             "--" BOUNDARY "\r\n");
    - u, K2 c1 N0 x+ k& {7 f/ @2 j
  14.     if(write(fd, buffer, strlen(buffer)) < 0)/ T2 ]. j' L& @5 z' }' P5 O
  15.     {" W1 b( F! h, s2 r
  16.         free(frame);
    9 K' X3 k/ _5 v- f6 s
  17.         return;
    7 K# v" ^* V+ I) m
  18.     }
复制代码

发送完HTTP标准头之后,就需要发送内容头(Content-Type),这处的Content-Type为image/jpeg,同样,HTTP标准协议里面image支持的类型远不止jpeg一种,发送完内容头之后就是正文和boundary结尾,这样帧完整的HTTP头发送到指定的TCP GET地址,就会在浏览器中显示刚刚发送的图片:

  1. <pre class="prettyprint lang-cpp" style="box-sizing: border-box; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, 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" \
    # b# ~" |3 R( R& R; w1 Q  [" z! _
  2.                 "Content-Length: %d\r\n" \+ v* ]$ p* l1 n/ k+ r, ]0 K
  3.                 "X-Timestamp: %d.%06d\r\n" \
    ) x% g$ H: ]3 M" }2 `+ L% M
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    6 k2 H& |( h2 V6 u, O1 V- V+ X* ^
  5.         printf("sending intemdiate header\n");
    + S: C% m& \0 d. M, M7 x' ^
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    + c; P# d) V% I
  7.             break;4 M2 ^/ ]1 c% X: e7 d& v
  8.         printf("sending frame\n");2 @! \- C3 ^7 j! P
  9.         if(write(fd, frame, frame_size) < 0): d% V' h# j0 t: L. Z8 {
  10.             break;0 A; x+ j+ J; ^+ {8 ^
  11.         printf("sending boundary\n");
    9 f: H/ [8 g. l' d4 F" S) W7 ~
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");% U6 [  ^3 e  S6 {
  13.         if(write(fd, buffer, strlen(buffer)) < 0)0 L! Y9 ~3 o" v. p+ Q
  14.             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指令,从客户端使用者角度来看的效果就是网页一直在等待。

5 f( ?& x) P" Q/ V) _5 D4 _

; \& _9 s8 r. j2 H: f5 J  z
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
5 j6 |" s: W, ~" ?9 q+ t0 A
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    3 z* K% O. P$ D+ C
  2. {
    # @" A) u, d! Z& D  U; G3 K# N
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    & H8 r& F2 d3 Q5 E  z
  4.     if(*socket_found == (~0))
    1 m) V3 k# W4 X
  5.     {- l$ Z, h% ?7 {! v
  6.         printf("Create udp send socket failed!\n");
    5 X8 k9 F( O  H' L. B! p5 N
  7.         return -1;
    6 g* M+ H5 A+ T1 v: T! e  `
  8.     }
    4 w" d) A$ `8 I$ e/ ~
  9.     addr->sin_family = AF_INET;6 Q$ R7 G9 A! ]: h
  10.     addr->sin_addr.s_addr = inet_addr(ip);, T& f4 X, e1 D
  11.     addr->sin_port = htons(port);; M  R  W0 X! z
  12.     memset(addr->sin_zero, 0, 8);
    5 |! Y. j/ _2 ^( x
  13.     return 0;
    , T$ i; ?+ b$ q* S* p
  14. }
复制代码
9 e, ]* ~' K" }, L0 I, s

! N# b; J% a# I: A2 b而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:  J' V: Q1 V7 R! ~# }9 z; D) `7 Q% X9 I

) D2 N8 o) a# ]6 _0 L" s. q% B% G( U" Y
  1. while(fend > 0)
    4 d: z$ ]/ e2 u/ L+ L4 H& E# U
  2. {' {, S1 w; y' f
  3. memset(picture.data , 0 , sizeof(picture.data));& R) u7 e# D# Q7 J/ d: j
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    8 n) v' H% Q4 |1 M( }" g
  5. if(fend >= UDP_FRAME_LEN)! K1 t) S' |+ ^5 R+ z+ @
  6. {) M( ?$ o3 [4 P8 D7 P- h: S4 z3 l9 Q
  7. picture.length = UDP_FRAME_LEN;9 ~& Z% l/ }: p" h7 Q6 E
  8. picture.fin = 0;
    5 n7 ?; W# w  x4 q% I2 u3 F1 N
  9. }1 J" l" l' R; i0 s5 U. y
  10. else+ A6 b. v0 P; `
  11. {6 ^* z% ^# k7 D; A( P1 l3 K! U" T7 g
  12. picture.length = fend;
    2 p9 W+ d$ c' p3 ~3 U- t$ A
  13. picture.fin = 1;, [7 P% O9 @7 D5 w8 i& z( z
  14. }! k1 |) h5 y- O- B9 n4 A7 J. w
  15. //printf("sendbytes = %d \n",sendbytes);' N- N, `8 ~/ n2 |
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);1 `/ q" z; D, m# S
  17. if(sendbytes == -1)
    3 Z. M4 K) t- e' g. c, h. @: C
  18. {* T- f, K9 y; x6 U# X0 `
  19. printf("Send Picture Failed!d\n");' X' {' y" N4 P0 O
  20. return -1;# }7 a: s0 f6 }0 X3 I3 E  ~
  21. }5 M0 M& b$ M% q% R
  22. else
    , X( D+ P6 ]' u" N
  23. {
    2 X; a! |  ?# K# L, s7 d$ o: N
  24. fend -= UDP_FRAME_LEN;
    - i% N# C  I5 M; J0 ^
  25. }% U1 Z+ b- |( D# Q/ c3 Z* ~
  26. }
复制代码
/ n% S6 z+ {; ]' Q+ r
3 N$ M' v/ G5 _  }! r; V0 I" Y8 n2 g/ \


& Y* l) ]: l7 O" F  f
7 I* F8 f0 o& |9 ZiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|小黑屋| 飞凌嵌入式 ( 冀ICP备12004394号-1 )

GMT+8, 2026-4-6 12:13

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表