关于shell:在Linux / bash中使用非阻塞FIFO流式传输视频

Streaming video using a non-blocking FIFO in linux/bash

我正在努力实现以下目标:

  • 将Raspberry Pi摄像机中的视频写入磁盘,而不受流的任何干扰
  • 通过网络流相同的视频,优化延迟

由于网络连接可能不稳定,例如WiFi路由器可能不在范围内,因此流媒体传输不会干扰视频写入磁盘,这一点很重要。

为此,我尝试的第一件事是:

1
2
3
#Receiver side
FPS="30"
netcat -l -p 5000 | mplayer -vf scale -zoom -xy 1280 -fps $FPS -cache-min 50 -cache 1024 - &

1
2
3
4
5
#RPi side
FPS="30"
mkfifo netcat_fifo
raspivid -t 0 -md 5 -fps $FPS -o - | tee --output-error=warn netcat_fifo > $video_out &
cat netcat_fifo | netcat -v 192.168.0.101 5000 &> $netcat_log &

流式传输效果很好。但是,当我关闭路由器,模拟网络问题时,我的$ video_out被切断了。我认为这是由于netcat_fifo造成的背压。

我在stackexchange上找到了一个关于非阻塞FIFO的解决方案,方法是用ftee替换tee:

Linux非阻塞FIFO(按需记录)

现在,它可以防止$ video_out受流影响,但是流本身非常不稳定。最好的结果是使用以下脚本:

1
2
3
4
5
6
#RPi side
FPS="30"
MULTIPIPE="ftee"
mkfifo netcat_fifo
raspivid -t 0 -md 5 -fps $FPS -o - | ./${MULTIPIPE} netcat_fifo > $video_out &
cat netcat_fifo | mbuffer --direct -t -s 2k 2> $mbuffer_log | netcat -v 192.168.0.101 5000 &> $netcat_log &

当我检查mbuffer日志时,我诊断出一个FIFO大部分时间都是空的,但峰值利用率为99-100%。在这些峰值期间,我的mplayer接收器端在解码视频时会遇到很多错误,大约需要5秒钟才能恢复。在此间隔之后,mbuffer日志再次显示一个空的FIFO。
empty-> full-> empty一直在继续。

我有两个问题:

  • 我是否使用正确的方法来解决我的问题?
  • 如果是这样,我如何在保持$ video_out文件完好无损的同时使流式传输更加健壮?

我对此做了一点尝试,并且在我的Raspberry Pi 3上似乎能很好地工作。它的注释很好,因此应该很容易理解,但是您可以随时询问是否有任何问题。

基本上有3个线程:

  • 主程序-它不断从raspivid读取其stdin并将其循环放入一堆缓冲区中

  • 磁盘写入器线程-它不断循环浏览缓冲区列表,等待下一个缓冲区变满。当缓冲区已满时,它将内容写入磁盘,将缓冲区标记为已写入,然后移至下一个

  • fifo编写器线程-它不断循环浏览缓冲区列表,等待下一个缓冲区变满。当缓冲区已满时,它将内容写入fifo,刷新fifo以减少延迟,并将缓冲区标记为已写入并移至下一个缓冲区。错误将被忽略。

所以,这是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
////////////////////////////////////////////////////////////////////////////////
// main.cpp
// Mark Setchell
//
// Read video stream from"raspivid" and write (independently) to both disk file
// and stdout - for onward netcatting to another host.
//
// Compiles with:
//    g++ main.cpp -o main -lpthread
//
// Run on Raspberry Pi with:
//    raspivid -t 0 -md 5 -fps 30 -o - | ./main video.h264 | netcat -v 192.168.0.8 5000
//
// Receive on other host with:
//    netcat -l -p 5000 | mplayer -vf scale -zoom -xy 1280 -fps 30 -cache-min 50 -cache 1024 -
////////////////////////////////////////////////////////////////////////////////
#include <iostream>
#include <chrono>
#include <thread>
#include <vector>
#include <unistd.h>
#include
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

#define BUFSZ    65536
#define NBUFS    64

class Buffer{
   public:
   int bytes=0;
   std::atomic<int> NeedsWriteToDisk{0};
   std::atomic<int> NeedsWriteToFifo{0};
   unsigned char data[BUFSZ];
};

std::vector<Buffer> buffers(NBUFS);

