airplayer 0.0.2 → 0.0.3
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/Gemfile.lock +9 -1
- data/Guardfile +8 -0
- data/README.markdown +35 -11
- data/airplayer.gemspec +1 -0
- data/lib/airplayer.rb +6 -4
- data/lib/airplayer/app.rb +7 -4
- data/lib/airplayer/controller.rb +83 -0
- data/lib/airplayer/media.rb +38 -0
- data/lib/airplayer/playlist.rb +24 -0
- data/lib/airplayer/server.rb +16 -11
- data/lib/airplayer/version.rb +1 -1
- data/spec/airplayer_spec.rb +40 -5
- metadata +24 -5
- data/lib/airplayer/player.rb +0 -105
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
airplayer (0.0.
|
4
|
+
airplayer (0.0.3)
|
5
5
|
airplay
|
6
6
|
rack
|
7
7
|
ruby-progressbar
|
@@ -16,6 +16,13 @@ GEM
|
|
16
16
|
net-http-persistent (>= 2.6)
|
17
17
|
diff-lcs (1.1.3)
|
18
18
|
dnssd (2.0)
|
19
|
+
guard (1.4.0)
|
20
|
+
listen (>= 0.4.2)
|
21
|
+
thor (>= 0.14.6)
|
22
|
+
guard-rspec (2.0.0)
|
23
|
+
guard (>= 1.1)
|
24
|
+
rspec (~> 2.11)
|
25
|
+
listen (0.5.3)
|
19
26
|
net-http-digest_auth (1.2.1)
|
20
27
|
net-http-persistent (2.7)
|
21
28
|
rack (1.4.1)
|
@@ -36,5 +43,6 @@ PLATFORMS
|
|
36
43
|
|
37
44
|
DEPENDENCIES
|
38
45
|
airplayer!
|
46
|
+
guard-rspec
|
39
47
|
rake
|
40
48
|
rspec
|
data/Guardfile
ADDED
data/README.markdown
CHANGED
@@ -15,6 +15,7 @@ Command-line AirPlay video client for Apple TV
|
|
15
15
|
Requirements
|
16
16
|
-------------------------------------------------------------------------------
|
17
17
|
|
18
|
+
- OSX or Ubuntu
|
18
19
|
- Ruby 1.9.2 or later
|
19
20
|
- AppleTV 2G or later
|
20
21
|
|
@@ -50,30 +51,53 @@ Usage
|
|
50
51
|
### Play online video
|
51
52
|
|
52
53
|
```sh
|
53
|
-
$ airplayer play http://
|
54
|
-
|
55
|
-
|
54
|
+
$ airplayer play http://heinlein.local/Movies/AKIRA.m4v
|
55
|
+
|
56
|
+
Source: http://heinlein.local/misc/Movies/AKIRA.m4v
|
57
|
+
Title: AKIRA.m4v
|
58
|
+
Device: Apple TV (10.0.1.2)
|
59
|
+
Time: 00:04:25 |= | 3% Streaming
|
56
60
|
```
|
57
61
|
|
58
|
-
### Play
|
62
|
+
### Play video
|
59
63
|
|
60
64
|
```sh
|
61
|
-
$ airplayer play
|
62
|
-
|
63
|
-
|
65
|
+
$ airplayer play '~/Movies/Trailers/007 SKYFALL.mp4'
|
66
|
+
|
67
|
+
Source: http://10.0.1.6:7070
|
68
|
+
Title: SKYFALL.mp4
|
69
|
+
Device: Apple TV (10.0.1.2)
|
70
|
+
Time: 00:00:20 |===== | 11% Streaming
|
64
71
|
```
|
65
72
|
|
73
|
+
### Play all video in directory
|
74
|
+
|
75
|
+
```sh
|
76
|
+
$ airplayer play ~/Movies/Trailers
|
77
|
+
|
78
|
+
Source: http://10.0.1.6:7070
|
79
|
+
Title: 007 Casino Royale.mp4
|
80
|
+
Device: Apple TV (10.0.1.2)
|
81
|
+
Time: 00:02:33 |==============================================| 100% Complete
|
82
|
+
|
83
|
+
Source: http://10.0.1.6:7070
|
84
|
+
Title: 007 Quantum Of Solace.mp4
|
85
|
+
Device: Apple TV (10.0.1.2)
|
86
|
+
Time: 00:02:01 |==============================================| 100% Complete
|
87
|
+
|
88
|
+
Source: http://10.0.1.6:7070
|
89
|
+
Title: 007 SKYFALL.mp4
|
90
|
+
Device: Apple TV (10.0.1.2)
|
91
|
+
Time: 00:02:36 |==============================================| 100% Complete
|
92
|
+
```
|
66
93
|
|
67
94
|
### Repeat play
|
68
95
|
|
69
96
|
```sh
|
70
|
-
$ airplayer play
|
71
|
-
AirPlay: http://10.0.1.13:7070 to Apple TV(10.0.1.18)
|
72
|
-
Time: 00:00:31 |===================== | 50% Streaming
|
97
|
+
$ airplayer play '~/Movies/Trailers/007 SKYFALL.mp4' --repeat
|
73
98
|
```
|
74
99
|
|
75
100
|
|
76
|
-
|
77
101
|
LICENSE
|
78
102
|
--------------------------------------------------------------------------------
|
79
103
|
|
data/airplayer.gemspec
CHANGED
data/lib/airplayer.rb
CHANGED
@@ -2,8 +2,10 @@ require 'rubygems'
|
|
2
2
|
require 'airplayer/version'
|
3
3
|
|
4
4
|
module AirPlayer
|
5
|
-
autoload :Logger,
|
6
|
-
autoload :App,
|
7
|
-
autoload :
|
8
|
-
autoload :Server,
|
5
|
+
autoload :Logger, 'airplayer/logger'
|
6
|
+
autoload :App, 'airplayer/app'
|
7
|
+
autoload :Controller, 'airplayer/controller'
|
8
|
+
autoload :Server, 'airplayer/server'
|
9
|
+
autoload :Playlist, 'airplayer/playlist'
|
10
|
+
autoload :Media, 'airplayer/media'
|
9
11
|
end
|
data/lib/airplayer/app.rb
CHANGED
@@ -2,10 +2,13 @@ require 'thor'
|
|
2
2
|
|
3
3
|
module AirPlayer
|
4
4
|
class App < Thor
|
5
|
-
desc 'play <URI|FILE> [-r|--repeat]', 'Play video(URI or local video file path)'
|
6
|
-
method_option :repeat, :aliases => '-r', :desc => 'Repeat play'
|
7
|
-
def play(
|
8
|
-
|
5
|
+
desc 'play <URI|FILE|DIR> [-r|--repeat] ', 'Play video(URI or local video file path or video directory)'
|
6
|
+
method_option :repeat, :aliases => '-r', :desc => 'Repeat play', :type => :boolean
|
7
|
+
def play(target)
|
8
|
+
controller = Controller.new
|
9
|
+
Playlist.new.add(target).entries(options.repeat) do |media|
|
10
|
+
controller.play(media)
|
11
|
+
end
|
9
12
|
end
|
10
13
|
|
11
14
|
map '--version' => :version
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'airplay'
|
3
|
+
require 'airplayer/progress_bar/base'
|
4
|
+
|
5
|
+
module AirPlayer
|
6
|
+
class Controller
|
7
|
+
BufferingTimeoutError = Class.new(TimeoutError)
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@airplay = Airplay::Client.new
|
11
|
+
@device = @airplay.browse.first
|
12
|
+
@player = nil
|
13
|
+
@progressbar = nil
|
14
|
+
@timeout = 30
|
15
|
+
@interval = 1
|
16
|
+
@total_sec = 0
|
17
|
+
@current_sec = 0
|
18
|
+
rescue Airplay::Client::ServerNotFoundError
|
19
|
+
abort '[ERROR] Apple device not found'
|
20
|
+
end
|
21
|
+
|
22
|
+
def play(media)
|
23
|
+
raise TypeError unless media.kind_of? Media
|
24
|
+
|
25
|
+
display_information(media)
|
26
|
+
@player = @airplay.send_video(media.open)
|
27
|
+
|
28
|
+
buffering
|
29
|
+
@progressbar.progress = @current_sec while playing
|
30
|
+
|
31
|
+
@progressbar.title = :Complete
|
32
|
+
pause
|
33
|
+
media.close
|
34
|
+
rescue BufferingTimeoutError
|
35
|
+
abort '[ERROR] Buffering timeout'
|
36
|
+
rescue TypeError
|
37
|
+
abort '[ERROR] Not media class'
|
38
|
+
end
|
39
|
+
|
40
|
+
def pause
|
41
|
+
@player.stop if @player
|
42
|
+
@progressbar.finish if @progressbar
|
43
|
+
end
|
44
|
+
|
45
|
+
def replay
|
46
|
+
@player.scrub(0)
|
47
|
+
@player.resume
|
48
|
+
|
49
|
+
@progressbar.reset
|
50
|
+
@progressbar.resume
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
def display_information(media)
|
55
|
+
puts
|
56
|
+
puts " Source: #{media.path}"
|
57
|
+
puts " Title: #{media.title}"
|
58
|
+
puts " Device: #{@device.name} (#{@device.ip})"
|
59
|
+
|
60
|
+
@progressbar = ProgressBar.create(:format => ' %a |%b%i| %p%% %t')
|
61
|
+
end
|
62
|
+
|
63
|
+
def buffering
|
64
|
+
timeout @timeout, BufferingTimeoutError do
|
65
|
+
@progressbar.title = :Buffering until playing
|
66
|
+
@progressbar.title = :Streaming
|
67
|
+
@progressbar.total = @total_sec
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def playing
|
72
|
+
scrub = @player.scrub
|
73
|
+
@total_sec = scrub['duration']
|
74
|
+
@current_sec = scrub['position']
|
75
|
+
sleep @interval
|
76
|
+
progress?
|
77
|
+
end
|
78
|
+
|
79
|
+
def progress?
|
80
|
+
0 < @current_sec && @current_sec < @total_sec
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module AirPlayer
|
4
|
+
class Media
|
5
|
+
attr_reader :title, :path, :type
|
6
|
+
|
7
|
+
def initialize(target)
|
8
|
+
path = File.expand_path(target)
|
9
|
+
@title = File.basename(path)
|
10
|
+
|
11
|
+
if File.exist? path
|
12
|
+
@video_server = AirPlayer::Server.new(path)
|
13
|
+
@path = @video_server.uri
|
14
|
+
@type = :file
|
15
|
+
else
|
16
|
+
@path = URI.encode(target)
|
17
|
+
@type = :url
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def open
|
22
|
+
Thread.start { @video_server.start } if file?
|
23
|
+
@path
|
24
|
+
end
|
25
|
+
|
26
|
+
def close
|
27
|
+
@video_server.stop if file?
|
28
|
+
end
|
29
|
+
|
30
|
+
def file?
|
31
|
+
@type == :file
|
32
|
+
end
|
33
|
+
|
34
|
+
def url?
|
35
|
+
@type == :url
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module AirPlayer
|
2
|
+
class Playlist < Array
|
3
|
+
def add(item)
|
4
|
+
path = File.expand_path(item)
|
5
|
+
Dir.exists?(path) ? concat(media_in(path)) : push(Media.new(item))
|
6
|
+
self
|
7
|
+
end
|
8
|
+
|
9
|
+
def entries(repeat = false, &blk)
|
10
|
+
loop do
|
11
|
+
send(:each, &blk)
|
12
|
+
break unless repeat
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def media_in(path)
|
18
|
+
Dir.entries(path).map do |node|
|
19
|
+
media_path = File.expand_path(node, path)
|
20
|
+
Media.new(media_path) if File.file? media_path
|
21
|
+
end.compact
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/airplayer/server.rb
CHANGED
@@ -18,18 +18,23 @@ module AirPlayer
|
|
18
18
|
$stderr = STDERR
|
19
19
|
end
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
def stop
|
22
|
+
@server.server.shutdown
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
# networking - Getting the Hostname or IP in Ruby on Rails - Stack Overflow
|
27
|
+
# http://stackoverflow.com/questions/42566/getting-the-hostname-or-ip-in-ruby-on-rails
|
28
|
+
def local_ip
|
29
|
+
# turn off reverse DNS resolution temporarily
|
30
|
+
orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true
|
26
31
|
|
27
|
-
|
28
|
-
|
29
|
-
|
32
|
+
UDPSocket.open do |s|
|
33
|
+
s.connect('8.8.8.8', 1)
|
34
|
+
s.addr.last
|
35
|
+
end
|
36
|
+
ensure
|
37
|
+
Socket.do_not_reverse_lookup = orig
|
30
38
|
end
|
31
|
-
ensure
|
32
|
-
Socket.do_not_reverse_lookup = orig
|
33
|
-
end
|
34
39
|
end
|
35
40
|
end
|
data/lib/airplayer/version.rb
CHANGED
data/spec/airplayer_spec.rb
CHANGED
@@ -1,17 +1,52 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe AirPlayer do
|
4
|
-
|
3
|
+
describe :AirPlayer do
|
4
|
+
context :App do
|
5
|
+
let(:app) { AirPlayer::App.new }
|
5
6
|
|
6
|
-
context 'App' do
|
7
7
|
it 'class type is Thor' do
|
8
8
|
expect(app).to be_kind_of Thor
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
-
context
|
12
|
+
context :Logger do
|
13
13
|
it 'file path is /tmp/airplayer-access.log on Linux' do
|
14
|
-
|
14
|
+
if RbConfig::CONFIG['target_os'] =~ /linux|unix/
|
15
|
+
expect(AirPlayer::Logger.path).to eq '/tmp/airplayer-access.log'
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context :Media do
|
21
|
+
it 'give to local file' do
|
22
|
+
media = AirPlayer::Media.new('./Gemfile')
|
23
|
+
expect(media.file?).to be_true
|
24
|
+
end
|
25
|
+
it 'give to url' do
|
26
|
+
media = AirPlayer::Media.new('http://example.com/video.mp4')
|
27
|
+
expect(media.url?).to be_true
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context :Playlist do
|
32
|
+
let(:playlist) { AirPlayer::Playlist.new }
|
33
|
+
|
34
|
+
it 'add URL to list, and that media type is url' do
|
35
|
+
playlist.add('http://example.com/video.mp4')
|
36
|
+
expect(playlist.first.path).to match 'http'
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'add file to list, and that media type is file' do
|
40
|
+
expect(playlist.add('./LICENSE').size).to eq 1
|
41
|
+
expect(playlist.add('./Gemfile').size).to eq 2
|
42
|
+
expect(playlist.first.file?).to be_true
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'has list contains url or file path' do
|
46
|
+
playlist.add('../airplayer')
|
47
|
+
playlist.entries do |media|
|
48
|
+
expect(media).to be_kind_of AirPlayer::Media
|
49
|
+
end
|
15
50
|
end
|
16
51
|
end
|
17
52
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: airplayer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-10-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: thor
|
@@ -107,6 +107,22 @@ dependencies:
|
|
107
107
|
- - ! '>='
|
108
108
|
- !ruby/object:Gem::Version
|
109
109
|
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: guard-rspec
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
110
126
|
description: Command-line AirPlay video client for Apple TV
|
111
127
|
email:
|
112
128
|
- tomohiro.t@gmail.com
|
@@ -120,6 +136,7 @@ files:
|
|
120
136
|
- .travis.yml
|
121
137
|
- Gemfile
|
122
138
|
- Gemfile.lock
|
139
|
+
- Guardfile
|
123
140
|
- LICENSE
|
124
141
|
- README.markdown
|
125
142
|
- Rakefile
|
@@ -127,8 +144,10 @@ files:
|
|
127
144
|
- bin/airplayer
|
128
145
|
- lib/airplayer.rb
|
129
146
|
- lib/airplayer/app.rb
|
147
|
+
- lib/airplayer/controller.rb
|
130
148
|
- lib/airplayer/logger.rb
|
131
|
-
- lib/airplayer/
|
149
|
+
- lib/airplayer/media.rb
|
150
|
+
- lib/airplayer/playlist.rb
|
132
151
|
- lib/airplayer/progress_bar/base.rb
|
133
152
|
- lib/airplayer/server.rb
|
134
153
|
- lib/airplayer/version.rb
|
@@ -148,7 +167,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
148
167
|
version: '0'
|
149
168
|
segments:
|
150
169
|
- 0
|
151
|
-
hash:
|
170
|
+
hash: 1247565518444440084
|
152
171
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
153
172
|
none: false
|
154
173
|
requirements:
|
@@ -157,7 +176,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
157
176
|
version: '0'
|
158
177
|
segments:
|
159
178
|
- 0
|
160
|
-
hash:
|
179
|
+
hash: 1247565518444440084
|
161
180
|
requirements: []
|
162
181
|
rubyforge_project:
|
163
182
|
rubygems_version: 1.8.23
|
data/lib/airplayer/player.rb
DELETED
@@ -1,105 +0,0 @@
|
|
1
|
-
require 'timeout'
|
2
|
-
require 'uri'
|
3
|
-
require 'airplay'
|
4
|
-
require 'airplayer/progress_bar/base'
|
5
|
-
|
6
|
-
module AirPlayer
|
7
|
-
class Player
|
8
|
-
BufferingTimeoutError = Class.new(TimeoutError)
|
9
|
-
|
10
|
-
def self.play(*opts)
|
11
|
-
new.play(*opts)
|
12
|
-
rescue Airplay::Client::ServerNotFoundError
|
13
|
-
abort '[ERROR] Apple device not found'
|
14
|
-
end
|
15
|
-
|
16
|
-
def initialize
|
17
|
-
@airplay = Airplay::Client.new
|
18
|
-
@player = nil
|
19
|
-
@progressbar = nil
|
20
|
-
@timeout = 30
|
21
|
-
@interval = 1
|
22
|
-
@total_sec = 0
|
23
|
-
@current_sec = 0
|
24
|
-
end
|
25
|
-
|
26
|
-
def play(target, repeat = false)
|
27
|
-
path = File.expand_path(target)
|
28
|
-
if local_file? path
|
29
|
-
video_server = AirPlayer::Server.new(path)
|
30
|
-
uri = video_server.uri
|
31
|
-
Thread.start { video_server.start }
|
32
|
-
else
|
33
|
-
uri = URI.encode(target)
|
34
|
-
end
|
35
|
-
|
36
|
-
device = @airplay.browse.first
|
37
|
-
puts "AirPlay: #{uri} to #{device.name}(#{device.ip})"
|
38
|
-
@progressbar = ProgressBar.create(:format => ' %a |%b%i| %p%% %t', :title => :Waiting)
|
39
|
-
@player = @airplay.send_video(uri)
|
40
|
-
|
41
|
-
loop do
|
42
|
-
buffering
|
43
|
-
@progressbar.progress = @current_sec while playing
|
44
|
-
break unless repeat
|
45
|
-
reset
|
46
|
-
end
|
47
|
-
|
48
|
-
stop
|
49
|
-
rescue BufferingTimeoutError
|
50
|
-
abort '[ERROR] Buffering timeout'
|
51
|
-
end
|
52
|
-
|
53
|
-
def reset
|
54
|
-
@player.scrub(0)
|
55
|
-
@player.resume
|
56
|
-
|
57
|
-
@progressbar.reset
|
58
|
-
@progressbar.resume
|
59
|
-
end
|
60
|
-
|
61
|
-
def stop
|
62
|
-
unless @player.nil?
|
63
|
-
@player.stop
|
64
|
-
end
|
65
|
-
|
66
|
-
unless @progressbar.nil?
|
67
|
-
@progressbar.title = :Complete
|
68
|
-
@progressbar.finish
|
69
|
-
end
|
70
|
-
end
|
71
|
-
|
72
|
-
private
|
73
|
-
def local_file?(path)
|
74
|
-
File.exist? path
|
75
|
-
end
|
76
|
-
|
77
|
-
def buffering
|
78
|
-
timeout @timeout, BufferingTimeoutError do
|
79
|
-
loop do
|
80
|
-
playing
|
81
|
-
redo unless buffering?
|
82
|
-
@progressbar.title = :Streaming
|
83
|
-
@progressbar.total = @total_sec
|
84
|
-
break
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
def playing
|
90
|
-
scrub = @player.scrub
|
91
|
-
@total_sec = scrub['duration']
|
92
|
-
@current_sec = scrub['position']
|
93
|
-
sleep @interval
|
94
|
-
progress?
|
95
|
-
end
|
96
|
-
|
97
|
-
def progress?
|
98
|
-
0 < @current_sec && @current_sec < @total_sec
|
99
|
-
end
|
100
|
-
|
101
|
-
def buffering?
|
102
|
-
0 < @total_sec && 0 < @current_sec
|
103
|
-
end
|
104
|
-
end
|
105
|
-
end
|