嵌入式爱好者

查看: 11300|回复: 1

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

[复制链接]

47

主题

54

帖子

300

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 9 y2 I, y9 y7 L( h2 G2 y7 ?( y" o9 h
6 \- k: _3 S2 n' T


; F: A" b& g) a; ~; S1 a, V

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
0 N5 @) x6 ^) Q2 N# t. ~$ m% K1 |. K- H& ~, G7 Y! H

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

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

1 S; [7 Z% l1 I) p


7 d2 ~, d) E' F% O- W& b+ Y) m

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

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

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


    # h9 j" s; O0 J+ X0 s

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


1 o' W" ?( r7 _4 {# @一、HTTP网页服务器- D- a) ?" i6 F# L

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)8 F% d: k8 H' y4 I3 B( K
  2. {
    " j+ I8 A3 Y8 S: L6 U, O9 k7 p, \1 g. ^
  3.     struct sockaddr_in servaddr;
    , v% q) Y  f6 @8 t# E
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    7 O/ \7 c  |  k
  5.     bzero(&servaddr , sizeof(servaddr));" B( L: `/ \! P9 v1 a9 \: t& K
  6.     servaddr.sin_family = AF_INET;
    # a' d" D( E3 V7 c' _
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    : h! S$ p6 p* V; S3 ~5 T
  8.     servaddr.sin_port = htons(port);5 r: I+ L0 d# J" Q. L; H
  9.     int ret;: m6 L+ ?  [0 q1 {# Y3 u  v
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    8 ~% s: ~0 L' G/ _  `5 O; w
  11.         {
    0 V/ H2 L! Y" U! S$ a& n, {$ @
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);- m* T; T  d" U% @
  13.             return -1;
    5 R. h! z: N# T% w% o5 D
  14.         }
    5 K+ H0 ]7 [$ `; K) S$ j3 i
  15.     int on = 1;+ u# G1 `+ v* n% I) U+ b  ?
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
    8 k1 X3 N% M* \5 `# r
  17.     {, x8 Y5 E+ N+ ?/ |# E' g8 V' l: z
  18.         printf("setsockopt error\n");
    3 y9 @% F& }( R7 y; L
  19.     }( ?' v% w+ T; N( D
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);5 V% Q6 Z4 k! |; z
  21.     if(ret == -1)8 _, s, h+ a1 ^* A/ ]; U" H
  22.     {& {: c2 C- `6 {5 |; j
  23.             printf("Tcp bind faiLED!\n");9 |, n; G$ t' i* ?9 u5 m/ H2 Q
  24.             return -1;  E# s7 h) s, Y# w% r1 v
  25.     }
      O- {3 O* ]% [8 @
  26.     if(listen(*socket_found , 5) == -1)2 B, M* ^5 Q. ^7 l* a" @1 M- N; _/ g
  27.     {
    ; l  H" d- h, ?) K
  28.             printf("Listen failed!\n");
    & U3 G  h- B: g5 i% g
  29.             return -1;6 W. R% Z; h2 \
  30.     }1 ^, y) N$ c% ^# I  ~
  31.     return 0;: x, a: f1 p6 b
  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);
    6 f+ ]6 D- d; B
  2. void * Thread_TCP_Web_Recv(void *arg)$ b* G3 X! F! N7 h
  3. {9 Q. V* Y5 N4 D; q5 [( v0 |
  4. 。。。
    8 K% _1 j$ u* X9 ~2 V. S1 G1 ]
  5. while(1)
    0 u" t& F9 l# M
  6. {
    9 c- h  w# y' i0 i! P0 {. `
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    0 d+ x$ [# B* S" X7 `5 P1 k
  8.            printf("fd_socket_conn = accept()\n");
    ( E% B2 w. T/ M9 L: {8 q, p0 R% H& |
  9.     。。。
    0 W1 ~- I& x! y( w
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    . P1 h" [7 M6 D0 Q( m$ x1 z( N: i
  11. }
    + {* O$ L7 p( {0 [, C% ~
  12. 。。。& S* A* A1 [0 X1 m) S& ?# R1 t5 n
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    9 f; z, u2 a* k3 d- s# a* J# i
  2.     pic_tmpbuffer = pic.tmpbuffer;
    $ d+ L. H- |& N0 P
  3.     pic.tmpbytesused = buff.bytesused;$ l% P6 K) I7 S, P2 I: u5 F2 b/ t
  4.     pic_tmpbytesused = pic.tmpbytesused;+ A# Q0 e# O6 s
  5.     pthread_cond_broadcast(&pct);
    2 n" x5 r% ^' v
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;) y5 Z- A. N2 {: l$ m" A
  2. pthread_cond_t pct;
      J" S6 ~$ o9 l
  3. int main(int argc, char* argv[])/ Y! `. |$ n7 |2 v* A( n
  4. {
    0 ~3 ^0 H* v) A8 u
  5. ...
    5 M: c& s1 Y% {* f0 O" k4 b) D
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);: B, O5 i( h5 l' x* R2 R& @* f% l
  7. pthread_mutex_init(&pmt , NULL);
    0 ^  W! v* p' X& I% h
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);. h2 ~2 P% P# Z$ g
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);  e2 t; Q  L( @6 O0 `* O1 j) u- Z
  10. ...& D; m6 ~2 h  \
  11.     while(1)
    " V& H7 E" g5 x. v2 |
  12.     {
    & \/ }# K6 w; \6 d$ M
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);7 A( L, Z0 O2 \
  14. ...% O  d% S  d  P& |  a$ `: h
  15.     }0 T! U- t6 _' v9 g& ~* S( n
  16. .../ N* }% x. [% H1 D
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    - z$ _' g5 _; ?0 {$ j/ `; 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" \
    # a  g( @  _7 T0 ?* U" K! Z
  2.     "Server: MJPG-Streamer/0.2\r\n" \7 t0 M* _" k6 N( j8 B! `! i  c4 D
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
    4 X4 U3 e3 l2 d" S, y- {
  4.     "Pragma: no-cache\r\n" \
    . n+ @6 l' x# T( Y  j7 I( a
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    % e! i/ }$ |; `/ }
  6. #define BOUNDARY "boundarydonotcross"  I) }# k3 D3 k5 T( g
  7.     printf("preparing header\n");" a$ i0 t7 {1 W  O! ^
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \) {& z5 M- X/ {+ ~+ p1 x! {
  9.             "Access-Control-Allow-Origin: *\r\n" \3 D; L' d3 B( p8 ?5 e9 _2 j3 `
  10.             STD_HEADER \' Z/ b: z4 @9 Y; w3 B7 p0 S
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    7 x( m/ S' t& j
  12.             "\r\n" \
    $ w2 Q. r7 f: a) K  J+ _
  13.             "--" BOUNDARY "\r\n");; U# q& j) w( C1 T) Y& q7 r* q( b' f
  14.     if(write(fd, buffer, strlen(buffer)) < 0)
      U  Q: i' T4 Y; r9 ~9 b& v
  15.     {" Z4 L# s( h7 w- o
  16.         free(frame);. Q$ J  f: j3 _  Q7 E% i
  17.         return;
    " G4 y, b1 ~7 W" A' K! 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 @9 ]# |1 X- w7 `5 c& c2 ?, a- N) {
  2.                 "Content-Length: %d\r\n" \, z( G4 e0 E# k; g0 S
  3.                 "X-Timestamp: %d.%06d\r\n" \* e/ C6 g& X2 k) j6 _  D4 m
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);
    % c* f9 X# B9 j! K
  5.         printf("sending intemdiate header\n");
    1 E" L# W, g8 z! e) o
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    $ ?; g/ J' G5 l+ k* S' {
  7.             break;  P( c1 a$ H8 |& e( U& d' ~: C: M" P* v
  8.         printf("sending frame\n");
    / _) t+ L6 w, Z7 P
  9.         if(write(fd, frame, frame_size) < 0)
    ! J1 a! z4 D1 U7 g
  10.             break;
      T# }2 [: C0 f1 A. ?$ J$ y
  11.         printf("sending boundary\n");
      h( l, ]8 T9 g
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");7 j7 f! F# H; o( h! I3 a" o( k
  13.         if(write(fd, buffer, strlen(buffer)) < 0)
    ( \$ K) @+ c5 f9 \
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

# F1 i% L  c4 L  ~3 c5 N0 |


4 |5 g& u1 Z4 g$ L, W! |6 [3 n8 ?二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
) K# U7 g0 B5 ]# L" w
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)/ V- [; @+ X$ h; G' X3 o* i
  2. {( }# O7 ?7 m  Y0 r
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);
    & V1 W- i* @9 U2 C. a
  4.     if(*socket_found == (~0))
    * C+ J6 e$ s( j6 `0 W
  5.     {
    9 u4 h% ]' I1 e! X% m6 x
  6.         printf("Create udp send socket failed!\n");) p: k2 P' A# n' F( Y
  7.         return -1;
    : b$ F& Q! h! g2 m2 ^: {+ Y
  8.     }
    - b/ r  P( |; S' [
  9.     addr->sin_family = AF_INET;
    / v$ O7 K, {, M+ G5 o: @( Z
  10.     addr->sin_addr.s_addr = inet_addr(ip);
    6 }* I, g) i; @5 C+ _
  11.     addr->sin_port = htons(port);; C  s. s& g- u" ?4 H  D' k% |
  12.     memset(addr->sin_zero, 0, 8);
    3 W* u) ?4 T- @9 `+ W2 s
  13.     return 0;9 C. i) B2 l9 {0 V" {
  14. }
复制代码
, v7 O# X9 e# q
- b7 c6 g  ^$ Z9 h/ J
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:( ^: a# B2 m7 T& b% ~/ v) z
4 _; m0 @, F. C6 H1 [

! A+ n4 q' H( q% p+ o& C
  1. while(fend > 0)" C# x  O7 n7 Y% M& I' j
  2. {
    " ]2 e4 S8 K* _8 |) c% j
  3. memset(picture.data , 0 , sizeof(picture.data));6 L% E: O0 s0 X+ U9 G" E% K4 S
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    ' S, `6 j7 L0 V; p2 m; r
  5. if(fend >= UDP_FRAME_LEN)
    $ ]; p- G) e, |$ m6 M
  6. {
    4 h( }5 F' b4 b
  7. picture.length = UDP_FRAME_LEN;5 |2 J+ g2 K2 H: Q, R
  8. picture.fin = 0;
    # ?( u" Q  H' Z7 ~: E3 @* ?# _, h' |
  9. }1 k9 }- W' |6 C" W4 s' N$ E
  10. else3 d2 U0 o: d! u6 \- X: u0 g
  11. {: u5 d8 c. a, V6 p
  12. picture.length = fend;
    + P/ _# d  h# `& d3 k; g
  13. picture.fin = 1;0 R' C5 ?* ]2 x: F' y2 G) H$ C
  14. }8 O% `7 P4 L5 I4 j
  15. //printf("sendbytes = %d \n",sendbytes);2 M4 ?+ s8 U, p4 k
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);3 P# D8 H0 O! y9 }2 i* t- D
  17. if(sendbytes == -1)( {, K$ v& l1 A7 k6 h+ R& m8 U
  18. {
    2 N4 [! g0 J) e8 V
  19. printf("Send Picture Failed!d\n");2 x8 O; n1 c$ e: m
  20. return -1;: V6 f% r3 G0 I) `# d" f
  21. }7 a3 s; z6 b( b* B. h6 f# O! O. A# v
  22. else
      ]4 W# m4 w0 j! x: Z+ J* a$ y) ^/ |
  23. {
    1 C5 h4 R4 I% j; |
  24. fend -= UDP_FRAME_LEN;
    * ~- A  k, ^" \+ l- ~
  25. }: A* y; v4 c- g+ A
  26. }
复制代码

( U. n! C$ a6 V' o& O6 f) A. b; m  \$ d8 {1 E  @

# E3 u" w. G- }/ p: E. f6 O* v

5 o8 n3 D( |9 B5 J) ?: NiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-3-2 23:41

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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