嵌入式爱好者

查看: 11877|回复: 1

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

[复制链接]

48

主题

55

帖子

305

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 8 `5 ]4 ?9 q4 V! k* {3 J# ^
3 d" ^( ]) i- G9 ?& g; s, q4 U


- Z4 V9 }- O5 t* L1 B

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html) E2 c' w: F- x4 s

& x. P$ z/ Y$ y- J

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

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

+ W: Q% g: M% d  S


% Z$ i' _( f- b+ n6 I% i7 w; L

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

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

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

    ( E, n# Z+ @* w* p

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

5 A4 ^! Y" _  ~& _* m  t* ~
一、HTTP网页服务器! B/ G+ r5 f6 Q$ ~* P

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)
    ! C" D* v$ M9 K$ ^- N9 n( P
  2. {  T, M" y) ~  U2 }
  3.     struct sockaddr_in servaddr;8 {+ J" i( O- V, n: T
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    - j9 o9 E+ U- Z% m  q7 ~
  5.     bzero(&servaddr , sizeof(servaddr));' ^, m! o5 B' T  ]- o+ k" s
  6.     servaddr.sin_family = AF_INET;2 `" }- a! g' Z7 V, w, d
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);; u" L& J- ^, u) N
  8.     servaddr.sin_port = htons(port);
    $ N7 M1 c& U: Y! p6 V5 v% K* t- n
  9.     int ret;
    " s) W; W4 N, d& e4 @' R4 X. W
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)- V% U8 p& @+ m: ]
  11.         {
    $ S* o- ^/ {" c% U: S
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);8 n. N* D/ W/ S
  13.             return -1;: P9 y" @' K9 q* o! f* @8 ~
  14.         }- s: o! T! q# K" ?
  15.     int on = 1;
    ) E1 l# T+ V- u3 h' \
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    . T( |' O* a# R
  17.     {! m( ^7 ^5 g6 N
  18.         printf("setsockopt error\n");) v( q$ o4 u& G" \, \& M1 o
  19.     }
    $ y. E/ p& C0 w
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    ; \: @9 X& S3 |
  21.     if(ret == -1)
    % K  Q: O3 t, j+ K. g
  22.     {
    ) e+ R# R$ W" y# p. H$ u; J
  23.             printf("Tcp bind faiLED!\n");# h- O: A6 U! S( N/ K
  24.             return -1;" H1 i" `: E2 e6 Y
  25.     }
    ; @' C7 ~9 U  ]- i0 `0 B8 H
  26.     if(listen(*socket_found , 5) == -1)3 r, r' n8 s/ @  a
  27.     {& p! ?3 O! `0 t& |6 @/ h3 @
  28.             printf("Listen failed!\n");, t5 m# }( T$ f" `( L0 X( I# y% ~
  29.             return -1;) \3 E+ j) q9 ]- J
  30.     }- V1 R3 W9 F. m
  31.     return 0;
    ' }  F6 T$ _8 e1 D: I/ `+ T
  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);
    9 `& n- q; o/ |  J9 R2 C
  2. void * Thread_TCP_Web_Recv(void *arg)
    6 [5 A- h/ J7 O2 {" ^1 ^! \
  3. {
      p5 W0 a: _, J% [2 O* H3 u! i
  4. 。。。
    ' N8 N, e  `$ m! P8 R$ A
  5. while(1)
    - S0 |- [- F% `" H2 D. d
  6. {1 P" w( U6 h! w8 D
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);' ^- k+ l' C& ]3 v5 e. J2 ]
  8.            printf("fd_socket_conn = accept()\n");8 l4 s$ e; A% H
  9.     。。。! H) l+ A* f+ r% `6 k0 s1 S, ]
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    ( n* k; T3 c9 o  W0 V
  11. }; b# R' A1 ?0 N# [/ i) V# e
  12. 。。。2 ^' C: M) O" ?3 q2 }- s- |
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    % b+ v1 D+ S: E
  2.     pic_tmpbuffer = pic.tmpbuffer;+ ^& S/ H5 e2 U! e' F
  3.     pic.tmpbytesused = buff.bytesused;/ `( r( b4 ~. R0 D7 l; ~+ w5 C
  4.     pic_tmpbytesused = pic.tmpbytesused;2 b$ `. |7 O$ b, C
  5.     pthread_cond_broadcast(&pct);  F& ?  W% R% k0 |$ x) Z
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    1 B, W( V& w  k+ v
  2. pthread_cond_t pct;
    , |: v# ]  N5 z5 H
  3. int main(int argc, char* argv[])
    3 W3 z, I, B4 |- G
  4. {
    ; K/ @* s& v6 y) d2 l
  5. ...* p9 w; u# O& y- ?) C
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);' _+ s; y8 K' y% s1 A$ s1 H2 y
  7. pthread_mutex_init(&pmt , NULL);
    ) K+ _$ S" l/ y1 M
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    $ ]/ w+ R. J: {1 U$ Z
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);, N# g& X% z0 q& ~/ Z
  10. ...
    5 T8 f' m. [+ |9 Y1 J4 Q
  11.     while(1)
    7 j+ {5 h4 \' {' C; J0 W5 D7 z
  12.     {9 Y  _: L1 d0 L/ e
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    7 X9 {$ U; w. L% b# G7 v
  14. ...
    7 r9 F9 q' c" |$ c
  15.     }3 \3 O+ `' O: L0 ]/ z: l" S
  16. ...
    : m8 _: T. X" r1 D: X
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    ) i  P2 A- B4 M; t  h& [
  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 C1 ~7 ?. b/ R1 @* D% E
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    , |- u$ ?# v! F
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
      e" M- {: V" ~; E& r
  4.     "Pragma: no-cache\r\n" \
    * B: k, J1 R0 x% T( ?
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    7 N4 Q' J. y9 V6 ~: [8 D+ y! L+ [
  6. #define BOUNDARY "boundarydonotcross"$ N8 O/ q4 A5 O# V
  7.     printf("preparing header\n");
    4 C( N4 h$ r; s% a
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
      V6 z/ g6 y& q
  9.             "Access-Control-Allow-Origin: *\r\n" \
    8 ?/ g3 }* G  ^
  10.             STD_HEADER \
      v0 B( o0 V) \+ ?9 }" c$ @
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \) x3 f. i0 }7 k
  12.             "\r\n" \/ J/ `& {3 g8 Y4 A+ M" G
  13.             "--" BOUNDARY "\r\n");
    ( R" ~* t: P: z$ G
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
    ( N% b. o( [9 m: J0 S+ C
  15.     {3 r5 F2 s+ b9 G1 z0 }/ s
  16.         free(frame);6 o% D+ T8 M. I3 f7 X2 s* q# a. i9 B
  17.         return;, a3 X" Q  ~/ S
  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" \
    4 I9 M' E2 s( C9 o+ e2 r  `' Q
  2.                 "Content-Length: %d\r\n" \
    % Q) @. ]/ g; r( O
  3.                 "X-Timestamp: %d.%06d\r\n" \
    2 |1 n7 A6 w4 |
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    ) W0 ?2 J7 J  @* u9 U% k
  5.         printf("sending intemdiate header\n");. x7 X* C0 u4 m( R3 ~5 O
  6.         if(write(fd, buffer, strlen(buffer)) < 0). P& g7 v. E7 b, p) Z
  7.             break;! J7 }; d3 U- {
  8.         printf("sending frame\n");
    7 h; B" v  w1 F; M  t, w
  9.         if(write(fd, frame, frame_size) < 0)
      C3 T; \2 V2 {: [
  10.             break;
    / V: ]* C! X8 O3 _6 c- }
  11.         printf("sending boundary\n");
    / r% b. x3 J# n1 I3 X! A
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");5 y0 Q9 d9 M% Y! q% D. a6 G+ K
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    0 \! d  Y5 g7 X/ v4 m
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

4 M1 C+ n% P# L0 K# C

, a' N- V3 `: q" W! {. E' S1 R8 u
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
7 L& I) Z) |9 l: B3 P  `) ^" w
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    5 Q3 i9 M. C4 i" v' K
  2. {
    8 ~* ~+ f  o6 c# J) v
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);& ?3 U. C) p2 f5 d
  4.     if(*socket_found == (~0))$ H9 O$ v+ j% X( _
  5.     {' S7 ]' V( n7 U1 @1 K
  6.         printf("Create udp send socket failed!\n");8 w3 T1 i7 }: l" ^6 h: d
  7.         return -1;9 j5 P/ B+ k7 }( b& ]! c
  8.     }* y" G* h. h- ~; ^4 w
  9.     addr->sin_family = AF_INET;
    ! |0 o/ o8 x9 Z: D* s
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    + ~8 q5 H" F" }  J
  11.     addr->sin_port = htons(port);- W0 v5 O1 ?0 {) c
  12.     memset(addr->sin_zero, 0, 8);
    7 y! l) a  O$ f( Y
  13.     return 0;9 H4 a# C! V: ]0 {0 N# v- H% l
  14. }
复制代码
/ F6 d  o$ c0 G- p2 ?

3 I. B5 `& }, |5 l而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
+ G6 x$ Q  U6 z  i! J( Z
6 S7 q0 y; S: [
" g  Y% p2 b* J8 g6 b8 S
  1. while(fend > 0)
    9 W6 }( U5 o. f: t% C: f
  2. {. D% ]5 P  D6 f* K  u/ F; d  s5 N
  3. memset(picture.data , 0 , sizeof(picture.data));
    4 r# F2 [7 p6 f
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    3 t& `2 M2 Q0 m' |. G
  5. if(fend >= UDP_FRAME_LEN)
      H+ A  O8 [0 V9 u8 S
  6. {9 j* a+ T) z1 E% q, T% r
  7. picture.length = UDP_FRAME_LEN;
    5 w$ }/ ^8 X! y; i% c6 q8 `9 }
  8. picture.fin = 0;% l/ K! v% n! C+ L& O3 v! u( H
  9. }4 [5 e5 D' k: {6 X" h! d7 l$ Y9 z/ P
  10. else, B/ ~/ B  T& t/ t- p. q/ m
  11. {
      J- `( L6 |$ [* i" \7 T
  12. picture.length = fend;
    + f5 `- F# ~8 R* L# }; s& F5 O
  13. picture.fin = 1;/ X/ @8 Y1 ]0 x2 Z4 z7 M4 {1 p# y
  14. }4 [0 t6 w7 W; {4 t8 `
  15. //printf("sendbytes = %d \n",sendbytes);+ B; V$ t2 a) B$ X1 `" y
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);% P" {: m8 d' Q# C5 F
  17. if(sendbytes == -1)
    1 w4 o( t1 j: D) n5 Z- B
  18. {0 Y' Y$ `0 |8 o+ Y# q
  19. printf("Send Picture Failed!d\n");( `4 ~$ K% [% [
  20. return -1;, I* @' G$ R* J
  21. }6 i! L$ F1 l! O3 K7 d4 E
  22. else
    , K* B6 I8 D' Q7 c6 H/ P
  23. {
    ) c6 N4 Z8 l8 J$ Y* }- M
  24. fend -= UDP_FRAME_LEN;* W  c% C8 T0 K' b
  25. }0 X- X/ q, i* ^: I: Y) |
  26. }
复制代码

% a! c4 T5 I% {! d
! W5 R2 I& U/ R) Z  N$ Z# c

& @0 g  [5 U# X4 ^) }8 M5 J# A+ w
! Z* W3 f6 t! s! T5 G
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-4-24 06:16

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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