嵌入式爱好者

查看: 10246|回复: 1

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

[复制链接]

46

主题

53

帖子

296

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 1 O. v) i& i+ ~, S

8 B* f+ o$ o% F- n

9 q# M/ X* S% Y# Q

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
8 C" ^1 Y! a, K9 q1 j  A: L7 U$ Q% [

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

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

% Y5 ~  Q  N: y' n+ z3 H3 I3 Z2 C

9 K! X9 q8 F) `, a

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

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

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

    - Q4 \, ]2 |, R' n

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

. E) J9 B/ V2 t" t" }) I! C0 S
一、HTTP网页服务器* S7 b; q* F* ?

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    $ P# L3 R9 r/ k0 K
  2. {0 I  q/ `% j  |: X
  3.     struct sockaddr_in servaddr;
    8 v9 X; q* m+ T
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    ( P5 W% t4 P% u2 x. h; Y4 W8 m
  5.     bzero(&servaddr , sizeof(servaddr));! H2 p! L0 V% p2 p& _
  6.     servaddr.sin_family = AF_INET;
    " u( @8 g8 |+ T1 G/ n0 h# ~
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    - h0 K  g% m! f9 r
  8.     servaddr.sin_port = htons(port);
    - \2 K! @7 ~; K" W3 D; z( e  n
  9.     int ret;
    ' t% Z& P# l6 o$ K! x: q/ }
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1), a2 P& Y% \5 U1 P( K0 p
  11.         {# D* r5 L: }( x) Z# |/ [7 Q# C
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);+ J+ S  K+ {; z" o  P7 F7 g" H+ b
  13.             return -1;
    " Q' E: I6 k  R! G% v, P6 O
  14.         }
    7 g* V& S! R) |9 x" S. n6 z
  15.     int on = 1;/ N+ e7 J' X. H( N* g! d: @
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0). k7 Q' M+ U, G" [0 \! F
  17.     {4 J% h" W! V0 u& E7 Q$ C
  18.         printf("setsockopt error\n");
    % `( E6 A' m9 Y
  19.     }0 a4 G8 d' j( N, l: M
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);; c5 N/ u+ c) Y" E) D3 q
  21.     if(ret == -1)9 T( U  s2 A7 u- n" I  F* v
  22.     {5 {7 G+ ~3 K4 D: e& u- h/ z* i
  23.             printf("Tcp bind faiLED!\n");
    ( e% Z! F, R0 L; W; H# l, {; @
  24.             return -1;
    1 }; ~" ~1 k$ Q2 w9 V  I  q
  25.     }! j  {6 Z  K6 U# p3 P# K
  26.     if(listen(*socket_found , 5) == -1)
    ( \% s) d/ x& l! a
  27.     {: q9 p5 C( l  U  c& h
  28.             printf("Listen failed!\n");
    8 Q' J, O5 D4 m' k5 a
  29.             return -1;
    ! ^; f* c+ n# C4 [( V+ c
  30.     }! ~! a0 h0 y, w- B3 K! R7 s' R
  31.     return 0;5 f' ?& m  B# |  j; o" Q
  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);
    ( Y* t9 Y5 z/ V) x& J# I0 D
  2. void * Thread_TCP_Web_Recv(void *arg)
    , o* E, o) Y  f- l5 j/ f
  3. {
    + S3 R$ ?5 G* ^' {( N5 v
  4. 。。。0 B1 |2 J  Z$ h/ [+ \  U
  5. while(1)
    1 W1 V( s& n2 C/ b4 z( Z* }
  6. {
    ) x% c0 B% n( ~/ k( `! T. ?
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);3 O, I7 p* I6 B4 ~* ?% j
  8.            printf("fd_socket_conn = accept()\n");
    5 ?  a/ X% I9 g1 m
  9.     。。。
    5 I' v1 T" [0 l7 s; o& j
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);" `: W& ?* I: {8 x
  11. }$ P! Q/ ]1 ~# j7 n# \" r" R0 l
  12. 。。。
    % y3 t" A8 ?, d4 A3 h
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);( }2 s) _& Y, E% V0 B& ]9 ^
  2.     pic_tmpbuffer = pic.tmpbuffer;
    / ?/ u! u+ l: x$ V; J, I( ?
  3.     pic.tmpbytesused = buff.bytesused;
    7 z# V* L' F: h, B1 t5 s
  4.     pic_tmpbytesused = pic.tmpbytesused;
    , Y, h' M( Q  _, M3 c
  5.     pthread_cond_broadcast(&pct);
      z7 g  o3 v8 ?% O' q" H. m
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    1 K/ `* O6 s; M& w
  2. pthread_cond_t pct;
    , q$ F5 J  ~( {3 \1 M
  3. int main(int argc, char* argv[])6 h# L( r& m! L/ G* x' V. a
  4. {/ c- `  }7 N  m, B3 Y3 a
  5. ...9 `2 \( E* t+ I& W
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    7 m1 t6 _8 S% z5 B+ i' H, R
  7. pthread_mutex_init(&pmt , NULL);' K+ y3 _$ y- @) a: s
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);* {6 o$ I- Q$ B6 ?$ Q: a
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);7 e5 W/ v8 N& \5 M! X- [- L
  10. ...& R( u0 x* N) w) l  l8 _
  11.     while(1)7 }9 p; j0 b: [- m
  12.     {5 I% p3 x4 Y! T$ T/ v  H* _
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);- }3 w. R& g' ]# r! m/ ~5 J  U
  14. ...0 h3 W: d. f; g& z& K% @9 i. v
  15.     }: e' Y5 [( z: R3 b' J- _) I" e, M
  16. ...
    ) K# f4 w( Z% `8 Z* W, y
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    7 `, p) S5 |3 Z# C# }* z% I. \
  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" \/ Z- @5 S- n3 ]) z- H
  2.     "Server: MJPG-Streamer/0.2\r\n" \  e5 c* M0 [5 Q8 S# J
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \4 W5 B2 z3 z4 n6 S
  4.     "Pragma: no-cache\r\n" \) Q! b1 p, L, v' M$ V
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"9 \7 Y5 M8 C) z) A7 W# i
  6. #define BOUNDARY "boundarydonotcross"# E3 p, N8 \/ W3 e0 Q" |5 N
  7.     printf("preparing header\n");
    , h6 v) y# f* w: g$ h4 _8 |
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    # j0 S- v) h6 ~& J7 q# a3 ?5 c
  9.             "Access-Control-Allow-Origin: *\r\n" \
    - E3 ~% k# t2 c
  10.             STD_HEADER \1 c- Z  R; i7 E) F" l; \5 t8 Q
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    6 G& S, j" p* A1 [% k, k( c
  12.             "\r\n" \
    : x2 N1 u% _$ {( @* y
  13.             "--" BOUNDARY "\r\n");
    / ]3 S$ y- j$ N# `
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
    . k2 Z# u6 b. |$ O- j, D0 s9 ?
  15.     {) v3 ~( L$ b( y0 R1 ?2 O
  16.         free(frame);6 r, k- g5 @4 f5 f4 ?2 W) h: F
  17.         return;  U2 _/ k! S3 W  s5 M* u$ p; J
  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 D. \( `* o( t9 {
  2.                 "Content-Length: %d\r\n" \- Z; M* I* a& D2 S& o1 U
  3.                 "X-Timestamp: %d.%06d\r\n" \$ |4 T3 q9 v* N9 E# Q0 y# V
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);7 {1 f4 k+ t4 U) l/ P. O3 c" G1 R9 f/ l
  5.         printf("sending intemdiate header\n");
    8 b4 v8 E2 c* s) B( Z% W
  6.         if(write(fd, buffer, strlen(buffer)) < 0)# y. g8 f: ?, s; w+ h
  7.             break;' q# h7 Z: w# b: p+ \
  8.         printf("sending frame\n");
    & E9 T/ \6 p6 a, |4 t3 o
  9.         if(write(fd, frame, frame_size) < 0)4 k' J0 R4 }, C5 ~* A2 e) e" R
  10.             break;+ k3 Q# a/ s7 J4 |- y6 Z" S" r# U5 Z
  11.         printf("sending boundary\n");
    " p! @) l0 g  h
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");4 |1 y7 S3 c( H+ R
  13.         if(write(fd, buffer, strlen(buffer)) < 0); y- n; l/ P0 F2 r( m5 d
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

& s7 I( @9 m  l* f

6 e7 s5 {8 Y; q( J/ h! w! a/ C
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:( }+ C" |8 e; U4 C
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    ) z3 Q& f: A2 `( \" {: ~3 T
  2. {
    & a( l: G( _6 `# P7 O; }) {
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    $ V+ x: H* K+ k: X+ _: q
  4.     if(*socket_found == (~0))+ F: k: T( F( Q/ C. g
  5.     {
    5 {; N" |) i5 T6 g8 E! A
  6.         printf("Create udp send socket failed!\n");/ G! y5 k; k3 |" d! I  d
  7.         return -1;  t6 w4 p5 T; N( @0 L; ?
  8.     }
    + d0 O; n" p( \
  9.     addr->sin_family = AF_INET;
    ; w2 U/ B$ ], K% s* s
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    2 ^: F* }( h6 u2 {. `  N
  11.     addr->sin_port = htons(port);
    ; {4 ~4 y9 a  f# w& k' J: j
  12.     memset(addr->sin_zero, 0, 8);
    " E9 Q- x3 g; ~4 }* c+ u
  13.     return 0;0 E' \- F. H8 _# B4 ^$ W8 M3 e% B
  14. }
复制代码

1 P0 v2 x% F4 Z' R0 O
7 R$ c# s! k0 d1 e. x7 x7 ]- A& I而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:6 p/ A6 d8 }6 y( a6 ~) Q+ s- y

# f3 Z' U% D0 w2 b# p6 c8 R/ e! _- _( ^3 f1 Y
  1. while(fend > 0)
    6 d$ Y6 W- L* U+ t: K+ ~
  2. {
    & h/ [& D; H3 ^. `+ D
  3. memset(picture.data , 0 , sizeof(picture.data));
    - D" E+ p/ I/ I7 n& P" E
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);+ T! v: k1 p. K, V8 v3 P
  5. if(fend >= UDP_FRAME_LEN)
    + _" q% j. C+ c3 t& E4 x
  6. {
    : A: B7 r+ W6 t, g& G! r
  7. picture.length = UDP_FRAME_LEN;
    / L3 b8 x: q! r+ c: ~* k
  8. picture.fin = 0;& D4 C! \/ Q' j( F+ f
  9. }2 k6 x' i: z! C! ?+ }
  10. else; B0 J$ e" z: J  ?0 T7 w0 {) N$ ]
  11. {
    % M* i! g! q. ?" S6 [  N& {
  12. picture.length = fend;
    - x* B/ A6 z: h9 @. Y* K8 R) _# {! o$ Y
  13. picture.fin = 1;6 O2 n7 u  q- t/ ?8 d, i
  14. }
    * |: {/ W& E( z
  15. //printf("sendbytes = %d \n",sendbytes);% i+ a. Q+ m/ y. `# X
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    1 I% x4 y+ y, V5 E1 T; c7 S6 n' z
  17. if(sendbytes == -1)
    ! H7 l# E& y0 ?$ p
  18. {
    1 a6 O  H# G8 J
  19. printf("Send Picture Failed!d\n");
    " h* }' @( t. b! T4 ^, B: v
  20. return -1;
    ) ]1 o; O. Q* t4 ]/ r
  21. }1 L2 I+ ^5 H- a; u  ?: e: c3 D
  22. else
    + U" N$ }" s: t, `& o
  23. {
    * A6 s; U+ _9 f8 ?5 g4 g. G
  24. fend -= UDP_FRAME_LEN;
    + o' |' u3 I  G
  25. }6 ]( C: L6 d! v% l4 T& B+ x% g
  26. }
复制代码
( l- M7 d; t$ f8 L" Q" L
( j3 y) c6 N7 l$ Y! S( G


: D2 P+ ~7 l8 A; q$ Q! M8 S+ o
' u+ h5 k# b% _& S9 ZiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-11-22 16:01

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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