嵌入式爱好者

查看: 11052|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
! v, H+ h( K2 r  b! o
; c" e% F, T  v1 W' ^


1 n0 Q( K  x. y3 Q0 J& w; S- A

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html# r% @) ?! C* H) h3 B: ]5 {" b
$ ~  q5 R  O# A( P4 q# J0 F

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

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


/ M& r! s6 t; U' t0 S


6 c1 P" Q$ g( C* k4 B

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

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

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


    + V  b  w+ m" k6 A) k# e

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

4 V+ \9 W& Z  n  T( F# }
一、HTTP网页服务器1 [3 e4 p) V2 s/ A! @0 {

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)! Z1 T( W+ t! _6 S3 I4 k& @( _
  2. {
    - _# U5 p1 i' e9 f% V* X
  3.     struct sockaddr_in servaddr;
    * b6 r2 r4 k/ W3 q; P( ]' P( r
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    & @9 E& ]( x) G& u3 _* t: N3 P! }7 l2 U* l
  5.     bzero(&servaddr , sizeof(servaddr));
    8 q* P- z8 Y8 N. F+ p8 `
  6.     servaddr.sin_family = AF_INET;+ q/ \1 e- r& ^4 x5 I* A( c* {
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    . |; C, q9 ~" U; Z
  8.     servaddr.sin_port = htons(port);
    : {0 @! \7 }1 m' ?
  9.     int ret;' t: N# V3 \0 s
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)* ^& w; H- E9 o: K3 {; @8 A/ F# |
  11.         {, F$ ~5 Z' ~, N' l2 [) W9 n- T
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    ! _( T' t/ S9 J0 x& c5 _7 s2 c8 [* m
  13.             return -1;/ \3 y* C9 `1 j: b0 J
  14.         }* b3 I/ d2 x& N" d
  15.     int on = 1;+ s( Q# T; C. `# |8 V
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    ) r8 n) l9 _: i( I7 H6 w
  17.     {
    . B( z+ N) K- m; C
  18.         printf("setsockopt error\n");/ D; q7 h& r5 f5 R( @/ [" c
  19.     }2 |! r2 Y( Q1 Q5 q- S) ^& Z1 f
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    - s+ b' j+ A. w1 d
  21.     if(ret == -1)5 L- C+ }0 q/ V1 V& L& c. G
  22.     {
    # U; c. O. @! a4 n/ R
  23.             printf("Tcp bind faiLED!\n");1 `. q( e! |6 a% l& _
  24.             return -1;
    + V! H% N1 k* ]7 _4 I# x
  25.     }
    9 v5 {6 c) f4 @
  26.     if(listen(*socket_found , 5) == -1)
    1 d2 |/ [0 U& |. P/ ~
  27.     {
    ( @/ G: k0 `8 v( c& Z2 }
  28.             printf("Listen failed!\n");, y2 Q" H6 V/ e+ |4 x# \( x* D
  29.             return -1;
    ) u( i; o; Z' o! ]' T
  30.     }: s2 c6 y9 z& E4 A  s; [: A/ m$ V
  31.     return 0;2 b& h" a' K( R" ^
  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 r( ?  l3 i  d) j$ B% f
  2. void * Thread_TCP_Web_Recv(void *arg)
    ) n8 Q  E* \8 a0 ^$ W
  3. {  K3 n4 X# w8 L5 D# `% [
  4. 。。。
    2 N2 J& g* f3 V4 }
  5. while(1)1 w& w8 r0 k; ]( P
  6. {$ b3 L1 T/ s; a+ X" L- e1 J% @; A+ _
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    ; R' X5 S/ V. R; I3 }* Z2 `
  8.            printf("fd_socket_conn = accept()\n");
    8 p9 Y, X  g( h9 J! V
  9.     。。。
    2 `7 ]. g8 M9 [* H& Y
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);3 i( w6 ~0 R6 Y
  11. }
    $ K$ k5 T: M% t' x' [# a' p0 d, W
  12. 。。。
    : q4 _& m+ d( Z! w  `* D2 j  |- n
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);$ g$ w. ~/ g5 Z- P
  2.     pic_tmpbuffer = pic.tmpbuffer;
    7 N9 F, C3 A* T0 l2 c2 W' W
  3.     pic.tmpbytesused = buff.bytesused;: |# v1 i7 L8 r1 a, @! e* U
  4.     pic_tmpbytesused = pic.tmpbytesused;" I1 {* i- M2 b  i+ A
  5.     pthread_cond_broadcast(&pct);2 C4 b, ?5 w9 R% m4 v2 l: \
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;' @# g- `, x6 u
  2. pthread_cond_t pct;
    & M. W6 |' G+ Y. P. d
  3. int main(int argc, char* argv[])
    3 u+ k9 G3 F8 S2 e; U0 `6 F
  4. {
    ; j# d7 p3 ^( X6 _
  5. ...
    $ S7 E" ]! A  y" k
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    0 O0 n3 ?5 q$ E% V7 x! u
  7. pthread_mutex_init(&pmt , NULL);' U: I" n8 T1 D; n, t! t1 X1 m/ z
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);6 J/ ]/ s/ o1 H! @+ @
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    2 d! d& @4 u! \$ ~1 W; E& j
  10. ...
    1 @; t! N& j4 i% I' ]
  11.     while(1)
    - N, N0 @2 z* w
  12.     {
    . ]5 M$ k7 ?3 {1 X$ x4 C
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    5 X- I( G9 F+ t- g' E, N0 u
  14. ...
    . D  G  t% m" q9 \* p' O, V6 J
  15.     }' D7 ?  Q" h/ ]! S; E8 ?% H" x
  16. ...
    ; n7 u) r6 i# ^! K* |
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">! q( M, x) p' D/ ]6 h6 f( i
  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" \
    ! T& `$ _% Q' K
  2.     "Server: MJPG-Streamer/0.2\r\n" \) ?* a, A+ M/ m; M; `% ~, ]
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \$ L6 X; ]; H. d$ j3 v" [) P' w
  4.     "Pragma: no-cache\r\n" \5 @2 y4 W! c" U- T6 R5 O
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    2 M1 X. ^4 _0 c9 [  y3 X' ^# c6 v
  6. #define BOUNDARY "boundarydonotcross"
    ! J& n/ j4 G! e5 C2 b2 X$ q
  7.     printf("preparing header\n");
    : \( |! h4 W$ d4 v
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    . f4 ^8 o9 V# U' F! I
  9.             "Access-Control-Allow-Origin: *\r\n" \
    1 J/ i; e$ b$ {$ K
  10.             STD_HEADER \
    7 k4 l, e, x2 i
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \- F" ?7 w; w$ c: L0 |  n+ k
  12.             "\r\n" \
    ! T4 n% @1 q8 ]& E
  13.             "--" BOUNDARY "\r\n");3 A8 W" {, i" n9 V
  14.     if(write(fd, buffer, strlen(buffer)) < 0)$ }6 x; M4 r0 L
  15.     {
    9 `  C1 H# l* s- P$ h0 ^5 T- }
  16.         free(frame);+ l- a" h9 F# e) g' X
  17.         return;
    : k( q8 u, I' z; R8 L( @
  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" \
    : g* ^8 a6 V0 T2 `$ L, Q
  2.                 "Content-Length: %d\r\n" \& J" A. S1 y+ B5 r& A
  3.                 "X-Timestamp: %d.%06d\r\n" \
    5 q6 z% E% U: c6 k6 F6 a
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);! _, @: |1 k" a  {+ L& R' ]
  5.         printf("sending intemdiate header\n");# o! {0 [( Z$ V7 K
  6.         if(write(fd, buffer, strlen(buffer)) < 0)) ^$ a% m3 z- \; m+ m. q
  7.             break;
    ) O8 z' y5 n/ g: m2 B( b5 F5 U- U' M
  8.         printf("sending frame\n");
    6 s/ h2 j' g. {1 R* @- q( L/ M
  9.         if(write(fd, frame, frame_size) < 0)
    / @  y: J& T8 |) F
  10.             break;
    / ?0 I  l8 t4 f( W) j( Q
  11.         printf("sending boundary\n");
    4 U; j  _8 k' K8 _
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");, ?$ j- i( |, X* w$ I
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    9 `# c: c$ ?& `* q$ F9 k3 X5 }" e
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


) E9 X2 t4 i- U


