嵌入式爱好者

查看: 11095|回复: 1

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

[复制链接]

46

主题

53

帖子

297

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 . I& V0 z. H) C7 B

7 v8 Z2 Z- V" P  y# |' ^4 z


6 ^7 ]2 ~) i$ }# @/ R+ d

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html/ D, M" w" A  Q( ~) S

: y0 p# z# C* V4 ?# v2 P1 g3 o

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

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

: i# h6 u9 S0 Y- `! J

- v! A: M2 O1 l1 g

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

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

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

    ' [( H- b8 m" Y; V

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

+ N5 D( T! ?* i4 i9 f* O
一、HTTP网页服务器* D/ {1 o2 [. i4 J% \

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port)1 d2 b/ B; Z% I( K* i2 r
  2. {
    # _" ^4 z" W; _8 |0 D4 u! N
  3.     struct sockaddr_in servaddr;0 B$ I' u( H' A$ E! D' t/ S
  4.     socklen_t addrsize = sizeof(struct sockaddr);
    2 w* q! l+ @$ }6 `) A. g; S
  5.     bzero(&servaddr , sizeof(servaddr));+ C; ]3 ]4 X' Z+ c% h: `4 R' t/ I
  6.     servaddr.sin_family = AF_INET;
    & m) e9 N3 {0 K9 `: [
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    $ _( K; [- d8 o$ B5 j: }
  8.     servaddr.sin_port = htons(port);
    7 F) j; F3 m8 T9 j7 \* B
  9.     int ret;
    3 l3 z  \* }( x" i! f, z  e
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    / [# b5 r4 R, s! h
  11.         {
    5 M. b( O9 _0 `" Y/ M
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);' X/ R0 a6 u) y! j0 n  r+ y
  13.             return -1;
    ( X% {7 j* B+ }! [2 j" p( x; R
  14.         }6 ?7 Z/ @8 Z$ ?
  15.     int on = 1;! c) H" P# K  ]- V8 {% P
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)# N5 T+ {5 P' Y) D
  17.     {, ~, b3 F8 C# m6 O
  18.         printf("setsockopt error\n");
    ' u* e9 y- G. s7 w
  19.     }
    : P0 m" }+ m  |2 A& J- {! Y/ O
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    ( H" M' c1 x8 w& S, H5 o; q0 t
  21.     if(ret == -1)6 v& t& @0 n! a/ C
  22.     {$ n2 Z9 q; R. B6 U
  23.             printf("Tcp bind faiLED!\n");
    7 _: |% U9 a) s
  24.             return -1;
    & Q  K* _- i$ f2 B" C
  25.     }
    5 \% E# G/ Q$ ]
  26.     if(listen(*socket_found , 5) == -1)  d3 o% W+ q$ g1 \+ S$ ?# U
  27.     {5 a) ?5 l" n# o, n
  28.             printf("Listen failed!\n");7 P+ M7 p3 [3 r2 u5 G2 Q6 E
  29.             return -1;
    9 Z/ n: E7 d# z9 Q3 A. k7 ^1 {+ o
  30.     }
    ; e- S+ {. }3 G  h, b( W
  31.     return 0;& t( X" i& I, ^2 C# J& 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);* M8 k1 U& R$ c4 [
  2. void * Thread_TCP_Web_Recv(void *arg)3 Z( j  m) o; W) _$ I$ _  A! M
  3. {" ?* r: T  t2 l- {. I
  4. 。。。( ^, k# U/ `, m/ c$ P/ U
  5. while(1)6 _" s3 Q+ }5 d; ]) {( G! F+ U
  6. {  ?8 k+ N7 t  C# r. S* Z  e5 v. S
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    : W0 f7 c: x$ `" m; c
  8.            printf("fd_socket_conn = accept()\n");0 R1 \. U0 n/ v- D, u/ |* G
  9.     。。。' F% V& J& N3 T6 v  s; C- _6 f
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);
    - C0 _$ ~+ P5 Z" m
  11. }8 z0 s6 N/ e+ x; z5 n" d1 u- d5 _% ~2 S
  12. 。。。
    3 W7 a. ]' a: c
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    3 D4 q8 q1 @; c: v3 C( Z, y9 m
  2.     pic_tmpbuffer = pic.tmpbuffer;
    % K' o' g1 F4 |3 ]- S* [+ h! }4 u
  3.     pic.tmpbytesused = buff.bytesused;2 f3 x9 X7 e9 N) h/ v
  4.     pic_tmpbytesused = pic.tmpbytesused;) `! r5 g) n6 Z" F8 X3 A
  5.     pthread_cond_broadcast(&pct);
    ) W  z2 O  s+ L. T) V5 o
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;
    & a0 q; U0 y+ w% j5 V; V/ {
  2. pthread_cond_t pct;
    ( R. M1 H- j4 n
  3. int main(int argc, char* argv[])
    : x$ U/ E  I1 {( G4 W$ \
  4. {
    , x! |8 e- j0 l+ y# |
  5. ...
    2 E  d5 K5 `- [1 ?* Z
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);
    3 o  M4 R1 X2 ~4 [
  7. pthread_mutex_init(&pmt , NULL);
    - r5 j% v3 S& ]/ T+ L, Q$ g
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    " v! D( o- k0 d% _$ f
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);0 c$ ^* |; r  I; T5 J7 \
  10. ..." p4 F$ Z! `& j' X8 W0 Y
  11.     while(1)5 m3 g: _: y6 F/ y4 q% {
  12.     {
    - F: i; O2 o) }8 O- Z* h1 }
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);  a2 `+ a$ l: K- [
  14. ...; v( H, p7 s& |* H$ l) }' Z- v: |
  15.     }# k9 B2 N/ g; }: |  Q
  16. ...
    " a, W- o& n; C4 r) Y% @9 U
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">( L( b' E" u+ V2 R2 f& S
  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" \
    2 O; ]# j, g7 ~9 j& e8 _
  2.     "Server: MJPG-Streamer/0.2\r\n" \& _8 S  c' s9 I
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \0 s$ T1 m$ {! k9 G" `" E
  4.     "Pragma: no-cache\r\n" \
    . b5 ]& F1 z. n3 T, z% e, w3 ^# @
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    4 Z$ {# X7 W1 B' V$ _7 V8 }
  6. #define BOUNDARY "boundarydonotcross"
    + n1 B2 f' ^( t; `  B5 \9 v
  7.     printf("preparing header\n");8 S. F  }% @) U# K- v0 I
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \+ K( L" z. H5 V7 k3 m! b. Z
  9.             "Access-Control-Allow-Origin: *\r\n" \0 {# a+ o% m4 j* k3 ?+ ~6 P# T! |
  10.             STD_HEADER \
    # M! e" R& x' e8 C0 n' |
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \" }9 l8 E2 R" g7 c8 D2 m
  12.             "\r\n" \% Q4 l. L+ Z, C
  13.             "--" BOUNDARY "\r\n");8 J* c0 t3 L- W4 n. C  k
  14.     if(write(fd, buffer, strlen(buffer)) < 0); c  h' Z8 \. V  m$ p3 V
  15.     {, I4 b8 X  [7 z9 u" o" i
  16.         free(frame);! O  ?4 W" m( w  K8 O* W" K
  17.         return;) h8 o$ a7 d$ h. Z, w% L, i6 l
  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" \
    0 f/ Z/ V( Q4 ?- j7 \4 F# W; E: \
  2.                 "Content-Length: %d\r\n" \# W6 Q; s" J  P
  3.                 "X-Timestamp: %d.%06d\r\n" \
    ! ^6 Z3 p+ d* N( A
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);8 w: {9 Z  L& }% K
  5.         printf("sending intemdiate header\n");
    6 ]. g$ @1 o" |4 c
  6.         if(write(fd, buffer, strlen(buffer)) < 0)% B- [- T4 E1 W
  7.             break;
    " v7 k! {! z  ]- g) z9 n
  8.         printf("sending frame\n");
    # c( X& v! x: W8 ^3 D# i" _# v9 p
  9.         if(write(fd, frame, frame_size) < 0)$ g) ]/ P* ]; d
  10.             break;
    - h$ `% s& d0 g' L/ ]0 I
  11.         printf("sending boundary\n");
    8 ~9 Y4 P4 d) t% M3 e' ^9 Z% i
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");6 t4 G/ J" n  ^% N* K9 y
  13.         if(write(fd, buffer, strlen(buffer)) < 0)# Q" P. _' _/ U: e/ ~/ q& `
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。


* P# H6 U# h# [* N% i7 D' @

& |1 W6 F4 ?+ ]) G- H: @# s
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:* w( P. `$ S: e+ x
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port), b- r) S! h/ e0 X  F
  2. {* P" b$ i3 P' V
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);% X0 P$ S3 N. D+ V1 o7 ~
  4.     if(*socket_found == (~0))
    & D8 t: h/ Y8 T) p
  5.     {3 j4 d) u" Y9 B# F
  6.         printf("Create udp send socket failed!\n");/ N) \+ S) x6 T) \" {0 u
  7.         return -1;
      W6 Q! \: f' ~# B( a
  8.     }1 T, s  Z3 T7 L( h+ b
  9.     addr->sin_family = AF_INET;4 a7 T) F8 ~. e
  10.     addr->sin_addr.s_addr = inet_addr(ip);/ e6 H) S: b+ ~+ J4 L9 m; E+ i8 @, D5 v
  11.     addr->sin_port = htons(port);
    , m3 Y- {& O: G
  12.     memset(addr->sin_zero, 0, 8);2 ?  n4 |2 D$ F& w
  13.     return 0;
    . u/ e3 I: P# k2 R+ C/ `( V) I! C
  14. }
