puremotion 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|