嵌入式爱好者

查看: 10945|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 / @; C( h7 I  `3 X0 V2 q8 r+ @

7 m/ }3 v6 ~) T1 c

  F: n8 @+ \6 G3 @; @6 [

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html0 {& J- c9 [8 L7 ~  |! C

4 z& v: @1 ~0 E7 D2 J# R

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

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

& I( d: m7 i/ u* X$ w+ S) W


  _: n# o+ D% L! I$ z- U3 z- U, `

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

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

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


      B  U, c# n1 f3 p1 b

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

. I) b; h! [2 w9 O
一、HTTP网页服务器3 }3 H0 |/ b' p( c( N

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    6 ~5 g  l/ d) g
  2. {
    " ?' Q6 h) i4 b# Q% E" X
  3.     struct sockaddr_in servaddr;
    - ]  N9 f9 S( q
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    : m+ l3 M2 p/ h; i7 T
  5.     bzero(&servaddr , sizeof(servaddr));
    # h$ n* Z" Q! ]0 [' N% a. h3 T
  6.     servaddr.sin_family = AF_INET;
    ( ?5 f  E+ s7 ]
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);2 W/ Z! h# ^2 N% }' K
  8.     servaddr.sin_port = htons(port);
    + l! y0 r# R! h9 m/ n( Q
  9.     int ret;/ H, i8 i4 m. C" }5 s3 o& @5 _
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)6 f! |$ q; y( J' d5 o
  11.         {
    " l" X$ t! F" y0 @$ K6 a" o
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    * h! I5 m/ p9 e5 T! d7 C+ x( z
  13.             return -1;
    9 d5 R* G+ l3 W+ _, G' O% J1 P* e
  14.         }5 V, k# _- {; Z5 |6 D, ]0 R
  15.     int on = 1;- O; j9 i, j. J3 u' l
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    & G0 u7 Z+ @" z8 Y8 Z. S( E
  17.     {
    % z% z8 B( A7 L; R# B
  18.         printf("setsockopt error\n");
    & B) W; `8 k4 k: j- L& o( V6 [
  19.     }8 O. d$ x+ D3 `0 B& c& A' j- V5 [* F
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    9 W5 s2 p, p' F% |# S
  21.     if(ret == -1)$ Z$ `/ f7 W0 @% E3 t
  22.     {$ u5 }% a' v% T" u
  23.             printf("Tcp bind faiLED!\n");/ o6 ]; \* U4 k$ a8 a
  24.             return -1;
    3 r8 R: n, O% V) G: F; k  [
  25.     }
    8 H: \/ a0 M4 Q( F3 c4 `
  26.     if(listen(*socket_found , 5) == -1)' ]4 d& f  j* B" T0 i* A
  27.     {7 ?; }0 @- @% v; f* m
  28.             printf("Listen failed!\n");6 `4 f+ u; B% B! w
  29.             return -1;
    2 h" t1 c, D+ P& y% M# `6 r- f
  30.     }$ Q1 O2 M5 u; m% s6 o6 Q
  31.     return 0;0 h( q" c4 x! @/ k- o& C
  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);, w( G% g' T* m7 I
  2. void * Thread_TCP_Web_Recv(void *arg): z9 M6 x# X& f
  3. {5 a, n: S: y$ E7 V
  4. 。。。
    & C7 i# O- C  G) I
  5. while(1)
    2 w! o; r+ i  ]/ c) M# w  d
  6. {, ^( _# o8 p7 P7 j; _
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    5 x3 t! l# f0 k( ~; ?5 @8 ?( i4 t
  8.            printf("fd_socket_conn = accept()\n");7 z$ g3 r8 u7 E7 t; g/ h
  9.     。。。8 r4 q, e0 O2 {- l4 G  U0 u
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    & l6 H4 f; Q# c. z3 f) u9 ^
  11. }
    4 Z5 C* {" m. a2 b
  12. 。。。2 \# J  m3 Y6 {8 N- e4 G! H8 l2 I+ |! W9 d
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);6 N$ i/ M2 W- F6 y& D0 o
  2.     pic_tmpbuffer = pic.tmpbuffer;9 h9 {0 {, p8 T' B/ X
  3.     pic.tmpbytesused = buff.bytesused;' d$ h+ W8 r! B0 R. n  X+ Q8 y2 W
  4.     pic_tmpbytesused = pic.tmpbytesused;
    % g- n6 P1 @0 M$ _
  5.     pthread_cond_broadcast(&pct);7 ?1 d8 G6 G% e- y7 ?/ b; K" h/ z  c
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;; F4 ^" \: E/ M  V" y
  2. pthread_cond_t pct;, n+ Z3 H2 A6 Q. _, O+ y. m
  3. int main(int argc, char* argv[])
    8 f4 ^, |' ]! k6 x% x3 i& e) ^: n
  4. {
    , t. I$ v/ H, \- A$ \1 {2 H
  5. ...- r4 x$ n5 z/ U! e1 y! d# B
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    # V! V2 [: A$ c# G8 r- Q4 P
  7. pthread_mutex_init(&pmt , NULL);* C6 ~* F  u& J9 L" k9 G
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);( K1 K$ n% y: i- p  V
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);1 D$ H! \7 A) |
  10. ...) P$ o. |' R6 z" O7 b% s
  11.     while(1)/ t' ~5 w2 Z1 ^8 c+ E
  12.     {
    2 E# S3 s3 o% P. y" `
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);& Q9 X* ^% s. L  O3 H
  14. ...
    ' L% M2 K, D  M' ?6 G9 C6 V
  15.     }( T& g  B( M% h5 K
  16. ...2 @& |- C* Z+ C. _6 [) m
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">4 k# N+ k- P! |" k$ E# {4 g7 S( U: x
  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" \, g+ L' B0 L: t& E, E0 t
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    , t2 M- |- k% X
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \: |5 u! Z: e0 M8 t
  4.     "Pragma: no-cache\r\n" \
    # k8 M3 l; K8 k# Z  F
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"! T/ F% T& m8 W& R* d* @. C. J9 m
  6. #define BOUNDARY "boundarydonotcross"
    ' W+ K( o4 e- t
  7.     printf("preparing header\n");% A) J% ?" P5 `" `' L, g$ b! \
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    ' G  @1 ~  W5 d" W1 G6 I7 V& E, ?
  9.             "Access-Control-Allow-Origin: *\r\n" \% u4 {- I; ~7 Z; A
  10.             STD_HEADER \
    ; Z6 O8 E6 G8 v1 j4 H& f; D
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \  U4 m2 D, _' |, u
  12.             "\r\n" \
    1 b% O1 ?* z( Q
  13.             "--" BOUNDARY "\r\n");
    4 O; D% r5 h" J  Z2 {
  14.     if(write(fd, buffer, strlen(buffer)) < 0), u0 R6 _; G3 T; Z6 w& E4 V7 _' \
  15.     {
    " t# [$ `0 D2 G. u' J8 ?# j( l4 B. j
  16.         free(frame);
    & p# l6 w. D5 k0 y/ G$ U
  17.         return;* l  G: M$ S8 f+ f1 v, 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" \
    / _7 W  V# H; l: r
  2.                 "Content-Length: %d\r\n" \, Q: s1 i2 T" ~+ X
  3.                 "X-Timestamp: %d.%06d\r\n" \6 \' t+ T$ w2 {+ F, p! r) I
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    0 I7 n) s) D% q; t0 t
  5.         printf("sending intemdiate header\n");
    4 _& e' U& |+ E; O$ B  {* _
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    ! d5 J0 V4 y* j, u
  7.             break;
    4 L  `) Q! n$ f- ?2 p0 b3 r3 c
  8.         printf("sending frame\n");1 ]' V2 s2 u6 M* z2 c
  9.         if(write(fd, frame, frame_size) < 0): a$ r  w: s5 d4 y/ g4 x
  10.             break;' `8 w  i3 w. N, Z  S3 P# d, o
  11.         printf("sending boundary\n");
    1 v) O% E! S& X1 L9 l' }
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    ! b# |  q. K5 q. }9 R
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    % v) P" G8 p) Q. r5 A" X/ _; H" h
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

9 L- |# J( Y) N; J


6 X5 c5 t$ ^2 O& c1 E$ R# H二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:) f1 y# u% F6 |1 x; M0 C9 C
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)& M$ T, ?6 [) f7 X& P
  2. {; n( `+ ?+ o; B  Y5 B, H
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);3 e% @; P2 P9 ?0 f8 a, O3 _" X$ P
  4.     if(*socket_found == (~0))
    & i$ |3 k- S3 k4 U9 e
  5.     {" q+ O- S9 b1 ^/ `, C. c
  6.         printf("Create udp send socket failed!\n");2 u; W. D. C/ c
  7.         return -1;. s7 u" X9 u: H, S) s
  8.     }' J2 L2 K/ n0 m/ {
  9.     addr->sin_family = AF_INET;
    8 J6 b( H4 U0 t
  10.     addr->sin_addr.s_addr = inet_addr(ip);1 m( s* Z9 M. P1 P: ^3 X1 v& d
  11.     addr->sin_port = htons(port);
    3 v3 L; M( y5 }/ L$ P6 W, ]
  12.     memset(addr->sin_zero, 0, 8);
    7 g  P  u1 u: j) F) |
  13.     return 0;
    ! k* C3 z1 L$ O
  14. }
