嵌入式爱好者

查看: 11140|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 3 D* N8 t% S8 y( `) Z* |

4 H3 `. u2 g. U0 h

8 C+ w, V/ ]# e* E6 W

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html, Y6 L; f6 b+ l
' B/ S& x: B# ^: _" d

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

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


& g, x+ }  `8 {1 E

* \) f& N+ E" O% d4 U- H

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

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

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

    8 p# m4 [! j; Q& G# p

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

- N) G6 T  w5 e# z
一、HTTP网页服务器3 b2 Z, o4 [& v- D1 T" L! k

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)) a& a/ t. [) ~4 [( H3 `# J
  2. {! p% O1 X# y) F/ p
  3.     struct sockaddr_in servaddr;
    3 u9 p' _4 z  {' U/ c3 h
  4.     socklen_t addrsize = sizeof(struct sockaddr);7 K* [0 [9 ^* r% v$ `& h
  5.     bzero(&servaddr , sizeof(servaddr));2 _8 S0 d# E  m( ~
  6.     servaddr.sin_family = AF_INET;
    ) q/ Y% e$ c/ ~+ H' p2 _
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    ) |4 V' o( ]- t* {) v3 ]# ?
  8.     servaddr.sin_port = htons(port);; W+ V" \+ r3 ]" T% o4 ~
  9.     int ret;. c% M" g2 K3 b: G6 U, a
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    - V) Z1 |9 v$ y0 W7 U9 o; G0 ~
  11.         {
    . R1 F; p! \: Z! R7 K/ q( _6 x' _
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);# p4 o* j5 ~) x- R# }6 ]  A% W  Z
  13.             return -1;- R' j; G* y2 C" o+ g
  14.         }
    7 [' Z+ I9 @" N7 S/ P. K+ l
  15.     int on = 1;
    ( U) |8 v7 r1 q4 m2 D3 w9 _  a% t
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    / Z* U1 I1 K. \* }! _5 D
  17.     {( s4 L1 k2 T% g
  18.         printf("setsockopt error\n");1 V" O7 I2 d- G
  19.     }2 ]! {# f, d, O1 N& a& s  r" d
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);1 F- ]; r1 b0 \' |0 X/ [; G
  21.     if(ret == -1)8 G# R4 i. @) l1 @
  22.     {/ X0 a0 \! t( P+ L" q9 Q: Y
  23.             printf("Tcp bind faiLED!\n");: }( a) ]3 o' Q5 x9 \( h
  24.             return -1;# b8 Q0 t  |. Z* D7 o: f
  25.     }6 ?% S* y: `2 v. `$ ^
  26.     if(listen(*socket_found , 5) == -1)8 E# \+ e$ x  I8 m, y# |9 p
  27.     {
    : K% |9 l1 I5 m" ~; L1 H
  28.             printf("Listen failed!\n");+ ?7 r1 m7 @0 W# w; c& @' \# q
  29.             return -1;+ f( ]+ \" ^3 e9 B$ B
  30.     }
    " {% \* H# `" g# E$ U* f. m( N
  31.     return 0;
    % l3 \( r& |; J$ 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);
    # T* O, r! [5 a" f
  2. void * Thread_TCP_Web_Recv(void *arg), y% F4 e8 z0 z
  3. {9 \7 H8 j! f  |0 t
  4. 。。。8 ~7 l1 s6 s; m' U. U: R7 f6 y7 `
  5. while(1)  r6 `' M: Z+ g) W8 m8 b
  6. {3 S/ Z6 R3 M# ^9 H+ u* Q) S
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    3 ?' ^% P% B* Y  h& ?0 |3 X
  8.            printf("fd_socket_conn = accept()\n");' P8 T4 s  F  b8 j% h- A
  9.     。。。2 O3 Z" ?2 U( ]# m1 u5 }" i/ j
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);8 ^+ |# C* j) K- i$ `$ ~8 ?
  11. }: ]# v8 O1 j5 @) c! B" G
  12. 。。。
    ) U# l+ A4 e9 F- Z$ ]1 p0 Z& \
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);) J$ l& u# U/ E0 A' w
  2.     pic_tmpbuffer = pic.tmpbuffer;# r% }8 N- ]" ]
  3.     pic.tmpbytesused = buff.bytesused;
    4 m; M% [  W1 T( |7 ?9 m) T
  4.     pic_tmpbytesused = pic.tmpbytesused;
    ( g$ ]3 f& j+ O- J6 c
  5.     pthread_cond_broadcast(&pct);
    8 }. j  {* \* v$ `4 t! L
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    6 i' @% V5 I! L: o% h1 Y- ?0 s0 M
  2. pthread_cond_t pct;
    5 k# D% ^- o, x5 ]' d6 [- E+ z3 K
  3. int main(int argc, char* argv[])# \5 N' u7 l8 i- ]- [3 }, Q4 P
  4. {/ R# e, O) ^) N4 }# H6 s
  5. ...! e7 F" ]5 k. N
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);9 {0 k# _0 p/ P  w
  7. pthread_mutex_init(&pmt , NULL);1 r- R6 K  e0 `: A( p" q5 h+ N
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    % L4 V2 V% y" P
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    ( G$ {, |0 Z+ `9 o$ N& ^
  10. ...4 b- E, p5 f( G9 s4 H% U
  11.     while(1)
    - `) s. H2 U3 B- P0 [" b- g
  12.     {
    ( J  R% B0 n9 M% I
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);* V" p1 [! B! g1 ?+ Q) d9 z% r1 [
  14. ...) p' W$ o3 K0 {2 \1 u
  15.     }4 r$ A/ p3 H4 Z7 Z7 J
  16. ...
    3 b  r1 x; H. q, J$ i
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">1 f9 m; S9 o& R/ Z* S; `0 w* 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" \# }7 y1 Y) v  `2 b: V- Y
  2.     "Server: MJPG-Streamer/0.2\r\n" \- X3 k; T9 c5 g# u
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    / `+ N( [- {3 u) z
  4.     "Pragma: no-cache\r\n" \6 y: c6 I7 ]7 \/ C. F9 F% L
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    ( T9 i5 A% _" W7 s
  6. #define BOUNDARY "boundarydonotcross"
    3 k! ]' H& |+ e$ w' K0 d
  7.     printf("preparing header\n");
    ) @0 ?  [) W, b- @
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    ; L" L3 C% w0 v. S
  9.             "Access-Control-Allow-Origin: *\r\n" \
    : O/ _  H$ h" s& C- |8 E4 g" u
  10.             STD_HEADER \
    / A: @3 ]% T* Y; u2 R
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    8 F% A& j( e/ c6 X+ b0 p
  12.             "\r\n" \5 A# c$ |  ?2 s
  13.             "--" BOUNDARY "\r\n");
    # y& p4 T  z* m3 w% b
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
    2 V9 ?2 l/ _/ L7 C9 R7 A8 V8 r
  15.     {
    : ?+ f' v6 L' I: u& G
  16.         free(frame);
    * S7 c2 j$ @/ F5 q4 f6 a; A  P0 |
  17.         return;; S7 U6 e9 ^$ D  b& b. p. 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" \, v- q0 x/ g+ z. {/ L
  2.                 "Content-Length: %d\r\n" \
    5 K) k! ?* r5 \# ]! f# [
  3.                 "X-Timestamp: %d.%06d\r\n" \' R" A; u8 b* Z' X6 H
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    ! ~) r: {  [4 |. x
  5.         printf("sending intemdiate header\n");, }7 L2 O6 ~: C/ b3 ]7 F: v. k& G
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    : t% s& n( c9 {- p. j" g
  7.             break;% Z. D+ \4 G; h0 o: R
  8.         printf("sending frame\n");
    4 A  b/ D- ]' j5 a0 |
  9.         if(write(fd, frame, frame_size) < 0)3 T1 t& A' [0 u6 J0 ~, H
  10.             break;
    + y) q6 U* H/ M* B4 i  }3 }
  11.         printf("sending boundary\n");
      X" A2 d8 M! V9 C0 |5 H
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    6 \; P7 T" r8 O# p* D8 ]4 l9 S- b
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    7 T, C8 h) c" o0 `, Y1 p5 \0 U
  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" q8 O, S  j" R) P

, T$ E/ ]! }% c' ]( l. v2 a, y5 [
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
8 K& m4 S( P: J4 x9 f5 r' y0 o4 \8 K9 f6 n
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    ) J( a$ B; u5 Z6 M
  2. {
    " x: {: Y( n' `8 u
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);  k3 Z3 m. d1 N1 L5 f3 m3 v, M
  4.     if(*socket_found == (~0))+ O. K1 u) ^1 F- U
  5.     {& E2 B$ B' X3 O5 S
  6.         printf("Create udp send socket failed!\n");
    5 d( p5 `  k& K, L: C$ m
  7.         return -1;! I# ?! S+ C5 g/ v
  8.     }$ C! {; Q. I& `; o
  9.     addr->sin_family = AF_INET;# G# H/ u4 x! i) o2 q: D9 G
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    4 P9 @) b1 n6 J" q5 P- t+ W2 N$ F
  11.     addr->sin_port = htons(port);
    ; L; w! B$ t5 Y/ _1 B. F
  12.     memset(addr->sin_zero, 0, 8);
    + l/ P8 y3 J$ f( j+ T2 r( }
  13.     return 0;4 O7 O0 b" v. `& n# f/ }' o
  14. }
复制代码
& H; r7 Z# D3 K6 o* z3 \
1 F) g- S: h1 N+ e" z
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
6 c3 p. S* x# g' T/ E  C9 T- ~. X+ |8 M/ E7 w& D5 R* v3 g7 W

