glued 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
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
@@ -0,0 +1,10 @@
1
+ require 'rspec'
2
+ require 'rspec/core/rake_task'
3
+
4
+ desc "Run all examples"
5
+ RSpec::Core::RakeTask.new(:spec) do |t|
6
+ t.rspec_opts = %w[--color]
7
+ end
8
+
9
+ task :t => [:spec]
10
+ task :default => [:spec]
data/bin/glued ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'glued'
4
+
5
+ puts %{Ruby HDS Glue #{Glued::VERSION}
6
+
7
+ Fetch it, glue it, watch it.
8
+
9
+ }
10
+
11
+ begin
12
+ Glue.new(ARGV[0])
13
+ rescue Exception => e
14
+ abort "Glue failed because: #{e.message}"
15
+ end
data/glued.watchr ADDED
@@ -0,0 +1,3 @@
1
+ watch( '.*\.rb' ) do
2
+ system 'rspec --color'
3
+ end
data/lib/glued.rb ADDED
@@ -0,0 +1,13 @@
1
+ # encoding: utf-8
2
+
3
+ require 'curb'
4
+ require 'base64'
5
+ require 'nokogiri'
6
+
7
+ require 'glued/bootstrap'
8
+ require 'glued/f4f'
9
+ require 'glued/f4m'
10
+ require 'glued/f4vio'
11
+ require 'glued/glue'
12
+ require 'glued/grabber'
13
+ require 'glued/version'
@@ -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
@@ -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,7 @@
1
+ # encoding: utf-8
2
+
3
+ module Glued
4
+ VERSION = '0.1.0'
5
+ NAME = 'glued'
6
+ USER_AGENT = "#{NAME}-#{VERSION}"
7
+ end
@@ -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
@@ -0,0 +1,4 @@
1
+ # encoding: utf-8
2
+
3
+ describe F4F, "Extracting Flash Media Fragments" do
4
+ end
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
@@ -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,3 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest xmlns="http://ns.adobe.com/f4m/2.0">
3
+ </manifest>
@@ -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,3 @@
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <manifest>
3
+ </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
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ $:.push File.join(File.dirname(__FILE__), "..", "lib")
4
+ $:.push File.dirname(__FILE__)
5
+
6
+ require 'glued'
7
+
8
+ require 'rspec'
9
+ require 'rspec/autorun'
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: []