嵌入式爱好者

查看: 10861|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 & }6 h3 W( g4 g$ D" A$ c$ B

0 \3 S2 O) x+ r6 @+ [9 b% @: z

$ V- o0 C! s0 U* I! \+ r6 e8 f0 v

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html# m' U  Q" e+ i, J0 r- \' e

4 `7 n1 N- L0 V8 e: `

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

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

# z! S" L, a/ q


2 b9 _% w+ U. A" }; z2 D

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

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

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

    5 z& K. W5 u/ I3 w# i& I% J8 h

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


7 z1 v6 @, \0 C, |+ `一、HTTP网页服务器
. o$ {1 J* \  C$ T0 C7 M6 ^7 U

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)8 i+ D. \# k* r  R5 k
  2. {0 x5 |$ ^% \5 U
  3.     struct sockaddr_in servaddr;3 V$ t9 W! X& g7 Z( U
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    # Z" S7 y# |" T, I/ i
  5.     bzero(&servaddr , sizeof(servaddr));
    8 K2 o* Y' l" m  Q0 p. |3 `
  6.     servaddr.sin_family = AF_INET;
    ( _" [( O" M* |0 {3 E, m3 u" h
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
      Q; C' d: a' Z, Y
  8.     servaddr.sin_port = htons(port);, O/ C( Y3 D, A& K
  9.     int ret;
    . J" [5 R, d" J& m8 L6 _0 b
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    ( @1 A7 m9 z/ U# C# i
  11.         {
    0 w* c) l) r4 K" C0 m* P. n3 W
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    & [3 v$ N4 T7 N  _
  13.             return -1;
    9 E5 f+ K- O8 u1 v$ W; v
  14.         }+ p6 h& Z! c+ h3 U, K
  15.     int on = 1;
    : {+ I$ N  _6 C
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)- ~3 _0 Z4 ?1 w( G/ ?
  17.     {
    ; l! z# Y0 K6 Y
  18.         printf("setsockopt error\n");
    ; P2 l  L7 C0 |0 N
  19.     }
    - g5 M" r4 A* I1 E# A
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);' `& G" B' r! f- v6 b
  21.     if(ret == -1)
    + F( p( {% _  O" @) D9 W$ q
  22.     {3 @( h+ x8 p/ s, _7 s
  23.             printf("Tcp bind faiLED!\n");" C8 |# p& r8 E2 z; A+ B" D
  24.             return -1;4 C( l8 {. \; Z4 I, o1 S
  25.     }& a9 e. B% `0 L; p- u" s
  26.     if(listen(*socket_found , 5) == -1)" i- ~+ S8 @( [0 d' K
  27.     {  F" O$ }) F+ C4 [( e: J$ L  m
  28.             printf("Listen failed!\n");
    ) }5 t8 J" F6 x, U# p  C% C9 P6 x
  29.             return -1;
    " r4 {. A4 k9 E' U! F# Y8 p
  30.     }0 }! H9 \* K6 ]
  31.     return 0;
    ( b/ x2 P$ I7 P# ~$ [- p; ]. u& W! d
  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);0 Z; m2 n( }6 G, f( @) {0 |# u
  2. void * Thread_TCP_Web_Recv(void *arg)' [% c7 A3 N/ ~' x
  3. {, ^3 ^; |2 t+ e9 p1 X
  4. 。。。
    1 g0 o1 t! R2 F8 t/ G5 V0 m
  5. while(1); d3 q, o$ M" ~* [1 R1 L/ N6 E. f
  6. {5 u" }- ?+ z& l5 J* W, y
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);' L, l+ w/ c( m& v1 d
  8.            printf("fd_socket_conn = accept()\n");" M  i! ~- d- x! t) L% k, q4 M1 C: Z
  9.     。。。
    $ X  i& B% w* k+ Y2 i
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    # \4 L( @, v4 B1 Q5 D
  11. }
    5 S5 D/ m% Q, a* C. g% r
  12. 。。。
    % w0 N* g; V$ Q, \. K1 f! ^% I
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    : I0 X& J- u4 {/ F+ U3 `
  2.     pic_tmpbuffer = pic.tmpbuffer;! b& H! \7 U' M4 T" z0 O
  3.     pic.tmpbytesused = buff.bytesused;
    2 ^# {  C" I+ Y/ ^5 H+ y
  4.     pic_tmpbytesused = pic.tmpbytesused;" E3 I/ |" H7 |8 T
  5.     pthread_cond_broadcast(&pct);
    * j( z, n) u+ \, r& F" P
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    7 r  j& N$ ]: U# c9 ?, x# T
  2. pthread_cond_t pct;8 D8 l+ I1 P4 D" _
  3. int main(int argc, char* argv[]): `- _6 m- O( E, m! T* O# g6 W+ x
  4. {9 e4 W) ~  `3 F( H: E! g
  5. ...
    * _& ?2 k( b, Q! m5 y1 y
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    & x/ ^+ G8 V9 }& Z
  7. pthread_mutex_init(&pmt , NULL);
    # r7 \! s6 H9 ?* T7 I
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    # g. l. K  T( ~) }7 I
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);3 z/ C- h' r* j/ V  l
  10. ...) h: X; T) t( F. V$ p
  11.     while(1)
    & S8 W- C2 \" D; d& r/ t
  12.     {3 `- O. Q& P' q' B% p* K  n
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    0 ^6 a( s  a9 ^* h; ]& `  O" O' O
  14. ...
    : h% p! d0 ~8 z- s& H: s- x# y" X- j
  15.     }
    ! a8 y$ W9 u+ _3 K* ~/ K* `- X
  16. ...3 L$ d/ b8 ]* q/ S, l) }- j/ y
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    ; u, v; m8 Q' G5 z# ~9 N
  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" \6 n1 \+ [- }! r' b
  2.     "Server: MJPG-Streamer/0.2\r\n" \) t8 o2 x  X; J9 e6 F
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    5 j) ]. C. I2 }3 o1 |1 ]& X
  4.     "Pragma: no-cache\r\n" \9 C* X, h" M3 v; s. v4 I
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    2 B% u* x( p. O& A
  6. #define BOUNDARY "boundarydonotcross"& e, P3 o2 X# @7 R# t4 R
  7.     printf("preparing header\n");+ `/ L+ z( u; b- j4 R
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \2 z+ H6 b# V/ |
  9.             "Access-Control-Allow-Origin: *\r\n" \
    , L+ \0 A4 v9 L( K0 Q
  10.             STD_HEADER \' g# c7 p+ B' e7 S/ w( o1 F) J
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \0 j: _( E( D1 O' D" F. j/ }
  12.             "\r\n" \2 V" Q+ P; C7 l, X. A: z$ |0 r
  13.             "--" BOUNDARY "\r\n");
      m3 f/ U: `3 P7 r) z9 O" P
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
    6 H; K* Y$ _! h. P1 G1 |
  15.     {
    ; c6 c: w3 [  g5 {
  16.         free(frame);
    4 a% Y' u4 B) f3 s
  17.         return;/ C# E6 y. k7 b: g2 C5 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" \/ ~# \9 T, B1 `- s: F
  2.                 "Content-Length: %d\r\n" \
    " k. P: W7 y$ b: W. H) c5 h7 m
  3.                 "X-Timestamp: %d.%06d\r\n" \) d* F: m. R( t, N
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);  h! [" `5 [- ~4 ]) ^& r
  5.         printf("sending intemdiate header\n");
    3 N. G* }$ F& b4 C" ?
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
      r. Y3 k- r/ e4 E8 v
  7.             break;3 A0 c( Q( q7 F* |9 O
  8.         printf("sending frame\n");
    + J' f' Y1 V0 r( y" M- u1 \; z
  9.         if(write(fd, frame, frame_size) < 0)! K( _. f* D) P( ~
  10.             break;/ o' a. Z" w: E' E( R+ w
  11.         printf("sending boundary\n");. ~( W- n, i: s6 R
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");7 V) e) ]5 Y5 k% G4 N1 {
  13.         if(write(fd, buffer, strlen(buffer)) < 0)  P: ~; p7 t3 C' D( \* \/ N/ n: q
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

) h0 m% }  B( e


6 }4 p4 D" w$ X) V1 R: _二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
/ o# Q( b$ F1 W2 A  _  J: ?
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    4 z; {' Z0 H+ b+ E- _5 z+ ^
  2. {/ B$ d- y& K' s) |$ h
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);* @3 S& \( _" C* B3 z( N
  4.     if(*socket_found == (~0))/ X, B7 Z7 h! h8 [( q. R" ]
  5.     {
      e" g3 x4 l: i
  6.         printf("Create udp send socket failed!\n");
    % M, j) q# d+ n1 u5 V8 t9 W8 q
  7.         return -1;
    1 t. X2 L0 W5 E4 R4 v7 W& Q+ j
  8.     }9 X% `8 a: G6 ~8 U  F" K9 _, I
  9.     addr->sin_family = AF_INET;* z5 j( t4 O& @, C8 V, m
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    . Z+ b) j8 y5 a1 N
  11.     addr->sin_port = htons(port);
    0 c8 L* Q- `- x
  12.     memset(addr->sin_zero, 0, 8);
    / {- }& o& s. ?
  13.     return 0;2 c2 h  J9 T  z8 v' a, Y
  14. }
复制代码
2 K' Z) R, }/ }; S( c

3 Z8 q# [! z: R' F而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:: m" W- p; {5 q1 R/ W7 Y
7 D$ v8 `) A3 q5 `

/ k8 \5 F2 z# u: H
  1. while(fend > 0)3 _) |; q1 i) X: \
  2. {% l  v' f9 h* C8 }
  3. memset(picture.data , 0 , sizeof(picture.data));
    . j8 I, D) w" g  q8 Q# P( I2 q: Z+ e
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    ' d' H- F8 j0 I, N4 Q
  5. if(fend >= UDP_FRAME_LEN)
    ; C* F. G5 A! m* _( K4 Q
  6. {) X: o3 x( N7 i' l5 ?
  7. picture.length = UDP_FRAME_LEN;
    & B3 R0 P; j5 W4 @
  8. picture.fin = 0;
    # e$ S1 S  o1 b  G; T
  9. }. {* y+ o6 [7 x+ ^$ p9 A% W& H
  10. else
    , {8 j/ E" Z# I& T' O7 \, i( I
  11. {
    4 A! N4 t2 V5 |1 y- x5 b  I6 ~
  12. picture.length = fend;
    ; K9 l6 k& Y1 r% }
  13. picture.fin = 1;
    * R3 @5 M9 p" f' L% l
  14. }
    ; Q9 K$ k$ J( @3 u9 R
  15. //printf("sendbytes = %d \n",sendbytes);
    4 X8 @8 H+ a! @7 H9 T
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    ! x: P0 Z8 P+ D! C) R: O; M7 f
  17. if(sendbytes == -1)/ U! j5 B; p5 g# |* K+ T
  18. {
    ' c! ~% ^8 A. i1 _& z5 \8 ?
  19. printf("Send Picture Failed!d\n");" d2 I! X! E% W9 _
  20. return -1;
    ; W' P% y: I5 z' [. A
  21. }. \1 A& \$ J$ B6 m' {! P
  22. else
    5 C, ]) u- f6 @. _4 ~7 q5 g& X  k( J2 X% d
  23. {
    % A$ w$ y( i" l2 e) Q' P  v
  24. fend -= UDP_FRAME_LEN;
    : F. d5 X8 K6 S
  25. }
    . J5 ?2 D& h% g2 g
  26. }
复制代码
% n" [2 R7 f5 A9 k, N
) f* Q5 k! V0 }8 ^, p


0 K, d; Z( W' X" f4 {9 K* |" Q# L. k0 e1 U5 f3 m# j
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-1-22 06:18

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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