webvtt-ruby 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/Gemfile.lock +1 -1
- data/README.md +3 -3
- data/lib/{parser.rb → webvtt/parser.rb} +61 -29
- data/lib/{segmenter.rb → webvtt/segmenter.rb} +22 -1
- data/lib/webvtt.rb +2 -2
- data/tests/parser.rb +58 -2
- data/tests/segmenter.rb +1 -1
- data/tests/subtitles/invalid_cue.srt +5 -0
- data/tests/subtitles/invalid_cue.vtt +6 -0
- data/tests/subtitles/test_carriage_returns.vtt +1 -0
- data/tests/subtitles/test_from_srt.vtt +1 -1
- data/tests/subtitles/test_mmss_format.vtt +9 -0
- data/tests/subtitles/test_multiple_line_separators.vtt +15 -0
- data/tests/subtitles/weird_format.srt +15 -0
- data/tests/subtitles/weird_format.vtt +17 -0
- data/tests/subtitles/weird_format_corrected.vtt +17 -0
- data/webvtt-ruby.gemspec +1 -1
- metadata +13 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f4334c8feffaab4ed2438a52554ce17ebd2bd0152622adbd8445c7de99e6e14d
|
4
|
+
data.tar.gz: 6de8052ac1d531bb77630c086425c3f2640295d510cd87546d357a2791d06695
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7cba26291e3f92b889ff2d03246873d44f07c12890dec24201bd4e13e61b286a0a2af4c22e207c37ff976d3056cd38381bd470be9894bf3ebccd3043fdfac0ab
|
7
|
+
data.tar.gz: 7350a1b409e9178eea25526bb0fb2d7167b7348fb4fc19bc96a3edcf353f6aa4ae3c86f3ea31ac988925604da6def8ce621f636d73b66dd02656c0d945fb13a7
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -116,6 +116,6 @@ Usage: bin/webvtt-segmenter [--arg]
|
|
116
116
|
|
117
117
|
**Bruno Celeste**
|
118
118
|
|
119
|
-
* http://
|
120
|
-
* bruno@
|
121
|
-
* [@
|
119
|
+
* http://coconut.co
|
120
|
+
* bruno@coconut.co
|
121
|
+
* [@brunoceleste](http://twitter.com/brunoceleste)
|
@@ -4,14 +4,20 @@ module WebVTT
|
|
4
4
|
File.new(file)
|
5
5
|
end
|
6
6
|
|
7
|
+
def self.from_blob(content)
|
8
|
+
Blob.new(content)
|
9
|
+
end
|
10
|
+
|
7
11
|
def self.convert_from_srt(srt_file, output=nil)
|
8
|
-
if !::File.
|
12
|
+
if !::File.exist?(srt_file)
|
9
13
|
raise InputError, "SRT file not found"
|
10
14
|
end
|
11
15
|
|
12
16
|
srt = ::File.read(srt_file)
|
13
17
|
output ||= srt_file.gsub(".srt", ".vtt")
|
14
18
|
|
19
|
+
# normalize timestamps in srt
|
20
|
+
srt.gsub!(/(:|^)(\d)(,|:)/, '\10\2\3')
|
15
21
|
# convert timestamps and save the file
|
16
22
|
srt.gsub!(/([0-9]{2}:[0-9]{2}:[0-9]{2})([,])([0-9]{3})/, '\1.\3')
|
17
23
|
# normalize new line character
|
@@ -23,19 +29,20 @@ module WebVTT
|
|
23
29
|
return File.new(output)
|
24
30
|
end
|
25
31
|
|
26
|
-
class
|
27
|
-
attr_reader :header
|
32
|
+
class Blob
|
33
|
+
attr_reader :header
|
28
34
|
attr_accessor :cues
|
29
35
|
|
30
|
-
def initialize(
|
31
|
-
|
32
|
-
raise InputError, "WebVTT file not found"
|
33
|
-
end
|
36
|
+
def initialize(content = nil)
|
37
|
+
@cues = []
|
34
38
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
+
if content
|
40
|
+
parse(
|
41
|
+
content.gsub("\r\n", "\n").gsub("\r","\n") # normalizing new line character
|
42
|
+
)
|
43
|
+
else
|
44
|
+
@header = 'WEBVTT'
|
45
|
+
end
|
39
46
|
end
|
40
47
|
|
41
48
|
def to_webvtt
|
@@ -50,20 +57,12 @@ module WebVTT
|
|
50
57
|
@cues.last.end_in_sec - @cues.first.start_in_sec
|
51
58
|
end
|
52
59
|
|
53
|
-
def
|
54
|
-
output ||= @path.gsub(".srt", ".vtt")
|
55
|
-
|
56
|
-
::File.open(output, "w") do |f|
|
57
|
-
f.write(to_webvtt)
|
58
|
-
end
|
59
|
-
return output
|
60
|
-
end
|
61
|
-
|
62
|
-
def parse
|
60
|
+
def parse(content)
|
63
61
|
# remove bom first
|
64
|
-
|
62
|
+
content.gsub!("\uFEFF", '')
|
63
|
+
|
64
|
+
cues = content.split(/\n\n+/)
|
65
65
|
|
66
|
-
cues = @content.split("\n\n")
|
67
66
|
@header = cues.shift
|
68
67
|
header_lines = @header.split("\n").map(&:strip)
|
69
68
|
if (header_lines[0] =~ /^WEBVTT/).nil?
|
@@ -72,7 +71,7 @@ module WebVTT
|
|
72
71
|
|
73
72
|
@cues = []
|
74
73
|
cues.each do |cue|
|
75
|
-
cue_parsed = Cue.
|
74
|
+
cue_parsed = Cue.parse(cue.strip)
|
76
75
|
if !cue_parsed.text.nil?
|
77
76
|
@cues << cue_parsed
|
78
77
|
end
|
@@ -81,12 +80,41 @@ module WebVTT
|
|
81
80
|
end
|
82
81
|
end
|
83
82
|
|
83
|
+
class File < Blob
|
84
|
+
attr_reader :path, :filename
|
85
|
+
|
86
|
+
def initialize(webvtt_file)
|
87
|
+
if !::File.exist?(webvtt_file)
|
88
|
+
raise InputError, "WebVTT file not found"
|
89
|
+
end
|
90
|
+
|
91
|
+
@path = webvtt_file
|
92
|
+
@filename = ::File.basename(@path)
|
93
|
+
super(::File.read(webvtt_file))
|
94
|
+
end
|
95
|
+
|
96
|
+
def save(output=nil)
|
97
|
+
output ||= @path.gsub(".srt", ".vtt")
|
98
|
+
|
99
|
+
::File.open(output, "w") do |f|
|
100
|
+
f.write(to_webvtt)
|
101
|
+
end
|
102
|
+
return output
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
84
106
|
class Cue
|
85
107
|
attr_accessor :identifier, :start, :end, :style, :text
|
86
108
|
|
87
|
-
def initialize(cue)
|
109
|
+
def initialize(cue = nil)
|
88
110
|
@content = cue
|
89
|
-
|
111
|
+
@style = {}
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.parse(cue)
|
115
|
+
cue = Cue.new(cue)
|
116
|
+
cue.parse
|
117
|
+
return cue
|
90
118
|
end
|
91
119
|
|
92
120
|
def to_webvtt
|
@@ -136,10 +164,14 @@ module WebVTT
|
|
136
164
|
lines.shift
|
137
165
|
end
|
138
166
|
|
139
|
-
if lines
|
167
|
+
if lines.empty?
|
168
|
+
return
|
169
|
+
end
|
170
|
+
|
171
|
+
if lines[0].match(/(([0-9]{2}:)?[0-9]{2}:[0-9]{2}\.[0-9]{3}) -+> (([0-9]{2}:)?[0-9]{2}:[0-9]{2}\.[0-9]{3})(.*)/)
|
140
172
|
@start = Timestamp.new $1
|
141
|
-
@end = Timestamp.new $
|
142
|
-
@style = Hash[$
|
173
|
+
@end = Timestamp.new $3
|
174
|
+
@style = Hash[$5.strip.split(" ").map{|s| s.split(":").map(&:strip) }]
|
143
175
|
end
|
144
176
|
@text = lines[1..-1].join("\n")
|
145
177
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
1
3
|
module WebVTT
|
2
4
|
|
3
5
|
def self.segment(input, options={})
|
@@ -25,6 +27,25 @@ module WebVTT
|
|
25
27
|
@options[:length] ||= 10
|
26
28
|
@options[:output] ||= "fileSequence-%05d.vtt"
|
27
29
|
@options[:playlist] ||= "prog_index.m3u8"
|
30
|
+
|
31
|
+
# a dirty hack to check if output and playlist are ending with / (indicating a directory) and if so use the default filename and playlist name and create directory which is required.
|
32
|
+
|
33
|
+
if @options[:output].end_with?('/')
|
34
|
+
@options[:output] = "#{@options[:output]}fileSequence-%05d.vtt"
|
35
|
+
end
|
36
|
+
if @options[:playlist].end_with?('/')
|
37
|
+
@options[:playlist] = "#{@options[:playlist]}prog_index.m3u8"
|
38
|
+
end
|
39
|
+
|
40
|
+
output_dirname = ::File.dirname(@options[:output])
|
41
|
+
playlist_dirname = ::File.dirname(@options[:playlist])
|
42
|
+
|
43
|
+
unless ::File.directory?(output_dirname)
|
44
|
+
::FileUtils.mkdir_p(output_dirname)
|
45
|
+
end
|
46
|
+
unless ::File.directory?(playlist_dirname)
|
47
|
+
::FileUtils.mkdir_p(playlist_dirname)
|
48
|
+
end
|
28
49
|
end
|
29
50
|
|
30
51
|
def find_segment_files(cue)
|
@@ -123,4 +144,4 @@ module WebVTT
|
|
123
144
|
return filenames.map{|f| File.new(f) }
|
124
145
|
end
|
125
146
|
end
|
126
|
-
end
|
147
|
+
end
|
data/lib/webvtt.rb
CHANGED
data/tests/parser.rb
CHANGED
@@ -20,6 +20,12 @@ class ParserTest < Minitest::Test
|
|
20
20
|
}
|
21
21
|
end
|
22
22
|
|
23
|
+
def test_can_create_empty_webvtt
|
24
|
+
webvtt = WebVTT::Blob.new
|
25
|
+
assert_equal 'WEBVTT', webvtt.header
|
26
|
+
assert_equal [], webvtt.cues
|
27
|
+
end
|
28
|
+
|
23
29
|
def test_list_cues
|
24
30
|
webvtt = WebVTT.read("tests/subtitles/test.vtt")
|
25
31
|
assert_instance_of Array, webvtt.cues
|
@@ -43,6 +49,16 @@ class ParserTest < Minitest::Test
|
|
43
49
|
assert_equal "English subtitle 15 -Forced- (00:00:27.000)\nline:75%", cue.text
|
44
50
|
end
|
45
51
|
|
52
|
+
def test_cue_non_hours
|
53
|
+
webvtt = WebVTT.read("tests/subtitles/test_mmss_format.vtt")
|
54
|
+
cue = webvtt.cues[0]
|
55
|
+
assert_equal "00:00:29.000", cue.start.to_s
|
56
|
+
assert_equal "00:00:31.000", cue.end.to_s
|
57
|
+
assert_instance_of Hash, cue.style
|
58
|
+
assert_equal "75%", cue.style["line"]
|
59
|
+
assert_equal "English subtitle 15 -Forced- (00:00:27.000)\nline:75%", cue.text
|
60
|
+
end
|
61
|
+
|
46
62
|
def test_cue_identifier
|
47
63
|
webvtt = WebVTT.read("tests/subtitles/test.vtt")
|
48
64
|
cue = webvtt.cues[1]
|
@@ -54,6 +70,11 @@ class ParserTest < Minitest::Test
|
|
54
70
|
assert_equal "English subtitle 16 -Unforced- (00:00:31.000)\nalign:start line:0%", cue.text
|
55
71
|
end
|
56
72
|
|
73
|
+
def test_multiple_line_separators
|
74
|
+
webvtt = WebVTT.read("tests/subtitles/test_multiple_line_separators.vtt")
|
75
|
+
assert_equal 2, webvtt.cues.length
|
76
|
+
end
|
77
|
+
|
57
78
|
def test_ignore_if_note
|
58
79
|
webvtt = WebVTT.read("tests/subtitles/withnote.vtt")
|
59
80
|
assert_equal 3, webvtt.cues.size
|
@@ -129,6 +150,17 @@ The text should change)
|
|
129
150
|
assert_equal 2, webvtt.cues.size
|
130
151
|
end
|
131
152
|
|
153
|
+
def test_convert_weird_format_srt_to_webvtt
|
154
|
+
webvtt = WebVTT.convert_from_srt("tests/subtitles/weird_format.srt")
|
155
|
+
correct_webvtt = WebVTT.read("tests/subtitles/weird_format_corrected.vtt")
|
156
|
+
|
157
|
+
converted_cues = webvtt.cues.map { |cue| cue.start.to_s }
|
158
|
+
correct_cues = correct_webvtt.cues.map { |cue| cue.start.to_s }
|
159
|
+
|
160
|
+
assert_instance_of WebVTT::File, webvtt
|
161
|
+
assert_equal correct_cues, converted_cues
|
162
|
+
end
|
163
|
+
|
132
164
|
def test_parse_big_file
|
133
165
|
return
|
134
166
|
webvtt = WebVTT.read("tests/subtitles/big_srt.vtt")
|
@@ -154,9 +186,9 @@ The text should change)
|
|
154
186
|
assert_equal "00:09:02.373", webvtt.cues[1].end.to_s
|
155
187
|
assert_equal "", webvtt.cues[1].text
|
156
188
|
end
|
157
|
-
|
189
|
+
|
158
190
|
def test_cue_offset_by
|
159
|
-
cue = WebVTT::Cue.
|
191
|
+
cue = WebVTT::Cue.parse <<-CUE
|
160
192
|
00:00:01.000 --> 00:00:25.432
|
161
193
|
Test Cue
|
162
194
|
CUE
|
@@ -195,4 +227,28 @@ The text should change)
|
|
195
227
|
assert_equal "03:39:34.008", ts3.to_s
|
196
228
|
end
|
197
229
|
|
230
|
+
def test_build_cue
|
231
|
+
cue = WebVTT::Cue.new
|
232
|
+
cue.start = WebVTT::Timestamp.new 0
|
233
|
+
cue.end = WebVTT::Timestamp.new 12
|
234
|
+
cue.text = "Built from scratch"
|
235
|
+
output = ""
|
236
|
+
output << "00:00:00.000 --> 00:00:12.000\n"
|
237
|
+
output << "Built from scratch"
|
238
|
+
assert_equal output, cue.to_webvtt
|
239
|
+
end
|
240
|
+
|
241
|
+
def test_invalid_cue
|
242
|
+
webvtt = WebVTT.convert_from_srt("tests/subtitles/invalid_cue.srt")
|
243
|
+
assert_equal 1, webvtt.cues.size
|
244
|
+
end
|
245
|
+
|
246
|
+
def test_can_validate_webvtt_with_carriage_returns
|
247
|
+
webvtt = WebVTT::File.new("tests/subtitles/test_carriage_returns.vtt")
|
248
|
+
assert_instance_of Array, webvtt.cues
|
249
|
+
assert !webvtt.cues.empty?, "Cues should not be empty"
|
250
|
+
assert_instance_of WebVTT::Cue, webvtt.cues[0]
|
251
|
+
assert_equal 15, webvtt.cues.size
|
252
|
+
end
|
253
|
+
|
198
254
|
end
|
data/tests/segmenter.rb
CHANGED
@@ -43,7 +43,7 @@ class ParserTest < Minitest::Test
|
|
43
43
|
subs = segmenter.split_to_files
|
44
44
|
segmenter.generate_playlist(subs)
|
45
45
|
|
46
|
-
assert File.
|
46
|
+
assert File.exist?("test.m3u8")
|
47
47
|
# clean up
|
48
48
|
subs.each {|f| FileUtils.rm(f.filename)}
|
49
49
|
FileUtils.rm("test.m3u8")
|
@@ -0,0 +1 @@
|
|
1
|
+
WEBVTT
|
@@ -0,0 +1,15 @@
|
|
1
|
+
WEBVTT X-TIMESTAMP-MAP=MPEGTS:900000,LOCAL:00:00:00.000
|
2
|
+
|
3
|
+
|
4
|
+
|
5
|
+
|
6
|
+
00:00:29.000 --> 00:00:31.000 line:75%
|
7
|
+
English subtitle 15 -Forced- (00:00:27.000)
|
8
|
+
line:75%
|
9
|
+
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
00:00:31.000 --> 00:00:33.000 align:start line:0%
|
14
|
+
English subtitle 16 -Unforced- (00:00:31.000)
|
15
|
+
align:start line:0%
|
data/webvtt-ruby.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'webvtt-ruby'
|
3
|
-
s.version = '0.
|
3
|
+
s.version = '0.4.0'
|
4
4
|
s.summary = "WebVTT parser and segmenter in ruby"
|
5
5
|
s.description = "WebVTT parser and segmenter in ruby for HTML5 and HTTP Live Streaming (HLS)."
|
6
6
|
s.authors = ["Bruno Celeste"]
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: webvtt-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bruno Celeste
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-04-06 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: WebVTT parser and segmenter in ruby for HTML5 and HTTP Live Streaming
|
14
14
|
(HLS).
|
@@ -24,18 +24,26 @@ files:
|
|
24
24
|
- LICENSE
|
25
25
|
- README.md
|
26
26
|
- bin/webvtt-segmenter
|
27
|
-
- lib/parser.rb
|
28
|
-
- lib/segmenter.rb
|
29
27
|
- lib/webvtt.rb
|
28
|
+
- lib/webvtt/parser.rb
|
29
|
+
- lib/webvtt/segmenter.rb
|
30
30
|
- tests/parser.rb
|
31
31
|
- tests/segmenter.rb
|
32
32
|
- tests/subtitles/big_srt.srt
|
33
33
|
- tests/subtitles/big_srt.vtt
|
34
|
+
- tests/subtitles/invalid_cue.srt
|
35
|
+
- tests/subtitles/invalid_cue.vtt
|
34
36
|
- tests/subtitles/no_text.vtt
|
35
37
|
- tests/subtitles/notvalid.vtt
|
36
38
|
- tests/subtitles/test.vtt
|
39
|
+
- tests/subtitles/test_carriage_returns.vtt
|
37
40
|
- tests/subtitles/test_from_srt.srt
|
38
41
|
- tests/subtitles/test_from_srt.vtt
|
42
|
+
- tests/subtitles/test_mmss_format.vtt
|
43
|
+
- tests/subtitles/test_multiple_line_separators.vtt
|
44
|
+
- tests/subtitles/weird_format.srt
|
45
|
+
- tests/subtitles/weird_format.vtt
|
46
|
+
- tests/subtitles/weird_format_corrected.vtt
|
39
47
|
- tests/subtitles/withnote.vtt
|
40
48
|
- webvtt-ruby.gemspec
|
41
49
|
homepage: https://github.com/HeyWatch/webvtt-ruby
|
@@ -57,8 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
57
65
|
- !ruby/object:Gem::Version
|
58
66
|
version: '0'
|
59
67
|
requirements: []
|
60
|
-
|
61
|
-
rubygems_version: 2.4.3
|
68
|
+
rubygems_version: 3.1.4
|
62
69
|
signing_key:
|
63
70
|
specification_version: 4
|
64
71
|
summary: WebVTT parser and segmenter in ruby
|