嵌入式爱好者

查看: 11128|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
4 ]4 h' P5 C4 b6 _$ M# Y' i7 o2 `4 }) J; k8 N; O


. i; B( d/ N: U

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html2 Q; O3 m" b7 r

  k) l: T) G, N+ ~9 r$ m

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

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


' ?0 @+ p* r, `- G5 r; G# I6 C- m


% u5 O7 S% J8 j1 Q8 H0 O

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

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

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


    6 \5 v# y( x" P

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


1 t% U3 m* {5 I( t$ T一、HTTP网页服务器
5 U0 y; c* j( x0 X% }

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)& L. G2 L* b* s! }: S6 [7 L
  2. {
    . m* k: j, ~# S9 T2 G2 P7 y
  3.     struct sockaddr_in servaddr;1 R- A) t7 {( G$ U& F
  4.     socklen_t addrsize = sizeof(struct sockaddr);9 v! @+ y0 e1 c( n2 v
  5.     bzero(&servaddr , sizeof(servaddr));6 c$ d8 f# ~9 e* \3 Z. i9 U
  6.     servaddr.sin_family = AF_INET;+ Q8 ~$ y: K$ }
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);+ {- T7 n7 b3 N: `, w- W5 e
  8.     servaddr.sin_port = htons(port);
    5 c- y' ]( H6 {% ?% }' c* T: H9 f
  9.     int ret;
    ! w& s* H! s& Q9 B6 u( ?
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    ' ^' m- T2 h4 s& v  n0 U; t& g# U) K$ e. a
  11.         {
    + B, f' @" }  @/ p! U) b9 w" {' F" k
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);3 ?( k: V* c5 X2 b. X5 f/ A
  13.             return -1;3 s2 _5 p  I6 |  ~! R
  14.         }
    + D" u2 S" _/ J3 i( Y0 _
  15.     int on = 1;
    : ^3 L, M( n% f) \9 m" }( h
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    , v2 P& J% M9 U3 m% I
  17.     {
      D& o: ~4 V9 R! D
  18.         printf("setsockopt error\n");" o# x$ O( F1 W# K2 m3 S8 E
  19.     }
    " q3 C$ P  _& B7 I- O! A
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    4 ?7 O; P' O- P% V
  21.     if(ret == -1)
    " z, |2 b- O; X) Z" x
  22.     {, V. R( x* A6 D7 u! d: H
  23.             printf("Tcp bind faiLED!\n");$ Z  ~1 @# n# p3 N! R
  24.             return -1;
    9 E. @5 b) e* q, U* X
  25.     }' V+ u! l' x% P- s" g) c
  26.     if(listen(*socket_found , 5) == -1)9 ?0 a# q  y# ?0 A$ d. @" E
  27.     {5 o! d6 n: \# c5 h' g4 Z' y3 g
  28.             printf("Listen failed!\n");- a4 q- A" B) w' U
  29.             return -1;& o" I  G5 r, Q4 m) s. r
  30.     }
    % R+ x1 D6 I+ s. z$ Y
  31.     return 0;
    , L7 o6 x- [. d; {
  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);
    . u* g5 Q, Y- o7 X
  2. void * Thread_TCP_Web_Recv(void *arg)
    . h/ z, D1 `( a
  3. {* S3 a1 B, E% r! k
  4. 。。。1 F& m8 n( l2 n# b  Z3 W
  5. while(1)
    , m- K7 F1 u/ h! n$ ~
  6. {
    . ]: X4 J6 k' o, |$ f% E4 @8 q
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);/ r6 \+ `  e; y0 s8 M; p, l- u
  8.            printf("fd_socket_conn = accept()\n");
    8 M/ P$ b- J) s) _' y" ^. ~( u7 x( C
  9.     。。。& D: {& p9 ^3 d: o7 d2 y. {& P
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    . J( h% d! L& y5 D
  11. }0 l+ p/ @, H; G8 s+ S# {
  12. 。。。
    7 @$ s) A9 U( \
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    5 h- Z8 e4 d  m, l. M) ~! v
  2.     pic_tmpbuffer = pic.tmpbuffer;
    # e$ a! i+ O- A1 ]* v3 D9 P+ }& K
  3.     pic.tmpbytesused = buff.bytesused;
    * s6 Q& F$ B! f% U
  4.     pic_tmpbytesused = pic.tmpbytesused;5 ^" C5 ~& d8 Y0 R* |
  5.     pthread_cond_broadcast(&pct);) q- M- T& R, |1 J4 g& f3 @
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    7 d9 X/ P( t- V+ g. R9 |" N1 T' d
  2. pthread_cond_t pct;; Q: f0 b  C1 |- H4 Z
  3. int main(int argc, char* argv[])
    % n1 j( E+ `5 v, V* n
  4. {! X1 N7 |* F% h- D! r& o# k# s4 Y
  5. ...: `6 N4 {- b$ z$ [" a3 A5 g
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    $ Y/ g: B/ h  J8 z/ ]/ k) S
  7. pthread_mutex_init(&pmt , NULL);+ P8 p% t, q# _. e+ W4 g6 n7 c6 Q
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    ! \- L$ O; _+ W, \7 F: V  Z
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);- S  Y* F5 j, m5 m. o# i2 O& U
  10. ...7 X; {* l8 ]2 j+ O! t: ^
  11.     while(1)
    # B* X, v4 m3 l8 D8 K. i
  12.     {
    $ w. w0 \$ Z- Y8 ~/ Z, r# P0 ?
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);0 w  R  n, q+ r! w; K- [, g
  14. ...
    7 E6 w6 H" |) P7 R' l; f
  15.     }
    / _8 Z  \  i& j, a8 y' i1 W$ j5 Y
  16. ...
    1 O1 i. G9 S5 q9 u2 k9 f
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    # A( F$ ~7 H2 Y/ s9 A5 ~0 }2 D2 s
  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" \0 Z2 D" Q4 N5 i
  2.     "Server: MJPG-Streamer/0.2\r\n" \+ S/ m2 S, u7 p3 b, s1 M& Y
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \: L) ^9 h0 Y- y/ y  t; G) n
  4.     "Pragma: no-cache\r\n" \
    & }2 @: X1 D2 H2 H
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    % ~+ N  U( E: G, m6 e8 s- b3 P! D
  6. #define BOUNDARY "boundarydonotcross"
    ' o' f0 a2 h4 b, H# d3 u
  7.     printf("preparing header\n");: Y! N" M% |/ o: L3 f3 p
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    3 ]( r: S4 b* s6 c5 }% [% e/ r
  9.             "Access-Control-Allow-Origin: *\r\n" \+ [6 u1 B5 `; A, b8 }8 o
  10.             STD_HEADER \# |' b) W% e2 M3 f$ d
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \1 |* c& T6 K1 y( E
  12.             "\r\n" \
    # G+ G2 K6 v+ K! ~  q( L
  13.             "--" BOUNDARY "\r\n");: N: G, Q$ Y1 n* A
  14.     if(write(fd, buffer, strlen(buffer)) < 0)1 a- h& X* n! P- H
  15.     {
    - Q( F& R5 K' L, B4 {
  16.         free(frame);: [9 {5 A* R* ~% L2 o
  17.         return;2 g. Q3 p5 Q- u6 j. m$ p# t. 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" \
    0 \' q. e  a2 r6 h" b
  2.                 "Content-Length: %d\r\n" \
    0 t' q& ~0 s9 |- Q' r
  3.                 "X-Timestamp: %d.%06d\r\n" \/ \6 v) n, y. L4 V$ F# n3 ], x
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    5 ]' F2 \$ t4 P$ l  r  |8 C# n
  5.         printf("sending intemdiate header\n");
    ! V; }4 [0 A, e, d% y9 t: \
  6.         if(write(fd, buffer, strlen(buffer)) < 0). H, P6 A) |2 u, I& W* S
  7.             break;
    2 y6 C8 Y* E' Y, @. u( v& X1 c& `1 S
  8.         printf("sending frame\n");
    7 i1 x  O! t1 \+ t$ c
  9.         if(write(fd, frame, frame_size) < 0)1 _4 R; N# U8 d9 c
  10.             break;- J# f1 x1 c6 K) w1 e0 T" D
  11.         printf("sending boundary\n");: \# H5 e3 i1 _9 ?, a" U
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");& V( r0 R& ]( g. G: c# O' C' ]
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    1 M/ ~/ W, R( i' A. k  m6 l
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


  O- O3 I- O) Q. L5 S3 R; X


% A$ l2 y4 g7 Z* [6 q( k6 z& g% J4 \二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:9 D9 n/ F+ r. M* j
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)# N: o* s. L& o3 R* q
  2. {; }3 G; X2 ]/ f  X
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);1 u8 J2 U/ X! S, \. m7 s
  4.     if(*socket_found == (~0))
    " |) ]1 p+ S; R& c2 Y" @
  5.     {
    3 K) V& _/ G# y1 S
  6.         printf("Create udp send socket failed!\n");% j; \1 p3 C9 K5 M2 c: Z2 i9 @# w$ S) ?
  7.         return -1;
    7 I) K! V' s- v
  8.     }1 ]4 u: W% V3 r1 ]/ L5 x
  9.     addr->sin_family = AF_INET;# Q4 e* Z+ D+ @7 {# `+ |- q" ^( Q
  10.     addr->sin_addr.s_addr = inet_addr(ip);8 y0 M/ M$ ?& n  W+ d
  11.     addr->sin_port = htons(port);# R3 K2 D; d: I. A
  12.     memset(addr->sin_zero, 0, 8);6 z% A" J3 }$ {) P
  13.     return 0;5 G# [8 i1 j- f% x0 j/ ]7 a
  14. }
