嵌入式爱好者

查看: 10269|回复: 1

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

[复制链接]

46

主题

53

帖子

296

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
+ [, H) J+ F: f8 N
8 x0 o( w3 ^* c  y4 ~% ~! z

0 ~& |' B8 ]) R1 w' v, \7 h

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
. ]$ ?, I0 M0 m6 R( q4 T; F! V8 c  O8 S

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

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


! _" T0 n8 a5 W- o7 G4 e* C4 V

1 }- P( U2 U  f1 ~( N; N% R

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

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

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


    / @! g: i- W7 C) ~4 L! c2 s

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

; Q- g4 S$ e: r2 C+ c' d, r: d
一、HTTP网页服务器
& P7 u: n6 p! r( _' h, Z

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    6 l, B4 ~+ K& m( u3 u
  2. {* C, V6 ]  x4 g- ]; l: F
  3.     struct sockaddr_in servaddr;, X+ s# |  j7 x2 X: r
  4.     socklen_t addrsize = sizeof(struct sockaddr);# x5 E  W4 B4 K3 [/ _
  5.     bzero(&servaddr , sizeof(servaddr));% a6 l4 g. w9 i3 O$ _
  6.     servaddr.sin_family = AF_INET;
    : A' `/ \" m" y" m. t* w3 y
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
      V+ V/ e& q6 F# Z/ D# w+ K" {1 i
  8.     servaddr.sin_port = htons(port);
    9 U" @  ?- X6 [) a
  9.     int ret;
    . e5 G( `# R. J+ m+ z: H
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)1 X1 c, A5 ]% F# M8 b
  11.         {, T8 t+ ^- W* x9 I. q- Y
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);! b( n. M; a' p  |
  13.             return -1;
    ( r4 d) z& x( O' Y* k# i3 ~
  14.         }
    / h" [2 c& `" z: D9 _. Z7 w
  15.     int on = 1;
    5 c; H, e- A  Z# k% F7 ^
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    - o3 p  Z! B- H5 v$ k9 x4 E4 Q
  17.     {; q" r( Q% v2 c
  18.         printf("setsockopt error\n");" N7 i, M8 e0 C7 s' t* p; G* [
  19.     }
    8 s5 t9 p% N6 I  S
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    4 t: M! M' q/ U: f, l8 q% n) J+ \
  21.     if(ret == -1)# G9 C% \% ]+ o( B9 @6 m" x8 A
  22.     {
    3 \, H5 k( [: X5 N
  23.             printf("Tcp bind faiLED!\n");) n4 c, s$ h& {; Y) u
  24.             return -1;/ C1 E. x7 f% t) }% Q' Z( O, L* F
  25.     }
    8 Q+ ^0 G" Z. K) R  W0 c" [
  26.     if(listen(*socket_found , 5) == -1)
    / [8 y# O6 j2 }* v
  27.     {
    & j7 i/ n, {/ C! K( H
  28.             printf("Listen failed!\n");2 z+ ]2 G9 Y: F7 F: R$ r
  29.             return -1;: `" c+ F0 }9 o! w
  30.     }$ C# z: Q. U3 J5 i7 A: q
  31.     return 0;, \- V7 x+ P1 T+ i6 o
  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);2 G7 Q" P0 n5 O" @1 ^% r
  2. void * Thread_TCP_Web_Recv(void *arg)
    ; i* g8 B" P+ V
  3. {+ d2 U& U/ P& a- Q9 U6 b; H
  4. 。。。
    0 i+ k% g2 Q8 |8 S
  5. while(1)
    1 }/ C/ X- C. Z6 n8 G' B! C1 j5 J
  6. {: A  p0 p. |0 m: E6 X' U/ `$ K
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    - @( A& U- L8 Z5 j/ W' S/ B9 K( p) t
  8.            printf("fd_socket_conn = accept()\n");
    ( L9 W; C1 _) X* l% f1 b
  9.     。。。" i9 Z! {) c) X3 N" A  ]
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    8 a! i% \) H' |* Q, }
  11. }
    ) j: n- r. z) A
  12. 。。。
    & x' j$ R$ z- o/ C' S$ O/ U
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    8 }3 G' n4 J2 C5 b- d& d& ]0 A
  2.     pic_tmpbuffer = pic.tmpbuffer;
    - p& E( L5 w( B. l3 d- }! E) M
  3.     pic.tmpbytesused = buff.bytesused;9 }0 ?% F, ^3 s3 m) I, P
  4.     pic_tmpbytesused = pic.tmpbytesused;6 S$ [8 G+ |9 k' x+ U
  5.     pthread_cond_broadcast(&pct);
    ; v. n/ |4 {( u2 g2 \9 D" Y
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;; v9 m$ n0 l; `* Y3 }
  2. pthread_cond_t pct;
    " B2 F9 S8 p! w6 ^: E
  3. int main(int argc, char* argv[])
      K+ y, ]4 ^- ~
  4. {
    5 [4 L8 \8 y/ h9 {2 I. l8 p
  5. ...2 e/ L. e# S& a! f7 x& U3 m
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    3 O' |- c! ]$ u! W- c; V
  7. pthread_mutex_init(&pmt , NULL);+ N# Y( n5 _! o+ [7 e" }
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    2 j- `8 z/ m* \+ @; s/ u
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);9 U3 q5 p2 R$ W; Z8 a+ z
  10. ...
    ' B* L; F8 l0 E
  11.     while(1)
    . U4 }) a, f1 O$ @
  12.     {
    " g& i! [. ^4 t9 p0 [6 _& \2 y  O
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);' q+ S( {; K6 C" [' q8 \7 C
  14. ...' e0 a6 N3 g; T0 a2 C
  15.     }
    4 I5 ~8 c1 s7 E. U8 h7 S$ m
  16. ...% S; Y: K+ w+ p; R$ `
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    ' [! {4 Z& R  n  t* p' N
  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" \& \- C  J- r. y. }" T7 o
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    0 y6 \' d* o5 r$ w1 V7 j7 O
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \/ C% }& m- p- w
  4.     "Pragma: no-cache\r\n" \
    + J4 b8 T* q7 x
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"6 m& C- H7 ~8 a7 r0 W8 j
  6. #define BOUNDARY "boundarydonotcross"1 h( }% p) @2 [( f( L+ j
  7.     printf("preparing header\n");
    , L8 C5 T1 K; _9 l# q
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \4 g/ r/ d" c/ B1 J$ \3 \5 ^% X
  9.             "Access-Control-Allow-Origin: *\r\n" \
    ) A' I6 c/ `+ w( T5 k; e  f# @
  10.             STD_HEADER \( N; U! `2 u) H+ ]% t. G2 ^7 `
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    4 k" W' W) W& N6 l8 Y9 ?* G) }
  12.             "\r\n" \! b; |4 [6 Y6 B; S) X
  13.             "--" BOUNDARY "\r\n");+ V# @& b/ n& T) O, C2 q1 {. O' H
  14.     if(write(fd, buffer, strlen(buffer)) < 0)- B# ?2 C- m4 q. `( I# {! M, N. S
  15.     {  i+ c* L" a( I
  16.         free(frame);& _) [7 K8 y/ S0 n
  17.         return;
    / Q4 m  Y+ C/ W! P& j
  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" \
    ( u% P' W7 C7 ^! j
  2.                 "Content-Length: %d\r\n" \# C# R4 N: }$ b& p7 l$ I% p
  3.                 "X-Timestamp: %d.%06d\r\n" \
    ' A. R, D9 i& S! E& N1 U5 Y
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);! x& f  s6 Q" c+ r
  5.         printf("sending intemdiate header\n");
    ( p# N: x$ g, M' S) m% u
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    * R) l# }; [) e/ Y' q! C/ W# b0 O
  7.             break;' Q" S7 g) Y, K; o( y; h
  8.         printf("sending frame\n");( V  @1 K' K, k/ _& l3 D
  9.         if(write(fd, frame, frame_size) < 0)6 {! T, t* p2 t; L4 L& G4 U
  10.             break;+ Z- ^( ^$ c; T' Q0 z8 ?
  11.         printf("sending boundary\n");
    % j1 u' R% q% ]6 \5 d( S. S; K' L
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");5 F6 z! F, P+ ~7 E$ e( y
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    " z6 ?7 n$ y! R+ s" r
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


9 n" h6 `7 A' D" b* H

8 L0 X) N3 k, b5 p( _/ z) V& e4 v
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:4 D2 h+ V# T% I% |# f3 j
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    3 \4 Y% v( ?" A* _' T: `
  2. {
    . k" ]# J+ J! h  s: j4 F8 B
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);. X. A1 U4 \. o2 m, f/ z6 b, b
  4.     if(*socket_found == (~0))
    / M5 T% a8 c5 n0 H
  5.     {
    " _- t+ X, l" B/ Y. K+ S
  6.         printf("Create udp send socket failed!\n");
    1 Y1 I6 p! ~! M; H* }
  7.         return -1;) A- w1 j' \# |+ z& O7 ^
  8.     }
    ' s4 D: x" e  _
  9.     addr->sin_family = AF_INET;
    ; F+ X* q1 D2 J
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    0 ?- d* |! N8 Q/ W3 H  D0 \! c
  11.     addr->sin_port = htons(port);
    . q3 `3 P- K$ A: {8 B; }
  12.     memset(addr->sin_zero, 0, 8);% b3 }, l5 @" [6 [# l: u; H% t
  13.     return 0;
    , w( i) \( x" {3 |8 O
  14. }
复制代码

1 N. ^& W6 I) ]* A/ f' }! _" I0 A; A5 F
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
6 @. T. j6 E4 W  x& H6 D: T! @4 O4 f- [  l0 Q* k
- w2 M( D) Q) Z6 G) {
  1. while(fend > 0)+ H- q8 b, M& P* P9 Q
  2. {
    ; B, C  h/ v0 s2 [2 a
  3. memset(picture.data , 0 , sizeof(picture.data));8 w* G7 |! N0 p3 L7 a- p+ A
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    . f) `. R& @2 {! w* f  H( D' @
  5. if(fend >= UDP_FRAME_LEN)
    3 O$ }* i5 x- A9 w1 I* D% _
  6. {
    7 g# Y' x- M5 c0 D  S- E+ ^2 E* a
  7. picture.length = UDP_FRAME_LEN;
    $ h' Z$ z/ l" `0 Y
  8. picture.fin = 0;
    , v$ H4 D( B: l. \# @* S$ g
  9. }9 Q  X+ O1 n% I2 s. `
  10. else1 Q( F5 O- ?. G* z4 l% K: t/ w
  11. {$ L: Y- O* x: A/ S
  12. picture.length = fend;5 x6 @' M- A7 R+ `. `* W3 r5 T& a) [
  13. picture.fin = 1;
    / V; @1 Z' X  f  r% a: R
  14. }
    - Z6 @' U! @% k2 w. d
  15. //printf("sendbytes = %d \n",sendbytes);- ^( K3 j- R( e6 x, J
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);, z. a7 s: R# J
  17. if(sendbytes == -1)1 D. r, t5 g+ F8 K
  18. {! k: V* D8 T5 h; R$ B& W% p; y
  19. printf("Send Picture Failed!d\n");
    3 h- ~* ]6 M0 @6 B8 A* k
  20. return -1;
    ! G2 e& O5 ^2 I. H1 C+ p# ?
  21. }
    . g4 L+ b& ^2 w3 ~
  22. else
    2 j+ Z) K" A% V5 q  O7 d( K9 W* Z
  23. {
    ; M4 L) c2 }# l, @0 j
  24. fend -= UDP_FRAME_LEN;( \/ x7 S* I% q1 g* D6 c
  25. }
    2 p! b4 p: a3 Q; \' R
  26. }
复制代码

6 E) t' z, i1 C5 f2 G/ x" N8 q; K+ F  @6 H# c


4 ]! [/ i" R% X7 K3 x9 @& b3 ?
; @4 L9 d( W- _) G0 y# HiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-11-25 07:35

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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