airvideo-ng 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (6) hide show
  1. data/LICENSE +20 -0
  2. data/README.rdoc +41 -0
  3. data/Rakefile +20 -0
  4. data/VERSION +1 -0
  5. data/lib/airvideo.rb +642 -0
  6. metadata +67 -0
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 JP Hastings-Spital
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.
@@ -0,0 +1,41 @@
1
+ = AirVideo for Ruby
2
+ Have you ever thought to yourself: I have AirVideo[http://www.inmethod.com/air-video] running on my computer and I enjoy it so much
3
+ that not only have I bought the iPhone and iPad apps, I'd also love to be able to watch my video on my laptop too?
4
+
5
+ Me too! So I reverse engineered their communication protocol and came up with this. It's a little hacky (and it's certainly not been tested outside of Mac OS X 10.6)
6
+ but it will give you the Streamable and Playable URLs of the videos on your AirVideo server from Ruby.
7
+
8
+ == Care & Share
9
+ I know you know this, but the guys at InMethod don't charge for their server. Purchases of their iPhone and iPad apps are how they get rewarded for their (epic, as I'm sure you'll agree) efforts.
10
+
11
+ Please, buy their apps if you haven't already, and send them an email or a forum post saying how much you love their software. If you're a member of InMethod, come to Nottingham in the UK - I'll buy you a pint.
12
+
13
+ == Usage
14
+ I'd like to be able to write a shiny GUI for all this, but alas, I am crap at the GUI. So as it stands you'll need to do this:
15
+
16
+ my_vids = AirVideo::Client.new('me.dyndns.org',45631,'YOUR PASSWORD')
17
+ # => <AirVideo Connection: me.dyndns.org:45631>
18
+ my_vids.ls
19
+ # => [<Folder: TV Shows>, <Folder: Movies>, <Folder: Music Videos>]
20
+ my_vids.ls[2].cd
21
+
22
+ # Bear in mind that the AirVideo::Client instance keeps track of where you are, like a console.
23
+ my_vids.ls
24
+ # => [<Video: Star Guitar>, <Video: A Glorious Dawn>, <Video: Stylo (Featuring Mos Def & Bobby Womack)>]
25
+
26
+ sagan = my_vids.ls[1]
27
+ # => <Video: A Glorious Dawn>
28
+ # Now you can select a video and get the streaming URL
29
+ sagan.url
30
+ # => "http://me.dyndns.org:45631/path_to_your.m4v"
31
+
32
+ # You can also specify (basic, for now) details as to how you want that file live transcoded to you!
33
+ my_vids.max_width = 640
34
+ my_vids.max_height = 480
35
+ sagan.live_url
36
+ # => "http://me.dyndns.org:45631/path_to_your_live_converting_resized.m4v"
37
+
38
+ # On a mac you can do this, but I'm sure you handsome folk can figure out how to do something similar on other OSes.
39
+ `open -a "QuickTime Player" "#{sagan.url}"`
40
+
41
+ If you have the ENV['HTTP_PROXY'] variable set (to something like 'myproxy.com:8080') then everything will be piped through there too.
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "airvideo-ng"
8
+ gem.summary = %Q{Allows communication with an AirVideo server}
9
+ gem.description = %Q{Communicate with an AirVideo server, even through a proxy: Retrieve the streaming URLs for your videos.}
10
+ gem.email = "regis.despres+rubygem@gmail.com"
11
+ gem.homepage = "http://github.com/kalw/AirVideo"
12
+ gem.authors = ["Kalw forked from JP Hastings"]
13
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ task :default => :build
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.9
@@ -0,0 +1,642 @@
1
+ require 'net/http'
2
+ require 'stringio'
3
+ require 'digest/sha1'
4
+
5
+ # == TODO
6
+ # * Potential bug: can you cd into a file?
7
+ # * Caching for details?
8
+ # - Active-record?
9
+ # - In-memory by default
10
+
11
+ module AirVideo
12
+ # The AirVideo Client. At this stage it can emulate the iPhone app in all major features.
13
+ #
14
+ # Minor features, such as requesting a specific streams when using the Live Conversion feature aren't yet supported, but it's just a question
15
+ # of figuring out what the server is expecting.
16
+ #
17
+ # Setting #max_height and #max_width *should* be working with the Live Conversion, but for some reason it's not quite happening. Defaults are 640x480
18
+ class Client
19
+ attr_accessor :max_height, :max_width
20
+ attr_reader :proxy
21
+
22
+ # Specify where your AirVideo Server lives. If your HTTP_PROXY environment variable is set, it will be honoured.
23
+ #
24
+ # At the moment I'm expecting ENV['HTTP_PROXY'] to have the form 'sub.domain.com:8080', I throw an http:// and bung it into URI.parse for convenience.
25
+ def initialize(server,port = 45631,password=nil)
26
+ set_proxy # Set to environment proxy settings, if applicable
27
+ @endpoint = URI.parse "http://#{server}:#{port}/service"
28
+ @passworddigest = Digest::SHA1.hexdigest("S@17" + password + "@1r").upcase if !password.nil?
29
+ @req = Net::HTTP::Post.new(@endpoint.path)
30
+ @req['User-Agent'] = 'AirVideo/2.4.1 CFNetwork/485.10.2 Darwin/10.3.1'
31
+ @req['Accept'] = '*/*'
32
+ @req['Accept-Language'] = 'en-us'
33
+ @req['Accept-Encoding'] = 'gzip, deflate'
34
+ @req['Connection'] = 'keep-alive'
35
+
36
+ @current_dir = "/"
37
+
38
+ @max_width = 640
39
+ @max_height = 480
40
+ end
41
+
42
+ # Potentially confusing:
43
+ # * Sending 'server:port' will use that address as an HTTP proxy
44
+ # * An empty string (or something not recognisable as a URL with http:// put infront of it) will try to use the ENV['HTTP_PROXY'] variable
45
+ # * Sending nil or any object that can't be parsed to a string will remove the proxy
46
+ #
47
+ # NB. You can access the @proxy URI object externally, but changing it will *not* automatically call set_proxy
48
+ def set_proxy(proxy_server_and_port = "")
49
+ begin
50
+ @proxy = URI.parse("http://"+((proxy_server_and_port.empty?) ? ENV['HTTP_PROXY'] : string_proxy))
51
+ @http = Net::HTTP::Proxy(@proxy.host, @proxy.port)
52
+ rescue
53
+ @proxy = nil
54
+ @http = Net::HTTP
55
+ end
56
+ end
57
+
58
+
59
+ # Lists the folders and videos in the current directory as an Array of AirVideo::VideoObject and AirVideo::FolderObject objects.
60
+ def ls(dir = ".")
61
+ dir = dir.location if dir.is_a? FolderObject
62
+ dir = File.expand_path(dir,@current_dir)[1..-1]
63
+ dir = nil if dir == ""
64
+
65
+ begin
66
+ request("browseService","getItems",[browse_settings(dir)])['result']['items'].collect do |hash|
67
+ #print "browsServere:getItems:\n"
68
+ #hash.each {|key, value| puts "key = #{key}, value = #{value}\n" }
69
+
70
+ case hash.name
71
+ when "air.video.DiskRootFolder", "air.video.ITunesRootFolder","air.video.Folder"
72
+ FolderObject.new(self,hash['name'],hash['itemId'])
73
+ when "air.video.VideoItem","air.video.ITunesVideoItem"
74
+ VideoObject.new(self,hash['name'],hash['itemId'],hash['detail'] || nil)
75
+ else
76
+ raise NotImplementedError, "Unknown: #{hash.name}"
77
+ end
78
+ end
79
+ rescue NoMethodError
80
+ raise RuntimeError, "This folder does not exist dir = #{dir}\n"
81
+ end
82
+ end
83
+
84
+ def browse_settings(dir)
85
+ AvMap::Hash.new("air.video.BrowseRequest",{
86
+ "folderId"=>dir,
87
+ "sortField"=>0,
88
+ "sortDirection"=>0,
89
+ "filterOriginalItems"=>0,
90
+ "metaData"=>nil,
91
+ "preloadDetails"=>0
92
+ })
93
+ end
94
+
95
+ # Changes to the given directory. Will accept an AirVideo::FolderObject or a string.
96
+ # Returns the AirVideo::Client instance, so you can string commands:
97
+ #
98
+ # AirVideo::Client.new('127.0.0.1').ls[0].cd.ls
99
+ #
100
+ # NB. This will *not* check to see if the folder actually exists!
101
+ def cd(dir)
102
+ dir = dir.location if dir.is_a? FolderObject
103
+ @current_dir = File.expand_path(dir,@current_dir)
104
+ self
105
+ end
106
+
107
+
108
+ def close_Playback(playback,service,method)
109
+ results = request(service.to_s,method.to_s,[playback.to_s])['result']
110
+ return results
111
+ end
112
+
113
+ # Returns the streaming video URL for the given AirVideo::VideoObject.
114
+ def get_url(videoobj,liveconvert = false)
115
+ raise NoMethodError, "Please pass a VideoObject" if not videoobj.is_a? VideoObject
116
+
117
+ begin
118
+ if liveconvert
119
+ details = videoobj.details
120
+ cs = conversion_settings(videoobj)
121
+ results = request("livePlaybackService","initLivePlayback",[cs])['result']['contentURL']
122
+
123
+ return results
124
+ else
125
+ request("playbackService","initPlayback",[videoobj.location[1..-1]])['result']['contentURL']
126
+ end
127
+ rescue NoMethodError
128
+ raise RuntimeError, "This video does not exist"
129
+ end
130
+ end
131
+
132
+
133
+ def getConversionLocation()
134
+ res = convertVideoRequest("conversionService","getConversionLocations",nil)
135
+ return res
136
+ end
137
+
138
+
139
+ def convertVideoRequest(service,method,params)
140
+ #print "PARAMS:\n"
141
+ #params[0].each {|key, value| print " #{key} = #{value}\n"}
142
+
143
+ avrequest = {
144
+ "requestURL" => @endpoint.to_s,
145
+ "clientVersion" =>240,
146
+ "serviceName" => service,
147
+ "methodName" => method,
148
+ "clientIdentifier" => "5e8ddc669b2098b0e3dcae5aa1d19338e517544f",
149
+ "parameters" =>params
150
+ }
151
+ avrequest['passwordDigest'] = @passworddigest if not @passworddigest.nil?
152
+
153
+ print "AVREQUEST:\n"
154
+ avrequest.each {|key, value| print " #{key} = #{value}\n"}
155
+
156
+ @req.body = AvMap::Hash.new("air.connect.Request", avrequest).to_avmap
157
+
158
+ print "@REQ.BODY:\n"
159
+ @req.body.each {|key, value| print " #{key} = #{value}\n"}
160
+
161
+ @http.start(@endpoint.host,@endpoint.port) do |http|
162
+ res = http.request(@req)
163
+ parse = AvMap.parse StringIO.new(res.body)
164
+ return parse
165
+ end
166
+ end
167
+
168
+ def convertVideo(videoobj)
169
+ raise NoMethodError, "Please pass a VideoObject" if not videoobj.is_a? VideoObject
170
+
171
+ begin
172
+ # don't remove call to details, if you do convertVideo will ot work
173
+ details = videoobj.details
174
+ location = getConversionLocation()
175
+ print "location = #{location}\n"
176
+ cs = conversion_settings(videoobj)
177
+
178
+
179
+ metaData = AvMap::Hash.new("metaData",{
180
+ "device"=>"iPhone",
181
+ "clientVersion"=>"2.4.1"
182
+ })
183
+
184
+ cs['metaData'] = metaData
185
+
186
+ print "metaData = ",cs['metaData'].each {|key, value| print "#{key} = #{value}\n"},"\n"
187
+ print "cs = #{cs}\n"
188
+
189
+ #sleep 2
190
+ result = convertVideoRequest("conversionService","convertItem",[cs])
191
+ print "result = #{result}\n"
192
+ return result
193
+ rescue NoMethodError
194
+ raise RuntimeError, "Not able to convert video"
195
+ end
196
+ end
197
+
198
+ def get_pin(p)
199
+ begin
200
+ pin = Array.new()
201
+ pin.push(p)
202
+ pin_request("trackerService","getServerState",pin)['result']
203
+ rescue NoMethodError
204
+ raise NoMethodError, "Could not get pin for server\n"
205
+ rescue => ex
206
+ raise ex,"Some sort of error\n"
207
+ end
208
+ end
209
+
210
+ def get_details(items)
211
+ items = [items] if !items.is_a? Array
212
+ items.collect! do |item|
213
+ case item
214
+ when VideoObject
215
+ item.location[1..-1]
216
+ when String
217
+ item
218
+ end
219
+ end.compact!
220
+
221
+ request("browseService","getItemsWithDetail",[items])['result'][0]
222
+ end
223
+
224
+ # Searches the current directory for items matching the given regular expression
225
+ def search(re_string,dir=".")
226
+ # Get the directory we're searching
227
+ dir = File.expand_path((dir.is_a? FolderObject) ? dir.location : dir,@current_dir)
228
+ ls(dir).select {|item| item.name =~ %r{#{re_string}}}
229
+ end
230
+
231
+ # Returns the path to the current directory
232
+ def pwd
233
+ @current_dir
234
+ end
235
+ alias :getcwd :pwd
236
+
237
+ def inspect
238
+ "<AirVideo Connection: #{@endpoint.host}:#{@endpoint.port}>"
239
+ end
240
+
241
+ #private
242
+ def conversion_settings(videoobj)
243
+ video = videoobj.video_stream
244
+ scaling = [video['width'] / @max_width, video['height'] / @max_height]
245
+
246
+ if scaling.max > 1.0
247
+ video['width'] = (video['width'] / scaling.max).to_i
248
+ video['height'] = (video['height'] / scaling.max).to_i
249
+ end
250
+
251
+ # TODO: fill these in correctly
252
+ AvMap::Hash.new("air.video.ConversionRequest", {
253
+ "resolutionWidth"=>video['width'],
254
+ "resolutionHeight"=>video['height'],
255
+ "cropLeft"=>0,
256
+ "cropRight"=>0,
257
+ "cropTop"=>0,
258
+ "cropBottom"=>0,
259
+ "itemId" => videoobj.location[1..-1],
260
+ "offset"=>0.0,
261
+ "quality"=>0.699999988079071,
262
+ "videoStream"=>0,#video['index'],
263
+ "audioStream"=>1,#videoobj.audio_stream['index'],
264
+ "subtitleInfo"=>nil,
265
+ "audioBoost"=>0.0,
266
+ "allowedBitratesLocal"=> AirVideo::AvMap::BitrateList["1536"],
267
+ "allowedBitratesRemote"=> AirVideo::AvMap::BitrateList["384"]
268
+ })
269
+ end
270
+
271
+ def pin_request(service,method,params)
272
+ server = "inmethod.com"
273
+ port = "1112"
274
+ pin_endpoint = URI.parse "http://#{server}:#{port}/service"
275
+
276
+ avrequest = {
277
+ "requestURL" => pin_endpoint.to_s,
278
+ "clientVersion" =>100,
279
+ "serviceName" => service,
280
+ "methodName" => method,
281
+ "parameters" => params,
282
+ #"clientIdentifier" => "89eae483355719f119d698e8d11e8b356525ecfb",
283
+ "clientIdentifier" => "5e8ddc669b2098b0e3dcae5aa1d19338e517544f",
284
+ "passwordDigest" => nil
285
+ }
286
+
287
+ req = Net::HTTP::Post.new(pin_endpoint.path)
288
+ req['User-Agent'] = 'AirVideo/2.4.1 CFNetwork/485.10.2 Darwin/10.3.1'
289
+ req['Accept'] = '*/*'
290
+ req['Accept-Language'] = 'en-us'
291
+ req['Accept-Encoding'] = 'gzip, deflate'
292
+ req['Connection'] = 'keep-alive'
293
+
294
+ req.body = AvMap::Hash.new("air.connect.Request", avrequest).to_avmap
295
+
296
+ http = Net::HTTP
297
+
298
+ http.start(pin_endpoint.host, pin_endpoint.port) do |http|
299
+ res = http.request(req)
300
+ AvMap.parse StringIO.new(res.body)
301
+ end
302
+ end
303
+
304
+ def request(service,method,params)
305
+ avrequest = {
306
+ "requestURL" => @endpoint.to_s,
307
+ "clientVersion" =>240,
308
+ "serviceName" => service,
309
+ "methodName" => method,
310
+ # TODO: Figure out what this is!
311
+ #"clientIdentifier" => "89eae483355719f119d698e8d11e8b356525ecfb",
312
+ "clientIdentifier" => "5e8ddc669b2098b0e3dcae5aa1d19338e517544f",
313
+ "parameters" =>params
314
+ }
315
+ avrequest['passwordDigest'] = @passworddigest if not @passworddigest.nil?
316
+
317
+ @req.body = AvMap::Hash.new("air.connect.Request", avrequest).to_avmap
318
+
319
+ @http.start(@endpoint.host,@endpoint.port) do |http|
320
+ res = http.request(@req)
321
+ parse = AvMap.parse StringIO.new(res.body)
322
+ return parse
323
+ end
324
+ end
325
+
326
+ # Represents a folder as listed by the AirVideo server.
327
+ #
328
+ # Has helper functions like #cd which will move the current directory of the originating AirVideo::Client instance to this folder.
329
+ class FolderObject
330
+ attr_reader :name, :location
331
+ Helpers = [:cd, :ls, :search]
332
+
333
+ # Shouldn't be used outside of the AirVideo module
334
+ def initialize(server,name,location) # :nodoc:
335
+ @server = server
336
+ @name = name
337
+ @location = "/"+location
338
+ end
339
+
340
+ def inspect
341
+ "<Folder: #{(name.nil?) ? "/Unknown/" : name}>"
342
+ end
343
+
344
+ def cd
345
+ @server.cd(self)
346
+ end
347
+
348
+ def ls
349
+ @server.ls(self)
350
+ end
351
+
352
+ def search(re_string)
353
+ @server.search(re_string,self)
354
+ end
355
+ end
356
+
357
+ # Represents a video file as listed by the AirVideo server.
358
+ #
359
+ # Has helper functions like #url and #live_url which give the video playback URLs of this video, as produced by the originating AirVideo::Client instance's AirVideo::Client.get_url method.
360
+ class VideoObject
361
+ attr_reader :name, :location, :details, :streams
362
+ attr_accessor :audio_stream, :video_stream
363
+
364
+ # Shouldn't be used outside of the AirVideo module
365
+ def initialize(server,name,location,detail = nil) # :nodoc:
366
+ @server = server
367
+ @name = name
368
+ @location = "/"+location
369
+ @details = detail # nil implies the details haven't been loaded
370
+ # These are the defaults, all videos *should* have these.
371
+ @video_stream = {'index' => 1}
372
+ @audio_stream = {'index' => 0}
373
+ details if !@details.nil?
374
+ end
375
+
376
+ def details
377
+ #print "####### In airvideo details #######\n"
378
+ @details = @server.get_details(self)
379
+ #print "########### @details = #{@details}\n"
380
+ ######dump attributes of hash
381
+ #@details.each {|key, value| puts "#{key} = #{value}\n"}
382
+ if !@details.nil?
383
+ @streams = {'video' => [],'audio' => [],'unknown' => []}
384
+ #print "@streams = #{@streams}\n"
385
+ #print "@details['detail'] = ",@details['detail'],"\n"
386
+ @details['detail']['streams'].each do |stream|
387
+ @streams[case
388
+ when 0
389
+ "video"
390
+ when 1
391
+ "audio"
392
+ else
393
+ "unknown"
394
+ end
395
+ ]
396
+ end
397
+
398
+ @audio_stream = @details['detail']['streams'][0]
399
+ #print "@audio_stream = #{@audio_stream}\n"
400
+ @video_stream = @details['detail']['streams'][0]
401
+ #print "@video_stream = #{@video_stream}\n"
402
+ end
403
+ @details
404
+ end
405
+
406
+ # Checks to see if this video has that audio stream index, then changes internal settings so that live conversions will use this stream.
407
+ def audio_stream=(stream_hash_or_index)
408
+ index = stream_hash_or_index['index'] rescue stream_hash_or_index
409
+ get_details if @details.nil?
410
+ raise RuntimeError, "Couldn't retrieve video details" if @details.nil?
411
+ raise RuntimeError, "No such audio stream" if @streams['audio'].collect{|stream| stream['index']}.include? index
412
+ @audio_stream = index
413
+ end
414
+
415
+ # Checks to see if this video has that video stream index, then changes internal settings so that live conversions will use this stream.
416
+ def video_stream=(stream_hash_or_index)
417
+ index = stream_hash_or_index['index'] rescue stream_hash_or_index
418
+ get_details if @details.nil?
419
+ raise RuntimeError, "Couldn't retrieve video details" if @details.nil?
420
+ raise RuntimeError, "No such audio stream" if @streams['video'].collect{|stream| stream['index']}.include? index
421
+ @video_stream = index
422
+ end
423
+
424
+ # Gives the URL for direct video playback
425
+ def url
426
+ @server.get_url(self,false)
427
+ end
428
+
429
+ # Gives the URL for live conversion video playback
430
+ def live_url
431
+ @server.get_url(self,true)
432
+ end
433
+
434
+ def inspect
435
+ "<Video: #{name}>"
436
+ end
437
+ end
438
+ end
439
+
440
+ # A two-way parser for AirVideo's communication protocol.
441
+ #
442
+ # s = "Hello World!".to_avmap
443
+ # # => "s\000\000\000\012Hello World!"
444
+ # p AvMap.parse(s)
445
+ # # => "Hello World!"
446
+ #
447
+ module AvMap
448
+ # Expects an IO object. I use either a file IO or a StringIO object here.
449
+ def self.parse(stream)
450
+ @input = stream
451
+ self.read_identifier
452
+ end
453
+
454
+ private
455
+ def self.read_identifier(depth = 0)
456
+ ident = @input.read(1)
457
+ case ident
458
+ when "o" # Hash
459
+ unknown = @input.read(4).unpack("N")[0]
460
+ hash = Hash.new(@input.read(@input.read(4).unpack("N")[0]), {})
461
+ unknown = @input.read(4).unpack("N")[0]
462
+ num_els = @input.read(4).unpack("N")[0]
463
+ #$stderr.puts "#{" "*depth}Hash: #{unknown} // #{num_els} times"
464
+ 1.upto(num_els) do |iter|
465
+ hash_item = @input.read(@input.read(4).unpack("N")[0])
466
+ #$stderr.puts "#{" "*depth}-#{unknown}:#{iter} - #{hash_item}"
467
+ hash[hash_item] = self.read_identifier(depth + 1)
468
+ end
469
+ hash
470
+ when "s" # String
471
+ #$stderr.puts "#{" "*depth}String"
472
+ unknown = @input.read(4).unpack("N")[0]
473
+ @input.read(@input.read(4).unpack("N")[0])
474
+ when "i" # Integer?
475
+ #$stderr.puts "#{" "*depth}Integer"
476
+ @input.read(4).unpack("N")[0]
477
+ when "a","e" # Array
478
+ #$stderr.puts "#{" "*depth}Array"
479
+ unknown = @input.read(4).unpack("N")[0]
480
+ num_els = @input.read(4).unpack("N")[0]
481
+ arr = []
482
+ 1.upto(num_els) do |iter|
483
+ arr.push self.read_identifier(depth + 1)
484
+ end
485
+ arr
486
+ when "n" # nil
487
+ #$stderr.puts "#{" "*depth}Nil"
488
+ nil
489
+ when "f" # Float?
490
+ @input.read(8).unpack('G')[0]
491
+ when "l" # Big Integer
492
+ @input.read(8).unpack("NN").reverse.inject([0,0]){|res,el| [res[0] + (el << (32 * res[1])),res[1] + 1]}[0]
493
+ when "r" # Integer?
494
+ #$stderr.puts "#{" "*depth}R?"
495
+ @input.read(4).unpack("N")[0]
496
+ when "x" # Binary Data
497
+ #$stderr.puts "#{" "*depth}R?"
498
+ unknown = @input.read(4).unpack("N")[0]
499
+ BinaryData.new @input.read(@input.read(4).unpack("N")[0])
500
+ else
501
+ raise NotImplementedError, "I don't know what to do with the '#{ident}' identifier"
502
+ end
503
+ end
504
+
505
+ # Just hack in an addition to the Hash object, we need to be able to give each hash a name to make everything a little simpler.
506
+ class Hash < Hash
507
+ attr_accessor :name
508
+
509
+ # Create a new Hash with a name. Yay!
510
+ def initialize(key,hash)
511
+ super()
512
+ @name = key
513
+ merge! hash
514
+ self
515
+ end
516
+
517
+ def inspect
518
+ @name+super
519
+ end
520
+ end
521
+
522
+
523
+ # A simple container for Binary Data. With AirVideo this is used to hold thumbnail JPG data.
524
+ class BinaryData
525
+ attr_reader :data
526
+
527
+ # Not really useful outside of the AvMap module
528
+ def initialize(data) # :nodoc:
529
+ @data = data
530
+ end
531
+
532
+ # Writes the data to the given filename
533
+ #
534
+ # TODO: LibMagic to detect what the extension should be?
535
+ def write_to(filename)
536
+ open(filename,"w") do |f|
537
+ f.write @data
538
+ end
539
+ end
540
+
541
+ def length
542
+ @data.length
543
+ end
544
+
545
+ def inspect
546
+ "<Data: #{@data.length} bytes>"
547
+ end
548
+ end
549
+
550
+ # In order to demand that an array be classified with the 'e' header, rather than the 'a' header, use a BitrateList array.
551
+ #
552
+ # I'm not certain that this is the only place the 'e' arrays are used, but they're definitely used for Bitrate Lists. So this is called the BitrateList. Yup.
553
+ class BitrateList < Array; end
554
+
555
+ end
556
+ end
557
+
558
+ # Add the #to_avmap method for ease of use in the Client
559
+ class Object
560
+ # Will convert an object into an AirVideo map, if the object and it's contents are supported
561
+ def to_avmap(reset_counter = true)
562
+ @@to_avmap_counter = 0 if reset_counter
563
+ case self
564
+ when Array
565
+ letter = (self.is_a? AirVideo::AvMap::BitrateList) ? "e" : "a"
566
+ self.push nil if self.length == 0 # Must have at least one entry in the hash, I think
567
+ "#{letter}#{[(@@to_avmap_counter += 1) - 1].pack("N")}#{[self.length].pack("N")}"+self.collect do |item|
568
+ item.to_avmap(false)
569
+ end.join
570
+ when AirVideo::AvMap::Hash
571
+ version = case self.name
572
+ when "air.video.ConversionRequest"
573
+ 240
574
+ when "air.video.BrowseRequest"
575
+ 240
576
+ else
577
+ 1
578
+ end
579
+
580
+ # Implement metaData:
581
+ # :length :type
582
+ # "\000\000\000\010"+"metaData"+
583
+ # :data :counter :number of items
584
+ # "d"+"\000\000\000\015"+"\000\000\000\002"+
585
+ # :string :counter :length of str :string value
586
+ # "s"+"\000\000\000\016"+"\000\000\000\006"+"device"+
587
+ # "s"+"\000\000\000\017"+"\000\000\000\006"+"iPhone"+
588
+ # "s"+"\000\000\000\020"+"\000\000\000\015"+"clientVersion"+
589
+ # "s"+"\000\000\000\021"+"\000\000\000\005"+"2.4.1"
590
+ # :nil:integer value
591
+ # "ni"+"\000\000\000\000" 'nili0000'
592
+
593
+ #iphone (working):
594
+ # 00 00 00 08 6d ........ 384....m
595
+ #06D2 65 74 61 44 61 74 61 64 00 00 00 0d 00 00 00 02 etaDatad ........
596
+ #06E2 73 00 00 00 0e 00 00 00 06 64 65 76 69 63 65 73 s....... .devices
597
+ #06F2 00 00 00 0f 00 00 00 06 69 50 68 6f 6e 65 73 00 ........ iPhones.
598
+ #0702 00 00 10 00 00 00 0d 63 6c 69 65 6e 74 56 65 72 .......c lientVer
599
+ #0712 73 69 6f 6e 73 00 00 00 11 00 00 00 05 32 2e 34 sions... .....2.4
600
+ #0722 2e 31 6e 69 00 00 00 00 .1ni....
601
+
602
+
603
+ #mine (not working):
604
+ #0319 2e 5b 56 54 56 5d 2e 6d 70 34 00 00 00 08 6d 65 .[VTV].m p4....me
605
+ #0329 74 61 44 61 74 61 64 00 00 00 0a 00 00 00 02 73 taDatad. .......s
606
+ #0339 00 00 00 0b 00 00 00 06 64 65 76 69 63 65 73 00 ........ devices.
607
+ #0349 00 00 0c 00 00 00 06 69 50 68 6f 6e 65 73 00 00 .......i Phones..
608
+ #0359 00 0d 00 00 00 0d 63 6c 69 65 6e 74 56 65 72 73 ......cl ientVers
609
+ #0369 69 6f 6e 73 00 00 00 0e 00 00 00 05 32 2e 34 2e ions.... ....2.4.
610
+ #0379 31 00 00 00 0f 72 65 73 6f 6c 75 74 69 6f 6e 57 1....
611
+
612
+ case self.name
613
+ when "metaData"
614
+ "d#{[(@@to_avmap_counter += 1) - 1].pack("N")}#{[self.count].pack("N")}"+self.to_a.collect do |key,val|
615
+ "s#{[(@@to_avmap_counter += 1) - 1].pack("N")}#{[key.length].pack("N")}#{key}"+val.to_avmap(false)
616
+ end.join # need to add "ni"+"\000\000\000\000 = nil0"
617
+ #(nil+0).to_avmap(true)
618
+ else
619
+ "o#{[(@@to_avmap_counter += 1) - 1].pack("N")}#{[self.name.length].pack("N")}#{self.name}#{[version].pack("N")}#{[self.length].pack("N")}"+self.to_a.collect do |key,val|
620
+ "#{[key.length].pack("N")}#{key}"+val.to_avmap(false)
621
+ end.join
622
+ end
623
+
624
+ when AirVideo::AvMap::BinaryData
625
+ "x#{[(@@to_avmap_counter += 1) - 1].pack("N")}#{[self.length].pack("N")}#{self.data}"
626
+ when String
627
+ # s:count:length:value
628
+ "s#{[(@@to_avmap_counter += 1) - 1].pack("N")}#{[self.length].pack("N")}#{self}"
629
+ #when Hash
630
+ #
631
+ when NilClass
632
+ "n"
633
+ when Integer
634
+ # i:integer N = Long, network (big-endian) byte order
635
+ "i#{[self].pack('N')}"
636
+ when Float # unsure of this is what this is meant to be
637
+ "f#{[self].pack('G')}"
638
+ else
639
+ raise NotImplementedError, "Can't turn a #{self.class} into an avmap"
640
+ end
641
+ end
642
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: airvideo-ng
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 9
9
+ version: 0.0.9
10
+ platform: ruby
11
+ authors:
12
+ - Kalw forked from JP Hastings
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2012-03-26 00:00:00 +02:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: "Communicate with an AirVideo server, even through a proxy: Retrieve the streaming URLs for your videos."
22
+ email: regis.despres+rubygem@gmail.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - LICENSE
29
+ - README.rdoc
30
+ files:
31
+ - LICENSE
32
+ - README.rdoc
33
+ - Rakefile
34
+ - VERSION
35
+ - lib/airvideo.rb
36
+ has_rdoc: true
37
+ homepage: http://github.com/kalw/AirVideo
38
+ licenses: []
39
+
40
+ post_install_message:
41
+ rdoc_options: []
42
+
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ segments:
50
+ - 0
51
+ version: "0"
52
+ required_rubygems_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ requirements: []
60
+
61
+ rubyforge_project:
62
+ rubygems_version: 1.3.6
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: Allows communication with an AirVideo server
66
+ test_files: []
67
+