嵌入式爱好者

查看: 11311|回复: 1

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

[复制链接]

47

主题

54

帖子

300

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
+ M: L+ `( o1 C: C# n- W$ C" p
% l7 @7 E9 r: k7 U! P% h8 q' J

7 l" v& u& j' ?: v' K

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
' U( }2 p: I- s4 S* \4 I* {
/ R% P. E* H7 O* `

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

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


3 U5 Y7 N6 S" P. N0 s" V


8 N1 z1 M- C- H( U

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

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

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


    7 o" y6 h2 F* ~( [

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

( E4 ^) [" f6 x( `
一、HTTP网页服务器
( Z1 C' f" v* x6 j3 {

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    9 Z/ t) w/ J0 l! n
  2. {
    ) S9 [1 Z* ^! L5 M
  3.     struct sockaddr_in servaddr;5 J$ K, r  l$ }- Q! O
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    ; W+ T4 G5 u$ q) B7 {/ p5 A
  5.     bzero(&servaddr , sizeof(servaddr));5 a2 O* M- v+ j/ o
  6.     servaddr.sin_family = AF_INET;
    + t9 |% X' P0 n* x$ ~5 W
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);0 _' ]! `5 m! b
  8.     servaddr.sin_port = htons(port);+ s4 s6 e8 p2 `) V
  9.     int ret;+ t3 R* C% ^/ N! U. F, d8 b/ o
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)4 G1 H) p  R& G! i! I9 b; _8 I
  11.         {
    - I, ~" u  @+ x# n& @, p, H
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    1 {/ @% Y; I' n' ]5 R) j
  13.             return -1;
    0 U. e3 {8 b2 |$ x
  14.         }, Y6 j5 n  E# e6 f
  15.     int on = 1;
    5 g, f) k" \% c  S' \6 \
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    6 s+ u. b1 U5 M0 M6 f3 X( h4 M
  17.     {
    & S& x' K! x2 _: e0 t4 i1 {
  18.         printf("setsockopt error\n");( G: @1 ^# Q" A* U" H
  19.     }
    % X8 m5 s* s1 ^) l4 |& g7 F
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    + x, I* p4 E$ Q2 K3 K
  21.     if(ret == -1)6 q1 |" }: H& c( R, z$ s/ U
  22.     {
    7 p& u- v& D$ O7 ~, `' Z1 N0 x, x
  23.             printf("Tcp bind faiLED!\n");* P$ @! S: b4 Q' l7 l( q6 A
  24.             return -1;
    9 a( V$ L. b$ }# F. f& }: J
  25.     }/ _* x2 K' M3 c& X! b2 W0 P
  26.     if(listen(*socket_found , 5) == -1)
    ) W" _9 A8 X3 \0 f* o% _4 a2 A
  27.     {
    5 j. [4 j1 p" i  T# O
  28.             printf("Listen failed!\n");% X+ t9 R  n( q7 o2 Y% n1 ?
  29.             return -1;$ c7 [% `( b' Z. U5 o' Z3 e( @
  30.     }
    2 n4 A4 r3 R4 Z( x- L# O
  31.     return 0;! {. u" S& [0 w  p  u2 \
  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);
    $ J( X+ M, R& w6 J) {! M
  2. void * Thread_TCP_Web_Recv(void *arg)
    ! W) y+ A# K* A. c
  3. {
    2 J/ }+ \: r5 {# l4 p' N
  4. 。。。5 @$ B( ^. z2 }; F( a1 j; N
  5. while(1)
    & o: o; R' t. ?& J/ h
  6. {: c. d: s' ?1 V
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);: F* G3 [, S% B# c& y4 r; d6 b
  8.            printf("fd_socket_conn = accept()\n");- `; y$ K! Y& q$ |  b
  9.     。。。9 R7 l5 ?1 i9 H# k3 S% E9 r7 Q. P( _
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    7 d0 N8 F/ A* `1 [' {- a, ]9 J2 [
  11. }( y# X( X; \7 w9 R* K) ]2 X
  12. 。。。
    - I( m, F) D  H: z
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);: ^8 T( w, E  m/ D2 r; W; w! h
  2.     pic_tmpbuffer = pic.tmpbuffer;
    6 Q. k9 e* S; A
  3.     pic.tmpbytesused = buff.bytesused;5 B& q/ t* U0 ^2 M- x" s
  4.     pic_tmpbytesused = pic.tmpbytesused;
    ! N8 y/ G* D7 o( {  c
  5.     pthread_cond_broadcast(&pct);$ }) f- H" z3 b' L# s
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;+ c" P: V& O) H9 w6 `' b; c6 ~
  2. pthread_cond_t pct;9 A9 w2 w/ R  H4 g$ E2 N; _
  3. int main(int argc, char* argv[])
    7 B8 y! _! O, n% E/ `: k
  4. {7 Z: L3 P" Q6 w8 a
  5. ...
    9 e8 `7 A0 X0 ^( X. l, w* i
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);' }9 }* K$ v2 N+ s0 V+ \1 @
  7. pthread_mutex_init(&pmt , NULL);
    ; L. ^* j  {8 z& i- ^
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    + ?: F/ c+ _0 K7 ?
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);4 ?2 k" |( ]/ t; {
  10. ...6 e" \2 h! {+ |
  11.     while(1)
    - q! h+ ~: A: x0 ^: B/ a
  12.     {0 E' O4 l1 q0 f
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    + }& k5 ]7 T/ l. G& S
  14. ...
    9 G! ]1 w; a4 R. U+ d. A
  15.     }
    1 y# |/ S, l# X  t+ a# t: {
  16. ...4 V0 U5 t$ T! Z/ ]' h' `: B) w$ S
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">8 `0 X& l5 y: 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" \
    7 k4 A4 j- p! Z0 I3 A
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    ; M4 B8 z, B- x" Y
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \8 T! u" X5 C8 Y1 J, o
  4.     "Pragma: no-cache\r\n" \8 v9 d; u6 J) d% l2 ?( ]
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"1 G) A% P4 \' }: s
  6. #define BOUNDARY "boundarydonotcross"
    - m# k5 @: t' a% @7 t8 @
  7.     printf("preparing header\n");2 W; L! [7 \" A- \9 n
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \1 W+ a  t" d8 o
  9.             "Access-Control-Allow-Origin: *\r\n" \2 n$ A' R+ w: N
  10.             STD_HEADER \
    ) S, T7 S" @6 d4 `
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    7 W4 u6 l5 O+ S3 y( W7 X
  12.             "\r\n" \6 L0 e4 U. x" J
  13.             "--" BOUNDARY "\r\n");
    ; y' G& J0 {5 ~) M. M; {" S
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
    - ^0 h9 B4 k! {6 w. B0 ?" W) A3 Y
  15.     {
    ' H5 o& z8 U( O  \: o
  16.         free(frame);
    ; \7 N" F: A' j+ x$ O/ L5 R
  17.         return;
    / r- u' M  \1 X9 s: 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" \4 b6 n: m) K6 |/ E% y) a' M; @3 O
  2.                 "Content-Length: %d\r\n" \8 Y5 a+ p: q# |8 Z' G" _" ?
  3.                 "X-Timestamp: %d.%06d\r\n" \# h1 Z' a1 I+ w" S& R- Q
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    $ a3 n2 [0 l0 J5 A  E1 h
  5.         printf("sending intemdiate header\n");
    7 H5 z) ?1 [" R/ k- C
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    " x0 [/ {) o! s( |. N7 I6 }* K
  7.             break;
    8 L/ d# R9 |; L( D/ A2 s; ?; _
  8.         printf("sending frame\n");
    3 U3 n" u: R; N0 a9 P' |* S+ P# U
  9.         if(write(fd, frame, frame_size) < 0)
      e9 L5 `) C* Q' ]! k* ^% \" e
  10.             break;
    ( d* }8 H0 |. d# u8 C
  11.         printf("sending boundary\n");6 @" p- L  Y3 W4 b" `( o0 N5 S
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");  ~1 o" G. m- b& C9 l
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    . r: a, t! m3 g% h6 C; O9 g
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

7 U1 B" t! L, {

3 q! U7 s3 j$ k" g& t+ k$ r& V
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
0 i7 y' o( V2 H7 t7 P
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)$ n* `7 @! ^$ h3 O; ~  S$ N
  2. {
    ) @8 B, O- |! J& D# ]6 C
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    4 A4 e5 w( t- J
  4.     if(*socket_found == (~0))
    8 y- l3 T6 F' c
  5.     {
    6 [! h+ x- k' p
  6.         printf("Create udp send socket failed!\n");; S1 P' B$ E4 c; Z: c: D
  7.         return -1;. k" `0 M' V" T0 B
  8.     }
    ! P" y7 e& {$ @, M7 R: ^1 w) J
  9.     addr->sin_family = AF_INET;- M5 R, O5 K2 ]
  10.     addr->sin_addr.s_addr = inet_addr(ip);, {- p# q5 Z: N
  11.     addr->sin_port = htons(port);
    % A1 P+ Z7 ~, i) U2 e2 {
  12.     memset(addr->sin_zero, 0, 8);- V2 {7 [, i  b% T$ K
  13.     return 0;# {1 I0 d. f7 j8 B# a
  14. }
复制代码
& Z. H# q0 h& v6 b+ Y; ~& \
6 S4 S9 L4 b0 M) ?# _: A
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:5 E5 n: N6 g& f. p) ?* U
. Y% \1 j  \1 _3 a" k# ^7 l3 r

4 V3 Q6 N8 e3 f
  1. while(fend > 0)7 `- f8 c  `, i# w
  2. {: L0 O9 N* Q7 h, B6 g, L. Q) q  l9 o
  3. memset(picture.data , 0 , sizeof(picture.data));
    1 C) i' U& I' g) {3 B. ~% r
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    8 {+ Z( B0 j) [! K2 ^; I
  5. if(fend >= UDP_FRAME_LEN)* a3 E. n3 E* t
  6. {# m  a* L; v8 V+ i% x) u+ d
  7. picture.length = UDP_FRAME_LEN;
    % J3 @! T$ S8 D* n  I
  8. picture.fin = 0;
    " A  y9 ?7 K3 V1 y1 R5 }+ ]: b+ ^& D
  9. }' l1 m3 n3 w6 y! U7 ]9 S
  10. else3 J* `9 f2 D" e3 f
  11. {
    $ K) g( r0 k4 A
  12. picture.length = fend;
    ) H) A3 F, G+ ]" m  w1 F
  13. picture.fin = 1;
    , A) x! [" G3 L5 u
  14. }
      O6 W+ t, b" g
  15. //printf("sendbytes = %d \n",sendbytes);
    & s: i% t) o# s6 V$ F/ q
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    5 J" J/ n) A$ M
  17. if(sendbytes == -1)
    ) L  b  v$ K4 M$ p9 A1 W9 X
  18. {
    2 H* f" ^+ z! L% a9 c
  19. printf("Send Picture Failed!d\n");* J6 }1 w/ E7 Z3 U( l' Y9 x
  20. return -1;
    - s1 X4 M7 f0 x7 {: E' Z
  21. }
    9 A( u! k( a: ~( \. e3 w+ u
  22. else
      N) s/ O- q5 B8 {2 m
  23. {; X# h. G' n$ L- S- r! v# O" J
  24. fend -= UDP_FRAME_LEN;
    8 }* r5 `& k, G
  25. }
    4 t' `! ?5 p5 q7 b6 \1 X
  26. }
复制代码

3 a1 }0 j( a8 T- w. n7 P* O: Q- J( Q; y' Q. n" I2 s3 |. f


8 r% ~  Y4 O. {  ^# Y  p) @5 Q' q; q) V% L
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-3-3 23:14

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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