复制代码

2 m) f3 K; x4 k' T4 y
+ P" k' `* @+ r而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:. O' A. G2 w- g& R7 a4 ~6 W

# T. o% s5 H# e5 |' H8 L: {2 M* G! E# _
  1. while(fend > 0)
    . H( p) B# {0 h, l4 u
  2. {) x8 z1 L+ U# h4 G& a6 n( _
  3. memset(picture.data , 0 , sizeof(picture.data));
    : @+ C8 m; n4 R6 ^" R; ^9 ~
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    2 y& i/ X1 T! c# n
  5. if(fend >= UDP_FRAME_LEN)0 C, a  Z; W# O% Q6 X
  6. {
    - n% |1 y. \# f" I8 ~8 m
  7. picture.length = UDP_FRAME_LEN;% D; L1 c: D( G  [
  8. picture.fin = 0;9 H8 h6 {; n" E( a. D9 ^6 q
  9. }# Q1 D) C7 c5 m. s" V; v) a5 j
  10. else5 y2 k! i8 Q, ?# S
  11. {
    ( l  ^3 s2 a9 R/ z$ b* g
  12. picture.length = fend;- g1 D, P8 ]6 }$ t+ q5 f/ \. @
  13. picture.fin = 1;
    & [7 X( Z' x( [" N+ ~+ Y. v
  14. }
      x: j" @8 |5 ?* X5 d7 Z6 F! G! x
  15. //printf("sendbytes = %d \n",sendbytes);
    $ ^, ]: N: S0 ?  W
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    + Q9 u0 X: h1 D& ?& C0 L- F. r
  17. if(sendbytes == -1)
    8 x% Y* y1 i) t  F
  18. {
    7 e  \' \/ p3 {, A
  19. printf("Send Picture Failed!d\n");: X" q. U& E! N, c, e! s+ n
  20. return -1;
      U0 [$ L/ F% j* Y. R6 z( Y
  21. }
    + I- k' B% l6 ?+ I
  22. else
    4 r+ B( \+ a: x" O5 S+ t) {
  23. {' `! `3 a# r# m! W8 n
  24. fend -= UDP_FRAME_LEN;, `8 j" S* s8 V0 ?
  25. }9 k7 X; i; i* I( h' a3 C
  26. }
复制代码

$ Z+ B5 `. w1 k- `2 |& A/ h3 C; e$ {/ D

) Q. L! F& \  {. s* T* o: s6 ~' {& n

6 e6 {3 S! [; M1 l5 N" DiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-1-30 20:05

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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