嵌入式爱好者

查看: 10696|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 . r& ^- B! [. X; m
. _. v8 k! x$ n, d$ w+ Z


) u, u7 s! ]) T& b1 e3 ?& V

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
5 _2 _2 \, H0 @2 R. ?' x4 `; m7 m* v4 ~, S% w

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

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

4 ~9 }* H# j5 T

( B7 y2 u8 ?9 ]7 t* F/ b

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

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

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


    # c6 P% c% X# U, W* R, J$ f) d

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

) s3 P/ y/ r( t1 N$ D$ B, u0 `0 `
一、HTTP网页服务器
8 W4 E* h5 _& T* G( Y6 j

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)+ b/ n& g- l( Y/ n. {9 ~% j
  2. {- j* R" u9 N0 V
  3.     struct sockaddr_in servaddr;6 W# j0 y1 C% f' ]
  4.     socklen_t addrsize = sizeof(struct sockaddr);; Q+ X" O7 }8 B* L+ u9 z# V* i0 q
  5.     bzero(&servaddr , sizeof(servaddr));0 X# G9 X+ I1 Q# J
  6.     servaddr.sin_family = AF_INET;
    % {# }1 e  _1 m: Z- w
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);6 ?/ t4 y& T5 x1 L
  8.     servaddr.sin_port = htons(port);
    9 V  y& n8 L) ]' w/ F
  9.     int ret;, g/ O9 y2 t7 D+ E. ]: p
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)' G" T! R( R; v7 T' G3 b9 |
  11.         {
    : R; t& _$ j1 I! B  Q
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    / y  G0 e* t% E
  13.             return -1;, \9 d# l* u2 L7 M
  14.         }- w3 a8 X+ p4 @0 O
  15.     int on = 1;/ M6 j2 t2 Y1 I/ o
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    ! d$ w8 b. w$ \, I- |6 U  t
  17.     {; [9 i2 K6 P% t& [5 C
  18.         printf("setsockopt error\n");
    " o8 J. C: S; n$ y4 n# S& S$ h+ X
  19.     }
    % s; J+ _# j* h( s" V2 n
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);5 ^' h5 o- }* k+ E/ {+ ^3 Y; O
  21.     if(ret == -1)
    6 i( l6 v  Z- b
  22.     {
    6 E: A$ g5 Z( Y" y! w  a
  23.             printf("Tcp bind faiLED!\n");8 H7 j7 m4 k: q7 n
  24.             return -1;
    : L4 n! A& a5 w1 b0 C( S
  25.     }/ \4 ^7 g$ H$ H) ^; y
  26.     if(listen(*socket_found , 5) == -1)- q5 J5 a# ~' m7 b. Y
  27.     {- K$ W; n6 Q  @9 X- I8 v# Z+ O
  28.             printf("Listen failed!\n");
      z8 D% ]4 S, O( P) w
  29.             return -1;
    % _2 L, p+ @) e; f+ h9 L
  30.     }
    , \- A5 N$ X+ O: z
  31.     return 0;. L5 K$ O6 n) w9 \+ [; y5 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);
    3 B, B" m3 _) y
  2. void * Thread_TCP_Web_Recv(void *arg)
    . I9 F% S$ u9 [; G% d3 m7 @
  3. {
    $ j+ t9 |" f0 i! _
  4. 。。。/ u2 R- l* c9 D+ O) t) {/ Z
  5. while(1)
    / N9 }0 \/ D( A1 N$ h  H3 x; }
  6. {
    ; a" v6 _. h$ b6 M4 M  T8 r% Z3 G
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);5 a4 W  p$ [) f$ r
  8.            printf("fd_socket_conn = accept()\n");# |8 ^, }1 G' @! r! a0 b
  9.     。。。
    % S1 @4 ]% j: h9 X; x/ y( _
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);' M6 m+ T+ ^: y
  11. }
    : R. ~( G9 o  t0 S, t8 X* ?0 v
  12. 。。。
    " V  N8 ~6 Z9 I0 }7 P
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);+ x8 Y4 n/ f6 ]5 M) \
  2.     pic_tmpbuffer = pic.tmpbuffer;
    9 D, h9 u9 {- o# h& B
  3.     pic.tmpbytesused = buff.bytesused;
    ( u0 x+ O' H  G  a2 S7 `
  4.     pic_tmpbytesused = pic.tmpbytesused;
    . r+ \+ X1 u2 h' R. O! {, ~
  5.     pthread_cond_broadcast(&pct);) b8 o; |% L9 E1 n
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;1 L1 J$ U6 U' |
  2. pthread_cond_t pct;9 Y/ T6 @" n* Y
  3. int main(int argc, char* argv[])
    - ]& A; Y! Q$ k$ f
  4. {
    3 o1 p* @/ A( `
  5. ...
    ! x# v. g. ]% p! H9 f4 h8 O
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
      s2 ]4 {7 x& c6 x* S
  7. pthread_mutex_init(&pmt , NULL);
    % n% q3 y$ o0 G4 N: k
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);1 C9 M+ q1 m7 Y0 p
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    ) ?9 ~% h& f/ I' h: g( w
  10. ...
    - y3 [, U- V* G. b
  11.     while(1)
    $ Q' ]" ?( y1 Z" E. V: U& ^1 r3 `
  12.     {5 p9 Z' S$ r5 Q# ~5 r# L
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);. S6 k( D# {- r+ Y
  14. ...
    0 A5 k+ P% W# Q. d0 _) ^
  15.     }
    ! `' Q$ N, r/ h! s/ s- N8 V0 n
  16. .../ D( C* `2 _9 P) v4 o& g. H& t
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">& o6 m! s) ~) L& g& K0 P
  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" \# ?5 q( }  T- T2 x: a6 S: m
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    5 ?* s) V, V* @
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    + f% Z" Q% S3 I* z
  4.     "Pragma: no-cache\r\n" \8 ]& j; G* U; t" [# x; ]/ [
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    9 L- `( O- D" g3 p
  6. #define BOUNDARY "boundarydonotcross"1 u0 {' b/ i; K1 q" E  [2 m
  7.     printf("preparing header\n");. m8 b/ O3 _. Z9 v% y: d& @
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \1 I! j& ~) }3 B3 B$ V
  9.             "Access-Control-Allow-Origin: *\r\n" \' E! j4 r7 h5 A: d/ F
  10.             STD_HEADER \
    / D( b3 e+ f- q5 C6 ]
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \$ T& \$ V7 e8 g2 I9 ^
  12.             "\r\n" \
    - A: T0 k7 @, t6 m( C% n1 O- j
  13.             "--" BOUNDARY "\r\n");, n# H  A' L7 O. _9 q" s: D7 r
  14.     if(write(fd, buffer, strlen(buffer)) < 0)! l$ i/ o* v) M0 r
  15.     {
    + p+ k7 A6 E9 ]
  16.         free(frame);
      y! q: k  K4 [7 E
  17.         return;
    4 U2 s% X' ^7 O+ f8 M* m! U" K/ s8 {
  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" \8 A8 v1 s+ _# ~; Y& ?
  2.                 "Content-Length: %d\r\n" \
    - ~/ i7 M" t5 |0 n# v$ K( M
  3.                 "X-Timestamp: %d.%06d\r\n" \* X# n8 N# o$ p
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
      |. _4 V8 B8 }$ A+ [
  5.         printf("sending intemdiate header\n");( R, U4 K4 u' C8 R% B& S
  6.         if(write(fd, buffer, strlen(buffer)) < 0)! f( y0 d( \+ O! m  O
  7.             break;
    : ^. I: m+ X5 `+ S
  8.         printf("sending frame\n");
    + B) \0 \2 h8 f: H* s
  9.         if(write(fd, frame, frame_size) < 0)/ m: \* T. a$ D: |$ z
  10.             break;
    ' v: E# [8 Q# H5 F2 I0 K0 o
  11.         printf("sending boundary\n");9 A4 r5 b' _+ c( y% j
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");
    , q8 w3 J/ J- h$ |" g! Y) B: S
  13.         if(write(fd, buffer, strlen(buffer)) < 0)& }( `) u( h  C1 ]
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


: k, E' W8 O' l* Y& N+ c0 a; V


; f2 E" Z; R8 T* ]; j二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:- I6 b: ?! N& c* Q9 K8 u
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)9 w6 P9 I( v; d3 e+ R
  2. {
    7 }0 H( p/ L  Z$ Q6 V+ h6 ~5 X
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);' Y$ K% U6 c7 L$ S$ |) I) j
  4.     if(*socket_found == (~0))
    3 w; U( z4 k& P' }1 I# i! ?
  5.     {
    % T) K1 k' r8 t8 m- V; t" F1 I
  6.         printf("Create udp send socket failed!\n");" U4 l4 S' c$ V+ O* J0 P7 H7 f  {  V
  7.         return -1;
    ' k: n: j# P4 p
  8.     }
    $ S, `! G& r% A! a5 T4 h
  9.     addr->sin_family = AF_INET;
    ( |1 L, v* Z4 Y& V) P
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    ( U3 _6 ?4 B# z* S! k8 U, O6 K: r2 b
  11.     addr->sin_port = htons(port);
    & @3 ]2 i" }# H$ o4 t
  12.     memset(addr->sin_zero, 0, 8);
    , |2 ]% @4 U0 q& n1 c  S
  13.     return 0;
    " i  d, A- e, g+ [4 c# I' t9 X
  14. }
