├── .gitignore ├── CMakeLists.txt ├── src ├── main.cpp ├── vwutils.h └── vwutils.cpp ├── lib └── FFmpeg │ └── CMakeLists.txt ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build/* 2 | python-example/* 3 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(vwriter C CXX) 3 | set(CMAKE_CXX_STANDARD 14) 4 | 5 | add_subdirectory(lib/FFmpeg) 6 | 7 | list(APPEND SOURCES 8 | src/main.cpp 9 | src/vwutils.h 10 | src/vwutils.cpp 11 | ) 12 | #MACOSX_BUNDLE WIN32 13 | 14 | add_library(video_writer SHARED src/vwutils.h src/vwutils.cpp) 15 | 16 | target_link_libraries(video_writer FFmpeg) 17 | 18 | add_executable(vwriter ${SOURCES}) 19 | 20 | target_link_libraries(vwriter FFmpeg) -------------------------------------------------------------------------------- /src/main.cpp: -------------------------------------------------------------------------------- 1 | #include "vwutils.h" 2 | 3 | void fillbgrframe(uint8_t* data) 4 | { 5 | for (int i=0;i<1920*1080*3;i++) 6 | { 7 | if (i<1920*1080) 8 | { 9 | *data=250; 10 | } 11 | else 12 | { 13 | *data=0; 14 | } 15 | data++; 16 | } 17 | } 18 | 19 | int main(void) 20 | { 21 | VideoWriter writer("vwtest.mp4", 29.97, 1920, 1080, true); 22 | uint8_t data[1920*1080*3]; 23 | fillbgrframe(&data[0]); 24 | //printf("%d %d %d %d\n", data[0], data[1], data[2], data[3]); 25 | for (int i=0; i<100; i++) 26 | writer.write(&data[0]); 27 | printf("Finished writing\n"); 28 | return 0; 29 | } -------------------------------------------------------------------------------- /lib/FFmpeg/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(FFmpeg) 3 | 4 | find_package(PkgConfig REQUIRED) 5 | pkg_check_modules(AVCODEC REQUIRED IMPORTED_TARGET libavcodec) 6 | pkg_check_modules(AVFORMAT REQUIRED IMPORTED_TARGET libavformat) 7 | pkg_check_modules(AVFILTER REQUIRED IMPORTED_TARGET libavfilter) 8 | pkg_check_modules(AVDEVICE REQUIRED IMPORTED_TARGET libavdevice) 9 | pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil) 10 | pkg_check_modules(SWRESAMPLE REQUIRED IMPORTED_TARGET libswresample) 11 | pkg_check_modules(SWSCALE REQUIRED IMPORTED_TARGET libswscale) 12 | 13 | add_library(FFmpeg INTERFACE IMPORTED GLOBAL) 14 | 15 | target_link_libraries(FFmpeg INTERFACE 16 | PkgConfig::AVCODEC 17 | PkgConfig::AVFORMAT 18 | PkgConfig::AVFILTER 19 | PkgConfig::AVDEVICE 20 | PkgConfig::AVUTIL 21 | PkgConfig::SWRESAMPLE 22 | PkgConfig::SWSCALE 23 | ) 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jaiyam Sharma 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/vwutils.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | extern "C" { 7 | #include 8 | #include 9 | #include 10 | #include 11 | } 12 | 13 | class VideoWriter 14 | { 15 | public: 16 | VideoWriter(char* filename, float fps, int fwidth, int fheight, bool iscolor=true); 17 | ~VideoWriter(); 18 | 19 | bool write(uint8_t* data); //assumed data is given in opencv bgr format 20 | //there is no need to release the object. It is done automatically 21 | 22 | private: 23 | char* filepath; 24 | float _fps; 25 | int frame_width; 26 | int frame_height; 27 | bool iscolor; 28 | 29 | void release(); 30 | void setup_frame(); 31 | void setup_encoder(); 32 | void add_video_stream(); 33 | void get_ready_to_write(); 34 | 35 | uint64_t pts=1; 36 | uint8_t* gray_rgb_data; 37 | AVIOContext* avioctx; 38 | AVCodecContext* avcctx; 39 | AVFormatContext* avfctx; 40 | AVStream* avs; 41 | AVPacket avpkt={0}; 42 | AVFrame* avframe; 43 | AVFrame* avbgrframe; 44 | AVCodec* encoder=NULL; 45 | struct SwsContext *converter; 46 | char ebuf[255]; 47 | 48 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # video-writer 2 | Super fast video writer for opencv using ffmpeg and libav 3 | (C++ and python) 4 | 5 | ## The Problem 6 | 7 | Opencv's `cv::VideoWriter` is a pain to use. 8 | - It hides the critical choice of encoders behind an obscure fourcc code and it isn't always transparent which codecs are available for use on the system. This is because opencv depends on a number of [backends](https://docs.opencv.org/3.4.15/d0/da7/videoio_overview.html) like ffmpeg and VFW. If you are someone who codes a lot on multiple systems (linux/mac/win/x86/arm) and embedded boards, this can quickly become frustrating. 9 | - Even setting aside the issue of codecs, `cv::VideoWriter` doesn't let you set important parameters like bitrate and pixel format. 10 | 11 | Wouldn't it be nice if there were a lightweight and transparent API for writing videos using hardware acceleration on python and C++? 12 | 13 | ## The easy solution 14 | 15 | Unsurprisingly, many other people have noticed the issue and the [most upvoted solution](https://stackoverflow.com/questions/38686359/opencv-videowriter-control-bitrate) on stackoverflow recommends (at least for python) opening a subprocess with ffmpeg and passing frames as JPEGs. Now there are better formats to pass frames into ffmpeg than JPEG and you would be better off really adapting this pipeline for your own usecase, but the bigger problem is that due to the limitations of python multiprocessing, the shells created this way are not freed as long as the parent python program is running (effin' GIL). This lead of all kinds of ugliness. For example, if you are making many videos from one python script, videos which have finished writing will not be viewable in vlc/ffplay/etc until all the videos have finished processing and the parent python script has exited. 16 | 17 | ## Why this project? 18 | In spite of its limitations, the easy solution works and 99% of users don't need anything else, but if video processing is an essential part of your workflow, you might find it worthwhile to invest in a more satisfying solution. This is where this project comes in. We use LibAV, the backend behind ffmpeg to build a simple video writer object which can be used in C++ an python. 19 | 20 | ## Status 21 | This is a **work in progress**. Please do not use it in anything critical and feel free to contribute by sending PRs. 22 | 23 | ## Build 24 | Currently verified on macOS. 25 | 26 | ```Shell 27 | #Install dependencies 28 | brew install ffmpeg pkg-config cmake 29 | 30 | #Clone repo 31 | git clone https://github.com/dataplayer12/video-writer.git 32 | cd video-writer 33 | mkdir build 34 | cd build 35 | 36 | #Configure and build project 37 | cmake ../ 38 | make 39 | ``` 40 | 41 | `make` will generate a minimum C++ sample and a shared library which can be used in a python script with ctypes module. 42 | 43 | ## How to use and To-Dos 44 | 45 | The goal is to create a `VideoWriter` class which can be instantiated like 46 | ```Cpp 47 | //C++ 48 | VideoWriter writer("filename.mp4", fps, width, height, encoder_name, bitrate); 49 | writer.write(cvFrame); //cvFrame is a cv::Mat object 50 | ``` 51 | 52 | ```Python 53 | #python 54 | import video_writer as vw 55 | import numpy as np 56 | 57 | writer= vw.VideoWriter(filename, fps, width, height, encoder_name, bitrate) 58 | x=np.ones((height, width, 3), dtype=np.uint8) 59 | writer.write(x) 60 | ``` 61 | Currently the `write` method accepts pointers and is not ready for use in python. 62 | 63 | - Make write method functional. 64 | - Add support for writing cvMat as frame. 65 | - Add support for python. 66 | - Make C++ and python examples 67 | 68 | ## Credits 69 | In making this project, I have learnt a lot from the excellent work of [Bartholomew Joyce](https://github.com/bartjoyce), his [videos](https://www.youtube.com/watch?v=MEMzo59CPr8) and [git repo](https://github.com/bartjoyce/video-app). It helped me set up the environment and get a feel for the functions of libav. The difference between his repo and this is that I want to encode rather than decode. 70 | -------------------------------------------------------------------------------- /src/vwutils.cpp: -------------------------------------------------------------------------------- 1 | #include "vwutils.h" 2 | 3 | 4 | VideoWriter::VideoWriter(char* filename, float fps, int fwidth, int fheight, bool iscolor) 5 | { 6 | 7 | int error=avio_open(&avioctx, filename, AVIO_FLAG_WRITE); 8 | 9 | if (error<0) 10 | { 11 | av_strerror(error, ebuf, sizeof(ebuf)); 12 | printf("Could not open output file: %s", ebuf); 13 | } 14 | filepath=filename; 15 | frame_width=fwidth; 16 | frame_height=fheight; 17 | iscolor=iscolor; 18 | _fps=fps; 19 | setup_frame(); 20 | setup_encoder(); 21 | add_video_stream(); 22 | get_ready_to_write(); 23 | } 24 | 25 | void VideoWriter::get_ready_to_write(void) 26 | { 27 | 28 | if (avcodec_open2(avcctx, NULL, NULL)<0) 29 | { 30 | std::cerr << "Could not open codec context\n"; 31 | } 32 | 33 | if (avcodec_parameters_from_context(avs->codecpar, avcctx)<0) 34 | { 35 | std::cerr << "Could not copy params\n"; 36 | } 37 | 38 | av_init_packet(&avpkt); 39 | avformat_write_header(avfctx, NULL); 40 | } 41 | 42 | void VideoWriter::setup_frame() 43 | { 44 | avframe=av_frame_alloc(); 45 | if (!avframe) 46 | { 47 | std::cerr << "Could not allocate frame. What is this even? smh\n"; 48 | } 49 | avframe->format=AV_PIX_FMT_YUV420P; 50 | avframe->width=frame_width; 51 | avframe->height=frame_height; 52 | if (av_frame_get_buffer(avframe, 32)<0) 53 | { 54 | std::cerr << "Yo frame so fat, the system ran out of memory!\n"; 55 | } 56 | 57 | avbgrframe=av_frame_alloc(); 58 | 59 | if(!avbgrframe) 60 | { 61 | std::cerr << "Could not allocate bgr\n"; 62 | } 63 | 64 | avbgrframe->format=AV_PIX_FMT_BGR24; 65 | avbgrframe->width=frame_width; 66 | avbgrframe->height=frame_height; 67 | 68 | if(av_frame_get_buffer(avbgrframe, 32)<0) 69 | { 70 | std::cerr << "Could not allocate frame buffer for bgr\n"; 71 | } 72 | 73 | converter=sws_getContext( 74 | frame_width, 75 | frame_height, 76 | AV_PIX_FMT_BGR24, 77 | frame_width, 78 | frame_height, 79 | AV_PIX_FMT_YUV420P, 80 | SWS_BILINEAR, 81 | NULL, NULL, NULL); 82 | 83 | if (!converter) 84 | { 85 | std::cerr << "Cannot initialize converter\n"; 86 | } 87 | } 88 | 89 | void VideoWriter::setup_encoder(void) 90 | { 91 | //setup encoder and its context and properties 92 | 93 | encoder=avcodec_find_encoder_by_name("h264_videotoolbox"); 94 | 95 | if (encoder==NULL) 96 | { 97 | printf("Could not load qsv encoder\n"); 98 | std::cerr << "oops" << std::endl; 99 | } 100 | else 101 | { 102 | printf("Successfully found qsv encoder\n"); 103 | /* H264 qsv supports: 0, 23, 160 ==> YUV420P, NV12, YUV440P12BE 104 | H265 qsv supports: 0, 23, 160, 28, 161 ==> all above and BGRA, AYUV64LE */ 105 | } 106 | 107 | avcctx=avcodec_alloc_context3(encoder); 108 | 109 | if (!avcctx) 110 | { 111 | std::cerr << "Could not allocate context for encoder\n"; 112 | } 113 | 114 | //allocate context for this encoder and set its properties 115 | avcctx->bit_rate=5000000; 116 | avcctx->width = frame_width; 117 | avcctx->height= frame_height; 118 | 119 | } 120 | 121 | void VideoWriter::add_video_stream(void) 122 | { 123 | avfctx = avformat_alloc_context(); 124 | 125 | if (!avfctx) 126 | { 127 | std::cerr <<"Could not create output format context\n"; 128 | } 129 | 130 | avs = avformat_new_stream(avfctx, NULL);//allocate new stream 131 | avfctx->pb = avioctx; 132 | avfctx->oformat = av_guess_format(NULL, filepath, NULL);//new AVOutputFormat; 133 | printf("oformat name: %s,\n full: %s \n, codecid: %d\n", 134 | avfctx->oformat->name, avfctx->oformat->long_name, avfctx->oformat->video_codec); 135 | avs->time_base=(AVRational){100, (int)(100*_fps)}; 136 | 137 | avcctx->time_base=avs->time_base; //yay 138 | avcctx->gop_size=12; //is this a typical value? 139 | avcctx->pix_fmt=AV_PIX_FMT_YUV420P; 140 | 141 | avfctx->oformat->video_codec=encoder->id; 142 | } 143 | 144 | bool VideoWriter::write(uint8_t *data) 145 | { 146 | av_frame_make_writable(avbgrframe); 147 | av_frame_make_writable(avframe); 148 | 149 | for (int j=0; jdata[0][j] = *data; 152 | data++; 153 | } 154 | 155 | if(sws_scale(converter, (const uint8_t * const *) avbgrframe->data, 156 | avbgrframe->linesize, 0, frame_height, avframe->data, 157 | avframe->linesize)!=frame_height) 158 | { 159 | std::cerr << "Error in scaling\n"; 160 | } 161 | 162 | avframe->pts=pts; 163 | 164 | int ret=avcodec_send_frame(avcctx, avframe); 165 | 166 | if (ret<0) 167 | { 168 | std::cerr << "Error submitting frame for encoding\n"; 169 | } 170 | printf("Frame number %d\n", avcctx->frame_number); 171 | while (ret>=0) 172 | { 173 | ret=avcodec_receive_packet(avcctx, &avpkt); 174 | 175 | if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) 176 | { 177 | //std::cerr<< "Unknown error in encoding video frame\n"; 178 | break; 179 | } 180 | else if (ret<0) 181 | { 182 | std::cerr << "Problem\n"; 183 | } 184 | av_packet_rescale_ts(&avpkt, avcctx->time_base, avs->time_base); 185 | avpkt.stream_index= avs->index; 186 | avpkt.pts=pts++; 187 | //pts++; 188 | ret=av_interleaved_write_frame(avfctx, &avpkt); 189 | if (ret<0) 190 | { 191 | std::cerr << "Error writing video frame\n"; 192 | } 193 | } 194 | 195 | return true; 196 | } 197 | 198 | void VideoWriter::release() 199 | { 200 | avcodec_free_context(&avcctx); 201 | av_frame_free(&avframe); 202 | av_frame_free(&avbgrframe); 203 | sws_freeContext(converter); 204 | avformat_free_context(avfctx); 205 | //avresample_free(&ost->avr); 206 | } 207 | 208 | VideoWriter::~VideoWriter() 209 | { 210 | release(); 211 | } 212 | --------------------------------------------------------------------------------