嵌入式爱好者

查看: 11274|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
7 s  @7 [# I; e* }+ I+ [
* n/ Y5 }( G0 J! p5 Q, t

( K" Y3 L5 C2 B& A: C: \

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
3 p" ^6 O9 U" T* z, I  M
, Y: c* k' ~# ?2 K

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

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

6 A+ x6 O  ?  p! \


* m( W' i$ B* G  s

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

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

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

    , D" t; t" @% m% m

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

# R" G3 i; r5 R4 Q5 P) q* Y: N; {0 F
一、HTTP网页服务器$ w2 k2 Z. ]3 e) ]" F; _' I

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port). R, e  M4 J% f6 W7 d- ~2 |
  2. {- r( B4 }. W& `' ~1 H% J1 F7 |# }
  3.     struct sockaddr_in servaddr;& O* X% M* ~- A: V: l! a# p
  4.     socklen_t addrsize = sizeof(struct sockaddr);+ u6 @# l( z& m( }9 D! h
  5.     bzero(&servaddr , sizeof(servaddr));
    " G- Y5 V! A0 ~8 p
  6.     servaddr.sin_family = AF_INET;
    1 [+ w9 r1 X) b! k2 [  ~3 p) \
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    2 _+ \+ [1 M: D& ^9 T6 U( q% z
  8.     servaddr.sin_port = htons(port);3 s" ?; W+ A" I
  9.     int ret;
    0 g# s! h2 D% |$ n  ?2 x
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1), S1 t4 ?' s! p; a
  11.         {; F2 y  a+ d- P& K& y- ^
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);# J( Y% _5 `+ p$ R" [
  13.             return -1;
    : [% f  G5 I, N# u: M4 Y
  14.         }
    8 w& u0 @/ y! h- z5 ]+ l. v
  15.     int on = 1;) o5 o6 i7 v" p
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    8 o# D/ m1 [  n2 B. g, q
  17.     {2 m8 A8 a, T9 n0 R2 q
  18.         printf("setsockopt error\n");
    ' A) T+ {% o% [& ~( U
  19.     }( J- Q) V: E  {! p
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
      M! q. O& j9 z, P9 n
  21.     if(ret == -1)* q- J  @9 M$ N% P1 D. o
  22.     {* k1 K- N9 X7 L% z4 X9 {
  23.             printf("Tcp bind faiLED!\n");
    ! i4 e- o# K: W/ N; G% a; U
  24.             return -1;% M& L- ^, N/ N8 y+ |
  25.     }/ v% G3 ]; E5 A( M) |* K2 _
  26.     if(listen(*socket_found , 5) == -1)
    . w% J% r& I) S7 `
  27.     {$ e; S. y! r4 R: r
  28.             printf("Listen failed!\n");
    3 O/ T$ L, _; i+ S, A; N
  29.             return -1;
      a0 T1 Z' }6 e- m
  30.     }* z/ h% j* i4 A% D; {
  31.     return 0;8 }& O3 C0 u2 k/ T/ |8 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);
    " \5 u+ s3 b. g5 k3 {& t9 h6 q
  2. void * Thread_TCP_Web_Recv(void *arg)1 j& b6 b/ ?. w. |+ P5 k$ H
  3. {& g  e- f1 D5 f8 K9 Q: H+ t
  4. 。。。& }) n  p. G3 _; T& B- A: {
  5. while(1)  x0 r" w. |0 ~+ h7 n
  6. {
    ! m# P1 t$ Z! ]0 C
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);, t; Y! [6 S$ n/ Q8 I/ b
  8.            printf("fd_socket_conn = accept()\n");$ v# k6 a" G( s3 a- ]
  9.     。。。$ I' g- w; {/ q3 ]
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);* X. R+ k- M- q; d9 S
  11. }
    " c! L0 t: C, q! D
  12. 。。。
    / F: l" s' O) e+ G8 v$ W
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);) F) I2 J( i% |0 @
  2.     pic_tmpbuffer = pic.tmpbuffer;' H7 F; `+ m8 |8 d* ]( p4 r. e
  3.     pic.tmpbytesused = buff.bytesused;
    9 h% \' j" r2 o/ S- z+ O
  4.     pic_tmpbytesused = pic.tmpbytesused;
    : A2 e0 }" w9 @& f3 ~
  5.     pthread_cond_broadcast(&pct);  P6 t1 K7 B3 k3 ~0 Y+ U7 Y8 O
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    ( h0 p% K+ `2 H6 c! ~( h, A
  2. pthread_cond_t pct;3 ]5 c3 ^  e2 X& B. [
  3. int main(int argc, char* argv[])7 l% k$ |7 P1 J; |
  4. {0 l& ]/ y! U& p) s! y( U7 _. l3 ~
  5. .... Y, H8 M% X( s, S% T
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    2 ?& f" `; c2 z! t  ^
  7. pthread_mutex_init(&pmt , NULL);
    ) r+ x" m) \% h0 I! |9 v
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);4 C: s, s% S& m; x4 D
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
      d- O& D7 ^9 h
  10. ...
    ( }2 I3 ]7 Y4 {! L# O" v: p
  11.     while(1)
    + o6 q0 i& |  o. e  B
  12.     {6 n/ y. @$ J% c/ @
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);% B9 Q' Q- X1 C& n" u( S3 r
  14. ...2 R% S. n" M8 ~  R% [, A& A
  15.     }
    ) L6 L  @. Q6 S! `* |
  16. ...
    # J; a7 V4 B- I4 ^4 O8 M* X. k4 L' U
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    ( Z3 \- C( _% o' E2 O9 b& o. _" Z; ?0 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" \
    & e9 x: x" w( s" K: B. ?
  2.     "Server: MJPG-Streamer/0.2\r\n" \+ F- [4 E/ K) }' ?: p; d8 i& ?
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \: G5 R* A, a5 ]( |$ A- u8 F
  4.     "Pragma: no-cache\r\n" \
    ( U9 d5 `: A8 y( k# \8 b3 V* _; p
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    ) @8 i/ D" y( `4 S, [0 n3 g
  6. #define BOUNDARY "boundarydonotcross"8 V* h0 z$ Q! k" g
  7.     printf("preparing header\n");
    ) a1 R* |4 m  X5 |  o2 v! y/ y
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    4 [9 G3 ^. L: G! P7 W  E+ F2 ?" N
  9.             "Access-Control-Allow-Origin: *\r\n" \
    * g1 m% e2 r, u6 \
  10.             STD_HEADER \
    + \' o7 c( a* d1 j) c" o
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \" e; t3 g! u, W9 M0 E% ?7 w6 s0 f2 a
  12.             "\r\n" \
    / R9 f! I- e/ f8 J
  13.             "--" BOUNDARY "\r\n");% `2 o( `# u' z4 R
  14.     if(write(fd, buffer, strlen(buffer)) < 0)1 v) k8 `, K3 O9 a5 o
  15.     {+ Y# N$ z" W6 {  m% t+ I- t
  16.         free(frame);
    : m# \+ |1 _6 [/ i( N
  17.         return;
    2 N- b6 T2 T" }) @! s/ b- |
  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" \( j, `3 c6 m  V' V2 H$ k% g
  2.                 "Content-Length: %d\r\n" \
    ; A7 s* X3 V) a+ P( q( C1 E6 i
  3.                 "X-Timestamp: %d.%06d\r\n" \
    4 j- S* `9 d( ~4 J8 }0 i
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    + K0 }; E4 M3 o: u$ ?8 f% C' b
  5.         printf("sending intemdiate header\n");/ P, ]! w, e) i7 J3 p2 @& ]9 H" X
  6.         if(write(fd, buffer, strlen(buffer)) < 0); P; F5 O: i3 N; `
  7.             break;
    4 I6 e, l( V5 G5 T) l6 Z3 F
  8.         printf("sending frame\n");$ S" V8 `2 t% u
  9.         if(write(fd, frame, frame_size) < 0)
    / r$ ~9 X; v6 J, u) c. y
  10.             break;
    - Y: f4 T- Y! a2 ?
  11.         printf("sending boundary\n");
    - n6 b9 @4 L7 d" W* x" f9 w0 v  N$ E
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");( G! F- V9 b% E; I8 f1 _* T
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    3 ^1 |$ K$ ~+ V( X7 t' i, u; k
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

/ k9 b4 g2 L: a


9 @6 H$ b* D: U  I0 n* e+ x二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
2 N; E& ^. D8 m) d
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)5 D! P1 Z9 \9 I! t9 ]
  2. {4 _  N+ U/ H" P$ r; Y4 d: e7 w
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);0 {. b3 v& A, K% u& r2 e0 m, d
  4.     if(*socket_found == (~0))# b) \; c+ J' B0 `& R
  5.     {2 t" ^9 M+ @7 |; T
  6.         printf("Create udp send socket failed!\n");9 C% q' P: R6 J. G) ?# P& p3 K3 S
  7.         return -1;( c" [8 r3 A# W: z+ F6 l. Z, f8 [
  8.     }
    6 g2 h- T# n1 r+ J5 [& h8 O
  9.     addr->sin_family = AF_INET;
    1 f& j4 }" ]; w% Q5 x6 n
  10.     addr->sin_addr.s_addr = inet_addr(ip);/ g. L, M  {5 L4 G& R2 P
  11.     addr->sin_port = htons(port);
    $ e3 o% p+ [9 X' E9 r, E; T3 e
  12.     memset(addr->sin_zero, 0, 8);- T0 F, W) E- T/ o+ ?0 U1 n3 v8 ]
  13.     return 0;
    ! M( e2 @6 g7 R. t/ Z6 e. Q
  14. }
复制代码

) D2 t, b  }. N/ @( n7 K/ I7 G5 w, K
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
% e" R8 E, b* g6 O  `
+ @/ E/ J- `# {
4 {# ^# P1 I+ _
  1. while(fend > 0)) }, n1 D* e- a( N! D5 p' L+ B) I% O: `
  2. {
    9 Q1 w+ Q& |; t: [/ n- ?
  3. memset(picture.data , 0 , sizeof(picture.data));
    3 c% [0 n1 p5 ^0 ~
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);% }' g3 x) a3 N) [. W* V, n0 y
  5. if(fend >= UDP_FRAME_LEN); q* U/ ^! L0 Q- `8 V
  6. {
    ' x# T# D" ]" H0 M& H  f
  7. picture.length = UDP_FRAME_LEN;8 |/ f4 T0 C+ e* a
  8. picture.fin = 0;
    8 v% e( _# g6 @- V& O: k" z
  9. }$ H7 i; Q3 Z" @; G3 U/ K
  10. else
    ) T# A. ~: Q  R
  11. {& H% j0 p& {  j! z% H' G* N
  12. picture.length = fend;3 ^+ i1 u, N1 g* T* i
  13. picture.fin = 1;) C: Z* q5 p8 ]( B
  14. }
      Z- ^/ ~; W5 m
  15. //printf("sendbytes = %d \n",sendbytes);
    # S$ ]/ H8 T/ K9 i! T! E9 [
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);" f- x5 p! n- e5 z  H* r
  17. if(sendbytes == -1)
    8 L2 x9 `4 l! H" [' m& R
  18. {8 g8 N# i; @$ ^% `: Y
  19. printf("Send Picture Failed!d\n");
    - T, v' c! `1 b3 O+ o( o
  20. return -1;: f. S$ I! w2 n0 b6 C$ e0 g
  21. }8 ^0 P" @% }5 r' C' C! U& Q
  22. else. R$ s( g3 {% h- f! x9 x& R
  23. {  N$ F+ v/ B  ~  \/ a3 f
  24. fend -= UDP_FRAME_LEN;( |  r* j" g: M5 m5 ^
  25. }
    # l+ T) \; T- i# n
  26. }
复制代码

- H/ b! f. x1 l1 D; Y7 `- `* C# x5 L- t0 [- q! n+ y

) x+ e; r9 m& k8 B  Y9 X

0 r4 z" P8 y; @* Y4 I4 riMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-2-27 11:55

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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