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.
- data/Gemfile +5 -0
- data/Gemfile.lock +68 -0
- data/HUGS +15 -0
- data/LICENSE +20 -0
- data/README.md +50 -72
- data/Rakefile +6 -9
- data/SPEC.md +34 -0
- data/airplay.gemspec +28 -16
- data/bin/air +27 -0
- data/examples/demo.rb +35 -0
- data/examples/image.rb +9 -0
- data/examples/video.rb +24 -0
- data/lib/airplay.rb +38 -17
- data/lib/airplay/browser.rb +68 -0
- data/lib/airplay/cli.rb +104 -0
- data/lib/airplay/configuration.rb +29 -0
- data/lib/airplay/connection.rb +114 -0
- data/lib/airplay/connection/authentication.rb +37 -0
- data/lib/airplay/connection/persistent.rb +44 -0
- data/lib/airplay/device.rb +81 -0
- data/lib/airplay/device/features.rb +43 -0
- data/lib/airplay/device/info.rb +22 -0
- data/lib/airplay/devices.rb +46 -0
- data/lib/airplay/logger.rb +20 -0
- data/lib/airplay/playable.rb +24 -0
- data/lib/airplay/protocol.rb +9 -72
- data/lib/airplay/protocol/app.rb +52 -0
- data/lib/airplay/protocol/message.rb +8 -0
- data/lib/airplay/protocol/playback_info.rb +49 -0
- data/lib/airplay/protocol/player.rb +170 -0
- data/lib/airplay/protocol/reverse.rb +69 -0
- data/lib/airplay/protocol/slideshow.rb +66 -0
- data/lib/airplay/protocol/timers.rb +25 -0
- data/lib/airplay/protocol/viewer.rb +56 -0
- data/lib/airplay/structure.rb +7 -0
- data/lib/airplay/viewable.rb +16 -0
- data/test/fixtures/cassettes/airplay/listing_slideshow_features.yml +201 -0
- data/test/fixtures/cassettes/airplay/play_an_entire_video.yml +100 -0
- data/test/fixtures/cassettes/airplay/sending_a_video.yml +71 -0
- data/test/fixtures/cassettes/airplay/sending_an_image.yml +26439 -0
- data/test/fixtures/cassettes/airplay/stop_any_transmission.yml +8851 -0
- data/test/fixtures/files/logo.png +0 -0
- data/test/fixtures/files/transition_0.png +0 -0
- data/test/fixtures/files/transition_1.png +0 -0
- data/test/fixtures/files/transition_2.png +0 -0
- data/test/fixtures/files/transition_3.png +0 -0
- data/test/integration/discovery_test.rb +13 -0
- data/test/integration/features_test.rb +14 -0
- data/test/integration/send_media_test.rb +37 -0
- data/test/integration/slideshow_test.rb +26 -0
- data/test/test_helper.rb +42 -0
- data/test/unit/node_test.rb +17 -0
- data/test/unit/protocol_test.rb +44 -0
- metadata +247 -77
- data/.gitignore +0 -2
- data/.travis.yml +0 -6
- data/examples/jobs.jpg +0 -0
- data/examples/send_image.rb +0 -5
- data/examples/send_video.rb +0 -7
- data/lib/airplay/client.rb +0 -60
- data/lib/airplay/protocol/image.rb +0 -41
- data/lib/airplay/protocol/media.rb +0 -64
- data/lib/airplay/protocol/scrub.rb +0 -37
- data/lib/airplay/server.rb +0 -2
- data/lib/airplay/server/browser.rb +0 -35
- data/lib/airplay/server/node.rb +0 -7
- data/test/authentication.rb +0 -13
- data/test/discovery.rb +0 -27
- data/test/fixtures/cassettes/airplay/authenticate_all_the_things_.yml +0 -139
- data/test/fixtures/cassettes/airplay/control_a_video_being_played_in_apple_tv.yml +0 -242
- data/test/fixtures/cassettes/airplay/get_current_scrub_from_apple_tv.yml +0 -69
- data/test/fixtures/cassettes/airplay/go_to_a_given_position_in_the_video.yml +0 -157
- data/test/fixtures/cassettes/airplay/send_audio_to_apple_tv.yml +0 -33
- data/test/fixtures/cassettes/airplay/send_image_to_apple_tv.yml +0 -661
- data/test/fixtures/cassettes/airplay/send_image_to_apple_tv_with_effects.yml +0 -591
- data/test/fixtures/cassettes/airplay/send_video_to_apple_tv.yml +0 -33
- data/test/fixtures/image.gif +0 -0
- data/test/fixtures/image2.gif +0 -0
- data/test/helper.rb +0 -31
- data/test/images.rb +0 -31
- data/test/media.rb +0 -47
- data/test/scrub.rb +0 -31
data/.gitignore
DELETED
data/.travis.yml
DELETED
data/examples/jobs.jpg
DELETED
Binary file
|
data/examples/send_image.rb
DELETED
data/examples/send_video.rb
DELETED
data/lib/airplay/client.rb
DELETED
@@ -1,60 +0,0 @@
|
|
1
|
-
class Airplay::Client
|
2
|
-
attr_reader :servers, :active, :password
|
3
|
-
|
4
|
-
def initialize(server = false, server_browser = Airplay::Server::Browser)
|
5
|
-
@server_browser = server_browser
|
6
|
-
browse unless server
|
7
|
-
use servers.first if !@servers.nil?
|
8
|
-
end
|
9
|
-
|
10
|
-
def use(server)
|
11
|
-
@active = if server.is_a?(Airplay::Server::Node)
|
12
|
-
server
|
13
|
-
else
|
14
|
-
@server_browser.find_by_name(server)
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def password(password)
|
19
|
-
@password = password
|
20
|
-
end
|
21
|
-
|
22
|
-
def find_by_name(name)
|
23
|
-
@server_browser.find_by_name(name)
|
24
|
-
end
|
25
|
-
|
26
|
-
def browse
|
27
|
-
@servers = @server_browser.browse
|
28
|
-
end
|
29
|
-
|
30
|
-
def handler
|
31
|
-
@_handler ||= Airplay::Protocol.new(@active.ip, @active.port, @password)
|
32
|
-
end
|
33
|
-
|
34
|
-
def send_image(image, transition = :none)
|
35
|
-
@image_proxy ||= Airplay::Protocol::Image.new(handler)
|
36
|
-
@image_proxy.send(image, transition)
|
37
|
-
end
|
38
|
-
|
39
|
-
def send_video(video, position = 0)
|
40
|
-
@media_proxy ||= Airplay::Protocol::Media.new(handler)
|
41
|
-
@media_proxy.send(video, position)
|
42
|
-
end
|
43
|
-
|
44
|
-
def send_audio(audio, position = 0)
|
45
|
-
@media_proxy ||= Airplay::Protocol::Media.new(handler)
|
46
|
-
@media_proxy.send(audio, position)
|
47
|
-
end
|
48
|
-
|
49
|
-
def scrub(position = false)
|
50
|
-
@scrub_proxy ||= Airplay::Protocol::Scrub.new(handler)
|
51
|
-
if position
|
52
|
-
@scrub_proxy.to position
|
53
|
-
else
|
54
|
-
@scrub_proxy.check
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
end
|
59
|
-
|
60
|
-
class Airplay::Client::ServerNotFoundError < StandardError; end;
|
@@ -1,41 +0,0 @@
|
|
1
|
-
class Airplay::Protocol::Image
|
2
|
-
|
3
|
-
def initialize(protocol_handler)
|
4
|
-
@http = protocol_handler
|
5
|
-
end
|
6
|
-
|
7
|
-
def resource
|
8
|
-
"/photo"
|
9
|
-
end
|
10
|
-
|
11
|
-
def transitions
|
12
|
-
{
|
13
|
-
:none => "None",
|
14
|
-
:slide_left => "SlideLeft",
|
15
|
-
:slide_right => "SlideRight",
|
16
|
-
:dissolve => "Dissolve"
|
17
|
-
}
|
18
|
-
end
|
19
|
-
|
20
|
-
def transition_header(transition)
|
21
|
-
{"X-Apple-Transition" => transitions.fetch(transition, :none)}
|
22
|
-
end
|
23
|
-
|
24
|
-
def send(image, transition = :none)
|
25
|
-
image = URI.parse(image) if !!(image =~ URI::regexp)
|
26
|
-
content = case image
|
27
|
-
when String
|
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
|
33
|
-
else
|
34
|
-
throw Airplay::Protocol::InvalidMediaError
|
35
|
-
end
|
36
|
-
end
|
37
|
-
|
38
|
-
@http.put(resource, content, transition_header(transition))
|
39
|
-
end
|
40
|
-
|
41
|
-
end
|
@@ -1,64 +0,0 @@
|
|
1
|
-
class Airplay::Protocol::Media
|
2
|
-
|
3
|
-
def initialize(protocol_handler)
|
4
|
-
@http = protocol_handler
|
5
|
-
@scrubber = Airplay::Protocol::Scrub.new(protocol_handler)
|
6
|
-
end
|
7
|
-
|
8
|
-
def resource
|
9
|
-
"/play"
|
10
|
-
end
|
11
|
-
|
12
|
-
def stop_resource
|
13
|
-
"/stop"
|
14
|
-
end
|
15
|
-
|
16
|
-
def pause_resource
|
17
|
-
"/rate"
|
18
|
-
end
|
19
|
-
|
20
|
-
def position_body(position = 0)
|
21
|
-
"Start-Position: #{position}\n"
|
22
|
-
end
|
23
|
-
|
24
|
-
def location_body(media)
|
25
|
-
"Content-Location: #{media}\n"
|
26
|
-
end
|
27
|
-
|
28
|
-
def send(media, position = 0)
|
29
|
-
play(media, position)
|
30
|
-
self
|
31
|
-
end
|
32
|
-
|
33
|
-
def play(media, position = 0)
|
34
|
-
raise Airplay::Protocol::InvalidMediaError if (media =~ URI::regexp).nil?
|
35
|
-
body = location_body(media)
|
36
|
-
body += position_body(position.to_s)
|
37
|
-
@http.post(resource, body)
|
38
|
-
end
|
39
|
-
|
40
|
-
def pause
|
41
|
-
rate(0)
|
42
|
-
end
|
43
|
-
|
44
|
-
def scrub(position = false)
|
45
|
-
if position
|
46
|
-
@scrubber.to position
|
47
|
-
else
|
48
|
-
@scrubber.check
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
def resume
|
53
|
-
rate(1)
|
54
|
-
end
|
55
|
-
|
56
|
-
def rate(play = 1)
|
57
|
-
@http.post("#{pause_resource}?value=#{play}")
|
58
|
-
end
|
59
|
-
|
60
|
-
def stop
|
61
|
-
@http.post(stop_resource)
|
62
|
-
end
|
63
|
-
|
64
|
-
end
|
@@ -1,37 +0,0 @@
|
|
1
|
-
class Airplay::Protocol::Scrub
|
2
|
-
|
3
|
-
def initialize(protocol_handler)
|
4
|
-
@http = protocol_handler
|
5
|
-
end
|
6
|
-
|
7
|
-
def resource
|
8
|
-
"/scrub"
|
9
|
-
end
|
10
|
-
|
11
|
-
def plist_headers
|
12
|
-
{'Content-Type' => 'text/x-apple-plist+xml'}
|
13
|
-
end
|
14
|
-
|
15
|
-
def check
|
16
|
-
response = @http.get(resource, plist_headers)
|
17
|
-
result = {}
|
18
|
-
response.delete(":").split.each_slice(2) do |key, value|
|
19
|
-
result[key] = value.to_i
|
20
|
-
end
|
21
|
-
result
|
22
|
-
end
|
23
|
-
|
24
|
-
def to(position)
|
25
|
-
if position.to_s.include?("%")
|
26
|
-
percent = position.to_i
|
27
|
-
raise Airplay::Protocol::OutOfRangeError if percent > 100
|
28
|
-
duration = check.fetch("duration")
|
29
|
-
position = (duration*percent)/100
|
30
|
-
end
|
31
|
-
|
32
|
-
@http.post("#{resource}?position=#{position}\n")
|
33
|
-
end
|
34
|
-
|
35
|
-
end
|
36
|
-
|
37
|
-
class Airplay::Protocol::OutOfRangeError < StandardError; end;
|
data/lib/airplay/server.rb
DELETED
@@ -1,35 +0,0 @@
|
|
1
|
-
module Airplay::Server::Browser
|
2
|
-
attr_reader :servers
|
3
|
-
|
4
|
-
def self.find_by_name(name)
|
5
|
-
found_server = @servers.detect do |server|
|
6
|
-
server if server.name == name
|
7
|
-
end
|
8
|
-
raise Airplay::Client::ServerNotFoundError unless found_server
|
9
|
-
found_server
|
10
|
-
end
|
11
|
-
|
12
|
-
def self.browse
|
13
|
-
@servers = []
|
14
|
-
timeout 3 do
|
15
|
-
DNSSD.browse!(Airplay::Protocol::SEARCH) do |node|
|
16
|
-
resolver = DNSSD::Service.new
|
17
|
-
target, port = nil
|
18
|
-
resolver.resolve(node) do |resolved|
|
19
|
-
port = resolved.port
|
20
|
-
target = resolved.target
|
21
|
-
break unless resolved.flags.more_coming?
|
22
|
-
end
|
23
|
-
info = Socket.getaddrinfo(target, nil, Socket::AF_INET)
|
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
|
-
end
|
28
|
-
end
|
29
|
-
rescue Timeout::Error
|
30
|
-
raise Airplay::Client::ServerNotFoundError
|
31
|
-
else
|
32
|
-
@servers
|
33
|
-
end
|
34
|
-
|
35
|
-
end
|
data/lib/airplay/server/node.rb
DELETED
data/test/authentication.rb
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
require File.expand_path("helper", File.dirname(__FILE__))
|
2
|
-
|
3
|
-
scope do
|
4
|
-
test "connect to an authenticated source" do
|
5
|
-
with_cassette("authenticate all the things!") do
|
6
|
-
airplay = Airplay::Client.new(false, MockedBrowser)
|
7
|
-
airplay.password("password")
|
8
|
-
|
9
|
-
assert airplay.scrub.has_key?("duration")
|
10
|
-
assert airplay.scrub.has_key?("position")
|
11
|
-
end
|
12
|
-
end
|
13
|
-
end
|
data/test/discovery.rb
DELETED
@@ -1,27 +0,0 @@
|
|
1
|
-
require File.expand_path("helper", File.dirname(__FILE__))
|
2
|
-
|
3
|
-
scope do
|
4
|
-
|
5
|
-
setup do
|
6
|
-
@airplay = Airplay::Client.new(false, MockedBrowser)
|
7
|
-
end
|
8
|
-
|
9
|
-
test "browse for available airplay servers" do
|
10
|
-
assert @airplay.servers.size > 0
|
11
|
-
end
|
12
|
-
|
13
|
-
test "find servers by name" do
|
14
|
-
assert @airplay.find_by_name("Mock TV").is_a?(Airplay::Server::Node)
|
15
|
-
end
|
16
|
-
|
17
|
-
test "raise on not found" do
|
18
|
-
assert_raise Airplay::Client::ServerNotFoundError do
|
19
|
-
@airplay.find_by_name("NotARealAirplayServer")
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
test "autoselect if only one server available" do
|
24
|
-
assert_equal "Mock TV", @airplay.active.name
|
25
|
-
end
|
26
|
-
|
27
|
-
end
|
@@ -1,139 +0,0 @@
|
|
1
|
-
---
|
2
|
-
http_interactions:
|
3
|
-
- request:
|
4
|
-
method: get
|
5
|
-
uri: http://mocktv.local:7000/scrub
|
6
|
-
body:
|
7
|
-
encoding: US-ASCII
|
8
|
-
base64_string: ""
|
9
|
-
headers:
|
10
|
-
user-agent:
|
11
|
-
- MediaControl/1.0
|
12
|
-
content-type:
|
13
|
-
- text/x-apple-plist+xml
|
14
|
-
connection:
|
15
|
-
- keep-alive
|
16
|
-
keep-alive:
|
17
|
-
- 30
|
18
|
-
response:
|
19
|
-
status:
|
20
|
-
code: 401
|
21
|
-
message: Unauthorized
|
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
|
36
|
-
uri: http://mocktv.local:7000/scrub
|
37
|
-
body:
|
38
|
-
encoding: US-ASCII
|
39
|
-
base64_string: ""
|
40
|
-
headers:
|
41
|
-
user-agent:
|
42
|
-
- MediaControl/1.0
|
43
|
-
content-type:
|
44
|
-
- text/x-apple-plist+xml
|
45
|
-
connection:
|
46
|
-
- keep-alive
|
47
|
-
keep-alive:
|
48
|
-
- 30
|
49
|
-
host:
|
50
|
-
- mocktv.local:7000
|
51
|
-
authorization:
|
52
|
-
- Digest username="Airplay", realm="AirPlay", uri="/scrub", nonce="MTMzMjU0MDYzMCArTzzIA84dWO8ijTtj5Rhw", nc=00000000, cnonce="0f55c9413ac973d5963a73572df0805b", response="e6458b63b53f22522a23138c94a5b02b"
|
53
|
-
response:
|
54
|
-
status:
|
55
|
-
code: 200
|
56
|
-
message: OK
|
57
|
-
headers:
|
58
|
-
date:
|
59
|
-
- Fri, 23 Mar 2012 22:10:35 GMT
|
60
|
-
content-type:
|
61
|
-
- text/parameters
|
62
|
-
content-length:
|
63
|
-
- "38"
|
64
|
-
body:
|
65
|
-
encoding: US-ASCII
|
66
|
-
base64_string: |
|
67
|
-
ZHVyYXRpb246IDAuMDAwMDAwCnBvc2l0aW9uOiAwLjAwMDAwMAo=
|
68
|
-
|
69
|
-
http_version: "1.1"
|
70
|
-
recorded_at: Fri, 23 Mar 2012 22:10:35 GMT
|
71
|
-
- request:
|
72
|
-
method: get
|
73
|
-
uri: http://mocktv.local:7000/scrub
|
74
|
-
body:
|
75
|
-
encoding: US-ASCII
|
76
|
-
base64_string: ""
|
77
|
-
headers:
|
78
|
-
user-agent:
|
79
|
-
- MediaControl/1.0
|
80
|
-
content-type:
|
81
|
-
- text/x-apple-plist+xml
|
82
|
-
connection:
|
83
|
-
- keep-alive
|
84
|
-
keep-alive:
|
85
|
-
- 30
|
86
|
-
response:
|
87
|
-
status:
|
88
|
-
code: 401
|
89
|
-
message: Unauthorized
|
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
|
104
|
-
uri: http://mocktv.local:7000/scrub
|
105
|
-
body:
|
106
|
-
encoding: US-ASCII
|
107
|
-
base64_string: ""
|
108
|
-
headers:
|
109
|
-
user-agent:
|
110
|
-
- MediaControl/1.0
|
111
|
-
content-type:
|
112
|
-
- text/x-apple-plist+xml
|
113
|
-
connection:
|
114
|
-
- keep-alive
|
115
|
-
keep-alive:
|
116
|
-
- 30
|
117
|
-
host:
|
118
|
-
- mocktv.local:7000
|
119
|
-
authorization:
|
120
|
-
- Digest username="Airplay", realm="AirPlay", uri="/scrub", nonce="MTMzMjU0MDYzMCArTzzIA84dWO8ijTtj5Rhw", nc=00000000, cnonce="0f55c9413ac973d5963a73572df0805b", response="e6458b63b53f22522a23138c94a5b02b"
|
121
|
-
response:
|
122
|
-
status:
|
123
|
-
code: 200
|
124
|
-
message: OK
|
125
|
-
headers:
|
126
|
-
date:
|
127
|
-
- Fri, 23 Mar 2012 22:10:45 GMT
|
128
|
-
content-type:
|
129
|
-
- text/parameters
|
130
|
-
content-length:
|
131
|
-
- "38"
|
132
|
-
body:
|
133
|
-
encoding: US-ASCII
|
134
|
-
base64_string: |
|
135
|
-
ZHVyYXRpb246IDAuMDAwMDAwCnBvc2l0aW9uOiAwLjAwMDAwMAo=
|
136
|
-
|
137
|
-
http_version: "1.1"
|
138
|
-
recorded_at: Fri, 23 Mar 2012 22:10:45 GMT
|
139
|
-
recorded_with: VCR 2.0.0
|