嵌入式爱好者

查看: 9802|回复: 1

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

[复制链接]

46

主题

53

帖子

295

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 % z" q/ u" R+ u1 t& f- d! l. `
& j- m; y7 A& |/ ?+ G1 N) B


/ D" g5 r+ e0 J, G* a/ W

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
5 W7 M$ V; Y/ `. D7 w9 h7 g
7 i- A) M* l2 N! T2 R

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

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


. h6 z/ M2 ^7 S2 }* n/ m


. F( T, `( b! a8 {9 T

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

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

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


    7 b4 Z) [; Y7 E: j% P2 ?' M

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

. ]1 P- P; }% P' U
一、HTTP网页服务器
2 D. K8 |1 f" \/ I7 K

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    0 U: x8 ]% z8 V! H- O3 N& Z
  2. {
    7 B. K0 `8 }+ M1 c  l( O
  3.     struct sockaddr_in servaddr;- O# d+ k! O3 Q4 b# F: P$ Q- c, x
  4.     socklen_t addrsize = sizeof(struct sockaddr);: O& O) p& G7 g) {/ e2 k
  5.     bzero(&servaddr , sizeof(servaddr));% Y9 |2 p. {8 H/ f4 z
  6.     servaddr.sin_family = AF_INET;2 O& x6 V: C  |. c* y9 U+ E
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    2 b6 G6 c% Y& q5 k3 U
  8.     servaddr.sin_port = htons(port);
      u- E: D7 I( z2 R7 D9 f
  9.     int ret;
    / g9 y0 B3 ?  L, M
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1); H% F" H$ v3 S9 q, O" {( j
  11.         {0 h( l. P3 R7 y+ y0 X/ Q
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    5 K% A& ^; W$ @& ?  Z  ]
  13.             return -1;
    ) Z: |- @) Z1 |
  14.         }* P% @, A$ Q2 k. V! c  x
  15.     int on = 1;6 H3 o# q0 {) z& J) b8 w' s  b
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)% {0 t# g( `6 H
  17.     {
    , b3 @1 D2 P! a
  18.         printf("setsockopt error\n");
    * M- E; F' R8 [. T7 d" U3 d
  19.     }9 I# Q8 H: f5 C" ?
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    * G+ ~6 h  S  d, ?" w1 W
  21.     if(ret == -1)
    / h/ {, o, ]% ]. E" N8 S
  22.     {
    ' V; {2 o- M! @/ k7 H
  23.             printf("Tcp bind faiLED!\n");- J) j, g$ p  r. R) j1 K2 P
  24.             return -1;
      [3 X$ r' v' K$ g3 t* ?
  25.     }
    / M$ M/ Q9 K& k" g
  26.     if(listen(*socket_found , 5) == -1)
    6 O" }% P, i* y3 L# }) |7 Z7 a
  27.     {6 f. r* ?: L. [$ {1 ^
  28.             printf("Listen failed!\n");+ D0 M0 k( I# l3 ?; @& m
  29.             return -1;
    0 S8 p7 ^2 L1 T3 S5 f* O5 \
  30.     }
    8 j! I3 p. H; I' Q: t* x. A# c: V/ G
  31.     return 0;
    8 r& Q  G8 m, K6 {$ }" t
  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);
    & s" F: u$ `* S' S! j' l* I
  2. void * Thread_TCP_Web_Recv(void *arg)
    . M4 p0 W6 w8 l4 i- l3 p4 {
  3. {
    4 P7 r8 [+ e: f# Z# [
  4. 。。。
    $ w4 Z& u8 X. @) q1 l
  5. while(1)
    - k' b8 H: l* }9 ]" R  m7 c7 n. q
  6. {2 ^# y& R, I5 _( z: H$ |0 g/ v
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);' h  g/ e$ H! f3 Q) e8 Q( p
  8.            printf("fd_socket_conn = accept()\n");1 N9 r# C. Y) V, X: |
  9.     。。。: J/ I& l/ f% g2 f8 x( K
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);% f/ O  u, A, C$ {6 o
  11. }% J' T! n. f: r2 S5 ^2 n
  12. 。。。
    : l( R8 }8 z8 K- T, E
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    2 j8 k) k- a1 _
  2.     pic_tmpbuffer = pic.tmpbuffer;
    4 ]5 ^8 i3 D# _% `. i2 d
  3.     pic.tmpbytesused = buff.bytesused;: ~$ I. Q2 g! ?4 m" f9 h' [
  4.     pic_tmpbytesused = pic.tmpbytesused;
    ( I$ U2 q. n& |( h  f. y
  5.     pthread_cond_broadcast(&pct);# |" S5 }) I0 B+ ]! ?: X. p
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    ( T; y/ a7 \+ O$ r, I0 T
  2. pthread_cond_t pct;
    ! R/ U3 n. c$ Q6 L5 z
  3. int main(int argc, char* argv[])
    # b5 U7 a5 j+ [2 Y. N
  4. {0 w# V; _+ }& t" c2 h! r" q- c
  5. ...7 O. e+ k+ p  ]' T( H% h! n
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    ) M  @& t# V/ u4 _. L. w. k" N. w
  7. pthread_mutex_init(&pmt , NULL);
    2 F+ d1 U% c+ P( [
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);( O( z9 O  N7 C/ k
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    $ G+ B; G; n7 Z: C& T
  10. ...
    6 b' O& d$ F& l" y% I) n
  11.     while(1)4 I5 s) {' B# N4 W4 X* ?/ E
  12.     {
    ( ]9 T2 B* ^$ V; d( J
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    " c  `  a! x  E7 G
  14. ...
      ^; G6 W' Z$ a3 H. F/ k
  15.     }
    ; z' P5 T9 ?3 q+ O' o/ w' G
  16. ...7 A2 f- S* ?, B, M! y0 {
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">6 @  R# B% `1 i8 v$ V, s8 r) z( L
  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" \( ~2 c( g; P! {
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    ' O# v$ o# {" {; B& }* Y
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    9 x2 _/ t2 R; j, k2 s. H7 u
  4.     "Pragma: no-cache\r\n" \7 |$ u' A% }# U0 o; E  S
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"# Z' v7 T. b8 b% j
  6. #define BOUNDARY "boundarydonotcross"
    " E% {0 v' c4 m3 w1 ^, j, h
  7.     printf("preparing header\n");0 a2 W) b) R  `# u" n4 C; W( O
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    ' h" Z3 p# o- X5 p" a
  9.             "Access-Control-Allow-Origin: *\r\n" \
    4 f8 _* L8 y. }9 X
  10.             STD_HEADER \
    + s' b  @3 T3 `4 V) D) b' _  T
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    6 F' A* F! o" C8 g' b; I
  12.             "\r\n" \1 ^3 x. K* e( S
  13.             "--" BOUNDARY "\r\n");
    , ^# g& m4 D4 T. C
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
    , h$ \+ p: q) T, y- L1 J. p1 O7 ]
  15.     {0 H* j0 h. B  p( r
  16.         free(frame);+ K( ~1 {+ f& q1 n# K
  17.         return;7 Y# Q' T$ h- R* m8 W5 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" \8 e7 z: J4 `5 D2 ?
  2.                 "Content-Length: %d\r\n" \' N: d5 B; Z9 v. F2 G, t8 Z( h
  3.                 "X-Timestamp: %d.%06d\r\n" \
    6 d" y  G' L3 S, k! \
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);" V( x- A- @- f! E. V6 _6 U
  5.         printf("sending intemdiate header\n");
    " E! Z) p/ y. ]8 ]# \
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    0 c1 |# `  F0 m& ?# D* ^& R
  7.             break;
    ( B/ S& m7 [) L9 m4 f, U4 z, W
  8.         printf("sending frame\n");
    0 X: z0 {& _1 N$ @0 i8 c: N# b0 {) {
  9.         if(write(fd, frame, frame_size) < 0)& q$ `9 E3 @* Y- }; R/ l4 N
  10.             break;$ a6 L( P( ?; ~1 y, F9 b" u
  11.         printf("sending boundary\n");
    - A2 Q1 ]- s; i" ^9 u% {. p) Q4 ~/ N
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    ; M, ~6 w8 j" u
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    ( D/ W* c  P9 l9 k% o
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


. w( m* N$ C5 Q" Q8 M- q) j

4 {' k3 T# n3 ?/ l2 d  y; w) j  v
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
! P: @/ l0 ~5 ~4 M
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)( G4 b# G/ s; R2 j
  2. {. `* Y3 L9 q( h2 ?2 N
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    ! z- a3 S3 b+ y8 G. X
  4.     if(*socket_found == (~0))
    , ~5 E( G# y0 {8 l% S
  5.     {# v7 t, F/ K% ^
  6.         printf("Create udp send socket failed!\n");/ |) g1 W* P% o  [! i4 p
  7.         return -1;
    / N# B! A0 m, Q
  8.     }6 ?9 J  o# |# M5 U( Z
  9.     addr->sin_family = AF_INET;
    5 H6 i# z1 E4 l: a$ C1 o' V
  10.     addr->sin_addr.s_addr = inet_addr(ip);+ I* q% h4 v& x
  11.     addr->sin_port = htons(port);7 P$ n) \- n! ~* ~
  12.     memset(addr->sin_zero, 0, 8);
    - M% W1 g  w4 g: a, ]
  13.     return 0;
    / x! E) p  s7 w8 Z
  14. }
复制代码
3 E9 u2 S. r; l, t1 l8 y: b
" L* z  B) W' f
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:  H% s7 Z& I' h! v" B2 K! F) [
1 M  i3 l% m+ o5 I

& S; v6 g7 N" E4 y. g& I, t6 z3 b( _
  1. while(fend > 0)
    & v" V" K0 x4 }' Q
  2. {
    : a% _# D8 H" @: e
  3. memset(picture.data , 0 , sizeof(picture.data));
    4 r' w( E) q. U6 F$ [
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    & _: k( v* M5 O, d$ w' O7 q
  5. if(fend >= UDP_FRAME_LEN)0 J6 u9 m. ~) h( B! d! ]
  6. {
    ! q  @- ~" w& G0 f/ {$ }
  7. picture.length = UDP_FRAME_LEN;/ |7 D, l: A- ?, @& i& c/ b
  8. picture.fin = 0;. P; q; {5 v* @7 \( H4 E
  9. }$ Z9 k  O, p: s# ?
  10. else
    : C: }: y1 f6 I+ F& P
  11. {2 g0 r( b% k: B# H) z: h& \' P
  12. picture.length = fend;) p- G* B* s8 @* I
  13. picture.fin = 1;, @" k9 G$ a0 c7 c9 m
  14. }$ o2 J& x$ d2 a
  15. //printf("sendbytes = %d \n",sendbytes);
    6 ~) l! m. K/ v
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    9 S3 M: D2 ?2 O9 v
  17. if(sendbytes == -1)
    ' _5 J) v2 j0 \" s) j
  18. {
      \7 ?3 s7 D' s8 L" [
  19. printf("Send Picture Failed!d\n");0 ~# w/ M  P0 I& N0 \3 N4 \
  20. return -1;# ?- `  ]9 M) U/ z$ l$ @
  21. }
    3 X" A$ _1 m& B; N# E) `
  22. else2 F# y& s, E1 d! H  u
  23. {5 ~4 X, U: W# V$ O9 Q/ c
  24. fend -= UDP_FRAME_LEN;
    / C, W( R6 x0 t% ~1 _. v' o
  25. }2 m5 R0 \" w6 G
  26. }
复制代码

3 L2 ]  ?3 M+ ]+ Y$ P
# ^& k& f! N/ a5 g" Y( a# ^% O


9 g* q3 o) B0 X' `7 Y8 E( D. |7 K$ s+ i2 {" }7 X
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-6-5 22:23

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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