复制代码

8 r" }. y; k' F6 d+ t( D7 e9 ^/ o4 l# x0 m
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:( m+ I. l" ]0 T

0 ]0 E6 Q, O; g+ F/ u) l; X" Z9 \; A; o/ `" k
  1. while(fend > 0)
    ) `* \! F' x( ^# ]1 c3 R
  2. {
    ) Z+ U. O+ k( H3 C' T. \) S: `
  3. memset(picture.data , 0 , sizeof(picture.data));
      h6 c/ q8 u& J6 ]/ C$ i2 _
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    ; a+ x6 i. `( k8 y. I
  5. if(fend >= UDP_FRAME_LEN)  \+ W# k) e+ V& g: b1 e
  6. {: M+ l2 Z1 g; c6 f. c0 W. ^+ s
  7. picture.length = UDP_FRAME_LEN;( V/ Y7 g8 A, @% `0 c" ~; L
  8. picture.fin = 0;
    : k' E( X- ], b  d
  9. }$ J* U. ]: u4 s# x4 L0 C3 J
  10. else
    ' ?' [" R3 S0 f5 h6 ^7 g
  11. {
    # p7 a/ S" Z$ f8 |! D
  12. picture.length = fend;
    / ]2 K5 o% Q2 p
  13. picture.fin = 1;
    ( C( f, u7 m8 V9 V
  14. }0 G0 Q$ ^9 v9 D6 w) i1 N
  15. //printf("sendbytes = %d \n",sendbytes);# L( W, z2 X* z' q2 t' r( [# A
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);! z( u# E2 I* @3 o) W" X3 C. P6 M/ a5 f
  17. if(sendbytes == -1)
    , E. c/ K/ h  E4 s0 \! O# u
  18. {3 {: @  x/ D2 d6 N0 ?, ]  c. E
  19. printf("Send Picture Failed!d\n");
    : x" f& E+ L, E
  20. return -1;
    : S1 z3 W; P+ j1 ]. A* q! J
  21. }
    5 h; B) V( i5 }+ r" D
  22. else
    * C6 W8 g( ?9 c+ s' v: C, o
  23. {
    4 c) v7 N) E; l) S2 l' ~9 h+ O1 h- R
  24. fend -= UDP_FRAME_LEN;
    * G8 d. g; H7 e: \  o1 H
  25. }+ I: q( r5 |- R; H6 n' p
  26. }
复制代码
% G! _! p/ j/ t: v

6 Y/ G* |# N* T1 ?

' V  V3 x; ]3 {- ?3 t) K0 t

6 e' t* M; ~7 f9 F# J% V* C) j  x6 ZiMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2026-2-12 00:25

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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