嵌入式爱好者

查看: 11103|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑
! u( u* f1 A# [5 l* f* g2 u. t# Y/ N9 q9 {. W


- d6 k" k5 L  b2 N( t8 c

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html4 ^: N, w* I' Y- o1 u* ~( r( J* j+ L
0 M3 M3 Z2 f$ |* j1 J

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

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

% n" ?4 S2 f. r1 _" I) o

2 `4 a: T; i* p& e, y2 R* t

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

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

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


    3 {7 z" Z. W& S* R5 M

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


' U6 X7 ~- t- U4 t9 O6 B5 U5 t; r一、HTTP网页服务器4 O; ~* x6 A* O6 |

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)- ^1 K) u! F' N9 _1 P& y# B
  2. {
    7 K2 E5 J7 V0 b, \5 L6 ]' u5 ?
  3.     struct sockaddr_in servaddr;( a6 A4 R' u8 A$ J, |7 n) B
  4.     socklen_t addrsize = sizeof(struct sockaddr);! v- J: x6 P' M
  5.     bzero(&servaddr , sizeof(servaddr));5 x9 U) p2 t0 B0 e) p# Y5 X+ F5 w/ B
  6.     servaddr.sin_family = AF_INET;
    ( \! I" T, }# f" l+ w" Q" G
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);, ~& ]  {; o* O3 K3 `' h8 v
  8.     servaddr.sin_port = htons(port);+ I3 b( ^1 Z/ ]7 g  F
  9.     int ret;
    7 G: n7 I' R7 J; y+ _* n7 {: [  G
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    - P8 A. a  c* G
  11.         {
    * @5 g5 G4 a9 @, R
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    6 X& U% W: T% |. F! f
  13.             return -1;
    ! ^- N) g# v; S/ a3 Z- `( u% N
  14.         }8 P, F" V- v* z# b
  15.     int on = 1;
    , ^) @  c. F2 b
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    9 d! b) H+ V& W; q3 v  s
  17.     {9 p( z- Q$ ?' w# \
  18.         printf("setsockopt error\n");8 T# F. Y" h2 P1 j( ~  G( U& e' Z) w
  19.     }! L4 y) w+ _: Z; @. I, l% w
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);: W7 j+ v$ i; r1 o& D) [
  21.     if(ret == -1)6 U+ g; X- W( U) b" k0 y
  22.     {8 y: B& x5 `' F
  23.             printf("Tcp bind faiLED!\n");
    7 ]5 ~* o: I# w+ i  I/ |
  24.             return -1;; ^. S! y  F! Y+ ^- S0 N
  25.     }
    & S, D5 f4 B  i" W9 m# x: I
  26.     if(listen(*socket_found , 5) == -1)
    1 G% B5 ~- H* b/ O$ u
  27.     {# w; _* B% A" z1 X
  28.             printf("Listen failed!\n");
    ( n# r& o) J2 i. r
  29.             return -1;; u% f, b# R( u% H( Z
  30.     }
    - U& y5 p6 x7 Y. t& v) A' S0 t
  31.     return 0;
    8 [5 z& _& z) Z3 }3 @
  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);' g$ e. k5 y. s) Q# w8 e
  2. void * Thread_TCP_Web_Recv(void *arg)
    & b' Z& {" V7 ?# l
  3. {. ^5 ?% I! R+ ~6 \) e4 @5 U" Q& q7 R
  4. 。。。$ T. G! M7 E) _; n8 T" O4 o
  5. while(1)
    " h9 s: _" L$ M& U8 }
  6. {
    5 \) B$ ], d- u% ]
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);% Z. b& k+ X( g
  8.            printf("fd_socket_conn = accept()\n");4 V) @) T1 f" z4 E) L! H
  9.     。。。
    0 \1 f1 P  K9 i& a( ?
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    - f6 R" R! `1 e3 [6 X& k. i
  11. }3 n, H: R# \# p1 ^+ V% Z
  12. 。。。' b0 Q: {9 o; [+ d1 |6 B
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    4 S/ v. o! S" v! U6 V) {% @1 r
  2.     pic_tmpbuffer = pic.tmpbuffer;
    6 ?" M8 X1 T% b; Z* h
  3.     pic.tmpbytesused = buff.bytesused;
    & O: E4 x2 a& C& r- J4 H) F: G
  4.     pic_tmpbytesused = pic.tmpbytesused;
    5 g! f( I+ j3 Y5 I
  5.     pthread_cond_broadcast(&pct);* O- D" Y* N) Q7 O5 s0 D  E4 u
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    & W: S* a7 f) v- ~3 `( ^
  2. pthread_cond_t pct;
    : A! h% v+ Z& Z+ y6 V% G5 W. V
  3. int main(int argc, char* argv[])( p, t0 d, x7 Y' N4 @; F
  4. {
    3 Q* P- N9 V% v7 a1 d! m
  5. ...
    * f. f% S: k/ l: P
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    5 U4 ^9 K6 n# n) W% B
  7. pthread_mutex_init(&pmt , NULL);6 A/ P2 ]5 ]  `# X
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);; m: F. i6 N8 c1 b: {% L* V& j
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);
    9 J# c( K; Q1 ~) h+ ]
  10. ...
    $ \- v, w$ J1 O2 e) U
  11.     while(1)5 W+ o# J8 p8 O' Z5 U. P
  12.     {
      p5 e' Q5 w' W
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
      f/ V" s# y( x, S
  14. ...
    ; h- r4 l' }: k
  15.     }' K* [  O9 W0 q. Y3 g/ l  r
  16. ...) V3 `6 w4 b4 u
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    ) S  r& b& T. b/ Q& L; J' i. O2 Z
  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" \7 u9 e. z3 `* u& p" R. f
  2.     "Server: MJPG-Streamer/0.2\r\n" \
    % I* u& u( c) \# O' P' ?' z
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    7 R( f; M3 E& `# h: z
  4.     "Pragma: no-cache\r\n" \
    $ |! G5 g/ A" y. R% F% z8 k
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    ! B! X! X1 V, A/ |2 B
  6. #define BOUNDARY "boundarydonotcross"
    + [2 A7 [2 x7 f+ `& u6 R7 u
  7.     printf("preparing header\n");  u* p: {6 u& W
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \- o6 V# t4 K8 _2 h4 V: a) |
  9.             "Access-Control-Allow-Origin: *\r\n" \7 i. w4 E' K; n# y. H; E) i
  10.             STD_HEADER \" m+ x# E$ J  Q6 ^2 r+ x
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \; N4 p1 ]9 j0 s! H) b! X
  12.             "\r\n" \
    7 ?/ @0 H& T6 j- |/ ^
  13.             "--" BOUNDARY "\r\n");
    ( u  u) e# K9 P4 q- v' k6 e
  14.     if(write(fd, buffer, strlen(buffer)) < 0): }# Y- G9 a- @% a
  15.     {
    $ O) U5 r! V4 l) `' ^
  16.         free(frame);
    . H0 K* `, A" Y
  17.         return;+ \+ m( ^2 U/ O2 A
  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" \
    - z; ~/ ?/ P% h! I6 k. u
  2.                 "Content-Length: %d\r\n" \
    7 G' O) u" S  y) L. h
  3.                 "X-Timestamp: %d.%06d\r\n" \: J0 f1 s% T1 m5 m% K- f) M7 h
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);$ {, C2 \' n  v4 G
  5.         printf("sending intemdiate header\n");$ s2 J" y1 R' Z) \, Z! |
  6.         if(write(fd, buffer, strlen(buffer)) < 0)2 g; M& E! G9 H$ \: o: J
  7.             break;
    1 }# N. F9 X! E
  8.         printf("sending frame\n");1 N4 r' T% m* v; S, I: P
  9.         if(write(fd, frame, frame_size) < 0)# O6 t- k1 n/ i$ G% W! ?
  10.             break;" w7 w8 b+ |- ~2 y- L
  11.         printf("sending boundary\n");
    0 v9 W5 w9 e1 e# H" M* ^# g
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");( p1 X& w- p" a2 f# a4 g
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    - j" S8 q# @! X' B3 l6 j
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

