嵌入式爱好者

查看: 12019|回复: 1

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

[复制链接]

48

主题

55

帖子

305

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
  x4 B+ z4 ?6 r& k  j3 \! y) t7 C6 i/ E' `" Y


* {* c$ [1 m0 P% f5 Q+ c

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
! {  y: l2 T: ]2 x; e8 Y
. ?. O: b. [+ k) e3 C0 v9 T1 K

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

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


" D" X; [/ U, L  x2 z+ N

! J) B3 E: W9 ]: v

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

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

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

    . l$ M# v+ c- P4 z' }

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

9 Q6 f( @; E( N+ c" [9 c2 l7 E  K" G
一、HTTP网页服务器- B" E- ?1 w; f0 |

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    $ p" v# |. l& V9 d$ H
  2. {
    $ A0 c3 E7 P% E1 V' \% D- T  O
  3.     struct sockaddr_in servaddr;$ T  {6 }& R  `$ c# ]7 u7 O+ \, z
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    1 i- v5 o* l+ q6 n0 L3 ]: i# k: ]
  5.     bzero(&servaddr , sizeof(servaddr));. U8 g1 V. Z! _
  6.     servaddr.sin_family = AF_INET;
    + R# O5 Z' w9 `4 u0 R! i
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);# g/ y9 k$ I6 Y1 x$ p
  8.     servaddr.sin_port = htons(port);
      V2 V4 `% a/ Y
  9.     int ret;+ ]6 P* @- D3 |1 d' N
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)" h/ g# N( ^" X: N
  11.         {; x/ u8 b2 L# u% ^: N/ B( h# ^. O
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);6 i7 ~' t3 {' u/ C
  13.             return -1;( W7 O% S; Z4 E. D9 ^+ p: W. Z1 G0 N, R- x
  14.         }
    # c6 d* s5 H- x& M, Q- o" }
  15.     int on = 1;4 w# h5 @- H% {1 D% j2 n' p  D
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
      F  X( p# o* T5 c& M4 c# k
  17.     {
    & y* ~7 S3 f" y3 `- h, P2 E
  18.         printf("setsockopt error\n");7 ~8 ?% x  F( U. }* R9 u
  19.     }
    & J- y8 ^" s: d" T0 D
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);8 `  p. E& F$ k) ~: u$ p- H8 y
  21.     if(ret == -1)& D) l* ?# c5 |6 y/ w1 b
  22.     {1 M% v, Y) L# X) ?
  23.             printf("Tcp bind faiLED!\n");
    : ?9 Y; Z  [( U0 a* K) x2 v+ j% p4 {
  24.             return -1;
    0 t& o8 X# X4 \- ^
  25.     }
    9 Q: _2 w2 O; \: y, `( t  n  `
  26.     if(listen(*socket_found , 5) == -1)
    , Z. v0 Y7 f3 j
  27.     {
    ; d, _; k, g5 y- Q! y' |9 H
  28.             printf("Listen failed!\n");
    . G! s, L' z5 J- L
  29.             return -1;
      t; i. l9 w8 E1 d* [
  30.     }# Z. N$ V% k3 E. @) d+ {
  31.     return 0;5 ]) b5 |1 r% l' j
  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);9 @. q  @6 t& V: \* K* {; X
  2. void * Thread_TCP_Web_Recv(void *arg). q/ r! Z) j# ~( p" l
  3. {
    4 q$ [9 j0 K; `8 c
  4. 。。。
    2 c! g8 M/ X0 }$ C
  5. while(1)$ y$ F% u. x9 y$ m
  6. {* B$ @9 A$ t% L1 H+ c
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);+ `' Q( N9 ?* @/ m
  8.            printf("fd_socket_conn = accept()\n");
      G+ E  u# Y+ N3 G% t2 P
  9.     。。。
    & X* V) G( d0 J. d$ \4 m
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);( O4 w' N( }$ c/ a8 g
  11. }% A- Y2 G6 c0 \+ t: @/ u4 S) ~9 v
  12. 。。。$ R7 e. y  D4 F7 K* c
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);- ~* T- Y  [8 B  a& e
  2.     pic_tmpbuffer = pic.tmpbuffer;" h5 g4 r* X4 |1 P* P6 j% A$ m
  3.     pic.tmpbytesused = buff.bytesused;0 }% G+ B& |8 p: R3 z4 @
  4.     pic_tmpbytesused = pic.tmpbytesused;- P5 G, O" ]( v$ Y5 V
  5.     pthread_cond_broadcast(&pct);+ v$ h2 ?- C* T0 X
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;8 h9 S# }6 H  @# r
  2. pthread_cond_t pct;
    7 _" B% |! P! J$ X: Y9 Y' O, c
  3. int main(int argc, char* argv[])3 l4 [6 W! Z$ G+ u; X: q/ ]
  4. {
    ' [+ }$ A4 ]. c$ j, u- t8 C$ f
  5. ...0 c* e% R3 U0 m) d
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    2 R( N4 I! L7 s$ ?' `5 g
  7. pthread_mutex_init(&pmt , NULL);
    7 L& e/ w& [+ i) l) o
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);4 k% Z( N: u9 [* F( x* H" l
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    5 Y+ ]0 _( `# g2 b
  10. ...
    / N3 o3 F- X9 e: [
  11.     while(1). Z8 [% x4 a: e' c' O* P+ M
  12.     {
    0 h+ @, {& W1 L- A3 D; T
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    : s/ J0 R& K6 o, U4 d7 F
  14. ...+ `5 m; V+ ^0 L9 ]
  15.     }
    1 P2 j8 u' K2 X' |! E
  16. ...
    ) J) Y+ F; z' k' `$ f- f
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    6 R& [( H& {( C* ^" s- h# o0 u( M" C
  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" \. d/ ?* B% ]9 Q4 y( }6 j8 L
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    3 K# t) N; ]) X8 X1 w- p7 m; S
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \0 T  C# y) o" R# k' J; N
  4.     "Pragma: no-cache\r\n" \
    7 `% y$ n8 e; ^6 j0 _
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    $ q+ {) f0 [! f, ], L
  6. #define BOUNDARY "boundarydonotcross"
    + H6 e" K" S" }; [( o! Z
  7.     printf("preparing header\n");4 C  N: p! g5 m$ R7 }+ T' k
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    6 d, I( C  Z& S/ `' U4 h
  9.             "Access-Control-Allow-Origin: *\r\n" \
    4 x7 J3 g2 q' @4 \
  10.             STD_HEADER \: d" c) J: X7 V2 V7 i3 S  r0 j
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \  i" i! v# k& n; E- O1 y
  12.             "\r\n" \
    : E7 X5 n1 x8 l7 o: y* L
  13.             "--" BOUNDARY "\r\n");
    7 b% F( d7 Y# f: h
  14.     if(write(fd, buffer, strlen(buffer)) < 0): v& q+ @1 p0 c
  15.     {
    8 ~# k; _' c3 F& r' ~+ K) [
  16.         free(frame);
    # k( d3 s' U; b3 V3 w: v
  17.         return;
    & }0 P: u  H6 x3 n8 S3 T$ Z. u
  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" \8 a" e' S- G# l/ ~4 w1 m+ K
  2.                 "Content-Length: %d\r\n" \5 j; u. T( o9 E; E7 F
  3.                 "X-Timestamp: %d.%06d\r\n" \1 n& g5 c& T8 b7 \, R3 Y
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    4 W3 a: L) U5 t/ D. `
  5.         printf("sending intemdiate header\n");7 ^  Z: g( z+ d. {  j& z0 A3 \
  6.         if(write(fd, buffer, strlen(buffer)) < 0)6 J) M! x+ o# w) G, G, A  a5 t% p
  7.             break;0 s! i' W- H; B/ c( @! S. C: m: L
  8.         printf("sending frame\n");1 i8 Q: m5 T+ g3 w4 G3 y+ x! _. d
  9.         if(write(fd, frame, frame_size) < 0)# n3 b3 h2 a; ^* Q- F2 p+ u
  10.             break;2 t1 S& h+ U5 ?3 w8 V
  11.         printf("sending boundary\n");
    / X1 t. Y+ i' h" `! t
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    7 h* L) C2 \% ^. ?) u& _; [( @$ s1 j
  13.         if(write(fd, buffer, strlen(buffer)) < 0)# b0 K6 T& T; D0 n0 b' g
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

  s) W9 w4 x4 q/ [


- L) P5 w% ~  }; h2 o二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:0 r$ x6 g  ]3 b" {; X) t  q
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    2 r$ Y6 H& M. s# \- @$ x1 b2 y. z
  2. {
    2 X( m  s2 Y  B, N1 q* c
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    3 D! m( F3 d' s: _$ v) q
  4.     if(*socket_found == (~0))2 ]  e8 t' J9 v- N% n1 D8 m
  5.     {
    ( Y) _) y3 F5 p
  6.         printf("Create udp send socket failed!\n");+ `# c$ P* ~( P6 D+ i  S) N
  7.         return -1;
    8 X% p: u9 [3 P, V1 b; e
  8.     }6 T" H1 }7 e0 z4 }2 Z" k! p% o
  9.     addr->sin_family = AF_INET;8 ?, `0 a2 B( _$ z( f
  10.     addr->sin_addr.s_addr = inet_addr(ip);
      j6 Y1 C' a$ [8 R2 d
  11.     addr->sin_port = htons(port);
    ) |- t; c6 N7 R+ {- @7 b. D6 s# T
  12.     memset(addr->sin_zero, 0, 8);
    8 d, T4 \, C4 F3 C
  13.     return 0;
    6 @$ ]* h0 l4 [; z
  14. }
复制代码
" q; W1 ]& ]% a) i4 m4 x( H" |. q" {
2 F: ?6 Y. W9 ?7 b0 g+ D$ a, E
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
$ K; I* s6 l7 L+ A8 d5 R4 Q3 Y& V3 W  E( l' V  W) b/ _
6 y: g0 ]/ h# P0 x
  1. while(fend > 0)
      F0 G9 I: W6 l9 u6 Z) X3 H, K
  2. {5 |1 ^* l) M$ R3 }4 y
  3. memset(picture.data , 0 , sizeof(picture.data));
    ' q4 p$ I0 g. a% q2 |# J6 Z; u
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    ; I8 t3 k5 z" u6 K9 S% G
  5. if(fend >= UDP_FRAME_LEN)
    1 r9 o# T/ R8 S8 `0 `+ U% v+ F
  6. {
    6 g) Q/ W- p- [
  7. picture.length = UDP_FRAME_LEN;! W% D0 U7 i% K% ^( Q$ N  ]% F9 C
  8. picture.fin = 0;" o+ C9 i7 N4 }: @8 |. t; r
  9. }- C1 D& x( U4 `# r
  10. else
    7 Y7 h1 _) v! A
  11. {
    ; V$ {+ C6 M, ~) `& X1 K( k' y
  12. picture.length = fend;$ o4 A  W, u/ s0 M5 S" ?/ S& L
  13. picture.fin = 1;
    6 J% t1 Q2 @* B1 w3 {# f6 U0 x0 ~
  14. }  K1 E# C  B6 a) j& x% k
  15. //printf("sendbytes = %d \n",sendbytes);
    0 [# o. Y1 y' U1 _  ?- k
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);# d8 d. V+ i1 C9 A( j! Z
  17. if(sendbytes == -1)
    : u" W/ c- Z  @# \0 `  }
  18. {
    : n$ r) a) _6 I- V7 V# q# k
  19. printf("Send Picture Failed!d\n");
    3 F0 @/ G9 V- c. I9 }  w' ?% V( ?4 h
  20. return -1;- ?) M6 a, }7 b
  21. }/ v: s9 C! w: m* u( W
  22. else
    * \0 \, e7 O+ R3 f
  23. {
    + a1 h  X7 A, U) S3 x) n0 S4 x* c' F
  24. fend -= UDP_FRAME_LEN;
    ! b1 _$ T, G- M+ w4 o6 U( g
  25. }
    " H. |; q+ n2 J
  26. }
复制代码

1 o' I; w1 v5 u- I, j  c/ z" ~- t  i- U


2 B1 ~8 ^$ ]# v( b; N+ Z7 [: Z9 W7 D, ~
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-5-5 05:24

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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