airplay 0.2.6 → 0.2.8
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +2 -1
- data/README.md +1 -1
- data/airplay.gemspec +9 -9
- data/lib/airplay/client.rb +7 -3
- data/lib/airplay/protocol.rb +45 -25
- data/lib/airplay/protocol/image.rb +7 -8
- data/lib/airplay/server/browser.rb +5 -5
- data/test/authentication.rb +1 -1
- data/test/discovery.rb +1 -1
- data/test/fixtures/cassettes/airplay/authenticate_all_the_things_.yml +105 -101
- data/test/fixtures/cassettes/airplay/control_a_video_being_played_in_apple_tv.yml +190 -169
- data/test/fixtures/cassettes/airplay/get_current_scrub_from_apple_tv.yml +51 -47
- data/test/fixtures/cassettes/airplay/go_to_a_given_position_in_the_video.yml +124 -108
- data/test/fixtures/cassettes/airplay/send_audio_to_apple_tv.yml +26 -22
- data/test/fixtures/cassettes/airplay/send_image_to_apple_tv.yml +628 -606
- data/test/fixtures/cassettes/airplay/send_image_to_apple_tv_with_effects.yml +563 -545
- data/test/fixtures/cassettes/airplay/send_video_to_apple_tv.yml +26 -22
- data/test/helper.rb +6 -3
- data/test/images.rb +2 -3
- data/test/media.rb +3 -3
- data/test/scrub.rb +2 -2
- metadata +48 -32
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
![Travis](https://secure.travis-ci.org/elcuervo/airplay.png)
|
4
4
|
|
5
|
-
![Ruby Airplay](
|
5
|
+
![Ruby Airplay](http://elcuervo.co/images/posts/airplay/ruby_airplay.png?1)
|
6
6
|
|
7
7
|
A client (and someday a server) of the superfancy http content stream technique
|
8
8
|
that Apple uses in its products.
|
data/airplay.gemspec
CHANGED
@@ -1,20 +1,20 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = "airplay"
|
3
|
-
s.version = "0.2.
|
3
|
+
s.version = "0.2.8"
|
4
4
|
s.summary = "Airplay client"
|
5
5
|
s.description = "Send image/video to an airplay enabled device"
|
6
6
|
s.authors = ["elcuervo"]
|
7
7
|
s.email = ["yo@brunoaguirre.com"]
|
8
8
|
s.homepage = "http://github.com/elcuervo/airplay"
|
9
9
|
s.files = `git ls-files`.split("\n")
|
10
|
-
s.test_files = `git ls-files
|
10
|
+
s.test_files = `git ls-files test`.split("\n")
|
11
11
|
|
12
|
-
s.add_dependency("dnssd")
|
13
|
-
s.add_dependency("net-http-persistent")
|
14
|
-
s.add_dependency("net-http-digest_auth")
|
12
|
+
s.add_dependency("dnssd", "~> 2.0")
|
13
|
+
s.add_dependency("net-http-persistent", "~> 2.5")
|
14
|
+
s.add_dependency("net-http-digest_auth", "~> 1.2")
|
15
15
|
|
16
|
-
s.add_development_dependency("cutest")
|
17
|
-
s.add_development_dependency("capybara")
|
18
|
-
s.add_development_dependency("fakeweb")
|
19
|
-
s.add_development_dependency("vcr")
|
16
|
+
s.add_development_dependency("cutest", "~> 1.1")
|
17
|
+
s.add_development_dependency("capybara", "~> 1.0")
|
18
|
+
s.add_development_dependency("fakeweb", "~> 1.3")
|
19
|
+
s.add_development_dependency("vcr", "~> 2.0")
|
20
20
|
end
|
data/lib/airplay/client.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
class Airplay::Client
|
2
|
-
attr_reader :servers, :
|
2
|
+
attr_reader :servers, :active, :password
|
3
3
|
|
4
4
|
def initialize(server = false, server_browser = Airplay::Server::Browser)
|
5
5
|
@server_browser = server_browser
|
@@ -8,7 +8,11 @@ class Airplay::Client
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def use(server)
|
11
|
-
@
|
11
|
+
@active = if server.is_a?(Airplay::Server::Node)
|
12
|
+
server
|
13
|
+
else
|
14
|
+
@server_browser.find_by_name(server)
|
15
|
+
end
|
12
16
|
end
|
13
17
|
|
14
18
|
def password(password)
|
@@ -24,7 +28,7 @@ class Airplay::Client
|
|
24
28
|
end
|
25
29
|
|
26
30
|
def handler
|
27
|
-
Airplay::Protocol.new(@
|
31
|
+
@_handler ||= Airplay::Protocol.new(@active.ip, @active.port, @password)
|
28
32
|
end
|
29
33
|
|
30
34
|
def send_image(image, transition = :none)
|
data/lib/airplay/protocol.rb
CHANGED
@@ -8,45 +8,65 @@ class Airplay::Protocol
|
|
8
8
|
def initialize(host, port, password)
|
9
9
|
@device = { :host => host, :port => port }
|
10
10
|
@password = password
|
11
|
+
@authentications = {}
|
11
12
|
@http = Net::HTTP::Persistent.new
|
13
|
+
@http.idle_timeout = 900 # until nil works
|
12
14
|
@http.debug_output = $stdout if ENV.has_key?('HTTP_DEBUG')
|
13
15
|
end
|
14
16
|
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
|
17
|
+
def put(resource, body = nil, headers = {})
|
18
|
+
@request = Net::HTTP::Put.new resource
|
19
|
+
@request.body = body
|
20
|
+
@request.initialize_http_header DEFAULT_HEADERS.merge(headers)
|
21
|
+
make_request
|
22
|
+
end
|
19
23
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
24
|
+
def post(resource, body = nil, headers = {})
|
25
|
+
@request = Net::HTTP::Post.new resource
|
26
|
+
@request.body = body
|
27
|
+
@request.initialize_http_header DEFAULT_HEADERS.merge(headers)
|
28
|
+
make_request
|
29
|
+
end
|
30
|
+
|
31
|
+
def get(resource, headers = {})
|
32
|
+
@request = Net::HTTP::Get.new resource
|
33
|
+
@request.initialize_http_header DEFAULT_HEADERS.merge(headers)
|
34
|
+
make_request
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def make_request
|
40
|
+
path = "http://#{@device[:host]}:#{@device[:port]}#{@request.path}"
|
41
|
+
@uri = URI.parse(path)
|
42
|
+
@uri.user = "Airplay"
|
43
|
+
@uri.password = @password
|
44
|
+
|
45
|
+
add_auth_if_needed
|
46
|
+
|
47
|
+
response = @http.request(@uri, @request) {}
|
27
48
|
|
28
49
|
raise Airplay::Protocol::InvalidRequestError if response.code == "404"
|
29
50
|
response.body
|
30
51
|
end
|
31
52
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
53
|
+
def add_auth_if_needed
|
54
|
+
if @password
|
55
|
+
authenticate
|
56
|
+
@request.add_field('Authorization', @authentications[@uri.path])
|
57
|
+
end
|
37
58
|
end
|
38
59
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
make_request(request)
|
60
|
+
def authenticate
|
61
|
+
response = @http.request(@uri, @request) {}
|
62
|
+
auth = response['www-authenticate']
|
63
|
+
digest_authentication(auth) if auth
|
44
64
|
end
|
45
65
|
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
|
66
|
+
def digest_authentication(auth)
|
67
|
+
digest = Net::HTTP::DigestAuth.new
|
68
|
+
@authentications[@uri.path] ||=
|
69
|
+
digest.auth_header(@uri, auth, @request.method)
|
50
70
|
end
|
51
71
|
|
52
72
|
end
|
@@ -22,18 +22,17 @@ class Airplay::Protocol::Image
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def send(image, transition = :none)
|
25
|
-
image = URI.parse(image) if
|
25
|
+
image = URI.parse(image) if !!(image =~ URI::regexp)
|
26
26
|
content = case image
|
27
27
|
when String
|
28
|
-
|
29
|
-
|
28
|
+
File.exists?(image) ? File.read(image) : image
|
29
|
+
when URI::HTTP then Net::HTTP.get(image)
|
30
|
+
else
|
31
|
+
if image.respond_to?(:read)
|
32
|
+
image.read
|
30
33
|
else
|
31
|
-
|
34
|
+
throw Airplay::Protocol::InvalidMediaError
|
32
35
|
end
|
33
|
-
when File
|
34
|
-
image.read
|
35
|
-
when URI::HTTP
|
36
|
-
Net::HTTP.get(image)
|
37
36
|
end
|
38
37
|
|
39
38
|
@http.put(resource, content, transition_header(transition))
|
@@ -12,18 +12,18 @@ module Airplay::Server::Browser
|
|
12
12
|
def self.browse
|
13
13
|
@servers = []
|
14
14
|
timeout 3 do
|
15
|
-
DNSSD.browse!(Airplay::Protocol::SEARCH) do |
|
15
|
+
DNSSD.browse!(Airplay::Protocol::SEARCH) do |node|
|
16
16
|
resolver = DNSSD::Service.new
|
17
17
|
target, port = nil
|
18
|
-
resolver.resolve(
|
18
|
+
resolver.resolve(node) do |resolved|
|
19
19
|
port = resolved.port
|
20
20
|
target = resolved.target
|
21
21
|
break unless resolved.flags.more_coming?
|
22
22
|
end
|
23
23
|
info = Socket.getaddrinfo(target, nil, Socket::AF_INET)
|
24
|
-
|
25
|
-
@servers << Airplay::Server::Node.new(
|
26
|
-
break unless
|
24
|
+
ip = info[0][2]
|
25
|
+
@servers << Airplay::Server::Node.new(node.name, node.domain, ip, port)
|
26
|
+
break unless node.flags.more_coming?
|
27
27
|
end
|
28
28
|
end
|
29
29
|
rescue Timeout::Error
|
data/test/authentication.rb
CHANGED
@@ -2,7 +2,7 @@ require File.expand_path("helper", File.dirname(__FILE__))
|
|
2
2
|
|
3
3
|
scope do
|
4
4
|
test "connect to an authenticated source" do
|
5
|
-
|
5
|
+
with_cassette("authenticate all the things!") do
|
6
6
|
airplay = Airplay::Client.new(false, MockedBrowser)
|
7
7
|
airplay.password("password")
|
8
8
|
|
data/test/discovery.rb
CHANGED
@@ -1,135 +1,139 @@
|
|
1
|
-
---
|
2
|
-
|
3
|
-
|
4
|
-
method:
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: get
|
5
5
|
uri: http://mocktv.local:7000/scrub
|
6
|
-
body:
|
7
|
-
|
8
|
-
|
6
|
+
body:
|
7
|
+
encoding: US-ASCII
|
8
|
+
base64_string: ""
|
9
|
+
headers:
|
10
|
+
user-agent:
|
9
11
|
- MediaControl/1.0
|
10
|
-
content-type:
|
12
|
+
content-type:
|
11
13
|
- text/x-apple-plist+xml
|
12
|
-
connection:
|
14
|
+
connection:
|
13
15
|
- keep-alive
|
14
|
-
keep-alive:
|
16
|
+
keep-alive:
|
15
17
|
- 30
|
16
|
-
response:
|
17
|
-
status:
|
18
|
+
response:
|
19
|
+
status:
|
18
20
|
code: 401
|
19
21
|
message: Unauthorized
|
20
|
-
headers:
|
21
|
-
date:
|
22
|
-
-
|
23
|
-
content-length:
|
24
|
-
-
|
25
|
-
www-authenticate:
|
26
|
-
- Digest realm="AirPlay", nonce="
|
27
|
-
body:
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
22
|
+
headers:
|
23
|
+
date:
|
24
|
+
- Fri, 23 Mar 2012 22:10:30 GMT
|
25
|
+
content-length:
|
26
|
+
- "0"
|
27
|
+
www-authenticate:
|
28
|
+
- Digest realm="AirPlay", nonce="MTMzMjU0MDYzMCArTzzIA84dWO8ijTtj5Rhw"
|
29
|
+
body:
|
30
|
+
encoding: US-ASCII
|
31
|
+
base64_string: ""
|
32
|
+
http_version: "1.1"
|
33
|
+
recorded_at: Fri, 23 Mar 2012 22:10:30 GMT
|
34
|
+
- request:
|
35
|
+
method: get
|
33
36
|
uri: http://mocktv.local:7000/scrub
|
34
|
-
body:
|
35
|
-
|
36
|
-
|
37
|
+
body:
|
38
|
+
encoding: US-ASCII
|
39
|
+
base64_string: ""
|
40
|
+
headers:
|
41
|
+
user-agent:
|
37
42
|
- MediaControl/1.0
|
38
|
-
content-type:
|
43
|
+
content-type:
|
39
44
|
- text/x-apple-plist+xml
|
40
|
-
connection:
|
45
|
+
connection:
|
41
46
|
- keep-alive
|
42
|
-
|
43
|
-
keep-alive:
|
44
|
-
- 30
|
47
|
+
keep-alive:
|
45
48
|
- 30
|
46
|
-
host:
|
49
|
+
host:
|
47
50
|
- mocktv.local:7000
|
48
|
-
authorization:
|
49
|
-
- Digest username="Airplay", realm="AirPlay", uri="/scrub", nonce="
|
50
|
-
|
51
|
-
|
52
|
-
status: !ruby/struct:VCR::ResponseStatus
|
51
|
+
authorization:
|
52
|
+
- Digest username="Airplay", realm="AirPlay", uri="/scrub", nonce="MTMzMjU0MDYzMCArTzzIA84dWO8ijTtj5Rhw", nc=00000000, cnonce="0f55c9413ac973d5963a73572df0805b", response="e6458b63b53f22522a23138c94a5b02b"
|
53
|
+
response:
|
54
|
+
status:
|
53
55
|
code: 200
|
54
56
|
message: OK
|
55
|
-
headers:
|
56
|
-
date:
|
57
|
-
-
|
58
|
-
content-type:
|
57
|
+
headers:
|
58
|
+
date:
|
59
|
+
- Fri, 23 Mar 2012 22:10:35 GMT
|
60
|
+
content-type:
|
59
61
|
- text/parameters
|
60
|
-
content-length:
|
61
|
-
-
|
62
|
-
body:
|
63
|
-
|
64
|
-
|
62
|
+
content-length:
|
63
|
+
- "38"
|
64
|
+
body:
|
65
|
+
encoding: US-ASCII
|
66
|
+
base64_string: |
|
67
|
+
ZHVyYXRpb246IDAuMDAwMDAwCnBvc2l0aW9uOiAwLjAwMDAwMAo=
|
65
68
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
request: !ruby/struct:VCR::Request
|
71
|
-
method: :get
|
69
|
+
http_version: "1.1"
|
70
|
+
recorded_at: Fri, 23 Mar 2012 22:10:35 GMT
|
71
|
+
- request:
|
72
|
+
method: get
|
72
73
|
uri: http://mocktv.local:7000/scrub
|
73
|
-
body:
|
74
|
-
|
75
|
-
|
74
|
+
body:
|
75
|
+
encoding: US-ASCII
|
76
|
+
base64_string: ""
|
77
|
+
headers:
|
78
|
+
user-agent:
|
76
79
|
- MediaControl/1.0
|
77
|
-
content-type:
|
80
|
+
content-type:
|
78
81
|
- text/x-apple-plist+xml
|
79
|
-
connection:
|
82
|
+
connection:
|
80
83
|
- keep-alive
|
81
|
-
keep-alive:
|
84
|
+
keep-alive:
|
82
85
|
- 30
|
83
|
-
response:
|
84
|
-
status:
|
86
|
+
response:
|
87
|
+
status:
|
85
88
|
code: 401
|
86
89
|
message: Unauthorized
|
87
|
-
headers:
|
88
|
-
date:
|
89
|
-
-
|
90
|
-
content-length:
|
91
|
-
-
|
92
|
-
www-authenticate:
|
93
|
-
- Digest realm="AirPlay", nonce="
|
94
|
-
body:
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
90
|
+
headers:
|
91
|
+
date:
|
92
|
+
- Fri, 23 Mar 2012 22:10:40 GMT
|
93
|
+
content-length:
|
94
|
+
- "0"
|
95
|
+
www-authenticate:
|
96
|
+
- Digest realm="AirPlay", nonce="MTMzMjU0MDY0MCCM+VRd9bJqrocG4oDxuVOU"
|
97
|
+
body:
|
98
|
+
encoding: US-ASCII
|
99
|
+
base64_string: ""
|
100
|
+
http_version: "1.1"
|
101
|
+
recorded_at: Fri, 23 Mar 2012 22:10:40 GMT
|
102
|
+
- request:
|
103
|
+
method: get
|
100
104
|
uri: http://mocktv.local:7000/scrub
|
101
|
-
body:
|
102
|
-
|
103
|
-
|
105
|
+
body:
|
106
|
+
encoding: US-ASCII
|
107
|
+
base64_string: ""
|
108
|
+
headers:
|
109
|
+
user-agent:
|
104
110
|
- MediaControl/1.0
|
105
|
-
content-type:
|
111
|
+
content-type:
|
106
112
|
- text/x-apple-plist+xml
|
107
|
-
connection:
|
113
|
+
connection:
|
108
114
|
- keep-alive
|
109
|
-
|
110
|
-
keep-alive:
|
111
|
-
- 30
|
115
|
+
keep-alive:
|
112
116
|
- 30
|
113
|
-
host:
|
117
|
+
host:
|
114
118
|
- mocktv.local:7000
|
115
|
-
authorization:
|
116
|
-
- Digest username="Airplay", realm="AirPlay", uri="/scrub", nonce="
|
117
|
-
|
118
|
-
|
119
|
-
status: !ruby/struct:VCR::ResponseStatus
|
119
|
+
authorization:
|
120
|
+
- Digest username="Airplay", realm="AirPlay", uri="/scrub", nonce="MTMzMjU0MDYzMCArTzzIA84dWO8ijTtj5Rhw", nc=00000000, cnonce="0f55c9413ac973d5963a73572df0805b", response="e6458b63b53f22522a23138c94a5b02b"
|
121
|
+
response:
|
122
|
+
status:
|
120
123
|
code: 200
|
121
124
|
message: OK
|
122
|
-
headers:
|
123
|
-
date:
|
124
|
-
-
|
125
|
-
content-type:
|
125
|
+
headers:
|
126
|
+
date:
|
127
|
+
- Fri, 23 Mar 2012 22:10:45 GMT
|
128
|
+
content-type:
|
126
129
|
- text/parameters
|
127
|
-
content-length:
|
128
|
-
-
|
129
|
-
body:
|
130
|
-
|
131
|
-
|
130
|
+
content-length:
|
131
|
+
- "38"
|
132
|
+
body:
|
133
|
+
encoding: US-ASCII
|
134
|
+
base64_string: |
|
135
|
+
ZHVyYXRpb246IDAuMDAwMDAwCnBvc2l0aW9uOiAwLjAwMDAwMAo=
|
132
136
|
|
133
|
-
|
134
|
-
|
135
|
-
|
137
|
+
http_version: "1.1"
|
138
|
+
recorded_at: Fri, 23 Mar 2012 22:10:45 GMT
|
139
|
+
recorded_with: VCR 2.0.0
|