3 w+ f& v7 H: ]+ m' w, o' X+ Y

5 v5 Y0 ]2 `$ R& p; \+ _- [- m
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
: _) H& W- M7 M3 C/ I
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)6 n& y4 F+ ?+ m; E
  2. {
    1 x  |) ?& k3 J* l: z( f% W" Q
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);9 R) E* g! s: C7 I0 B9 Z
  4.     if(*socket_found == (~0))' X" P6 u! Y: C
  5.     {# h6 E$ }6 z+ t! _$ q  \2 O* _( Y$ Q9 H% V
  6.         printf("Create udp send socket failed!\n");/ n/ R2 G7 s) X+ i7 g5 w7 q8 M
  7.         return -1;$ P9 A, K/ x, l
  8.     }. m& z/ V  V) B- ]
  9.     addr->sin_family = AF_INET;' `; v8 Y  z6 w2 r# C9 P, R7 S
  10.     addr->sin_addr.s_addr = inet_addr(ip);2 e* }& Z% F, q$ }
  11.     addr->sin_port = htons(port);
    / q8 f7 g, }8 d/ u5 M
  12.     memset(addr->sin_zero, 0, 8);
    8 \1 e' _# k* L3 f# `
  13.     return 0;
    , s; W4 F5 t. a$ r
  14. }
