嵌入式爱好者

查看: 7834|回复: 1

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

[复制链接]

46

主题

53

帖子

295

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
6 K9 R( M3 l" d2 ^. A+ \. ?* `" J4 J! F1 t  Y. |# \; J

1 M# c4 `3 c7 s2 N; j; C

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
% D+ ~1 ^, K) V+ L) ?
0 G. K' \2 y7 a

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

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

2 Z3 R7 E' X( v1 ~1 F5 u2 ]

. m" d3 e! K0 k! O; W

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

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

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

    % w; L$ H" k' [0 q3 s) E

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


0 P& V( F: v) O- u6 K一、HTTP网页服务器+ H: }9 E! Y1 b# a5 ^

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)  i' F$ Z" L. @- @' r& U' N
  2. {  x6 o% M( C- v! H6 R2 [
  3.     struct sockaddr_in servaddr;6 D( K+ S2 N! f, m6 \; J
  4.     socklen_t addrsize = sizeof(struct sockaddr);0 j4 t! l0 T9 @& {
  5.     bzero(&servaddr , sizeof(servaddr));/ [( j5 D# U8 f5 l& x& Q2 G- T
  6.     servaddr.sin_family = AF_INET;
    + j: u4 e1 j) V2 m
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    % S7 K7 {3 J6 }: b: v' u  }
  8.     servaddr.sin_port = htons(port);. i  Q7 e. b/ t4 S
  9.     int ret;
    ) P, O5 _( H$ T) t* k6 l5 w
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    7 ^* U8 H, E/ G
  11.         {
    7 s! s5 x( N. Z, U* s4 D' r4 O
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    4 R% f: F( _" n8 k' ]3 M
  13.             return -1;
    + [2 t$ ?6 j$ I% m! ^! M& L5 e
  14.         }4 d* r) D8 [& E
  15.     int on = 1;8 \9 ?: z; ^, ~4 M5 C, z2 A, K
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    8 a. @* @+ f: l; I. V# w1 R% E
  17.     {
    " v2 X' B% s7 x) m3 S
  18.         printf("setsockopt error\n");  o9 P# B# X4 s0 Q+ m: N; a7 w( @
  19.     }
    4 |, o3 y% ]& u- s8 e3 B. g: G. g
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);: U, n# S& Z5 R* H
  21.     if(ret == -1)% j9 R: A: a' {4 P* e1 f$ K- `
  22.     {
      y0 x+ H4 H' V9 D& @* F* x6 K
  23.             printf("Tcp bind faiLED!\n");: |* |1 j* \2 d- {/ I1 s
  24.             return -1;1 a! g( C+ T% w  f/ z- S: u( h) u+ U' L
  25.     }" C) }$ ~' E7 ^5 Z
  26.     if(listen(*socket_found , 5) == -1)* M, A6 n* X0 o0 |& M  d
  27.     {
    1 C8 r- u) p& w; D( @( V, p! P2 U5 I3 g
  28.             printf("Listen failed!\n");
    # D0 G9 r. c3 n$ ]1 V
  29.             return -1;7 H# d/ n6 \: M" t, G: o
  30.     }( A' h; r" [8 A8 v( `$ ~5 G
  31.     return 0;
    7 F' _7 [1 [0 E5 u
  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);" A( i% Z! w  @3 L7 }. W
  2. void * Thread_TCP_Web_Recv(void *arg)* u- x/ k0 [. S6 l! D3 K9 N
  3. {0 n3 i' w% Z  W9 P/ y) ]0 Q
  4. 。。。/ g% [+ `7 c! D# j4 ~
  5. while(1)8 }; h) H, c$ E2 O" b" N
  6. {
    3 Y; J4 v8 \0 J7 T
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    " H3 ]9 M+ j: h0 O( M% x  F/ t% i
  8.            printf("fd_socket_conn = accept()\n");
    5 I8 a% h( t& c+ z- C$ j$ B
  9.     。。。
    5 V  z# b" N5 f9 I
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);- n' p0 R( R- z6 D
  11. }
    ! }8 ~* I6 o$ s1 F' S8 A9 z/ y0 W
  12. 。。。
    3 a9 ?" F2 Q) y' G! Q) p% p
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);, f5 N9 H5 b  o3 j1 G# c% B
  2.     pic_tmpbuffer = pic.tmpbuffer;( O4 C! E1 Q5 j
  3.     pic.tmpbytesused = buff.bytesused;7 W2 H3 Y' \; o+ b' N8 c& O* s
  4.     pic_tmpbytesused = pic.tmpbytesused;3 x0 `# k1 ]6 W# M
  5.     pthread_cond_broadcast(&pct);) Z5 ^% e: A; _
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    2 a# ^9 S9 g; K1 d! r, M
  2. pthread_cond_t pct;( z. y2 ?4 i# O  I, L
  3. int main(int argc, char* argv[])
    + v9 d! w$ V3 E
  4. {- w" w. L, o8 F8 C6 @+ t
  5. .... P  s( K0 I/ u9 ?
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);4 F4 j. q9 `4 d2 f' H2 l
  7. pthread_mutex_init(&pmt , NULL);, `/ b3 ~$ i2 g9 Y; \8 e
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);$ n; q/ @3 v1 C# A2 Q7 N# z! y
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);8 M1 n% z& O* j* P
  10. ...
    4 n  c4 H/ E" H/ E& C9 d8 g
  11.     while(1)* ]1 h( E$ ]4 X& p" n
  12.     {
    9 P! w, q) N% R- p* l( A
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    ' H+ \- R, i: r5 @7 i
  14. ...
    % n  Q6 Q1 I1 X% U, @$ F. C% m
  15.     }
    % h2 G% @" N5 a5 |' r
  16. ...
    3 t) f2 m$ _) S4 b9 l/ j  c
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">; R. I' w$ T4 y4 X9 Q0 A
  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" \, I" o: M" ^+ w' D% d! k
  2.     "Server: MJPG-Streamer/0.2\r\n" \% L/ s- T4 r) \, p; ~6 m
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \* v6 J& t- P& H; I# p5 s+ r
  4.     "Pragma: no-cache\r\n" \! I) {2 X2 S8 K- m6 G) r+ _; F9 p
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"& G4 u& b  M' l. V3 I( V. G% Z
  6. #define BOUNDARY "boundarydonotcross"
    5 p: p+ B% J  p) \- y* g7 A6 H& o
  7.     printf("preparing header\n");
    2 V" d- ~9 U3 B. w1 Z. Z
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    , h( j& D( J" v4 v! e
  9.             "Access-Control-Allow-Origin: *\r\n" \
    9 W: m  D+ Z/ H0 R( Q1 h
  10.             STD_HEADER \
    : V; A- ^7 M% u* b: E, A
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \; X# V! J4 }/ E- E5 D+ |& H+ ?$ `
  12.             "\r\n" \
    5 p, h1 Y6 h/ f
  13.             "--" BOUNDARY "\r\n");. r6 s! o" j% `, i' v! \3 G6 A" Y
  14.     if(write(fd, buffer, strlen(buffer)) < 0)# o% S! K5 N: ^4 O9 S0 @4 Q
  15.     {
    5 f5 ?: R9 }0 _+ d7 S9 J; S
  16.         free(frame);
    % h) H+ s8 ?* b5 T: ]* w
  17.         return;
    : C5 o, T7 _: q% d: a$ 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" \# q" N2 m/ [. [8 o/ `) v/ w9 q
  2.                 "Content-Length: %d\r\n" \
    % {- u0 @( E& b3 e- [
  3.                 "X-Timestamp: %d.%06d\r\n" \  R! ?: \1 X. m7 m/ K+ Q6 H
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);1 k7 [, ]0 G9 U. z. o1 X' D
  5.         printf("sending intemdiate header\n");
    * L1 B% u6 J6 R7 W3 [% |
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    7 x4 t+ y: l$ k) W- |
  7.             break;
    , F3 I' K* l9 \7 K$ Z8 L+ U
  8.         printf("sending frame\n");
    3 W; e0 r; r8 f2 x% G3 |
  9.         if(write(fd, frame, frame_size) < 0)! Y% I  x8 E9 ]& h$ m
  10.             break;: E% i8 W6 M( [2 \5 q
  11.         printf("sending boundary\n");$ x8 q8 V. d2 f. [8 U9 @6 S' N
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");+ \. ]6 \1 @7 J  Q2 C
  13.         if(write(fd, buffer, strlen(buffer)) < 0), f8 [+ B5 L% H4 J! R
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


" W# G! Z9 Y8 T4 D% x5 e2 S


! C3 {1 c# e4 L: F: Q6 N+ ~# J0 ?4 r二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
+ V  c7 C( W' {$ |5 B" a
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    / H4 ]; M. R' W; ^) u
  2. {7 I- s2 e! K2 N1 K
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    4 Q3 X4 \! ?! W8 k- u* i
  4.     if(*socket_found == (~0))) d0 c: A8 d* f* C1 F* N- f
  5.     {6 |! t! k" o% g8 z
  6.         printf("Create udp send socket failed!\n");" p7 w9 a9 e5 E7 Q
  7.         return -1;5 G# x+ s9 s. V9 @- d% H
  8.     }, m! ]4 m1 E8 u3 O+ G
  9.     addr->sin_family = AF_INET;2 W# a+ W4 E* M2 K" k2 s6 S
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    1 U, i4 l" l4 V5 f
  11.     addr->sin_port = htons(port);+ o+ w; E/ i% e+ S* {& V
  12.     memset(addr->sin_zero, 0, 8);% S; @) I7 x6 B# P+ N  `4 f
  13.     return 0;" b8 [* ~5 {* k6 z' w  r  d
  14. }
复制代码
6 s: B/ [- s: m0 {0 |0 j1 D- i# ^5 x
; x) ^) t( E2 F  x( @9 Z
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
$ x) ?% z, \- X6 b; r4 R
' G% m% f% H7 z3 q8 X1 u, W' B# b  K3 W# X
  1. while(fend > 0)& h+ x: W* D( I# y1 O( A
  2. {
    : m% c  r( o9 g+ S7 K9 H
  3. memset(picture.data , 0 , sizeof(picture.data));
    # I- U5 j2 H; p/ W2 e6 g9 |3 X
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    ! A0 q8 ]" Z  s' `- c7 s( x7 t
  5. if(fend >= UDP_FRAME_LEN)
    * z- ?! Y9 I2 v1 w' S
  6. {0 Y) R. n1 L6 n1 _
  7. picture.length = UDP_FRAME_LEN;7 r7 |) @- h! C' }4 G' B! X
  8. picture.fin = 0;
    ) H# m2 V' @9 \& C! w6 Z) e+ ~
  9. }/ M1 `0 c$ p& g. ]& p
  10. else
    " u. M  H+ H2 }: L
  11. {
    7 z9 [3 d: r) V" _$ h1 D( E
  12. picture.length = fend;
    - ^' D! C" f) a2 j6 x: W% @5 I7 F
  13. picture.fin = 1;4 H0 a; c8 _3 G3 P4 K
  14. }
    ( \5 v/ E$ N8 v! z0 V% v
  15. //printf("sendbytes = %d \n",sendbytes);. z# i1 F* Y$ D- t$ R
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    " Z9 |' q( h( m" p* e
  17. if(sendbytes == -1)
    , f1 P: F- F7 k: Y+ ?4 s' P) @
  18. {, \* k) C( B( |" y9 E7 f
  19. printf("Send Picture Failed!d\n");2 x! c2 v% [2 u2 U5 S& a6 C
  20. return -1;
      O% O. G  h: Q; Y1 D# m
  21. }
    1 |% E$ F9 v4 h+ z
  22. else
    . u! @7 X2 z; Q; u+ q& s3 K) K
  23. {
    ' y- d3 K# a5 {
  24. fend -= UDP_FRAME_LEN;2 g+ c3 u  w( P( Q
  25. }
    9 ^, x4 E! c! C3 z& B1 a5 C
  26. }
复制代码
; Q# {, w6 \( l% ^8 ~3 ^+ T5 L
3 `. p5 T0 ?  B4 E: M


- W5 t$ ~) u) v" w
+ v# C3 k0 ]) r; X8 t) piMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2024-9-8 08:28

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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