嵌入式爱好者

查看: 11121|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 : t# U% T1 X" w. ^5 }
# u. b" w) G7 n' B  j0 e! c% u

8 O0 j: ^. P+ X" ~) Y

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html9 f# n& D1 ]' Q2 h) d
0 m, B) L; u" W7 h; o5 j  {- ?9 ~

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

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

+ o% P. R! O0 l

2 H& F0 i: B7 h: x, l  q9 \' v

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

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

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


    7 j5 v; J% A( e6 R+ ^+ k) K1 k% [

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

" O! H' j- H! L+ B/ ^
一、HTTP网页服务器
7 f$ F4 ]' d) Z

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    $ G* x$ h" x3 R3 A' `
  2. {
    3 `) x% Y- e3 `
  3.     struct sockaddr_in servaddr;
    ' q" `' G$ K/ e  l0 C9 U' e4 z/ ]
  4.     socklen_t addrsize = sizeof(struct sockaddr);' x9 Q; k2 D* ^  K9 L3 O
  5.     bzero(&servaddr , sizeof(servaddr));
    ! g! ^; m2 Z% R8 u5 w) w
  6.     servaddr.sin_family = AF_INET;$ M6 \1 s8 i7 @' }
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);" u7 O- g- i/ P; U4 [! c
  8.     servaddr.sin_port = htons(port);
    3 w. W2 \8 r) w
  9.     int ret;- y; B& @4 X  {# J" m
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)4 k( J1 N* H' [" n" P
  11.         {9 ~2 {5 }0 y( ]4 ~8 i% F7 W" x$ m
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    1 q# H9 N! y0 u4 O/ o
  13.             return -1;
    ! e) h' ?5 _+ N( O$ b$ R
  14.         }1 i5 Z; c3 g" j( z+ i- w
  15.     int on = 1;3 n1 k: |; W- ]% _; Y- D( \/ u" s9 K
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    0 F$ ^- b/ s' K  B; W. }
  17.     {
    0 u* M! `% V! z+ \+ q/ I" Q
  18.         printf("setsockopt error\n");1 r3 X" B2 d, q6 [
  19.     }. U7 `* F6 X/ }% ~; q4 f% B2 T
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);3 }2 U" j# X3 ^& k! k, s
  21.     if(ret == -1)
    $ i5 g2 Q  L( g
  22.     {
    ( i8 {* k3 R) ^5 V' F" s9 K
  23.             printf("Tcp bind faiLED!\n");
    ) m" l7 ?( u! |9 l
  24.             return -1;
    ! a4 N% F4 f+ s& S  ]; ]( t
  25.     }
    ' A& m9 O9 S% ]/ U" |6 ^
  26.     if(listen(*socket_found , 5) == -1)
    # |' u# G  ~  O; N( k# t
  27.     {
    ! f/ t. O& H$ v. b" z2 I3 n
  28.             printf("Listen failed!\n");
    $ u% p. R! n- K+ w
  29.             return -1;
    0 I6 C$ L4 Z! ^; i
  30.     }, S4 F. P1 g- Z& K: W; w8 P
  31.     return 0;) M: w9 ]2 a( N# u# F
  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);
    , ?* E" @, o. L3 H* _+ j' x! o
  2. void * Thread_TCP_Web_Recv(void *arg). C1 n3 Y, A8 K4 M  F6 u3 ]) }+ D
  3. {
      k  {, \+ P5 R' V6 d, \8 ^( l6 \
  4. 。。。
    # L# R8 N$ \6 a2 B# w
  5. while(1)9 ~3 {3 J* \* P) {0 c/ p5 b* O5 v; F
  6. {
    ) u7 p: C9 y8 N' e
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
      k! F- j& X8 S  Z1 j3 B
  8.            printf("fd_socket_conn = accept()\n");
    . r. @7 W1 n& X; V5 S
  9.     。。。; Z  |6 K4 K( M4 \* x
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);4 O! a) P" Y; ?1 d, B0 d
  11. }" n8 E% N% B( t) |
  12. 。。。6 Q. h% {: V: A9 T7 l
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    0 r% Q3 ^: H) }  ^- `5 m% \% w
  2.     pic_tmpbuffer = pic.tmpbuffer;
    9 W( h- {; Y1 `' v5 @3 a& B4 P
  3.     pic.tmpbytesused = buff.bytesused;0 E6 o. w& b( u: L$ V3 z, `
  4.     pic_tmpbytesused = pic.tmpbytesused;# b5 X) F# _/ p# W9 G8 H
  5.     pthread_cond_broadcast(&pct);! D9 N  C3 ~2 r/ p2 t( X
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    5 O' E5 H% i5 R% Y0 W  M6 c
  2. pthread_cond_t pct;
    , T; M: W* D- o6 r+ T
  3. int main(int argc, char* argv[])
    ) d; x  o% d4 Z( Y
  4. {( E! ^% R: |0 D
  5. ...
    8 r% h' j2 w/ i* X! [: [9 U
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    3 @1 m: l( ~( R* {
  7. pthread_mutex_init(&pmt , NULL);% a* j- h: a( a+ D
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);3 _- {; @+ A$ c( T% j; S
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    5 U* e4 J  d. |; q5 y- j, w
  10. ...
    5 A" A8 C4 B* |* e/ F
  11.     while(1): _; z  {& c4 q# k) c! R
  12.     {- W" U7 [0 D& X8 F. W: V
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    5 U; x$ S# a+ m- N8 r" s, t
  14. ...
    2 r* x- z& m/ G8 U
  15.     }( Q5 G- |, g1 D/ K9 d
  16. ...
    ) [; R: I" a" N1 _# A! X. ]- o
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">9 [6 M3 T% }$ p5 M  s/ @( Y3 _7 a! _
  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" \
    ! y0 q+ m2 J- O! k
  2.     "Server: MJPG-Streamer/0.2\r\n" \# ~# Z+ Q1 f# u- S) E  [6 F
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \' z; B6 ~/ \6 w3 Q% z/ K- t
  4.     "Pragma: no-cache\r\n" \+ d& O" J/ V5 Q; V# v
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"! \) W" q) v$ p2 i1 {( y6 J
  6. #define BOUNDARY "boundarydonotcross"2 d! {" S1 F7 V) `% F' {( m+ e
  7.     printf("preparing header\n");3 z5 c0 _' H$ Z* |: n  _  _6 R3 @
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    ! j6 \& B& F! U1 M* P# O
  9.             "Access-Control-Allow-Origin: *\r\n" \* I3 F7 ?* z, q& E$ d
  10.             STD_HEADER \
    , e& P2 k' l# g
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \- K% ?; W& h2 ]! d/ p
  12.             "\r\n" \# D8 r; R. h2 \' x
  13.             "--" BOUNDARY "\r\n");- N% X' ?+ V( p. j6 e1 ^. H! A
  14.     if(write(fd, buffer, strlen(buffer)) < 0)( F, ^3 g# z, p. B& W5 h
  15.     {
      g+ |" C- U0 `+ m; ]
  16.         free(frame);/ Y* j2 o7 y( G" O: O* L
  17.         return;, g* ]5 O4 n* K1 N0 _7 k& v" O8 @( `
  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" \) I% e* a+ G" u5 r$ b6 S
  2.                 "Content-Length: %d\r\n" \
    7 N# H1 Q2 ?- z: d# ]& C8 P6 ~
  3.                 "X-Timestamp: %d.%06d\r\n" \, l0 J) a- W! S1 [/ K
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    . Q; o+ {; [5 c3 ]+ u6 P4 ?9 P2 M$ P
  5.         printf("sending intemdiate header\n");
    ) X) f  H% p( h/ a# H
  6.         if(write(fd, buffer, strlen(buffer)) < 0)0 g/ E; j# X0 Z
  7.             break;& D4 d1 m  h4 k6 K. R/ p( P9 M
  8.         printf("sending frame\n");3 O8 N; ]$ [, `6 Z+ S* ^4 C
  9.         if(write(fd, frame, frame_size) < 0)
    : K/ I/ _+ p+ G0 C, e$ m+ J
  10.             break;# C9 K# X% D8 s- u$ y: F& F' Y
  11.         printf("sending boundary\n");$ }0 v2 R) k$ M- @  C9 \
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");" l: W; b- ?' b2 V4 ^7 q$ |
  13.         if(write(fd, buffer, strlen(buffer)) < 0)3 Z/ U% @2 ^$ r! N
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


% t3 b4 v6 g7 T2 y- j

, b: ~% M/ N7 o6 p5 e7 q$ {, S
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
1 s  V4 y- T0 h( S
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    9 c" O* o) X: ]# A
  2. {8 r4 f: W' Q) p
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    4 U$ B, }- K2 I: P% w6 [
  4.     if(*socket_found == (~0))  H' a# F- o+ d" E5 D9 r" a, m( _
  5.     {9 X8 c$ M* ?$ j6 N
  6.         printf("Create udp send socket failed!\n");$ ?# W* u, o5 \. t7 \* {1 V
  7.         return -1;' X0 H0 j* L2 f$ U- k
  8.     }" f0 L! x5 _9 z# h
  9.     addr->sin_family = AF_INET;4 @% `& q+ u, ?" X5 x
  10.     addr->sin_addr.s_addr = inet_addr(ip);
      q% u5 b  T( Q$ N1 Z; ^( J) l# V* j
  11.     addr->sin_port = htons(port);
    + K& I7 H- p. E" Y
  12.     memset(addr->sin_zero, 0, 8);+ x0 f1 C+ q1 @/ ~, M! K2 r* P0 K
  13.     return 0;
    7 i- @7 B- Z/ ?- E8 {3 ~8 C6 w0 q
  14. }
复制代码
) D  O' J. B( H( t  P( i
; \0 l2 b. f5 L
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
; q, q& y, _1 i, m- f7 A& j5 u
0 Z3 t: f, z% v4 w$ y9 _5 i" x! O7 x; I' F  I( v
  1. while(fend > 0)7 @' `3 L6 i6 d. H$ g4 G
  2. {
    5 M. y/ n5 U' H
  3. memset(picture.data , 0 , sizeof(picture.data));
    ) u  U: t* d8 v  ?2 p
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);, i$ }& f" Q& T7 X) K
  5. if(fend >= UDP_FRAME_LEN)& ?% E% ^1 S- `. |! [7 F8 N( C
  6. {
    - ^: M: s& T9 c0 q& |
  7. picture.length = UDP_FRAME_LEN;  X! o* ~0 m3 i4 u7 p
  8. picture.fin = 0;
    4 A. G7 ?7 g6 j; e0 |) N+ U
  9. }+ {5 b9 O& L* l
  10. else& I* m3 F+ h: u5 Z
  11. {
    : u$ f9 t5 B' ]" n0 k
  12. picture.length = fend;
    " v! r6 n. e# J2 x  k8 ~. O* Y- g
  13. picture.fin = 1;! @8 `: ?. ]4 }* s1 N' _1 @( [
  14. }* S" Z, R, ?) ?0 q
  15. //printf("sendbytes = %d \n",sendbytes);
    " C/ P2 p7 y) @& B1 u) R( s, {1 `
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    6 P9 o8 C: V2 b+ N7 O
  17. if(sendbytes == -1)
    ! w/ p% A: C8 i. m
  18. {$ n8 t" o4 T; \; A
  19. printf("Send Picture Failed!d\n");' L! \& k+ n2 w! \; a+ G
  20. return -1;4 G* B0 t2 v! L0 O" ?8 G  g$ j" W
  21. }
    . z6 O* @" c; d3 O! Y* _1 A
  22. else
    " `% Q* J: f& l
  23. {
    1 a, d. C& o* |1 C
  24. fend -= UDP_FRAME_LEN;
    6 d9 i1 f: O4 P( t* U
  25. }
    ; X5 O6 ^' z9 i* C% S
  26. }
复制代码
! o1 r2 j& c5 Z8 G4 s: `+ q
# B0 F# T: Q! T! H! s7 p6 t


1 ?4 V4 B, S- q
1 p# H) v. b% Q) O9 g% FiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-2-14 11:25

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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