glued 0.1.0
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/Gemfile +3 -0
- data/Gemfile.lock +34 -0
- data/LICENSE +20 -0
- data/README.md +76 -0
- data/Rakefile +10 -0
- data/bin/glued +15 -0
- data/glued.watchr +3 -0
- data/lib/glued.rb +13 -0
- data/lib/glued/bootstrap.rb +196 -0
- data/lib/glued/f4f.rb +44 -0
- data/lib/glued/f4m.rb +44 -0
- data/lib/glued/f4vio.rb +47 -0
- data/lib/glued/glue.rb +16 -0
- data/lib/glued/grabber.rb +91 -0
- data/lib/glued/version.rb +7 -0
- data/spec/bootstrap_spec.rb +89 -0
- data/spec/f4f_spec.rb +4 -0
- data/spec/f4m_spec.rb +58 -0
- data/spec/f4vio_spec.rb +45 -0
- data/spec/fixtures/bad_namespace.f4m +3 -0
- data/spec/fixtures/live.f4m +13 -0
- data/spec/fixtures/no_namespace.f4m +3 -0
- data/spec/fixtures/vod.f4m +28 -0
- data/spec/glue_spec.rb +15 -0
- data/spec/grabber_spec.rb +43 -0
- data/spec/spec_helper.rb +9 -0
- metadata +161 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6525daf00b7018118ea699e561d5da854e6b2c6b
|
4
|
+
data.tar.gz: 2f88d6c7f82b4e132623840697f985d0dd02ce91
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: dfbbc52f1ca331675ec8e488cdd395876afc88bdc001d2623f24b48b7475d902a609cf999063663dac85051254913fffa7a0502b89fbf4a8bf3e5c27f80bed67
|
7
|
+
data.tar.gz: 71ac92849df89959ffe10b9191be2375845b32542e5be1de0405f08aa1abb5f6419b3d0c320caea76c22f4945230dd82181850f99d02ab18712c1a7941dbfc51
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
glued (0.1.0)
|
5
|
+
curb
|
6
|
+
nokogiri
|
7
|
+
|
8
|
+
GEM
|
9
|
+
remote: https://rubygems.org/
|
10
|
+
specs:
|
11
|
+
curb (0.8.5)
|
12
|
+
diff-lcs (1.2.5)
|
13
|
+
mini_portile (0.5.2)
|
14
|
+
nokogiri (1.6.1)
|
15
|
+
mini_portile (~> 0.5.0)
|
16
|
+
observr (1.0.5)
|
17
|
+
rake (0.9.6)
|
18
|
+
rspec (2.14.1)
|
19
|
+
rspec-core (~> 2.14.0)
|
20
|
+
rspec-expectations (~> 2.14.0)
|
21
|
+
rspec-mocks (~> 2.14.0)
|
22
|
+
rspec-core (2.14.7)
|
23
|
+
rspec-expectations (2.14.5)
|
24
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
25
|
+
rspec-mocks (2.14.5)
|
26
|
+
|
27
|
+
PLATFORMS
|
28
|
+
ruby
|
29
|
+
|
30
|
+
DEPENDENCIES
|
31
|
+
glued!
|
32
|
+
observr
|
33
|
+
rake
|
34
|
+
rspec
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) MMXIV The original author or authors
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
# Glued
|
2
|
+
|
3
|
+
[](https://travis-ci.org/simongregory/glued)
|
4
|
+
|
5
|
+
A Ruby client to download [HDS][hds] *recorded* fragments and glue them together. If it's live content you want then you need to start coding.
|
6
|
+
|
7
|
+
The primary aim is to download video on demand content. Encrypted content will download, however it will not playback unless your player accomodates it and a license is available for the media.
|
8
|
+
|
9
|
+
[HDS][hds] is designed for live media that dynamically adapts to network conditions and avoids buffering. It uses the HTTP protocol to benefit from existing network infastructre (ie [CDN][cdn]s). It is similar to [MPEG-DASH][mpeg-dash] and [HLS][hls].
|
10
|
+
|
11
|
+
## References
|
12
|
+
|
13
|
+
### Specification Documents
|
14
|
+
|
15
|
+
* [Flash Media Manifest Specification][f4m-spec]
|
16
|
+
* [Flash Video Format Specification][f4v-spec]
|
17
|
+
|
18
|
+
### Code
|
19
|
+
|
20
|
+
* [OSMF][] [httpstreaming][osmf-httpstreaming] (AS3)
|
21
|
+
* [OSMF][] [f4mClasses][osmf-f4mclasses] with [F4MLoader][osmf-f4mloader] as the entry point. (AS3)
|
22
|
+
* [HDS Union](https://github.com/AndyA/hds_union) (perl)
|
23
|
+
* [K-S-V Scripts](https://github.com/K-S-V/Scripts/blob/master/AdobeHDS.php) (php)
|
24
|
+
|
25
|
+
### Etc
|
26
|
+
|
27
|
+
* [Getting started with HDS](http://www.thekuroko.com/http-dynamic-streaming-getting-started/)
|
28
|
+
* [Fragmented MP4](http://technology-pedia.blogspot.co.uk/2012/09/fragmented-mp4-format-fmp4-f4f-adobe.html)
|
29
|
+
|
30
|
+
* [flvtool2](https://github.com/unnu/flvtool2)
|
31
|
+
* [get-flash-videos](https://github.com/monsieurvideo/get-flash-videos)
|
32
|
+
* [rtmp-dump](http://rtmpdump.mplayerhq.hu)
|
33
|
+
* [get_iplayer](https://github.com/dinkypumpkin/get_iplayer/)
|
34
|
+
|
35
|
+
### Questions / Answers / Guesses
|
36
|
+
|
37
|
+
A *box* is a convention within a stream of bytes. Each box starts with a header, which describes length and type, followed by the data in the box.
|
38
|
+
|
39
|
+
### Abbreviations
|
40
|
+
|
41
|
+
HDS - HTTP Dynamic Streaming
|
42
|
+
F4M - Flash Media Manfiest
|
43
|
+
F4F - Flash Media File fragment
|
44
|
+
F4X - Flash Media Index file
|
45
|
+
F4V - H.264/AAC based content
|
46
|
+
FLV - Other flash supported codecs
|
47
|
+
|
48
|
+
## How it works
|
49
|
+
|
50
|
+
Load the f4m manifest
|
51
|
+
- Bad url? go 💥
|
52
|
+
- Detect any alternate bitrate manifests? go 💥
|
53
|
+
- Detect a live stream? go 💥
|
54
|
+
- Automatically picks the higest bitrate stream
|
55
|
+
Builds a list of fragment urls
|
56
|
+
Download each fragment
|
57
|
+
- Fragment fails? go 💥
|
58
|
+
Glue it to the previous fragment
|
59
|
+
Repeat till done
|
60
|
+
|
61
|
+
## License
|
62
|
+
|
63
|
+
[MIT][], see accomanying [LICENSE](LICENSE) document
|
64
|
+
|
65
|
+
[ruby]: https://www.ruby-lang.org
|
66
|
+
[hds]: http://www.adobe.com/uk/products/hds-dynamic-streaming.html "Adobe HTTP Dynamic Streaming"
|
67
|
+
[cdn]: http://en.wikipedia.org/wiki/Content_delivery_network
|
68
|
+
[hls]: http://en.wikipedia.org/wiki/HTTP_Live_Streaming
|
69
|
+
[mpeg-dash]: http://en.wikipedia.org/wiki/MPEG_DASH
|
70
|
+
[OSMF]: http://osmf.org/ "Open Source Media Framework"
|
71
|
+
[osmf-httpstreaming]: http://opensource.adobe.com/svn/opensource/osmf/trunk/framework/OSMF/org/osmf/net/httpstreaming/
|
72
|
+
[osmf-f4mclasses]: http://opensource.adobe.com/svn/opensource/osmf/trunk/framework/OSMF/org/osmf/elements/f4mClasses/
|
73
|
+
[osmf-f4mloader]: http://opensource.adobe.com/svn/opensource/osmf/trunk/framework/OSMF/org/osmf/elements/F4MLoader.as
|
74
|
+
[f4m-spec]: doc/adobe-media-manifest-specification.pdf
|
75
|
+
[f4v-spec]: doc/adobe-flash-video-file-format-spec.pdf
|
76
|
+
[MIT]: http://opensource.org/licenses/MIT
|
data/Rakefile
ADDED
data/bin/glued
ADDED
data/glued.watchr
ADDED
data/lib/glued.rb
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Bootstrap
|
4
|
+
attr_reader :boxes
|
5
|
+
|
6
|
+
def initialize(data)
|
7
|
+
@reader = F4VIO.new(data)
|
8
|
+
@boxes = []
|
9
|
+
scan
|
10
|
+
end
|
11
|
+
|
12
|
+
#Top level
|
13
|
+
AFRA = 'afra' #Fragment random access for HTTP streaming
|
14
|
+
ABST = 'abst' #Bootstrap info for HTTP streaming
|
15
|
+
MOOV = 'moov' #Container for structural metadata
|
16
|
+
MOOF = 'moof' #Movie Fragment
|
17
|
+
MDAT = 'mdat' #Moovie data container
|
18
|
+
|
19
|
+
#Inside ABST
|
20
|
+
ASRT = 'asrt' #Segment run table box
|
21
|
+
AFRT = 'afrt' #Fragment runt table box
|
22
|
+
|
23
|
+
def segments
|
24
|
+
@boxes.first.segments
|
25
|
+
end
|
26
|
+
|
27
|
+
def fragments
|
28
|
+
@boxes.first.segment_run_tables.first.run_entry_table.first.fragments_per_segment
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def scan
|
34
|
+
# Scan for 'boxes' in the stream see spec 1.3 F4V box format
|
35
|
+
until (@reader.eof?) do
|
36
|
+
box = get_box_info
|
37
|
+
|
38
|
+
case box.type
|
39
|
+
when ABST
|
40
|
+
@boxes << get_bootstrap_box(box)
|
41
|
+
when AFRA
|
42
|
+
@boxes << box
|
43
|
+
when MDAT
|
44
|
+
@boxes << box
|
45
|
+
else
|
46
|
+
break;
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
raise "Computer says no" if @boxes.empty?
|
51
|
+
|
52
|
+
@boxes
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_box_info
|
56
|
+
pos = @reader.pos
|
57
|
+
size = @reader.int32
|
58
|
+
type = @reader.fourCC
|
59
|
+
size = @reader.int64 if size == 1 #For boxes over 4GB the size is moved here.
|
60
|
+
|
61
|
+
Header.new(pos, size, type)
|
62
|
+
end
|
63
|
+
|
64
|
+
def get_bootstrap_box(header)
|
65
|
+
# 2.11.2 Bootstrap Info box
|
66
|
+
b = BootstrapBox.new
|
67
|
+
b.header = header
|
68
|
+
b.version = @reader.byte
|
69
|
+
b.flags = @reader.int24
|
70
|
+
b.bootstrap_info_version = @reader.int32
|
71
|
+
|
72
|
+
plu = @reader.byte
|
73
|
+
b.profile = plu >> 6
|
74
|
+
b.live = (plu & 0x20) ? 1 : 0
|
75
|
+
b.update = (plu & 0x01) ? 1 : 0
|
76
|
+
|
77
|
+
b.time_scale = @reader.int32
|
78
|
+
b.current_media_time = @reader.int64
|
79
|
+
b.smpte_timecode_offset = @reader.int64
|
80
|
+
b.movie_identifier = @reader.string
|
81
|
+
b.servers = @reader.byte_ar
|
82
|
+
b.quality = @reader.byte_ar
|
83
|
+
b.drm_data = @reader.string
|
84
|
+
b.metadata = @reader.string
|
85
|
+
b.segments = @reader.byte
|
86
|
+
b.segment_run_tables = []
|
87
|
+
b.segments.times { b.segment_run_tables << get_asrt_box(get_box_info) }
|
88
|
+
|
89
|
+
raise "There should be at least one segment entry" if b.segment_run_tables.empty?
|
90
|
+
|
91
|
+
b.fragments = @reader.byte
|
92
|
+
b.fragment_run_tables = []
|
93
|
+
b.fragments.times { b.fragment_run_tables << get_afrt_box(get_box_info) }
|
94
|
+
|
95
|
+
raise "There should be at least one fragment entry" if b.fragment_run_tables.empty?
|
96
|
+
|
97
|
+
b
|
98
|
+
end
|
99
|
+
|
100
|
+
def get_asrt_box(header)
|
101
|
+
# 2.11.2.1 Segment Run Table box
|
102
|
+
raise "Unexpected segment run table box header '#{header.type}' instead of '#{ASRT}'" unless header.type == ASRT
|
103
|
+
|
104
|
+
b = RunTableBox.new
|
105
|
+
b.header = header
|
106
|
+
b.version = @reader.byte
|
107
|
+
b.flags = @reader.int24
|
108
|
+
b.quality_segment_url_modifiers = @reader.byte_ar
|
109
|
+
|
110
|
+
table = []
|
111
|
+
runs = @reader.int32
|
112
|
+
|
113
|
+
runs.times do
|
114
|
+
first_segment = @reader.int32
|
115
|
+
fragments_per_segment = @reader.int32
|
116
|
+
|
117
|
+
table << SegmentRunEntry.new(first_segment, fragments_per_segment)
|
118
|
+
end
|
119
|
+
|
120
|
+
b.run_entry_table = table
|
121
|
+
b
|
122
|
+
end
|
123
|
+
|
124
|
+
def get_afrt_box(header)
|
125
|
+
# 2.11.2.2 Fragment Run Table box
|
126
|
+
raise "Unexpected fragment run table box header '#{header.type}' instead of '#{AFRT}'" unless header.type == AFRT
|
127
|
+
|
128
|
+
b = RunTableBox.new
|
129
|
+
b.header = header
|
130
|
+
b.version = @reader.byte
|
131
|
+
b.flags = @reader.int24
|
132
|
+
b.time_scale = @reader.int32
|
133
|
+
b.quality_segment_url_modifiers = @reader.byte_ar
|
134
|
+
|
135
|
+
table = []
|
136
|
+
runs = @reader.int32
|
137
|
+
|
138
|
+
runs.times do
|
139
|
+
f = FragmentRunEntry.new
|
140
|
+
f.first_fragment = @reader.int32
|
141
|
+
f.first_fragment_timestamp = @reader.int64
|
142
|
+
f.fragment_duration = @reader.int32
|
143
|
+
f.discontinuity_indicator = @reader.byte if f.fragment_duration == 0
|
144
|
+
|
145
|
+
table << f
|
146
|
+
end
|
147
|
+
|
148
|
+
b.run_entry_table = table
|
149
|
+
b
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
class Header < Struct.new(:pos, :size, :type)
|
154
|
+
#pos, starting position within the byte stream
|
155
|
+
#size, number of bytes within the box
|
156
|
+
#type, descriptive type for the bytes stored in the box
|
157
|
+
end
|
158
|
+
|
159
|
+
class BootstrapBox < Struct.new(:header,
|
160
|
+
:version,
|
161
|
+
:flags,
|
162
|
+
:bootstrap_info_version,
|
163
|
+
:profile,
|
164
|
+
:live,
|
165
|
+
:update,
|
166
|
+
:time_scale,
|
167
|
+
:current_media_time,
|
168
|
+
:smpte_timecode_offset,
|
169
|
+
:movie_identifier,
|
170
|
+
:servers,
|
171
|
+
:quality,
|
172
|
+
:drm_data,
|
173
|
+
:metadata,
|
174
|
+
:segments,
|
175
|
+
:segment_run_tables,
|
176
|
+
:fragments,
|
177
|
+
:fragment_run_tables)
|
178
|
+
end
|
179
|
+
|
180
|
+
class SegmentRunEntry < Struct.new(:first_segment, :fragments_per_segment)
|
181
|
+
end
|
182
|
+
|
183
|
+
class FragmentRunEntry < Struct.new(:first_fragment,
|
184
|
+
:first_fragment_timestamp,
|
185
|
+
:fragment_duration,
|
186
|
+
:discontinuity_indicator)
|
187
|
+
end
|
188
|
+
|
189
|
+
#For Segment and Fragment boxes
|
190
|
+
class RunTableBox < Struct.new(:header,
|
191
|
+
:version,
|
192
|
+
:flags,
|
193
|
+
:time_scale,
|
194
|
+
:quality_segment_url_modifiers,
|
195
|
+
:run_entry_table)
|
196
|
+
end
|
data/lib/glued/f4f.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class F4F
|
4
|
+
attr_reader :boxes
|
5
|
+
|
6
|
+
def initialize(reader)
|
7
|
+
@reader = reader
|
8
|
+
@boxes = []
|
9
|
+
|
10
|
+
# Scan for boxes in the stream see spec 1.3 F4V box format
|
11
|
+
until (@reader.pos >= @reader.size) do
|
12
|
+
box = next_box
|
13
|
+
@boxes << box
|
14
|
+
@reader.pos = box.pos + box.size
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def ok?
|
19
|
+
# WARNING: There are rumours that "Some moronic servers add wrong
|
20
|
+
# boxSize in header causing fragment verification to fail so we
|
21
|
+
# have to fix the boxSize before processing the fragment."
|
22
|
+
|
23
|
+
(@boxes[0].type == Bootstrap::AFRA && @boxes[1].type == Bootstrap::MOOF && @boxes[2].type == Bootstrap::MDAT)
|
24
|
+
end
|
25
|
+
|
26
|
+
def next_box
|
27
|
+
pos = @reader.pos
|
28
|
+
size = @reader.int32
|
29
|
+
type = @reader.fourCC
|
30
|
+
size = @reader.int64 if size == 1 #For boxes over 4GB the size is moved here.
|
31
|
+
|
32
|
+
header_size = @reader.pos-pos
|
33
|
+
content_size = size - header_size
|
34
|
+
|
35
|
+
F4FHeader.new(pos, size, type, @reader.pos, content_size)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
class F4FHeader < Struct.new(:pos, :size, :type, :content_start, :content_size)
|
41
|
+
#pos, starting position within the byte stream
|
42
|
+
#size, number of bytes within the box
|
43
|
+
#type, descriptive type for the bytes stored in the box
|
44
|
+
end
|
data/lib/glued/f4m.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class F4M
|
4
|
+
NAMESPACE = 'http://ns.adobe.com/f4m/1.0'
|
5
|
+
|
6
|
+
attr_reader :duration,
|
7
|
+
:bootstrap_info,
|
8
|
+
:base_ref,
|
9
|
+
:media_filename,
|
10
|
+
:media
|
11
|
+
|
12
|
+
def initialize(url, data)
|
13
|
+
xml = Nokogiri::XML(data)
|
14
|
+
|
15
|
+
namespace = xml.root.namespace.href rescue 'not found'
|
16
|
+
raise "Invalid manifest namespace. It was #{namespace} but should have been #{NAMESPACE}" unless namespace == NAMESPACE
|
17
|
+
|
18
|
+
xml.remove_namespaces!
|
19
|
+
|
20
|
+
stream_type = xml.xpath("//streamType").first.text rescue 'unknown'
|
21
|
+
raise "Only recorded streams are supported." unless stream_type == 'recorded'
|
22
|
+
|
23
|
+
@duration = xml.xpath("//duration").first.text.to_i
|
24
|
+
|
25
|
+
b64_bootstrap_info = xml.xpath("//bootstrapInfo").first.text
|
26
|
+
@bootstrap_info = Base64.strict_decode64(b64_bootstrap_info)
|
27
|
+
|
28
|
+
media_nodes = xml.xpath("/manifest[1]/media")
|
29
|
+
@media = find_highest_bitrate(media_nodes)
|
30
|
+
@media_filename = @media.at_css('@url').value
|
31
|
+
|
32
|
+
@base_ref = url.split('/').slice(0...-1).join('/') #can be specified in xml
|
33
|
+
end
|
34
|
+
|
35
|
+
def find_highest_bitrate(list)
|
36
|
+
rates = []
|
37
|
+
list.each do |media|
|
38
|
+
br = media.at_css("@bitrate").value.to_i
|
39
|
+
rates[br] = media
|
40
|
+
end
|
41
|
+
|
42
|
+
rates.reject {|e| e.nil? }.pop
|
43
|
+
end
|
44
|
+
end
|
data/lib/glued/f4vio.rb
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class F4VIO < StringIO
|
4
|
+
|
5
|
+
def byte
|
6
|
+
self.read(1).unpack('C').first
|
7
|
+
end
|
8
|
+
|
9
|
+
def int16
|
10
|
+
self.read(2).unpack('n').first
|
11
|
+
end
|
12
|
+
|
13
|
+
def int24
|
14
|
+
"\x00#{self.read(3)}".unpack('N').first
|
15
|
+
end
|
16
|
+
|
17
|
+
def int32
|
18
|
+
self.read(4).unpack('N').first
|
19
|
+
end
|
20
|
+
|
21
|
+
def int64
|
22
|
+
hi = int32
|
23
|
+
lo = int32
|
24
|
+
(hi * 4294967296) + lo
|
25
|
+
end
|
26
|
+
|
27
|
+
def fourCC
|
28
|
+
self.read(4).unpack('A4').first
|
29
|
+
end
|
30
|
+
|
31
|
+
def string
|
32
|
+
o, p = self.pos, 0
|
33
|
+
p += 1 while (self.read(1) != "\x00")
|
34
|
+
|
35
|
+
self.pos = o
|
36
|
+
str = self.read(p)
|
37
|
+
self.pos += 1
|
38
|
+
|
39
|
+
str
|
40
|
+
end
|
41
|
+
|
42
|
+
def byte_ar
|
43
|
+
ar = []
|
44
|
+
byte.times { ar << byte }
|
45
|
+
ar
|
46
|
+
end
|
47
|
+
end
|
data/lib/glued/glue.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Glue
|
4
|
+
|
5
|
+
def initialize(url)
|
6
|
+
raise "Invalid manifest url '#{url}' (it should end with .f4m)" unless url.to_s =~ /\.f4m$/ #Only by convention
|
7
|
+
|
8
|
+
xml = Curl::Easy.perform(url).body
|
9
|
+
manifest = F4M.new(url, xml)
|
10
|
+
bootstrap = Bootstrap.new(manifest.bootstrap_info)
|
11
|
+
grabber = Grabber.new(manifest, bootstrap)
|
12
|
+
|
13
|
+
puts "\rComplete "
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Grabber
|
4
|
+
attr_reader :urls
|
5
|
+
|
6
|
+
def initialize(manifest, bootstrap, io=nil)
|
7
|
+
raise "Only one segment can be handled" if bootstrap.segments != 1 #As we've hardcoded 1 below
|
8
|
+
raise "Not enough fragments" if bootstrap.fragments < 1
|
9
|
+
raise "Too many fragments" if bootstrap.fragments > 10000 #not sure what this limit should be
|
10
|
+
|
11
|
+
@uri = "#{manifest.media_filename}.flv"
|
12
|
+
@url = "#{manifest.base_ref}/#{manifest.media_filename}Seg1-Frag"
|
13
|
+
@total_fragments = bootstrap.fragments
|
14
|
+
@urls = []
|
15
|
+
@downloaded_fragments = []
|
16
|
+
@fragments_downloaded = 0
|
17
|
+
|
18
|
+
#TODO: Track how much has already been downloaded and append from that point
|
19
|
+
raise "Aborting as the download target file '#{@uri}' already exists" if File.exist? @uri
|
20
|
+
|
21
|
+
@out = io ||= File.new(@uri, 'ab')
|
22
|
+
@out.write(flv_header(1,1))
|
23
|
+
|
24
|
+
build
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
#TODO: Inspect first fragment, test for audio and video, write header accordingly
|
30
|
+
def flv_header(audio, video)
|
31
|
+
#Audio and video need to be a 1 or 0
|
32
|
+
flv_header = ["464c5601050000000900000000"].pack("H*")
|
33
|
+
|
34
|
+
result = audio << 2 | video
|
35
|
+
flv_header[4] = [result].pack('C')
|
36
|
+
|
37
|
+
flv_header
|
38
|
+
end
|
39
|
+
|
40
|
+
def build
|
41
|
+
#TODO, correctly set the fragment start
|
42
|
+
fragment = 1
|
43
|
+
while fragment <= @total_fragments
|
44
|
+
@urls << "#{@url}#{fragment}"
|
45
|
+
fragment += 1
|
46
|
+
end
|
47
|
+
|
48
|
+
@urls.each { |url| download url }
|
49
|
+
end
|
50
|
+
|
51
|
+
def download url
|
52
|
+
file_name = url.split('/').pop
|
53
|
+
|
54
|
+
dl = fetch_and_report(url)
|
55
|
+
|
56
|
+
raise "Invalid content type '#{dl.content_type}' for fragment #{file_name}." unless dl.content_type == 'video/f4f'
|
57
|
+
|
58
|
+
reader = F4VIO.new(dl.body)
|
59
|
+
f4f = F4F.new(reader)
|
60
|
+
|
61
|
+
raise "Fragment did not verify" unless f4f.ok?
|
62
|
+
|
63
|
+
f4f.boxes.each do |box|
|
64
|
+
if box.type == 'mdat'
|
65
|
+
reader.pos = box.content_start
|
66
|
+
@out.write(reader.read(box.content_size))
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
@fragments_downloaded += 1
|
71
|
+
end
|
72
|
+
|
73
|
+
def fetch_and_report(url)
|
74
|
+
start_time = Time.now
|
75
|
+
|
76
|
+
dl = Curl::Easy.perform(url)
|
77
|
+
|
78
|
+
report(dl.body.length, Time.now-start_time)
|
79
|
+
|
80
|
+
dl
|
81
|
+
end
|
82
|
+
|
83
|
+
def report(number_of_bytes, in_seconds)
|
84
|
+
bps = (number_of_bytes*8) / in_seconds
|
85
|
+
kbps = bps/1000
|
86
|
+
mbps = bps/1000000
|
87
|
+
|
88
|
+
print "\rDownloading #{@fragments_downloaded+1}/#{@total_fragments} at #{mbps.round(2)} Mbps"
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
4
|
+
|
5
|
+
describe Bootstrap, "parsing a bootstrap" do
|
6
|
+
|
7
|
+
def info
|
8
|
+
Base64.strict_decode64 'AAAAjWFic3QAAAAAAAACFAAAAAPoAAAAAAA+QYAAAAAAAAAAAEFXAAAAAAABAAAAGWFzcnQAAAAAAAAAAAEAAAABAAACFAEAAABGYWZydAAAAAAAAAPoAAAAAAMAAAABAAAAAAAAAAAAAB4AAAACFAAAAAAAPjoAAAAHgAAAAAAAAAAAAAAAAAAAAAAA'
|
9
|
+
end
|
10
|
+
|
11
|
+
it "decodes bootstrap info (abst) boxes" do
|
12
|
+
boot = Bootstrap.new(info)
|
13
|
+
box = boot.boxes.first
|
14
|
+
|
15
|
+
box.version.should eq(0)
|
16
|
+
box.flags.should eq(0)
|
17
|
+
box.bootstrap_info_version.should eq(532)
|
18
|
+
box.current_media_time.should eq(4080000)
|
19
|
+
box.smpte_timecode_offset.should eq(0)
|
20
|
+
box.movie_identifier.should eq('AW')
|
21
|
+
box.servers.should eq([])
|
22
|
+
box.quality.should eq([])
|
23
|
+
box.drm_data.should eq('')
|
24
|
+
box.metadata.should eq('')
|
25
|
+
end
|
26
|
+
|
27
|
+
it "errors when data is corrupt" do
|
28
|
+
expect { Bootstrap.new('Papa+LAZAR0U') }.to raise_error "Computer says no"
|
29
|
+
end
|
30
|
+
|
31
|
+
it "decodes segment run table (asrt) boxes" do
|
32
|
+
boot = Bootstrap.new(info)
|
33
|
+
box = boot.boxes.first
|
34
|
+
|
35
|
+
srt = box.segment_run_tables.first
|
36
|
+
sre = srt.run_entry_table.first
|
37
|
+
|
38
|
+
sre.first_segment.should eq(1)
|
39
|
+
sre.fragments_per_segment.should eq(532)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "determines the number of segments" do
|
43
|
+
boot = Bootstrap.new(info)
|
44
|
+
|
45
|
+
boot.segments.should eq(1)
|
46
|
+
end
|
47
|
+
|
48
|
+
it "determines the number of fragments" do
|
49
|
+
boot = Bootstrap.new(info)
|
50
|
+
|
51
|
+
boot.fragments.should eq(532)
|
52
|
+
end
|
53
|
+
|
54
|
+
|
55
|
+
it "decodes fragment run table (afra) boxes" do
|
56
|
+
boot = Bootstrap.new(info)
|
57
|
+
box = boot.boxes.first
|
58
|
+
|
59
|
+
box.fragments.should eq(1)
|
60
|
+
|
61
|
+
box.fragment_run_tables.length.should eq(1)
|
62
|
+
|
63
|
+
frt = box.fragment_run_tables.first
|
64
|
+
fre = frt.run_entry_table.first
|
65
|
+
|
66
|
+
frt.run_entry_table.length.should eq(3)
|
67
|
+
|
68
|
+
fre.first_fragment.should eq(1)
|
69
|
+
end
|
70
|
+
|
71
|
+
it "errors when unexpected fragment boxes are found" do
|
72
|
+
bad_info = Base64.strict_decode64 "AAAAjWFic3QAAAAAAAACFAAAAAPoAAAAAAA+QYAAAAAAAAAAAEFXAAAAAAABAAAAGWFzcnQAAAAAAAAAAAEAAAABAAACFAEAAABGYWZ6dAAAAAAAAAPoAAAAAAMAAAABAAAAAAAAAAAAAB4AAAACFAAAAAAAPjoAAAAHgAAAAAAAAAAAAAAAAAAAAAAA"
|
73
|
+
|
74
|
+
expect {
|
75
|
+
boot = Bootstrap.new(bad_info)
|
76
|
+
}.to raise_error "Unexpected fragment run table box header 'afzt' instead of 'afrt'"
|
77
|
+
end
|
78
|
+
|
79
|
+
it "errors unless there are one or more segment table entries" do
|
80
|
+
end
|
81
|
+
|
82
|
+
it "errors when unexpected segment boxes are found" do
|
83
|
+
bad_info = Base64.strict_decode64 'AAAAjWFic3QAAAAAAAACFAAAAAPoAAAAAAA+QYAAAAAAAAAAAEFXAAAAAAABAAAAGWFzcHQAAAAAAAAAAAEAAAABAAACFAEAAABGYWZydAAAAAAAAAPoAAAAAAMAAAABAAAAAAAAAAAAAB4AAAACFAAAAAAAPjoAAAAHgAAAAAAAAAAAAAAAAAAAAAAA'
|
84
|
+
|
85
|
+
expect {
|
86
|
+
boot = Bootstrap.new(bad_info)
|
87
|
+
}.to raise_error "Unexpected segment run table box header 'aspt' instead of 'asrt'"
|
88
|
+
end
|
89
|
+
end
|
data/spec/f4f_spec.rb
ADDED
data/spec/f4m_spec.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
describe F4M, "Loading manifests" do
|
4
|
+
|
5
|
+
describe "a recorded manifest" do
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
@manifest = F4M.new('http://cloud.org/only/fools/vod.f4m',
|
9
|
+
IO.read('spec/fixtures/vod.f4m'))
|
10
|
+
end
|
11
|
+
|
12
|
+
after(:each) do
|
13
|
+
@manifest = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
it "creates a base url for fragments downloads" do
|
17
|
+
@manifest.base_ref.should eq('http://cloud.org/only/fools')
|
18
|
+
end
|
19
|
+
|
20
|
+
it "defines the total duration of the media in seconds" do
|
21
|
+
@manifest.duration.should eq(4080)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "has bootstrap information" do
|
25
|
+
@manifest.bootstrap_info.should_not be_empty
|
26
|
+
end
|
27
|
+
|
28
|
+
it "defines the media filename" do
|
29
|
+
@manifest.media_filename.should eq('some-audio-video-')
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "loading a live manifest" do
|
35
|
+
it "raises errors if a live manifest is used" do
|
36
|
+
expect {
|
37
|
+
F4M.new('http://cloud.org/and/horses/live.f4m',
|
38
|
+
IO.read('spec/fixtures/live.f4m'))
|
39
|
+
}.to raise_error "Only recorded streams are supported."
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "loading manifests with unrecognised namespaces" do
|
44
|
+
it "raises errors with no namespaces" do
|
45
|
+
expect {
|
46
|
+
F4M.new('http://only.fools/and/horses/live.f4m',
|
47
|
+
IO.read('spec/fixtures/no_namespace.f4m'))
|
48
|
+
}.to raise_error "Invalid manifest namespace. It was not found but should have been http://ns.adobe.com/f4m/1.0"
|
49
|
+
end
|
50
|
+
|
51
|
+
it "raises errors for incompatible namespaces" do
|
52
|
+
expect {
|
53
|
+
F4M.new('http://only.fools/and/horses/live.f4m',
|
54
|
+
IO.read('spec/fixtures/bad_namespace.f4m'))
|
55
|
+
}.to raise_error "Invalid manifest namespace. It was http://ns.adobe.com/f4m/2.0 but should have been http://ns.adobe.com/f4m/1.0"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/spec/f4vio_spec.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
4
|
+
|
5
|
+
describe F4VIO, "Flash video binary decoding (big endian)" do
|
6
|
+
|
7
|
+
it "reads sequences of Unicode 8-bit characters (UTF-8), terminated with 0x00 (unless otherwise specified)" do
|
8
|
+
scanner = F4VIO.new "abc\x00xyz\x00"
|
9
|
+
|
10
|
+
scanner.string.should eq('abc')
|
11
|
+
scanner.string.should eq('xyz')
|
12
|
+
end
|
13
|
+
|
14
|
+
it "reads bytes" do
|
15
|
+
scanner = F4VIO.new 'hello'
|
16
|
+
|
17
|
+
scanner.byte.should eq(104)
|
18
|
+
scanner.byte.should eq(101)
|
19
|
+
scanner.byte.should eq(108)
|
20
|
+
scanner.byte.should eq(108)
|
21
|
+
scanner.byte.should eq(111)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "reads 16 bit integers" do
|
25
|
+
# eg = {}
|
26
|
+
end
|
27
|
+
|
28
|
+
it "reads 24 bit integers" do
|
29
|
+
# eg = {}
|
30
|
+
end
|
31
|
+
|
32
|
+
it "reads 32 bit integers" do
|
33
|
+
# eg = {}
|
34
|
+
end
|
35
|
+
|
36
|
+
it "reads 64 bit integers" do
|
37
|
+
# eg = {}
|
38
|
+
end
|
39
|
+
|
40
|
+
it "reads 4CC, Four-character ASCII code, such as 'moov', encoded as UI32" do
|
41
|
+
scanner = F4VIO.new 'moov'
|
42
|
+
scanner.fourCC.should eq('moov')
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
2
|
+
<manifest xmlns="http://ns.adobe.com/f4m/1.0" version="3.0">
|
3
|
+
<id>myvideo</id>
|
4
|
+
<duration>253</duration>
|
5
|
+
<mimeType>video/x-flv</mimeType>
|
6
|
+
<streamType>live</streamType>
|
7
|
+
<baseURL>http://comedy</baseURL>
|
8
|
+
<drmAdditionalHeader url="http://drm.com/mydrmadditionalheader"/>
|
9
|
+
<bootstrapInfo profile="named" url="/mybootstrapinfo" fragmentDuration="4"/>
|
10
|
+
<media url="/horses/low" bitrate="408" width="640" height="480"/>
|
11
|
+
<media url="/horses/med" bitrate="908" width="800" height="600"/>
|
12
|
+
<media url="/horses/hi" bitrate="1708" width="1920" height="1080"/>
|
13
|
+
</manifest>
|
@@ -0,0 +1,28 @@
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
2
|
+
<manifest
|
3
|
+
xmlns="http://ns.adobe.com/f4m/1.0">
|
4
|
+
<id>jan2014_p40</id>
|
5
|
+
<startTime>2006-07-24T07:15:00+01:00</startTime>
|
6
|
+
<duration>4080</duration>
|
7
|
+
<mimeType>video/mp4</mimeType>
|
8
|
+
<streamType>recorded</streamType>
|
9
|
+
<deliveryType>streaming</deliveryType>
|
10
|
+
<!-- -audio_eng=128000-video=4947245 -->
|
11
|
+
<bootstrapInfo
|
12
|
+
id="boot1"
|
13
|
+
profile="named">AAAAjWFic3QAAAAAAAACFAAAAAPoAAAAAAA+QYAAAAAAAAAAAEFXAAAAAAABAAAAGWFzcnQAAAAAAAAAAAEAAAABAAACFAEAAABGYWZydAAAAAAAAAPoAAAAAAMAAAABAAAAAAAAAAAAAB4AAAACFAAAAAAAPjoAAAAHgAAAAAAAAAAAAAAAAAAAAAAA</bootstrapInfo>
|
14
|
+
<media
|
15
|
+
url="some-audio-video-"
|
16
|
+
bitrate="5075"
|
17
|
+
bootstrapInfoId="boot1"
|
18
|
+
width="1280"
|
19
|
+
height="720">
|
20
|
+
<metadata>AgAKb25NZXRhRGF0YQgAAAAAAA9tZXRhZGF0YWNyZWF0b3ICACVDb2RlU2hvcCdzIFVuaWZpZWQgU3RyZWFtaW5nIFBsYXRmb3JtAAhoYXNBdWRpbwEBAAhoYXNWaWRlbwEBAAhkdXJhdGlvbgBAr+AAAAAAAAAPYXVkaW9zYW1wbGVyYXRlAEDncAAAAAAAAA1hdWRpb2RhdGFyYXRlAEBgAAAAAAAAAAxhdWRpb2NvZGVjaWQCAARtcDRhAAZhYWNhb3QAQAAAAAAAAAAABXdpZHRoAECUAAAAAAAAAAZoZWlnaHQAQIaAAAAAAAAADXZpZGVvZGF0YXJhdGUAQLNTPrhR64UADHZpZGVvY29kZWNpZAIABEFWQzEACmF2Y3Byb2ZpbGUAQFkAAAAAAAAACGF2Y2xldmVsAEBAAAAAAAAAAAAJ</metadata>
|
21
|
+
</media>
|
22
|
+
|
23
|
+
|
24
|
+
<media url="/horses/low" bitrate="408" width="640" height="480"/>
|
25
|
+
<media url="/horses/med" bitrate="908" width="800" height="600"/>
|
26
|
+
<media url="/horses/hi" bitrate="1708" width="1920" height="1080"/>
|
27
|
+
|
28
|
+
</manifest>
|
data/spec/glue_spec.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
4
|
+
|
5
|
+
describe "Glue" do
|
6
|
+
it "downloads hds fragments and glues them together" do
|
7
|
+
# one day it just might
|
8
|
+
end
|
9
|
+
|
10
|
+
it "raises errors when the manifest url looks doubtful" do
|
11
|
+
expect {
|
12
|
+
Glue.new('/steptoe/and/son.f5m')
|
13
|
+
}.to raise_error "Invalid manifest url '/steptoe/and/son.f5m' (it should end with .f4m)"
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
4
|
+
|
5
|
+
describe "grabbing fragments" do
|
6
|
+
|
7
|
+
before(:each) do
|
8
|
+
@bs = double(Bootstrap)
|
9
|
+
@f4m = double(F4M)
|
10
|
+
@io = StringIO.new
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:each) do
|
14
|
+
@bs = nil
|
15
|
+
@f4m = nil
|
16
|
+
@io = nil
|
17
|
+
end
|
18
|
+
|
19
|
+
it "builds a list of url fragments" do
|
20
|
+
@f4m.stub(:base_ref).and_return('http://the.young.ones')
|
21
|
+
@f4m.stub(:media_filename).and_return('some-audio-video-')
|
22
|
+
@bs.stub(:segments).and_return(1)
|
23
|
+
@bs.stub(:fragments).and_return(20)
|
24
|
+
|
25
|
+
grabber = Grabber.new(@f4m, @bs, @io)
|
26
|
+
|
27
|
+
expect(grabber.urls.first).to eq('http://the.young.ones/some-audio-video-Seg1-Frag1')
|
28
|
+
expect(grabber.urls.last).to eq('http://the.young.ones/some-audio-video-Seg1-Frag20')
|
29
|
+
end
|
30
|
+
|
31
|
+
it "fails without the correct number of segments" do
|
32
|
+
@bs.stub(:segments).and_return(0)
|
33
|
+
expect { Grabber.new(@f4m, @bs) }.to raise_error "Only one segment can be handled"
|
34
|
+
|
35
|
+
@bs.stub(:segments).and_return(1)
|
36
|
+
@bs.stub(:fragments).and_return(0)
|
37
|
+
|
38
|
+
expect { Grabber.new(@f4m, @bs) }.to raise_error "Not enough fragments"
|
39
|
+
end
|
40
|
+
|
41
|
+
it "downloads the list of fragments" do
|
42
|
+
end
|
43
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,161 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: glued
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Simon Gregory
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-02-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: curb
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: nokogiri
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: observr
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '>='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '>='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: HDS Downloader
|
84
|
+
email: simon@helvector.org
|
85
|
+
executables:
|
86
|
+
- glued
|
87
|
+
extensions: []
|
88
|
+
extra_rdoc_files: []
|
89
|
+
files:
|
90
|
+
- bin/glued
|
91
|
+
- Gemfile
|
92
|
+
- Gemfile.lock
|
93
|
+
- glued.watchr
|
94
|
+
- lib/glued/bootstrap.rb
|
95
|
+
- lib/glued/f4f.rb
|
96
|
+
- lib/glued/f4m.rb
|
97
|
+
- lib/glued/f4vio.rb
|
98
|
+
- lib/glued/glue.rb
|
99
|
+
- lib/glued/grabber.rb
|
100
|
+
- lib/glued/version.rb
|
101
|
+
- lib/glued.rb
|
102
|
+
- LICENSE
|
103
|
+
- Rakefile
|
104
|
+
- README.md
|
105
|
+
- spec/bootstrap_spec.rb
|
106
|
+
- spec/f4f_spec.rb
|
107
|
+
- spec/f4m_spec.rb
|
108
|
+
- spec/f4vio_spec.rb
|
109
|
+
- spec/fixtures/bad_namespace.f4m
|
110
|
+
- spec/fixtures/live.f4m
|
111
|
+
- spec/fixtures/no_namespace.f4m
|
112
|
+
- spec/fixtures/vod.f4m
|
113
|
+
- spec/glue_spec.rb
|
114
|
+
- spec/grabber_spec.rb
|
115
|
+
- spec/spec_helper.rb
|
116
|
+
homepage: https://github.com/simongregory/glued
|
117
|
+
licenses:
|
118
|
+
- MIT
|
119
|
+
metadata: {}
|
120
|
+
post_install_message: |
|
121
|
+
Welcome to Glued
|
122
|
+
================
|
123
|
+
|
124
|
+
To download HDS delivered media pass a f4m manifest to the
|
125
|
+
tool, ie:
|
126
|
+
|
127
|
+
glued http://royston.vasey.org/papa/lazarou.f4m
|
128
|
+
|
129
|
+
The flv file should then play in a media player such as VLC.
|
130
|
+
DRM is not removed. If the file has DRM it will only be
|
131
|
+
watchable with a Flash based player and a valid license.
|
132
|
+
|
133
|
+
Functionality is basic. The tool assumes you want the highest
|
134
|
+
quality media. Live content cannot be recorded. Multilevel
|
135
|
+
manifests are not supported, and bootstrap info has to be
|
136
|
+
embedded within the manifest.
|
137
|
+
|
138
|
+
If you try a manifest that doesn't work please open an issue
|
139
|
+
at https://github.com/simongregory/glue/issues and leave the
|
140
|
+
details.
|
141
|
+
rdoc_options: []
|
142
|
+
require_paths:
|
143
|
+
- lib
|
144
|
+
- lib
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
146
|
+
requirements:
|
147
|
+
- - '>='
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
version: '0'
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
151
|
+
requirements:
|
152
|
+
- - '>='
|
153
|
+
- !ruby/object:Gem::Version
|
154
|
+
version: '0'
|
155
|
+
requirements: []
|
156
|
+
rubyforge_project:
|
157
|
+
rubygems_version: 2.0.14
|
158
|
+
signing_key:
|
159
|
+
specification_version: 4
|
160
|
+
summary: Downloads and joins HTTP Dynamic Stream fragments into a single media file
|
161
|
+
test_files: []
|