嵌入式爱好者

查看: 11683|回复: 1

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

[复制链接]

48

主题

55

帖子

304

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 1 b3 E- u5 t& B) {

6 t% V3 N" u4 Z0 |- F' F

2 h( `& }# z6 w; k

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html4 l* f; U/ S# S5 X, o1 K8 @
& _( ?! X$ V+ `4 g% u/ }# E/ D- b

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

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


+ @" T# O( P" V; o


5 a/ b1 k/ E& t8 n

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

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

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

    6 V4 a* ]2 T; y1 J$ w

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

! G' f+ G- @9 ]7 d
一、HTTP网页服务器
. Z; p0 F8 m5 Q; n- y

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    , g& N& z/ ]0 `( u, ^# ~
  2. {
    % r3 k: w$ `% R
  3.     struct sockaddr_in servaddr;4 @: R8 r* ?7 n1 o
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    : S+ ?  R& o, A2 ?
  5.     bzero(&servaddr , sizeof(servaddr));
    ' b4 n0 T* S" i5 L- P9 f
  6.     servaddr.sin_family = AF_INET;+ @) R5 `# S3 B1 H/ i; G) ?
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);/ j) M, d5 e1 r) Q6 V# c
  8.     servaddr.sin_port = htons(port);
    3 G* F# N& ^: v: O* P: k. D
  9.     int ret;
    3 w" ^+ g9 Z- m6 I( s" i6 J! q6 k: l. q
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)/ Q7 U2 i6 p! s% C
  11.         {1 k% b& m3 M- y3 T% U
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    2 m& u3 b9 T7 ^2 b, J
  13.             return -1;
    % a, z9 j. z5 q. j+ @2 y* [
  14.         }
    $ R) D5 w9 ]- x" q4 A8 u6 R
  15.     int on = 1;& B6 G4 `6 W/ x# n  q9 a8 F
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    9 D& N+ O/ [  U9 X3 }8 p2 k# E
  17.     {
    - A' g  S$ b7 v  y$ e3 x
  18.         printf("setsockopt error\n");4 y) w) v# R- V3 \: E' a
  19.     }
    ; n! |. v/ u  p$ z) |( W3 {
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);% l- z/ W4 f; R: P0 Q) v' t
  21.     if(ret == -1)# t' |* _) g& P* w' B2 c3 s
  22.     {
    % Z; ~4 T! W( `$ L
  23.             printf("Tcp bind faiLED!\n");9 Q& e. c8 I1 G
  24.             return -1;' t* J: f; s* [2 g4 ]& X$ t
  25.     }2 b4 u7 x+ |; d# M& R1 R% z
  26.     if(listen(*socket_found , 5) == -1)
    ; ]! L  y; {7 B8 Z9 b) [, z
  27.     {
    ( s, q) W: P  p8 q/ `1 w* x, Q
  28.             printf("Listen failed!\n");. T4 m' I" X9 `' S
  29.             return -1;
    3 N4 ~+ t& @6 U
  30.     }$ C% i& \2 M  I* }) X. u/ n( ^
  31.     return 0;! {+ N3 _+ U9 H: @1 N, @
  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);" T: ^4 T, K9 z) u6 m$ j2 a& N# n
  2. void * Thread_TCP_Web_Recv(void *arg)
    * S3 n, Q# l: |) y. b/ Y' y
  3. {
    5 K* ]1 X6 H) d* }4 ~, k
  4. 。。。
    * L, b4 n8 h+ D9 Z$ |
  5. while(1)
    & x# j+ x* u9 E: q( Q5 M, w. T6 |7 p
  6. {. v. z2 b6 v3 d* l5 x8 h
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);, e" e7 r* A6 d9 v8 t$ S
  8.            printf("fd_socket_conn = accept()\n");
    6 |& @! G" o: [' l
  9.     。。。
    & K; s- F- x0 Y" Q
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    + A  s9 }+ w* x- W* U* s
  11. }
    : @8 J. Y  v! Y! z" E4 ?& |  G
  12. 。。。
    ( |" {. K" S+ ~: u5 {
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);8 \( {  l7 g" b* _7 o' T% ]3 A
  2.     pic_tmpbuffer = pic.tmpbuffer;
    7 ~1 Q0 p8 P4 P4 f0 O. s
  3.     pic.tmpbytesused = buff.bytesused;
    - v; ], W( Q4 R8 k- q) C
  4.     pic_tmpbytesused = pic.tmpbytesused;& V8 _" C( C6 b, P- N
  5.     pthread_cond_broadcast(&pct);
    0 C$ N! D  k8 d9 m, }( Z4 ?
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    ( n8 ^2 L$ `/ q* D% d0 u
  2. pthread_cond_t pct;
    # w  ~) @. P# h' z9 l! P
  3. int main(int argc, char* argv[])0 T6 o9 ~+ P9 I& p0 j
  4. {! u% Q2 W" A: b$ b9 k0 N- x3 t
  5. ...
    - |; n1 Q9 c. s
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    $ [0 f, z( G- x) Q7 u- u
  7. pthread_mutex_init(&pmt , NULL);
    - j; k% o) U/ S0 d
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    + _# E9 z8 ]% S! k2 _
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    ! k( y% {: }- e
  10. ...! Z7 v/ H; w  c! o* i7 Q9 }; e" z
  11.     while(1)
    8 J* ~. |) ]  m% H9 i
  12.     {# f, v. I; @7 ~( ~
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);, z; g; N1 t7 p+ x1 f
  14. ...4 s2 v2 B6 d' n- E
  15.     }
    % Y; _* b; y+ L" ~5 T5 P
  16. ...
    ' g1 e, e, G8 }# J- `9 F; z8 F
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">% Q: k9 A3 x! H7 ]( g2 _
  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" \% F* W" ?5 {; e6 s+ [2 C
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    , V. z" `% j0 X' a5 z+ s
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    8 q$ J2 M, M& o1 j
  4.     "Pragma: no-cache\r\n" \
    $ `* R; Y$ d7 {( y. _( _- [: m
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
      M1 H8 H( p/ h3 R. P: x
  6. #define BOUNDARY "boundarydonotcross"
    6 |0 Y. o" S6 x) T3 y8 ?  F
  7.     printf("preparing header\n");
    4 M- A* v3 ~9 r! ~
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \8 U2 n& ]# d" H! f1 C( q! X
  9.             "Access-Control-Allow-Origin: *\r\n" \
    % D/ r0 x9 Z* w3 D9 ?6 [' W
  10.             STD_HEADER \
    : t0 X( P- U. d5 g0 r9 Z
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \- ?7 ^4 {, |- O% H1 X  P  o  z
  12.             "\r\n" \
    $ n0 z6 }) v3 ~5 Z% u( r
  13.             "--" BOUNDARY "\r\n");
    4 V8 _0 Z; P* m$ u3 y" W
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
    : _: t3 }- x6 V3 i
  15.     {
    , j- g, }: U. v* g. ?
  16.         free(frame);
    8 v" ?2 N$ v& i
  17.         return;/ h' \- e3 Y" s7 }% w
  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 T1 v) Q3 C' o4 z$ c
  2.                 "Content-Length: %d\r\n" \* r7 n. \. N- k6 J
  3.                 "X-Timestamp: %d.%06d\r\n" \* a. F, ]* I, j6 p; X9 L3 B
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);1 y+ @9 X4 B, A& C# t/ j
  5.         printf("sending intemdiate header\n");# ~! Y+ e* s8 a( C7 P5 P/ X
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    : E/ A6 E$ x; i
  7.             break;8 G$ }  [3 Q; b% L1 H; l7 P
  8.         printf("sending frame\n");
    * l; r- o+ p) ~* o" _
  9.         if(write(fd, frame, frame_size) < 0): ^" G3 o% h2 t' J. n  G+ S# q
  10.             break;, W/ r& \' T, P% {' S9 f
  11.         printf("sending boundary\n");
    ; p( b9 H. p3 k8 `. Q
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");. B3 T/ V7 x# s0 s/ i+ N
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    / w7 I& R3 V) k) z  t% t
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


$ H0 r& B# q4 m: |1 |8 J

. B9 X3 z5 a" B5 ~) Z4 n) N
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
! q1 _4 g; n& l* a" y
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    % t; d6 W* d' K( ?1 c* K' W
  2. {
    - M. W1 F: v2 y4 G( u5 c
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);# x6 {# ^0 o# |/ u" p. l
  4.     if(*socket_found == (~0))
    " @# I4 Q- A& T  N
  5.     {. S0 Q+ m9 q0 e
  6.         printf("Create udp send socket failed!\n");
    , R/ @# u$ |! D& r
  7.         return -1;) T6 u) s8 o0 u7 |
  8.     }
    : k. B: ?% m! x1 ?
  9.     addr->sin_family = AF_INET;- V& h$ p8 h; M& s! z9 G
  10.     addr->sin_addr.s_addr = inet_addr(ip);5 V. `$ d2 H5 H8 D$ [, Y
  11.     addr->sin_port = htons(port);
      J6 M7 O; c0 j. X% k9 B
  12.     memset(addr->sin_zero, 0, 8);1 Z( O: c& X7 g: y
  13.     return 0;
    - n2 o! L5 Z7 ^6 ^# Y
  14. }
复制代码
3 I! a0 T& A8 m. [5 U
" ~. r/ G3 p( A, O) F: w9 V
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:% [# E6 B. i# ?( C; ~0 U

9 c) Z1 S* r+ I- v8 C* w% C
$ h1 f" H" a9 F: {+ B
  1. while(fend > 0)
    2 `+ I2 V, _% k9 @
  2. {
    1 N9 `; q- v/ K& j0 O
  3. memset(picture.data , 0 , sizeof(picture.data));
    % R6 c+ ^7 U0 r. K; w. A, A0 a" |1 I
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    2 g# Y  L4 n: U4 W( \
  5. if(fend >= UDP_FRAME_LEN)
    : N8 @  q: M% S) y4 r4 |
  6. {" `2 m3 S- o* x8 G
  7. picture.length = UDP_FRAME_LEN;
    5 R0 V0 O* x* ^& S% ?5 Y' i
  8. picture.fin = 0;; Z% T1 I6 r  Z- v  Z9 a
  9. }- W. k+ A: w/ k& X2 m8 x
  10. else+ K4 z" k& G, i) \2 F1 Y( p& @
  11. {
    ) I" g( V7 d+ q9 L( I  I! Z
  12. picture.length = fend;- c0 K% b. N+ Q# {& t
  13. picture.fin = 1;
    5 d1 ~8 |: p7 D( J: l6 ]4 f
  14. }
    % G- N4 w8 \3 X/ g( U7 z7 a
  15. //printf("sendbytes = %d \n",sendbytes);: j/ D' B- G1 R0 n3 P! ?
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);) d/ M6 K5 J6 y6 X
  17. if(sendbytes == -1)
    1 s! f* W' w6 i  d% C: \& j
  18. {7 x- v+ X' h6 f7 V
  19. printf("Send Picture Failed!d\n");
    * h6 B# N! h0 `8 R" a
  20. return -1;8 C2 w; d1 y3 I1 K9 D. f) x
  21. }
    ) V: o' k# o& g+ a
  22. else
    % |& Y- e2 |; ^' _
  23. {
    - z1 A! e2 Z1 |! @, m4 t
  24. fend -= UDP_FRAME_LEN;6 [3 z: L9 L  o/ D% O. x  E2 t
  25. }
    ( v' a" i9 Z* w4 D3 s0 A
  26. }
复制代码
! ?$ L1 L& Q( U; {2 e4 @0 |

( i$ [+ m( U7 A$ X% w* ^, \2 G

  h) q0 _; @8 g5 t/ f: i
# Z+ A" Z$ \* D' a2 K, x
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-4-7 21:15

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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