嵌入式爱好者

查看: 10602|回复: 1

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

[复制链接]

46

主题

53

帖子

296

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
4 W6 n5 S6 E" S0 Q" b/ ^, I' P* _. o7 g* o

, s/ m) S0 i% x* k9 S

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html0 i2 Y% j! F: X2 m" |3 `" i

( k6 ~" G/ g' D

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

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

6 p& Q" O/ u- n


. \4 \  y7 E! M* |5 A8 {

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

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

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


    9 l, }3 e: w6 I" `& P* G9 ~

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

2 b9 f& r. w+ M, B/ Q
一、HTTP网页服务器
& [& c% B! d0 |: J. ]

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)& h3 m- N5 a* u: B% p
  2. {
    5 t4 x3 }1 O5 ]( J
  3.     struct sockaddr_in servaddr;1 e& g9 C2 A7 U( m9 [6 ^( I
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    # v  K2 r/ X' o' [, o- s9 j
  5.     bzero(&servaddr , sizeof(servaddr));1 F+ N% v4 D1 B7 N
  6.     servaddr.sin_family = AF_INET;
    # j2 g1 }0 @3 s6 z; M/ |1 }
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    6 D  L4 H3 M' V8 \/ o
  8.     servaddr.sin_port = htons(port);9 S/ P8 Y9 @- L7 j) M% s* S, J, \
  9.     int ret;
    - U9 e2 A: B: t& k
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    ! A' F, X8 g! u- O5 w
  11.         {
    ) x% H( q7 t" S6 a
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    3 ]8 Y  ~. q" l" ~" V; g7 k
  13.             return -1;9 |: l$ _: x; U% V
  14.         }
    " a2 ]9 `! }, \; @/ P2 ^
  15.     int on = 1;
    0 A. W: Z4 _( u6 p1 J. I! v' E' z1 C3 s
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    , U1 s7 Y2 {, I+ k2 d2 Y: B
  17.     {3 p# i! ]; A+ {0 R
  18.         printf("setsockopt error\n");
    : }  ]! S0 x6 x/ _4 U7 w6 I( _
  19.     }
    1 w9 P4 y* V) `$ H/ Z" b+ F
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);! n* u4 o# Q: R+ a7 b
  21.     if(ret == -1)+ I4 m. {& N4 W  C# |: r
  22.     {
    / i6 U- U6 e& C4 l
  23.             printf("Tcp bind faiLED!\n");
    5 H. f8 w. M9 Z/ j
  24.             return -1;" a! J+ ?) k! D  h' l
  25.     }# S. D* Z0 |8 C3 u. w$ [
  26.     if(listen(*socket_found , 5) == -1)
    - O% d3 D# N  l; j! B! Z
  27.     {
    7 C) k5 R+ o5 I9 Q0 J  f
  28.             printf("Listen failed!\n");
    % v# n1 b- R6 ^/ Z+ u! a1 c
  29.             return -1;$ t( j( O9 R1 P, J
  30.     }
    3 r# U3 B& r4 N/ [1 |" E  H9 A
  31.     return 0;/ i, b6 j! H. U9 [! b" A( S2 ]
  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);9 k- @! F' d  K5 p: s- \, ^
  2. void * Thread_TCP_Web_Recv(void *arg)% f7 N* L9 V% c5 {
  3. {
    % }$ m5 i! s9 C' l; m9 h5 V
  4. 。。。
    # `0 h% x( L( q
  5. while(1)
    ( s6 E3 j: @" x
  6. {
    ! m9 P! v  U+ X7 c8 r/ b7 O
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    & ]* n" \" F4 ?
  8.            printf("fd_socket_conn = accept()\n");' D- c6 p$ t- _7 ?: k# N
  9.     。。。6 n/ ~' T6 o7 K! f
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    # V; D/ e9 y7 C/ Z0 ~" e
  11. }2 x' I1 ~2 Q: `$ p
  12. 。。。
    * n: x0 z. k$ s0 d3 L) |- v
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);) x, ]/ ~9 k2 c1 _
  2.     pic_tmpbuffer = pic.tmpbuffer;
      w" w0 x! v' B5 w+ K: r0 h1 v
  3.     pic.tmpbytesused = buff.bytesused;" i( X" r5 V9 Q! g" q0 d' F
  4.     pic_tmpbytesused = pic.tmpbytesused;
    ; _, A7 V1 I; K2 s5 R9 W
  5.     pthread_cond_broadcast(&pct);
    + n2 M7 U( O: N& \8 @$ W+ U
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;6 l4 |/ E2 |1 M9 ?# L) z1 T
  2. pthread_cond_t pct;
    , e: `  }' e/ p
  3. int main(int argc, char* argv[])
    ! x5 X( `5 X$ t$ ]
  4. {
    ) Q! O1 y8 V& h1 O
  5. ...
    - i: I  C4 o9 y' B  P
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    / H6 g% U$ D. Y! W. O
  7. pthread_mutex_init(&pmt , NULL);+ s$ ~$ e- A) {' D8 I, ?, H  ^& f' B
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);" _) m& ^( k- e3 ^( P
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    8 D, x9 l* I' \$ w, Y& M
  10. ...
    5 x" a; P/ V5 M  a
  11.     while(1)& o! ~1 I' n! h) [8 G
  12.     {
    9 c" N3 o* a2 c7 i" h6 |1 K
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);: d9 X8 k: j1 e: r
  14. ...: N+ Z% p' R1 c* B
  15.     }1 W' g1 Q: W* h+ U' g
  16. ...; I5 b$ Y0 L% P4 P) e6 p% K: J5 R
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    : \+ {# m2 N6 U. N. u
  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" \
    3 c# H2 j8 O( Q4 z' _6 T; G- D
  2.     "Server: MJPG-Streamer/0.2\r\n" \+ A% w) j7 u) q0 G  Z$ U- z/ d
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \9 Q) t# v5 w! E& h9 d5 _: b9 D- C
  4.     "Pragma: no-cache\r\n" \: w- k( K1 A8 A( ?# x( k
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    % D- ]. G5 O# ]
  6. #define BOUNDARY "boundarydonotcross"( z7 z5 M6 a* y. {+ X4 e8 X7 g
  7.     printf("preparing header\n");
    1 k% ?% k& B* u' A: [
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    ' ~  k; P) V: W' u3 h3 I+ P
  9.             "Access-Control-Allow-Origin: *\r\n" \: \3 e2 `: V5 S/ S
  10.             STD_HEADER \
    9 D3 ^) i5 |5 m+ }6 C; Y/ v5 a
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \: o! G. ^* q& o( {" t8 P' z1 v& x
  12.             "\r\n" \, R0 X, v! N$ q5 F5 d! v
  13.             "--" BOUNDARY "\r\n");
    . O9 p5 @" ^% U( L/ C
  14.     if(write(fd, buffer, strlen(buffer)) < 0)& R( }9 n. z5 F" ^4 |
  15.     {
    ! a0 d: y% i  M7 N6 [, E7 k
  16.         free(frame);. s8 s- W4 R9 T0 p' y- {2 s, k
  17.         return;) |9 u# ]  L  A) a
  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" \/ S6 H' |* j6 A
  2.                 "Content-Length: %d\r\n" \/ W2 h0 f) Y% ?! n8 k, D
  3.                 "X-Timestamp: %d.%06d\r\n" \
    " d9 m% T, y3 H9 E% J, E
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);% P& [3 K( H2 q, B
  5.         printf("sending intemdiate header\n");
    * S# K% N* P+ P/ g& F, _% a
  6.         if(write(fd, buffer, strlen(buffer)) < 0)( ~* p5 {: T! w4 G/ b9 u" D
  7.             break;" v) @+ Y$ X; |9 W) `7 z- M6 x
  8.         printf("sending frame\n");
    ) R( v8 q5 ^( m2 i. y4 u: Z) ]
  9.         if(write(fd, frame, frame_size) < 0)' q0 H3 }" t. h) I( t7 X1 j
  10.             break;6 Y$ f6 w8 C: n: y
  11.         printf("sending boundary\n");# y- A5 D+ w4 f; Z( C1 r* B
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");; p# \0 H+ d6 P$ o2 O
  13.         if(write(fd, buffer, strlen(buffer)) < 0)0 v- T4 F1 _& d) Z' X. Y
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


