marcandre-flvedit 0.6.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.
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