, B8 h) Q1 {) a$ \; V3 @二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
6 n% O2 X& A% I  k
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
      y6 Y6 C, [0 }2 i
  2. {
    # X2 Q5 n6 a2 k% j* C
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    : C& ^( f( d- }' U+ G1 O
  4.     if(*socket_found == (~0))2 U; @# }  y$ n* U5 T7 Y+ r
  5.     {
    + z" S$ y% k1 G- F$ r! Z
  6.         printf("Create udp send socket failed!\n");7 [; @' ^) [* ]& z4 O- j
  7.         return -1;/ X  _$ F% U* b: T# _+ R3 b/ ?7 W8 A9 S
  8.     }8 ^- e* r8 F! H. C& L
  9.     addr->sin_family = AF_INET;
    ' h, H3 {% p9 S! b
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    + H7 W% Q: w! j: |* d5 o. J
  11.     addr->sin_port = htons(port);: ^; [/ E- u- c6 g  L
  12.     memset(addr->sin_zero, 0, 8);
    . c. j6 u1 ]  X
  13.     return 0;) Y: y8 V5 @+ S3 ?
  14. }
复制代码
" P9 ~, v7 r3 y+ u% t& q2 E# u
+ J$ T( `+ k# S1 n/ Y
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:+ u2 ?& L( e2 P" U" N
5 ?. o/ z9 d4 t
% K5 X% Y1 z7 Y+ J' ~( X+ ?0 W
  1. while(fend > 0)0 s" b( Y% `" h% C
  2. {
    4 v! p, }4 |' B* B1 q
  3. memset(picture.data , 0 , sizeof(picture.data));$ T& H4 W0 o- O  M& ?. z
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    # }$ X6 p2 r0 u% Y% Z$ ]
  5. if(fend >= UDP_FRAME_LEN)
    1 P4 J; P" k; o. O7 l
  6. {
    * v/ e; P9 q) i3 O$ F: F5 F
  7. picture.length = UDP_FRAME_LEN;
    5 M  V7 f  U: n6 \: R* a3 a8 f
  8. picture.fin = 0;
    + \7 q) e3 j4 Y6 d; m
  9. }, N, H+ i+ x( v. g4 k% h
  10. else: q  W+ C  x& o$ Q3 c" p( I
  11. {
    ( E" }' o: @/ u& w0 g1 L
  12. picture.length = fend;! I9 O( T1 y1 \$ w
  13. picture.fin = 1;2 {: {( H5 I( ~2 U: e9 k
  14. }# A! T8 Y5 H1 Y. J' p" g4 p
  15. //printf("sendbytes = %d \n",sendbytes);3 M6 g& d0 F* f* {
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);/ d1 t& y1 ]7 _
  17. if(sendbytes == -1)
    + k, N2 L: ~' q6 n/ M: B4 ^- u
  18. {
    2 W. H/ X4 q8 ^& L. T. @
  19. printf("Send Picture Failed!d\n");
    $ z0 T0 ]. Q- e" w- E( ^
  20. return -1;
    ' ]$ _( j  N3 d3 d
  21. }0 S1 v, n' E/ L4 F( ~. e! w
  22. else
    & E% @' D( |' |6 {
  23. {
    0 l% u# B) }& g( J
  24. fend -= UDP_FRAME_LEN;
    7 }: K" h8 ~8 y5 j& a% `4 n
  25. }+ @- Z- M, |6 [
  26. }
复制代码

7 V  p; R; n: n- e' q6 i$ v
) G1 l" ^7 j' {% r- @2 y( [, I

; p+ {: T. @; N1 h. I0 O0 k# ~5 a

, R' c- b% O; @: Q1 S' LiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-2-8 17:19

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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