嵌入式爱好者

查看: 11852|回复: 1

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

[复制链接]

48

主题

55

帖子

305

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
* k* d5 e: V4 y) j
" ?# G5 G* L( _6 |& I

2 W6 Q- u5 l) ^; Q# p

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
, Z$ h: |: C0 w6 H
4 p- a! L& ?! a0 L

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

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

) t" }! r2 @5 l9 D


( H- q  E, n" m, {. J, v& {

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

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

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


    % o* U( I, F6 f# R- l! n4 ~

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


: ~5 h/ j  _/ I, W一、HTTP网页服务器4 s: O, D  v% M# C

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)8 B9 u- y1 y- w( Z* G! V' r
  2. {9 o- n2 u/ N/ Y& A
  3.     struct sockaddr_in servaddr;
    ; C& C5 Y) f& _* c3 j; D
  4.     socklen_t addrsize = sizeof(struct sockaddr);* W1 `. G% I9 |/ A' m& @
  5.     bzero(&servaddr , sizeof(servaddr));1 \" s5 K' k, J( l: \
  6.     servaddr.sin_family = AF_INET;
    $ L9 z2 N& u! l
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);0 `& c9 V% e. W1 e
  8.     servaddr.sin_port = htons(port);7 p) E2 X% J* m* g. F& M' \
  9.     int ret;6 L* _4 p8 j' e" f
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)$ ]! o, n- }7 v! |; P3 X% p
  11.         {! U, H; q! y0 z7 q' M
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);4 m8 Q5 t; F/ t4 P
  13.             return -1;
    3 B# i2 P! c! e+ ], S2 T/ s
  14.         }9 v5 W- M, x+ P/ v) z  }
  15.     int on = 1;
    8 @- V! s, M0 X4 X3 @' ~
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    , y7 Y* Q- T; m1 g
  17.     {
    & O9 Y1 b! P6 d9 D! J; ^. _  ^
  18.         printf("setsockopt error\n");3 T, K) N; j3 _& Y0 Z. L' r$ n
  19.     }
    # B$ I0 N3 c) X7 F  o
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);4 q* `: C: t  y2 B; b
  21.     if(ret == -1)
    4 n$ x) T  R. r2 H' M* m6 l3 Z/ r$ h
  22.     {+ Y$ N5 B: y/ Q  s, v- O
  23.             printf("Tcp bind faiLED!\n");& G& k( o9 x( V% Y" ]
  24.             return -1;4 L# L7 G) ?0 B0 x7 B' D6 K
  25.     }$ ^3 B/ b% e  s: P& I4 c& [
  26.     if(listen(*socket_found , 5) == -1)+ D8 d5 t% d9 T& j0 t/ h  S2 I
  27.     {
    0 P0 I3 e2 W/ f: w7 u
  28.             printf("Listen failed!\n");
    , {6 J! G# C) ?1 s
  29.             return -1;
    ; U1 ?6 V: j  B+ L+ Z) s# Z/ n
  30.     }7 W' ~$ H' R# k! h, o. }( V; y
  31.     return 0;/ P5 q! ^* w. h' j4 M$ Y. H" e
  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);
    1 r6 ^7 \3 ]+ l7 M6 a+ k$ n% T: F
  2. void * Thread_TCP_Web_Recv(void *arg)/ f/ f2 k8 j9 @6 w% A! |0 R
  3. {9 b4 q2 h5 Q% X5 P
  4. 。。。- r  }' N1 |/ \1 L4 ~9 v: p7 {$ ?
  5. while(1)
    0 ^  r& }( r$ i/ j7 y, e+ p0 i- t
  6. {
    1 B- o) \/ l" q  P$ ]4 B4 m
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    1 @0 i$ ]7 y5 `
  8.            printf("fd_socket_conn = accept()\n");
    % U! ]2 k5 |+ y& D  |. C% o
  9.     。。。* R2 P; a* w2 n! j  F
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);! J9 j- a9 E  g  `. \' y5 _# Z: V
  11. }* I4 Z6 m8 A% k8 X
  12. 。。。7 r5 u) M9 C& L# t' V3 B5 `3 x
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    4 r% L* ~3 o; D8 p6 h
  2.     pic_tmpbuffer = pic.tmpbuffer;6 T7 P' d& J3 Z, ^- W5 I, z
  3.     pic.tmpbytesused = buff.bytesused;% @; v4 G6 W' Y6 k
  4.     pic_tmpbytesused = pic.tmpbytesused;4 x, X8 ^/ w% w# J& }5 p/ m
  5.     pthread_cond_broadcast(&pct);
      m- K, D0 J# \! {- l& D9 r
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    ! `! Q2 a* v# {7 W7 K
  2. pthread_cond_t pct;
    % Z% |0 @" C* n' Q1 u
  3. int main(int argc, char* argv[]). v2 w: `9 z. e4 G
  4. {( T+ p2 F: U( d# J" G- D5 p# S
  5. ...3 r4 |! d( g5 I3 }+ L( q
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    7 d3 C1 I/ e6 N1 ?6 z
  7. pthread_mutex_init(&pmt , NULL);/ I9 m& F1 q: ~; X9 s: h' J* i2 ]
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    ! r- J7 M( b9 _3 C+ e) Q# }
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    6 d+ L( E. C  f6 l! s+ l
  10. ...( s" E4 w! ^) d; N5 n
  11.     while(1)" N0 Q* H4 f1 a, Q& K
  12.     {- Q4 n3 Z+ _( u7 l7 G, d
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);) y7 n# {9 @  R$ l
  14. ...# W" }, B$ b7 J" s) o, u% \' V
  15.     }7 y4 x. l8 j( o, n
  16. ...( c  j2 t' F, }  x/ \2 u' s3 x
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    2 P  E7 Y5 V( z: F; Y
  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" \* p' a! r' G4 x4 s' u8 ~
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    , u: o5 M3 F$ K1 I5 l
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    * D: i. |% T$ X: g
  4.     "Pragma: no-cache\r\n" \
    # E* |* K; L' c+ d
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
      @8 n" M1 w1 n- R$ R
  6. #define BOUNDARY "boundarydonotcross"
    2 r9 O9 E; [+ `: d
  7.     printf("preparing header\n");
    7 @1 s3 t$ A/ l, S! z4 U
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \; z- A- u" `6 k* {* z) W8 c. g1 B
  9.             "Access-Control-Allow-Origin: *\r\n" \
    & B1 O& U1 D, m% {/ |) L
  10.             STD_HEADER \
    5 |' T. k- N6 G! Q! o4 y
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \1 R$ e; \( \& w, U1 @& a1 b
  12.             "\r\n" \. \$ @" |: V* t" H6 c" M
  13.             "--" BOUNDARY "\r\n");
    ; t, v2 O3 m+ U6 B8 D2 C
  14.     if(write(fd, buffer, strlen(buffer)) < 0)2 b- d5 H- Q& U8 R
  15.     {, Q5 }' j8 w" |7 ]2 |5 ~
  16.         free(frame);4 x5 c; k  [: h. H& h5 q" l
  17.         return;
    & f- }7 z# E' 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" \) m* V7 w' N1 F* ^8 \/ t$ j- p
  2.                 "Content-Length: %d\r\n" \
      Z! M' I) ]) e4 |/ K& O" E
  3.                 "X-Timestamp: %d.%06d\r\n" \+ l: E: y7 {1 \+ j6 {0 o4 Z
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    ' R( q3 R6 ~5 x9 C5 P. o# h
  5.         printf("sending intemdiate header\n");7 D5 S$ `# c5 h, y
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    7 u4 p! j0 M$ R5 G6 {
  7.             break;* _  k- k+ Z: _' v1 |% [
  8.         printf("sending frame\n");/ e% n7 a1 `3 M/ I5 ]5 k" e3 v( E
  9.         if(write(fd, frame, frame_size) < 0), H. f6 P1 h6 ^7 P$ n
  10.             break;$ D. R! P" N' s1 Q) J% Q$ ~/ G
  11.         printf("sending boundary\n");! F" }; d  [  Q1 j3 ]- w
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    6 x$ b. Y, l0 j. T. p5 N
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    1 {; T( @) d9 I
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


* A# K3 @. x. H

/ b  m. U2 j! a; J, Y3 p# }
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
" S# z' N4 e1 p! Q  B
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)# L+ U6 m, J7 s: @
  2. {
    % g6 |: c  t2 H# u% ~5 p
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    # P" C* C1 l! ^, E5 k4 L$ H4 p% G
  4.     if(*socket_found == (~0))
    ! E  b" X1 _/ ?/ ?" ^
  5.     {
    ; ?1 O8 H' i! u8 I0 W5 E
  6.         printf("Create udp send socket failed!\n");  d7 l) y& e8 l7 o, q
  7.         return -1;
    # N) N# z4 e% k! b
  8.     }4 R  M) S9 R. \8 \: T
  9.     addr->sin_family = AF_INET;
    ' F# a( x6 \. @. j/ e! a
  10.     addr->sin_addr.s_addr = inet_addr(ip);! `+ X( z: y6 I5 G! B
  11.     addr->sin_port = htons(port);$ G) l1 [* w( W$ f; ]* r5 U
  12.     memset(addr->sin_zero, 0, 8);5 w# H  ]0 Q! x9 U# M/ Q
  13.     return 0;
    , a" c* D" O; t* z
  14. }
复制代码

6 x6 F' W6 y( }  s. @5 _' Z
  }) t8 A) \/ _而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
+ e8 R4 a4 @4 B% ~9 m+ Q/ R
, E9 s+ C/ x3 b' D
' Q2 U+ {' c6 O* F) u
  1. while(fend > 0)
    2 y" F3 b; h; C- p6 l
  2. {& e! V; Q/ w% _$ K& f
  3. memset(picture.data , 0 , sizeof(picture.data));2 p5 ?* m7 r/ P( |( a
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);% ?  _' J" z. R# x1 i1 e4 u2 P
  5. if(fend >= UDP_FRAME_LEN)
    & D4 B* n2 R( y2 O! \0 y
  6. {$ o- M3 j2 G! I/ K& S4 K
  7. picture.length = UDP_FRAME_LEN;4 c" c, R8 a8 G
  8. picture.fin = 0;+ d$ N1 n5 j& s
  9. }
    ( U% h0 H* C$ @! ?# A" i* g
  10. else
    . K  e$ }9 r5 m- E; ^
  11. {  z, e( c% d+ b; C6 B; o
  12. picture.length = fend;" L% y+ O0 T1 b: R) m
  13. picture.fin = 1;
    0 G) q0 [6 U( a" T5 @2 ?' c
  14. }
    % s6 |/ u  S- @, e$ U. u; p' V) y/ ]
  15. //printf("sendbytes = %d \n",sendbytes);
    * F2 _" W. s, z/ c6 }8 p
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    * ^1 Y8 Q, E$ @4 N
  17. if(sendbytes == -1)# z3 G9 n/ e2 s0 [; F% w0 l
  18. {) |* Y) k) j* |
  19. printf("Send Picture Failed!d\n");( v" u6 u% c5 Q- m8 C! Q5 }
  20. return -1;
    " x4 s3 ~# {8 I' F" V
  21. }! O2 m- D) u+ V: @
  22. else
    ( i* N- U8 D% m+ L; q, f9 ?
  23. {
    3 z+ D+ {7 n; h  J- G$ K7 ~% [+ y
  24. fend -= UDP_FRAME_LEN;. J  I" h6 ~* Z: c* b
  25. }2 W0 F7 D" z" e$ G2 A1 g
  26. }
复制代码

; w6 m$ `( m! k9 Q6 _" p; ]4 ^( S, }* s8 n/ i; u6 a9 d3 ]

8 B9 w$ C* G2 v# o# a2 j

, @  X( N: A0 N. W: Q& ]. giMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-4-22 04:01

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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