复制代码

7 J7 G: c' X9 s8 S0 V$ u  ~( [! H6 I5 ]
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:1 L* Q  `  W9 U9 T: @" q
% z! c  Y: B+ n: y5 d- i2 `

6 c! F0 j) C+ }: B+ Q
  1. while(fend > 0)
    2 P  _( E7 v0 z: W
  2. {/ k5 R" s, U3 w) q
  3. memset(picture.data , 0 , sizeof(picture.data));) G# D9 o8 K8 f
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    6 U$ N% ?0 C. v  ?3 G; H
  5. if(fend >= UDP_FRAME_LEN)
    , h* P& Q. p( L  S5 ]0 {5 P3 t/ E
  6. {
    * S, J. j. S1 F8 d, |, S$ P9 ^
  7. picture.length = UDP_FRAME_LEN;
    + P- t/ ~6 p% [- Q8 n' K- \
  8. picture.fin = 0;' M8 O& b9 t1 `
  9. }% E" D7 C: v. M7 N7 W
  10. else9 E+ z& n. e8 n
  11. {
    8 y. E* k! {9 s( b
  12. picture.length = fend;* |5 N9 {6 Z) |  L2 j
  13. picture.fin = 1;
    : k% f- v) ]* K, _
  14. }
    5 O; }. n# C% b" n
  15. //printf("sendbytes = %d \n",sendbytes);
    7 A' W# L6 z+ A) ^, h( v9 H
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    & E0 Y9 K( K; _  P
  17. if(sendbytes == -1)! w$ b& \7 E2 ?4 `* b4 K6 p7 r
  18. {
    2 O9 i8 [7 x5 e- ~/ P
  19. printf("Send Picture Failed!d\n");& A; ?9 L, K) Y3 v6 F4 H; p2 E
  20. return -1;
    ! r" i0 s, L, r7 n9 H$ Q  t! r' v6 w
  21. }
    . J$ x$ ~( N" s( J! X$ {/ {% l
  22. else
    $ v0 q* e- I3 ]9 _! x7 v
  23. {4 i, J  y/ z3 S$ b" n( C
  24. fend -= UDP_FRAME_LEN;4 d6 f  d$ ]% _' M1 \, V2 d
  25. }
    1 d2 ~9 \) U+ \: v( f! ^* u0 w
  26. }
复制代码

# L! x8 Q/ |% E; _2 x- R. t  u& \( n4 S3 m" o

7 k. d$ D" ?% b& V1 t5 x# r. b! ^  d! A
5 |4 O4 ^3 o7 H$ Q% e8 Q8 O6 Y" r
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-1-9 04:19

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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