webvtt-ruby 0.3.2 → 0.4.2
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.
- checksums.yaml +5 -5
- data/README.md +3 -3
- data/lib/webvtt/parser.rb +50 -26
- data/lib/webvtt/segmenter.rb +22 -1
- data/tests/parser.rb +47 -1
- data/tests/segmenter.rb +1 -1
- data/tests/subtitles/test_carriage_returns.vtt +1 -0
- 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 +9 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 378a27bdabf62c13b0446de29e0aefc7267ac38f67401f1bab668f1ea673edab
|
4
|
+
data.tar.gz: 9b68d17e5435a1ef088bc06ec1c5346cc3526dc3af5b56578c14afca6c922656
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5b7e76f6306f2df4fcc2f45a194583eb288b4822f2f414723fb6b1675023adbc9ba088be5bbf6b0e6062ef13c3d649dcd0794ab5ff0bca4bbb87012f01c318e6
|
7
|
+
data.tar.gz: ea5a3ea7d52dc8f61bc1ee66fe495e1721f11c4430a1eda17a73b54fd279758eb861b716e2fda1ad5a2e1154400dde4694f83aefe8669995fee3cb0574c528d2
|
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)
|
data/lib/webvtt/parser.rb
CHANGED
@@ -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?
|
@@ -81,6 +80,29 @@ 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
|
|
@@ -146,10 +168,12 @@ module WebVTT
|
|
146
168
|
return
|
147
169
|
end
|
148
170
|
|
149
|
-
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})(.*)/)
|
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})(.*)/)
|
150
172
|
@start = Timestamp.new $1
|
151
|
-
@end = Timestamp.new $
|
152
|
-
@style = Hash[$
|
173
|
+
@end = Timestamp.new $3
|
174
|
+
@style = Hash[$5.strip.split(" ").map{|s| s.split(":").map(&:strip) }]
|
175
|
+
else
|
176
|
+
raise WebVTT::MalformedFile
|
153
177
|
end
|
154
178
|
@text = lines[1..-1].join("\n")
|
155
179
|
end
|
data/lib/webvtt/segmenter.rb
CHANGED
@@ -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/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,7 +186,7 @@ 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
191
|
cue = WebVTT::Cue.parse <<-CUE
|
160
192
|
00:00:01.000 --> 00:00:25.432
|
@@ -210,4 +242,18 @@ The text should change)
|
|
210
242
|
webvtt = WebVTT.convert_from_srt("tests/subtitles/invalid_cue.srt")
|
211
243
|
assert_equal 1, webvtt.cues.size
|
212
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
|
+
|
254
|
+
def test_invalid_vtt_without_milliseconds
|
255
|
+
assert_raises WebVTT::MalformedFile do
|
256
|
+
vtt = WebVTT::File.new('tests/subtitles/no_milliseconds.vtt')
|
257
|
+
end
|
258
|
+
end
|
213
259
|
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.2'
|
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.2
|
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: 2024-05-17 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).
|
@@ -36,8 +36,14 @@ files:
|
|
36
36
|
- tests/subtitles/no_text.vtt
|
37
37
|
- tests/subtitles/notvalid.vtt
|
38
38
|
- tests/subtitles/test.vtt
|
39
|
+
- tests/subtitles/test_carriage_returns.vtt
|
39
40
|
- tests/subtitles/test_from_srt.srt
|
40
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
|
41
47
|
- tests/subtitles/withnote.vtt
|
42
48
|
- webvtt-ruby.gemspec
|
43
49
|
homepage: https://github.com/HeyWatch/webvtt-ruby
|
@@ -59,8 +65,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
65
|
- !ruby/object:Gem::Version
|
60
66
|
version: '0'
|
61
67
|
requirements: []
|
62
|
-
|
63
|
-
rubygems_version: 2.4.3
|
68
|
+
rubygems_version: 3.1.4
|
64
69
|
signing_key:
|
65
70
|
specification_version: 4
|
66
71
|
summary: WebVTT parser and segmenter in ruby
|