airplay 0.2.6 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|

|
|
4
4
|
|
|
5
|
-

|
|
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
|