嵌入式爱好者

查看: 10783|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 # I. w0 e" N# b; u8 i

$ b1 q! w0 z9 a1 l


, K  \6 R! V# `4 `; ~

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
3 m, K% Q2 I1 q6 W1 G) d6 A6 T7 \5 y, b1 I$ _2 M$ L1 f

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

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

+ F- D5 `; ~4 {# o5 b


5 C' K, Q8 O  x0 ^/ Z! D0 C

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

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

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

    1 i5 ]! [6 k2 R- T! ~+ g, F+ P/ K

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

" \8 ]% O4 C; u
一、HTTP网页服务器
- B* N, ~/ e' j2 Q- L

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)! v, y4 i2 b  Z
  2. {: N* C6 c" x. A* z
  3.     struct sockaddr_in servaddr;
    , J" K. O; R8 Y* @0 I% ?4 Y% {
  4.     socklen_t addrsize = sizeof(struct sockaddr);' K4 ~8 g. ?- a) K) [
  5.     bzero(&servaddr , sizeof(servaddr));2 p5 U8 T7 h& i9 G1 c  T# e
  6.     servaddr.sin_family = AF_INET;) z3 _% b8 U9 i  Q1 b8 |
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    % d: S  `  i- ?' J. P9 T- D! F
  8.     servaddr.sin_port = htons(port);! y+ v5 N3 k) G& U9 f
  9.     int ret;4 d& k+ @0 m. l# ^- v  |4 b
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)( m5 M% M  J9 _0 A4 ~: F% Z+ _
  11.         {
    $ q  v+ z$ z: B" V1 K. h; \
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);. {8 l' D# O* N9 N/ H
  13.             return -1;" B7 \& l3 _5 v" V# j
  14.         }( a. y* j6 J% w  j/ Z/ V
  15.     int on = 1;
      e* U+ J' A$ S9 M; c) b/ p( c
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)- C/ r8 y7 K: v- C2 Q& m  J
  17.     {
      m( G! J, ]% ]" f1 P8 }" R- b
  18.         printf("setsockopt error\n");+ R7 Y3 T/ j8 q4 W1 I
  19.     }
    $ U1 U6 x# F; y- ]
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    ) z, c( C9 g/ S
  21.     if(ret == -1)
    ) e9 p- b5 \& S; o, |
  22.     {
    - P1 m6 Q4 |) c$ h$ _0 G: p/ t
  23.             printf("Tcp bind faiLED!\n");4 p( v& _4 Y, u* j
  24.             return -1;
    ; s+ C( \0 h8 Z" Z6 l0 ]
  25.     }) ~* _6 n  s6 a6 P, x  K
  26.     if(listen(*socket_found , 5) == -1)
    : o% O% I  [% a. K6 N
  27.     {
    3 u: }/ Y3 s0 \( o" \
  28.             printf("Listen failed!\n");
    % s4 X" ~0 B) s& B8 q
  29.             return -1;) H" g9 Z% v/ r% G* A; _2 v
  30.     }2 Y& C( j6 c6 E' T2 {: ]
  31.     return 0;* A7 W+ U* O& ^5 ^2 m+ B
  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);
    3 J* W2 M2 N, I3 r6 M6 k$ k
  2. void * Thread_TCP_Web_Recv(void *arg)/ P$ M; m. v9 D8 S+ |  W( S9 E
  3. {/ i: B* B7 m) |% i, v7 `  h- e
  4. 。。。
    3 _. S; v3 ^' B
  5. while(1)  ?* u+ m& q* _. k) B. ?) ?
  6. {9 ~8 t# Q- J7 E0 O, \! s% R
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    6 T, P9 P7 @! s% U8 \, Z. n
  8.            printf("fd_socket_conn = accept()\n");
    + j0 H7 \& Y5 F1 F- Z# x' |- d" U
  9.     。。。- }  k' d6 v8 d5 U
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);5 F# U. v7 K+ W7 Z
  11. }5 }: M; x" }' H! K5 I0 N
  12. 。。。
      `/ d( }3 K( H  `
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);( q8 p3 l6 O9 a" W5 y3 f# |# l
  2.     pic_tmpbuffer = pic.tmpbuffer;
    3 Y2 N- r# I' S( z) _
  3.     pic.tmpbytesused = buff.bytesused;
    , @( D4 D2 h$ \9 U8 O
  4.     pic_tmpbytesused = pic.tmpbytesused;
    # k4 c; g6 H* w' S
  5.     pthread_cond_broadcast(&pct);
    ; z* U4 l4 Y7 ~5 O
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    ' f8 \# f5 V: E. b# V4 L
  2. pthread_cond_t pct;
    1 L. x' p; k" ]' q# G
  3. int main(int argc, char* argv[])
    4 M2 a& b: W- C0 D: }2 b1 T
  4. {
    * W( E5 ^" z5 f) }$ g% o$ [# x9 C
  5. ...
    8 Q' ^  W' y& g2 b! |% h
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);; Q2 f1 J. c! k# |' Q3 H0 ?
  7. pthread_mutex_init(&pmt , NULL);8 ]$ h1 b$ v- c0 c3 J3 F
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    * Q" E( c! P: I8 M( D" {
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);2 Q/ ^0 L: t; ]' ?, O
  10. ...( N! N( c& x/ k
  11.     while(1)
    6 r7 x. j( y' _. B% d$ _
  12.     {2 m6 n3 f+ i- ]! ^1 U
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);% J* |- B% r" R
  14. ...
    2 S4 A* c4 s8 }3 [9 k! ^
  15.     }
    7 o# X  ~1 q- `2 V; W2 }8 G0 x0 d
  16. ...
    1 G. G2 y3 M7 X" I' O
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    5 s9 D: p9 r9 z& ]- t% `1 @7 D9 \0 w
  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" \, f7 E- P. d5 x6 M" y
  2.     "Server: MJPG-Streamer/0.2\r\n" \8 j! a8 U* E) J: r( A. q1 Z
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    $ n: P9 X& z8 Z) A
  4.     "Pragma: no-cache\r\n" \4 |4 O2 }% {( X5 U) n: s
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    1 ?* q" Q/ f9 c! J$ P
  6. #define BOUNDARY "boundarydonotcross"
    ; b7 b% Y  g; V1 a- v  R# [% z/ _9 I
  7.     printf("preparing header\n");
    + [2 S$ z4 e. L) q: s1 m0 @- J
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    - N7 U5 }& y: c+ D) S4 m$ j1 T# W
  9.             "Access-Control-Allow-Origin: *\r\n" \3 }9 i) e7 s! b* w6 [
  10.             STD_HEADER \
    * M% z- Y/ X# I' n2 T( `8 Z1 ~
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    7 }' k- v7 |  g; r6 v
  12.             "\r\n" \4 I8 C1 l0 P7 ^$ W
  13.             "--" BOUNDARY "\r\n");
    + U) u. A5 M5 K* _, y
  14.     if(write(fd, buffer, strlen(buffer)) < 0): w" Q" v3 q$ ^
  15.     {
    4 G" f* z- c5 Y. s# {$ x% p( Y
  16.         free(frame);
      Y! I2 Y  @" C, C1 _, H- W* N* _
  17.         return;
    3 W0 ^1 r8 U: o6 t
  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 M0 M% Z# m+ p8 z
  2.                 "Content-Length: %d\r\n" \
    7 M) ?! r  o$ @& x# Z1 e: g: e
  3.                 "X-Timestamp: %d.%06d\r\n" \
      c+ @$ ~8 l7 E4 ~! ]
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);" u% F4 d* o- a( g4 m( [
  5.         printf("sending intemdiate header\n");
    6 }: r5 V. N' \& Y3 J9 q
  6.         if(write(fd, buffer, strlen(buffer)) < 0)4 C  F7 S* m9 Z# K
  7.             break;5 V1 C  z9 E2 J: K0 {  k0 O$ t$ o
  8.         printf("sending frame\n");% r0 t- c  S/ S+ a' A5 e% z
  9.         if(write(fd, frame, frame_size) < 0)* g1 F9 a& q& n# o
  10.             break;  Y8 H& o6 t! P; q0 B( C5 ]
  11.         printf("sending boundary\n");9 D1 r( X+ q) I4 k# C
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    1 b: a* O$ t- W3 Q! a% j
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    ( }7 T  R/ u, p: P! k7 E" R( x
  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 w9 U9 L9 R- }3 y5 W; ^  a$ A


