嵌入式爱好者

查看: 10916|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 8 U$ W& ~# L( Q9 h: h& W

/ n0 Q) _' h2 a


& D& a9 h) \# S  k- b' R

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
  o+ x* d% [; S# z! T' y
, ^; Y% C, R8 t6 P" Z: r$ g

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

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

9 b1 E: Y: ]9 }- f

9 p$ A. A+ K: ^, @3 W1 z

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

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

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


    4 O+ v0 c$ w! [7 T

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

6 [' i6 Y5 h4 _1 m. P
一、HTTP网页服务器4 u) Z% D* E5 `  s2 M: c2 f  {5 g

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    7 T% O1 ^7 X& Y9 a
  2. {) {9 T9 V7 N, G% q
  3.     struct sockaddr_in servaddr;6 s, x  A' @/ |3 h% h3 F* Q2 m
  4.     socklen_t addrsize = sizeof(struct sockaddr);+ I8 d! C  q) w7 U
  5.     bzero(&servaddr , sizeof(servaddr));
    - G1 Q, ~$ i+ A1 x# E8 ~) w
  6.     servaddr.sin_family = AF_INET;
    % t4 N  W5 U; P. u7 H
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);% b3 l* D. E# |) m. I$ V
  8.     servaddr.sin_port = htons(port);, Y4 A3 w9 {7 f! t9 n) c- t
  9.     int ret;) G) M, {5 [! C. L7 a( K
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)0 R" u5 a- b2 |3 ~2 k% A7 ~
  11.         {# H% q3 |& Q. m0 K! E  W! S( Y# W: `; l
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);3 R9 v* A# j5 D2 f# z& p
  13.             return -1;1 l9 v: r3 y8 d( \
  14.         }# ^- w% L7 `1 T. t6 u
  15.     int on = 1;$ W$ `  s4 B/ `3 f$ f4 U" U
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0): w9 q- F( ~7 q
  17.     {2 m* a/ z' _# ?" G2 b
  18.         printf("setsockopt error\n");
    8 s8 M4 s& r' `7 m( E
  19.     }0 w% T* _  h7 m- w9 C8 ^& p- g, v& R
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);# M, E6 w% |, K( b) ?. V; z# l
  21.     if(ret == -1), T6 \4 _3 Z) |  C! O# f
  22.     {: S; o& k7 @% e1 m# H7 }( S
  23.             printf("Tcp bind faiLED!\n");
    " l+ @9 N8 i6 C# J
  24.             return -1;& K/ l' \3 e0 y: m- e; F$ A
  25.     }# b$ O" K+ ~, H4 r' M- K4 \6 `- [
  26.     if(listen(*socket_found , 5) == -1)( y0 N7 R0 }; X
  27.     {
    4 Z0 f5 d/ f9 P! B1 c6 ?
  28.             printf("Listen failed!\n");! R  t$ i0 n; I( F2 P# Z1 `0 U8 f
  29.             return -1;0 x( Z7 q2 I+ @( _1 s9 Y  f
  30.     }
    7 o$ k- P, x: W& q2 i7 L. F
  31.     return 0;
    2 t7 o- t) S+ L, m% 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);; B' J# F2 ]. Q& q# r* R2 f) E" L* n
  2. void * Thread_TCP_Web_Recv(void *arg)
    0 N9 j! X$ r" T0 `" h0 k7 e
  3. {) H2 q( D5 i4 v( |+ M5 Y* ~9 c
  4. 。。。' H* }$ m5 c/ l: p2 Y% C
  5. while(1)
    1 x2 [. u$ y6 o7 d4 p; ~
  6. {1 l; m" G- i: V" |  o5 t
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);) |* m9 l' h6 G" w0 y
  8.            printf("fd_socket_conn = accept()\n");* }2 w2 P3 k4 F# Z' L0 o" g
  9.     。。。
    ) Q; S1 l4 Z1 t$ ^3 {
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    1 v  n5 `0 D5 j
  11. }
    ' l2 v, y( y) f
  12. 。。。+ g* ^4 \# `, Z  X5 u6 Y- E9 P
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);3 J/ p; K, m$ t& i# b
  2.     pic_tmpbuffer = pic.tmpbuffer;3 H: P: ]' u! j
  3.     pic.tmpbytesused = buff.bytesused;
    $ |3 y7 W: V% a! F8 R
  4.     pic_tmpbytesused = pic.tmpbytesused;
    2 \; e) \  g/ E
  5.     pthread_cond_broadcast(&pct);
      [! i. J$ I" S" y6 `; A3 o& g" h7 u
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;' P. o& C5 p5 y7 X
  2. pthread_cond_t pct;& w9 f+ g) x3 H1 l: h
  3. int main(int argc, char* argv[]): b8 F/ `/ k$ I3 ?
  4. {
    5 u8 K: a7 G* i# h5 |
  5. ...
    , a& p& G# Q! H2 q7 B. _
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);1 i) n, B# w  v9 K- D
  7. pthread_mutex_init(&pmt , NULL);/ X) c6 \" F! X  s, B& U
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);6 n: U' t; g2 Z5 h9 _
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    7 u0 `' f6 e1 P7 ?% K/ X
  10. ...
    " h9 b/ O& f$ E
  11.     while(1)
    / z/ Z/ k: b5 d# ]9 V! t2 F
  12.     {
    9 [9 B) y0 B, {& A7 T
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);: s9 a+ L$ S3 {7 P6 U
  14. ...
    4 y- w6 H: C9 T! T+ \+ f
  15.     }0 b. f! A* D6 n0 i! S5 m% T, C
  16. ...0 A; A3 `$ ~: n% ]' U* e* V0 F
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    : Z$ _2 G, ^/ c8 M
  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" \; p7 J+ U  U6 w: c, ~
  2.     "Server: MJPG-Streamer/0.2\r\n" \* [4 W% k/ L, r3 o. v
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \7 J# B' V( a, V4 H2 U
  4.     "Pragma: no-cache\r\n" \
    & @3 E! t9 r! j, N9 V0 D3 W2 N' @
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"0 }) B' X4 N  M# T1 `: C  v
  6. #define BOUNDARY "boundarydonotcross"
    2 K# H( m. B) z/ t+ F
  7.     printf("preparing header\n");
    $ x' ^5 ^- X  G1 z. U7 q
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    $ p+ K! d: Y: S4 K
  9.             "Access-Control-Allow-Origin: *\r\n" \
    9 u1 C& I8 D' V* n1 Y! X* G- p3 c* Z
  10.             STD_HEADER \
    / m% s. S4 o) V1 n. D8 R
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    ! p' e  ^+ {4 ^. V4 R% h/ S9 O
  12.             "\r\n" \
    # S6 d2 z2 w5 y( Y
  13.             "--" BOUNDARY "\r\n");$ ]0 Y) x) q- y
  14.     if(write(fd, buffer, strlen(buffer)) < 0)! P2 b3 f$ K; ~  H% j4 U
  15.     {
    8 d& m8 q; R4 d. ^4 q$ e& |9 h
  16.         free(frame);0 |, Y# O! Q$ v7 F
  17.         return;* d0 K, ~6 r# j* g/ b
  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" \
    $ {& [; e! c% t7 Y. @
  2.                 "Content-Length: %d\r\n" \- J6 [+ n( d/ H4 X4 q' y! p% o
  3.                 "X-Timestamp: %d.%06d\r\n" \
    5 t( m) V) x# p% H
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    / {, c# N+ S( c7 o
  5.         printf("sending intemdiate header\n");- T( L% ]4 f0 H2 [! l6 ?
  6.         if(write(fd, buffer, strlen(buffer)) < 0)1 G0 u" k9 c4 `
  7.             break;
    " |- y! u: j9 G3 m5 c9 G1 ~
  8.         printf("sending frame\n");
    0 p* a2 Y; h' A' U" L5 B1 \5 h
  9.         if(write(fd, frame, frame_size) < 0)
      e5 i/ _7 v: p( A! O. q
  10.             break;
    9 R! x& Q. Q1 O# S5 U
  11.         printf("sending boundary\n");  y. @! T# i4 [$ z
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");* P  J3 M7 s8 y
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    4 v* J  ?9 z  F- R* o4 F
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

( M. W8 r" j( _7 @


' k* v8 e: r2 H5 w5 Z9 ~5 g. d' `% \5 R二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
9 w( q! c  ^, ?1 a  x
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    7 u: H6 L# B. q' V: Y: @' z; s' b
  2. {
    % m# W9 t2 n; O0 `* t  R8 Z
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    ( Q' {5 L% `2 a! c, V. q2 }
  4.     if(*socket_found == (~0))6 s# P9 }% e5 b3 Z& e: P
  5.     {6 j: H8 c) r, A& n
  6.         printf("Create udp send socket failed!\n");
    ' ?. W2 p& l' Y  m
  7.         return -1;6 N5 O$ E" f1 z) ~3 H6 k
  8.     }& O. d* O6 F5 a- Q( f
  9.     addr->sin_family = AF_INET;
    + j- D' K( i! i7 ~& o& t
  10.     addr->sin_addr.s_addr = inet_addr(ip);8 Y4 x# p' J. {; Q+ L$ |3 ]7 y
  11.     addr->sin_port = htons(port);% C# K& j+ d# x3 G
  12.     memset(addr->sin_zero, 0, 8);) M! A: c3 X( h* j' ~
  13.     return 0;, G! C9 H- i6 u# y- {8 F
  14. }
复制代码

! w: Y* |; x6 x! J: c# k% ?% V( u% L% G% G  Z- G/ y0 A3 ^
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:* R' z" w/ Y, c. |  g8 I; z# `6 R
, C" Q$ i- s. o3 g% h2 X# @- F

2 i" R+ F0 Y$ ~& J
  1. while(fend > 0)) Q. ^$ v& ^8 }# [2 u2 J0 r
  2. {5 [, j4 b8 K1 X$ E; H
  3. memset(picture.data , 0 , sizeof(picture.data));
    9 f& w; ?# [7 d% Z$ q
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    8 J2 j4 ]4 @: b  I8 z
  5. if(fend >= UDP_FRAME_LEN)
    8 D; n& @& b9 Z. l1 c
  6. {: H4 t- w! T9 y5 p; E  z7 f7 k
  7. picture.length = UDP_FRAME_LEN;
    & B  S" B. R: u/ h7 r! }$ s) k
  8. picture.fin = 0;
      r- x; w- }$ M6 J; w1 {
  9. }4 u; T- ]5 |/ y6 w( ~; G' W* [
  10. else
    # h( S# B, k1 [/ f8 R
  11. {
    $ Z0 E4 e  \3 @! L2 E
  12. picture.length = fend;! v9 L* @7 s) q6 S: G# K
  13. picture.fin = 1;9 ~3 X: l3 e9 M4 Y
  14. }7 g+ l; g) e3 \& T% \
  15. //printf("sendbytes = %d \n",sendbytes);3 q, [% H% E3 p0 ~
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);/ u; {8 @' S) I' e" D
  17. if(sendbytes == -1)
    * V3 g9 u$ `1 O+ y" m
  18. {- f. Z, e$ h( Y6 T, {/ u* m0 a
  19. printf("Send Picture Failed!d\n");
    9 _9 d" L$ t) r- {
  20. return -1;9 K# b8 C) R+ Q/ _6 Q1 L6 V1 U
  21. }
    0 d7 G8 D- @) n/ g; |+ A  G
  22. else
    ' G4 S3 p9 d4 W/ N
  23. {' o6 L2 \1 W& c; T% q
  24. fend -= UDP_FRAME_LEN;; [: {9 ^- U# D5 {6 {
  25. }
    ; k( d3 i. J4 o$ j
  26. }
复制代码

  E+ C6 T- r, o. Q6 H9 A& Y$ m/ b' u. s. F: b


  F9 U2 w/ p( V0 Q5 o- J( j0 T3 Q2 ~2 }; S0 w) A
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-1-28 01:58

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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