复制代码
. Z( M! E. \5 r1 k

% l* N. ~# E  G3 S2 w6 n8 s而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
. q$ [1 \0 H0 |3 ^. R6 E$ w. \0 i0 @4 R& {1 H

) g' W) D- G0 l  C
  1. while(fend > 0)
    7 \; v  q4 ~( H* I& L2 V. l
  2. {& ]5 T) g8 E# p* l$ b
  3. memset(picture.data , 0 , sizeof(picture.data));
    9 Q' n5 S+ _9 K# \, c/ r
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);, W, @* y9 z: A3 R  @6 V' x
  5. if(fend >= UDP_FRAME_LEN)6 q/ Z: ]- J- {/ Q' W: ^6 N' G  i
  6. {1 x  F: i* X5 x! X1 T' Q: u8 W
  7. picture.length = UDP_FRAME_LEN;, z( Z2 \& {9 P  }* L$ i
  8. picture.fin = 0;
    ! E4 V8 h$ {3 |3 L. J# B
  9. }9 R! q- x5 v8 d: ~; ~: j/ Y
  10. else( u: e! A7 Z3 \4 {! @# v& J% ]
  11. {
    9 b/ `& K/ @: y
  12. picture.length = fend;
    * ^  p; C3 o* e6 \( [8 k/ C
  13. picture.fin = 1;4 _6 o+ S# c* C
  14. }! g# Q1 r9 C, b2 Z. O% S& Y" q
  15. //printf("sendbytes = %d \n",sendbytes);8 s+ y& ]9 t$ D) Z
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);
    - q3 l; a( h6 |5 q  I6 L
  17. if(sendbytes == -1)
    # m" c0 S5 X6 E% G& X; A
  18. {
    ! D; F& r+ X; H$ X# @
  19. printf("Send Picture Failed!d\n");( `. O! V- i: y$ G9 {( T
  20. return -1;
    % N8 g: Z( L) D6 d. c- c& v
  21. }
    0 Y0 Z" |* F' t. ?' k% B4 g1 W
  22. else
    * w) _4 L* o+ J/ ~+ @6 A
  23. {" I) J( M# p# x
  24. fend -= UDP_FRAME_LEN;
    3 m; G6 ]  R! w* L2 q, ~
  25. }- R2 F1 W' m( q
  26. }
复制代码
: A0 @' Y; R5 |' O2 R
8 f0 G  v1 x0 U8 {2 o: |: e

3 f3 N* l; F# P
2 V6 f2 r0 U: S& r" m  Z  g* J: ?
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-2-13 00:48

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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