airplay 0.2.9 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (82) hide show
  1. data/Gemfile +5 -0
  2. data/Gemfile.lock +68 -0
  3. data/HUGS +15 -0
  4. data/LICENSE +20 -0
  5. data/README.md +50 -72
  6. data/Rakefile +6 -9
  7. data/SPEC.md +34 -0
  8. data/airplay.gemspec +28 -16
  9. data/bin/air +27 -0
  10. data/examples/demo.rb +35 -0
  11. data/examples/image.rb +9 -0
  12. data/examples/video.rb +24 -0
  13. data/lib/airplay.rb +38 -17
  14. data/lib/airplay/browser.rb +68 -0
  15. data/lib/airplay/cli.rb +104 -0
  16. data/lib/airplay/configuration.rb +29 -0
  17. data/lib/airplay/connection.rb +114 -0
  18. data/lib/airplay/connection/authentication.rb +37 -0
  19. data/lib/airplay/connection/persistent.rb +44 -0
  20. data/lib/airplay/device.rb +81 -0
  21. data/lib/airplay/device/features.rb +43 -0
  22. data/lib/airplay/device/info.rb +22 -0
  23. data/lib/airplay/devices.rb +46 -0
  24. data/lib/airplay/logger.rb +20 -0
  25. data/lib/airplay/playable.rb +24 -0
  26. data/lib/airplay/protocol.rb +9 -72
  27. data/lib/airplay/protocol/app.rb +52 -0
  28. data/lib/airplay/protocol/message.rb +8 -0
  29. data/lib/airplay/protocol/playback_info.rb +49 -0
  30. data/lib/airplay/protocol/player.rb +170 -0
  31. data/lib/airplay/protocol/reverse.rb +69 -0
  32. data/lib/airplay/protocol/slideshow.rb +66 -0
  33. data/lib/airplay/protocol/timers.rb +25 -0
  34. data/lib/airplay/protocol/viewer.rb +56 -0
  35. data/lib/airplay/structure.rb +7 -0
  36. data/lib/airplay/viewable.rb +16 -0
  37. data/test/fixtures/cassettes/airplay/listing_slideshow_features.yml +201 -0
  38. data/test/fixtures/cassettes/airplay/play_an_entire_video.yml +100 -0
  39. data/test/fixtures/cassettes/airplay/sending_a_video.yml +71 -0
  40. data/test/fixtures/cassettes/airplay/sending_an_image.yml +26439 -0
  41. data/test/fixtures/cassettes/airplay/stop_any_transmission.yml +8851 -0
  42. data/test/fixtures/files/logo.png +0 -0
  43. data/test/fixtures/files/transition_0.png +0 -0
  44. data/test/fixtures/files/transition_1.png +0 -0
  45. data/test/fixtures/files/transition_2.png +0 -0
  46. data/test/fixtures/files/transition_3.png +0 -0
  47. data/test/integration/discovery_test.rb +13 -0
  48. data/test/integration/features_test.rb +14 -0
  49. data/test/integration/send_media_test.rb +37 -0
  50. data/test/integration/slideshow_test.rb +26 -0
  51. data/test/test_helper.rb +42 -0
  52. data/test/unit/node_test.rb +17 -0
  53. data/test/unit/protocol_test.rb +44 -0
  54. metadata +247 -77
  55. data/.gitignore +0 -2
  56. data/.travis.yml +0 -6
  57. data/examples/jobs.jpg +0 -0
  58. data/examples/send_image.rb +0 -5
  59. data/examples/send_video.rb +0 -7
  60. data/lib/airplay/client.rb +0 -60
  61. data/lib/airplay/protocol/image.rb +0 -41
  62. data/lib/airplay/protocol/media.rb +0 -64
  63. data/lib/airplay/protocol/scrub.rb +0 -37
  64. data/lib/airplay/server.rb +0 -2
  65. data/lib/airplay/server/browser.rb +0 -35
  66. data/lib/airplay/server/node.rb +0 -7
  67. data/test/authentication.rb +0 -13
  68. data/test/discovery.rb +0 -27
  69. data/test/fixtures/cassettes/airplay/authenticate_all_the_things_.yml +0 -139
  70. data/test/fixtures/cassettes/airplay/control_a_video_being_played_in_apple_tv.yml +0 -242
  71. data/test/fixtures/cassettes/airplay/get_current_scrub_from_apple_tv.yml +0 -69
  72. data/test/fixtures/cassettes/airplay/go_to_a_given_position_in_the_video.yml +0 -157
  73. data/test/fixtures/cassettes/airplay/send_audio_to_apple_tv.yml +0 -33
  74. data/test/fixtures/cassettes/airplay/send_image_to_apple_tv.yml +0 -661
  75. data/test/fixtures/cassettes/airplay/send_image_to_apple_tv_with_effects.yml +0 -591
  76. data/test/fixtures/cassettes/airplay/send_video_to_apple_tv.yml +0 -33
  77. data/test/fixtures/image.gif +0 -0
  78. data/test/fixtures/image2.gif +0 -0
  79. data/test/helper.rb +0 -31
  80. data/test/images.rb +0 -31
  81. data/test/media.rb +0 -47
  82. data/test/scrub.rb +0 -31
