嵌入式爱好者

查看: 10467|回复: 1

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

[复制链接]

46

主题

53

帖子

296

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
: @0 v; z) a% k% P5 j& \. ^5 {3 Q- r4 [9 j& H

& f4 v1 I$ ~' \4 H4 o4 s, D' d* U) s

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
4 V/ k, `1 G, M6 X
- }& q* d1 g1 G' z. }# p* M

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

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


9 P' v# t& N+ A0 h0 h

- B4 {( C9 D3 s" b. z) [- L- r

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

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

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


    9 A/ G# J  N5 O& z- R0 y; n% K

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

7 c. S' @2 S: K: ?5 @5 ?; q' ?, o
一、HTTP网页服务器7 u% F1 U2 h/ I9 r% ?0 F  w+ f

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)$ Y0 T3 s" O8 u) E, n& O
  2. {0 O3 v& ~( c( p5 t& q8 e
  3.     struct sockaddr_in servaddr;
    5 W% v" c# n: P( j" {# i
  4.     socklen_t addrsize = sizeof(struct sockaddr);# S( F* J0 {. O- E
  5.     bzero(&servaddr , sizeof(servaddr));( n5 [, I: P2 q( x. B) ^& s, E
  6.     servaddr.sin_family = AF_INET;) k; u/ f: t6 M9 r; B; V) r5 L* {
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    8 A% ?, g* L/ m* o
  8.     servaddr.sin_port = htons(port);, M: |8 c, c% y" F: k% U
  9.     int ret;
    1 ]+ Q$ L* \: K  [6 q
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    3 S2 ?6 H. ]2 c
  11.         {
    8 x$ A& j1 n0 M# r" H: x& ~
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    & v/ ]% u! z& o" t
  13.             return -1;
    ; l; g# P) l3 a3 G6 i7 L
  14.         }
    % |/ G( J0 x- S2 I$ Y9 p% r( n
  15.     int on = 1;, W( ?) P# T7 S7 r& P8 m
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)5 }) R5 e# k; h9 _. X: j
  17.     {8 V" i  v3 f4 k$ V
  18.         printf("setsockopt error\n");1 W/ W7 c( m" D! Y  k
  19.     }* o* A* }3 R, s- D  B0 V4 J5 ~
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);( z5 j; S8 N, z- W* g5 S# l
  21.     if(ret == -1)
    6 r: M2 f2 s! T9 D2 p: J" ^
  22.     {4 {0 \) ~+ E5 F* ?1 ^1 v
  23.             printf("Tcp bind faiLED!\n");) r$ }1 I0 g$ x8 d9 c+ Q5 b" G6 W
  24.             return -1;! t$ o+ o6 p+ ^' J5 ^3 ^6 ~% M! v) u4 y
  25.     }& s; V. B' o) P8 T1 X
  26.     if(listen(*socket_found , 5) == -1)
    ) C) V! M) @; k1 ~6 p# x
  27.     {) e. ?5 _* N8 c3 H5 w1 B
  28.             printf("Listen failed!\n");8 c3 r# l) `; C: m
  29.             return -1;  |9 O4 o4 u: T1 k4 p
  30.     }1 I, c6 e( F# @' N$ O" |
  31.     return 0;
    0 h$ r- r! t8 t7 h) g. q
  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);) r. U% g; ^) z) z. _# r
  2. void * Thread_TCP_Web_Recv(void *arg)
    2 ?/ N. N0 F9 |' p- s
  3. {$ i) b; b: w% i$ s. X) d2 w. B! {
  4. 。。。
    9 p3 b* y. r+ [; E6 v
  5. while(1)
    4 N8 r9 S7 H  K6 K! T7 g
  6. {
    9 H% j7 a  a, I, u4 i4 i
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    % I' ~8 s7 }" w' r$ k4 x. A
  8.            printf("fd_socket_conn = accept()\n");- B/ ?; r" K) }3 w
  9.     。。。
    # W! @3 Q" I# w; ?6 R  B
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    , \( ]. T& ?! D3 w1 R
  11. }- o/ d6 [7 ^" U3 U1 h0 h
  12. 。。。. P2 C, |. `! Q, @( T4 S7 W/ q' k5 b
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);+ X6 Y3 i# s/ @% m; r* D5 o
  2.     pic_tmpbuffer = pic.tmpbuffer;8 A* H% X0 T6 [) F
  3.     pic.tmpbytesused = buff.bytesused;
    + S8 S+ k+ g. T/ B' H
  4.     pic_tmpbytesused = pic.tmpbytesused;
    . ^( c. F6 p* t
  5.     pthread_cond_broadcast(&pct);
    4 w* G, X/ u8 h3 i7 j
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    6 X. ]* i: G) I! f5 M
  2. pthread_cond_t pct;; C0 v9 Z' s; Y
  3. int main(int argc, char* argv[])
    * T& w7 {+ D8 S3 V2 I7 r
  4. {* a7 ?) N7 t2 w2 T# R* `
  5. ...# _1 k4 i: X# `8 R& S- X! h
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    % Q  ]9 K6 ^- M! C' J
  7. pthread_mutex_init(&pmt , NULL);
    $ Z+ W* V# T; C# ~+ D% \" x$ e; O1 r
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    , R9 f4 b( p$ `2 h, |) Y( L8 _
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);) X6 M: d( |* a2 }, c' L+ s
  10. ...
    / I3 @: G5 G5 p5 `/ s
  11.     while(1)1 d6 y8 n! F4 X2 x! f9 \" \
  12.     {
    5 p8 \' r% T2 p) `- I, J' g/ X- C' X2 @- |
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);# a5 y* s0 |  a+ v
  14. ...- k- c+ o' A& }$ ]2 G. I* V' ^  [
  15.     }
    % p1 S# P4 K) z/ T
  16. .... l( _- @# O2 T( A1 Z7 R/ J; d
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">9 k1 T9 L7 T) b' o# p6 i
  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" \
    2 l7 ]: h8 _7 r9 U+ I: R; [
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    ! H1 Y6 S: B) \+ n$ L% E5 g: B' k
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \4 z6 @( `3 |8 {8 y: w8 X/ n# n
  4.     "Pragma: no-cache\r\n" \. ^: J. B) d2 l
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    ' p* H6 Z: S  {& }
  6. #define BOUNDARY "boundarydonotcross"% r# y. L% m4 H
  7.     printf("preparing header\n");
    ) _2 Q& D& h1 T  W: U
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \5 I& Q0 p, y4 F8 k- a* r
  9.             "Access-Control-Allow-Origin: *\r\n" \8 L/ o- Y. {2 `  W4 c/ @; H
  10.             STD_HEADER \" l; f. h% [$ \- i) j+ j7 v
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    9 }* R- X. g) W8 Y* l
  12.             "\r\n" \* F; e6 U4 |* K
  13.             "--" BOUNDARY "\r\n");
    4 O! \( v2 R5 [; ]$ W3 Q0 ]6 Y8 f
  14.     if(write(fd, buffer, strlen(buffer)) < 0)/ K$ k- [  \6 s4 t; Y9 B
  15.     {
      {4 U- [1 L7 Y% o1 U
  16.         free(frame);
    & I4 j' R9 v8 n0 j/ K
  17.         return;
    ; D4 R4 l: @* q# n
  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" \
    / q9 L4 i8 J! s" D- Y: h
  2.                 "Content-Length: %d\r\n" \
    " T" _/ i8 O; I$ m
  3.                 "X-Timestamp: %d.%06d\r\n" \
    2 H3 U1 @- P) t! @, Z" P
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);: U0 o1 H- _0 B1 n* v4 s! E) n
  5.         printf("sending intemdiate header\n");6 s( \) w' Z; n; i. a# V
  6.         if(write(fd, buffer, strlen(buffer)) < 0)# K* z  m! g* Y$ o
  7.             break;
    ( f9 K. \# `  f% |; u! w
  8.         printf("sending frame\n");) J  |! C! F* \+ x8 h( V! t- ^
  9.         if(write(fd, frame, frame_size) < 0)# G0 M3 [. ^" P% v5 Y9 P. W
  10.             break;
    ) e& N4 k9 [; j0 e2 Y( N6 i
  11.         printf("sending boundary\n");, R6 J0 {9 ~- e8 q' D& e: V
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");$ }. D% p3 y2 x5 c
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    ; i8 b: ]; l" z8 _6 R/ {9 D4 i; z
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


* b+ N, ^7 O2 x3 }$ i! J/ W$ j( ]

+ O& C/ ^7 U; Y) U
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
5 Y3 [) S* q) S# D3 L, {
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    5 N. R4 n8 D) p; c
  2. {
    " p4 F1 x7 A" E
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);1 y3 [$ e7 P, L
  4.     if(*socket_found == (~0))
    * m+ \  j- L. p7 F9 e# y) N2 \) a
  5.     {* t6 \  {$ U8 w7 J
  6.         printf("Create udp send socket failed!\n");
    , k1 y) W. A0 o! H  A! D: V7 C9 H
  7.         return -1;
    5 W8 g% [% s7 e1 ?6 l& ]9 i- z
  8.     }
    ) o) g" c0 o. N
  9.     addr->sin_family = AF_INET;! I: g. R( P: e6 ~! h3 F1 d" \
  10.     addr->sin_addr.s_addr = inet_addr(ip);) H" s* w5 K, X; `+ E% Y" G
  11.     addr->sin_port = htons(port);
    + E9 C& G! i& O- f$ p
  12.     memset(addr->sin_zero, 0, 8);3 Q* k4 h, b. K/ A
  13.     return 0;* I8 ]7 S0 u0 D/ v& R( V
  14. }
复制代码
" \+ e' _' Q4 E2 i: v% z) B4 I

3 N$ h4 b" ?$ O而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
+ {/ h3 @. x9 w9 P3 g
6 X: x% Z& t8 W# V0 P( b5 r3 n, {7 I. y# C  }  b
  1. while(fend > 0); ]: G* v: t$ p  M' s6 d4 ^4 x5 V
  2. {9 s$ E( Q9 z; m* T- |
  3. memset(picture.data , 0 , sizeof(picture.data));
    ( j4 ~1 N% D1 z1 ~' Z& \- M8 x
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);9 G, j# s7 C* _% }: V4 W/ I
  5. if(fend >= UDP_FRAME_LEN)) {9 @* u+ |# I% Z2 r; d; E; Y
  6. {
    % R" m' y) F! `4 k) M" t, o( G
  7. picture.length = UDP_FRAME_LEN;$ k; p3 Z/ h" d8 b( t( I  D" @
  8. picture.fin = 0;
    6 ?$ i. ]! F2 |# e: E- P
  9. }
    2 r3 p4 F  R6 X7 u5 t
  10. else
    6 W3 _1 g0 {& I) |6 [+ O& ]
  11. {
    . B- M4 X3 n- z
  12. picture.length = fend;7 Y/ h' G! U" g% N! m
  13. picture.fin = 1;
    % _4 O5 S  w. K! q: G
  14. }
    9 e  p2 P. [- }. F. M1 E8 k
  15. //printf("sendbytes = %d \n",sendbytes);
    ( E* x: F: ~8 E6 T  F- j* G+ w
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);3 b$ q' k6 n, _8 X/ S
  17. if(sendbytes == -1), U% C& w* T" q( {# m. r( v# Z4 `3 q
  18. {
    ( v; p! q8 ^' A/ V/ g$ ?5 l
  19. printf("Send Picture Failed!d\n");
    4 g- O8 C5 A! v
  20. return -1;
    ) n( n$ k! j, ]2 |& w+ {
  21. }' I  M3 ~: O4 |% |" j# ^8 Q
  22. else1 ~  Q0 P4 |( [" v% v
  23. {4 d9 A/ D3 a7 s3 R5 q! m
  24. fend -= UDP_FRAME_LEN;
    1 ?# J, b+ L* ~( ]
  25. }7 C9 u* Q# C3 u, L1 r
  26. }
复制代码

6 e0 b# A$ e6 R8 I6 k# ^
2 p" @0 W% G# e) v  g6 q$ ?- M* v

) Q# m( [- r1 W

9 a* C3 l. ^. i/ ~- q* A& x: H- t- Z+ u( y" kiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-12-14 01:54

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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