puremotion 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/{README.md → README.markdown} +4 -3
- data/Rakefile +7 -1
- data/bin/default.yaml +0 -0
- data/bin/pmts +19 -0
- data/examples/failure_handling.rb +28 -0
- data/examples/process.rb +7 -0
- data/examples/progress_reporting.rb +1 -1
- data/ext/puremotion/frame.c +38 -2
- data/ext/puremotion/media.c +2 -1
- data/ext/puremotion/stream.c +0 -22
- data/ext/puremotion/utils.c +1 -1
- data/ext/puremotion/video.c +51 -1
- data/lib/events/event.rb +3 -3
- data/lib/events/generator.rb +24 -43
- data/lib/preset/preset.rb +31 -5
- data/lib/process.rb +88 -0
- data/lib/puremotion.rb +2 -2
- data/lib/puremotion_native.so +0 -0
- data/lib/server/app.rb +27 -0
- data/lib/server/database.rb +19 -0
- data/lib/server/server.rb +39 -0
- data/lib/settings.rb +19 -0
- data/lib/tools/ffmpeg.rb +4 -92
- data/lib/transcode/transcode.rb +95 -46
- data/spec/spec_helper.rb +4 -0
- data/spec/units/media_spec.rb +69 -5
- data/spec/units/transcode_spec.rb +15 -0
- metadata +16 -7
@@ -1,7 +1,8 @@
|
|
1
1
|
PureMotion (0.1.0)
|
2
2
|
==================
|
3
3
|
|
4
|
-
**Homepage**: [http://github.com/ominiom/puremotion](http://github.com/ominiom/puremotion)
|
4
|
+
* **Homepage**: [http://github.com/ominiom/puremotion](http://github.com/ominiom/puremotion)
|
5
|
+
* **Docs (Incomplete)**: [http://ominiom.net/projects/puremotion/doc/](http://ominiom.net/projects/puremotion/doc/)
|
5
6
|
|
6
7
|
OVERVIEW
|
7
8
|
--------
|
@@ -57,7 +58,7 @@ a video stream.
|
|
57
58
|
Media 'sample.mp4' do
|
58
59
|
|
59
60
|
if video? then
|
60
|
-
seek(5).grab.resize(320, 240).save('thumb.png')
|
61
|
+
video.seek(5).grab.resize(320, 240).save('thumb.png')
|
61
62
|
end
|
62
63
|
|
63
64
|
end
|
@@ -66,4 +67,4 @@ BUILDING
|
|
66
67
|
--------
|
67
68
|
|
68
69
|
To build the PureMotion gem from source you will need to have libav* and libgd
|
69
|
-
installed.
|
70
|
+
installed.
|
data/Rakefile
CHANGED
@@ -12,7 +12,7 @@ begin
|
|
12
12
|
gem.authors = ["Ominiom"]
|
13
13
|
gem.extensions = ["ext/puremotion/extconf.rb"]
|
14
14
|
gem.files = [
|
15
|
-
"README.
|
15
|
+
"README.markdown",
|
16
16
|
"LICENSE",
|
17
17
|
"Rakefile",
|
18
18
|
".yardopts",
|
@@ -63,3 +63,9 @@ rescue LoadError
|
|
63
63
|
abort "YARD is not available. In order to run yardoc, you must: sudo gem install yard"
|
64
64
|
end
|
65
65
|
end
|
66
|
+
|
67
|
+
require 'spec/rake/spectask'
|
68
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
69
|
+
spec.libs << 'lib' << 'spec'
|
70
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
71
|
+
end
|
data/bin/default.yaml
ADDED
File without changes
|
data/bin/pmts
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'daemons'
|
5
|
+
|
6
|
+
server = File.join(File.dirname(__FILE__), %w(.. lib server server.rb))
|
7
|
+
|
8
|
+
options = {
|
9
|
+
:app_name => "pmts",
|
10
|
+
:dir_mode => :normal,
|
11
|
+
:dir => File.dirname(__FILE__),
|
12
|
+
:multiple => false,
|
13
|
+
:ontop => false,
|
14
|
+
:mode => :load,
|
15
|
+
:backtrace => true,
|
16
|
+
:monitor => true
|
17
|
+
}
|
18
|
+
|
19
|
+
Daemons.run(server, options)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
Transcode '../spec/samples/sample.ogv' do
|
2
|
+
|
3
|
+
event :failure do |reason, message|
|
4
|
+
puts "Failure - #{message}"
|
5
|
+
end
|
6
|
+
|
7
|
+
event :complete do |output|
|
8
|
+
puts "Success! Output is #{output.video.duration}"
|
9
|
+
end
|
10
|
+
|
11
|
+
overwrite!
|
12
|
+
|
13
|
+
video do
|
14
|
+
codec :flv
|
15
|
+
bitrate '200k'
|
16
|
+
resolution 320, 176
|
17
|
+
end
|
18
|
+
|
19
|
+
audio do
|
20
|
+
codec :libmp3lame
|
21
|
+
bitrate '96k'
|
22
|
+
end
|
23
|
+
|
24
|
+
log 'failure_handling.log'
|
25
|
+
|
26
|
+
output '/totally/invalid/path/on/normal/systems.flv'
|
27
|
+
|
28
|
+
end
|
data/examples/process.rb
ADDED
data/ext/puremotion/frame.c
CHANGED
@@ -22,6 +22,13 @@ static AVFrame * alloc_picture(int pix_fmt, int width, int height) {
|
|
22
22
|
return picture;
|
23
23
|
}
|
24
24
|
|
25
|
+
/* Convert frame to RGB24
|
26
|
+
*
|
27
|
+
* Used internally to convert the pixel format when resizing and saving to PNG
|
28
|
+
*
|
29
|
+
* @return [PureMotion::Frame] Frame in RGB24 format
|
30
|
+
*
|
31
|
+
*/
|
25
32
|
static VALUE frame_to_rgb24(VALUE self) {
|
26
33
|
int width = NUM2INT(rb_iv_get(self, "@width"));
|
27
34
|
int height = NUM2INT(rb_iv_get(self, "@height"));
|
@@ -42,8 +49,10 @@ static VALUE frame_to_rgb24(VALUE self) {
|
|
42
49
|
return build_frame_object(to, width, height, PIX_FMT_RGB24);
|
43
50
|
}
|
44
51
|
|
45
|
-
|
46
|
-
|
52
|
+
/* Encodes the frame in PPM format
|
53
|
+
*
|
54
|
+
* @return [String] PPM encoded representation of the frame
|
55
|
+
*/
|
47
56
|
static VALUE frame_to_ppm(VALUE self) {
|
48
57
|
VALUE rb_frame = frame_to_rgb24(self);
|
49
58
|
AVFrame * frame = get_frame(rb_frame);
|
@@ -63,6 +72,13 @@ static VALUE frame_to_ppm(VALUE self) {
|
|
63
72
|
return rb_str_new(data_string, size);
|
64
73
|
}
|
65
74
|
|
75
|
+
/* Resizes the frame using libgd
|
76
|
+
*
|
77
|
+
* @param [Integer] width Target width
|
78
|
+
* @param [Integer] height Target height
|
79
|
+
*
|
80
|
+
* @return [PureMotion::Frame] New frame that's been resized
|
81
|
+
*/
|
66
82
|
static VALUE frame_resize(VALUE self, VALUE w, VALUE h) {
|
67
83
|
|
68
84
|
int orig_width = NUM2INT(rb_iv_get(self, "@width"));
|
@@ -89,8 +105,28 @@ static VALUE frame_resize(VALUE self, VALUE w, VALUE h) {
|
|
89
105
|
|
90
106
|
}
|
91
107
|
|
108
|
+
/* Saves the frame to PNG
|
109
|
+
*
|
110
|
+
* @param [String] path A writable path to save the frame to
|
111
|
+
*
|
112
|
+
* @return [PureMotion::Frame] self
|
113
|
+
*/
|
92
114
|
static VALUE frame_save(VALUE self, VALUE filename) {
|
93
115
|
|
116
|
+
VALUE directory = rb_funcall(rb_cFile, rb_intern("dirname"), 1, filename);
|
117
|
+
VALUE writable = rb_funcall(rb_cFile, rb_intern("writable?"), 1, directory);
|
118
|
+
VALUE exists = rb_funcall(rb_cFile, rb_intern("exists?"), 1, filename);
|
119
|
+
|
120
|
+
if( exists == Qtrue ) {
|
121
|
+
rb_raise(rb_eArgError, "File '%s' already exists.", STR2CSTR(filename));
|
122
|
+
return self;
|
123
|
+
}
|
124
|
+
|
125
|
+
if( writable == Qfalse ) {
|
126
|
+
rb_raise(rb_eArgError, "File '%s' is not writable.", STR2CSTR(filename));
|
127
|
+
return self;
|
128
|
+
}
|
129
|
+
|
94
130
|
int w = NUM2INT(rb_iv_get(self, "@width"));
|
95
131
|
int h = NUM2INT(rb_iv_get(self, "@height"));
|
96
132
|
|
data/ext/puremotion/media.c
CHANGED
@@ -115,7 +115,7 @@ static VALUE media_init(VALUE self, VALUE file) {
|
|
115
115
|
if( error < 0 ) {
|
116
116
|
rb_raise(rb_eUnsupportedFormat, "File '%s' unable to be opened. Unsupported format.", StringValuePtr(file));
|
117
117
|
rb_iv_set(self, "@valid", Qfalse);
|
118
|
-
return
|
118
|
+
return self;
|
119
119
|
}
|
120
120
|
|
121
121
|
error = av_find_stream_info(fmt_ctx);
|
@@ -123,6 +123,7 @@ static VALUE media_init(VALUE self, VALUE file) {
|
|
123
123
|
if( error < 0 ) {
|
124
124
|
rb_raise(rb_eUnsupportedFormat, "File '%s': Streams are unreadable.", StringValuePtr(file));
|
125
125
|
rb_iv_set(self, "@valid", Qfalse);
|
126
|
+
return self;
|
126
127
|
}
|
127
128
|
|
128
129
|
rb_iv_set(self, "@valid", Qtrue);
|
data/ext/puremotion/stream.c
CHANGED
@@ -4,28 +4,6 @@
|
|
4
4
|
VALUE rb_cStream;
|
5
5
|
VALUE rb_mStreams;
|
6
6
|
|
7
|
-
static int next_packet(AVFormatContext * format_context, AVPacket * packet)
|
8
|
-
{
|
9
|
-
if(packet->data != NULL)
|
10
|
-
av_free_packet(packet);
|
11
|
-
|
12
|
-
if(av_read_frame(format_context, packet) < 0) {
|
13
|
-
return -1;
|
14
|
-
}
|
15
|
-
|
16
|
-
return 0;
|
17
|
-
}
|
18
|
-
|
19
|
-
static int next_packet_for_stream(AVFormatContext * format_context, int stream_index, AVPacket * packet)
|
20
|
-
{
|
21
|
-
int ret = 0;
|
22
|
-
do {
|
23
|
-
ret = next_packet(format_context, packet);
|
24
|
-
} while(packet->stream_index != stream_index && ret == 0);
|
25
|
-
|
26
|
-
return ret;
|
27
|
-
}
|
28
|
-
|
29
7
|
static VALUE stream_type( VALUE self ) {
|
30
8
|
AVStream * stream = get_stream(self);
|
31
9
|
|
data/ext/puremotion/utils.c
CHANGED
data/ext/puremotion/video.c
CHANGED
@@ -5,6 +5,35 @@ VALUE rb_cStream;
|
|
5
5
|
VALUE rb_cVideoStream;
|
6
6
|
VALUE rb_mStreams;
|
7
7
|
|
8
|
+
static int next_packet(AVFormatContext * format_context, AVPacket * packet)
|
9
|
+
{
|
10
|
+
if(packet->data != NULL)
|
11
|
+
av_free_packet(packet);
|
12
|
+
|
13
|
+
if(av_read_frame(format_context, packet) < 0) {
|
14
|
+
return -1;
|
15
|
+
}
|
16
|
+
|
17
|
+
return 0;
|
18
|
+
}
|
19
|
+
|
20
|
+
static int next_packet_for_stream(AVFormatContext * format_context, int stream_index, AVPacket * packet)
|
21
|
+
{
|
22
|
+
int ret = 0;
|
23
|
+
do {
|
24
|
+
ret = next_packet(format_context, packet);
|
25
|
+
} while(packet->stream_index != stream_index && ret == 0);
|
26
|
+
|
27
|
+
return ret;
|
28
|
+
}
|
29
|
+
|
30
|
+
/*
|
31
|
+
* call-seq: frame_rate => Float
|
32
|
+
*
|
33
|
+
* Returns the video frame rate as a Float
|
34
|
+
*
|
35
|
+
* @return [Float] Framerate
|
36
|
+
*/
|
8
37
|
static VALUE stream_frame_rate(VALUE self) {
|
9
38
|
AVStream * stream = get_stream(self);
|
10
39
|
return(rb_float_new(av_q2d(stream->r_frame_rate)));
|
@@ -44,8 +73,23 @@ static int extract_next_frame(AVFormatContext * format_context, AVCodecContext *
|
|
44
73
|
/*
|
45
74
|
* call-seq: grab => PureMotion::Frame
|
46
75
|
*
|
76
|
+
* Grabs a single frame from the current position in the stream.
|
77
|
+
*
|
78
|
+
* Example Usage:
|
79
|
+
*
|
80
|
+
* Media 'sample.wmv' do
|
81
|
+
*
|
82
|
+
* if video? then
|
83
|
+
*
|
84
|
+
* frame = video.seek(5).grab
|
85
|
+
*
|
86
|
+
* # Resize, save
|
47
87
|
*
|
88
|
+
* end
|
48
89
|
*
|
90
|
+
* end
|
91
|
+
*
|
92
|
+
* @return [PureMotion::Frame]
|
49
93
|
*/
|
50
94
|
|
51
95
|
static VALUE stream_grab(VALUE self) {
|
@@ -98,6 +142,12 @@ static VALUE stream_grab(VALUE self) {
|
|
98
142
|
return self;
|
99
143
|
}
|
100
144
|
|
145
|
+
/* call-seq: resolution => Array<Integer width, Integer height>
|
146
|
+
*
|
147
|
+
* Returns video frame resolution as [width, height] array
|
148
|
+
*
|
149
|
+
* @return [Array<Integer width, Integer height>]
|
150
|
+
*/
|
101
151
|
static VALUE stream_resolution(VALUE self, VALUE media) {
|
102
152
|
AVFormatContext * format_context = get_format_context(rb_iv_get(self, "@media"));
|
103
153
|
AVStream * stream = get_stream(self);
|
@@ -138,4 +188,4 @@ void Init_video_stream() {
|
|
138
188
|
rb_define_method(rb_cVideoStream, "resolution", stream_resolution, 0);
|
139
189
|
rb_define_method(rb_cVideoStream, "frame_rate", stream_frame_rate, 0);
|
140
190
|
rb_define_method(rb_cVideoStream, "grab", stream_grab, 0);
|
141
|
-
}
|
191
|
+
}
|
data/lib/events/event.rb
CHANGED
@@ -43,17 +43,17 @@ module PureMotion::Events
|
|
43
43
|
|
44
44
|
# Calls all of the handlers for the events, with the given sender and set
|
45
45
|
# of arguments.
|
46
|
-
def call(
|
46
|
+
def call(*args)
|
47
47
|
# Make sure we don't get surprised with new friends while we're calling
|
48
48
|
# the handlers
|
49
49
|
named_handlers = @named_handlers
|
50
50
|
handlers = @handlers
|
51
51
|
|
52
52
|
named_handlers.each do |name, handler|
|
53
|
-
handler.call(
|
53
|
+
handler.call(*args)
|
54
54
|
end
|
55
55
|
handlers.each do |handler|
|
56
|
-
handler.call(
|
56
|
+
handler.call(*args)
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
data/lib/events/generator.rb
CHANGED
@@ -1,50 +1,31 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
# Copyright :: Copyright (C) 2009 Stefan Nuxoll
|
1
|
+
module PureMotion
|
2
|
+
module Events
|
3
|
+
module Generator
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
@_events["#{event_name}"] = PureMotion::Events::Event.new
|
20
|
-
end
|
21
|
-
if args != []
|
22
|
-
@_events["#{event_name}"].call(self, args)
|
23
|
-
else
|
24
|
-
if block
|
25
|
-
#{event_name} + block
|
26
|
-
else
|
27
|
-
@_events["#{event_name}"]
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
def #{event_name}=(event)
|
33
|
-
_ensure_events
|
34
|
-
if PureMotion::Events::Event === event
|
35
|
-
@_events["#{event_name}"] = event
|
5
|
+
def event(event_name)
|
6
|
+
@@_events_built = false unless class_variable_defined?(:@@_events_built)
|
7
|
+
self._build_events unless @@_events_built
|
8
|
+
end
|
9
|
+
|
10
|
+
def _build_events
|
11
|
+
self.class_eval do
|
12
|
+
|
13
|
+
def on(event_name, &block)
|
14
|
+
@_events = {} unless instance_variable_defined?(:@_events)
|
15
|
+
@_events[event_name] = PureMotion::Events::Event.new unless @_events[event_name]
|
16
|
+
return @_events[event_name] unless block_given?
|
17
|
+
@_events[event_name] + block
|
36
18
|
end
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
@_events = {}
|
19
|
+
|
20
|
+
def fire(event_name, *args)
|
21
|
+
@_events = {} unless instance_variable_defined?(:@_events)
|
22
|
+
@_events[event_name].call(*args) unless @_events[event_name].nil?
|
42
23
|
end
|
24
|
+
|
43
25
|
end
|
44
|
-
|
45
|
-
|
46
|
-
|
26
|
+
@@_events_built = true
|
27
|
+
end
|
28
|
+
|
47
29
|
end
|
48
|
-
|
49
30
|
end
|
50
31
|
end
|
data/lib/preset/preset.rb
CHANGED
@@ -4,7 +4,7 @@ module PureMotion
|
|
4
4
|
|
5
5
|
def self.build(&block)
|
6
6
|
|
7
|
-
preset = self.new(:dsl)
|
7
|
+
preset = self.new({ :from => :dsl })
|
8
8
|
preset.instance_eval(&block) if block_given?
|
9
9
|
|
10
10
|
preset
|
@@ -13,7 +13,16 @@ module PureMotion
|
|
13
13
|
|
14
14
|
attr_accessor :event_handlers
|
15
15
|
|
16
|
-
def initialize(
|
16
|
+
def initialize(options = {})
|
17
|
+
|
18
|
+
defaults = {
|
19
|
+
:from => :dsl,
|
20
|
+
:input => nil,
|
21
|
+
:output => nil
|
22
|
+
}
|
23
|
+
|
24
|
+
options = defaults.merge(options)
|
25
|
+
|
17
26
|
@input = nil
|
18
27
|
@output = nil
|
19
28
|
@log = nil
|
@@ -22,7 +31,11 @@ module PureMotion
|
|
22
31
|
@commands = []
|
23
32
|
@event_handlers = {}
|
24
33
|
@dsls = [:general, :file, :video, :audio, :metadata ,:crop, :pad]
|
25
|
-
|
34
|
+
|
35
|
+
input(options[:input]) unless options[:input].nil?
|
36
|
+
output(options[:output]) unless options[:output].nil?
|
37
|
+
|
38
|
+
extend_for(:general) if options[:from] == :dsl
|
26
39
|
end
|
27
40
|
|
28
41
|
def method_missing(name, *args, &block)
|
@@ -42,7 +55,7 @@ module PureMotion
|
|
42
55
|
end
|
43
56
|
|
44
57
|
def event_handler(name)
|
45
|
-
@event_handlers[name]
|
58
|
+
@event_handlers[name] or nil
|
46
59
|
end
|
47
60
|
|
48
61
|
def log(log=nil)
|
@@ -65,15 +78,28 @@ module PureMotion
|
|
65
78
|
return false unless @input.nil?
|
66
79
|
if input.is_a?(String) then
|
67
80
|
if ::File.exists?(input) then # Ruby's File class is hidden by Preset::File
|
68
|
-
|
81
|
+
begin
|
82
|
+
@input = PureMotion::Media.new(input)
|
83
|
+
rescue PureMotion::UnsupportedFormat
|
84
|
+
if event_handler(:failure).nil? then
|
85
|
+
raise ArgumentError, "Invalid input file '#{input}'" if event_handler(:failure).nil?
|
86
|
+
return false
|
87
|
+
else
|
88
|
+
event_handler(:failure).call(:invalid_input, "Invalid input file '#{input}'")
|
89
|
+
return false
|
90
|
+
end
|
91
|
+
end
|
69
92
|
else
|
70
93
|
raise ArgumentError, "Input file '#{input}' not found"
|
94
|
+
return false
|
71
95
|
end
|
72
96
|
else
|
73
97
|
if input.is_a?(PureMotion::Media)
|
74
98
|
@input = input
|
99
|
+
return true
|
75
100
|
else
|
76
101
|
raise ArgumentError, "Invalid input"
|
102
|
+
return false
|
77
103
|
end
|
78
104
|
end
|
79
105
|
raise ArgumentError, "Invalid media input '#{@input.filename}'" unless @input.valid?
|
data/lib/process.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
module PureMotion
|
2
|
+
|
3
|
+
class Process
|
4
|
+
|
5
|
+
event :start
|
6
|
+
event :exit
|
7
|
+
event :output
|
8
|
+
|
9
|
+
def initialize(options = {})
|
10
|
+
|
11
|
+
defaults = {
|
12
|
+
:command => nil,
|
13
|
+
:autostart => false
|
14
|
+
}
|
15
|
+
|
16
|
+
options = { :command => options } if options.is_a?(String)
|
17
|
+
|
18
|
+
options = defaults.merge(options)
|
19
|
+
|
20
|
+
@command = options[:command]
|
21
|
+
|
22
|
+
raise ArgumentError, "No command given" if @command.nil?
|
23
|
+
|
24
|
+
@status = :ready
|
25
|
+
@buffer = ''
|
26
|
+
@output = []
|
27
|
+
end
|
28
|
+
|
29
|
+
def run
|
30
|
+
@status = :running
|
31
|
+
|
32
|
+
@process = IO.popen("#{@command} 2>&1")
|
33
|
+
@id = @process.pid
|
34
|
+
|
35
|
+
runner = lambda {
|
36
|
+
@process.each_byte { |byte|
|
37
|
+
@process.eof? ? handle_exit : handle_output(byte)
|
38
|
+
}
|
39
|
+
}
|
40
|
+
|
41
|
+
if PureMotion::Settings[:threaded] then
|
42
|
+
PureMotion::Thread.new { runner.call }
|
43
|
+
else
|
44
|
+
runner.call
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
def kill!
|
50
|
+
return false unless @status == :running
|
51
|
+
::Process.kill('TERM', @id)
|
52
|
+
end
|
53
|
+
|
54
|
+
def kill!
|
55
|
+
return false unless @status == :running
|
56
|
+
::Process.kill('KILL', @id)
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def find_in_output(regexp)
|
62
|
+
return '' if @status == :ready
|
63
|
+
m = regexp.match(@output.join('\n'))
|
64
|
+
return m[1] if m
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
def handle_output(byte)
|
69
|
+
if [10, 13].include?(byte) then
|
70
|
+
@output << @buffer
|
71
|
+
fire(:output, @buffer)
|
72
|
+
@buffer = ''
|
73
|
+
else
|
74
|
+
byte = byte.chr.to_s
|
75
|
+
@buffer = @buffer + byte
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def handle_exit
|
80
|
+
@status = :exited
|
81
|
+
@id = nil
|
82
|
+
@process.close
|
83
|
+
fire(:exit)
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
data/lib/puremotion.rb
CHANGED
@@ -12,6 +12,7 @@ require req + 'events/generator'
|
|
12
12
|
|
13
13
|
Object.extend(PureMotion::Events::Generator)
|
14
14
|
|
15
|
+
require req + 'settings'
|
15
16
|
require req + 'preset/preset'
|
16
17
|
|
17
18
|
require req + 'preset/general'
|
@@ -22,9 +23,8 @@ require req + 'preset/video/crop'
|
|
22
23
|
require req + 'preset/video/pad'
|
23
24
|
require req + 'preset/audio/audio'
|
24
25
|
|
25
|
-
|
26
|
+
require req + 'process'
|
26
27
|
require req + 'threading'
|
27
28
|
require req + 'media'
|
28
|
-
require req + 'codecs'
|
29
29
|
require req + 'transcode/transcode'
|
30
30
|
require req + 'tools/ffmpeg'
|
data/lib/puremotion_native.so
CHANGED
Binary file
|
data/lib/server/app.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
Models = PureMotion::Server::Models
|
2
|
+
|
3
|
+
class PureMotion::Server::App < Sinatra::Base
|
4
|
+
|
5
|
+
set :port, OPTIONS[:port]
|
6
|
+
set :public, File.dirname(__FILE__) + '/static'
|
7
|
+
set :views, File.dirname(__FILE__) + '/views'
|
8
|
+
|
9
|
+
use Rack::Auth::Basic do |username, password|
|
10
|
+
[username, password] == ['admin', 'admin']
|
11
|
+
end
|
12
|
+
|
13
|
+
get '/transcode/new' do
|
14
|
+
haml :'transcode/new'
|
15
|
+
end
|
16
|
+
|
17
|
+
post '/transcode/create' do
|
18
|
+
params[:file][:tempfile].methods.inspect
|
19
|
+
end
|
20
|
+
|
21
|
+
get '/transcode/:id/download' do
|
22
|
+
transcode = Models::Transcode.get(params[:id])
|
23
|
+
return error 404 unless transcode
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
DataMapper::Logger.new($stdout, :debug)
|
2
|
+
DataMapper.setup(:default, 'sqlite3::memory:')
|
3
|
+
|
4
|
+
module PureMotion::Server::Models
|
5
|
+
|
6
|
+
class Transcode
|
7
|
+
include DataMapper::Resource
|
8
|
+
|
9
|
+
property :id, Serial
|
10
|
+
property :name, String
|
11
|
+
property :file, String
|
12
|
+
property :status, String
|
13
|
+
property :attempts, Integer
|
14
|
+
property :progress, String
|
15
|
+
end
|
16
|
+
|
17
|
+
DataMapper.auto_migrate!
|
18
|
+
|
19
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
LOAD_PATH = File.dirname(__FILE__)
|
2
|
+
require File.expand_path(File.join(LOAD_PATH, *%w(.. puremotion)))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'dm-core'
|
6
|
+
require 'sinatra'
|
7
|
+
require 'json'
|
8
|
+
require 'log4r'
|
9
|
+
require 'optparse'
|
10
|
+
require 'yaml'
|
11
|
+
|
12
|
+
OPTIONS = {
|
13
|
+
:config => 'default.yml',
|
14
|
+
:port => 9005
|
15
|
+
}
|
16
|
+
|
17
|
+
OptionParser.new do |opts|
|
18
|
+
|
19
|
+
opts.on("-c", "--config PATH", "Path to config file") do |path|
|
20
|
+
OPTIONS[:config] = path
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("-p", "--port N", "Listening port") do |port|
|
24
|
+
OPTIONS[:port] = port
|
25
|
+
end
|
26
|
+
|
27
|
+
end.parse!
|
28
|
+
|
29
|
+
CONFIG = YAML.load(OPTIONS[:config]) if File.exists?(OPTIONS[:config])
|
30
|
+
|
31
|
+
module PureMotion::Server
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
%w(database app).each do |file|
|
36
|
+
require File.join(LOAD_PATH, file)
|
37
|
+
end
|
38
|
+
|
39
|
+
PureMotion::Server::App.run!
|
data/lib/settings.rb
ADDED
data/lib/tools/ffmpeg.rb
CHANGED
@@ -1,101 +1,13 @@
|
|
1
1
|
module PureMotion::Tools
|
2
2
|
|
3
|
-
class FFmpeg
|
3
|
+
class FFmpeg < PureMotion::Process
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
NOT_STARTED = 1
|
8
|
-
INITIALIZING = 2
|
9
|
-
ANALYZING = 4
|
10
|
-
PREPARING_OUTPUT = 8
|
11
|
-
ENCODING = 16
|
12
|
-
ERROR = 32
|
13
|
-
|
14
|
-
end
|
15
|
-
|
16
|
-
attr_reader :pid
|
17
|
-
attr_reader :args
|
18
|
-
attr_accessor :output
|
19
|
-
|
20
|
-
event :line
|
21
|
-
event :complete
|
22
|
-
event :exited
|
23
|
-
event :status_change
|
24
|
-
|
25
|
-
@pid = nil
|
26
|
-
|
27
|
-
def initialize(params = {})
|
28
|
-
if params.class != Hash then raise ArgumentError, "Invalid parameters" end
|
29
|
-
|
30
|
-
@defaults = {
|
31
|
-
:ffmpeg => 'ffmpeg',
|
32
|
-
:options => nil
|
33
|
-
}
|
34
|
-
|
35
|
-
@params = params
|
36
|
-
|
37
|
-
@options = @params[:options]
|
38
|
-
|
39
|
-
if @options.nil? then
|
40
|
-
raise ArgumentError, "No options given"
|
41
|
-
end
|
42
|
-
|
43
|
-
@output = []
|
44
|
-
end
|
45
|
-
|
46
|
-
def run
|
47
|
-
|
48
|
-
temp = ''
|
49
|
-
|
50
|
-
@thread = PureMotion::Thread.new do
|
51
|
-
@pio = IO.popen("ffmpeg #{@options} 2>&1")
|
52
|
-
@pid = @pio.pid
|
53
|
-
@pio.each_byte do |l|
|
54
|
-
if l == 10 or l == 13 then
|
55
|
-
@output.push temp
|
56
|
-
line(temp)
|
57
|
-
temp = ''
|
58
|
-
l = ''
|
59
|
-
else
|
60
|
-
l = l.chr
|
61
|
-
end
|
62
|
-
temp = temp + l
|
63
|
-
#@output << l.gsub("\r","\n")
|
64
|
-
end
|
65
|
-
if @pio.eof? then
|
66
|
-
@done = true
|
67
|
-
complete(true)
|
68
|
-
@pid = nil
|
69
|
-
@pio.close
|
70
|
-
exited(true)
|
71
|
-
end
|
72
|
-
end
|
73
|
-
|
74
|
-
end
|
75
|
-
|
76
|
-
def ran?
|
77
|
-
@pio.eof? unless @pio.nil?
|
78
|
-
false
|
79
|
-
end
|
80
|
-
|
81
|
-
def ended?
|
82
|
-
return false unless ran?
|
83
|
-
@pio.eof?
|
5
|
+
def initialize(arguments)
|
6
|
+
super("ffmpeg #{arguments}")
|
84
7
|
end
|
85
8
|
|
86
9
|
def version
|
87
|
-
|
88
|
-
find(/^FFmpeg version (\S+)/)
|
89
|
-
end
|
90
|
-
|
91
|
-
def find(regexp)
|
92
|
-
m = regexp.match(@output)
|
93
|
-
return m[1] if m
|
94
|
-
nil
|
95
|
-
end
|
96
|
-
|
97
|
-
def status
|
98
|
-
@status
|
10
|
+
find_in_ouput(/^FFmpeg version (\S+)/)
|
99
11
|
end
|
100
12
|
|
101
13
|
end
|
data/lib/transcode/transcode.rb
CHANGED
@@ -2,38 +2,46 @@ def Transcode(*args, &block)
|
|
2
2
|
|
3
3
|
raise ArgumentError, "No block given for transcode preset" unless block_given?
|
4
4
|
|
5
|
-
|
5
|
+
preset_options = {
|
6
|
+
:from => :dsl
|
7
|
+
}
|
8
|
+
|
9
|
+
preset_options[:input] = args[0] || nil
|
10
|
+
|
11
|
+
preset = PureMotion::Preset.new(preset_options)
|
6
12
|
preset.instance_eval(&block)
|
7
13
|
|
8
|
-
|
9
|
-
:input => preset.input,
|
10
|
-
:output => preset.output,
|
14
|
+
transcode_options = {
|
11
15
|
:preset => preset
|
12
16
|
}
|
13
17
|
|
14
|
-
|
18
|
+
transcode_options[:log] = preset.log unless preset.log.nil?
|
15
19
|
|
16
|
-
transcode = PureMotion::Transcode::Transcode.new(
|
20
|
+
transcode = PureMotion::Transcode::Transcode.new(transcode_options)
|
17
21
|
|
18
|
-
transcode.
|
19
|
-
|
20
|
-
|
22
|
+
if transcode.valid? then
|
23
|
+
transcode.run
|
24
|
+
return transcode
|
25
|
+
else
|
26
|
+
return false
|
27
|
+
end
|
21
28
|
|
22
29
|
end
|
23
30
|
|
24
31
|
module PureMotion::Transcode
|
25
32
|
|
26
|
-
class Transcode
|
33
|
+
class Transcode
|
27
34
|
|
28
35
|
event :progress
|
29
36
|
event :complete
|
30
37
|
event :output
|
31
|
-
|
32
|
-
|
33
|
-
@input = nil
|
34
|
-
|
38
|
+
event :failure
|
39
|
+
|
35
40
|
def initialize(options)
|
36
|
-
|
41
|
+
|
42
|
+
@valid = true # Until it all goes wrong
|
43
|
+
|
44
|
+
# Default everything to nil
|
37
45
|
default_options = {
|
38
46
|
:output => nil,
|
39
47
|
:preset => nil,
|
@@ -43,46 +51,45 @@ module PureMotion::Transcode
|
|
43
51
|
raise ArgumentError, "Invalid transcode parameters given" unless options.is_a?(Hash)
|
44
52
|
|
45
53
|
options = default_options.merge(options)
|
46
|
-
|
47
|
-
raise ArgumentError, "No output file given" if options[:output].nil?
|
48
54
|
|
49
|
-
if options[:input].is_a?(String) then
|
50
|
-
@input = PureMotion::Media.new(options[:input])
|
51
|
-
else
|
52
|
-
@input = options[:input]
|
53
|
-
end
|
54
55
|
@preset = options[:preset]
|
55
|
-
@output = options[:output]
|
56
|
-
|
57
|
-
validate
|
58
56
|
|
59
57
|
wire_events
|
60
58
|
|
59
|
+
@input = (@preset.input || options[:input])
|
60
|
+
validate_input
|
61
|
+
|
62
|
+
@output = File.expand_path(@preset.output || options[:output])
|
63
|
+
validate_output
|
64
|
+
|
65
|
+
@ffmpeg = PureMotion::Tools::FFmpeg.new(@preset.arguments)
|
66
|
+
|
61
67
|
if options[:log] then
|
62
68
|
@log = File.new(options[:log], 'w')
|
63
|
-
|
69
|
+
on(:output) do |line|
|
64
70
|
@log.puts line
|
65
71
|
end
|
66
|
-
|
72
|
+
@ffmpeg.on(:exit) do
|
67
73
|
@log.close
|
68
74
|
end
|
69
75
|
end
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
@ffmpeg = PureMotion::Tools::FFmpeg.new(:options => @preset.arguments)
|
74
|
-
|
75
|
-
@ffmpeg.line + lambda do |ffmpeg, line|
|
76
|
+
|
77
|
+
@ffmpeg.on(:output) do |line|
|
76
78
|
handle_output line
|
77
79
|
end
|
78
80
|
|
79
|
-
@ffmpeg.
|
80
|
-
|
81
|
+
@ffmpeg.on(:exit) do
|
82
|
+
handle_exit
|
81
83
|
end
|
82
|
-
|
84
|
+
|
85
|
+
end
|
86
|
+
|
87
|
+
def valid?
|
88
|
+
return @valid
|
83
89
|
end
|
84
90
|
|
85
91
|
def run
|
92
|
+
raise ArgumentError, "Transcode is invalid" unless valid?
|
86
93
|
@ffmpeg.run
|
87
94
|
end
|
88
95
|
|
@@ -95,9 +102,27 @@ module PureMotion::Transcode
|
|
95
102
|
end
|
96
103
|
|
97
104
|
private
|
98
|
-
|
105
|
+
|
106
|
+
def invalidate!
|
107
|
+
@valid = false
|
108
|
+
end
|
109
|
+
|
110
|
+
def handle_exit
|
111
|
+
if !File.exists?(@output) then
|
112
|
+
fire(:failure, :output_missing)
|
113
|
+
return
|
114
|
+
end
|
115
|
+
begin
|
116
|
+
output_media = Media(@output)
|
117
|
+
fire(:complete, output_media) if output_media.valid?
|
118
|
+
rescue PureMotion::UnsupportedFormat
|
119
|
+
fire(:failure, :transcode_unreadable)
|
120
|
+
return
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
99
124
|
def handle_output line
|
100
|
-
output line
|
125
|
+
fire(:output, line)
|
101
126
|
# /^frame=\s*(?<frame>\d*)\s*fps=\s*(?<fps>\d*)\s*q=(?<q>\S*)\s*size=\s*(?<size>[\d]*)(?<size_unit>[kmg]B)\s*time=\s*(?<time>[\d.]*)\s*bitrate=\s*(?<bitrate>[\d.]*)(?<bitrate_unit>[km]b).*$/
|
102
127
|
progress_line = /frame=\s*(\d*)\s*fps=\s*(\d*)\s*q=(\S*)\s*[L]?size=\s*([\d]*)([kmg]B)\s*time=\s*([\d.]*)\s*bitrate=\s*([\d.]*)([km]b)/
|
103
128
|
# The regex is documentated above and the matches it should give below
|
@@ -113,8 +138,7 @@ module PureMotion::Transcode
|
|
113
138
|
end
|
114
139
|
|
115
140
|
def handle_progress line
|
116
|
-
|
117
|
-
p = {
|
141
|
+
progress = {
|
118
142
|
:frame => line[1].to_i,
|
119
143
|
:fps => line[2].to_i,
|
120
144
|
:q => line[3].to_f,
|
@@ -123,17 +147,42 @@ module PureMotion::Transcode
|
|
123
147
|
:bitrate => line[7].to_f,
|
124
148
|
:percent => ((100.00 / @input.duration) * line[6].to_f).to_i
|
125
149
|
}
|
126
|
-
|
127
|
-
progress
|
150
|
+
progress[:percent] = 100 if progress[:percent] > 100 unless progress[:percent].nil?
|
151
|
+
fire(:progress, self, progress)
|
128
152
|
end
|
129
153
|
|
130
|
-
def
|
131
|
-
|
154
|
+
def validate_input
|
155
|
+
if @input.is_a?(String) then
|
156
|
+
begin
|
157
|
+
@input = Media @input
|
158
|
+
rescue PureMotion::UnsupportedFormat
|
159
|
+
make_fail(:invalid_input, "Invalid input '#{@input}'")
|
160
|
+
end
|
161
|
+
end
|
162
|
+
if @input.is_a?(PureMotion::Media) then
|
163
|
+
make_fail(:invalid_input, "Invalid input '#{@input}'") unless @input.valid?
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def validate_output
|
168
|
+
make_fail(:invalid_output, "No output file given") if @output.nil?
|
169
|
+
# TODO : Check if output file exists and overwite not set
|
170
|
+
make_fail(:invalid_output, "Output file '#{@output}' not writable") unless File.writable?(File.dirname(@output))
|
171
|
+
end
|
172
|
+
|
173
|
+
def make_fail(reason=:unknown,message="Unknown reason")
|
174
|
+
#invalidate!
|
175
|
+
if @preset.event_handler(:failure).nil? then
|
176
|
+
raise ArgumentError, message
|
177
|
+
else
|
178
|
+
fire(:failure, reason, message)
|
179
|
+
end
|
132
180
|
end
|
133
181
|
|
134
182
|
def wire_events
|
135
|
-
|
136
|
-
|
183
|
+
@preset.event_handlers.each_pair do |event, handler|
|
184
|
+
on(event) + handler
|
185
|
+
end
|
137
186
|
end
|
138
187
|
|
139
188
|
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/units/media_spec.rb
CHANGED
@@ -1,13 +1,77 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
2
|
|
3
|
-
describe
|
3
|
+
describe "media" do
|
4
|
+
|
5
|
+
context "opening media" do
|
6
|
+
|
7
|
+
it "should read supported formats" do
|
8
|
+
lambda do
|
9
|
+
PureMotion::Media.new(sample_path)
|
10
|
+
end.should_not raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should handle file not found" do
|
14
|
+
lambda {
|
15
|
+
Media('a_file_that_does_not_exist')
|
16
|
+
}.should raise_exception(ArgumentError)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should handle invalid files" do
|
20
|
+
lambda {
|
21
|
+
Media(invalid_file)
|
22
|
+
}.should raise_exception(PureMotion::UnsupportedFormat)
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
context "reading media" do
|
28
|
+
|
29
|
+
before(:all) do
|
30
|
+
@media = Media(sample_path)
|
31
|
+
end
|
32
|
+
|
33
|
+
it "can check for streams" do
|
34
|
+
@media.video?.should be_true and @media.audio?.should be_true
|
35
|
+
end
|
36
|
+
|
37
|
+
it "can read video resolution" do
|
38
|
+
@media.video.resolution.should == [320, 176]
|
39
|
+
end
|
40
|
+
|
41
|
+
it "can detect stream types" do
|
42
|
+
@media.video.type.should == :video and
|
43
|
+
@media.audio.type.should == :audio
|
44
|
+
end
|
45
|
+
|
46
|
+
it "can read the duration" do
|
47
|
+
@media.video.duration.should be_close(55, 3)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "can seek reasonably accurately" do
|
51
|
+
@media.video.seek 5
|
52
|
+
@media.video.position.should be_close(5, 1)
|
53
|
+
end
|
54
|
+
|
55
|
+
after(:all) do
|
56
|
+
@media = nil
|
57
|
+
end
|
4
58
|
|
5
|
-
it "should read supported formats" do
|
6
|
-
lambda do
|
7
|
-
PureMotion::Media.new(sample_path)
|
8
|
-
end.should_not raise_error
|
9
59
|
end
|
10
60
|
|
61
|
+
context "grabbing" do
|
11
62
|
|
63
|
+
before(:all) do
|
64
|
+
@media = Media(sample_path)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "can grab a video frame" do
|
68
|
+
@media.video.grab.should be_an_instance_of(PureMotion::Frame)
|
69
|
+
end
|
70
|
+
|
71
|
+
after(:all) do
|
72
|
+
@media = nil
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
12
76
|
|
13
77
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../spec_helper.rb'
|
2
|
+
|
3
|
+
describe PureMotion::Transcode::Transcode do
|
4
|
+
|
5
|
+
it "should detect missing files" do
|
6
|
+
lambda {
|
7
|
+
|
8
|
+
Transcode '/gibberish/path' do
|
9
|
+
|
10
|
+
end
|
11
|
+
|
12
|
+
}.should raise_error ArgumentError
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 1
|
8
|
-
-
|
9
|
-
version: 0.1.
|
8
|
+
- 1
|
9
|
+
version: 0.1.1
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Ominiom
|
@@ -14,23 +14,24 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-03
|
17
|
+
date: 2010-04-03 00:00:00 +01:00
|
18
18
|
default_executable:
|
19
19
|
dependencies: []
|
20
20
|
|
21
21
|
description: A Ruby wrapper for FFmpeg
|
22
22
|
email: iain@ominiom.com
|
23
|
-
executables:
|
24
|
-
|
23
|
+
executables:
|
24
|
+
- default.yaml
|
25
|
+
- pmts
|
25
26
|
extensions:
|
26
27
|
- ext/puremotion/extconf.rb
|
27
28
|
extra_rdoc_files:
|
28
29
|
- LICENSE
|
29
|
-
- README.
|
30
|
+
- README.markdown
|
30
31
|
files:
|
31
32
|
- .yardopts
|
32
33
|
- LICENSE
|
33
|
-
- README.
|
34
|
+
- README.markdown
|
34
35
|
- Rakefile
|
35
36
|
- ext/puremotion/audio.c
|
36
37
|
- ext/puremotion/extconf.rb
|
@@ -54,8 +55,13 @@ files:
|
|
54
55
|
- lib/preset/video/crop.rb
|
55
56
|
- lib/preset/video/pad.rb
|
56
57
|
- lib/preset/video/video.rb
|
58
|
+
- lib/process.rb
|
57
59
|
- lib/puremotion.rb
|
58
60
|
- lib/puremotion_native.so
|
61
|
+
- lib/server/app.rb
|
62
|
+
- lib/server/database.rb
|
63
|
+
- lib/server/server.rb
|
64
|
+
- lib/settings.rb
|
59
65
|
- lib/threading.rb
|
60
66
|
- lib/tools/ffmpeg.rb
|
61
67
|
- lib/transcode/transcode.rb
|
@@ -91,7 +97,10 @@ specification_version: 3
|
|
91
97
|
summary: PureMotion
|
92
98
|
test_files:
|
93
99
|
- spec/units/media_spec.rb
|
100
|
+
- spec/units/transcode_spec.rb
|
94
101
|
- spec/units/preset_spec.rb
|
95
102
|
- spec/spec_helper.rb
|
96
103
|
- examples/progress_reporting.rb
|
104
|
+
- examples/failure_handling.rb
|
97
105
|
- examples/simple.rb
|
106
|
+
- examples/process.rb
|