嵌入式爱好者

查看: 11954|回复: 1

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

[复制链接]

48

主题

55

帖子

305

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 " j' s, n/ i3 d9 Y
/ n; G" n+ ]+ W# W+ \5 P


; ?' k8 z' A: a6 h, i" Z$ k

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html- r) j1 B) q4 ^  B6 M$ i
3 Z0 |4 n4 j% u0 s8 z. j# {) V6 F! ]

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

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


3 g  i& o3 V" R% w, d: |

$ Z- Q5 N) V( |7 `  h$ l$ O

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

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

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

    ) G/ a/ L9 i; j+ n: N7 i; U

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


. t9 i! E6 l0 G: T: N2 a5 U. Z一、HTTP网页服务器5 \5 M1 s: y. \, O

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)' k' L7 p3 }3 j3 m4 p6 u1 [
  2. {
    : g% }* r! ]9 ^  n( {& y: \, N$ I" B) J
  3.     struct sockaddr_in servaddr;6 Y; _3 s$ Y9 Y# l- |" `1 z. h
  4.     socklen_t addrsize = sizeof(struct sockaddr);1 n3 M; d- B% L
  5.     bzero(&servaddr , sizeof(servaddr));
    # C$ v2 A& p2 F! K8 n% d! ]
  6.     servaddr.sin_family = AF_INET;. D$ @  A9 F' D; q* Y
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    - R( A  G$ ]7 R( R, L5 `9 t
  8.     servaddr.sin_port = htons(port);* }% D9 ]. @! @
  9.     int ret;" r3 G' z2 a/ X8 k8 r6 b% N
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    9 D! m, ]; l: a* }) q9 D& n9 N! H
  11.         {! ^" g. ?/ M# ^' b' ^( P- i
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);6 X6 B8 o, Y6 W8 e
  13.             return -1;
      a0 x  U0 W$ I$ _6 z# |' |
  14.         }
    " ~- ^  P" m1 _
  15.     int on = 1;7 S# |9 X1 {, A
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    8 Q+ H9 U1 [! ?9 W
  17.     {
    0 D0 v5 g/ _+ c' H; @
  18.         printf("setsockopt error\n");
    0 A# V- h$ h) ]; ^: o
  19.     }
    7 y* m& M* O. u4 E& g  S8 g
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    % J  `. Z" U% [
  21.     if(ret == -1)
    - a. S, ^/ X1 ^: Z+ |) s+ }1 T: w6 I+ O
  22.     {# v8 E$ h& _; s6 W" G
  23.             printf("Tcp bind faiLED!\n");
    : o$ `: \4 z+ z3 i
  24.             return -1;8 c: w4 i) v4 H+ O5 h! F5 q
  25.     }
    * I- Y% K9 j3 w! p
  26.     if(listen(*socket_found , 5) == -1)0 W. J4 V, F# D
  27.     {
    . ^: f" j/ ~+ p$ q) b  a/ }- a: E
  28.             printf("Listen failed!\n");! d3 h, R. C7 R5 ?/ w
  29.             return -1;$ O- y2 L- [' L# ?6 r! J
  30.     }3 e: U1 H+ h, o! a: ~
  31.     return 0;
    6 B" U! Y: m! K3 p0 Z8 y
  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);
    ( s+ H* A! p; _5 [; I; q
  2. void * Thread_TCP_Web_Recv(void *arg)0 n1 t' I3 {: a4 j' q( K* m( F
  3. {
    : f$ J! L' |) `4 E  r7 e
  4. 。。。
    5 {  I% Q/ c$ q
  5. while(1); J( V- l% D3 P7 u/ l6 f; s
  6. {
    / M, U  r3 r0 X5 K0 P# t5 b" A8 M1 T
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    - O+ x) X9 m3 H; c4 [' V3 r+ J9 Q
  8.            printf("fd_socket_conn = accept()\n");
    " T7 h2 ?. }* I4 T' t0 x0 ~' w1 x( F
  9.     。。。
    5 r* |! i; }( b
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);- R' C+ Y, m# h# g5 P7 D5 p" a( K
  11. }
    $ r- d1 R' n1 |% j
  12. 。。。
    " _0 g5 C4 R! [6 W) C
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);) R1 d; q& Z5 K' g5 e
  2.     pic_tmpbuffer = pic.tmpbuffer;
    & f1 ~9 d+ l4 ]4 v) t- h
  3.     pic.tmpbytesused = buff.bytesused;
    ' ]- e' _; X( b! q, j6 {8 N
  4.     pic_tmpbytesused = pic.tmpbytesused;
    7 [# N3 N# @# `! `
  5.     pthread_cond_broadcast(&pct);9 a* u! a( X+ ]# E9 |2 E
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    + m' x( E# h7 Z: @
  2. pthread_cond_t pct;
    ' w. Y; Y! k. |8 `3 S) \
  3. int main(int argc, char* argv[])
    3 v. _+ f" L! r# E3 z0 c. d) y
  4. {* x" S. E1 F! h/ J4 Z
  5. ...6 q- T; ~% I3 B2 S# R8 E! q  {
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    7 ]- j. R& @4 X7 u; t
  7. pthread_mutex_init(&pmt , NULL);. V6 w. l" y9 D/ h
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    , x) X& S9 b: {# T
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    + X8 J7 t  @4 I' j; F- x
  10. ...
    % P0 S- R6 Y  h4 ~& `& @
  11.     while(1)$ Z- x! `3 ^$ D6 l% j9 N+ t% Z) _
  12.     {0 j% Z$ r& H2 F. B% w, Y
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    # N) h+ ]# ?& W% B1 n7 t; G
  14. ...0 }( r) e* v7 M5 A
  15.     }
    / k$ ]" u/ ^" O- X5 t3 \. G
  16. ...8 W1 A! @7 j6 _: [( |- K" I8 ?- K6 G5 t! o
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">1 s  ~# K* B" E' W6 ~
  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" \8 ]/ @; `( `/ U
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    & H: _9 S1 J5 K1 b+ C' B! q) x
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    4 ]" u) z. {5 k) @) Y0 C3 \
  4.     "Pragma: no-cache\r\n" \% N3 N% _* V- U
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"0 ]) t  `- j% C6 H' r
  6. #define BOUNDARY "boundarydonotcross") z9 N3 _9 n; t$ d( ?/ c6 L( |
  7.     printf("preparing header\n");
      u+ ]/ p; X) r1 E9 R% G$ a0 B0 q
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    7 u$ r/ y; x1 ^; q
  9.             "Access-Control-Allow-Origin: *\r\n" \
    8 W. ~4 H/ E# _; G9 z& s  `
  10.             STD_HEADER \
    ' X( R! [* x" o- j; c
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \/ d, n4 Y4 w2 x$ o' u+ X: \. z
  12.             "\r\n" \
    2 \, N* O. X/ K8 ~8 w- f- [- P, B
  13.             "--" BOUNDARY "\r\n");$ Z9 B- ^3 x6 q% p5 P
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
    5 ]1 S( U* O, z3 c4 r1 d3 l
  15.     {
    7 O, B& F: T/ B9 Q7 r& e' Q
  16.         free(frame);
    * |1 P. U* T# y. T) j
  17.         return;' U! @% @7 a! y
  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" \5 U- i) }, p8 D
  2.                 "Content-Length: %d\r\n" \; m2 o9 p( G1 D/ G
  3.                 "X-Timestamp: %d.%06d\r\n" \
    7 o( ^7 F: y9 |: P8 g/ T; T! \
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);( G- a0 T* |0 F4 g1 t$ I
  5.         printf("sending intemdiate header\n");
    & q) J7 o2 e- I9 c
  6.         if(write(fd, buffer, strlen(buffer)) < 0)+ C9 a: ?4 }$ Q2 j
  7.             break;. p2 z6 q; \" [- [1 d
  8.         printf("sending frame\n");! g5 \6 l1 a; d( M. d) S$ X8 F/ I
  9.         if(write(fd, frame, frame_size) < 0)1 ^) t7 G! u. w( e0 K
  10.             break;
    & U- L. R2 [$ @- Z% p1 ^% l# h4 x
  11.         printf("sending boundary\n");
    0 k3 |9 y" R6 Z# s9 a  ]
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");/ q  C1 \* S0 h& ~
  13.         if(write(fd, buffer, strlen(buffer)) < 0)) P) J3 O1 ^8 t, |# R5 ]
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


/ b7 H" h( R& g  q& }3 R" U* n: R8 O


  s' f/ M5 d; M4 [/ x二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:! ~; R. h# ]8 G& {5 c& x, B1 p
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    " V" B. d9 B& q9 @) t- w4 K
  2. {# O  u! W% r# \1 Y" {+ U- y
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    ( \5 R9 V* F" e. F
  4.     if(*socket_found == (~0))# }$ W$ R4 P9 Y* F5 s# k
  5.     {
    9 f9 p0 R/ b; @. W8 ^: X4 o
  6.         printf("Create udp send socket failed!\n");4 e, \& }+ m7 v9 p! U* `1 g
  7.         return -1;( p  l) Q; R! Y! P- ]) B4 \; m
  8.     }
    ; S' y; Y# \1 v2 c$ \
  9.     addr->sin_family = AF_INET;! v) H$ r( n( T& a& q! o
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    . z! h+ {- Z3 V
  11.     addr->sin_port = htons(port);
    4 U, b: l/ t# K+ X3 g' o8 s
  12.     memset(addr->sin_zero, 0, 8);
    - i; X1 a7 b8 ]! E
  13.     return 0;
    ; N7 G" z) g$ l% z$ |, Q
  14. }
复制代码

3 I$ y& B1 }# r4 R, _0 D" Q) ?# b* h# q6 Q3 t+ a3 t! c: K$ X
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
3 D5 G4 R8 a/ t! |% c* W3 u
" c, y  z9 @5 c5 t3 |; l$ h& S7 G0 M2 Z$ }( X! R+ W
  1. while(fend > 0)9 ]/ u* h( g  E1 \% [6 X
  2. {  Q& B, [6 p) M0 H) F
  3. memset(picture.data , 0 , sizeof(picture.data));
    " U8 Z. X) [! z0 I
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
      S0 J" ?8 ?3 H+ Y; ?" e
  5. if(fend >= UDP_FRAME_LEN)3 d8 @; f8 H0 f
  6. {  B3 ^/ T% G0 V0 w' J
  7. picture.length = UDP_FRAME_LEN;  x9 A. E, i! _7 P
  8. picture.fin = 0;
    & p8 A- n- F  k4 c" {
  9. }
    0 T( c/ i' D3 Z
  10. else
    / g9 B! p# \* L  w$ z8 }7 `. c
  11. {
    6 K& A, t6 l) [/ B8 S9 M
  12. picture.length = fend;
    & b1 Y+ w+ V' m- @
  13. picture.fin = 1;
    & ~2 Y/ h  R- r/ i7 s& L
  14. }/ Q7 i* v  [; g1 \* J/ M& C
  15. //printf("sendbytes = %d \n",sendbytes);
    ' n4 ]& e6 Z* \
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    " y' E! H1 R8 S! ^0 G
  17. if(sendbytes == -1)$ I1 O, K+ h) Y+ e
  18. {
    : S2 @6 `! v; h  R7 n- Y  d
  19. printf("Send Picture Failed!d\n");
    ' @& U; j( ]9 s( _( l0 ?0 V
  20. return -1;0 H4 e8 V# V1 q* n" A
  21. }( J% Q5 Q/ J8 M5 z" J7 F
  22. else+ N% k6 H' g* A/ Q- G* D, S  v
  23. {+ R# A8 U6 n7 V9 v; D8 ?
  24. fend -= UDP_FRAME_LEN;/ R" A7 H' `5 Q4 ^' D, V( f
  25. }# `6 Z" S1 s9 n) p6 N
  26. }
复制代码
) n1 R$ d1 o7 {/ B0 m

8 M+ \; y" s# t2 S* Y% `. \$ q

: S( U1 ?: }4 P; G1 e

+ P  i5 O) n7 U8 aiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-4-30 03:04

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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