/ c: l$ q6 {* b& Y) y# J


4 J- v* `( p0 {) Q) I2 e. y二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:" _4 Y6 G+ P0 K
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    ! o( t  F. C& W( w6 [/ P, j
  2. {8 ?) \: ~3 x! H1 r
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
      W- P6 U4 J% n1 G% U8 o
  4.     if(*socket_found == (~0))
    " _: z1 n2 J% V6 ?0 r! y
  5.     {
    ) b, U& L0 Z: Y+ E' v, h$ K8 `
  6.         printf("Create udp send socket failed!\n");0 u* L3 L- q# x1 S$ N5 ]5 ?9 e; J
  7.         return -1;9 ]  |& b2 X5 h$ J+ \" a
  8.     }+ R$ Q) W' i5 k$ P+ i* V
  9.     addr->sin_family = AF_INET;
    ' P) h" S/ I- e" Y0 }0 |) M! O
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    8 D' {- c1 ^5 @- T( i
  11.     addr->sin_port = htons(port);
    * N+ s/ F' q9 R
  12.     memset(addr->sin_zero, 0, 8);
    & [; Z' k4 u( y. @
  13.     return 0;5 ^% V2 d) h5 _2 J  W( h, B
  14. }
复制代码
) l* b& X) {! u! O3 O

$ K" `* {; `' ]1 C7 D. R而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
! K9 d2 n; K& f) ]. s! S* p6 n! V7 z

