livelist 0.2.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.
- checksums.yaml +7 -0
- data/lib/livelist/exceptions/invalid_format.rb +13 -0
- data/lib/livelist/hls/hls.rb +25 -0
- data/lib/livelist/hls/pipeline.rb +42 -0
- data/lib/livelist/playlist.rb +71 -0
- data/lib/livelist/segment.rb +60 -0
- data/lib/livelist/sequence.rb +38 -0
- data/lib/livelist/version.rb +5 -0
- data/lib/livelist.rb +2 -0
- metadata +55 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 71f726fd7f7b086ff553ddcbfee959ef777185bbeab869f86426b0dbeb6e59d4
|
|
4
|
+
data.tar.gz: 54c1894d723b52a28ea85f77f631f852497edccf210f025aefe492e5bc583256
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 324b514aacc3ebe288319c56c7d89bb1f2a648af3cf907b42ec68c4bed140c1816762e084f32ce7862fb4b2f1c3356a0a453b25a5320dac6ac3e7db4c63fb880
|
|
7
|
+
data.tar.gz: ea1188459a10e9503edfb62995341ee399d29a264111c5f297cc781ee3125aba3d04c380a1c60fe5e30a52a77bcc86349de7daf18274c574248fb7e2b6497eb8
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Livelist
|
|
4
|
+
module Exceptions
|
|
5
|
+
# Livelist::Exceptions::InvalidFormat
|
|
6
|
+
# Throws an error if file has invalid format (if not .m3u8)
|
|
7
|
+
class InvalidFormat < StandardError
|
|
8
|
+
def initialize(message: 'That file has invalid format')
|
|
9
|
+
super(message)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Livelist
|
|
4
|
+
# Livelist::HLS
|
|
5
|
+
module HLS
|
|
6
|
+
def self.current_time(playlist, seconds = 2)
|
|
7
|
+
lines = File
|
|
8
|
+
.readlines(playlist)
|
|
9
|
+
.select { |line| line =~ /EXTINF:[0-9]/ }
|
|
10
|
+
lines.length * seconds
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Finish the playlist with a endlist tag
|
|
14
|
+
# @param String playlist
|
|
15
|
+
# @return void
|
|
16
|
+
def self.finish(playlist)
|
|
17
|
+
file = File.read(playlist)
|
|
18
|
+
content = file.gsub(/#EXT-X-PLAYLIST-TYPE:EVENT/, '#EXT-X-PLAYLIST-TYPE:VOD')
|
|
19
|
+
File.open(playlist, 'w') { |f| f.puts content}
|
|
20
|
+
File.open(playlist, 'a') do |file|
|
|
21
|
+
file.puts '#EXT-X-ENDLIST'
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Livelist
|
|
4
|
+
module HLS
|
|
5
|
+
# A pipeline for manipulate playlist
|
|
6
|
+
# using HTTP Livestreaming standards
|
|
7
|
+
class Pipeline
|
|
8
|
+
# @param [String] path
|
|
9
|
+
# @param [Hash] options
|
|
10
|
+
def initialize(path, options = {})
|
|
11
|
+
@path = path
|
|
12
|
+
@options = options
|
|
13
|
+
@playlist = Livelist::Playlist.new(@path, options)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Run the pipeline to HTTP streaming
|
|
17
|
+
# @param [String] segment_name
|
|
18
|
+
# @return [void]
|
|
19
|
+
def run(segment_name)
|
|
20
|
+
create_playlist
|
|
21
|
+
append_segment(segment_name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
# Write the playlist file
|
|
27
|
+
def create_playlist
|
|
28
|
+
@playlist.write unless File.exist? @path
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Append a segment to playlist file
|
|
32
|
+
# the segment name can be a URL or a simple file
|
|
33
|
+
#
|
|
34
|
+
# @param [String] segment_name
|
|
35
|
+
def append_segment(segment_name)
|
|
36
|
+
Livelist::Segment
|
|
37
|
+
.new(segment_name, @options[:target_duration].to_f)
|
|
38
|
+
.append(@playlist)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Livelist
|
|
4
|
+
# A Playlist m3u8
|
|
5
|
+
class Playlist
|
|
6
|
+
# @param path [String] where will be the final m3u8
|
|
7
|
+
#
|
|
8
|
+
# @param options [Hash] all options allowed
|
|
9
|
+
# @option options [Integer] :version playlist version defaults 1
|
|
10
|
+
# @option options [Boolean] :allow playlist allow cache defaults false
|
|
11
|
+
# @option options [Integer] :target_duration playlist target duration in seconds defaults 10
|
|
12
|
+
#
|
|
13
|
+
# @raise [Exceptions::InvalidFormat] invalid format if file is not a .m3u8
|
|
14
|
+
def initialize(path, options = {})
|
|
15
|
+
@path = path
|
|
16
|
+
@options = options
|
|
17
|
+
@tag = "#EXTM3U"
|
|
18
|
+
raise Exceptions::InvalidFormat unless File.extname(path) == '.m3u8'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def path
|
|
22
|
+
@path
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Write the m3u8 file
|
|
26
|
+
def write
|
|
27
|
+
File.open(@path, 'w') do |file|
|
|
28
|
+
[
|
|
29
|
+
@tag,
|
|
30
|
+
type,
|
|
31
|
+
version,
|
|
32
|
+
media_sequence,
|
|
33
|
+
allow_cache?,
|
|
34
|
+
target_duration,
|
|
35
|
+
].each { |tag| file.puts tag }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Return a string version of m3u8 file
|
|
40
|
+
def to_s
|
|
41
|
+
[
|
|
42
|
+
@tag,
|
|
43
|
+
version,
|
|
44
|
+
media_sequence,
|
|
45
|
+
allow_cache?,
|
|
46
|
+
target_duration
|
|
47
|
+
].join("\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
def version
|
|
52
|
+
"#EXT-X-VERSION:#{@options[:version] ||= 1}"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def allow_cache?
|
|
56
|
+
"#EXT-X-ALLOW-CACHE:#{@options[:allow] ? 'YES' : 'NO'}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def target_duration
|
|
60
|
+
"#EXT-X-TARGETDURATION:#{@options[:target_duration] ||= 10}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def media_sequence
|
|
64
|
+
'#EXT-X-MEDIA-SEQUENCE:0'
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def type
|
|
68
|
+
"#EXT-X-PLAYLIST-TYPE:#{@options[:type] ||= 'EVENT'}"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Livelist
|
|
4
|
+
# A Segment to playlist file
|
|
5
|
+
class Segment
|
|
6
|
+
# @param src [String] src can be a URI or a file name
|
|
7
|
+
# @param duration [Float] duration in seconds
|
|
8
|
+
def initialize(src, duration)
|
|
9
|
+
@src = src
|
|
10
|
+
@duration = duration
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Append a new segment on playlist file
|
|
14
|
+
#
|
|
15
|
+
# @param [Livelist::Playlist] playlist
|
|
16
|
+
def append(playlist)
|
|
17
|
+
write(playlist) if File.exist? playlist.path
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Append a new segment but removing
|
|
21
|
+
# the previous segment from playlist
|
|
22
|
+
#
|
|
23
|
+
# @param [Livelist::Playlist] playlist
|
|
24
|
+
def hls_append(playlist)
|
|
25
|
+
hls_write(playlist) if File.exist? playlist.path
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
# @param [Livelist::Playlist] playlist
|
|
31
|
+
def write(playlist)
|
|
32
|
+
File.open(playlist.path, 'a') do |file|
|
|
33
|
+
[
|
|
34
|
+
"#EXTINF:#{@duration},",
|
|
35
|
+
@src
|
|
36
|
+
].each { |line| file.puts line }
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param [Livelist::Playlist] playlist
|
|
41
|
+
def hls_write(playlist)
|
|
42
|
+
append(playlist)
|
|
43
|
+
segment = File.open(playlist.path, 'r') do |file|
|
|
44
|
+
context = file.readlines.map(&:chomp)
|
|
45
|
+
remove_tag_and_segment(context)
|
|
46
|
+
end
|
|
47
|
+
File.open(playlist.path, 'w') { |f| f.puts segment }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param context [Array] an array containing tag and segment
|
|
51
|
+
# @return [Array] updated array without tag and segment
|
|
52
|
+
def remove_tag_and_segment(context)
|
|
53
|
+
if context.grep(/#EXTINF:[0-9]+/).length > 2
|
|
54
|
+
context.delete_at 5
|
|
55
|
+
context.delete_at 5
|
|
56
|
+
end
|
|
57
|
+
context
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Livelist
|
|
4
|
+
# A Media Sequence for playlist file
|
|
5
|
+
class Sequence
|
|
6
|
+
# @param playlist [Livelist::Playlist] Playlist instance
|
|
7
|
+
def initialize(playlist)
|
|
8
|
+
@playlist = playlist
|
|
9
|
+
@pattern = /#EXT-X-MEDIA-SEQUENCE:[0-9]+/
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Writes the new sequence on playlist file
|
|
13
|
+
def add
|
|
14
|
+
file = File.read(@playlist.path)
|
|
15
|
+
content = file.gsub(@pattern, new_sequence_line(sequence_line))
|
|
16
|
+
File.open(@playlist.path, 'w') { |f| f.puts content}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# Return the sequence line adding 1 to the previous value
|
|
22
|
+
# @param line [String]
|
|
23
|
+
# @return [String] the sequence tag with the updated value
|
|
24
|
+
def new_sequence_line(line)
|
|
25
|
+
tag, sequence = line.split(':')
|
|
26
|
+
seq = sequence.strip.to_i + 1
|
|
27
|
+
"#{tag}:#{seq}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Return the sequence tag based on regex pattern
|
|
31
|
+
# @return [String] sequence tag line
|
|
32
|
+
def sequence_line
|
|
33
|
+
File.open(@playlist.path, 'r') do |file|
|
|
34
|
+
file.find { |line| line =~ @pattern }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/livelist.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: livelist
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Vitor Roque
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2021-10-12 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: A simple gem to manipulate m3u8 files
|
|
14
|
+
email:
|
|
15
|
+
- vitor.roquep@gmail.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- lib/livelist.rb
|
|
21
|
+
- lib/livelist/exceptions/invalid_format.rb
|
|
22
|
+
- lib/livelist/hls/hls.rb
|
|
23
|
+
- lib/livelist/hls/pipeline.rb
|
|
24
|
+
- lib/livelist/playlist.rb
|
|
25
|
+
- lib/livelist/segment.rb
|
|
26
|
+
- lib/livelist/sequence.rb
|
|
27
|
+
- lib/livelist/version.rb
|
|
28
|
+
homepage: https://github.com/roqueando/livelist
|
|
29
|
+
licenses:
|
|
30
|
+
- MIT
|
|
31
|
+
metadata:
|
|
32
|
+
allowed_push_host: https://rubygems.org
|
|
33
|
+
homepage_uri: https://github.com/roqueando/livelist
|
|
34
|
+
source_code_uri: https://github.com/roqueando/livelist
|
|
35
|
+
changelog_uri: https://github.com/roqueando/livelist
|
|
36
|
+
post_install_message:
|
|
37
|
+
rdoc_options: []
|
|
38
|
+
require_paths:
|
|
39
|
+
- lib
|
|
40
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
41
|
+
requirements:
|
|
42
|
+
- - ">="
|
|
43
|
+
- !ruby/object:Gem::Version
|
|
44
|
+
version: 2.7.0
|
|
45
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
46
|
+
requirements:
|
|
47
|
+
- - ">="
|
|
48
|
+
- !ruby/object:Gem::Version
|
|
49
|
+
version: '0'
|
|
50
|
+
requirements: []
|
|
51
|
+
rubygems_version: 3.2.15
|
|
52
|
+
signing_key:
|
|
53
|
+
specification_version: 4
|
|
54
|
+
summary: A simple gem to manipulate m3u8 files
|
|
55
|
+
test_files: []
|