├── .gitignore ├── LICENSE ├── README.mdown ├── Rakefile ├── animated_gif_example.rb ├── ext ├── extconf.rb ├── ffmpeg.c ├── ffmpeg.h ├── ffmpeg.rb ├── ffmpeg_codec.c ├── ffmpeg_format.c ├── ffmpeg_frame.c ├── ffmpeg_input_format.c ├── ffmpeg_output_format.c ├── ffmpeg_stream.c ├── ffmpeg_utils.c └── ffmpeg_utils.h ├── ffmpeg-ruby.gemspec └── spec ├── spec_helper.rb └── units ├── codec_spec.rb ├── frame_spec.rb ├── input_format_spec.rb └── stream_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | spec/data 2 | *.gem 3 | *.o 4 | *.bundle 5 | ext/Makefile 6 | Makefile 7 | ext/mkmf.log 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2008 Antonin Amand 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.mdown: -------------------------------------------------------------------------------- 1 | # ffmpeg-ruby 2 | 3 | [project home on github](http://github.com/gwik/ffmpeg-ruby) 4 | 5 | ## Summary 6 | 7 | ffmpeg-ruby is a ruby C extension binding to ffmpeg/libav* library. 8 | 9 | It's main purpose is to extract frame in order to make video thumbnails. 10 | It also give access to main video attributes (frame rate, bit rate, duration, codecs, ...) 11 | 12 | So, it doesn't support encoding (at least for now). 13 | 14 | ## Installation 15 | 16 | Download latest sources of ffmpeg : 17 | 18 | wget http://ffmpeg.mplayerhq.hu/ffmpeg-export-snapshot.tar.bz2 19 | tar xjvf ffmpeg-export-snapshot.tar.bz2 20 | cd ffmpeg-export-20* 21 | 22 | Configure, optionally with some prefix : 23 | 24 | ./configure --prefix=/opt/ffmpeg --enable-pthreads --enable-shared --enable-gpl 25 | make 26 | sudo make install 27 | 28 | You can now continue with ffmpeg-ruby. 29 | 30 | git clone git://github.com/gwik/ffmpeg-ruby.git 31 | 32 | cd ffmpeg-ruby 33 | gem build ./ffmpeg-ruby.gemspec 34 | sudo gem install ./ffmpeg-ruby-0.1.0.gem -- --with-ffmpeg-dir=/opt/ffmpeg 35 | 36 | You can now test it : 37 | 38 | irb 39 | >> require 'rubygems' 40 | => false 41 | >> require 'ffmpeg' 42 | => true 43 | >> FFMPEG 44 | => FFMPEG 45 | 46 | ## Tutorial 47 | 48 | ffmpeg-ruby does not have real document YET (I promise it will change soom). 49 | You can take a look a specs for in depth usage. 50 | 51 | Here is basic usage. 52 | 53 | ## Video attributes 54 | 55 | ```ruby 56 | require 'rubygems' 57 | require 'ffmpeg' 58 | 59 | video = FFMPEG::InputFormat.new('alligator.mp4') 60 | # => # 61 | 62 | video.public_methods - Object.public_instance_methods 63 | # => ["bit_rate", "filename", "duration", "has_stream_with_codec_type?", "first_video_stream", "human_duration", "has_video?", "video_stream_count", "streams", "has_audio?", "first_audio_stream", "audio_stream_count"] 64 | ``` 65 | 66 | ## Streams 67 | 68 | ```ruby 69 | video.first_video_stream.public_methods - Object.public_instance_methods 70 | # => ["position", "duration", "index", "codec", "decode_frame", "seek"] 71 | ``` 72 | 73 | ### Seeking in stream 74 | 75 | ```ruby 76 | video.first_video_stream.seek(10) 77 | video.first_video_stream.position 78 | # => 10.2333333333333 79 | ``` 80 | 81 | As you can see, seeking is not very precise. 82 | 83 | ### Extracting frame 84 | 85 | ```ruby 86 | frame = video.first_video_stream.decode_frame 87 | # => # 88 | 89 | "frame size #{frame.width}x#{frame.height}" 90 | # => "frame size 240x176" 91 | ``` 92 | 93 | ### Animated GIF example with RMagick 94 | 95 | See animated_gif_example.rb 96 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'spec' 3 | require 'spec/rake/spectask' 4 | 5 | # Make tasks (originally from curb) ----------------------------------------------------- 6 | MAKECMD = ENV['MAKE_CMD'] || 'make' 7 | MAKEOPTS = ENV['MAKE_OPTS'] || '' 8 | 9 | FFMPEG_SO = "ext/FFMPEG_core.#{Config::MAKEFILE_CONFIG['DLEXT']}" 10 | 11 | file 'ext/Makefile' => 'ext/extconf.rb' do 12 | Dir.chdir('ext') do 13 | ruby "extconf.rb #{ENV['EXTCONF_OPTS']}" 14 | end 15 | end 16 | 17 | def make(target = '') 18 | Dir.chdir('ext') do 19 | pid = system("#{MAKECMD} #{MAKEOPTS} #{target}") 20 | $?.exitstatus 21 | end 22 | end 23 | # Let make handle dependencies between c/o/so - we'll just run it. 24 | file FFMPEG_SO => (['ext/Makefile'] + Dir['ext/*.c'] + Dir['ext/*.h']) do 25 | m = make 26 | fail "Make failed (status #{m})" unless m == 0 27 | end 28 | 29 | desc "Compile the shared object" 30 | task :compile => [FFMPEG_SO] 31 | 32 | task :build => :compile do 33 | %x{cd 'ext' && make clean all && cd '..'} 34 | $?.success? 35 | end 36 | 37 | task :default => :build 38 | 39 | desc "see spec:spec" 40 | task :spec => 'spec:spec' 41 | 42 | namespace :spec do 43 | desc "Print Specdoc for all specs" 44 | Spec::Rake::SpecTask.new(:doc) do |t| 45 | t.spec_opts = ["--format", "specdoc", "--dry-run"] 46 | t.spec_files = FileList['spec/**/*_spec.rb'] 47 | end 48 | 49 | desc "run specs" 50 | Spec::Rake::SpecTask.new(:spec) do |t| 51 | t.spec_files = FileList["spec/**/*_spec.rb"] 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /animated_gif_example.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'ffmpeg' 3 | require 'RMagick' 4 | 5 | video = FFMPEG::InputFormat.new('spec/data/alligator.mp4') 6 | stream = video.first_video_stream 7 | stream.seek 12 8 | 9 | i = 0 10 | image_list = Magick::ImageList.new 11 | 12 | # pts is presentation timestamp 13 | # dts is decoding timestamp 14 | stream.decode_frame do |frame, pts, dts| 15 | i += 1 16 | # stop when decoding timestamp (~position) reach 18 17 | break if dts > 18 18 | # decode 1 frame for 5 19 | next unless i % 5 == 0 20 | image_list.from_blob(frame.to_ppm) 21 | end 22 | 23 | puts i 24 | image_list.delay = 20 25 | image_list.write('animated.gif') 26 | -------------------------------------------------------------------------------- /ext/extconf.rb: -------------------------------------------------------------------------------- 1 | # --with-ffmpeg-dir=/opt/ffmpeg 2 | 3 | require 'mkmf' 4 | 5 | if find_executable('pkg-config') 6 | $CFLAGS << ' ' + `pkg-config libavfilter --cflags`.strip 7 | $CFLAGS << ' ' + `pkg-config libavcodec --cflags`.strip 8 | $CFLAGS << ' ' + `pkg-config libavutil --cflags`.strip 9 | $CFLAGS << ' ' + `pkg-config libswscale --cflags`.strip 10 | $LDFLAGS << ' ' + `pkg-config libavfilter --libs`.strip 11 | $LDFLAGS << ' ' + `pkg-config libavcodec --libs`.strip 12 | $LDFLAGS << ' ' + `pkg-config libavutil --libs`.strip 13 | $LDFLAGS << ' ' + `pkg-config libswscale --libs`.strip 14 | end 15 | 16 | ffmpeg_include, ffmpeg_lib = dir_config("ffmpeg") 17 | dir_config("libswscale") 18 | 19 | $CFLAGS << " -W -Wall" 20 | #$LDFLAGS << " -rpath #{ffmpeg_lib}" 21 | 22 | if have_library("avformat") and find_header('libavformat/avformat.h') and 23 | have_library("avcodec") and find_header('libavutil/avutil.h') and 24 | have_library("avutil") and find_header('libavcodec/avcodec.h') and 25 | have_library("swscale") and find_header('libswscale/swscale.h') then 26 | 27 | $objs = %w(ffmpeg.o ffmpeg_format.o ffmpeg_input_format.o ffmpeg_stream.o ffmpeg_utils.o ffmpeg_frame.o ffmpeg_codec.o) 28 | 29 | create_makefile("FFMPEG_core") 30 | 31 | else 32 | STDERR.puts "missing library" 33 | exit 1 34 | end 35 | -------------------------------------------------------------------------------- /ext/ffmpeg.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c)2008 Antonin Amand. 2 | * Licensed under the Ruby License. See LICENSE for details. 3 | * 4 | */ 5 | #include "ffmpeg.h" 6 | 7 | VALUE rb_mFFMPEG; 8 | 9 | void 10 | Init_FFMPEG_core() 11 | { 12 | //fprintf(stderr, "----------------- init FFMPEG ------------------\n"); 13 | // initialize ffmpeg 14 | 15 | av_register_all(); 16 | avcodec_register_all(); 17 | 18 | // initialize Module and Class 19 | rb_mFFMPEG = rb_define_module("FFMPEG"); 20 | rb_define_const(rb_mFFMPEG, "LIBAVCODEC_VERSION", LONG2NUM((long)avcodec_version())); 21 | 22 | Init_FFMPEGFormat(); 23 | Init_FFMPEGInputFormat(); 24 | //Init_FFMPEGOutputFormat(); 25 | Init_FFMPEGStream(); 26 | Init_FFMPEGCodec(); 27 | Init_FFMPEGFrame(); 28 | } 29 | -------------------------------------------------------------------------------- /ext/ffmpeg.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c)2008 Antonin Amand. 2 | * Licensed under the Ruby License. See LICENSE for details. 3 | * 4 | */ 5 | #ifndef RUBY_FFMPEG_H__ 6 | #define RUBY_FFMPEG_H__ 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | #ifdef RSHIFT 14 | #undef RSHIFT 15 | #endif 16 | 17 | #include "ruby.h" 18 | 19 | 20 | /* Ruby classes and modules */ 21 | RUBY_EXTERN VALUE rb_mFFMPEG; 22 | RUBY_EXTERN VALUE rb_cFFMPEGFormat; 23 | RUBY_EXTERN VALUE rb_cFFMPEGInputFormat; 24 | RUBY_EXTERN VALUE rb_cFFMPEGStream; 25 | RUBY_EXTERN VALUE rb_cFFMPEGCodec; 26 | RUBY_EXTERN VALUE rb_cFFMPEGFrame; 27 | 28 | /* ERRORS */ 29 | RUBY_EXTERN VALUE rb_eUnsupportedFormat; 30 | 31 | RUBY_EXTERN void Init_FFMPEG_core(); 32 | RUBY_EXTERN void Init_FFMPEGFormat(); 33 | RUBY_EXTERN void Init_FFMPEGInputFormat(); 34 | RUBY_EXTERN void Init_FFMPEGOutputFormat(); 35 | RUBY_EXTERN void Init_FFMPEGStream(); 36 | RUBY_EXTERN void Init_FFMPEGCodec(); 37 | RUBY_EXTERN void Init_FFMPEGFrame(); 38 | 39 | VALUE build_stream_object(AVStream * stream, VALUE format); 40 | VALUE build_codec_object(AVCodecContext * codec); 41 | VALUE build_frame_object(AVFrame * frame, int width, int height, int pixel_format); 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /ext/ffmpeg.rb: -------------------------------------------------------------------------------- 1 | # Copyright (c)2008 Antonin Amand. 2 | # Licensed under the Ruby License. See LICENSE for details. 3 | # 4 | begin 5 | require 'HFL_core' 6 | rescue LoadError 7 | require File.dirname(__FILE__) + '/FFMPEG_core' 8 | end 9 | 10 | module FFMPEG 11 | 12 | class InputFormat 13 | 14 | def first_video_stream 15 | streams.find { |st| st.type == :video } 16 | end 17 | 18 | def first_audio_stream 19 | streams.find { |st| st.type == :audio } 20 | end 21 | 22 | end 23 | 24 | class Stream 25 | 26 | def type 27 | codec.type 28 | end 29 | 30 | end 31 | 32 | end -------------------------------------------------------------------------------- /ext/ffmpeg_codec.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c)2008 Antonin Amand. 2 | * Licensed under the Ruby License. See LICENSE for details. 3 | * 4 | */ 5 | #include "ffmpeg.h" 6 | #include "ffmpeg_utils.h" 7 | 8 | VALUE rb_cFFMPEGCodec; 9 | 10 | static VALUE codec_type(VALUE self) { 11 | AVCodecContext * codec_context = get_codec_context(self); 12 | return codec_type_id_to_sym(codec_context->codec_type); 13 | } 14 | 15 | static VALUE 16 | codec_open_decoder(VALUE self) 17 | { 18 | int err = 0; 19 | AVCodecContext * codec_context = get_codec_context(self); 20 | AVCodec * codec = avcodec_find_decoder(codec_context->codec_id); 21 | 22 | if (NULL == codec) 23 | rb_raise(rb_eRuntimeError, "not enough memory to open codec"); 24 | 25 | //fprintf(stderr, "opening codec\n"); 26 | err = avcodec_open(codec_context, codec); 27 | 28 | if (err < 0) 29 | rb_raise(rb_eRuntimeError, "unable to open codec"); 30 | 31 | return self; 32 | } 33 | 34 | static VALUE 35 | codec_open_encoder(VALUE self) 36 | { 37 | int err = 0; 38 | AVCodecContext * codec_context = get_codec_context(self); 39 | AVCodec * codec = avcodec_find_encoder(codec_context->codec_id); 40 | 41 | if (NULL == codec) 42 | rb_raise(rb_eRuntimeError, "not enough memory to open codec"); 43 | 44 | //fprintf(stderr, "opening codec\n"); 45 | err = avcodec_open(codec_context, codec); 46 | 47 | if (err < 0) 48 | rb_raise(rb_eRuntimeError, "unable to open codec"); 49 | 50 | return self; 51 | } 52 | 53 | static VALUE 54 | codec_name(VALUE self) 55 | { 56 | AVCodecContext * codec_context = get_codec_context(self); 57 | 58 | // open codec if needed 59 | if (NULL == codec_context->codec) 60 | codec_open_decoder(self); 61 | 62 | return rb_str_new2(codec_context->codec_name); 63 | } 64 | 65 | static VALUE 66 | codec_id(VALUE self) 67 | { 68 | AVCodecContext * codec_context = get_codec_context(self); 69 | return INT2FIX(codec_context->codec_id); 70 | } 71 | 72 | // build a Stream Object from an existing AVStream structure 73 | VALUE build_codec_object(AVCodecContext * codec) 74 | { 75 | //fprintf(stderr, "build stream\n"); 76 | // will be freed by owner 77 | VALUE obj = Data_Wrap_Struct(rb_cFFMPEGCodec, 0, 0, codec); 78 | return obj; 79 | } 80 | 81 | void 82 | Init_FFMPEGCodec() 83 | { 84 | rb_cFFMPEGCodec = rb_define_class_under(rb_mFFMPEG, "Codec", rb_cObject); 85 | //rb_define_alloc_func(rb_cFFMPEGCodec, alloc_codec); 86 | rb_define_method(rb_cFFMPEGCodec, "name", codec_name, 0); 87 | rb_define_method(rb_cFFMPEGCodec, "id", codec_id, 0); 88 | rb_define_method(rb_cFFMPEGCodec, "type", codec_type, 0); 89 | } 90 | -------------------------------------------------------------------------------- /ext/ffmpeg_format.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c)2008 Antonin Amand. 2 | * Licensed under the Ruby License. See LICENSE for details. 3 | * 4 | */ 5 | #include "ffmpeg.h" 6 | #include "ffmpeg_utils.h" 7 | 8 | VALUE rb_cFFMPEGFormat; 9 | 10 | // ############## ATTRIBUTE METHODS ############################ 11 | 12 | static VALUE 13 | format_filename(VALUE self) 14 | { 15 | AVFormatContext * format_context = get_format_context(self); 16 | if (NULL == format_context->filename) { 17 | return Qnil; 18 | } 19 | return rb_str_new2(format_context->filename); 20 | } 21 | 22 | static VALUE 23 | format_bit_rate(VALUE self) 24 | { 25 | AVFormatContext * format_context = get_format_context(self); 26 | return INT2NUM(format_context->bit_rate); 27 | } 28 | 29 | static VALUE 30 | format_duration(VALUE self) 31 | { 32 | AVFormatContext * format_context = get_format_context(self); 33 | 34 | if (format_context->duration == AV_NOPTS_VALUE) { 35 | return Qnil; 36 | } 37 | 38 | return rb_float_new(format_context->duration / (double)AV_TIME_BASE); 39 | } 40 | 41 | static VALUE 42 | format_streams(VALUE self) 43 | { 44 | int i; 45 | AVFormatContext * format_context = get_format_context(self); 46 | volatile VALUE streams = rb_ary_new2(format_context->nb_streams); 47 | 48 | for (i = 0; i < format_context->nb_streams; i++) { 49 | AVStream * format_context_entry = format_context->streams[i]; 50 | AVStream * streams_entry = NULL; 51 | VALUE entry = rb_ary_entry(streams, i); 52 | 53 | if (entry != Qnil) 54 | Data_Get_Struct(entry, AVStream, streams_entry); 55 | 56 | if (streams_entry != format_context_entry) 57 | rb_ary_store(streams, i, build_stream_object(format_context_entry, self)); 58 | } 59 | 60 | return streams; 61 | } 62 | 63 | static VALUE 64 | format_stream_count(VALUE self) 65 | { 66 | AVFormatContext * format_context = get_format_context(self); 67 | return INT2FIX(format_context->nb_streams); 68 | } 69 | 70 | //TODO : translate codec types to symbols 71 | static VALUE 72 | format_count_stream_with_codec_type(VALUE self, int codec_type) 73 | { 74 | AVFormatContext * format_context = get_format_context(self); 75 | int i, count = 0; 76 | 77 | for (i = 0; i < format_context->nb_streams; i++) { 78 | if (format_context->streams[i]->codec->codec_type == codec_type) 79 | ++count; 80 | } 81 | return INT2FIX(count); 82 | } 83 | 84 | static VALUE 85 | format_video_stream_count(VALUE self) 86 | { 87 | return format_count_stream_with_codec_type(self, CODEC_TYPE_VIDEO); 88 | } 89 | 90 | static VALUE 91 | format_audio_stream_count(VALUE self) 92 | { 93 | return format_count_stream_with_codec_type(self, CODEC_TYPE_AUDIO); 94 | } 95 | 96 | static VALUE 97 | format_has_stream_with_codec_type(VALUE self, int codec_type) 98 | { 99 | AVFormatContext * format_context = get_format_context(self); 100 | int i; 101 | 102 | for (i = 0; i < format_context->nb_streams; i++) { 103 | if (format_context->streams[i]->codec->codec_type == codec_type) 104 | return Qtrue; 105 | } 106 | return Qfalse; 107 | } 108 | 109 | static VALUE 110 | format_has_video(VALUE self) 111 | { 112 | return format_has_stream_with_codec_type(self, CODEC_TYPE_VIDEO); 113 | } 114 | 115 | static VALUE 116 | format_has_audio(VALUE self) 117 | { 118 | return format_has_stream_with_codec_type(self, CODEC_TYPE_AUDIO); 119 | } 120 | 121 | // copied from avformat util.c 122 | static VALUE 123 | format_duration_human(VALUE self) 124 | { 125 | AVFormatContext * format_context = get_format_context(self); 126 | 127 | if (format_context->duration == AV_NOPTS_VALUE) { 128 | return Qnil; 129 | } 130 | 131 | int hours, mins, secs, us; 132 | char cstr[64] = ""; 133 | 134 | secs = format_context->duration / AV_TIME_BASE; 135 | us = format_context->duration % AV_TIME_BASE; 136 | mins = secs / 60; 137 | secs %= 60; 138 | hours = mins / 60; 139 | mins %= 60; 140 | sprintf(cstr, "%02d:%02d:%02d.%01d", hours, mins, secs, 141 | (10 * us) / AV_TIME_BASE); 142 | 143 | return rb_str_new2(cstr); 144 | } 145 | 146 | // #################### CONSTRUCT AND DESTROY ################### 147 | 148 | static VALUE 149 | init_format(VALUE self) 150 | { 151 | return self; 152 | } 153 | 154 | static void 155 | free_format(AVFormatContext * format_context) 156 | { 157 | if (NULL == format_context) 158 | return; 159 | 160 | int i; 161 | for(i = 0; i < format_context->nb_streams; i++) { 162 | if (NULL != format_context->streams[i]->codec->codec) { 163 | avcodec_close(format_context->streams[i]->codec); 164 | } 165 | } 166 | 167 | if (format_context->iformat) { 168 | av_close_input_file(format_context); 169 | } else { 170 | av_free(format_context); 171 | } 172 | } 173 | 174 | static VALUE 175 | alloc_format(VALUE klass) 176 | { 177 | //fprintf(stderr, "allocating Format\n"); 178 | AVFormatContext * format_context = av_alloc_format_context(); 179 | VALUE obj; 180 | format_context->oformat = NULL; 181 | format_context->iformat = NULL; 182 | 183 | obj = Data_Wrap_Struct(klass, 0, free_format, format_context); 184 | return obj; 185 | } 186 | 187 | void 188 | Init_FFMPEGFormat() { 189 | //fprintf(stderr, "init FFMPEG Format\n"); 190 | rb_cFFMPEGFormat = rb_define_class_under(rb_mFFMPEG, "Format", rb_cObject); 191 | 192 | //fprintf(stderr, "init FFMPEG Format allocation\n"); 193 | rb_define_alloc_func(rb_cFFMPEGFormat, alloc_format); 194 | 195 | //fprintf(stderr, "init FFMPEG Format initialize\n"); 196 | rb_define_method(rb_cFFMPEGFormat, "initialize", init_format, 0); 197 | rb_define_method(rb_cFFMPEGFormat, "filename", format_filename, 0); 198 | rb_define_method(rb_cFFMPEGFormat, "bit_rate", format_bit_rate, 0); 199 | rb_define_method(rb_cFFMPEGFormat, "duration", format_duration, 0); 200 | rb_define_method(rb_cFFMPEGFormat, "human_duration", format_duration_human, 0); 201 | 202 | rb_define_method(rb_cFFMPEGFormat, "streams", format_streams, 0); 203 | rb_define_method(rb_cFFMPEGFormat, "video_stream_count", format_video_stream_count, 0); 204 | rb_define_method(rb_cFFMPEGFormat, "audio_stream_count", format_audio_stream_count, 0); 205 | rb_define_method(rb_cFFMPEGFormat, "has_stream_with_codec_type?", format_has_stream_with_codec_type, 1); 206 | rb_define_method(rb_cFFMPEGFormat, "has_video?", format_has_video, 0); 207 | rb_define_method(rb_cFFMPEGFormat, "has_audio?", format_has_audio, 0); 208 | } 209 | -------------------------------------------------------------------------------- /ext/ffmpeg_frame.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c)2008 Antonin Amand. 2 | * Licensed under the Ruby License. See LICENSE for details. 3 | * 4 | */ 5 | #include "ffmpeg.h" 6 | #include "ffmpeg_utils.h" 7 | 8 | VALUE rb_cFFMPEGFrame; 9 | 10 | static AVFrame * 11 | alloc_picture(int pix_fmt, int width, int height) 12 | { 13 | AVFrame *picture; 14 | uint8_t *picture_buf; 15 | int size; 16 | 17 | picture = avcodec_alloc_frame(); 18 | if (!picture) 19 | return NULL; 20 | size = avpicture_get_size(pix_fmt, width, height); 21 | picture_buf = av_malloc(size); 22 | if (!picture_buf) { 23 | av_free(picture); 24 | return NULL; 25 | } 26 | avpicture_fill((AVPicture *)picture, picture_buf, 27 | pix_fmt, width, height); 28 | return picture; 29 | } 30 | 31 | static VALUE 32 | frame_to_rgb24(VALUE self) 33 | { 34 | int width = NUM2INT(rb_iv_get(self, "@width")); 35 | int height = NUM2INT(rb_iv_get(self, "@height")); 36 | int pixel_format = NUM2INT(rb_iv_get(self, "@pixel_format")); 37 | 38 | struct SwsContext *img_convert_ctx = NULL; 39 | img_convert_ctx = sws_getContext(width, height, pixel_format, 40 | width, height, PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL); 41 | 42 | AVFrame * from = get_frame(self); 43 | AVFrame * to = alloc_picture(PIX_FMT_RGB24, width, height); 44 | 45 | sws_scale(img_convert_ctx, from->data, from->linesize, 46 | 0, height, to->data, to->linesize); 47 | 48 | av_free(img_convert_ctx); 49 | 50 | return build_frame_object(to, width, height, PIX_FMT_RGB24); 51 | } 52 | 53 | static VALUE 54 | frame_to_ppm(VALUE self) 55 | { 56 | VALUE rb_frame = frame_to_rgb24(self); 57 | AVFrame * frame = get_frame(rb_frame); 58 | 59 | int width = NUM2INT(rb_iv_get(self, "@width")); 60 | int height = NUM2INT(rb_iv_get(self, "@height")); 61 | 62 | char header[255]; 63 | sprintf(header, "P6\n%d %d\n255\n", width, height); 64 | 65 | int size = strlen(header) + frame->linesize[0] * height; 66 | char * data_string = malloc(size); 67 | strcpy(data_string, header); 68 | 69 | memcpy(data_string + strlen(header), frame->data[0], frame->linesize[0] * height); 70 | 71 | return rb_str_new(data_string, size); 72 | } 73 | 74 | static void 75 | free_frame(AVFrame * frame) 76 | { 77 | //fprintf(stderr, "will free frame\n"); 78 | av_free(frame); 79 | } 80 | 81 | static VALUE 82 | alloc_frame(VALUE klass) 83 | { 84 | AVFrame * frame = avcodec_alloc_frame(); 85 | VALUE obj; 86 | obj = Data_Wrap_Struct(klass, 0, free_frame, frame); 87 | return obj; 88 | } 89 | 90 | static VALUE 91 | frame_initialize(VALUE self, VALUE width, VALUE height, VALUE pixel_format) 92 | { 93 | //fprintf(stderr, "new frame : %dx%d, pix:%d\n", NUM2INT(width), NUM2INT(height), NUM2INT(pixel_format)); 94 | rb_iv_set(self, "@width", width); 95 | rb_iv_set(self, "@height", height); 96 | rb_iv_set(self, "@pixel_format", pixel_format); 97 | return self; 98 | } 99 | 100 | VALUE 101 | build_frame_object(AVFrame * frame, int width, int height, int pixel_format) 102 | { 103 | VALUE obj = Data_Wrap_Struct(rb_cFFMPEGFrame, 0, free_frame, frame); 104 | 105 | return frame_initialize(obj, 106 | INT2FIX(width), 107 | INT2FIX(height), 108 | INT2FIX(pixel_format)); 109 | } 110 | 111 | void 112 | Init_FFMPEGFrame() { 113 | rb_cFFMPEGFrame = rb_define_class_under(rb_mFFMPEG, "Frame", rb_cObject); 114 | 115 | rb_define_alloc_func(rb_cFFMPEGFrame, alloc_frame); 116 | rb_define_method(rb_cFFMPEGFrame, "initialize", frame_initialize, 3); 117 | 118 | rb_funcall(rb_cFFMPEGFrame, rb_intern("attr_reader"), 1, rb_sym("width")); 119 | rb_funcall(rb_cFFMPEGFrame, rb_intern("attr_reader"), 1, rb_sym("height")); 120 | rb_funcall(rb_cFFMPEGFrame, rb_intern("attr_reader"), 1, rb_sym("pixel_format")); 121 | 122 | rb_define_method(rb_cFFMPEGFrame, "to_rgb24", frame_to_rgb24, 0); 123 | rb_define_method(rb_cFFMPEGFrame, "to_ppm", frame_to_ppm, 0); 124 | } 125 | -------------------------------------------------------------------------------- /ext/ffmpeg_input_format.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c)2008 Antonin Amand. 2 | * Licensed under the Ruby License. See LICENSE for details. 3 | * 4 | */ 5 | #include "ffmpeg.h" 6 | #include "ffmpeg_utils.h" 7 | 8 | VALUE rb_cFFMPEGInputFormat; 9 | VALUE rb_eUnsupportedFormat; 10 | 11 | static VALUE 12 | input_format_initialize(VALUE self, VALUE filename) 13 | { 14 | AVFormatContext * format_context = NULL; 15 | 16 | Data_Get_Struct(self, AVFormatContext, format_context); 17 | //fprintf(stderr, "init format %p\n", format_context); 18 | AVFormatParameters fp, *ap = &fp; 19 | 20 | memset(ap, 0, sizeof(fp)); 21 | 22 | ap->prealloced_context = 1; 23 | ap->width = 0; 24 | ap->height = 0; 25 | ap->pix_fmt = PIX_FMT_NONE; 26 | 27 | if (Qfalse == rb_funcall(rb_cFile, rb_intern("file?"), 1, filename)) 28 | rb_raise(rb_eArgError, 29 | "ffmpeg failed to open input file %s", 30 | StringValuePtr(filename)); 31 | 32 | int error = av_open_input_file(&format_context, StringValuePtr(filename), 33 | NULL, FFM_PACKET_SIZE, ap); 34 | 35 | if (error < 0) { 36 | DATA_PTR(self) = format_context; 37 | rb_raise(rb_eUnsupportedFormat, 38 | "ffmpeg failed to open input file %s", 39 | StringValuePtr(filename)); 40 | } 41 | 42 | error = av_find_stream_info(format_context); 43 | 44 | if (error < 0) { 45 | DATA_PTR(self) = format_context; 46 | rb_raise(rb_eUnsupportedFormat, 47 | "ffmpeg internal error while looking for stream info on %s", 48 | StringValuePtr(filename)); 49 | } 50 | 51 | //fprintf(stderr, "InputFormat end initialize\n"); 52 | return self; 53 | } 54 | 55 | static VALUE 56 | input_format_available_input_formats(VALUE klass) 57 | { 58 | return rb_cv_get(klass, "@@available_input_formats"); 59 | } 60 | 61 | void Init_FFMPEGInputFormat() { 62 | rb_cFFMPEGFormat = rb_const_get(rb_mFFMPEG, rb_intern("Format")); 63 | rb_cFFMPEGInputFormat = rb_define_class_under(rb_mFFMPEG, "InputFormat", rb_cFFMPEGFormat); 64 | rb_define_method(rb_cFFMPEGInputFormat, "initialize", input_format_initialize, 1); 65 | // rb_define_method(rb_cFFMPEGInputFormat, "seek", input_format_seek, 1); 66 | // rb_define_method(rb_cFFMPEGInputFormat, "position", input_format_position, 0); 67 | // rb_define_method(rb_cFFMPEGInputFormat, "extract_next_frame", input_format_extract_next_frame, 1); 68 | 69 | rb_eUnsupportedFormat = rb_define_class_under(rb_mFFMPEG, "UnsupportedFormat", rb_eStandardError); 70 | } 71 | -------------------------------------------------------------------------------- /ext/ffmpeg_output_format.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c)2008 Antonin Amand. 2 | * Licensed under the Ruby License. See LICENSE for details. 3 | * 4 | */ 5 | #include "ffmpeg.h" 6 | #include "ffmpeg_utils.h" 7 | 8 | VALUE rb_cFFMPEGOutputFormat; 9 | 10 | static VALUE output_format_add_video_stream(VALUE self, VALUE codec_id_or_sym) 11 | { 12 | AVFormatContext * format_context = get_format_context(self); 13 | AVCodecContext * codec_context = NULL; 14 | AVStream * stream = NULL; 15 | AVCodec * codec = NULL; 16 | unsigned int codec_id = CODEC_ID_NONE; 17 | 18 | if (SYMBOL_P(codec_id_or_sym)) { 19 | fprintf(stderr, "get available codecs\n"); 20 | // VALUE rb_h_available_codecs = rb_funcall(rb_const_get(rb_mFFMPEG, rb_intern("Codec")), rb_intern("available_codecs"), 0); 21 | 22 | //VALUE rb_h_codec = rb_hash_aref(rb_h_available_codecs, codec_id_or_sym); 23 | 24 | if (Qnil == rb_h_codec) 25 | rb_raise(rb_eArgError, "invalid codec symbol / codec not found in FFMPEG::Codec.available_codecs"); 26 | 27 | //if (Qfalse == rb_hash_aref(rb_h_codec, rb_sym("encoder"))) 28 | // rb_raise(rb_eArgError, "codec is not available for encoding purpose"); 29 | fprintf(stderr, "codec %s : %s\n", STR2CSTR(rb_funcall(codec_id_or_sym, rb_intern("inspect"), 0)), 30 | STR2CSTR(rb_funcall(rb_h_codec, rb_intern("inspect"), 0))); 31 | 32 | 33 | codec_id = NUM2INT(rb_hash_aref(rb_h_codec, rb_sym("id"))); 34 | fprintf(stderr, "codec id : %d\n", codec_id); 35 | } 36 | 37 | if (FIXNUM_P(codec_id_or_sym)) { 38 | //fprintf(stderr, "fixnum mode\n"); 39 | codec_id = NUM2INT(codec_id_or_sym); 40 | } 41 | 42 | if (format_context->nb_streams + 1 > MAX_STREAMS) 43 | rb_raise(rb_eRuntimeError, "over max stream count"); 44 | 45 | stream = av_new_stream(format_context, format_context->nb_streams); 46 | 47 | if (!stream) { 48 | rb_raise(rb_eRuntimeError, "no memory"); 49 | } 50 | 51 | codec_context = stream->codec; 52 | codec_context->codec_id = codec_id; 53 | codec_context->codec_type = CODEC_TYPE_VIDEO; 54 | 55 | strcat(format_context->filename, "test"); 56 | 57 | // some formats want stream headers to be separate 58 | if(!strcmp(format_context->oformat->name, "mp4") || 59 | !strcmp(format_context->oformat->name, "mov") || 60 | !strcmp(format_context->oformat->name, "3gp")) 61 | format_context->flags |= CODEC_FLAG_GLOBAL_HEADER; 62 | 63 | codec = avcodec_find_encoder(codec_context->codec_id); 64 | 65 | if (!codec) { 66 | rb_raise(rb_eRuntimeError, "codec not found"); 67 | } 68 | 69 | avcodec_get_context_defaults2(codec_context, CODEC_TYPE_VIDEO); 70 | 71 | if (av_set_parameters(format_context, NULL) < 0) { 72 | // FIXME raise error 73 | } 74 | 75 | //codec_context->bit_rate = 400000; 76 | //fprintf(stderr, "bitrate : %d kb/s\n", codec_context->bit_rate / 1024); 77 | 78 | //codec_context->width = 320; 79 | //codec_context->height = 240; 80 | 81 | /* resolution must be a multiple of two */ 82 | // codec_context->width = 624; 83 | // codec_context->height = 352; 84 | 85 | codec_context->bit_rate_tolerance = 1000000000; 86 | //codec_context->gop_size = 12; /* emit one intra frame every twelve frames at most */ 87 | 88 | if(codec && codec->pix_fmts){ 89 | const enum PixelFormat *p= codec->pix_fmts; 90 | for(; *p!=-1; p++){ 91 | if(*p == codec_context->pix_fmt) 92 | break; 93 | } 94 | if(*p == -1) 95 | codec_context->pix_fmt = codec->pix_fmts[0]; 96 | } 97 | // if (codecodec_context->supported_framerates) 98 | // codec_context->time_base = codecodec_context->supported_framerates[0]; 99 | // else 100 | // codec_context->time_base = (AVRational){0,1}; 101 | /* set the output parameters (must be done even if no 102 | parameters). */ 103 | AVRational fps = (AVRational){25,1}; 104 | codec_context->time_base = fps; 105 | 106 | if(codec && codec->supported_framerates){ 107 | const AVRational *p = codec->supported_framerates; 108 | const AVRational *best = NULL; 109 | AVRational best_error= (AVRational){INT_MAX, 1}; 110 | for(; p->den!=0; p++) { 111 | AVRational error= av_sub_q(fps, *p); 112 | if(error.num < 0) error.num *= -1; 113 | if(av_cmp_q(error, best_error) < 0) { 114 | best_error= error; 115 | best= p; 116 | } 117 | } 118 | codec_context->time_base.den = best->den; 119 | codec_context->time_base.num = best->num; 120 | } 121 | 122 | fprintf(stderr, "\ncodec name : %s, codec id : %d\n", codec->name, codec->id); 123 | fprintf(stderr, "codec time base : %d/%d\n", (int)codec_context->time_base.num, (int)codec_context->time_base.den); 124 | // open the codec 125 | if (avcodec_open(codec_context, codec) < 0) { 126 | rb_raise(rb_eRuntimeError, "error while opening codec for encoding (%d)", 127 | codec_context->codec_id); 128 | } 129 | 130 | return self; 131 | } 132 | 133 | static VALUE 134 | output_format_init(VALUE self, VALUE format_string) 135 | { 136 | VALUE obj = rb_call_super(0, NULL); 137 | AVFormatContext * format_context = NULL; 138 | AVOutputFormat * ofmt = NULL; 139 | 140 | Data_Get_Struct(obj, AVFormatContext, format_context); 141 | 142 | ofmt = guess_format(StringValuePtr(format_string), NULL, NULL); 143 | 144 | if (!ofmt) { 145 | rb_raise(rb_eRuntimeError, "Format '%s' not found or not available", 146 | StringValuePtr(format_string)); 147 | } 148 | 149 | format_context->oformat = ofmt; 150 | 151 | return obj; 152 | } 153 | 154 | static void 155 | free_format(AVFormatContext * format_context) 156 | { 157 | // int i; 158 | // for (i = 0; i < format_context->nb_streams; i++) { 159 | // av_free(format_context->streams[i]->codec); 160 | // av_free(format_context->streams[i]); 161 | // } 162 | // av_free(format_context); 163 | } 164 | 165 | static VALUE 166 | alloc_format(VALUE klass) 167 | { 168 | //fprintf(stderr, "allocating Format\n"); 169 | AVFormatContext * format_context = av_alloc_format_context(); 170 | VALUE obj; 171 | format_context->oformat = NULL; 172 | format_context->iformat = NULL; 173 | 174 | obj = Data_Wrap_Struct(klass, 0, free_format, format_context); 175 | return obj; 176 | } 177 | 178 | static VALUE 179 | output_format_available_ouput_formats(VALUE klass) 180 | { 181 | return rb_cv_get(klass, "@@available_output_formats"); 182 | } 183 | 184 | void Init_FFMPEGOutputFormat() { 185 | //fprintf(stderr, "init FFMPEG Input Format\n"); 186 | // rb_mFFMPEG = rb_const_get(rb_cObject, rb_intern("FFMPEG")); 187 | // rb_cFFMPEGFormat = rb_const_get(rb_mFFMPEG, rb_intern("Format")); 188 | // rb_cFFMPEGOutputFormat = rb_define_class_under(rb_mFFMPEG, "OutputFormat", rb_cFFMPEGFormat); 189 | // 190 | // //instance methods 191 | // rb_define_method(rb_cFFMPEGOutputFormat, "initialize", output_format_init, 1); 192 | // rb_define_method(rb_cFFMPEGOutputFormat, "add_video_stream", 193 | // output_format_add_video_stream, 1); 194 | 195 | //AVOutputFormat * p = first_oformat; 196 | 197 | // rb_available_output_formats = rb_hash_new(); 198 | // 199 | // while(p) { 200 | // VALUE rb_h_format = rb_hash_new(); 201 | // rb_hash_aset(rb_h_format, rb_sym("name"), rb_str_new2(p->long_name)); 202 | // if (p->mime_type) 203 | // rb_hash_aset(rb_h_format, rb_sym("mime-type"), rb_str_new2(p->mime_type)); 204 | // if (p->extensions) 205 | // rb_hash_aset(rb_h_format, rb_sym("extensions"), 206 | // rb_funcall(rb_str_new2(p->extensions), rb_intern("split"), 1, rb_str_new2(","))); 207 | // rb_hash_aset(rb_available_output_formats, rb_sym(p->name), rb_h_format); 208 | // p = p->next; 209 | // } 210 | // 211 | // rb_cv_set(rb_cFFMPEGOutputFormat, "@@available_output_formats", rb_available_output_formats); 212 | // rb_define_singleton_method(rb_cFFMPEGOutputFormat, "available_output_formats", output_format_available_ouput_formats, 0); 213 | } 214 | -------------------------------------------------------------------------------- /ext/ffmpeg_stream.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c)2008 Antonin Amand. 2 | * Licensed under the Ruby License. See LICENSE for details. 3 | * 4 | */ 5 | #include "ffmpeg.h" 6 | #include "ffmpeg_utils.h" 7 | 8 | VALUE rb_cFFMPEGStream; 9 | 10 | static int 11 | next_packet(AVFormatContext * format_context, AVPacket * packet) 12 | { 13 | if(packet->data != NULL) 14 | av_free_packet(packet); 15 | 16 | if(av_read_frame(format_context, packet) < 0) { 17 | return -1; 18 | } 19 | 20 | return 0; 21 | } 22 | 23 | static int 24 | next_packet_for_stream(AVFormatContext * format_context, int stream_index, AVPacket * packet) 25 | { 26 | int ret = 0; 27 | do { 28 | ret = next_packet(format_context, packet); 29 | } while(packet->stream_index != stream_index && ret == 0); 30 | 31 | return ret; 32 | } 33 | 34 | static VALUE stream_codec(VALUE self) 35 | { 36 | AVStream * stream = get_stream(self); 37 | 38 | VALUE rb_codec = rb_iv_get(self, "@codec"); 39 | 40 | if (rb_codec == Qnil && NULL != stream->codec) 41 | rb_codec = rb_iv_set(self, "@codec", build_codec_object(stream->codec)); 42 | 43 | return rb_codec; 44 | } 45 | 46 | static VALUE stream_index(VALUE self) 47 | { 48 | AVStream * stream = get_stream(self); 49 | return INT2FIX(stream->index); 50 | } 51 | 52 | static VALUE 53 | stream_duration(VALUE self) 54 | { 55 | AVStream * stream = get_stream(self); 56 | if (stream->duration == AV_NOPTS_VALUE) { 57 | return Qnil; 58 | } 59 | return(rb_float_new(stream->duration * av_q2d(stream->time_base))); 60 | } 61 | 62 | static VALUE 63 | stream_frame_rate(VALUE self) 64 | { 65 | AVStream * stream = get_stream(self); 66 | return(rb_float_new(av_q2d(stream->r_frame_rate))); 67 | } 68 | 69 | static VALUE 70 | stream_seek(VALUE self, VALUE position) 71 | { 72 | AVFormatContext * format_context = get_format_context(rb_iv_get(self, "@format")); 73 | AVStream * stream = get_stream(self); 74 | 75 | int64_t timestamp = NUM2LONG(position) / av_q2d(stream->time_base); 76 | 77 | int ret; 78 | if (format_context->start_time != AV_NOPTS_VALUE) 79 | timestamp += format_context->start_time; 80 | 81 | //fprintf(stderr, "seeking to %d\n", NUM2INT(position)); 82 | ret = av_seek_frame(format_context, stream->index, timestamp, 0); 83 | if (ret < 0) { 84 | rb_raise(rb_eRangeError, "could not seek %s to pos %f", 85 | format_context->filename, timestamp * av_q2d(stream->time_base)); 86 | } 87 | 88 | //fprintf(stderr, "seeked.\n"); 89 | return self; 90 | } 91 | 92 | static VALUE 93 | stream_position(VALUE self) 94 | { 95 | AVFormatContext * format_context = get_format_context(rb_iv_get(self, "@format")); 96 | AVStream * stream = get_stream(self); 97 | AVPacket decoding_packet; 98 | 99 | av_init_packet(&decoding_packet); 100 | 101 | do { 102 | if(av_read_frame(format_context, &decoding_packet) < 0) { 103 | rb_raise(rb_eRuntimeError, "error extracting packet"); 104 | } 105 | } while(decoding_packet.stream_index != stream->index); 106 | 107 | return rb_float_new(decoding_packet.pts * (double)av_q2d(stream->time_base)); 108 | } 109 | 110 | static int 111 | extract_next_frame(AVFormatContext * format_context, AVCodecContext * codec_context, 112 | int stream_index, AVFrame * frame, AVPacket * decoding_packet) 113 | { 114 | // open codec to decode the video if needed 115 | if (NULL == codec_context->codec) { 116 | rb_fatal("codec should have already been opened"); 117 | } 118 | 119 | uint8_t * databuffer; 120 | 121 | int remaining = 0; 122 | int decoded; 123 | int frame_complete = 0; 124 | int next; 125 | 126 | while(!frame_complete && 127 | 0 == (next = next_packet_for_stream(format_context, stream_index, decoding_packet))) { 128 | // setting parameters before processing decoding_packet data 129 | remaining = decoding_packet->size; 130 | databuffer = decoding_packet->data; 131 | 132 | while(remaining > 0) { 133 | decoded = avcodec_decode_video(codec_context, frame, &frame_complete, 134 | databuffer, remaining); 135 | remaining -= decoded; 136 | // pointer seek forward 137 | databuffer += decoded; 138 | } 139 | } 140 | 141 | return next; 142 | } 143 | 144 | static VALUE 145 | stream_decode_frame(VALUE self) 146 | { 147 | AVFormatContext * format_context = get_format_context(rb_iv_get(self, "@format")); 148 | AVStream * stream = get_stream(self); 149 | 150 | AVCodecContext * codec_context = stream->codec; 151 | 152 | // open codec to decode the video if needed 153 | if (!codec_context->codec) { 154 | AVCodec * codec = avcodec_find_decoder(codec_context->codec_id); 155 | if (!codec) 156 | rb_raise(rb_eRuntimeError, "error codec not found"); 157 | if (avcodec_open(codec_context, codec) < 0) 158 | rb_raise(rb_eRuntimeError, "error while opening codec : %s", codec->name); 159 | } 160 | 161 | VALUE rb_frame = rb_funcall(rb_const_get(rb_mFFMPEG, rb_intern("Frame")), 162 | rb_intern("new"), 3, 163 | INT2NUM(codec_context->width), 164 | INT2NUM(codec_context->height), 165 | INT2NUM(codec_context->pix_fmt)); 166 | 167 | AVFrame * frame = get_frame(rb_frame); 168 | avcodec_get_frame_defaults(frame); 169 | 170 | AVPacket decoding_packet; 171 | av_init_packet(&decoding_packet); 172 | 173 | if (rb_block_given_p()) { 174 | int ret; 175 | do { 176 | ret = extract_next_frame(format_context, stream->codec, 177 | stream->index, frame, &decoding_packet); 178 | rb_yield( 179 | rb_ary_new3( 180 | 3, 181 | rb_frame, 182 | rb_float_new(decoding_packet.pts * (double)av_q2d(stream->time_base)), 183 | rb_float_new(decoding_packet.dts * (double)av_q2d(stream->time_base)) 184 | ) 185 | ); 186 | } while (ret == 0); 187 | } else { 188 | extract_next_frame(format_context, stream->codec, 189 | stream->index, frame, &decoding_packet); 190 | return rb_frame; 191 | } 192 | 193 | return self; 194 | } 195 | 196 | 197 | // ###################### CONSTRUCT / DESTROY ############################# 198 | 199 | void 200 | mark_stream(AVStream * stream) 201 | {} 202 | 203 | void 204 | free_stream(AVStream * stream) 205 | {} 206 | 207 | static VALUE 208 | alloc_stream(VALUE klass) 209 | { 210 | AVStream * stream = av_new_stream(NULL, 0); 211 | return Data_Wrap_Struct(rb_cFFMPEGStream, 0, 0, stream); 212 | } 213 | 214 | static VALUE 215 | stream_initialize(VALUE self, VALUE format) 216 | { 217 | rb_iv_set(self, "@format", format); 218 | return self; 219 | } 220 | 221 | VALUE build_stream_object(AVStream * stream, VALUE rb_format) 222 | { 223 | VALUE rb_stream = Data_Wrap_Struct(rb_cFFMPEGStream, 0, 0, stream); 224 | return stream_initialize(rb_stream, rb_format); 225 | } 226 | 227 | void 228 | Init_FFMPEGStream() 229 | { 230 | rb_cFFMPEGStream = rb_define_class_under(rb_mFFMPEG, "Stream", rb_cObject); 231 | rb_define_alloc_func(rb_cFFMPEGStream, alloc_stream); 232 | rb_define_method(rb_cFFMPEGStream, "initialize", stream_initialize, 0); 233 | 234 | rb_define_method(rb_cFFMPEGStream, "index", stream_index, 0); 235 | rb_define_method(rb_cFFMPEGStream, "codec", stream_codec, 0); 236 | rb_define_method(rb_cFFMPEGStream, "duration", stream_duration, 0); 237 | rb_define_method(rb_cFFMPEGStream, "frame_rate", stream_frame_rate, 0); 238 | rb_define_method(rb_cFFMPEGStream, "position", stream_position, 0); 239 | rb_define_method(rb_cFFMPEGStream, "decode_frame", stream_decode_frame, 0); 240 | rb_define_method(rb_cFFMPEGStream, "seek", stream_seek, 1); 241 | } 242 | -------------------------------------------------------------------------------- /ext/ffmpeg_utils.c: -------------------------------------------------------------------------------- 1 | /* Copyright (c)2008 Antonin Amand. 2 | * Licensed under the Ruby License. See LICENSE for details. 3 | * 4 | */ 5 | #include "ffmpeg.h" 6 | #include "ffmpeg_utils.h" 7 | 8 | AVFormatContext * get_format_context(VALUE self) 9 | { 10 | AVFormatContext * format_context = NULL; 11 | Data_Get_Struct(self, AVFormatContext, format_context); 12 | if (NULL == format_context) { 13 | rb_fatal("FFMPEG internal error\n"); 14 | } 15 | return format_context; 16 | } 17 | 18 | AVStream * get_stream(VALUE self) 19 | { 20 | AVStream * stream = NULL; 21 | Data_Get_Struct(self, AVStream, stream); 22 | if (NULL == stream) { 23 | rb_fatal("FFMPEG internal error\n"); 24 | } 25 | return stream; 26 | } 27 | 28 | AVCodecContext * get_codec_context(VALUE self) 29 | { 30 | AVCodecContext * codec_context = NULL; 31 | Data_Get_Struct(self, AVCodecContext, codec_context); 32 | if (NULL == codec_context) { 33 | rb_fatal("FFMPEG internal error\n"); 34 | } 35 | return codec_context; 36 | } 37 | 38 | AVFrame * get_frame(VALUE self) 39 | { 40 | AVFrame * frame = NULL; 41 | Data_Get_Struct(self, AVFrame, frame); 42 | if (NULL == frame) { 43 | rb_fatal("FFMPEG internal error\n"); 44 | } 45 | return frame; 46 | } 47 | 48 | VALUE rb_sym(const char *s) { 49 | return rb_str_intern(rb_str_new2(s)); 50 | } 51 | 52 | VALUE codec_type_id_to_sym(int codec_type) 53 | { 54 | VALUE type_sym; 55 | switch(codec_type) { 56 | case CODEC_TYPE_AUDIO: 57 | type_sym = rb_sym("audio"); 58 | break; 59 | 60 | case CODEC_TYPE_VIDEO: 61 | type_sym = rb_sym("video"); 62 | break; 63 | 64 | case CODEC_TYPE_SUBTITLE: 65 | type_sym = rb_sym("subtitle"); 66 | break; 67 | 68 | case CODEC_TYPE_DATA: 69 | type_sym = rb_sym("data"); 70 | break; 71 | 72 | case CODEC_TYPE_ATTACHMENT: 73 | type_sym = rb_sym("attachment"); 74 | break; 75 | 76 | case CODEC_TYPE_NB: 77 | type_sym = rb_sym("nb"); 78 | break; 79 | 80 | default: 81 | type_sym = rb_sym("unknown"); 82 | break; 83 | } 84 | 85 | return type_sym; 86 | } 87 | -------------------------------------------------------------------------------- /ext/ffmpeg_utils.h: -------------------------------------------------------------------------------- 1 | /* Copyright (c)2008 Antonin Amand. 2 | * Licensed under the Ruby License. See LICENSE for details. 3 | * 4 | */ 5 | #ifndef RUBY_FFMPEG_UTILS_H__ 6 | #define RUBY_FFMPEG_UTILS_H__ 7 | 8 | #include "ffmpeg.h" 9 | 10 | AVFormatContext * get_format_context(VALUE self); 11 | AVStream * get_stream(VALUE self); 12 | AVCodecContext * get_codec_context(VALUE self); 13 | AVFrame * get_frame(VALUE self); 14 | 15 | VALUE rb_sym(const char *s); 16 | VALUE codec_type_id_to_sym(int codec_type); 17 | 18 | #endif 19 | -------------------------------------------------------------------------------- /ffmpeg-ruby.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = 'ffmpeg-ruby' 3 | spec.version = '0.1.0' 4 | spec.summary = 'Ruby C bindings to ffmpeg/av library, especially usefull to extract thumbnails' 5 | spec.author = "Antonin Amand" 6 | spec.email = 'antonin.amand@gmail.com' 7 | spec.files = Dir['ext/*.{rb,c,h}'] 8 | spec.homepage = 'http://github.com/gwik/ffmpeg-ruby' 9 | spec.has_rdoc = false 10 | spec.extensions << 'ext/extconf.rb' 11 | spec.require_path = 'ext' 12 | spec.date = Time.now 13 | spec.requirements << 'ffmpeg library patched for ffmpeg-ruby' 14 | end 15 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../ext/ffmpeg' 2 | include FFMPEG 3 | 4 | Spec::Runner.configure do |config| 5 | 6 | end 7 | -------------------------------------------------------------------------------- /spec/units/codec_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe FFMPEG::Codec do 4 | 5 | before :each do 6 | GC.start 7 | @testfile = File.dirname(__FILE__) + '/../data/alligator.mp4' 8 | @iformat = InputFormat.new(@testfile) 9 | @codec = @iformat.first_video_stream.codec 10 | end 11 | 12 | it "has a name" do 13 | @codec.name == 'mp4' 14 | end 15 | 16 | it "has an id" do 17 | @codec.id.should == 13 18 | end 19 | 20 | it "has a type" do 21 | @codec.type.should == :video 22 | @iformat.streams[1].codec.type.should == :audio 23 | end 24 | 25 | end -------------------------------------------------------------------------------- /spec/units/frame_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe FFMPEG::Frame do 4 | 5 | before :each do 6 | GC.start 7 | @testfile = File.dirname(__FILE__) + '/../data/alligator.mp4' 8 | @iformat = InputFormat.new(@testfile) 9 | @outfile = '/tmp/ffmpeg-test-frame.out' 10 | @iformat.first_video_stream.seek(9) 11 | @frame = @iformat.first_video_stream.decode_frame 12 | 13 | end 14 | 15 | it "has a width" do 16 | @frame.width.should == 240 17 | end 18 | 19 | it "has a height" do 20 | @frame.height.should == 176 21 | end 22 | 23 | it "convert to Netpbm PPM \"rawbits\" image data format" do 24 | File.open(@outfile, 'w') do |f| 25 | f.write @frame.to_ppm 26 | end 27 | 28 | %x{/usr/bin/env file #{@outfile}}.should match(/ppm/i) 29 | end 30 | 31 | end -------------------------------------------------------------------------------- /spec/units/input_format_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe InputFormat, "loading files" do 4 | 5 | before :each do 6 | GC.start 7 | @testfile = File.dirname(__FILE__) + '/../data/alligator.mp4' 8 | end 9 | 10 | it "does not raise when opening a real supported file" do 11 | lambda do 12 | InputFormat.new(@testfile) 13 | end.should_not raise_error 14 | end 15 | 16 | it "raise a custom exception if format is not supported" do 17 | lambda do 18 | InputFormat.new(File.dirname(__FILE__) + '/../data/corrupt.avi') 19 | end.should raise_error(UnsupportedFormat) 20 | end 21 | 22 | it "raises argument error when file does not exists" do 23 | lambda do 24 | InputFormat.new('/thisfiledoesnotexists.mp4') 25 | end.should raise_error(ArgumentError) 26 | end 27 | 28 | end 29 | 30 | describe InputFormat, "querying information" do 31 | 32 | before :each do 33 | GC.start 34 | @testfile = File.dirname(__FILE__) + '/../data/alligator.mp4' 35 | @iformat = InputFormat.new(@testfile) 36 | end 37 | 38 | it "has a filename" do 39 | @iformat.filename.should == @testfile 40 | end 41 | 42 | it "has a bitrate" do 43 | (@iformat.bit_rate / 1000).should == 314 44 | end 45 | 46 | it "has a duration in seconds" do 47 | @iformat.duration.should == 23.2 48 | end 49 | 50 | it "has an human duration" do 51 | @iformat.human_duration.should == "00:00:23.2" 52 | end 53 | 54 | end 55 | 56 | describe InputFormat, "with streams" do 57 | 58 | before :each do 59 | GC.start 60 | @testfile = File.dirname(__FILE__) + '/../data/alligator.mp4' 61 | @iformat = InputFormat.new(@testfile) 62 | end 63 | 64 | it "has streams" do 65 | @iformat.streams.each do |stream| 66 | stream.should be_kind_of(Stream) 67 | end 68 | end 69 | 70 | it "detects video streams" do 71 | @iformat.has_video?.should be(true) 72 | end 73 | 74 | it "detects audio streams" do 75 | @iformat.has_audio?.should be(true) 76 | end 77 | 78 | it "counts its video streams" do 79 | @iformat.video_stream_count.should == 1 80 | end 81 | 82 | it "counts its audio streams" do 83 | @iformat.audio_stream_count.should == 1 84 | end 85 | 86 | it "has a first video stream" do 87 | @iformat.first_video_stream.index.should == 0 88 | end 89 | 90 | it "has a first audio stream" do 91 | @iformat.first_audio_stream.index.should == 1 92 | end 93 | 94 | end -------------------------------------------------------------------------------- /spec/units/stream_spec.rb: -------------------------------------------------------------------------------- 1 | require File.dirname(__FILE__) + '/../spec_helper' 2 | 3 | describe FFMPEG::Stream, "attributes" do 4 | 5 | before :each do 6 | GC.start 7 | @testfile = File.dirname(__FILE__) + '/../data/alligator.mp4' 8 | @iformat = InputFormat.new(@testfile) 9 | end 10 | 11 | it "has a duration" do 12 | @iformat.streams.each do |stream| 13 | stream.duration.to_i.should == 23 14 | end 15 | end 16 | 17 | it "has the index of its format" do 18 | @iformat.streams.each_with_index do |stream, index| 19 | stream.index.should == index 20 | end 21 | end 22 | 23 | it "has a codec" do 24 | @iformat.streams.each do |stream| 25 | stream.codec.should be_kind_of(FFMPEG::Codec) 26 | end 27 | end 28 | 29 | it "has a frame rate" do 30 | @iformat.first_video_stream.frame_rate.should == 30 31 | end 32 | 33 | end 34 | 35 | describe FFMPEG::Stream, "seeking and decoding frame" do 36 | 37 | before :each do 38 | GC.start 39 | @testfile = File.dirname(__FILE__) + '/../data/alligator.mp4' 40 | @iformat = InputFormat.new(@testfile) 41 | @stream = @iformat.first_video_stream 42 | end 43 | 44 | it "decodes a frame" do 45 | @stream.decode_frame.should be_kind_of(FFMPEG::Frame) 46 | end 47 | 48 | it "has a position" do 49 | @stream.position.should == 0 50 | end 51 | 52 | it "seeks to a position" do 53 | @stream.seek(10) 54 | @stream.position.to_i.should == 10 55 | end 56 | 57 | it "raises when seek to far" do 58 | lambda do 59 | @stream.seek(100) 60 | end.should raise_error(RangeError) 61 | end 62 | 63 | it "decode each frame" do 64 | 65 | @stream.seek(10) 66 | i = 0 67 | frames = [] 68 | @stream.decode_frame do |frame, pts, dts| 69 | i += 1 70 | frames << frame 71 | frame.should be_kind_of(FFMPEG::Frame) 72 | break if dts > 12 73 | end 74 | 75 | frames.size.should > 0 76 | end 77 | 78 | 79 | end --------------------------------------------------------------------------------