嵌入式爱好者

查看: 11047|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 8 ]2 L  [0 d3 u
! J) _6 m% _0 B* T% W& \


  t+ w" S8 O/ N* k- J# n+ }

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
  t3 ^4 z* p( E: Y0 u% R: F; |" \4 c: H/ t1 s* C

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

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

* W5 Y0 x7 V2 K3 o5 H1 w6 ?1 s


( Y( u. R. t0 p9 ^" c2 L

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

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

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


    & [& M, z) ^# ]

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

, W! \% l  l; K, B# K, d3 ?
一、HTTP网页服务器/ }$ J7 }  d' T7 l, ]$ }: g: \

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    * d- s2 ?6 h) @2 X" u" G) }
  2. {+ f. B# o6 a" q# b
  3.     struct sockaddr_in servaddr;
    4 l8 F+ K$ N5 h, J/ z9 U% X
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    ; w/ `) ^( d/ U; F8 b* E6 A2 L
  5.     bzero(&servaddr , sizeof(servaddr));+ K0 [$ o$ J8 A2 i
  6.     servaddr.sin_family = AF_INET;2 o2 s( ?! a, d5 c- m0 J8 R
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);7 g3 {0 Z# x1 B; o8 ]
  8.     servaddr.sin_port = htons(port);" a  @4 l$ |3 ~, o. g4 R, k% e9 z9 |, a
  9.     int ret;( Q$ [& r5 A- j( b: t2 h$ f
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    3 {' N6 |+ N% _3 ~8 G
  11.         {: {/ c) N2 P8 N1 h7 u
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    " s( c  ]. L% O9 P% o" H" g
  13.             return -1;
    4 Z# S0 n; b& P+ [, m, `+ U
  14.         }
    5 t6 u' _/ B' i1 {6 T% {
  15.     int on = 1;
    ; c! }4 x, o& B, `) N  E; H
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)! v" m9 R% Q& Q# `
  17.     {
    - a. f0 b* O4 G# s' N
  18.         printf("setsockopt error\n");  W5 n" u3 J  y' r: W
  19.     }
    ; T' i& b, o! R% ^
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    2 {/ u+ x: U  u% P2 H+ z
  21.     if(ret == -1)' |5 {7 P1 m' k9 K% h' I) T
  22.     {
    ; D6 \- x! i7 S6 k
  23.             printf("Tcp bind faiLED!\n");
    % v1 a: R' x8 _
  24.             return -1;
    . C3 x) z. J7 L  u
  25.     }
    1 x- C  E# t% s6 @' |/ `3 q
  26.     if(listen(*socket_found , 5) == -1)% h0 f( L/ t: \* z
  27.     {
    - G9 g+ t! N4 Z, r3 u1 M
  28.             printf("Listen failed!\n");& A% E) u$ R1 {' T2 L1 d
  29.             return -1;0 P8 ]8 |" z  O% Q
  30.     }% `' d; m7 K7 {
  31.     return 0;, ~2 Z' A; p9 X) E
  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 ?- \' e9 F8 o& O" h- q& y6 a
  2. void * Thread_TCP_Web_Recv(void *arg)
    3 \6 ]9 f( F) v: w. A7 I+ y5 c2 u
  3. {
    ( J0 `; a# D8 J* M* ^8 J
  4. 。。。
    5 k, L4 q7 N* e/ m7 R: ~0 W2 ^
  5. while(1)
    & t3 f7 D& \& B4 S4 X
  6. {
    . L# ]. k; m2 s
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);7 f+ T$ `7 Q$ n# G+ a+ K
  8.            printf("fd_socket_conn = accept()\n");
    4 X  Y6 Y- l0 X
  9.     。。。
    2 Q' V* r, j5 c6 x
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    6 N+ x5 ]1 U  P! w. ]# _
  11. }
    . b1 B9 ^, D* x3 N2 V4 q* p
  12. 。。。+ O# y8 R5 m) S- Z% e  e
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);- T, r2 Z( }$ M
  2.     pic_tmpbuffer = pic.tmpbuffer;4 k0 C! p: t; }. E1 Y: ^# P+ d% c8 j
  3.     pic.tmpbytesused = buff.bytesused;* C7 R+ f6 r4 ^" F1 h% R
  4.     pic_tmpbytesused = pic.tmpbytesused;
    + v, a' `0 c. G. Z
  5.     pthread_cond_broadcast(&pct);6 l0 q. U& ?1 J- ?6 X& e: j
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;5 i$ f, X! _, B! H
  2. pthread_cond_t pct;( N6 M5 X- P  T7 l9 I
  3. int main(int argc, char* argv[])) V9 v: n- ~) e6 c7 D: b
  4. {+ L$ H# r, _6 N4 m6 f) x
  5. .../ g  V. t1 I. ~! F
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    : k  @. a! h$ Z9 f
  7. pthread_mutex_init(&pmt , NULL);
    7 S, o' }/ U6 O( \
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    ( U- ^* u2 A6 d8 d  |$ n
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    ( F" X+ F: ?2 @+ Y3 A7 n2 [4 h
  10. ...
    2 @5 s; B5 w  |0 S. e
  11.     while(1)  H9 P. C7 S/ Y
  12.     {1 J: }& [; r+ Z" h) n- d/ B
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    ( j. G" x) F& b$ p& E4 C3 `
  14. ...
    ! P: w" A# h) B4 n
  15.     }+ K( r) z# U# r( k
  16. ...
    ! ~) {& R+ j% }& g7 }6 S) K% Y5 a
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    ( {/ h. e# N/ o1 @- c# ^# ]! G2 D
  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" \
    8 x+ i0 z0 X% F7 [" {& k. ^; F
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    3 @0 d* {, O! _, L  `% T% f( V
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    5 `3 E8 T& G" y
  4.     "Pragma: no-cache\r\n" \9 H$ d- E$ j8 I8 c( z$ o: w* z+ N
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"& c  m6 ]* ^2 M1 m- z. _( p8 V
  6. #define BOUNDARY "boundarydonotcross"- Y9 c6 [; A' }# ~
  7.     printf("preparing header\n");2 D+ {* {/ X5 j" K: n; y
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \3 C- e2 P" N9 Y# l  ?
  9.             "Access-Control-Allow-Origin: *\r\n" \
    . k# P. X, ]& {5 \0 H* q$ k6 J
  10.             STD_HEADER \( x) {1 d% U2 M- V$ O4 }
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    ) d1 f! Q; @' ]! C1 v
  12.             "\r\n" \
    6 E* e1 T4 }6 z- q; [9 q7 \: ?
  13.             "--" BOUNDARY "\r\n");+ g% N7 N' x# ^
  14.     if(write(fd, buffer, strlen(buffer)) < 0)9 _2 h7 O) [: p- S1 O$ \
  15.     {6 d/ d! I# ^( Q
  16.         free(frame);: H) w' Q: u3 V8 G7 A, n& l# q! g
  17.         return;
    ! ^9 f" P& D; N% m
  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" \3 w! C5 u/ y5 }# f, u8 k+ o
  2.                 "Content-Length: %d\r\n" \
    6 ^# u  L0 i$ m! C
  3.                 "X-Timestamp: %d.%06d\r\n" \. m3 U  e: t% e8 P$ O4 y* P
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    7 v1 C/ L* M/ _# s( {9 b6 l+ f: T# f; j
  5.         printf("sending intemdiate header\n");
    . Z$ X% ?0 e  y0 P6 \
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    * X, i7 e1 T0 E. x3 y6 R! d
  7.             break;; z7 k# P8 l& {3 u/ s. t
  8.         printf("sending frame\n");# |# V( R( A3 S* R8 j( n
  9.         if(write(fd, frame, frame_size) < 0)
    2 m7 r& [4 P) Q5 |" }
  10.             break;
    2 G$ k- a( X2 k& b; v% l! G
  11.         printf("sending boundary\n");, G4 S8 L+ A8 @" X
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    6 R8 ~  X* n4 J
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    + E3 ^" S8 p. m) L( F
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

$ J  |$ Q5 A3 ^% v( j& ?


9 ]9 w1 p1 z6 P5 z! O% G+ t二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
) V" I: `+ Z6 K1 W# w2 t4 Z! A
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    * O8 n1 o# c9 ?
  2. {) j0 ]6 V3 s. W6 W# I+ _$ A
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);% f+ D; ^+ l9 M; s7 N
  4.     if(*socket_found == (~0))
    - @- D& p+ V5 Y
  5.     {5 i5 a# f* X( [9 g0 w  c" `. N
  6.         printf("Create udp send socket failed!\n");
      m$ I" B" |: }' U9 E4 B
  7.         return -1;* {# @% T1 ^+ l. Z
  8.     }
    & |+ B% S0 A) Y2 m( h( V
  9.     addr->sin_family = AF_INET;) j6 _6 U& v1 A" n& `, w3 \3 b% ]' x
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    # p8 U3 q% }! y) O
  11.     addr->sin_port = htons(port);1 V) P* a) ~1 H% w# M
  12.     memset(addr->sin_zero, 0, 8);# F. U% X. Q& ]4 y# Y0 b3 v
  13.     return 0;6 _% w% |, G; O0 K
  14. }
