glued 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/simongregory/glued.png?branch=master)](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: []
|