嵌入式爱好者

查看: 10527|回复: 1

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

[复制链接]

46

主题

53

帖子

296

积分

扫一扫,手机访问本帖
发表于 2022-1-27 10:54:18 | 显示全部楼层 |阅读模式
本帖最后由 飞凌-marketing 于 2022-1-27 11:00 编辑 ' d6 p8 o4 p+ l

, R& [. ~% n& \5 o0 y


/ P4 ?8 I( u" K, E6 ^1 O

作者|donatello1996

来源 | 电子发烧友

题图|飞凌嵌入式

iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
4 B3 h$ S! x3 U4 Q/ B; h
5 Y) T* q2 b# j* [

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

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


0 t7 q: ?1 J: H9 ~% \; r# E

1 \) d% J+ d; C1 R

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

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

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


    % E( b/ {4 u8 l# Q  E% k0 F

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

( B, S# v2 |+ ?; N  d  K! T% q
一、HTTP网页服务器' w* Y$ y) ~! G6 W4 v* I

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

  1. int TCP_Server_Found(socklen_t* socket_found , char* ip , int port), A6 @/ j9 H9 Y3 S6 f
  2. {- M5 f9 N8 [; _- V% Q9 D* B
  3.     struct sockaddr_in servaddr;% H& {/ Y* S2 t5 m7 G) n  X: H
  4.     socklen_t addrsize = sizeof(struct sockaddr);! M) i& V1 [9 B( T) {  @, n
  5.     bzero(&servaddr , sizeof(servaddr));; S" h: `7 M6 F1 {" k( R3 a$ s
  6.     servaddr.sin_family = AF_INET;
    $ I, [/ Q/ j% \
  7.     servaddr.sin_addr.s_addr = inet_addr(ip);
    9 G# u, g6 Q) G/ Q0 A8 w; V, R
  8.     servaddr.sin_port = htons(port);0 @; \, O! H( r" g: H4 L! H
  9.     int ret;3 B6 u3 i+ x! f0 @" ^. T2 I, r7 L
  10.     IF( (*socket_found = socket(AF_INET , SOCK_STREAM , 0)) == -1)
    + u1 @/ k0 e5 L( F, d+ Y
  11.         {; a+ T9 p2 d& w/ r
  12.             printf("Create socket error: %s (errno :%d)\n",strerror(errno),errno);
    ) d' n/ y+ {! @1 n
  13.             return -1;& C5 h% \" E$ I9 [' V; P+ L
  14.         }
    , [0 {9 d% k. O) g6 g
  15.     int on = 1;
    4 F: e: P6 R9 W
  16.     if(setsockopt(*socket_found , SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)0 s  O1 q% @. F0 a% D+ I
  17.     {
    ' a% j: N: e8 Y( R3 x
  18.         printf("setsockopt error\n");/ X' [9 h! B! G# F8 w# \3 j/ m. }
  19.     }
    2 T% M8 e5 S8 v: q4 H3 g# f% j. D
  20.     ret = bind(*socket_found , (struct sockaddr *)&servaddr , addrsize);
    ) T, h% g- D; L3 O$ y7 g
  21.     if(ret == -1)
    - M6 r* y/ h. F. B- L  @% D
  22.     {
    8 i& V# r( a6 ]4 `. f. T
  23.             printf("Tcp bind faiLED!\n");( \, V' H% l; G
  24.             return -1;# J- E' X" _: c: y1 D4 X( s
  25.     }$ L7 K% O: T0 K9 v7 |. Y7 s
  26.     if(listen(*socket_found , 5) == -1)
    : `# J/ o* X+ ?! G+ B$ Z* {, |
  27.     {
    % r6 a2 {8 ~, t* G( ?
  28.             printf("Listen failed!\n");% h: G. L( W& b2 n! F* s5 F) h
  29.             return -1;/ D# f8 J, B" U9 n/ {) T- m* [
  30.     }
    + k9 t8 r! P' G1 K7 o; t
  31.     return 0;$ _+ Z' I; W% u7 ^  H% H
  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);! V. R: R( t  h! U& w1 W( F5 |& S3 W
  2. void * Thread_TCP_Web_Recv(void *arg)- Y! i  R0 L& ?
  3. {1 W% o3 z4 v, s- \1 X) L
  4. 。。。. _" d2 c) w# F) h7 M7 ]" V
  5. while(1)
    % i/ V$ a& y/ i. {' Y; O- X/ w) @7 y
  6. {9 b3 M1 O" m3 b* e( \( Q
  7.             fd_socket_conn = accept(socket_web_server , (struct sockaddr *)&sockaddr_in_conn , &addrsize);
    . B- f+ p) R' |1 p6 `. L
  8.            printf("fd_socket_conn = accept()\n");
    ' ^- b" C0 J+ I; i  Q9 R. }* G4 L
  9.     。。。
    & t4 W0 y: c  e$ D5 M% }; Y) g
  10.     recv(fd_socket_conn , recvbuf , 1000 , 0);( Q) E* {1 m8 o* x. W
  11. }
    " P6 K$ Y+ l* D- U5 e
  12. 。。。' F* Q# z+ R% D
  13. }
复制代码

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

  1. pthread_mutex_lock(&pmt);
    ' B$ x- P% }$ Q
  2.     pic_tmpbuffer = pic.tmpbuffer;' Z8 ~/ `6 y' \5 e$ x
  3.     pic.tmpbytesused = buff.bytesused;
    " ^1 U5 l0 `" s8 E2 K" |9 @! q
  4.     pic_tmpbytesused = pic.tmpbytesused;
    9 V: t2 l9 M6 G$ r6 S8 b
  5.     pthread_cond_broadcast(&pct);0 F6 Z% E" B7 I+ f8 u2 D( y
  6.     pthread_mutex_unlock(&pmt);
复制代码

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

  1. pthread_mutex_t pmt;/ W, k, P# Q: R) h( l! P
  2. pthread_cond_t pct;& J7 E& t, L" ~' O5 L) J
  3. int main(int argc, char* argv[])- G, w# t' y5 B# @6 J$ l' N
  4. {: U9 s5 R. w7 R7 v& d! |
  5. ...
    & }: g% O- B0 N
  6. TCP_Server_Found(&socket_web_server , (char*)argv[2] , PORT_TCP);' d$ D+ ]4 m0 o( H3 z4 n6 s
  7. pthread_mutex_init(&pmt , NULL);) Y3 l. q% _+ u7 u5 k# E2 X
  8.     pthread_create(&tid_tcp_web_recv , NULL , Thread_TCP_Web_Recv , NULL);
    ; H( [2 R3 s# ?) }4 B3 g
  9.     pthread_create(&tid_tcp_web_send , NULL , Thread_TCP_Web_Send , NULL);. ?. d# Q6 ~; [( [2 ]
  10. ...
    3 d. L$ I5 C& x: q0 O
  11.     while(1)& o9 t  o$ ]; f% W8 m6 e* r
  12.     {0 u8 w: S; q% f0 P: E* T0 l
  13.         V4l2_Grab_Mjpeg(false , MJPEG_FILE_NAME);
    3 l. h5 T6 H* u# V( U; ?
  14. ...; X1 n) k" h: F, k
  15.     }/ s+ W+ V7 o' p- U" m
  16. ...
    8 U" c; \# v, K# b% O, A9 i
  17. }
复制代码

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

  1. <p style="box-sizing: border-box; border: 0px; vertical-align: baseline; line-height: 26px;"><span style="text-indent: 32px;">
    + ]% m2 h5 k$ ?+ H2 [; C( @
  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" \
    $ L6 t# a3 z# H
  2.     "Server: MJPG-Streamer/0.2\r\n" \0 p, T# M1 |1 ]; X  X5 I* O* ]
  3.     "Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n" \
      ?/ W+ l8 V, u8 Z: x/ K8 E, Q
  4.     "Pragma: no-cache\r\n" \! S9 D6 B0 }- U3 ~, j; S5 k
  5.     "Expires: Mon, 3 Jan 2000 12:34:56 GMT\r\n"
    $ [5 r# F2 E6 D+ |& i4 m
  6. #define BOUNDARY "boundarydonotcross"
    ! y+ h) U4 d) A) v4 B
  7.     printf("preparing header\n");
    " ^4 n1 r) i( s7 f  Q7 A
  8.     sprintf(buffer, "HTTP/1.0 200 OK\r\n" \
    5 U; O6 V  o5 S$ L, u
  9.             "Access-Control-Allow-Origin: *\r\n" \* i9 P/ K+ T% Z: t, f$ R
  10.             STD_HEADER \9 H3 e1 D5 C7 |7 U' C1 {
  11.             "Content-Type: multipart/x-mixed-replace;boundary=" BOUNDARY "\r\n" \
    6 C- _- G, y0 j3 g0 s
  12.             "\r\n" \+ x/ i" O$ W, N( y& y, u$ d9 ^
  13.             "--" BOUNDARY "\r\n");
    ' X4 k3 n. }8 ~' A7 o
  14.     if(write(fd, buffer, strlen(buffer)) < 0)! t* e$ v3 J2 r: U; ]0 \( s
  15.     {
    $ [$ M$ M& G7 T, p6 L4 [. l
  16.         free(frame);" O. x- A4 o- z6 w1 x& _
  17.         return;
    ; }9 V4 ^7 r( K; A6 V  u
  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" \
    ' o( @0 C$ R* A6 R( v; F6 d. E
  2.                 "Content-Length: %d\r\n" \7 \. s, I( m5 K  C  T. e0 E, l
  3.                 "X-Timestamp: %d.%06d\r\n" \
    7 v- n& [% i6 }7 G
  4.                 "\r\n", frame_size, (int)timestamp.tv_sec, (int)timestamp.tv_usec);- L. [- b$ G5 E5 B* X
  5.         printf("sending intemdiate header\n");+ R; S# Y  j! b/ J
  6.         if(write(fd, buffer, strlen(buffer)) < 0)
    . Z+ C7 Z6 Z+ h: l  O8 y$ y) _4 A
  7.             break;
    $ x) p; l. v+ |8 J9 z' \
  8.         printf("sending frame\n");
    5 v6 Y: L: ]/ ]/ b% j* o5 i
  9.         if(write(fd, frame, frame_size) < 0)& ^) p+ u1 J: ?6 D  f7 J
  10.             break;% X$ }* l) `) ^
  11.         printf("sending boundary\n");5 g, G/ N  ^% K& O: D- I( g
  12.         sprintf(buffer, "\r\n--" BOUNDARY "\r\n");  M) I) O0 r" d7 P
  13.         if(write(fd, buffer, strlen(buffer)) < 0)4 t8 _: ?# a) h/ Q1 s' l. g$ x/ a% D
  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指令,从客户端使用者角度来看的效果就是网页一直在等待。

( q' n& h" q+ R/ K! ~6 \& L+ D

2 Z2 c4 w3 t) D/ V% X( q& H
二、UDP上位机UDP发送操作,同样需要先建立UDP Socket:
% S. U- O: @6 a. n+ c9 }
  1. int UDP_Send_Found(socklen_t* socket_found , struct sockaddr_in *addr , char* ip , int port)
    + M" `; Z! g. `
  2. {
      Z: w- _! B4 n4 L# @% Z
  3.     *socket_found = socket(AF_INET, SOCK_DGRAM, 0);8 r! N+ C: O, t& \; s
  4.     if(*socket_found == (~0))6 t- Y, F  v( e, Y! W6 L
  5.     {$ D1 U% m5 g$ q+ i
  6.         printf("Create udp send socket failed!\n");% Q" _4 }6 D5 h, h% O* Q
  7.         return -1;7 T) F7 n4 }: x
  8.     }6 j3 R% I. D, [: ^1 S/ X
  9.     addr->sin_family = AF_INET;
    % T' T2 \1 `* u9 l
  10.     addr->sin_addr.s_addr = inet_addr(ip);( b* C5 p! Z; Y7 Y9 Q
  11.     addr->sin_port = htons(port);* u; K9 w5 q: V) l- q% }
  12.     memset(addr->sin_zero, 0, 8);( p- j5 t( w) P. `
  13.     return 0;
    4 ?" @9 G0 c) {. A: s5 Q; P7 q
  14. }
复制代码

( M* g2 ^/ x# W7 K5 N  ^% ~& @7 s& |/ q0 `! Q. I
而UDP文件发送则要比HTTP发送简单得多,只需要将文件切片,每一片为固定长度的UDP帧长度,逐帧发送即可:
5 p2 z  _) `1 i; q9 [3 U
! g2 v# W; O- b5 V+ n! k" y) u. a
7 s  H4 M; k* e; B3 c9 t* `
  1. while(fend > 0)
    9 `% {; B1 q6 i& R$ \
  2. {: }3 b1 }/ _" y  r& D; u7 a+ c* `
  3. memset(picture.data , 0 , sizeof(picture.data));
      X, \: @% L) j! z9 |. N! p& N
  4. fread(picture.data , UDP_FRAME_LEN , 1, fp);
    5 }3 G* T- a, n4 U5 [
  5. if(fend >= UDP_FRAME_LEN)9 Z4 j1 @) s( ?. ^( I
  6. {
    + [$ K- a. D/ L! ]1 u8 s$ O$ c7 o
  7. picture.length = UDP_FRAME_LEN;
    6 H+ U0 `0 s  W+ @, I
  8. picture.fin = 0;
    0 z$ F0 n: y) u- W" ?2 Q0 g0 A  ^
  9. }
      n& f" s( S6 T) |
  10. else
    5 M+ f% T- ?- Q6 A& i
  11. {
    8 h: a6 i+ Q+ ^$ \2 y4 N
  12. picture.length = fend;
    - S0 F) F  _- U- q
  13. picture.fin = 1;& L1 y3 M( D6 d0 D
  14. }
    1 f% s7 {4 p, N7 g
  15. //printf("sendbytes = %d \n",sendbytes);6 A1 G: P, o$ W/ o. j. B
  16. sendbytes = sendto(socket_send, (char *)&picture, sizeof(struct Package), 0, (struct sockaddr*)&addr,addr_len);9 o, V8 e; O) D5 [, c, `
  17. if(sendbytes == -1)
    " {  U) T2 M/ n
  18. {
    0 p( r3 V9 c6 Z& s% t6 Z
  19. printf("Send Picture Failed!d\n");! U: s! P$ j* M$ U( ^" B4 X
  20. return -1;
    0 a+ G5 Q/ j$ h& {; S
  21. }
    $ `  Z, S+ X) t- n7 d* J* w: x) D
  22. else
    , h) ^% W- h& q5 T) ~
  23. {
    % I% m6 X- [* ^# R1 o
  24. fend -= UDP_FRAME_LEN;+ v" ~! T' K3 w1 G( O
  25. }0 _4 T  a5 S. N6 `( {' P
  26. }
复制代码

9 }# `% x) ~- u
4 x* s! r' Z8 L' B8 D3 S


' r+ d- @$ l& t5 _# P: _- c5 A& Q1 U
iMX8MPlus 核心板: https://www.forlinx.com/product/136.html
回复

使用道具 举报

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

本版积分规则

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

GMT+8, 2025-12-22 14:15

Powered by Discuz! X3.4

© 2001-2013 Comsenz Inc.

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