嵌入式爱好者

查看: 11702|回复: 1

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

[复制链接]

48

主题

55

帖子

304

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
/ m" O$ h+ x) M% ~6 s! N! x! [) s. a% \$ @/ N: A

9 R& G# u' v- X# ^! b: l5 s9 Q, e% V. z

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html( H5 r! F7 F# |  q, s: c* w
3 O3 v/ X" Y' A$ P

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

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


9 \% s# Q# K: r; v$ E; {. b


& S0 c& E4 i7 j/ n

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

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

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

    4 n" m+ J' j, r' J+ ^: w+ h

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

& F4 c( r5 t5 p) r; d
一、HTTP网页服务器( a4 g+ P  \: u* e9 K1 X

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)3 _  C) b) {% `/ i! Q
  2. {
    % f0 A: S; _, t" k/ r1 y+ N
  3.     struct sockaddr_in servaddr;
    ; I6 v; @3 p- J3 t
  4.     socklen_t addrsize = sizeof(struct sockaddr);! S/ M- b5 @/ v& I2 E1 U3 Z
  5.     bzero(&servaddr , sizeof(servaddr));
      @) G# Y4 F5 T3 H% ?
  6.     servaddr.sin_family = AF_INET;
    9 `0 D, S1 W* Q0 q1 ~
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);+ X. k6 O0 H& ^) p( s" |, e
  8.     servaddr.sin_port = htons(port);
    $ e) O( c  G3 V
  9.     int ret;1 }* s" n4 @# B) x* L7 n  g4 \  F
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)5 U) o+ G' O, a$ q
  11.         {& J2 d5 y: J& h3 x* p7 h
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);: o! H. B8 V, J& c. G" {4 T
  13.             return -1;. C+ P" t' n; |9 L
  14.         }% P/ B; o  |  }9 n9 t8 ~7 o( [' V
  15.     int on = 1;1 _: j  d) W+ O# C. D8 x* _/ L0 |
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    & c9 Y5 N: L: u$ [
  17.     {, b$ z2 A( f0 Y% D# _* v
  18.         printf("setsockopt error\n");6 B7 X) N3 O4 R# }8 E# Y
  19.     }% E; T9 _0 X, _" Z  Z+ _: U
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);& @/ A2 [  @8 c" s! `- k
  21.     if(ret == -1)7 ]6 Q8 Y( R& L0 Y9 O
  22.     {
    . x! W7 h7 F$ b4 I5 z9 v% n4 f' W
  23.             printf("Tcp bind faiLED!\n");! [$ Q8 w6 i& _2 G1 J1 n) }5 W
  24.             return -1;
    " \$ l$ q; _+ |7 H) f  y. ~
  25.     }
    ) q2 |# k- D3 [, u7 h) D
  26.     if(listen(*socket_found , 5) == -1)4 W; }1 C$ w$ I' _9 [
  27.     {( y; {* x' q* i1 L& {" V2 x  E
  28.             printf("Listen failed!\n");
    ' z, V6 M; s: i) A
  29.             return -1;
    9 X# e" y  q7 _6 u# }9 u  R
  30.     }0 X9 T9 Y+ T9 A% X# {, W% r
  31.     return 0;
    7 x7 @' Q1 m, p. N, X
  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);
    0 J! b1 m$ f. a$ Q1 F1 g3 Y, g
  2. void * Thread_TCP_Web_Recv(void *arg)
    ; ]: d* f7 x: I" b
  3. {
    2 V( R- z. ?) G' n, S4 l) [; }
  4. 。。。
    4 e( M" a2 S4 {% W9 j! b0 `  m
  5. while(1)
    * ~% V) a; [# w6 H4 J6 N- _* l
  6. {
    0 Z- Z5 w8 M( e3 H( P- f: K
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);7 k; ?) F; ^7 h# L  a5 H/ S: y
  8.            printf("fd_socket_conn = accept()\n");* k0 B: b" o& o, z% d
  9.     。。。' \2 T0 ^( G6 ]) p/ b
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);% _' G' \4 v1 N$ C8 z1 b
  11. }
    " I4 S0 i2 I' L3 x( e- i" H
  12. 。。。
    / v  J8 A4 f; s
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);2 p0 O! N( {% |' s& }# Z2 ]
  2.     pic_tmpbuffer = pic.tmpbuffer;+ R) C$ e' s$ s4 r) }4 r1 n/ y5 P
  3.     pic.tmpbytesused = buff.bytesused;
    ; Y/ E% y" b7 f. m# Z5 `
  4.     pic_tmpbytesused = pic.tmpbytesused;
    5 v, {; ]+ q; g  o: h
  5.     pthread_cond_broadcast(&pct);
    2 C8 f8 j$ ~* O/ f
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    , i4 Y4 a' H& S# k
  2. pthread_cond_t pct;* B6 `  w+ T$ }" `
  3. int main(int argc, char* argv[])9 z1 _( F9 u5 f3 G9 j+ x
  4. {
    7 J8 b" S8 ]3 [. u6 @
  5. ...* f, o  Z3 g' d' N0 U
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);  j, q7 M3 }5 u6 A  N- a# j
  7. pthread_mutex_init(&pmt , NULL);1 K; l! p0 [+ j& L
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    6 k8 D( q5 ~& \. z" J* b3 ~9 T& {
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    + m% }: g' N- Q/ {1 l! e
  10. ...
    ! |$ P$ e. o# t& k7 a  W
  11.     while(1)
    9 e0 Z# Y3 U, f' ^/ Q
  12.     {4 q6 M8 Q8 x' F/ l
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);7 ^9 S) S1 k, m+ \; {4 l1 G
  14. ...1 a1 x, o8 Z: ]% r
  15.     }
      h% s: `! p8 K7 i% g: i
  16. ...
    5 t% `% a2 d: x% N5 F' s
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    3 c% ^( U7 ]' P4 n6 I3 O
  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" \
    $ |* r1 \& k7 i) p" S( l9 K# k: N: r
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    0 ?* @2 n0 C5 x, V2 y2 p- _
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    / _% R% Q/ C* \2 {# s6 G
  4.     "Pragma: no-cache\r\n" \
    9 @/ p, _9 _0 |
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"7 `: x# a) i7 e# d7 `; C
  6. #define BOUNDARY "boundarydonotcross"
    & ?$ w. M* {) {7 s! g5 j2 S8 x9 ~
  7.     printf("preparing header\n");  d% \5 i. p: G& k1 }
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \* o6 l" U; y7 r* ]
  9.             "Access-Control-Allow-Origin: *\r\n" \6 y. n7 X1 Y: @" ?/ V
  10.             STD_HEADER \& Q4 B( `8 M% Y4 k# B, A( e
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    8 I" a. R" v: U& O% F, n( ~
  12.             "\r\n" \
    6 y5 X1 u+ V( N9 Y: Y
  13.             "--" BOUNDARY "\r\n");6 l8 g! j) ~. S( S3 B
  14.     if(write(fd, buffer, strlen(buffer)) < 0)( q/ s. k9 C* {! j: b/ _! s
  15.     {
      s0 k2 I3 G/ F1 n' g$ W" z
  16.         free(frame);
    ! `. |2 X3 t( `+ f5 Q# x$ f5 k4 d
  17.         return;
    6 u( ~1 a% M7 f0 e1 v! N6 L6 t
  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" \0 P7 D( w# P9 i- K
  2.                 "Content-Length: %d\r\n" \. D# v/ j' w* T1 t  }! I
  3.                 "X-Timestamp: %d.%06d\r\n" \6 }$ V+ M" d7 p0 i* C
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
      L! H  x+ i0 I9 l4 U9 E. @
  5.         printf("sending intemdiate header\n");7 j! r; l% \: R8 H+ J$ R
  6.         if(write(fd, buffer, strlen(buffer)) < 0): X5 t6 ], Q, M+ D
  7.             break;2 b! ~" p9 C' K5 o- O' v6 W
  8.         printf("sending frame\n");5 g6 j3 Z9 E/ [
  9.         if(write(fd, frame, frame_size) < 0)
    3 k  l% M# g& @0 T' d7 \  C
  10.             break;  a5 _: B" C2 s
  11.         printf("sending boundary\n");9 a* k9 |' U  `5 B: S  `
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    ) h4 X) y' c" s% Z" l$ O- K3 k
  13.         if(write(fd, buffer, strlen(buffer)) < 0)) t5 x0 w, |% V
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

+ n) s# `  c/ C- ~/ E4 Q' u. P

2 F! Y/ t1 `, N4 B# O# q7 L% Q
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
: ~3 J6 r% V0 y
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)+ V2 C% z: h: F1 e. a
  2. {
    ! t( I7 V7 }' i
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    : I" ]7 A* T( L3 f, W% |
  4.     if(*socket_found == (~0))
    % n- g4 Y- s/ Z
  5.     {  c9 f0 B4 q0 m
  6.         printf("Create udp send socket failed!\n");! @  [3 \! l( ~5 R, b! N
  7.         return -1;
    $ J3 H) ]5 Y' S7 g1 a/ m
  8.     }& k5 F) n6 `4 z7 ]
  9.     addr->sin_family = AF_INET;% F9 _# p) n7 E4 E1 H! W+ o
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    4 ~/ a! _7 }( D/ E  C5 I
  11.     addr->sin_port = htons(port);" }- j  A! S6 p( k! L3 ]
  12.     memset(addr->sin_zero, 0, 8);4 b! A6 b: e- x& X& }+ |" P
  13.     return 0;* ?' Z2 L) u2 |# j# c2 M( ]
  14. }
复制代码
9 \  L( i/ m% L, D  c
; a$ N; V! c$ ~6 L8 ]8 f
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
3 _! n3 }+ X" _2 `7 g
+ Y" z" C2 U1 A* E  L) d
6 h$ s: _6 d/ R' {0 S0 ~$ c  c" I
  1. while(fend > 0)+ f" e3 ?3 N. k. |8 u9 E9 i
  2. {" n$ H3 R" E( u, b7 @) ~/ ^
  3. memset(picture.data , 0 , sizeof(picture.data));
    ! Q0 P7 T! K0 x9 J6 B
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);0 m/ H$ F) i. S3 x( s3 Y
  5. if(fend >= UDP_FRAME_LEN)
    1 @# g9 r* P5 m* f2 r; N0 }# z; C
  6. {# e# G. U6 F. E, j" F1 L3 T! O
  7. picture.length = UDP_FRAME_LEN;
    : _- p  E/ i7 s9 a# w6 T
  8. picture.fin = 0;
      M; y7 {. A( a; p9 Y
  9. }0 u# I; D3 ]( U( n3 q/ B; B. U
  10. else
    ) }0 x7 P& b9 p- V, M3 X# T) l
  11. {6 X+ A7 b/ Q2 b3 ~. A) S
  12. picture.length = fend;6 v; g2 U5 j( d9 n* e
  13. picture.fin = 1;8 @, U: G4 @9 k, s2 @% D
  14. }8 L& w/ ^6 t* W- h/ R
  15. //printf("sendbytes = %d \n",sendbytes);! U0 o5 d# F, ~1 {6 ^* k
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    ' o* E: A7 Y) @; L  O0 [
  17. if(sendbytes == -1)
    5 m: Z4 T% X9 F; t/ O! b1 ^+ C: {
  18. {
    * i# k5 q6 J$ [/ A* {# j- B
  19. printf("Send Picture Failed!d\n");2 H) h/ W, e) W! p# L  K1 r2 G
  20. return -1;
    % N2 y& a) Q. G8 o# l
  21. }
    7 R1 v2 ~3 u# \: y) g- B( H
  22. else' p. M# e5 `( r8 Q; h/ @
  23. {
    " f; }" T. A8 M, _: f
  24. fend -= UDP_FRAME_LEN;
    % I6 K% e' g  j  s0 ^
  25. }
      A4 Q7 j0 q. i" W6 V1 q& ?9 I
  26. }
复制代码

, ?3 Z, E1 g7 v- _4 I+ l
) d* r& q# f( [; g


# F6 ]6 y5 \' n" P- q& ^3 E) ~$ Z: s: P% k4 u! m5 w
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-4-9 14:02

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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