* k, S* {4 W/ B7 H3 G( t二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:2 P6 L) t5 A( X2 Z) \
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)9 s* Q  ?* b/ y" _+ [5 B" `) e1 U
  2. {
    , A' r5 a1 I, H  Y
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);# k: D  g* J9 x6 G8 A
  4.     if(*socket_found == (~0))
      Q1 T* ^7 S2 H$ }7 I3 k
  5.     {
    ) Z* s* f  o" u  ~5 A4 A9 `
  6.         printf("Create udp send socket failed!\n");
    * L$ P4 p1 F$ @8 ?
  7.         return -1;
    : b$ r; l! ?" k
  8.     }
    ' \) I+ e  |8 c: u8 r- a  s& a& Y
  9.     addr->sin_family = AF_INET;
    % [3 b& U: @  a- D# ~
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    $ o# Q; I7 W/ J, c
  11.     addr->sin_port = htons(port);( B) ^  o( u( E1 C2 n  U# P3 g
  12.     memset(addr->sin_zero, 0, 8);8 [9 o9 f8 m6 c  O# u" x7 b: m
  13.     return 0;
    ! a5 U- _* h5 l, d! y
  14. }
复制代码
  i, z) [4 O3 k# c
* h+ O0 E: c8 y; F5 C& ^
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
! i& p& A6 P/ l, t8 l  g
5 _$ ?( E3 G8 g: g- K3 ?+ V: i# N0 c0 e: I( ^2 J4 q3 W
  1. while(fend > 0)
    3 L0 Y& n$ l# l5 _
  2. {4 E0 y# }/ u) g+ @2 [: D" E1 |
  3. memset(picture.data , 0 , sizeof(picture.data));+ m! I8 S: G/ n2 f" f1 i
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    0 T; N+ _+ y6 ?
  5. if(fend >= UDP_FRAME_LEN)
    , f4 C+ H0 J3 F) i. }" ^7 W
  6. {
    - C# t: V% y3 V
  7. picture.length = UDP_FRAME_LEN;
    ! ^, J* f. r0 r7 j' j+ T) E
  8. picture.fin = 0;: c( r5 Y! t( K5 p
  9. }4 [# {* o3 q' z7 a9 y4 P( Z/ f
  10. else: }# b3 J0 o) G! P& a6 Y/ A
  11. {( F  N. Q& [% a' l6 y
  12. picture.length = fend;9 K$ |/ x8 M) l7 u, K% F
  13. picture.fin = 1;
    7 L4 D. T" U; M$ E. v' g
  14. }
    : h+ q* Y2 S8 ~- [/ J
  15. //printf("sendbytes = %d \n",sendbytes);
    0 _* @6 `& W/ s$ X- }
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    " e6 Z: U# }: N. w/ S# I
  17. if(sendbytes == -1)
    6 Y1 T. t1 f4 l( i7 I
  18. {: G& I! Y9 c2 ]
  19. printf("Send Picture Failed!d\n");
    . v: H( R) D' k% o5 C$ y3 ~
  20. return -1;
    " W, w9 A0 k( a1 l7 g
  21. }
    ' Z- @1 o0 D) y2 L( }7 X
  22. else3 ^4 m: P( g0 P2 u4 J! S
  23. {
    7 g# i/ d$ T6 U, p" Y
  24. fend -= UDP_FRAME_LEN;' ~/ Z% P1 O! Q! j. {3 E
  25. }/ @& x! o, x! F" P: W
  26. }
复制代码
& r  a: T! j& G6 ^1 B. @
' a6 y% [! g+ b: z- e& P& t% R


9 n! I, U) k$ q, B7 c4 r' V' c7 L+ v1 ^* h+ ?; n) p9 S' N; T7 x  y
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-1-16 12:19

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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