////////////////////////////////////////////////////////////////////////////////
// This is the DiskWriter thread.
// It loops through all the buffers waiting in turn for each one to become ready
// then writes it to disk and marks the buffer as written before moving to next
// buffer.
////////////////////////////////////////////////////////////////////////////////
void DiskWriter(char* filename){
   int bufIndex=0;

   // Open output file
   int fd=open(filename,O_CREAT|O_WRONLY|O_TRUNC,S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP);
   if(fd==-1)
   {
      std::cerr <<"ERROR: Unable to open output file" << std::endl;
      exit(EXIT_FAILURE);
   }

   bool Error=false;
   while(!Error){

      // Wait for buffer to be filled by main thread
      while(buffers[bufIndex].NeedsWriteToDisk!=1){
   //      std::this_thread::sleep_for(std::chrono::milliseconds(1));
      }

      // Write to disk
      int bytesToWrite=buffers[bufIndex].bytes;
      int bytesWritten=write(fd,reinterpret_cast<unsigned char*>(&buffers[bufIndex].data),bytesToWrite);
      if(bytesWritten!=bytesToWrite){
         std::cerr <<"ERROR: Unable to write to disk" << std::endl;
         exit(EXIT_FAILURE);
      }

      // Mark buffer as written
      buffers[bufIndex].NeedsWriteToDisk=0;

      // Move to next buffer
      bufIndex=(bufIndex+1)%NBUFS;
   }
}

////////////////////////////////////////////////////////////////////////////////
// This is the FifoWriter thread.
// It loops through all the buffers waiting in turn for each one to become ready
// then writes it to the Fifo, flushes it for reduced lag, and marks the buffer
// as written before moving to next one. Errors are ignored.
////////////////////////////////////////////////////////////////////////////////
void FifoWriter(){
   int bufIndex=0;

   bool Error=false;
   while(!Error){

      // Wait for buffer to be filled by main thread
      while(buffers[bufIndex].NeedsWriteToFifo!=1){
    //     std::this_thread::sleep_for(std::chrono::milliseconds(1));
      }

      // Write to fifo
      int bytesToWrite=buffers[bufIndex].bytes;
      int bytesWritten=write(STDOUT_FILENO,reinterpret_cast<unsigned char*>(&buffers[bufIndex].data),bytesToWrite);
      if(bytesWritten!=bytesToWrite){
         std::cerr <<"ERROR: Unable to write to fifo" << std::endl;
      }
      // Try to reduce lag
      fflush(stdout);

      // Mark buffer as written
      buffers[bufIndex].NeedsWriteToFifo=0;

      // Move to next buffer
      bufIndex=(bufIndex+1)%NBUFS;
   }
}

int main(int argc, char *argv[])
{  
   int bufIndex=0;

   if(argc!=2){
      std::cerr <<"ERROR: Usage" << argv[0] <<" filename" << std::endl;
      exit(EXIT_FAILURE);
   }
   char * filename = argv[1];

   // Start disk and fifo writing threads in parallel
   std::thread tDiskWriter(DiskWriter,filename);
   std::thread tFifoWriter(FifoWriter);

   bool Error=false;
   // Continuously fill buffers from"raspivid" on stdin. Mark as full and
   // needing output to disk and fifo before moving to next buffer.
   while(!Error)
   {
      // Check disk writer is not behind before re-using buffer
      if(buffers[bufIndex].NeedsWriteToDisk==1){
         std::cerr <<"ERROR: Disk writer is behind by" << NBUFS <<" buffers" << std::endl;
      }

      // Check fifo writer is not behind before re-using buffer
      if(buffers[bufIndex].NeedsWriteToFifo==1){
         std::cerr <<"ERROR: Fifo writer is behind by" << NBUFS <<" buffers" << std::endl;
      }

      // Read from STDIN till buffer is pretty full
      int bytes;
      int totalBytes=0;
      int bytesToRead=BUFSZ;
      unsigned char* ptr=reinterpret_cast<unsigned char*>(&buffers[bufIndex].data);
      while(totalBytes<(BUFSZ*.75)){
         bytes = read(STDIN_FILENO,ptr,bytesToRead);
         if(bytes<=0){
            Error=true;
            break;
         }
         ptr+=bytes;
         totalBytes+=bytes;
         bytesToRead-=bytes;
      }

      // Signal buffer ready for writing
      buffers[bufIndex].bytes=totalBytes;
      buffers[bufIndex].NeedsWriteToDisk=1;
      buffers[bufIndex].NeedsWriteToFifo=1;

      // Move to next buffer
      bufIndex=(bufIndex+1)%NBUFS;
   }
}