嵌入式爱好者

查看: 11973|回复: 1

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

[复制链接]

48

主题

55

帖子

305

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 . N# m* h! h4 [3 G8 a8 v# L

1 I4 P7 ~  C. t  J, W3 J


6 A1 M' N' h5 Q! k2 l

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
1 Z3 }! B: r# Z, t5 f  K! ]" p) Z

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

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

7 k( C3 V$ f0 Q; R3 D) g4 U+ @6 M8 ]

/ b* o- C  }# v& k! @- x- ]) u. r

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

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

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

    # b; O6 ?) @8 r' |) s2 u

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

1 e8 ]2 @: }( z; \' z; E! j* P
一、HTTP网页服务器/ T0 L2 j% Z( p

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)6 ]8 v- W' i6 a* u
  2. {
    3 i3 ?/ N: W4 n2 P
  3.     struct sockaddr_in servaddr;" j, Z, C# y$ a6 b
  4.     socklen_t addrsize = sizeof(struct sockaddr);- ]( E  W; S, V6 T3 I2 ~5 u1 g
  5.     bzero(&servaddr , sizeof(servaddr));1 S0 o8 s* r6 j# Q
  6.     servaddr.sin_family = AF_INET;
    & P( P9 }. H4 B
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);) [. r& b0 X; W! W3 P4 ~0 Q( E
  8.     servaddr.sin_port = htons(port);0 k+ A: C$ k0 ?0 C% h( z+ O" d
  9.     int ret;% }# E) z' p! j/ z% Y' ?
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)! _5 u; w" J+ i0 f, y( w
  11.         {9 w) n5 ?, y4 Z4 R% N; S" Y
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    $ T8 C/ J2 |4 G" l+ P
  13.             return -1;
    9 p- L7 {7 X: z# R- `9 r
  14.         }# Q! \) l0 i. \; Q5 B- {, W
  15.     int on = 1;
    6 |( b2 \. J+ t8 g
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
      c/ K' u4 a0 d/ G6 V( {- \
  17.     {
    8 V( n8 H: S7 h+ f# d
  18.         printf("setsockopt error\n");
    1 Z) b' J+ \  w2 h1 k1 |
  19.     }
    + J, T) C2 S0 N, ]+ ?
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    8 b  ^5 L# J7 l* e6 T
  21.     if(ret == -1)
      L4 g) e, E+ |9 ]) p& e1 s+ p3 D
  22.     {% E& o8 n4 Q. K. p& y
  23.             printf("Tcp bind faiLED!\n");  ]* _1 y) B) H7 }! ~
  24.             return -1;0 b8 `& M% b3 v
  25.     }
    : O8 j; x! A: k/ }1 d/ ~
  26.     if(listen(*socket_found , 5) == -1)( C# L2 z3 E0 D2 v" e: C3 r( Q) N
  27.     {. H* X2 q7 P, e
  28.             printf("Listen failed!\n");
    , W4 j' @: @2 z5 D" z
  29.             return -1;
    . X0 P7 `; e0 G4 Z! c
  30.     }& K/ e; n* q: \! g6 S( O8 w8 ]
  31.     return 0;5 S/ d' b3 o5 z4 Y/ a
  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);
    8 [& U% H3 l5 ^
  2. void * Thread_TCP_Web_Recv(void *arg)2 W! v- `% i( [% X" Z& a" x
  3. {2 }7 l) i1 @9 v! Y
  4. 。。。
    * }, E- [4 T5 j3 h
  5. while(1)7 d" D: M$ {7 i% Y
  6. {
    , V; _+ r9 J1 z
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    7 j% J: h" m! j, Q
  8.            printf("fd_socket_conn = accept()\n");
    4 \( W" y. }5 ]2 L( V0 ]
  9.     。。。. m4 q& \; h, X( j9 _1 e/ N
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    0 _  x% N+ M. {0 k& m# o
  11. }% X4 C! q' |& M( Z+ R
  12. 。。。
    - m( |/ r) G* e) U1 \& h4 p8 L* G
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);& b4 ]7 P4 L; ]
  2.     pic_tmpbuffer = pic.tmpbuffer;6 q& a$ F) F! p# i1 c* k) ?
  3.     pic.tmpbytesused = buff.bytesused;5 t! j, m( [& [: H
  4.     pic_tmpbytesused = pic.tmpbytesused;( I+ C+ y2 d! U# ^, c2 L
  5.     pthread_cond_broadcast(&pct);. _) x: ]) q! S' g; L
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;$ k: [) m& `1 W
  2. pthread_cond_t pct;
    * B$ q0 n9 ~6 T" s
  3. int main(int argc, char* argv[])
    9 e- V- f6 C8 M8 s8 m  V
  4. {' j% u1 {' X7 Z. I" s* j
  5. ...
    3 x( _" W( {/ |& ]/ ?
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    ; D) x4 |/ k$ a* D
  7. pthread_mutex_init(&pmt , NULL);
    ) V  x  [4 J) M# A. p
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    - z1 n' ^! M& W8 c$ j
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);3 v6 ~4 i9 \1 K* s
  10. ...
      q" }7 C  c, ]' g$ N
  11.     while(1)
    9 w$ p8 G' v$ ?
  12.     {' u& b8 D; r$ V9 r# m- H
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    6 [8 g* F! W9 m1 Z3 D. S
  14. ...) V# W" }( d) h( o) }
  15.     }' X( y; w2 E4 k4 O' t4 M* A
  16. ...' w& w- T7 {6 V1 n( ^
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    ( M5 Z+ x- d' X0 e; Y4 h! s! s
  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" \2 H8 A6 f  W" Z$ ~: @( q, _; L5 U
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    / ?+ a& w2 ?5 r/ a; H- p2 J
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    - p( E& g$ x' t; \# P3 m& G1 b! X
  4.     "Pragma: no-cache\r\n" \2 d# r+ _. j0 q7 Y% N
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    1 f/ U2 o, u: g$ V
  6. #define BOUNDARY "boundarydonotcross"
    2 O" o6 d& i1 Z9 j
  7.     printf("preparing header\n");
      `7 S7 ~- }* v4 a$ _! K
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \# t& W/ x' [$ ]3 M. i% h" D% s
  9.             "Access-Control-Allow-Origin: *\r\n" \
    % U3 s. s/ U) [% K% M
  10.             STD_HEADER \
    2 P, j7 `; B' [5 ?& ]
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \+ P7 p' Q" ]0 |+ `, @7 U
  12.             "\r\n" \
    ) ~" n2 h  P: U3 h: O! O6 y
  13.             "--" BOUNDARY "\r\n");# u  d; M! @# K' \. y: k
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
    $ ?7 _/ Z! p0 ]1 M
  15.     {
    ' X4 n) N8 t9 p& R# `0 w
  16.         free(frame);
    % v* w! X/ t( P/ w7 q
  17.         return;
    & l! v! T, ?1 S
  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" \
    9 q0 N) l/ I2 `0 i. {/ o$ c. C! {9 {
  2.                 "Content-Length: %d\r\n" \) q3 e2 Y/ y" e. t2 c" F
  3.                 "X-Timestamp: %d.%06d\r\n" \7 a! R8 B' H$ V$ q+ p4 K6 x
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    5 ~+ i( P1 b0 \" D4 g
  5.         printf("sending intemdiate header\n");
    % @' ^9 g$ M6 Y1 k; a4 @
  6.         if(write(fd, buffer, strlen(buffer)) < 0)- h9 |3 g; B6 `3 I2 e! s
  7.             break;6 t4 ^9 a' _/ W6 ^. C
  8.         printf("sending frame\n");3 |" d( N2 S" a* R' N* b4 u
  9.         if(write(fd, frame, frame_size) < 0)
    8 j* T. L" w# d
  10.             break;2 ~1 O" Z$ Y( r4 i/ X
  11.         printf("sending boundary\n");
    ; I4 F( M: Z( e
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");/ q/ P2 j! U2 b  k. x2 i
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    , a2 t+ R, F  x7 o  q5 Z/ @$ 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指令,从客户端使用者角度来看的效果就是网页一直在等待。


+ F/ \! x0 T9 j  f' m


) s! i4 T. v. n0 k二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
$ t4 D( I2 F5 o/ \$ {* L. R& y) c
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    5 v; h- D. A  u5 f, B
  2. {
    4 m5 x& b/ J8 j% z: a0 I) L
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    1 w- \* P  H1 ^/ e8 _% U( U: h
  4.     if(*socket_found == (~0))" _( f3 s  Z& ~. D2 N  v, R
  5.     {  m; C7 _3 T1 N) c; Z1 Z$ q
  6.         printf("Create udp send socket failed!\n");
    5 K/ D3 j6 D1 ~9 a: u, z9 N
  7.         return -1;( ~6 ]4 `+ Y2 p6 j. O
  8.     }
    2 e, j! I2 ^) s2 q% O1 i6 N
  9.     addr->sin_family = AF_INET;( G7 y1 n1 I7 w9 s* K- d! h( H
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    . {9 D5 |7 r3 {+ o+ L
  11.     addr->sin_port = htons(port);  c& W2 h0 O. x) g7 {$ e
  12.     memset(addr->sin_zero, 0, 8);
    ( b5 |3 `( Q' v4 i" k1 P
  13.     return 0;) D6 ~% p3 \3 D
  14. }
复制代码
1 g6 V7 l& r* K6 ?6 J6 K& A* m  c

) u. o0 q* W1 s7 w而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
0 w/ G7 Y: k, t0 f* A1 _" ~! {
, M9 z, g- v; t+ z1 L" q
8 }4 \6 s# p) d, u- K7 O
  1. while(fend > 0)9 l" F; g+ M5 n+ Y
  2. {; J5 B' I7 V$ v2 P" {
  3. memset(picture.data , 0 , sizeof(picture.data));: C! T% y1 e- I2 a2 R& S4 o+ _
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);) n. |2 k* ]1 b1 y$ l- \) {
  5. if(fend >= UDP_FRAME_LEN)
      M# _* V9 Z0 u3 x# b
  6. {( T# ^3 k; Q/ G  O5 H4 I2 y  t
  7. picture.length = UDP_FRAME_LEN;
    , X1 a) ?# Y: C( I
  8. picture.fin = 0;( v( J* ^9 ?7 S& r
  9. }& X# ^: Q* K2 J8 U) O8 Y. p
  10. else
    / k, h2 a- J' _# s& T) Z8 n
  11. {
    ; Q; Y; V( l% z
  12. picture.length = fend;* x6 |4 A, ]$ {# K, W# a
  13. picture.fin = 1;
    # I' ^8 e8 m! [
  14. }
    ) ^" r: f# q2 h) D
  15. //printf("sendbytes = %d \n",sendbytes);
    * f4 x7 R- W0 G) q! p6 u
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);2 r7 e/ M: R) R) m/ ]4 e
  17. if(sendbytes == -1)* {- H1 n. V) R3 d. Q
  18. {8 k; M6 \) T, y
  19. printf("Send Picture Failed!d\n");
    2 a6 f/ n% {  V
  20. return -1;
    : C$ O4 s& ?9 L& Z7 ~( G0 L
  21. }" w8 a0 B. `! t; _
  22. else. _8 M4 k" j4 P, t
  23. {% [, L% P1 b; j, |3 \7 w
  24. fend -= UDP_FRAME_LEN;
    " c! D' }/ |) z
  25. }
    ! L6 c4 M1 `: M4 N; P% {( l
  26. }
复制代码
" K* H9 P1 ^3 T- a0 |

  g! T3 [( z6 v! T" L

. r: l  P/ v- f! \

+ N7 a) u9 c1 HiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-5-1 20:42

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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