嵌入式爱好者

查看: 10533|回复: 1

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

[复制链接]

46

主题

53

帖子

296

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
! {( z0 u$ \& Q" {5 t
$ s+ ^% Z$ W; A+ f$ h: ?) W


( B$ L! @- l3 h3 q0 s/ N

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
1 i' K* \( E) [6 m
1 e" W1 r3 ?/ d% v+ O, v3 U2 y

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

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


1 `- f/ `! A$ R! a# m; Q- W! }" O

+ ^0 q0 n( \/ n5 {

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

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

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


    0 J( y% U5 e' C$ W, `& S

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

! r- g9 Q% R' H7 J6 F
一、HTTP网页服务器8 [3 W: {: G4 u# k

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    & x. O! p* B* _
  2. {
    2 v: V! q8 I0 N1 l% c4 f* W& U& S
  3.     struct sockaddr_in servaddr;
    ( i5 B1 F! u( B) }/ P& L" b
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    ! N; `. @! @% i
  5.     bzero(&servaddr , sizeof(servaddr));
    9 s9 y3 G5 v) J( r8 I# G
  6.     servaddr.sin_family = AF_INET;
    1 g$ l7 S" {  Y( l5 s
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    0 D2 r" g) T; e
  8.     servaddr.sin_port = htons(port);  B7 C% E2 d' m5 ^  Q! t* f5 u
  9.     int ret;
    7 ~  R! b% A4 D
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)8 x( y+ A7 u7 ?" z5 y0 ?& ]
  11.         {- @8 L8 k; b/ r
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    ' `7 n) P1 d* E
  13.             return -1;; V# D. L) ?8 P6 `
  14.         }
    ' X* f2 E3 {1 z! @: F7 J
  15.     int on = 1;$ R4 @1 A. }, k
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)( V1 t4 o+ q/ Y. f3 M
  17.     {3 X( c6 J! [! u$ l* B* ?
  18.         printf("setsockopt error\n");/ b* I; n+ ^$ v- ~; J) c" r
  19.     }. {; g% A' t3 Y& p
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);9 e, Y* J) @* ^/ x/ O
  21.     if(ret == -1)8 ?5 b9 b& \6 S' H
  22.     {8 z. u- _7 M# v
  23.             printf("Tcp bind faiLED!\n");
    * f% N0 {& E3 H5 S, w
  24.             return -1;
    0 o, D6 D/ U7 p% O
  25.     }
    ' t3 Z1 t* O  W) S
  26.     if(listen(*socket_found , 5) == -1)
    ) D# v4 a  u2 B" t& n  x# T8 Z
  27.     {9 J: O& O. y( v5 y
  28.             printf("Listen failed!\n");5 W7 B$ \6 z: F* n4 q% ?
  29.             return -1;
      D+ ]0 R; S4 M9 [! a
  30.     }
    2 b+ f6 S6 O$ Y- ]; W
  31.     return 0;4 L& \" J$ ]7 u! G; ]
  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);
    * m' V+ D; Q* [* w/ c. p
  2. void * Thread_TCP_Web_Recv(void *arg)
    - a( w6 S; Y. O2 k$ ?
  3. {  }, Q6 k) r4 c: O' q, f
  4. 。。。
    4 X9 t* p5 s4 @$ T
  5. while(1)8 C4 }0 m  z5 Z2 w; g1 }
  6. {
    # ]) ?: K8 K1 F9 h3 {8 q
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    - Y. |7 _$ b8 l* N7 v4 n: h
  8.            printf("fd_socket_conn = accept()\n");
    ) Q( g4 F% q0 a
  9.     。。。- T8 X6 l" ~1 ]. r& O
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    2 i/ R. E8 l1 I
  11. }& g0 d! L" Z  H7 s: d6 U
  12. 。。。9 l8 i% B; {0 P7 G! T
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    7 z  a$ I1 z9 z, Q1 U
  2.     pic_tmpbuffer = pic.tmpbuffer;  a) v2 B4 M$ |* N: ]) n2 ^$ v
  3.     pic.tmpbytesused = buff.bytesused;
    : c) C* `2 j  T1 i$ Q
  4.     pic_tmpbytesused = pic.tmpbytesused;  a+ n2 a7 f; K" v$ k
  5.     pthread_cond_broadcast(&pct);
    3 h' u3 ^# S  q% U0 M
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    " ^3 E$ R0 d; K# a
  2. pthread_cond_t pct;
    & E6 I- w& R- a9 e6 A
  3. int main(int argc, char* argv[])
    5 x/ O+ |* L4 g& X7 x2 c
  4. {( u0 h, g+ \! W/ q3 u
  5. ...
    0 c! A8 K" T* d- M* V5 d
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);2 C3 N3 C" B9 h, D# X, O
  7. pthread_mutex_init(&pmt , NULL);$ s7 B8 U/ O2 I. o
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);0 M4 H9 E. w" }. h. {) F
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    / h$ F0 V3 h* J  x2 W* H
  10. ...
    ; F. H: ]  @% N! e4 w
  11.     while(1)( q: E. G8 v9 K. T6 n
  12.     {
    # S3 l! t; g! {4 _4 l
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);- J- T8 i0 |  [/ x! ]
  14. ...9 R( b3 |9 y8 I
  15.     }' ~+ \' ^, D  p" \% b
  16. ...2 e) E+ g% ?+ Y% o3 q' L
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    2 B6 u% c+ E) ^4 `
  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" \
    9 G5 \& m# A0 w$ A
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    + J/ J2 O/ W" S" E
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    $ n6 E8 ]$ o7 E2 |' ?7 n" r
  4.     "Pragma: no-cache\r\n" \
    # S* g& k( l" j) i) H, G# F
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"- X1 S9 ^& @: e$ V( ~) ~
  6. #define BOUNDARY "boundarydonotcross"
    ( M4 U6 ?  q( ]3 j9 |& p
  7.     printf("preparing header\n");( K* D9 d( a% p% m$ V- [. T
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    $ S2 p0 V8 j8 l( V8 L( O0 D% w4 U
  9.             "Access-Control-Allow-Origin: *\r\n" \
    9 W3 \* G# j- O6 A7 g
  10.             STD_HEADER \5 V) s9 j# q& ?/ H- m* f; q; q
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \! b( X1 e( T0 ?$ {) @! J$ \
  12.             "\r\n" \: q* x2 S. s6 ~2 r5 ~4 V' ]
  13.             "--" BOUNDARY "\r\n");2 h+ K2 s" h  b% _' O$ _
  14.     if(write(fd, buffer, strlen(buffer)) < 0)6 W) M' l' e: N
  15.     {8 |5 F& u3 B; @3 q
  16.         free(frame);
    7 B" N9 Q- @2 N/ J
  17.         return;; z* K: U9 d, p
  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" \
    : u0 Y: {" A0 T- C0 @; E* M
  2.                 "Content-Length: %d\r\n" \; B0 r* H" z' w1 V, b
  3.                 "X-Timestamp: %d.%06d\r\n" \
    * D5 Z! H+ [7 |7 G
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);: m2 ^: r$ d# P8 z7 e% a" f8 b
  5.         printf("sending intemdiate header\n");
    * W8 h$ ?* j4 u  A0 M
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    : T% b# b$ Z# X9 ~2 o: J* s- x9 {
  7.             break;
    6 t" m" i1 s- v9 P( K. n4 I
  8.         printf("sending frame\n");; t0 o' P' {2 Q! J* o
  9.         if(write(fd, frame, frame_size) < 0). P+ [. f; c: G" T
  10.             break;2 e2 F6 C1 Z9 L6 Y! `7 I1 m4 g
  11.         printf("sending boundary\n");
    2 z" u! f* g" g: [3 H
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    / v+ |% T8 a# D  E& \& D# a( p
  13.         if(write(fd, buffer, strlen(buffer)) < 0)+ e$ F' P1 Y/ Y* w7 z3 ?0 k9 w
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


- H( s2 ?3 W" R9 ?# o) `


- X9 v: M+ B0 f5 U# r# s二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
/ t' B" Y1 U' J$ x8 J6 Y7 l1 N
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)9 i9 D- x9 J  O% y
  2. {9 _% I1 M6 Q- q' _
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);- H% h3 H- k  g$ T9 b
  4.     if(*socket_found == (~0))
    ; \9 Q- `: \  Z8 w* q
  5.     {
    2 g8 ]$ e) |6 X. v/ J) X$ Q
  6.         printf("Create udp send socket failed!\n");8 p, j. I+ H* R# P' ~/ K$ H/ Q$ p% J
  7.         return -1;* V1 O5 x# }( W9 G+ }4 c8 d
  8.     }
    : h; m+ k) @' \* t' G9 Q
  9.     addr->sin_family = AF_INET;2 ^( C+ _" Z& L# ]: X) R: w: L( N
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    ; M3 x" ]5 l; S" n' W' v+ Q
  11.     addr->sin_port = htons(port);
    , p: J- _; Q3 {* C
  12.     memset(addr->sin_zero, 0, 8);
    3 g) b% j3 j7 t2 u' v
  13.     return 0;1 o; \8 z4 ]; s- H) N) J
  14. }
复制代码
; c1 @# h2 W% A5 x* @2 ]9 K2 i

; {& `% I. d3 R9 b5 x2 v* t$ y而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:2 ^( C# j1 B8 ^* M8 K; }$ {4 X
# p, ?# H0 w" r" Q& e& D. r* C: F

+ U% v# C/ U4 S/ B( ?5 O2 M
  1. while(fend > 0)3 k# \& _1 G* W4 u
  2. {
    0 q6 f/ I$ T" ^- c
  3. memset(picture.data , 0 , sizeof(picture.data));
    ; ~3 D( e5 d+ T2 z4 x5 l: C
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    # U2 L# ~$ m& |  B+ M+ ~8 Q
  5. if(fend >= UDP_FRAME_LEN)
    3 \+ r% E; x6 h/ }) F5 x
  6. {6 z( M$ f: Y8 D" A( A
  7. picture.length = UDP_FRAME_LEN;
    ( o* P, d! o+ \2 N" F- N, g
  8. picture.fin = 0;4 \* S  g6 I- v- i7 q6 ^! e3 ^$ G+ k- ~
  9. }
    4 A0 }9 t: W5 y6 V$ ?, ?2 W3 B
  10. else
    / A; @7 m5 p" c+ }" F
  11. {: i: e6 @# P) y' ~) p3 \
  12. picture.length = fend;
    6 R6 d) Y2 {7 f* t6 p
  13. picture.fin = 1;
      f3 w, n. o( _* X9 `
  14. }
    " j% [9 g, `  H/ K( u
  15. //printf("sendbytes = %d \n",sendbytes);
    5 }1 _) i, r/ W4 e
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    ! B( X0 p3 ?5 z, I, Y
  17. if(sendbytes == -1)
    ' k/ X! k' A9 w
  18. {" J2 t3 h6 u) A5 U6 a
  19. printf("Send Picture Failed!d\n");. f6 n& L0 a: M; _
  20. return -1;
    2 `' K0 N0 T3 r# A" R* G$ x6 \6 W
  21. }  v+ ]  b4 [( ]5 I
  22. else" ^; M0 V$ a/ A1 d& h4 ?
  23. {
    6 m: h% M" J; {
  24. fend -= UDP_FRAME_LEN;
    9 U5 Q* f, S( e+ K$ ?5 C
  25. }
    1 L6 T; Q$ W$ b! `
  26. }
复制代码
5 g- a2 Z0 ?& z: g6 G: L- V  S
, v% u! q% M' F! G4 W

; _$ I, s0 ]! t# r0 u. H( O4 i, ]
* S1 m9 d1 ^, R) c, m
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-12-23 04:58

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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