0 x5 Q2 L0 u* v1 l
  1. while(fend > 0)+ ~2 {0 M3 `0 s
  2. {
    ! ^: J! X% @! t
  3. memset(picture.data , 0 , sizeof(picture.data));
    & H3 Y# Q' Y% M  |; T
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    5 ~7 V( c5 U; B4 ^1 @6 W! h
  5. if(fend >= UDP_FRAME_LEN)
    6 D' x+ U3 T# Y/ M3 Q" ^, }
  6. {
    1 s% w, m' u2 V7 `, i# D+ b
  7. picture.length = UDP_FRAME_LEN;' m2 Q$ \7 ^) a* y4 k; B* T
  8. picture.fin = 0;5 A( @! G9 J/ r: R
  9. }- q! l  Z4 ^* u% Z% [# f" `7 K
  10. else7 r! o4 I' ~1 {
  11. {
    4 P0 F. L1 c( Q1 _3 r7 d) ?( B/ Y
  12. picture.length = fend;
    ! K6 @4 I# ?8 s$ j
  13. picture.fin = 1;; b+ c0 O% P! ], U+ y6 h) \6 X- s: {
  14. }9 D' l- T7 S; E) ~+ g' W
  15. //printf("sendbytes = %d \n",sendbytes);
    % \6 U5 Z0 [: e
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    : V0 F' \6 J- o' n+ z
  17. if(sendbytes == -1)# [" W7 Q+ e/ {: U- S
  18. {% X; D2 A. w, C  U" b' k* j
  19. printf("Send Picture Failed!d\n");
      ~& ~+ z9 C  L! S
  20. return -1;1 g( [5 G2 {  e3 M
  21. }* J, W, F. u# M4 m2 `% `
  22. else
    0 D+ o9 r8 C7 {3 K$ p3 j
  23. {9 |# [2 v1 M# Y
  24. fend -= UDP_FRAME_LEN;
      g3 V; o& ~+ U% Z7 t; y2 |! u
  25. }
    0 G2 D9 ?/ @: B0 ^( z
  26. }
复制代码
$ F0 H. {: ?% C) K, h  M/ t0 ~  n
9 [' b' \6 d  N7 l, \


; x/ h2 i9 T0 x7 \3 ?/ r8 F/ Z( ^# {5 I& P3 x% P; Z8 @
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-2-15 23:09

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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