嵌入式爱好者

查看: 10273|回复: 1

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

[复制链接]

46

主题

53

帖子

296

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
: H; A) v8 M4 g3 A* e/ ~! A/ ~0 m2 {% g; I& w


6 h+ B5 Y' k4 y2 P% v& V  I- T/ y

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
  s0 f. q- p8 B/ m3 m) Q' W  b! l+ B

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

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


% d4 G& t7 e# q  }$ a4 Y5 e" m

: c0 V$ \4 U" B1 S- E) L6 Z

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

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

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


    : X/ S7 I5 n0 z

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


" m! E  v( ~% G: }* j- I) [# E, W一、HTTP网页服务器
% H2 A& p, M+ B( l4 R

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)9 `* Q% X- L- p8 ]; d: E
  2. {6 w1 Z# [. B; M$ R
  3.     struct sockaddr_in servaddr;" i0 q& {; E* T% V3 {
  4.     socklen_t addrsize = sizeof(struct sockaddr);% i" e; Z: b& q7 q
  5.     bzero(&servaddr , sizeof(servaddr));& f9 b- G* ^8 W* v9 P: M9 h3 U7 B
  6.     servaddr.sin_family = AF_INET;+ z- @; f) S9 x+ u- {
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);/ }' E9 [4 k" E$ ]2 U4 r
  8.     servaddr.sin_port = htons(port);
    5 V- Y6 I+ O* E$ ~. z- U9 @7 o8 p
  9.     int ret;
    ! C) l$ f1 Y4 O# [! t' I& L+ n
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    5 y; p( ^( C! Q) ]) [8 v
  11.         {7 ?5 R' F3 |  g! Q7 A1 ^1 m
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);; q( K. j; }2 L# D
  13.             return -1;3 u$ l% d  x. q
  14.         }
    * M. c! S4 |8 T& h, r
  15.     int on = 1;
    " o. q" D- T' _4 k% _( w" \
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)% m% e# @0 t$ w6 P* }/ O
  17.     {( r8 p& M& u% {+ f" J1 L
  18.         printf("setsockopt error\n");; F9 B  u' _6 h& o( [
  19.     }
      [' b/ H4 b& O# o
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    . }  s( k4 q. |7 P: \1 o% Z1 B1 z: `* h
  21.     if(ret == -1)8 t$ _  H- I5 g0 ?9 r
  22.     {
    % `. n) U* \6 c- n3 g* f  z. S6 N
  23.             printf("Tcp bind faiLED!\n");  T, y8 }) o" r
  24.             return -1;, O6 q1 H' W* B  p4 A
  25.     }  R( m/ _* y* g8 ~0 V
  26.     if(listen(*socket_found , 5) == -1)
    7 E1 E0 l3 ~( w* q4 X- C5 I
  27.     {
    9 L7 x/ o1 p4 }" H" S% j
  28.             printf("Listen failed!\n");) N! Q/ k' r& @3 x" M
  29.             return -1;
    8 v$ `; O8 {* W% a& b
  30.     }
    ; y" U/ ^! x! {4 M& c, b
  31.     return 0;
    * G1 d0 }; F' O0 ], z7 y
  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);$ r6 ^/ T+ w+ _
  2. void * Thread_TCP_Web_Recv(void *arg)
    . F! L2 O1 N. M- C. ^
  3. {' L# M; _- s! a; h" a; u' E7 n- u: Z8 y
  4. 。。。
    2 e0 a5 B; O* n
  5. while(1)
    4 h+ I: {# |' V. q2 ?0 N5 C
  6. {8 t( ?: b. O) P/ ~$ B
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);( f! c( K, m( _% Q+ p
  8.            printf("fd_socket_conn = accept()\n");) p9 g6 D5 Q; R0 t
  9.     。。。3 [8 B7 C5 n0 |6 j% t% ?1 V; d
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    ; }$ n! C( [3 R7 d: u/ H2 F
  11. }) _; F7 X" V. Y2 r7 K: r5 {
  12. 。。。/ ]6 P9 O$ \: Z2 _& I
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);6 H0 j! W8 p  r1 \6 R6 \. u2 j
  2.     pic_tmpbuffer = pic.tmpbuffer;
      [5 `$ `. Q% x# I6 t6 S. m
  3.     pic.tmpbytesused = buff.bytesused;3 |7 v* |) u- o# V- A$ h4 N
  4.     pic_tmpbytesused = pic.tmpbytesused;9 P% Z% D. p5 Q$ n5 W) b' K
  5.     pthread_cond_broadcast(&pct);" L# d3 g' N# E
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    % I* y3 s8 {- A% g# C( N( _
  2. pthread_cond_t pct;
    1 ?, j8 l$ R) {& E
  3. int main(int argc, char* argv[])
    5 ]) x9 D. C) x* G  J" f( C% u
  4. {
    . a2 _" V; f1 L! x* u7 d, S
  5. ...) N7 i3 ^& q' g& t; j9 e. n& s
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    4 C0 }: y. d) Z
  7. pthread_mutex_init(&pmt , NULL);
    3 W5 f' k- R5 n- [1 V. g7 e: e
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    $ B: o/ \7 k* L: H1 N  X: b
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    1 H8 T( w5 W' i, {
  10. ...( {; x, Z% y% w7 ]  X, d- r
  11.     while(1)+ m+ S1 E# Y) J
  12.     {$ N( f% q0 |% E: f, }( p
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);4 y# _9 a6 B" T9 Z( L* f
  14. ...
    1 {' F1 F$ `% f7 M
  15.     }. {. E# U( z/ l
  16. ..." ~$ k9 _! ^/ j5 A' p. B" w5 o
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    , T7 V) D7 ?8 S+ q
  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" \* g6 z- w3 s+ Q( z
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    ; P2 ^1 x- {; B% J( B* Z, p
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    0 H: v. I3 h9 s
  4.     "Pragma: no-cache\r\n" \
    , Y) B  k& K- ^
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    : i( I6 O8 f) D
  6. #define BOUNDARY "boundarydonotcross"
    4 \6 Y6 Q  M: D
  7.     printf("preparing header\n");
    ! W9 G, N7 {# [6 u) [8 s; D
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \# ^4 Q6 l" H; {: ^' I* I) Z
  9.             "Access-Control-Allow-Origin: *\r\n" \8 b7 S/ H  k2 ^3 v- p0 N
  10.             STD_HEADER \9 M" n' @0 z- a1 P3 M# ?
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \7 _4 F/ z/ P" p0 T: w1 z' b! q
  12.             "\r\n" \
    # W, i0 _$ x+ [% w% F0 r( t
  13.             "--" BOUNDARY "\r\n");
    + }- D% l1 a$ |# }% Y
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
    $ N% s9 X, _' ^/ E% P+ b8 a+ O6 X2 l8 r
  15.     {
      D% e, B& ~8 A! Z9 p$ n" K  `/ X- B' r
  16.         free(frame);
    * K# w6 i7 w$ |, T# t0 v2 N9 G
  17.         return;
    3 F4 n4 o8 M* G' e( j: E. _+ 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" \
    ! K* X) @; y: k
  2.                 "Content-Length: %d\r\n" \
    + j, I7 [! l$ Y3 {. f
  3.                 "X-Timestamp: %d.%06d\r\n" \
    * U) M2 Y! C3 e) }, ?2 [
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);5 x% }  f" J# Q# b5 `% _/ K4 h
  5.         printf("sending intemdiate header\n");
    ( p- |6 T0 U7 r) \/ V8 R. S) g0 W
  6.         if(write(fd, buffer, strlen(buffer)) < 0)( b7 j/ Y6 C# b/ ?. ^3 ?( ^
  7.             break;4 `& \" s: K) V3 O
  8.         printf("sending frame\n");
    * H# x* D) E/ Q7 r9 `
  9.         if(write(fd, frame, frame_size) < 0)
    , ~# k' K, R' a
  10.             break;
    / w5 ^* R  h) _& z8 ~; Z
  11.         printf("sending boundary\n");! u6 S3 |. b5 I: ~4 e6 I, J1 t0 e
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");2 G7 X, v0 Y7 u+ O% O3 L
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    % m: b9 g$ g0 j% r6 S/ _
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

7 g" p* f/ |9 P3 J; N: H) G. {


- a7 r7 ]* N, K7 A: h$ m! e0 C& ]) X* b二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:* x) S0 f# ?7 B4 }2 s! L% _
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)- q+ \. {$ o; D4 _! ~2 O
  2. {; u8 C  ^, R# q* w
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);& q# X$ Z' p+ K, R( T8 I; N- v; M
  4.     if(*socket_found == (~0))! c: R' ]. D: g( R
  5.     {; o$ z- f2 M$ c  z1 B
  6.         printf("Create udp send socket failed!\n");
    & t% `  J/ H; a8 m- G! y
  7.         return -1;$ T# x3 b8 i3 j7 c& T! u
  8.     }
    3 l# `0 B) Y) a$ O" `
  9.     addr->sin_family = AF_INET;
    / J: ^' a  m% o/ _6 `7 R6 T* t5 F
  10.     addr->sin_addr.s_addr = inet_addr(ip);/ D/ G( t/ n" X( Q/ e5 \
  11.     addr->sin_port = htons(port);
    . A% W8 r  }0 m% c
  12.     memset(addr->sin_zero, 0, 8);  G+ y  d5 x6 ?0 p9 \: F
  13.     return 0;/ K! B! v0 ]( I! E9 A
  14. }