7 N% A& p. K( c
  1. while(fend > 0)
    . W# Q, }* u9 Z9 `% [1 Q
  2. {( M. H1 s1 K5 D- u2 d( z- Z. P
  3. memset(picture.data , 0 , sizeof(picture.data));
    9 k: h6 Y1 J6 c* u) z0 W
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);& `; M' f' W3 [# |
  5. if(fend >= UDP_FRAME_LEN): h7 \( ?7 j7 Z, j1 m" f( b
  6. {
    / B0 l6 m+ K+ W/ \$ r
  7. picture.length = UDP_FRAME_LEN;
    - w6 ~3 s+ ~& q1 y
  8. picture.fin = 0;- t! U) t( @) n% f- q
  9. }
    ' d# X3 P. s9 s$ W# m8 {
  10. else1 e4 @' L" q% o
  11. {
    1 X# r8 e  m' R* M4 J0 }) I
  12. picture.length = fend;8 k. q) t2 l* D% E& A
  13. picture.fin = 1;
    0 a) {2 S/ c0 d7 b
  14. }/ U1 ~; b4 T# ?: r- d
  15. //printf("sendbytes = %d \n",sendbytes);1 T( J/ b# @& A5 h3 n7 Y
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);. O/ z4 k& D. F
  17. if(sendbytes == -1)
    : Z, D7 P' R5 S- [( a2 B! ^$ S4 i# k
  18. {
    1 r, ]# ^: ?. a; @- m  o, v
  19. printf("Send Picture Failed!d\n");2 J0 G. P: x9 J6 i2 h8 e
  20. return -1;
    & T3 R4 @2 f$ R; Y; Y& P
  21. }
    ) d  h3 j( [8 g, J% o/ |& L* D
  22. else
    6 X# J" D2 |3 u9 ~0 C
  23. {$ n" K. {( Z3 B" Q& Y3 Y" s( _
  24. fend -= UDP_FRAME_LEN;
      q# N( B5 a8 p$ Y) J' n
  25. }0 S9 U5 W- c' j6 e
  26. }
复制代码
4 X# J: u4 O2 P: A, H2 w5 t$ d  d
& N0 m! E, G) L) f' e' r


: j' E: e& G3 Y, E+ T! l$ ~9 J5 N' B4 s; }! r# A( _
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-12-30 09:22

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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