嵌入式爱好者

查看: 11912|回复: 1

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

[复制链接]

48

主题

55

帖子

305

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
, `& [) g- |7 ]' m; J! @4 o! J
: E; N  R* x8 |/ Y* k& \. S1 f( Z


. H' t# p1 ?8 L/ e: p# P( O& K

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html5 `/ d) \: r; E6 `5 K0 R

1 v* k. z2 m- J

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

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


: Z  ?7 }; y6 s3 L+ `

& T2 d; E$ I+ Y  U/ z1 F9 `

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

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

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


    ' ^: g" h1 R( \5 H# n1 ^' N

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


) k5 [" o4 M; \3 ~8 _$ t5 X( i( x: ?一、HTTP网页服务器; f' u2 `9 ~, O4 K! z  w

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)7 w% K  k  @: I8 @* V! W
  2. {
    ! K, [- T4 C0 T# d, r
  3.     struct sockaddr_in servaddr;
    0 [$ g* h* o0 {6 k) _
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    : N' [+ q- I3 }
  5.     bzero(&servaddr , sizeof(servaddr));
    1 S. ~9 p* F$ M7 {6 s- I
  6.     servaddr.sin_family = AF_INET;
    * `6 b) l+ X- s2 ?7 g0 C+ w& V
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);- N7 V5 u$ T6 J; |
  8.     servaddr.sin_port = htons(port);0 F% E( O: X2 f6 B9 Q0 t9 r+ K
  9.     int ret;
    . I4 c8 O7 M4 I# L4 |1 A0 C( w
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)7 X6 s* @6 \0 @' W1 k
  11.         {  u9 K4 k% ~7 w
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    1 |4 n) N  f4 z8 v) h
  13.             return -1;0 I4 z6 y9 r6 @6 A: m, b! s6 N/ [
  14.         }0 n8 A. i; G! ]  X, f8 O- p* J
  15.     int on = 1;
    ! h% W4 h7 ?* G- }! v! Y& C
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)) n( A; Y, C2 f& R* e# |4 U# ^
  17.     {
    # ]. N% ]  ?( s# |& A' b
  18.         printf("setsockopt error\n");
    0 G0 c: P+ F8 y/ l
  19.     }
    ; \2 |9 v/ e  s; o
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);5 F7 F, D' v' P; R" y/ T
  21.     if(ret == -1)+ W' g4 m  ?6 W* g1 z
  22.     {
    / J  G5 n0 F4 q- I" ?
  23.             printf("Tcp bind faiLED!\n");0 V5 s3 E! r# o5 O+ z, M
  24.             return -1;
    : H! t" @& N0 N
  25.     }
    # \! Q9 C' ?+ M* c, K/ N
  26.     if(listen(*socket_found , 5) == -1)) b. Q# O  ]- S* a- E. C; @, F
  27.     {
    3 B) m* R  i5 ^" |& R: g
  28.             printf("Listen failed!\n");; ^4 f- W- H. r4 V8 ?; c+ t  O, \
  29.             return -1;. U! S. X) f) J
  30.     }: [5 g' Z1 G) H
  31.     return 0;" L! [6 n5 i# x/ }/ a2 g! D" 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);
    ) T& e' v. X8 |$ y2 l: M
  2. void * Thread_TCP_Web_Recv(void *arg)
    ! e! R9 j+ L. J" B) e, V
  3. {- R$ M0 z* C0 }1 ^+ s8 z  }
  4. 。。。
    6 w  u% J) j, C# j, L
  5. while(1)
    $ y  G3 `, s0 _) D( x
  6. {
      a7 K; a: |; Y" g: ?
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    4 d: `. N) u3 z+ N* W! p6 E
  8.            printf("fd_socket_conn = accept()\n");% h1 Z& j/ X8 E9 R& U9 k
  9.     。。。
    , F6 P2 X# @4 d+ A
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);( L0 N1 z3 K; K/ B  X
  11. }0 o) I: t- @* g0 c2 j
  12. 。。。
    ! a- B* {3 c# U
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    8 j5 U+ v' F9 W
  2.     pic_tmpbuffer = pic.tmpbuffer;4 [7 @3 E- b7 M" P' r
  3.     pic.tmpbytesused = buff.bytesused;- }" ?( L' x. M+ ~, L
  4.     pic_tmpbytesused = pic.tmpbytesused;, G2 \' F, ^5 i" h! D' V
  5.     pthread_cond_broadcast(&pct);4 }1 h9 V' J; y; d& |( O1 O
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    ) M7 E3 J  M- C* w6 l
  2. pthread_cond_t pct;7 O( @3 U( H6 P
  3. int main(int argc, char* argv[])% F& }* N" t* L* r( f0 \
  4. {6 ^4 B( k6 ~7 z1 ?: H( {
  5. ...
    # h, p, M* \" z3 i; L
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    : R* D# u" Q: l7 E- S- w0 q" E6 O" {
  7. pthread_mutex_init(&pmt , NULL);! E( Z% ?5 N. }4 y/ |# C/ I
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    ( B  P9 a0 Z, t5 c) I5 o+ P
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);6 u2 o6 W5 N, `" i, F
  10. ...: C2 S1 O- E9 ]* v) I; m
  11.     while(1)& P( V" g: R$ C! d" \6 E* M) P. G
  12.     {$ P1 l+ a9 ?) o9 r# t" T
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    & W( R' O( a4 }6 s
  14. ...0 p" d3 S9 J: w# X
  15.     }
    0 H4 b7 E: \2 F: j. u
  16. ...
    : h! S. J+ K5 M7 R+ Q; j" m; @
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    : m1 \8 |) U3 J5 i5 `* x0 G, e( z
  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" \" s5 \$ J* r5 p5 X
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    " R  V( `, H7 X% K3 }+ w5 z
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \$ [9 @0 z1 F8 Q+ D4 ^7 _/ w
  4.     "Pragma: no-cache\r\n" \7 r3 a% K1 _; n; `8 Y$ `
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n". l' z8 |, @8 B0 [  Q
  6. #define BOUNDARY "boundarydonotcross"
    0 h: `0 _1 H' q" h; f- P' {+ P
  7.     printf("preparing header\n");
    , y* Z% |& w, t/ \5 K
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    . n" e: ]7 y7 R  _! e
  9.             "Access-Control-Allow-Origin: *\r\n" \5 _: m) S$ ]4 v# c: J+ ?  g) o- p
  10.             STD_HEADER \" P; c4 S- ~4 n$ y3 u
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \* h. _8 Z3 w2 E- |
  12.             "\r\n" \
    + W' h5 Q, Q* x, B, e. c% v
  13.             "--" BOUNDARY "\r\n");
    0 P4 f, v$ L! S
  14.     if(write(fd, buffer, strlen(buffer)) < 0)+ j9 T3 s# P; ~
  15.     {
    1 K$ U2 P: O) S# A* N
  16.         free(frame);  o1 }% ]: I4 q( b6 q
  17.         return;
    ' ]0 Y* c7 s9 Z  ?) Q
  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" \; R8 K' s. `- S% D) p% ?+ X
  2.                 "Content-Length: %d\r\n" \
    + @2 @9 c  d" X9 N
  3.                 "X-Timestamp: %d.%06d\r\n" \
    : d' m5 c% V3 d  }& l9 V$ Y
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);3 s5 t0 A' f; j0 x' {, m
  5.         printf("sending intemdiate header\n");
    6 F% |4 L3 n/ k0 i
  6.         if(write(fd, buffer, strlen(buffer)) < 0): I- [8 z9 i: s3 A& v. U' V# N8 N3 M
  7.             break;1 Z' R* A8 H+ G  P3 o4 J
  8.         printf("sending frame\n");- R0 v( K9 p; u5 H5 d
  9.         if(write(fd, frame, frame_size) < 0)" `' I9 ^& J# Q7 h; z& p
  10.             break;5 F  }# d! h" k3 j" y# W
  11.         printf("sending boundary\n");2 C; S5 o# }8 C+ T( ?3 Y" s
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");' ~# b8 \4 `! ?. Q7 U
  13.         if(write(fd, buffer, strlen(buffer)) < 0)3 c: z; i. c& U! P5 ~
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

- {% }" w6 y1 L/ ~4 I$ W


6 b, F- O8 S& I+ i7 |; K' j3 s$ K二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
9 E3 i+ n$ d: K+ O( b1 x; x% d
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port): z+ c& y# H! R3 O- U* M( R, J0 ^5 t
  2. {
    ! r6 J  t9 ?5 N: P% U6 r
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    * u9 f8 t! R. p9 W" J" H
  4.     if(*socket_found == (~0))" A- L9 ~! z$ ]1 h  Y1 e+ w$ p
  5.     {# z" c, K6 k7 l( u
  6.         printf("Create udp send socket failed!\n");+ I5 x( A' S1 q  ]
  7.         return -1;8 F$ E! Z6 T1 O' [1 ~
  8.     }( k! ]& S$ J+ w- q2 G; T
  9.     addr->sin_family = AF_INET;  d0 |3 [" F& c& s) R
  10.     addr->sin_addr.s_addr = inet_addr(ip);( U7 ], y" r& y% y2 ^
  11.     addr->sin_port = htons(port);
    * c# t) m7 g2 y% Z! @$ N
  12.     memset(addr->sin_zero, 0, 8);
    ! k/ C! @0 \7 W% \+ [; a2 `
  13.     return 0;
    4 W, Y; A- N7 c/ b  ^
  14. }
复制代码
( t9 {/ m: v* B' Q7 ]

5 R# v: U6 q/ \, W, @* E而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
) \: A/ z7 W% n: f9 D1 }% Z
+ J' N9 M8 e8 J; O* p4 z4 c' g" N
+ P& V! ~! w7 C' }0 P
  1. while(fend > 0)& ^' e  A; H. i8 n
  2. {
    ' w* e) P; g+ y
  3. memset(picture.data , 0 , sizeof(picture.data));
    $ G0 `5 n1 T2 C& f: `
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);9 f4 R7 c- q( Y* H2 n! |9 m
  5. if(fend >= UDP_FRAME_LEN)
    ' |5 `! \4 H: p+ J
  6. {
      l! t0 V& R9 P$ D' Z
  7. picture.length = UDP_FRAME_LEN;
    4 \. i6 n( M6 j2 ]* ?
  8. picture.fin = 0;, D& u+ V6 \( t6 b3 x
  9. }, i( P$ z: \6 {) n5 Z) T
  10. else
    # {$ m2 k" a8 d; w7 V& d: M
  11. {
    ( [* o' w9 ^8 l
  12. picture.length = fend;7 i9 D0 e+ O- F. l: Z( n* w" x
  13. picture.fin = 1;5 ?( Z0 Y- Y. A$ r
  14. }3 }( c4 G7 N% l0 K& P' w, a9 L
  15. //printf("sendbytes = %d \n",sendbytes);
    . A9 Y$ r4 ]. S; h8 i
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    " W) F$ T! Y4 m" l3 H9 U8 m
  17. if(sendbytes == -1)
    / O" B. R$ H' [' F5 m
  18. {4 [+ p* W9 @, N9 h+ v6 u  D
  19. printf("Send Picture Failed!d\n");
    $ B7 _: n' J' C# n1 q
  20. return -1;3 {' s& L4 ~0 z1 _
  21. }8 ]6 S2 Z+ K7 k6 W
  22. else
    % o* e% \* x( k" ~3 j
  23. {; M  d1 a3 \, y0 ?: `- K& M
  24. fend -= UDP_FRAME_LEN;
    ; Y+ ]/ u: s% @! L( ~6 E! u/ @  B6 Z
  25. }- e, b4 W$ h: b+ y9 j5 w3 @" ~
  26. }
复制代码
$ l* M3 s  {  `  @1 b* U8 ?
: ^! P- p' P5 {, `


6 g5 n" D8 D2 |/ Z6 L/ F2 j9 m  x$ b' y) R. W" f$ y: \" X
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-4-27 15:56

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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