marcandre-flvedit 0.6.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.rdoc +5 -0
- data/LICENSE +24 -0
- data/README.rdoc +90 -0
- data/Rakefile +131 -0
- data/VERSION.yml +4 -0
- data/bin/flvedit +14 -0
- data/lib/flv.rb +24 -0
- data/lib/flv/audio.rb +66 -0
- data/lib/flv/base.rb +38 -0
- data/lib/flv/body.rb +57 -0
- data/lib/flv/edit.rb +20 -0
- data/lib/flv/edit/options.rb +162 -0
- data/lib/flv/edit/processor.rb +3 -0
- data/lib/flv/edit/processor/add.rb +67 -0
- data/lib/flv/edit/processor/base.rb +209 -0
- data/lib/flv/edit/processor/command_line.rb +23 -0
- data/lib/flv/edit/processor/cut.rb +27 -0
- data/lib/flv/edit/processor/debug.rb +30 -0
- data/lib/flv/edit/processor/head.rb +16 -0
- data/lib/flv/edit/processor/join.rb +52 -0
- data/lib/flv/edit/processor/meta_data_maker.rb +127 -0
- data/lib/flv/edit/processor/print.rb +13 -0
- data/lib/flv/edit/processor/printer.rb +27 -0
- data/lib/flv/edit/processor/reader.rb +30 -0
- data/lib/flv/edit/processor/save.rb +28 -0
- data/lib/flv/edit/processor/update.rb +27 -0
- data/lib/flv/edit/runner.rb +23 -0
- data/lib/flv/edit/version.rb +15 -0
- data/lib/flv/event.rb +40 -0
- data/lib/flv/file.rb +41 -0
- data/lib/flv/header.rb +37 -0
- data/lib/flv/packing.rb +140 -0
- data/lib/flv/tag.rb +62 -0
- data/lib/flv/timestamp.rb +124 -0
- data/lib/flv/util/double_check.rb +22 -0
- data/lib/flv/video.rb +73 -0
- data/test/fixtures/corrupted.flv +0 -0
- data/test/fixtures/short.flv +0 -0
- data/test/fixtures/tags.xml +39 -0
- data/test/test_flv.rb +145 -0
- data/test/test_flv_edit.rb +32 -0
- data/test/test_flv_edit_results.rb +27 -0
- data/test/test_helper.rb +9 -0
- data/test/text_flv_edit_results/add_tags.txt +132 -0
- data/test/text_flv_edit_results/cut_from.txt +114 -0
- data/test/text_flv_edit_results/cut_key.txt +20 -0
- data/test/text_flv_edit_results/debug.txt +132 -0
- data/test/text_flv_edit_results/debug_limited.txt +18 -0
- data/test/text_flv_edit_results/debug_range.txt +32 -0
- data/test/text_flv_edit_results/join.txt +237 -0
- data/test/text_flv_edit_results/print.txt +16 -0
- data/test/text_flv_edit_results/stop.txt +38 -0
- data/test/text_flv_edit_results/update.txt +33 -0
- metadata +134 -0
data/lib/flv/edit.rb
ADDED
@@ -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,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
|