复制代码

7 N" C8 P, [0 ]5 Y& ]3 G9 Q# l$ ~3 M/ p' m: j6 ]' [9 n2 Z
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
6 h$ x/ o6 w8 J- [2 A/ j& m6 \( z
5 ?) }. Z% f6 g2 n! d" Q
3 C0 {$ m, Y# v
  1. while(fend > 0)
    " b! `% F2 f5 {3 X: Z
  2. {( f: W* G7 W1 q" b/ j* V2 h; `
  3. memset(picture.data , 0 , sizeof(picture.data));
    % d$ a" K( B) H. f. B% m
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    9 Y4 [. }, y5 k2 L- q7 P4 p
  5. if(fend >= UDP_FRAME_LEN)
    : ^0 I7 \5 u# E$ R
  6. {
    5 y3 ]# ]9 H% q# h7 @8 n
  7. picture.length = UDP_FRAME_LEN;& b4 q# g$ v5 i' [
  8. picture.fin = 0;
    4 j) b- |" r9 x  v( {1 G: h2 c) X
  9. }
    # F' m% u6 E0 c9 l7 q
  10. else) v5 l0 ]! Y3 H6 g( H
  11. {
      I4 j% z" w, v! N5 h
  12. picture.length = fend;
    " L( \1 W. ^4 ?0 B. Y+ |
  13. picture.fin = 1;
    * r1 Q2 f/ O. M: G: w6 }
  14. }
    . M' v" s! ~  ^7 S3 m
  15. //printf("sendbytes = %d \n",sendbytes);
    - v7 t- M6 |( _$ i! V
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);7 e6 p& i( d8 k$ _/ [! V
  17. if(sendbytes == -1)
    / s! ]/ X8 @# F/ K4 j
  18. {3 S8 X3 [* l( d$ T" |
  19. printf("Send Picture Failed!d\n");6 P5 I+ B0 p# j; Q
  20. return -1;
    - F" H* ^, y2 ]+ v4 P- [
  21. }* L  ]: T" Z  e' ?
  22. else
    6 X' Y) R, t/ T  q1 t0 m& P' g
  23. {) B, {6 e" [5 |2 L( v$ I2 ^2 X6 i
  24. fend -= UDP_FRAME_LEN;
    , S; r2 C2 C8 T3 ?9 M+ a
  25. }! `; y- s: ^1 s& {
  26. }
复制代码

0 n0 s/ u7 I4 K( |1 h  `7 @) L
; z" a9 I3 V. z% D& M

  V) X6 |' A* [2 B/ B/ c5 E8 n

% b$ }/ {1 [2 t4 GiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-2-15 03:08

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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