marcandre-flvedit 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. data/CHANGELOG.rdoc +5 -0
  2. data/LICENSE +24 -0
  3. data/README.rdoc +90 -0
  4. data/Rakefile +131 -0
  5. data/VERSION.yml +4 -0
  6. data/bin/flvedit +14 -0
  7. data/lib/flv.rb +24 -0
  8. data/lib/flv/audio.rb +66 -0
  9. data/lib/flv/base.rb +38 -0
  10. data/lib/flv/body.rb +57 -0
  11. data/lib/flv/edit.rb +20 -0
  12. data/lib/flv/edit/options.rb +162 -0
  13. data/lib/flv/edit/processor.rb +3 -0
  14. data/lib/flv/edit/processor/add.rb +67 -0
  15. data/lib/flv/edit/processor/base.rb +209 -0
  16. data/lib/flv/edit/processor/command_line.rb +23 -0
  17. data/lib/flv/edit/processor/cut.rb +27 -0
  18. data/lib/flv/edit/processor/debug.rb +30 -0
  19. data/lib/flv/edit/processor/head.rb +16 -0
  20. data/lib/flv/edit/processor/join.rb +52 -0
  21. data/lib/flv/edit/processor/meta_data_maker.rb +127 -0
  22. data/lib/flv/edit/processor/print.rb +13 -0
  23. data/lib/flv/edit/processor/printer.rb +27 -0
  24. data/lib/flv/edit/processor/reader.rb +30 -0
  25. data/lib/flv/edit/processor/save.rb +28 -0
  26. data/lib/flv/edit/processor/update.rb +27 -0
  27. data/lib/flv/edit/runner.rb +23 -0
  28. data/lib/flv/edit/version.rb +15 -0
  29. data/lib/flv/event.rb +40 -0
  30. data/lib/flv/file.rb +41 -0
  31. data/lib/flv/header.rb +37 -0
  32. data/lib/flv/packing.rb +140 -0
  33. data/lib/flv/tag.rb +62 -0
  34. data/lib/flv/timestamp.rb +124 -0
  35. data/lib/flv/util/double_check.rb +22 -0
  36. data/lib/flv/video.rb +73 -0
  37. data/test/fixtures/corrupted.flv +0 -0
  38. data/test/fixtures/short.flv +0 -0
  39. data/test/fixtures/tags.xml +39 -0
  40. data/test/test_flv.rb +145 -0
  41. data/test/test_flv_edit.rb +32 -0
  42. data/test/test_flv_edit_results.rb +27 -0
  43. data/test/test_helper.rb +9 -0
  44. data/test/text_flv_edit_results/add_tags.txt +132 -0
  45. data/test/text_flv_edit_results/cut_from.txt +114 -0
  46. data/test/text_flv_edit_results/cut_key.txt +20 -0
  47. data/test/text_flv_edit_results/debug.txt +132 -0
  48. data/test/text_flv_edit_results/debug_limited.txt +18 -0
  49. data/test/text_flv_edit_results/debug_range.txt +32 -0
  50. data/test/text_flv_edit_results/join.txt +237 -0
  51. data/test/text_flv_edit_results/print.txt +16 -0
  52. data/test/text_flv_edit_results/stop.txt +38 -0
  53. data/test/text_flv_edit_results/update.txt +33 -0
  54. metadata +134 -0
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'backports'
3
+ require_relative '../flv'
4
+ require_relative 'edit/version'
5
+ require_relative 'edit/options'
6
+ require_relative 'edit/processor'
7
+ require_relative 'edit/runner'
8
+
9
+ #todo Change bin/flvedit
10
+ #todo Auto write to files
11
+ #todo Command Save no|false
12
+ #todo add & nearest keyframe & overwrite
13
+ #todo fix timestamps
14
+ #todo cut & nearest keyframe?
15
+ #todo print & xml/yaml & multiple files
16
+ #todo onLastSecond
17
+ #todo in|out pipe
18
+ #todo recursive?
19
+ #todo fix offset, both 24bit issue & apparent skips
20
+ #todo bug join & time for audio vs video
@@ -0,0 +1,162 @@
1
+ # encoding: utf-8
2
+ require 'optparse'
3
+
4
+ module FLV
5
+ module Edit
6
+ class Options
7
+ attr_reader :commands, :options
8
+ def initialize(argv)
9
+ @commands, @options = parse(argv)
10
+ end
11
+
12
+ def to_a
13
+ [@commands, @options]
14
+ end
15
+
16
+ private
17
+ def parse(argv)
18
+ commands = []
19
+ options = {}
20
+ parser = OptionParser.new do |parser|
21
+ parser.banner = banner
22
+ parser.separator ""
23
+ parser.separator "Commands:"
24
+ Processor::Base.registry.sort_by(&:to_s).
25
+ each do |command, desc, cmd_opt|
26
+ name = command.name.split('::').last.downcase
27
+ option = name.downcase.to_sym
28
+ shortcut = cmd_opt[:shortcut] || name[0..0]
29
+ if param = cmd_opt[:param]
30
+ name << " " << (param[:name] || param[:class].to_s.upcase)
31
+ desc = [param[:class], *desc] if param[:class]
32
+ end
33
+ parser.on("-#{shortcut}", "--#{name}", *desc) do |v|
34
+ options[option] = v
35
+ commands << command
36
+ end
37
+ end
38
+
39
+ parser.separator ""
40
+ parser.separator "Switches:"
41
+ [
42
+ [:keyframe_mode, "Keyframe mode slides on_cue_point(navigation) tags added by the",
43
+ "add command to nearest keyframe position"]
44
+ ].each do |switch, *desc|
45
+ shortcut = desc.first.is_a?(Symbol) ? desc.shift.to_s : switch.to_s[0..0]
46
+ full = desc.first.is_a?(Class) ? "--#{switch.to_s} N" : "--#{switch.to_s}"
47
+ parser.on("-#{shortcut}", full, *desc) { |v| options[switch] = v }
48
+ end
49
+
50
+ parser.separator "Common:"
51
+ parser.on("-h", "--help", "Show this message") {}
52
+ parser.on("--version", "Show version") do
53
+ puts "Current version: #{FLV::Edit.version}"
54
+ exit
55
+ end
56
+
57
+ parser.separator ""
58
+ parser.separator "flvedit (#{FLV::Edit.version}), copyright (c) 2009 Marc-André Lafortune"
59
+ parser.separator "This program is published under the BSD license."
60
+ end
61
+ options[:files] = parser.parse!(argv)
62
+ if commands.empty?
63
+ puts parser
64
+ exit
65
+ end
66
+ return commands, options
67
+ end
68
+
69
+ def banner
70
+ <<-EOS
71
+ Usage: flvedit#{(RUBY_PLATFORM =~ /win32/) ? '.exe' : ''} [--input] files --processing --more_processing ... [--save [PATH]]
72
+ ******
73
+ *NOTE*: THIS IS NOT A STABLE RELEASE. API WILL CHANGE!
74
+ ******
75
+ flvedit will apply the given processing to the input files.
76
+
77
+ Examples:
78
+ # Printout of the first 42 tags:
79
+ flvedit example.flv --debug 42
80
+
81
+ # Extract the first 2 seconds of a.flv and b.flv, join them and update the meta data.
82
+ flvedit a.flv b.flv --cut 0-2 --join --update --save out.flv
83
+
84
+ Option formats:
85
+ TIMESTAMP: Given in seconds with decimals for fractions of seconds.
86
+ Use 'h' and 'm' or ':' for hours and minutes separators.
87
+ Examples: 1:00:01 (for 1 hour and 1 second), 1h1 (same),
88
+ 2m3.004 (2 minutes, 3 seconds and 4 milliseconds), 123.004 (same)
89
+ RANGE: Expressed as BEGIN-END, where BEGIN or END are timestamps
90
+ that can be let empty for no limit
91
+ Examples: 1m-2m (second minute), 1m- (all but first minute), -1m (first minute)
92
+ RANGE/TS: RANGE or TIMESTAMP.
93
+ EOS
94
+ #s.split("\n").collect!(&:strip).join("\n")
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ #todo: explicit Input command
101
+ #todo: conditions for implicit Save
102
+
103
+ # options[:metadatacreator] = "inlet media FLVTool2 v#{PROGRAMM_VERSION.join('.')} - http://www.inlet-media.de/flvtool2"
104
+ # options[:metadata] = {}
105
+
106
+ # when /^-([a-zA-Z0-9]+?):(.+)$/
107
+ # options[:metadata][$1] = $2
108
+ # when /^-([a-zA-Z0-9]+?)#([0-9]+)$/
109
+ # options[:metadata][$1] = $2.to_f
110
+ # when /^-([a-zA-Z0-9]+?)@(\d{4,})-(\d{2,})-(\d{2,}) (\d{2,}):(\d{2,}):(\d{2,})$/
111
+ # options[:metadata][$1] = Time.local($2, $3, $4, $5, $6, $7)
112
+ # when /^([^-].*)$/
113
+ # if options[:in_path].nil?
114
+ # options[:in_path] = $1
115
+ # if options[:in_path].downcase =~ /stdin|pipe/
116
+ # options[:in_pipe] = true
117
+ # options[:in_path] = 'pipe'
118
+ # else
119
+ # options[:in_path] = File.expand_path( options[:in_path] )
120
+ # end
121
+ # else
122
+ # options[:out_path] = $1
123
+ # if options[:out_path].downcase =~ /stdout|pipe/
124
+ # options[:out_pipe] = true
125
+ # options[:out_path] = 'pipe'
126
+ # else
127
+ # options[:out_path] = File.expand_path( options[:out_path] )
128
+ # end
129
+ # end
130
+ # end
131
+ # end
132
+
133
+
134
+ # def self.validate_options( options )
135
+ # if options[:commands].empty?
136
+ # show_usage
137
+ # exit 0
138
+ # end
139
+ #
140
+ # options[:commands].each do |command|
141
+ # case command
142
+ # when :print
143
+ # if options[:out_pipe]
144
+ # throw_error "Could not use print command in conjunction with output piping or redirection"
145
+ # exit 1
146
+ # end
147
+ # when :debug
148
+ # if options[:out_pipe]
149
+ # throw_error "Could not use debug command in conjunction with output piping or redirection"
150
+ # exit 1
151
+ # end
152
+ # when :help
153
+ # show_usage
154
+ # exit 0
155
+ # when :version
156
+ # show_version
157
+ # exit 0
158
+ # end
159
+ # end
160
+ # options
161
+ # end
162
+ #
@@ -0,0 +1,3 @@
1
+ %w(base add command_line cut debug head join print reader save update).each do |proc|
2
+ require_relative "processor/#{proc}"
3
+ end
@@ -0,0 +1,67 @@
1
+ require 'yaml'
2
+ require 'rexml/document'
3
+ require 'optparse'
4
+
5
+ module FLV
6
+ module Edit
7
+ module Processor
8
+
9
+ class Add < Base
10
+ desc "Adds tags from the xml or yaml file PATH (default: tags.yaml/xml)", :param => {:name => "[PATH]"}
11
+
12
+ def initialize(*)
13
+ super
14
+ @add = (options[:add_tags] || read_tag_file).sort_by(&:timestamp)
15
+ end
16
+
17
+ def on_tag(tag)
18
+ insert_before tag, @add
19
+ end
20
+
21
+ # def on_keyframe(tag)
22
+ # insert_before tag, @add_before_key_frame
23
+ # end
24
+
25
+ private
26
+ def insert_before(tag, list_to_insert)
27
+ stop_at = list_to_insert.find_index{|i| i.timestamp > tag.timestamp} || list_to_insert.length
28
+ dispatch_instead(*(list_to_insert.slice!(0...stop_at) << tag)) if stop_at > 0
29
+ end
30
+
31
+ DEFAULT = ["tags.yaml", "tags.xml"]
32
+ #todo: overwrite, navigation
33
+ #todo: default event
34
+ def read_tag_file
35
+ filename = options[:add]
36
+ DEFAULT.each{|fn| filename ||= fn if ::File.exists?(fn)}
37
+ raise "You must either specify a tag file or have a file named #{DEFAULT.join(' or ')}" unless filename
38
+ ::File.open(filename) {|f| read_tags(f)}
39
+ end
40
+
41
+ def read_tags(file)
42
+ tags =
43
+ unless file.path.downcase.end_with? ".xml"
44
+ YAML.load(file)
45
+ else
46
+ xml = REXML::Document.new(file, :ignore_whitespace_nodes => :all)
47
+ xml.root.elements.to_a("metatag").map do |tag|
48
+ {
49
+ :event => tag.attributes['event'],
50
+ :overwrite => tag.attributes['overwrite'],
51
+ :timestamp => (tag.elements['timestamp'].text rescue 0),
52
+ :navigation => (tag.elements['type'].text == "navigation" rescue false),
53
+ :arguments => Hash[tag.elements['parameters'].map{|param| [param.name, param.text]}]
54
+ }
55
+ end
56
+ end
57
+ tags = [tags] unless tags.is_a? Array
58
+ tags.map do |info|
59
+ info.symbolize_keys!
60
+ Tag.new(info[:timestamp], Event.new(info[:event], info[:arguments]))
61
+ end
62
+ end
63
+ end
64
+
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,209 @@
1
+ module FLV
2
+ module Edit
3
+ module Processor
4
+ # Processors are used to process FLV files. Processors form are chain and the data (header and tags) will 'flow'
5
+ # through the chain in sequence. Each processor can inspect the data and change the flow,
6
+ # either by modifying the data, inserting new data in the flow or stopping the propagation of data.
7
+ #
8
+ # A FLV file can be seen as a sequence of chunks, the first one being a Header and the following ones
9
+ # a series of Tags with different types of bodies: Audio, Video or Event. Events store all meta data
10
+ # related information: onMetaData, onCuePoint, ...
11
+ #
12
+ # To tap into the flow of chunks, a processor can define any of the following methods:
13
+ # on_chunk
14
+ # on_header
15
+ # on_tag
16
+ # on_audio
17
+ # on_video
18
+ # on_keyframe
19
+ # on_interframe
20
+ # on_disposable_interframe
21
+ # on_event
22
+ # on_meta_data
23
+ # on_cue_point
24
+ # on_other_event
25
+ #
26
+ # All of these methods will have one argument: the current chunk being processed.
27
+ # For example, if the current chunk is an 'onMetaData' event, then
28
+ # the following will be called (from the most specialized to the least).
29
+ # processor.on_meta_data(chunk)
30
+ # processor.on_event(chunk)
31
+ # processor.on_chunk(chunk)
32
+ # # later on, the next processor will handle it:
33
+ # next_processor.on_meta_data(chunk)
34
+ # #...
35
+ #
36
+ # The methods need not return anything. It is assumed that the chunk will continue to flow through the
37
+ # processing chain. When the chunk should not continue down the chain, call +absorb+.
38
+ # To insert other tags in the flow, call +dispatch_instead+.
39
+ # Finally, it's possible to +stop+ the processing of the file completely.
40
+ #
41
+ # It is possible to look back at already processed chunks (up to a certain limit) with +look_back+
42
+ # or even in the future with +look_ahead+
43
+ #
44
+ class Base
45
+ attr_reader :options
46
+
47
+ def initialize(source=nil, options={})
48
+ @options = options.freeze
49
+ @source = source
50
+ on_calls = self.class.instance_methods(false).select{|m| m.to_s.start_with?("on_")}.map(&:to_sym) #Note: to_s needed for ruby 1.9, to_sym for ruby 1.8
51
+ unless (potential_errors = on_calls - ALL_EVENTS).empty?
52
+ warn "The following are not events: #{potential_errors.join(',')} (class #{self.class})"
53
+ end
54
+ end
55
+
56
+ def has_next_file?
57
+ @source.has_next_file?
58
+ end
59
+
60
+ def rewind
61
+ @source.rewind
62
+ end
63
+
64
+ def absorb(*) # Note: the (*) is so that we can alias events like on_meta_data
65
+ throw :absorb
66
+ end
67
+
68
+ def stop
69
+ throw :stop
70
+ end
71
+
72
+ def process_all
73
+ each{} while has_next_file?
74
+ end
75
+
76
+ def each(&block)
77
+ return to_enum(:each_chunk) unless block_given?
78
+ @block = block
79
+ catch :stop do
80
+ process_next_file
81
+ end
82
+ end
83
+
84
+ def process_next_file
85
+ dispatch_chunks(@source)
86
+ end
87
+
88
+ def dispatch_chunks(enum)
89
+ enum.each do |chunk|
90
+ dispatch_chunk(chunk)
91
+ end
92
+ end
93
+
94
+ def dispatch_chunk(chunk)
95
+ evt = chunk.main_event
96
+ catch :absorb do
97
+ EVENT_TRIGGER_LIST[evt].each do |event|
98
+ send(event, chunk) if respond_to?(event)
99
+ end
100
+ @block.call chunk
101
+ end
102
+ end
103
+
104
+ def dispatch_instead(*chunks)
105
+ chunks.each do |chunk|
106
+ @block.call chunk
107
+ end
108
+ absorb
109
+ end
110
+
111
+ def stdout
112
+ options[:out] || STDOUT
113
+ end
114
+
115
+ EVENT_TRIGGER = {
116
+ :on_header => :on_chunk,
117
+ :on_tag => :on_chunk,
118
+ :on_audio => :on_tag,
119
+ :on_video => :on_tag,
120
+ :on_event => :on_tag,
121
+ :on_meta_data => :on_event,
122
+ :on_cue_point => :on_event,
123
+ :on_last_second => :on_event,
124
+ :on_other_event => :on_event,
125
+ :on_keyframe => :on_video,
126
+ :on_interframe => :on_video,
127
+ :on_disposable_interframe => :on_video
128
+ }.freeze
129
+
130
+ ALL_EVENTS = (EVENT_TRIGGER.keys | EVENT_TRIGGER.values).freeze
131
+
132
+ MAIN_EVENTS = ALL_EVENTS.reject{ |k| EVENT_TRIGGER.has_value?(k)}.freeze
133
+
134
+ EVENT_TRIGGER_LIST = Hash.new{|h, k| h[k] = [k] + h[EVENT_TRIGGER[k]]}.tap{|h| h[nil] = []; h.values_at(*MAIN_EVENTS)}.freeze
135
+
136
+ protected
137
+
138
+ def self.desc(text, options = {})
139
+ registry << [self, text, options]
140
+ end
141
+
142
+ def self.registry # :nodoc:
143
+ @@registry ||= []
144
+ end
145
+
146
+ def self.absorb(*events)
147
+ events.each{|evt| alias_method evt, :absorb}
148
+ end
149
+
150
+ end #class Base
151
+
152
+ def self.chain(chain_classes, options = {})
153
+ next_chain_class = chain_classes.pop
154
+ next_chain_class.new(chain(chain_classes, options), options) if next_chain_class
155
+ end
156
+
157
+ # Let's extend the different kinds of chunks so that any_chunk.main_event returns
158
+ # the desired trigger.
159
+
160
+ module MainEvent # :nodoc:
161
+ module Header
162
+ def main_event
163
+ :on_header
164
+ end
165
+ end
166
+
167
+ module Video
168
+ MAPPING = {
169
+ :keyframe => :on_keyframe,
170
+ :interframe => :on_interframe,
171
+ :disposable_interframe => :on_disposable_interframe
172
+ }.freeze
173
+
174
+ def main_event
175
+ MAPPING[frame_type]
176
+ end
177
+ end
178
+
179
+ module Audio
180
+ def main_event
181
+ :on_audio
182
+ end
183
+ end
184
+
185
+ module Event
186
+ MAPPING = Hash.new(:on_other_event).merge!(
187
+ :onMetaData => :on_meta_data,
188
+ :onCuePoint => :on_cue_point,
189
+ :onLastSecond => :on_last_second
190
+ ).freeze
191
+ def main_event
192
+ MAPPING[event]
193
+ end
194
+ end
195
+
196
+ module Tag
197
+ def main_event
198
+ body.main_event
199
+ end
200
+ end
201
+ end #module MainEvent
202
+ end #module Processor
203
+ end #module Edit
204
+
205
+ [Header, Tag, Audio, Video, Event].each do |klass|
206
+ klass.class_eval{ include Edit::Processor::MainEvent.const_get(klass.to_s.sub('FLV::', '')) }
207
+ end
208
+
209
+ end #module FLV