嵌入式爱好者

查看: 12452|回复: 1

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

[复制链接]

48

主题

55

帖子

306

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
! _& E  C( X: J( `, g/ O) g6 p- ?% w


/ ]; Q/ L$ [0 c% J

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html8 _$ e2 A: a3 M" N

; n9 l* K4 Y) ]7 E$ F6 B

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

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

5 y* _: R* B; F; o% j2 G! \# b) y


* {7 V. c1 `+ ]& w5 L4 X# Z

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

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

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


    % X3 U% p- j  u

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


3 Z( l- Q5 A6 n一、HTTP网页服务器5 U+ ~+ _% C, w

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    " S; _( J& b7 z9 S# D9 {; O6 B, t
  2. {
    3 S& j: o0 N  X4 ?' I; k- v( O
  3.     struct sockaddr_in servaddr;
    0 C8 `* i  u; i. W. k
  4.     socklen_t addrsize = sizeof(struct sockaddr);0 G% _6 r) y! W
  5.     bzero(&servaddr , sizeof(servaddr));/ T* t% T* V  V- ~7 m$ H' R( h
  6.     servaddr.sin_family = AF_INET;& c' [9 B/ w$ `) }
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    & F& U0 M0 J) q, q9 H0 e+ |, v7 J+ `
  8.     servaddr.sin_port = htons(port);1 K6 ^" d( [8 r$ y( \
  9.     int ret;
    1 O. X1 M' O. m4 d. u, |
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    % I: z6 y" ]4 ~! ~% u+ L
  11.         {
    6 Z# Y3 Y6 a) _9 @+ H3 S
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);2 A: [5 H, E. f% L+ B: u
  13.             return -1;
    3 A9 D7 n: H7 H+ d- z9 a
  14.         }! q  N+ {$ k  a" ^% p1 k
  15.     int on = 1;
    . m. l: M/ P. g, S
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    8 O6 M3 K6 W, }+ `
  17.     {" q, T+ k; J. s& d; `
  18.         printf("setsockopt error\n");
    . z% Z! e' \/ |) a; |2 p/ [
  19.     }
    " b5 v4 a& h" d/ i$ @  D
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    + E( f5 Q$ F7 Y8 t0 K0 U
  21.     if(ret == -1)
    9 I. ]. f$ d) Q+ l
  22.     {" E( L* Z- e5 N( h
  23.             printf("Tcp bind faiLED!\n");
    5 t- A; e3 r6 ~0 K7 ]' h
  24.             return -1;1 {9 D1 q) O6 @
  25.     }6 G% e6 R0 }7 U# A' s2 w( p) k
  26.     if(listen(*socket_found , 5) == -1)) b# Q5 |& X. I) U5 f
  27.     {/ N( R" _. a7 l: ~+ [) t
  28.             printf("Listen failed!\n");  W- B, h5 F6 t* h/ j0 ?! o+ M
  29.             return -1;
    - ~7 o. e& ]6 z4 V" _% m
  30.     }
    ! N$ Q& h, u7 v2 J
  31.     return 0;
    ! K: O3 ^/ m& A, K
  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);
    - n7 P. X; b) c$ M$ O) C+ U) L: e" g2 B
  2. void * Thread_TCP_Web_Recv(void *arg)
      k5 d* {; `3 y( J" H& K, P
  3. {
    & s& W2 X6 Z8 c7 K
  4. 。。。8 \& V3 s8 t, Y0 L. _
  5. while(1)1 K0 j+ J4 f" Q9 E+ {
  6. {
    8 M. X1 ~" o8 R0 n
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);  w$ ~) Z4 r& ?0 {: ^: m; r5 G# h
  8.            printf("fd_socket_conn = accept()\n");
    $ G$ O% P- A( Q, Z
  9.     。。。0 o' P5 z- k- Z0 _
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    9 p1 Q. z4 v! h; K. X
  11. }
    ! m1 w9 M# C9 a4 p! i4 M
  12. 。。。0 b" p+ N' g; k% Q- @3 j5 z
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    - b/ @% P1 U3 S6 P, [: ^. i
  2.     pic_tmpbuffer = pic.tmpbuffer;
    ( U! i5 f1 h  E1 E
  3.     pic.tmpbytesused = buff.bytesused;
    + p9 \7 r, p( K: i
  4.     pic_tmpbytesused = pic.tmpbytesused;+ `/ M# Z$ J% F, g$ P
  5.     pthread_cond_broadcast(&pct);2 ~+ ]% X, w5 Y
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;: R: |. ~  Z/ M3 ?/ f
  2. pthread_cond_t pct;! n4 P8 B% R+ Z% Q* D) [/ y# Z
  3. int main(int argc, char* argv[])9 I! f/ R9 m' f
  4. {
    4 H' E, T% [9 h: z3 E
  5. ...+ d) Q8 u. p. W( ~4 t, N
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);4 x+ M: X  E# O! d1 B6 t- Q
  7. pthread_mutex_init(&pmt , NULL);
    4 m( @* W' r% ^: y' o$ G
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    9 ]  p( b3 s+ N2 h0 k# d
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);8 T# j7 D$ a% F9 w
  10. ...
    1 j! g6 e; j4 t' V2 a% Z% J
  11.     while(1). D' H' Y2 L& ?/ ?2 E) p
  12.     {# `, W" }# a/ z/ z% |  D. \
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);" O* }, c: h4 _- W0 R
  14. ...( f# y) u0 H( G
  15.     }" v  ?* \" p9 W1 o
  16. ...
    7 O- ]" `& j, c& Z
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    / B; ^) o! \& z0 y5 A
  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" \; I* _8 l& c$ w
  2.     "Server: MJPG-Streamer/0.2\r\n" \4 {2 R* t1 A* x. R& o
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    ; A; A; N+ Y# N2 g4 n
  4.     "Pragma: no-cache\r\n" \
    $ Z9 W4 V2 L5 Q: k- W8 X3 Z
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    ( U; Y+ R( M, `# i1 D+ v2 Y8 \
  6. #define BOUNDARY "boundarydonotcross"
    ' p9 E" {) v8 F& S( r
  7.     printf("preparing header\n");
    / \+ B, W& Q1 `7 f
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \0 i6 M; ?, ^/ U* l
  9.             "Access-Control-Allow-Origin: *\r\n" \
    ' S7 N+ P. U0 C; \7 Q* m& N
  10.             STD_HEADER \
    6 ~/ K2 {# u. e) |  G; b
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \. b! x8 K9 j2 ^2 \. a0 t' b: U
  12.             "\r\n" \
    2 i" ]4 `, |9 J4 }
  13.             "--" BOUNDARY "\r\n");
    7 q+ ]: o" U( N6 H# W) {7 v
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
    " z5 t6 k( h( L* a
  15.     {
    ( L+ ]7 b( z! x: ^6 g5 k3 t
  16.         free(frame);
    & X) w7 P' C* q( W. U
  17.         return;
    0 c3 u1 x9 x1 B& C# H. Q5 k7 Z
  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" \
    4 l9 }9 f) E* ~2 }
  2.                 "Content-Length: %d\r\n" \
    7 Y' _! V$ F2 g, S- _- H
  3.                 "X-Timestamp: %d.%06d\r\n" \
    : }2 i9 O1 B" b: ^2 ]6 f9 [5 U+ d0 j/ m
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    $ S9 }9 A# L7 j* ^9 S. R( C
  5.         printf("sending intemdiate header\n");
    - d$ Y7 R6 u, y  g
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    " L  B% {' ?  @
  7.             break;* \1 S' k  B: w3 E" `: e, u
  8.         printf("sending frame\n");* |5 q& o6 x1 s0 Y1 n
  9.         if(write(fd, frame, frame_size) < 0)
    ( T4 p+ Y  ?4 u& b
  10.             break;' }6 ?1 x: q; r' g$ T: d( x$ F" T. t
  11.         printf("sending boundary\n");
    " B  O, x: M, ]: j, H
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");7 G/ M: q+ o$ f7 Z
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    5 d1 U1 L! Y0 R0 I; J8 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指令,从客户端使用者角度来看的效果就是网页一直在等待。

2 t8 p8 K* a" U3 x8 z

! O0 }1 {* u# \, z) D9 d. a
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:( S# j3 @6 j6 H. q- h* [
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    - M0 w( R  |2 C: f$ Z4 I9 h% `& w
  2. {
    ( O5 ~% S/ f1 f% d8 c8 O7 i7 }
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    / O# y& d& x4 ~" ^4 V
  4.     if(*socket_found == (~0))
    , U! L$ x' C) j/ l$ k- ^
  5.     {
    % l' z! f& w* ^$ B! [/ t
  6.         printf("Create udp send socket failed!\n");  @- R5 W& U  Z6 a
  7.         return -1;# c) y) w8 w% N& r6 X  Y4 }
  8.     }/ {$ U8 u$ X  ?, j: Z& ~( }
  9.     addr->sin_family = AF_INET;
    4 l( k$ N: J) x( V
  10.     addr->sin_addr.s_addr = inet_addr(ip);* L& M: O) h) X3 F% D. d
  11.     addr->sin_port = htons(port);# p* s  D# t1 V6 L) f9 F+ v
  12.     memset(addr->sin_zero, 0, 8);' U) C: W6 N' s  H6 N1 u% N
  13.     return 0;
    $ d9 I* G8 o6 @3 m% @/ w/ x) a
  14. }
复制代码
4 n" U( L) U% H/ G
- O4 p2 Q. F! ~3 {% \1 k
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:* s, y3 m  _. E5 w( b

" Q/ j( Y! V- K( f7 z2 Y  |! m$ `9 h! C, U# X( j  D. f+ {
  1. while(fend > 0)1 U7 A0 v8 K2 v% _* T3 b# g5 L
  2. {
    2 n; z" [- M9 e
  3. memset(picture.data , 0 , sizeof(picture.data));
    4 p( |/ B, O8 B1 K' }8 `* r
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    + r5 j! \3 P4 W% Z6 ?; N+ d1 a
  5. if(fend >= UDP_FRAME_LEN)
    5 l! z/ h- [1 Y) V3 I1 w3 q
  6. {
    * ]1 H% _: W/ y6 |* A/ ^% o. b
  7. picture.length = UDP_FRAME_LEN;4 o0 Q. x& B" b3 e0 b/ n: J3 D
  8. picture.fin = 0;' R# |/ r* X' F& }: b" n& I* @2 Q* w
  9. }
    ) V+ F/ e) L' t3 |' R
  10. else) I3 T, Q! `3 b7 K0 W" l) F
  11. {
    ! B* l( D) E7 \. S" p
  12. picture.length = fend;! U9 u& n0 M" Z* C
  13. picture.fin = 1;
    1 G! i  k, T, X% X) b7 w
  14. }
    # l- G$ M- G* C2 A
  15. //printf("sendbytes = %d \n",sendbytes);! d. G; @' w  K/ N; P4 P8 j
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
      Z; r/ N4 ^  h7 n& P8 t. k
  17. if(sendbytes == -1)# {/ v5 a% K5 n; l# D  @/ g: |
  18. {  T& X, |) @" u. v" b# {
  19. printf("Send Picture Failed!d\n");6 A7 e( M4 P7 s; j% a
  20. return -1;8 Q, s4 I% s) L/ W. }6 `
  21. }2 e: T$ ?# ^. ]' k2 B& K
  22. else* n7 W7 F& m2 j+ n- d
  23. {; X; E# a+ `6 ?! H' M
  24. fend -= UDP_FRAME_LEN;
    , \+ Y. w$ x3 Y3 }+ i* P  g9 l
  25. }$ z5 m$ C; a2 g% J# c0 R
  26. }
复制代码

( B% I! w$ U8 m. |
1 C: h3 {3 l5 T+ n3 y  w( z& m2 r' `% a

; C1 Y/ {7 z* f& K
1 |8 {9 \; L+ Y5 [
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-8-27 02:54

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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