嵌入式爱好者

查看: 10369|回复: 1

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

[复制链接]

46

主题

53

帖子

296

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 ! I8 _) O' T6 n! M8 ?% ?% I& v/ q" ~

4 _* s( ?+ G, s, H

5 R: ]7 f% r( P& J8 b/ b  b

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
8 I' d( F2 @( E4 s5 o, Z$ L+ Q
9 U3 e4 P' V2 s4 w5 p

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

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


$ N4 J3 q0 F9 k: N- M4 a; s

8 K- S7 z: f/ a7 ~& ?

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

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

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


    , I; B5 Y# L/ w% |; L3 J, C

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


% ^0 t3 l; {! n: x4 O( n4 }- m一、HTTP网页服务器/ y7 L: Y% o/ W3 _2 K4 b4 Q

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)5 ?, u4 ?$ L$ g+ r4 z: I
  2. {4 W# ]$ @, E* i2 ]$ U  A
  3.     struct sockaddr_in servaddr;
    / Y. k% h3 h0 l# o7 {8 \
  4.     socklen_t addrsize = sizeof(struct sockaddr);+ Q) x0 j5 [7 i2 \- {
  5.     bzero(&servaddr , sizeof(servaddr));
    3 t" Y  {4 L- r% h- z
  6.     servaddr.sin_family = AF_INET;7 a' ^5 M- r. j9 k; s
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    3 q/ k0 g- R: T9 N9 g8 f* ]; D
  8.     servaddr.sin_port = htons(port);, f; ~5 g% H% C' l  c7 M
  9.     int ret;1 w& h$ ~' s5 G( z( a9 [  n: d
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    8 p: B: i2 q6 u) `
  11.         {
    5 d; S3 b4 v9 h0 [% i$ E
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);9 m8 ^+ T2 T# L
  13.             return -1;
    ! K0 j3 n' |9 |, ?
  14.         }4 U; [- n4 @: v% n" P8 {
  15.     int on = 1;
    , X5 _& d3 z: Y1 l4 a2 Q
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)3 H& I5 t3 N; \9 i% c: l- V5 s
  17.     {
    5 [  S! f7 h8 b% e' a6 O
  18.         printf("setsockopt error\n");* j% n2 s! |" s7 A9 `. z+ n  w
  19.     }7 I3 H. c6 L3 p; v* \4 K4 s
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);8 p) \& V$ r/ g8 c5 m
  21.     if(ret == -1)1 X  |1 t& @1 L. ~. z
  22.     {' o  {) K+ j; B8 p$ f& s0 |+ ]
  23.             printf("Tcp bind faiLED!\n");/ s3 q" E8 t7 B: i4 N1 y: f
  24.             return -1;/ ^- ~. ~8 O; x8 \9 |, z, H8 J. r/ H
  25.     }
    ! I2 j0 z6 |8 ~  |1 r
  26.     if(listen(*socket_found , 5) == -1)
    # S  o( Y* O+ K& h! ?8 D
  27.     {
    8 ]. R% A- F7 a# t: ~
  28.             printf("Listen failed!\n");
    ' f, g5 M6 Q3 y! q6 _
  29.             return -1;
    ' ~5 d, S: i1 a5 [8 N! J) b8 z- t
  30.     }, F1 D$ K( }, w4 C, r& k
  31.     return 0;
    & S! q% n+ U& g( o, v3 O# G3 m, \
  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);% o4 f3 P6 i- {' \8 g4 p
  2. void * Thread_TCP_Web_Recv(void *arg)
    2 x. X) i6 z0 |) N4 _
  3. {
    + B( q  a8 H1 S! S$ E
  4. 。。。
    ! ?$ k+ L2 F& Z+ {# j7 U+ t
  5. while(1)0 R+ I2 Z" Z, z# w2 ]+ m
  6. {. Z1 O: }* s% _2 w
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    % x3 @& U9 l+ Q% ~
  8.            printf("fd_socket_conn = accept()\n");! g  X' x4 F' B: M6 R% K7 ?
  9.     。。。& L5 X+ h; y9 I) ?/ u
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    ) v5 p1 z% i3 c  ]- R+ X% E
  11. }
    2 W: |. o. R0 x
  12. 。。。- Z8 t( T, @9 |4 @
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    / e  T" w9 x- }6 `% V) ~/ n/ R
  2.     pic_tmpbuffer = pic.tmpbuffer;& A3 u' P: M, c( A& G) E
  3.     pic.tmpbytesused = buff.bytesused;4 a2 N6 l" k+ ?2 W) Q2 l9 ]( O7 Y
  4.     pic_tmpbytesused = pic.tmpbytesused;
    0 E8 `. F5 Z( w5 n4 x0 p6 h
  5.     pthread_cond_broadcast(&pct);
    + ]: I4 Z% r. D" N
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    ; o7 b" ]. |+ b1 }; V' g
  2. pthread_cond_t pct;
    % f$ q! v' w7 [: X+ k
  3. int main(int argc, char* argv[])6 s# v9 b9 W5 s  n
  4. {% s# w% b6 e. K- E
  5. ...( z, o5 k' l7 t
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    % W. ~2 c# Y, M4 c' E, X
  7. pthread_mutex_init(&pmt , NULL);
    2 ^2 o8 P' M6 l3 d3 M, g5 c
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    1 e. J" C$ i+ H8 S; j
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);9 [2 y, R9 d* ~. v
  10. ...
    9 M+ K( j2 v0 D7 W% @# W+ d8 T
  11.     while(1)
    9 u. D$ a: k- O1 N: b2 v" d$ i
  12.     {: ^& i6 R# F# F( |
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    ( ]- v; v) V4 _2 B
  14. ...
    / j' A, i! D' }0 i0 Q' b' B) C
  15.     }. y- |. ?. U- j" L
  16. ...
    & U, c+ t# R9 M/ C" @/ U( K
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    9 c) i. h+ ^' k/ [; d- @; ^- C
  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" \4 G5 x2 O, N1 s# S5 @% p
  2.     "Server: MJPG-Streamer/0.2\r\n" \+ v* W- F: A& l9 e  h0 i7 f5 ?* F
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    0 K- J3 O, ~' D6 H% a8 z) e
  4.     "Pragma: no-cache\r\n" \
    + f* o  }$ H% V" P
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    ' r% ?$ s" f8 r9 ^; P; e; N
  6. #define BOUNDARY "boundarydonotcross". Y( ^, `! s! R
  7.     printf("preparing header\n");
    1 C6 x5 L$ o, f0 h
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \2 n& z6 Y' c# ?7 _4 R0 U
  9.             "Access-Control-Allow-Origin: *\r\n" \
    . C- T- P- P, k! |: f% ~7 Y
  10.             STD_HEADER \
    + d- X+ E& L( a3 V
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    2 _$ l- g$ q9 [% |6 j. q/ o: {
  12.             "\r\n" \
    7 N: v2 j5 g0 u6 l% H% H
  13.             "--" BOUNDARY "\r\n");
    ( K9 K' b4 V* o! G/ N+ Z
  14.     if(write(fd, buffer, strlen(buffer)) < 0): I; J+ a! u  d; ^# \% k% a
  15.     {- @5 D, {) J4 V# i" I
  16.         free(frame);2 a" P% F9 g: H
  17.         return;
    ; X$ U  P' c* ?
  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" \
    # R( W9 [2 c) U& G4 J
  2.                 "Content-Length: %d\r\n" \5 H+ }, [9 w1 _5 e+ T8 B# o
  3.                 "X-Timestamp: %d.%06d\r\n" \1 L) Z) Y" F# R- i
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    : E( t/ X6 F# T7 [$ z2 n! r( k% L
  5.         printf("sending intemdiate header\n");  i' @3 V' p" p# q. N+ L
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    ! O& {$ `8 w7 T! ^7 a6 }/ ]5 N6 R
  7.             break;" J3 S: s8 ^* N% N: {" s$ @. x
  8.         printf("sending frame\n");. [$ @9 L7 `) w8 v
  9.         if(write(fd, frame, frame_size) < 0)/ z5 ~+ q: `) V$ a% F" v6 l7 [
  10.             break;
    1 U+ J4 c, [% f" z- U
  11.         printf("sending boundary\n");; o$ [% H/ k, J# D7 \. Z- ]
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    ; v: |" ~" W' y" }
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    ; I5 x% T1 z# 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指令,从客户端使用者角度来看的效果就是网页一直在等待。

0 o! Z9 ^; {, j  D: P

+ P* h& }/ T9 M& H- i; r
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:! d2 ]# G2 Z; q
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)4 @$ k. H  |& Q% e
  2. {
    / e5 i5 X, i5 Q" }* R% B
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);9 l6 @, S8 R4 T* E4 v0 O
  4.     if(*socket_found == (~0))0 K2 I; w+ [5 U5 O. @
  5.     {4 R) l; X  _1 ~' U
  6.         printf("Create udp send socket failed!\n");
    9 O) M4 d9 X2 C2 L, s
  7.         return -1;
    ) i2 U. q% Y, ~1 q) I7 S* v$ }3 Q
  8.     }
    : K7 Q8 t, n( Q! }' p3 p, v
  9.     addr->sin_family = AF_INET;
      l( h: ]9 m3 I+ O
  10.     addr->sin_addr.s_addr = inet_addr(ip);. C3 p3 O4 \% x' e9 {, E1 [
  11.     addr->sin_port = htons(port);
    2 w, C  P9 ~2 {) D0 `- t8 ]
  12.     memset(addr->sin_zero, 0, 8);
    ( W2 g7 s, C1 g( D5 B* A
  13.     return 0;5 k9 f1 V: T. S6 [( {. Q+ Y* E
  14. }
复制代码

: Y5 ^) z$ I+ R; j+ g( e! P% G6 e' h; o6 g* i+ C0 l3 W( E
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:7 p+ `& M$ ?" N0 y# v2 b
' P6 B/ S' w4 h* x/ P* `/ y. D' V3 O
  y, F- x& ~8 n* V7 b7 H$ C
  1. while(fend > 0)
    7 _& u6 d% @, s* ]4 d
  2. {, T  g6 v# ^$ ^
  3. memset(picture.data , 0 , sizeof(picture.data));: n. h. F1 |( c0 p+ {, w( ]; v
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);2 W/ c+ Z  x+ N! m
  5. if(fend >= UDP_FRAME_LEN)
    5 [* a, N" E1 Y" R3 y6 Y7 N% f
  6. {
    & G: c7 ?: P8 S: e. p0 ^4 ]
  7. picture.length = UDP_FRAME_LEN;6 u/ D% ?* k; l, C( z' [
  8. picture.fin = 0;/ Y$ y8 f- J' u) j
  9. }$ O3 Q; h2 l% D  ]9 u: C8 h
  10. else% a1 P/ y! E! X/ ^* O/ g
  11. {
    / f* q* N  o8 ?* @0 w
  12. picture.length = fend;( C( E$ N$ N% Q0 l! |
  13. picture.fin = 1;8 H6 n/ R1 @# e' H& b4 D7 v; e
  14. }
    ; @! _5 d) ^/ t7 I
  15. //printf("sendbytes = %d \n",sendbytes);
    + R8 [' z2 k: A) R5 _; e
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);! ]; q# w8 T' {, Q. w+ J, _8 D: L
  17. if(sendbytes == -1)
    2 W$ d8 l, @7 h$ n/ [# V
  18. {! _8 s5 p& l# N3 D
  19. printf("Send Picture Failed!d\n");( Z6 ~) ]$ M& G* d* ?& j) P% I: R0 R
  20. return -1;8 m. c5 S5 s6 L  n% Z6 t2 D: I( H
  21. }- a3 Y3 u' v6 @( \; ]% z
  22. else
    3 u* Z4 X( N3 @( @
  23. {0 ?6 b" R9 b. n
  24. fend -= UDP_FRAME_LEN;
      }9 Q+ B3 ^& Y9 \
  25. }/ O) j+ K  a  v4 ~" |! v
  26. }
复制代码
! s3 F, d& P* @3 N0 |

; z: V# I7 W4 d* D+ B: b5 @: r! C4 Q

: }, H- t+ _: t$ h1 D* D8 W. l
+ |! s0 }! i, v4 \( A% v$ r; m& S+ Y
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-12-5 14:28

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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