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; } } |