嵌入式爱好者

查看: 12355|回复: 1

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

[复制链接]

48

主题

55

帖子

306

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
3 _, r1 y7 w; e+ G' _' x/ E- N! [( Y


* i3 h9 v$ d* t  L; U2 {

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html+ ?* r# a% ^) N" |$ V
4 a& a; L" v) |- Z

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

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


5 C0 f: y2 H8 U/ V


' `% j+ ?& r0 A6 b: _$ c! c6 g& J" `

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

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

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


    % ?# Q" i( {$ t$ d) S- E1 N

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

8 e9 X: [  x) }0 b/ A
一、HTTP网页服务器
) }& l0 l7 ]7 M- J6 L

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    ! C- `+ K( V9 }! Z
  2. {2 r/ S- C: q. l5 i! |0 |5 h$ B
  3.     struct sockaddr_in servaddr;
    6 n  }/ S1 P: p% G: M5 T
  4.     socklen_t addrsize = sizeof(struct sockaddr);! X4 J  S. \: |' P$ @3 x
  5.     bzero(&servaddr , sizeof(servaddr));- l; |& d" v7 Z3 D2 W; m' c# k
  6.     servaddr.sin_family = AF_INET;
    . p+ K) |* Y" Y
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);: T4 }/ |4 A7 e) I
  8.     servaddr.sin_port = htons(port);: \  v& z' D3 Z  ~
  9.     int ret;2 {5 j7 K& u3 j! a% G8 s& _* s
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    5 m0 n  ~/ I* j" I! r
  11.         {
    $ y4 e) n# a8 w& o: F% B/ L" s
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
      b  V2 i/ w" P) U5 T& R
  13.             return -1;2 {4 {* p! Q( Y" |1 ~
  14.         }" ]4 _* g* S8 N2 s% J- _
  15.     int on = 1;
    ! L# \5 t. _, n7 z
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)9 n$ u$ Z% v3 q# _; \3 s' {, t
  17.     {7 z* k% v# {& H6 V3 s
  18.         printf("setsockopt error\n");
    ( P. F0 X" p7 ~
  19.     }
    ; S  E0 r0 n, G; x( V8 k
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    . U) {: {: R  v1 J2 ]- ~- v4 X* L
  21.     if(ret == -1)
    ' c) K- J( O4 e9 P2 j- q
  22.     {
    ; t5 |) p, p7 b
  23.             printf("Tcp bind faiLED!\n");/ t; h4 {" a- W: E3 ^
  24.             return -1;6 {0 B- Q1 u1 |8 K' D
  25.     }3 r1 Q& [! C; o
  26.     if(listen(*socket_found , 5) == -1)
    ( o: `3 ]# G# C
  27.     {
    2 g- G0 x, A- A! N" L! e4 t& X2 K
  28.             printf("Listen failed!\n");7 O- z9 C2 q9 U4 `8 p9 O
  29.             return -1;5 f3 x- V$ H; x/ N# `& O
  30.     }* t, z8 r7 n, c9 @9 L0 @
  31.     return 0;
    ; g% z' _- \4 X2 r0 `) b- N6 I! |
  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);
    2 n6 t) B0 r7 ~6 x
  2. void * Thread_TCP_Web_Recv(void *arg)
    9 r# s3 v/ u; K5 Q* K- Q
  3. {! L; Y; P- ^8 P
  4. 。。。
      j- X6 U1 I8 e. g7 n
  5. while(1)8 S5 b* I0 @$ D; }* Y- l  r
  6. {0 A( x9 v7 `3 s  J. w  d5 R8 d8 X
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    ( q- R0 w  j9 {& N+ U1 u+ p
  8.            printf("fd_socket_conn = accept()\n");$ f0 O0 K! X9 U1 c
  9.     。。。* u3 n" K* }3 g6 K$ ~. ^4 S
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);: ?6 T+ Q; P. |1 M. Z
  11. }! `" G6 k6 P6 O8 _! h; Y
  12. 。。。
    8 s- F/ l/ t8 s% e: T* x
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);% P; B( s2 W. B1 U) y1 x
  2.     pic_tmpbuffer = pic.tmpbuffer;
    7 ?$ A+ s2 r; ~; H1 p5 q$ m0 [0 p
  3.     pic.tmpbytesused = buff.bytesused;3 t) {7 n' t% n7 b: q. c
  4.     pic_tmpbytesused = pic.tmpbytesused;6 ~" d/ J1 @) ?9 q% \9 J. M& _
  5.     pthread_cond_broadcast(&pct);
    4 R( H$ W) C# A. F% v
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;) c8 B+ ]( \1 w8 C
  2. pthread_cond_t pct;1 y$ E0 ?- ]: ^$ _; [
  3. int main(int argc, char* argv[])9 L3 C# V, x$ N5 h: L6 k
  4. {; L% e1 w9 L. h; `& C7 W6 ~# Q# E
  5. ...
    2 q" k/ [4 }: v; U+ k
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    4 Y! F: e: c: J0 y, U5 q
  7. pthread_mutex_init(&pmt , NULL);& q6 P1 S8 i9 v) h
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    7 W* L' g# O2 ~- @
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    & ]: g6 W: c( P4 s
  10. ...
    3 g/ ], h& `8 ^2 p4 l) h4 L3 W
  11.     while(1)
    $ t7 H  k2 P( B% U. J; h
  12.     {: l* A4 J+ ^5 H( A+ `6 f, k: m: z6 e% k
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);+ |, V- c) o% r+ l4 w
  14. ...
    9 ?  D( ?! w1 W% `* {5 v2 r
  15.     }
    4 d7 |! Q6 j+ h9 m+ d  j& l
  16. ..." R  f9 ?6 T# q
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">% e  a! y0 S6 g7 P& R+ d& k( z
  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 H0 T( L4 u# ~" m" ^* z
  2.     "Server: MJPG-Streamer/0.2\r\n" \- j! X$ i* G# t! q5 O
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \$ U! b) q2 |: }4 ~
  4.     "Pragma: no-cache\r\n" \1 [  W) x: X  r6 Y" I  J
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"7 \( U' H% f! d
  6. #define BOUNDARY "boundarydonotcross"
    ) w& p3 d  M  Z& y
  7.     printf("preparing header\n");
    " o- F5 V- f) ^- n! i
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \8 J, K& j/ y9 v0 z$ k
  9.             "Access-Control-Allow-Origin: *\r\n" \
    ) o5 i1 S/ i# j( I' r
  10.             STD_HEADER \8 l% S% }1 {. U
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \# s1 s& s, J$ R" Z
  12.             "\r\n" \' |, A7 o7 v3 i& B" v# D0 p8 k
  13.             "--" BOUNDARY "\r\n");( x- W8 r! V7 H% H# f% ?" e* e" @
  14.     if(write(fd, buffer, strlen(buffer)) < 0)5 j+ c3 Y! c/ e- K6 Z
  15.     {
    % k- K' [- u: l' H* C% r' P! d
  16.         free(frame);) h3 C4 e: P6 F, H" k
  17.         return;' ?, N/ A! B" A" _- q3 f4 k
  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" \
    : A3 P1 n+ y3 U3 A/ h" s" s
  2.                 "Content-Length: %d\r\n" \
    % A5 p& w. S! E+ G2 ]* A$ c% Q- k3 U
  3.                 "X-Timestamp: %d.%06d\r\n" \
    * C# a1 B, b0 X1 {
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    + P; B. d1 k' M: s  \! Z
  5.         printf("sending intemdiate header\n");
      ?" R; V8 V  A: N
  6.         if(write(fd, buffer, strlen(buffer)) < 0)4 A, x, ?7 {; S- `6 Z7 [+ @
  7.             break;0 V) E! S, k8 z* J
  8.         printf("sending frame\n");
    0 T+ h, x# _4 G$ E
  9.         if(write(fd, frame, frame_size) < 0)
    & \" P1 U3 ]) y. i
  10.             break;; c% X: Z) y) o( o$ O' @7 Y7 Z
  11.         printf("sending boundary\n");
    ' t9 k; |3 n8 R+ M+ |: F4 U
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    . ?  _8 q9 L: O& z3 W  ]; b
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    ) K# o% J: U' s5 \( H$ b; G% |
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

/ {1 r7 T" [. u/ e( t

) o& N+ x5 A6 |! h* ^' D! p
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:6 W% @: m0 L4 B9 X& v) N
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    2 C  p3 _# h  B* _9 P2 W. h
  2. {
    1 u/ p# B" O/ L9 e9 g: i
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    ( t+ J6 K$ ~& b! K6 g+ m7 M
  4.     if(*socket_found == (~0))
    $ w0 J4 M9 h4 ]% R; B! G9 i
  5.     {
      U+ c5 I; p9 x# Y2 z( n
  6.         printf("Create udp send socket failed!\n");
    3 G, {, l3 Q" @/ p- Y2 E6 T
  7.         return -1;
      w0 L3 Q( D6 {( L
  8.     }, o# b' r! X( m, B" Y8 o* [9 w( y" I( s
  9.     addr->sin_family = AF_INET;
    ! r! \, e6 s2 U, N# D+ V, }
  10.     addr->sin_addr.s_addr = inet_addr(ip);& r9 W' \# H% u& T1 P
  11.     addr->sin_port = htons(port);+ X( T5 X7 X: H0 ~, j3 R2 S
  12.     memset(addr->sin_zero, 0, 8);
    & r5 i' V' \1 C+ ^) Q
  13.     return 0;
    8 P( I) Z( U. ]6 F
  14. }
复制代码
9 D# m) R% P! O$ m
9 z9 W( g! ?7 p; K8 d' o
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
1 r; B" ~9 f  X5 k+ s4 I
& V0 t/ J8 r( t- ^# o& i
- R5 Z3 x  B# o+ p  T! b" e& R
  1. while(fend > 0)
      V* Q" x6 C$ \& v0 M( z
  2. {& E" ~3 A( R# F7 e+ I/ ], j
  3. memset(picture.data , 0 , sizeof(picture.data));
    $ ^( K0 E' B- V, D( d1 T% G
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);' h/ B* k- m* T+ u# d
  5. if(fend >= UDP_FRAME_LEN)$ }: m) l" q+ r+ N
  6. {1 v  v3 Y$ l5 @/ y- G
  7. picture.length = UDP_FRAME_LEN;4 w) y4 T( u7 J- g. {
  8. picture.fin = 0;) ]& V. X# H! r6 ]: ?
  9. }
    ) h0 s) j9 Y( ~2 T- L- F1 p- s4 J) j7 T
  10. else
    9 m# x& o6 b$ \) i: ?, K7 z" ~
  11. {
    - I7 P( R! ]  t! |1 T
  12. picture.length = fend;* g1 Z5 c' [2 }/ ~- B: x7 U3 a
  13. picture.fin = 1;
    " I/ n0 _3 s3 y" F6 D5 h
  14. }; i; t% U- u! b7 M# w' f/ \
  15. //printf("sendbytes = %d \n",sendbytes);
    & Q: M5 G+ u/ n) {* ~
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);( |1 _2 g" X% R, u5 L
  17. if(sendbytes == -1)
    + L* B: e9 H4 U2 w' F
  18. {
    1 N' Q. S% m8 g6 j$ i& ?- O
  19. printf("Send Picture Failed!d\n");1 [6 T# N% \) Y1 E
  20. return -1;
      @! n, K- D( m
  21. }
    5 C; `; `; z0 }7 y0 |
  22. else4 v* i6 C8 Z  }9 I. U
  23. {
    3 @: X$ v: [$ F3 M$ p5 j3 r
  24. fend -= UDP_FRAME_LEN;8 g2 i: {  T4 i$ I3 v1 I, P. Y- U
  25. }
    ' j" e6 e! y# E
  26. }
复制代码
# |5 S- n" T3 r8 \1 D
1 x/ G6 T) O/ W& n$ ]' @+ b9 I

) l+ Z: p6 g- g5 P. k  m

8 C) Z! z6 H: a+ U0 QiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-7-27 07:54

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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