data/examples/image.rb ADDED
@@ -0,0 +1,9 @@
1
+ require "airplay"
2
+
3
+ Airplay.use "corax", "corax"
4
+ Airplay.configure do |c|
5
+ c.log_level = "debug"
6
+ end
7
+
8
+ Airplay.view("http://fitdeck.com/Portals/24254/images/Sunrise.jpg")
9
+ sleep 4
data/examples/video.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "airplay"
2
+
3
+ Airplay.configure do |c|
4
+ # c.log_level = "debug"
5
+ end
6
+
7
+ video = "http://trailers.apple.com/movies/marvel/ironman3/ironman3-tlr1-m4mb0_h1080p.mov"
8
+
9
+ Airplay.play video
10
+ puts "Starts playing: #{video}"
11
+ Airplay.player.progress -> info {
12
+ if info["rate"]
13
+ if info["readyToPlay"]
14
+ total = info["duration"]
15
+ current = info["position"]
16
+ percent = (current*100/total).round
17
+
18
+ print "\r|#{"=" * percent}> #{percent}%"
19
+ end
20
+ end
21
+ }
22
+
23
+ Airplay.player.wait
24
+ puts "Video playback finished!"
data/lib/airplay.rb CHANGED
@@ -1,17 +1,38 @@
1
- require 'dnssd'
2
- require 'net/http'
3
- require 'net/http/persistent'
4
- require 'net/http/digest_auth'
5
- require 'uri'
6
-
7
- module Airplay end
8
- require 'airplay/server'
9
- require 'airplay/server/browser'
10
- require 'airplay/server/node'
11
-
12
- require 'airplay/protocol'
13
- require 'airplay/protocol/image'
14
- require 'airplay/protocol/media'
15
- require 'airplay/protocol/scrub'
16
-
17
- require 'airplay/client'
1
+ require "airplay/configuration"
2
+ require "airplay/browser"
3
+
4
+ # Public: Airplay core module
5
+ #
6
+ module Airplay
7
+ class << self
8
+ def configure(&block)
9
+ yield(configuration) if block
10
+ end
11
+
12
+ def browse
13
+ browser.browse
14
+ end
15
+
16
+ # Public: Lists found devices
17
+ #
18
+ def devices
19
+ browse if browser.devices.empty?
20
+ browser.devices
21
+ end
22
+
23
+ def configuration
24
+ @_configuration ||= Configuration.new
25
+ end
26
+
27
+ def [](device_name)
28
+ devices.find_by_name(device_name)
29
+ end
30
+
31
+ private
32
+
33
+ def browser
34
+ @_browser ||= Browser.new
35
+ end
36
+ end
37
+ end
38
+
@@ -0,0 +1,68 @@
1
+ require "dnssd"
2
+ require "timeout"
3
+
4
+ require "airplay/logger"
5
+ require "airplay/devices"
6
+
7
+ module Airplay
8
+ # Public: Browser class to find Airplay-enabled devices in the network
9
+ #
10
+ class Browser
11
+ SEARCH = "_airplay._tcp."
12
+
13
+ def initialize
14
+ @logger = Airplay::Logger.new("airplay::browser")
15
+ end
16
+
17
+ # Public: Browses in the search of devices and adds them to the nodes
18
+ #
19
+ def browse
20
+ timeout(5) do
21
+ DNSSD.browse!(SEARCH) do |node|
22
+ resolve(node)
23
+ break unless node.flags.more_coming?
24
+ end
25
+ end
26
+ end
27
+
28
+ # Public: Access to the node list
29
+ #
30
+ def devices
31
+ @_devices ||= Devices.new
32
+ end
33
+
34
+ private
35
+
36
+ # Private: Resolves a node given a node and a resolver
37
+ #
38
+ # node - The given node
39
+ # resolver - The DNSSD::Server that is resolving nodes
40
+ #
41
+ # Returns if there are more nodes coming
42
+ #
43
+ def node_resolver(node, resolved)
44
+ info = Socket.getaddrinfo(resolved.target, nil, Socket::AF_INET)
45
+ ip = info[0][2]
46
+
47
+ airplay_device = Device.new(
48
+ name: node.name.gsub(/\u00a0/, ' '),
49
+ address: "#{ip}:#{resolved.port}",
50
+ )
51
+
52
+ devices << airplay_device
53
+
54
+ resolved.flags.more_coming?
55
+ end
56
+
57
+ # Private: Resolves the node information given a node
58
+ #
59
+ # node - The node from the DNSSD browsing
60
+ #
61
+ def resolve(node)
62
+ resolver = DNSSD::Service.new
63
+ resolver.resolve(node) do |resolved|
64
+ break unless node_resolver(node, resolved)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,104 @@
1
+ require "airplay"
2
+ require "ruby-progressbar"
3
+
4
+ module Airplay
5
+ module CLI
6
+ class << self
7
+ def list
8
+ Airplay.devices.each do |device|
9
+ puts <<-EOS.gsub(/^\s{12}/,'')
10
+ * #{device.name} (#{device.info.model} running #{device.info.os_version})
11
+ ip: #{device.ip}
12
+ resolution: #{device.info.resolution}
13
+
14
+ EOS
15
+ end
16
+ end
17
+
18
+ def play(video, options)
19
+ device = options[:device]
20
+ player = device.play(video)
21
+ puts "Playing #{video}"
22
+ bar = ProgressBar.create(
23
+ title: device.name,
24
+ format: "%a [%B] %p%% %t"
25
+ )
26
+
27
+ player.progress -> playback {
28
+ bar.progress = playback.percent
29
+ }
30
+
31
+ player.wait
32
+ end
33
+
34
+ def view(file_or_dir, options)
35
+ device = options[:device]
36
+ wait = options[:wait]
37
+
38
+ if File.directory?(file_or_dir)
39
+ files = Dir.glob("#{file_or_dir}/*")
40
+
41
+ if options[:interactive]
42
+ view_interactive(files)
43
+ else
44
+ view_slideshow(files)
45
+ end
46
+ else
47
+ view_image(device, file_or_dir)
48
+ sleep
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def view_interactive(files)
55
+ numbers = Array(0...files.count)
56
+ transition = "None"
57
+
58
+ i = 0
59
+ loop do
60
+ view_image(device, files[i], transition)
61
+
62
+ case read_char
63
+ # Right Arrow
64
+ when "\e[C"
65
+ i = i + 1 > numbers.count - 1 ? 0 : i + 1
66
+ transition = "SlideLeft"
67
+ when "\e[D"
68
+ i = i - 1 < 0 ? numbers.count - 1 : i - 1
69
+ transition = "SlideRight"
70
+ else
71
+ break
72
+ end
73
+ end
74
+ end
75
+
76
+ def view_slideshow(files)
77
+ files.each do |file|
78
+ view_image(device, file)
79
+ sleep wait
80
+ end
81
+ end
82
+
83
+ def read_char
84
+ STDIN.echo = false
85
+ STDIN.raw!
86
+
87
+ input = STDIN.getc.chr
88
+ if input == "\e" then
89
+ input << STDIN.read_nonblock(3) rescue nil
90
+ input << STDIN.read_nonblock(2) rescue nil
91
+ end
92
+ ensure
93
+ STDIN.echo = true
94
+ STDIN.cooked!
95
+
96
+ return input
97
+ end
98
+
99
+ def view_image(device, image, transition = "SlideLeft")
100
+ device.view(image, transition: transition)
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,29 @@
1
+ require 'log4r/config'
2
+
3
+ module Airplay
4
+ # Public: Handles the Airplay configuration
5
+ #
6
+ class Configuration
7
+ attr_accessor :log_level, :output
8
+
9
+ def initialize
10
+ Log4r.define_levels(*Log4r::Log4rConfig::LogLevels)
11
+
12
+ @log_level = Log4r::WARN
13
+ @output = Log4r::Outputter.stdout
14
+ end
15
+
16
+ # Public: Loads the configuration into the affected parts
17
+ #
18
+ def load
19
+ level = if @log_level.is_a?(Fixnum)
20
+ @log_level
21
+ else
22
+ Log4r.const_get(@log_level.upcase)
23
+ end
24
+
25
+ Log4r::Logger.root.add @output
26
+ Log4r::Logger.root.level = level
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,114 @@
1
+ require "celluloid"
2
+ require "airplay/connection/persistent"
3
+ require "airplay/connection/authentication"
4
+
5
+ module Airplay
6
+ # Public: The class that handles all the outgoing basic HTTP connections
7
+ #
8
+ class Connection
9
+ Response = Struct.new(:connection, :response)
10
+
11
+ attr_accessor :reverse, :events, :persistent
12
+
13
+ include Celluloid
14
+
15
+ def initialize(device, options = {})
16
+ @device = device
17
+ @options = options
18
+ @logger = Airplay::Logger.new("airplay::connection")
19
+ end
20
+
21
+ def persistent
22
+ address = @options[:address] || "http://#{@device.address}"
23
+ @_persistent ||= Airplay::Connection::Persistent.new(address, @options)
24
+ end
25
+
26
+ def start_reverse_connection
27
+ @reverse = Airplay::Protocol::Reverse.new(Airplay.active)
28
+ @reverse.async.connect
29
+ end
30
+
31
+ def close
32
+ persistent.close
33
+ @_persistent = nil
34
+ end
35
+
36
+ # Public: Executes a POST to a resource
37
+ #
38
+ # resource - The resource on the currently active Device
39
+ # body - The body of the action
40
+ # headers - Optional headers
41
+ #
42
+ # Returns a response object
43
+ #
44
+ def post(resource, body = "", headers = {})
45
+ @logger.info("POST #{resource} with #{body.bytesize} bytes")
46
+ request = Net::HTTP::Post.new(resource)
47
+ request.body = body
48
+
49
+ send_request(request, headers)
50
+ end
51
+
52
+ # Public: Executes a PUT to a resource
53
+ #
54
+ # resource - The resource on the currently active Device
55
+ # body - The body of the action
56
+ # headers - Optional headers
57
+ #
58
+ # Returns a response object
59
+ #
60
+ def put(resource, body = "", headers = {})
61
+ @logger.info("PUT #{resource} with #{body.bytesize} bytes")
62
+ request = Net::HTTP::Put.new(resource)
63
+ request.body = body
64
+
65
+ send_request(request, headers)
66
+ end
67
+
68
+ # Public: Executes a GET to a resource
69
+ #
70
+ # resource - The resource on the currently active Device
71
+ # headers - Optional headers
72
+ #
73
+ # Returns a response object
74
+ #
75
+ def get(resource, headers = {})
76
+ @logger.info("GET #{resource}")
77
+ request = Net::HTTP::Get.new(resource)
78
+
79
+ send_request(request, headers)
80
+ end
81
+
82
+ private
83
+
84
+ # Private: The defaults connection headers
85
+ #
86
+ def default_headers
87
+ {
88
+ "User-Agent" => "MediaControl/1.0",
89
+ "X-Apple-Session-Id" => persistent.session
90
+ }
91
+ end
92
+
93
+ # Private: Sends a request to the Device
94
+ #
95
+ # request - The Request object
96
+ # headers - The headers of the request
97
+ #
98
+ # Returns a response object
99
+ #
100
+ def send_request(request, headers)
101
+ request.initialize_http_header(default_headers.merge(headers))
102
+
103
+ if @device.password?
104
+ authentication = Airplay::Connection::Authentication.new(persistent)
105
+ request = authentication.sign(request)
106
+ end
107
+
108
+ @logger.info("Sending request to #{@device.address}")
109
+ response = persistent.request(request)
110
+
111
+ Airplay::Connection::Response.new(persistent, response)
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,37 @@
1
+ require "net/http/digest_auth"
2
+
3
+ module Airplay
4
+ class Connection
5
+ class Authentication < Struct.new(:handler)
6
+ def sign(request)
7
+ auth_token = authenticate(request)
8
+ request.add_field('Authorization', auth_token) if auth_token
9
+ request
10
+ end
11
+
12
+ private
13
+
14
+ def uri(request)
15
+ server = Airplay.active
16
+ path = "http://#{server.address}#{request.path}"
17
+ uri = URI.parse(path)
18
+ uri.user = "Airplay"
19
+ uri.password = server.password
20
+
21
+ uri
22
+ end
23
+
24
+ def authenticate(request)
25
+ response = handler.request(request)
26
+
27
+ auth = response["www-authenticate"] || response["WWW-Authenticate"]
28
+ digest_authentication(request, auth) if auth
29
+ end
30
+
31
+ def digest_authentication(request, auth)
32
+ digest = Net::HTTP::DigestAuth.new
33
+ digest.auth_header(uri(request), auth, request.method)
34
+ end
35
+ end
36
+ end
37
+ end