复制代码
3 B, G9 g: A, R: L1 s5 Q) ~! h

: ^  i* M4 D- I7 F  \2 F9 N, ^而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
7 q2 T0 n* Q* j$ m: W: _! h$ P" X9 f& P5 N6 v6 X8 d/ w. D" S! ~
1 b) w! ?+ q( R5 Q* [$ _" L3 Z- [* m
  1. while(fend > 0)1 s+ j! j* ?( ^& ?2 r9 ]
  2. {
    ' D5 \4 |& Z& B+ T9 K% [
  3. memset(picture.data , 0 , sizeof(picture.data));
    ! V& E+ _" R% ~2 e$ f
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);. P7 V# l5 \  [$ i; |: E, Q
  5. if(fend >= UDP_FRAME_LEN)
    ! p. w- H# k' R* p, ^) V# d5 Q
  6. {
    8 J* |  v& a4 p/ P: w
  7. picture.length = UDP_FRAME_LEN;
    + c9 N7 P$ `( W2 i8 G
  8. picture.fin = 0;, N8 l6 X# a7 `6 f: a7 A4 q" T( z, }: {
  9. }
    $ C! z, d( `5 B  q* n/ |% C& @9 {" R
  10. else
    ; s9 X0 b: [5 E. C0 ^4 U
  11. {2 r$ `) x$ Y2 J. p$ u" `" U5 n  A' k
  12. picture.length = fend;9 |( L+ A) T' L+ |7 |. E# F
  13. picture.fin = 1;! g* L" X% H, w4 D6 j" x
  14. }2 D- p/ {' H* a' ?' v3 S
  15. //printf("sendbytes = %d \n",sendbytes);
    + O- v5 [$ x+ q6 C
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);0 m3 z2 y. a( B
  17. if(sendbytes == -1)
    ; |. u' Q7 d- |4 \/ R- r* E. P* n
  18. {
    ( m. b2 B# W6 v$ G  }
  19. printf("Send Picture Failed!d\n");
    . ?  m/ @/ b9 n/ \( l- {) v- G3 |5 j
  20. return -1;  W; K! {4 v* e" B) ^; i
  21. }" W: m8 H4 O  F2 ~& K* g- y* Q5 P
  22. else1 A2 P5 L* H2 u7 w+ r( N' L
  23. {
    4 z) I; y( M1 \& i7 z- J4 D
  24. fend -= UDP_FRAME_LEN;
    4 d* k$ `6 l2 w* ?
  25. }
    - t# F( |: @( K: V" d1 }3 c
  26. }
复制代码
) b* A! P8 a0 ]+ m, u% t
: L( I$ g9 Q+ o* r; c+ _9 n* x


9 v" G: h; i; N: F8 b2 P2 z' |& x& b0 i" z( I
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-11-25 22:04

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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