嵌入式爱好者

查看: 10868|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 6 U7 }  i' O# }- b* O; p0 i1 l3 G

. N6 F" U5 W' O  Z


1 `: Q; S; B7 N$ T5 F+ j5 Y( @; ?

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
3 r* q3 T7 O* R' T2 W6 x3 l: H5 ^* I. V; y# r) u4 `8 Y8 ^

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

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

" _" E) K8 B6 j' f

* E3 o7 s" m3 L% _3 p# N+ p  J

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

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

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

    * W/ l8 a4 p% O2 Z3 d

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

  Y; b/ \, z# s+ B- y' e% Q) ?0 R
一、HTTP网页服务器4 ~+ }- F; b- A

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)  p6 b  _) v# A8 x1 P
  2. {
    # H4 a5 f# Z' ?" d( F! ?$ N  H
  3.     struct sockaddr_in servaddr;
    9 F* o3 J! Q3 n$ I9 B
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    5 l# S; t+ @: X
  5.     bzero(&servaddr , sizeof(servaddr));( A' [7 i1 `8 O1 v; o; \' u
  6.     servaddr.sin_family = AF_INET;
    * v$ S  X, q  Q. V8 T
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);! q/ I+ [) m# V/ s3 f# [
  8.     servaddr.sin_port = htons(port);* e" S, A. d7 ]# Z3 @
  9.     int ret;
      E, D5 x5 ~# O+ ~1 d2 S
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    3 T" K6 t8 L8 b, d$ r1 ]
  11.         {7 b! D* N/ a; A1 u1 `" r4 m3 E
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    6 e7 B& O, W7 Z8 {1 a) B: u/ ]
  13.             return -1;
    4 j9 P, h6 ]! w
  14.         }
    7 s( a# K# e5 r) d/ h% M
  15.     int on = 1;4 p2 j0 [. {1 [) [( j. H& F% @
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)+ G' S: e$ S1 a- _
  17.     {% S2 e9 H3 w1 p9 Q+ A% h3 v/ h
  18.         printf("setsockopt error\n");
    & N' X7 D7 S4 H+ }: o. y
  19.     }6 C3 b* e, ]. `, K; c5 H
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);; Z6 u$ F+ u. W% r( h6 U* Y
  21.     if(ret == -1)% h+ @2 _: F0 |; @. s# L
  22.     {
    ; k8 k: s6 j5 e: t5 x5 f
  23.             printf("Tcp bind faiLED!\n");' ~; ~& k, S/ Y7 Z5 F, F' r
  24.             return -1;3 l+ y- p7 d8 J2 C7 [, z# o) K
  25.     }
    4 V! c# M% C6 \8 S) ?2 \9 u; h
  26.     if(listen(*socket_found , 5) == -1)
    / B9 y8 D6 e; }4 k
  27.     {
    2 P1 Z: C4 E6 F* t; a
  28.             printf("Listen failed!\n");
    + G: p8 a3 d5 S6 U/ E5 W) j
  29.             return -1;
    ! j; r3 x' J0 |5 w7 r  y
  30.     }
    # P& t: l, }% k  I0 D; j4 ]
  31.     return 0;6 K' J  ^# y. W3 P7 R. ?, j" D% m) W
  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);
    5 e5 I! a+ M  |/ O' t! Y
  2. void * Thread_TCP_Web_Recv(void *arg)3 _& q: L) ^5 g
  3. {
    ( u. P( q' ?6 `# B- p6 ~8 o# N
  4. 。。。- ^/ h" g( I: h  o
  5. while(1)! |: [& R2 K% V8 K& O
  6. {5 _' Q8 Q" E/ X& e
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    3 ]1 K+ b6 j: P
  8.            printf("fd_socket_conn = accept()\n");
    0 S- J( g% R9 a  `3 `
  9.     。。。
    ' b3 v" V8 ?1 S9 N7 ?: L
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    ; p2 ], n1 `% m4 G' h
  11. }
      ]* b/ G* C3 G6 ?) U* U1 r/ {
  12. 。。。
    . \3 ^% x% C7 D3 D; T: U
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    # c$ f9 ~  A3 m- G; B% r' p4 F
  2.     pic_tmpbuffer = pic.tmpbuffer;
    6 e6 ?: C: z6 W% `: H
  3.     pic.tmpbytesused = buff.bytesused;  v! X+ r$ K! K" Q0 B
  4.     pic_tmpbytesused = pic.tmpbytesused;
    ) X9 w1 ?7 M; K5 g
  5.     pthread_cond_broadcast(&pct);! }' N/ t+ O6 R% j1 k, Z$ e/ ?
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;+ p/ Q" E4 `- a% [) u. [
  2. pthread_cond_t pct;7 l+ a0 d" e$ J8 y, {5 U& i
  3. int main(int argc, char* argv[])
    - d, n8 t8 Z* c0 j2 G  D: v
  4. {6 F. f# O4 k3 e& g9 W: ^8 ~
  5. .../ J" e) U9 L! O& W/ w& p
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    & v  O8 n8 s* L9 `2 v* Y
  7. pthread_mutex_init(&pmt , NULL);
    6 S: R! r' h" w
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);. C& {( `: Z" N! R* z" z
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);% N+ t& X8 H6 v" A' d8 c
  10. ...; A( v: e: D  @( |  K9 h# S
  11.     while(1)
    ( l- M+ Y8 [% [0 s( J: K
  12.     {
    . T) u8 d/ P4 m: j" r: B
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    ! G  m' O& ?" t, f
  14. ...6 O5 r  l' u4 ^! m( ~
  15.     }
    9 l) ]9 }! ^4 h/ y
  16. ...$ e* Q+ [% g' Y" K- g$ }$ S
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    , n! n% y. u; G1 G3 M' h2 t# `
  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" \
    / x1 M7 R7 M( a+ F  Z( N3 M# ?
  2.     "Server: MJPG-Streamer/0.2\r\n" \* Z) q# ]* n+ s
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    - R$ I: p! ?8 w; h# \2 h
  4.     "Pragma: no-cache\r\n" \
    , T: a, s3 y9 Z4 B: |' `1 C
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"" {6 W1 m& x; S8 U) l( w
  6. #define BOUNDARY "boundarydonotcross"0 X( w/ ^3 `/ v7 k7 Y
  7.     printf("preparing header\n");2 K* c3 E: I6 m+ \+ F
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \8 X  o2 H7 h1 \, s: @: K0 s5 k/ }
  9.             "Access-Control-Allow-Origin: *\r\n" \
    $ j- Z# M" F- L3 A: c* o1 V6 W
  10.             STD_HEADER \& U# U' s- E% m# u, y
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \$ `& y! q, J& j! Z/ G8 q& X/ d. H- o
  12.             "\r\n" \
    5 F+ Y. ^* c7 ^; U3 ?: t( b
  13.             "--" BOUNDARY "\r\n");# E6 h5 S6 j7 S( B1 j+ l* ]
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
    ! C9 h& Z; X! ]0 n7 A! `! K
  15.     {
    7 B5 W+ e& \% z5 [4 }2 g
  16.         free(frame);
    2 R. O( u' K" Z: {. K! e1 L
  17.         return;( V$ `9 [$ ]0 t% r( e
  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" \
    ! N6 M7 m6 g5 Z4 l# a* T( r+ k
  2.                 "Content-Length: %d\r\n" \6 E( s7 A4 t/ `& k/ d2 ~/ u. r
  3.                 "X-Timestamp: %d.%06d\r\n" \
    . ?# l9 q1 G# P8 Q3 g" R
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);7 }& S* E% u3 g) W& B& i
  5.         printf("sending intemdiate header\n");
    , w& M$ X$ V7 q
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    8 T* w& q- O% j4 \' {- O& {
  7.             break;- T/ X5 e3 B9 k% m, t  j4 k! p! f
  8.         printf("sending frame\n");, A; y1 E' a" y0 o( i6 g
  9.         if(write(fd, frame, frame_size) < 0)  F9 n$ ~1 X' B" j
  10.             break;
    $ N" g  N1 x9 O, }" n# t
  11.         printf("sending boundary\n");$ `3 @- W% r2 ]( K, M  u* d0 H
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    : [/ u* `0 F% N8 H% m
  13.         if(write(fd, buffer, strlen(buffer)) < 0). Z* [$ Q! R; a3 K  E6 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指令,从客户端使用者角度来看的效果就是网页一直在等待。

8 R! o( V( s$ u. u- i9 T  b6 @% D


  p! }+ L2 [0 V4 K二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
" V! M+ w6 V$ M+ P
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    . L3 }& ~% e' S
  2. {
    + y3 Q; n. x* l, V. Z$ O# s
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);6 @7 [( I. _& D; u0 h7 A
  4.     if(*socket_found == (~0))2 D3 }3 n% D  T; M% Z) \& g
  5.     {
    * |( n7 F  l1 b- ^& \2 a
  6.         printf("Create udp send socket failed!\n");
    3 ^2 |8 l3 ^. J/ L; J
  7.         return -1;+ p+ h: j& \5 `! j6 [
  8.     }
    2 s' b6 a# A+ c: P' Q
  9.     addr->sin_family = AF_INET;/ [2 Q, P* r1 P4 T/ E
  10.     addr->sin_addr.s_addr = inet_addr(ip);: K- K+ t5 ~- Z: O" k- K
  11.     addr->sin_port = htons(port);
    & l  I0 b) |6 \  r% L0 B" _3 A* }0 l
  12.     memset(addr->sin_zero, 0, 8);
    1 L7 m: ]. b+ y
  13.     return 0;
    0 X: p+ D) Q) X5 Y% B  J8 O
  14. }
复制代码
4 ?, j! }% p* c; |& E% G
6 A8 R/ I3 b9 U
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
! Z( |+ ]0 l# i2 z: o
+ `2 n( _; {* z, v+ f. x1 M' `% M# m9 v& Z& b0 d! P
  1. while(fend > 0)7 h' M; q1 Y- Q) F3 c2 i3 F
  2. {$ D, ?, |" }  [5 d7 s0 g0 I& D% [
  3. memset(picture.data , 0 , sizeof(picture.data));
      u  Z! B2 C  x3 U' I5 E0 N* ^
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);+ e7 \0 h6 ~4 P, W% j
  5. if(fend >= UDP_FRAME_LEN)4 b, s, S) n' n" r7 z+ Y! b
  6. {
    9 c+ X) A8 n$ r% {
  7. picture.length = UDP_FRAME_LEN;
    # z' v- L0 \6 \: T+ f2 B
  8. picture.fin = 0;
    ) H/ T+ O2 k/ F, g7 F, U2 U
  9. }& N* o: @* ]1 G& H) j- i
  10. else
    ' Z" H8 Z3 Z2 F) v+ @
  11. {+ S- o$ Q# a" G; D+ r  [
  12. picture.length = fend;
    5 B3 g+ Y/ p! D* Q% X
  13. picture.fin = 1;& o5 @/ ?) W7 A
  14. }, H" l! \- Q2 O$ \0 W1 Y
  15. //printf("sendbytes = %d \n",sendbytes);
    * t4 _( a2 b" i2 Y
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    9 A9 q' d7 l3 ~+ c
  17. if(sendbytes == -1)/ q" Y, e" @- a3 `
  18. {& j. W1 @2 V5 o$ g- D4 i( X
  19. printf("Send Picture Failed!d\n");
    ) g5 o5 I4 l# ^" B  {
  20. return -1;
    . d( c$ G1 f' M
  21. }
    ' e; F# c% _- Y  \% X
  22. else
    9 k9 v% n2 l+ f# ~+ O5 c6 \
  23. {
    ) g  A% I& v* m9 c
  24. fend -= UDP_FRAME_LEN;& q; W8 }2 L2 [$ J: Y" B% y
  25. }
    ) V9 L, i$ x- L
  26. }
复制代码

* h/ c. ^! H+ }  m3 [+ _, e: G% D  c. [$ @

. i) E2 M3 s8 O; Z9 `$ w7 |8 I

! m, P* l2 t  E! t: M% fiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-1-23 01:57

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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