嵌入式爱好者

查看: 10935|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
7 E. |, x# L$ l0 I- W5 v& c: U  x1 i3 d3 s2 T0 k6 u2 e

7 x6 U' B( c9 C' t

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
2 N6 A# C" E# A: D5 B9 k: S: i: `, L  Y/ Z. k8 t

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

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


1 G: m! L6 z  d7 Z! V. i! b! O


5 w) z3 p3 w. d

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

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

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

    # m. A6 p) f! f

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

. ?$ n0 [  a# u0 B2 o! y$ h
一、HTTP网页服务器+ v. s) C( e! w2 A+ }; f

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)9 I6 ^0 x  w4 j9 P- E5 Y# G  k+ i
  2. {0 O$ S2 C$ g, r1 b
  3.     struct sockaddr_in servaddr;
    3 `: c0 `5 @# U' G8 Y8 b5 i
  4.     socklen_t addrsize = sizeof(struct sockaddr);0 `/ H4 |9 A' Q
  5.     bzero(&servaddr , sizeof(servaddr));& e$ |! _! r" R4 k2 N/ U5 h
  6.     servaddr.sin_family = AF_INET;
    + J$ V( q2 _/ q0 X  t. L
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);. w+ n& z6 H* ?# Z7 ?
  8.     servaddr.sin_port = htons(port);
    ( |8 q$ X- C* M
  9.     int ret;' t  k! y7 p/ T* C
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    % [; K2 w, M7 ?" D% L3 T& Y
  11.         {
    # U" c9 E9 b0 t- B
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);# V3 m8 m  Q, g1 W
  13.             return -1;8 s  }2 r) k3 X7 W1 Q- t
  14.         }
    ( L/ s, T! Y6 w1 c  p0 a, N% Q
  15.     int on = 1;1 ^  j% L( V% W( ~) H" U
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0); ^$ W( \$ a4 [7 e3 t, P
  17.     {  p+ x: E5 o# Q9 v& O6 y
  18.         printf("setsockopt error\n");
    & ~6 l$ V" v1 {! G
  19.     }5 g/ N+ g" ?2 f  D: L
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    ! l' ?3 H& w) I4 L1 u! w$ V
  21.     if(ret == -1)
    : @; Q3 g; O1 i) {. X8 s& @; P
  22.     {
    # W/ b" M  l7 S! t$ t4 M
  23.             printf("Tcp bind faiLED!\n");+ w  V8 N- j9 q  k2 C7 L$ T
  24.             return -1;/ K, `2 d% j8 E! R* b7 z9 v) A$ Q
  25.     }5 G- u; V; Q/ Y
  26.     if(listen(*socket_found , 5) == -1)4 U% n5 f- S* U
  27.     {& o) w2 Y9 O0 x4 w  _
  28.             printf("Listen failed!\n");
    4 ~2 g2 t4 }) }/ }* s' g
  29.             return -1;
    % H2 O0 c& x- I  G1 k; }5 Z
  30.     }
    # S. e' T# Y- u8 {/ N( ^* `; v7 }
  31.     return 0;, A" I6 L4 n- V7 f
  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);' x% ?9 o8 b7 I: v+ }! x
  2. void * Thread_TCP_Web_Recv(void *arg)0 y: M% o: M/ v. a1 Z
  3. {
    , n; q4 I! p3 f* Z5 s6 b# R
  4. 。。。7 [* p6 J$ X: b' y5 U
  5. while(1)7 M: C( E5 ?' f8 G
  6. {) w8 M- m. s- R6 p& |* |" ?4 C$ J. f
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);, f5 a! X+ g# x! Y7 m2 y7 X3 N
  8.            printf("fd_socket_conn = accept()\n");" F( _- |% {" C' e
  9.     。。。7 W4 p6 l- k1 C
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);7 a7 j; C! K2 Y3 [  k7 k
  11. }& t# N5 X! H2 W8 e4 D
  12. 。。。
    + C. N6 ?  n# q% e9 t
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);( {4 ?/ ]3 x0 I1 \. D( q4 W
  2.     pic_tmpbuffer = pic.tmpbuffer;, v4 }0 N% V( ~
  3.     pic.tmpbytesused = buff.bytesused;
    ( l0 c+ ^: A: g0 V6 t* N, M4 |
  4.     pic_tmpbytesused = pic.tmpbytesused;: ^2 B0 D( l8 O0 ?  h* G* j& A" O  H
  5.     pthread_cond_broadcast(&pct);" @- e$ \0 e4 L0 g4 {4 U
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    $ n( Z. |% _% ?7 L- x
  2. pthread_cond_t pct;1 D9 F' f! _. b! j. c  r8 N! k
  3. int main(int argc, char* argv[])$ k4 Q0 Y6 B3 [; u
  4. {" v: Q7 P' R; V$ L
  5. ...6 q2 A5 p: l7 q. E8 ^
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);+ K/ i; s% c: U0 |* ^' T
  7. pthread_mutex_init(&pmt , NULL);
    ' @: h" j# x" j" p! ]
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);  n. Q; J( a7 |8 x& s: t  f, P* v& }* S
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);2 Y6 Z3 A% ?( K; @3 V% G1 ?  P
  10. ...
    1 e: l- B9 F  r% r6 }3 j  D: B
  11.     while(1)
    # h- B! j9 K% U( D2 n" w
  12.     {* Z0 ?1 R* b" d* _6 w6 j) t3 V3 p$ ?
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    " k' Z, Z7 I& P
  14. ...; W) W* v, c/ a  m/ D0 l
  15.     }
    . ^) z7 m; ?) [$ t+ ^+ v
  16. ...1 d$ {' P7 E" {  V* p/ E
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">5 V- _1 k" n0 ]4 w8 Y+ K" S
  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" \$ ]" M; {: \; |  x" N& Q
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    1 T: t( F1 D) b( c+ J
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    * T! ~0 X2 [9 ~1 R
  4.     "Pragma: no-cache\r\n" \
    . ?3 D* }9 g2 S0 ~
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    . o7 J0 h& t0 O4 N0 G
  6. #define BOUNDARY "boundarydonotcross"2 Y: G) M' g+ U
  7.     printf("preparing header\n");
    " I+ b* m2 t; u5 R% j
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    ( d- {9 n+ _3 p! v; f
  9.             "Access-Control-Allow-Origin: *\r\n" \
    ; H9 h8 I% N/ z* W
  10.             STD_HEADER \
    4 `5 A# t5 V+ ~
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    ( F; R4 _# V* Z3 m8 [0 |
  12.             "\r\n" \
    ( h7 f# V% o; B$ Y
  13.             "--" BOUNDARY "\r\n");6 l8 v+ Q) {8 q+ W, D" ]! q$ \
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
    5 O/ U# b2 t4 L
  15.     {
    . I* N  A8 p  F0 x9 S% i1 @1 J
  16.         free(frame);
    4 ^) o) x. G2 k: ~8 d2 |$ q- `. m
  17.         return;
    : g$ W" Z. ]0 A( D. }
  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" \6 L! F  i3 E8 }% A  [7 ?; d
  2.                 "Content-Length: %d\r\n" \" i* m6 \8 r/ m7 y
  3.                 "X-Timestamp: %d.%06d\r\n" \
    - Y. m/ A8 a) ~% y1 [
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    9 N/ z( B) X! H7 |6 l
  5.         printf("sending intemdiate header\n");
    , v: ?8 ]: R7 z; {6 O/ y
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    2 s# y" B  j* x+ j
  7.             break;2 f/ o; f' F3 L7 O! e1 x# j
  8.         printf("sending frame\n");
    1 I; J( Y. l! y7 d4 d, H8 r0 |+ }
  9.         if(write(fd, frame, frame_size) < 0)/ V/ t! k7 ]( G5 W- }6 [- I* A6 Q
  10.             break;
      k+ e) I5 `; o3 M
  11.         printf("sending boundary\n");
    ) p: U/ i& `4 y  m. z$ Z- o4 Z  o
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");: h) f( A* }* p- [/ ~
  13.         if(write(fd, buffer, strlen(buffer)) < 0)2 F9 P$ o6 d# n; t: O
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

" C4 r" v% s5 I$ E6 b


) T7 A$ g' X8 D4 O# c二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:- W4 X- C! K$ O) H# ~- t* Z
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    0 A& o! q) r" r2 \
  2. {% v; ]; m5 k' ?; P
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);; b5 C2 c2 J0 @2 [' ^: x
  4.     if(*socket_found == (~0))' k( s$ [1 s2 S2 o
  5.     {
    3 O8 n, d: z$ e& g6 d
  6.         printf("Create udp send socket failed!\n");0 l# H2 [- k9 O+ K3 y
  7.         return -1;2 A' }* U7 B# S/ ?9 j6 [" T
  8.     }) M* d5 |, S% P4 O1 }
  9.     addr->sin_family = AF_INET;
    & b3 T9 r9 |& g$ [$ T0 U
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    , O5 D$ Q4 C7 K9 E0 t9 }; H1 ~( @
  11.     addr->sin_port = htons(port);% t+ R! U, c4 h/ k7 k- L$ \
  12.     memset(addr->sin_zero, 0, 8);
    & R* `5 y6 {& s# Y4 k: v% \
  13.     return 0;- R/ L+ Y9 ]2 ?7 j- S: w6 t$ M$ C
  14. }
复制代码

; e$ ]9 [4 x$ o  S) L6 m
& `. v  k2 s0 p( T9 ^+ x8 }% c而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
9 B7 Z4 M" d7 e% I$ }
. a4 Q$ k$ W( f) s) G
( e  T) p; K3 Y/ z3 E
  1. while(fend > 0)' v" l' A* w0 }; t2 }
  2. {
    3 C+ C" v8 W; u: W  W& D9 N
  3. memset(picture.data , 0 , sizeof(picture.data));
    & {$ P0 T" b, W/ h" E7 ~
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    * _7 E: `& o0 L/ |4 M
  5. if(fend >= UDP_FRAME_LEN)
    7 N; B2 p" G+ \; R* O: ]
  6. {
    ) s3 _+ T( n* ~, n( i3 i3 p
  7. picture.length = UDP_FRAME_LEN;! F% ~; p" o; J/ R$ F
  8. picture.fin = 0;6 X5 a6 G% I" y8 H' J+ g
  9. }
    2 H; Z5 |" a# w
  10. else# G, d% Q1 o  c# Y# p
  11. {' @; X4 F  ~( d; E
  12. picture.length = fend;
    6 ?6 H& _6 O7 b( {% K' g
  13. picture.fin = 1;) `, F7 N: g( i0 e% |  a7 x
  14. }
    ! u6 t- X- B& w/ |5 Y6 K' G3 j
  15. //printf("sendbytes = %d \n",sendbytes);
    * a$ c7 P% U' F( a
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    2 y# S- U% p7 N: C4 Z4 u; o
  17. if(sendbytes == -1)
    & S! B7 _: ]8 ]) k" j
  18. {
    7 v' |% u# Z2 D0 T* l: E2 M
  19. printf("Send Picture Failed!d\n");
    , g; [1 k, @! Y
  20. return -1;) u( A6 U- M  b8 I* e# {$ j
  21. }
    , {- Q1 }# ?1 }' ~6 }
  22. else
    # i, \4 n( P7 o4 w) K3 G( l
  23. {
    $ v# c4 q; r. I9 Y5 q; @" R, ], o1 L+ D
  24. fend -= UDP_FRAME_LEN;/ v" N3 O3 p" H, |6 n& G
  25. }
    " t$ ~1 W0 ?5 G& X0 U) Y& ]% m
  26. }
复制代码
, b3 V2 U; R" l1 x. n
8 x! e2 L) c: Q) ^1 U


/ g' q* J# }7 N. b* P1 z7 y4 d
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-1-29 22:26

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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