复制代码
# _) a1 C6 U/ r& k

, F1 W' e# v2 ^3 z7 M. w* d8 G而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:$ a8 h! p: N# d7 z* H
- J; |' c5 N( o8 v  o9 F
- e' |% S- ?/ d
  1. while(fend > 0)2 p5 M: _; `  f6 S
  2. {7 ^4 o" N. N( g1 O1 e
  3. memset(picture.data , 0 , sizeof(picture.data));
    4 d2 `3 J8 B- D3 E! X/ E$ w0 }
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    ) x6 `$ I/ w4 ?( B- e4 v
  5. if(fend >= UDP_FRAME_LEN)* E) T3 T" e4 J7 `2 `" L
  6. {$ Q+ G5 u: x  _: B1 j
  7. picture.length = UDP_FRAME_LEN;' P2 K0 @' N2 W# z$ j. b
  8. picture.fin = 0;, \; H8 O, s1 j3 k* r
  9. }
    ( i, d* x& |2 b
  10. else
    6 d/ e, u6 R' V- @( u' t0 _, @
  11. {8 R8 c; U* A, M3 I( A1 d
  12. picture.length = fend;2 o% s) J" ?  U3 }0 y' S
  13. picture.fin = 1;, n$ o+ N6 V- ^- V! ^! I) d
  14. }1 Y$ x' s0 ^# I
  15. //printf("sendbytes = %d \n",sendbytes);4 ]: F  p' Q- _' t2 ]% X! W
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);6 [2 s7 ^: ?/ }3 |# n6 I, p8 a( O
  17. if(sendbytes == -1)3 r: S- y" I6 f$ r8 Z
  18. {1 E% t& S3 M$ ~3 m; X
  19. printf("Send Picture Failed!d\n");
    4 m- c: j! m! D5 y' T
  20. return -1;
    3 p! m/ ~6 d; F
  21. }
    . n; u9 O! a4 w& m, ?5 T( c6 v* I
  22. else8 @& r2 O, J# ]- G. _
  23. {
    ( u( }; i* D7 V
  24. fend -= UDP_FRAME_LEN;, x/ Y" s4 R, }, q
  25. }
    & H7 y; r& Y2 B' n6 Y; m0 \* g7 r
  26. }
复制代码

# c  ^9 _( _6 v- ]3 {8 F9 |
- h" }4 Q! v% C' l0 ?

2 i1 ^. Y7 C! f

! U) `1 V3 }, [% h$ W, `1 ViMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-2-8 08:35

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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