vlc-client 0.0.1.beta
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +21 -0
- data/.rspec +1 -0
- data/.travis.yml +10 -0
- data/.yardopts +2 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +108 -0
- data/Rakefile +40 -0
- data/lib/vlc-client.rb +102 -0
- data/lib/vlc-client/client/connection_management.rb +23 -0
- data/lib/vlc-client/client/media_controls.rb +98 -0
- data/lib/vlc-client/client/video_controls.rb +10 -0
- data/lib/vlc-client/connection.rb +77 -0
- data/lib/vlc-client/core_ext/array.rb +12 -0
- data/lib/vlc-client/errors.rb +16 -0
- data/lib/vlc-client/null_object.rb +24 -0
- data/lib/vlc-client/server.rb +73 -0
- data/lib/vlc-client/system.rb +58 -0
- data/lib/vlc-client/version.rb +3 -0
- data/spec/helper.rb +40 -0
- data/spec/system_spec.rb +31 -0
- data/spec/vlc-client/client/connection_management_spec.rb +24 -0
- data/spec/vlc-client/client/media_controls_spec.rb +188 -0
- data/spec/vlc-client/client/video_controls_spec.rb +13 -0
- data/spec/vlc-client/connection_spec.rb +70 -0
- data/spec/vlc-client/server_spec.rb +19 -0
- data/spec/vlc_client_spec.rb +42 -0
- data/vlc.gemspec +29 -0
- metadata +159 -0
@@ -0,0 +1,77 @@
|
|
1
|
+
module VLC
|
2
|
+
#@ private
|
3
|
+
#
|
4
|
+
# Manages the connection to a VLC server
|
5
|
+
#
|
6
|
+
class Connection
|
7
|
+
attr_accessor :host, :port
|
8
|
+
|
9
|
+
def initialize(host, port)
|
10
|
+
@host, @port = host, port
|
11
|
+
@socket = NullObject.new
|
12
|
+
end
|
13
|
+
|
14
|
+
# Connects to VLC RC interface on Client#host and Client#port
|
15
|
+
def connect
|
16
|
+
@socket = TCPSocket.new(@host, @port)
|
17
|
+
2.times { read } #Clean the reading channel
|
18
|
+
true
|
19
|
+
rescue Errno::ECONNREFUSED => e
|
20
|
+
raise VLC::ConnectionRefused, "Could not connect to #{@host}:#{@port}: #{e}"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Queries if there is a connection to VLC RC interface
|
24
|
+
#
|
25
|
+
# @return [Boolean] true is connected, false otherwise
|
26
|
+
#
|
27
|
+
def connected?
|
28
|
+
not @socket.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
# Disconnects from VLC RC interface
|
32
|
+
def disconnect
|
33
|
+
@socket.close
|
34
|
+
@socket = NullObject.new
|
35
|
+
end
|
36
|
+
|
37
|
+
alias :close :disconnect
|
38
|
+
|
39
|
+
# Writes data to the TCP server socket
|
40
|
+
#
|
41
|
+
# @param data the data to write
|
42
|
+
# @param fire_and_forget if true, no response response is expected from server,
|
43
|
+
# when false, a response from the server will be returned.
|
44
|
+
#
|
45
|
+
# @return the server response data if there is one
|
46
|
+
#
|
47
|
+
def write(data, fire_and_forget = true)
|
48
|
+
raise NotConnectedError, "no connection to server" unless connected?
|
49
|
+
@socket.puts(data)
|
50
|
+
@socket.flush
|
51
|
+
|
52
|
+
return true if fire_and_forget
|
53
|
+
read
|
54
|
+
rescue Errno::EPIPE
|
55
|
+
disconnect
|
56
|
+
raise BrokenConnectionError, "the connection to the server is lost"
|
57
|
+
end
|
58
|
+
|
59
|
+
# Reads data from the TCP server
|
60
|
+
#
|
61
|
+
# @return [String] the data
|
62
|
+
#
|
63
|
+
def read
|
64
|
+
#TODO: Timeouts
|
65
|
+
raw_data = @socket.gets.chomp
|
66
|
+
if (data = process_data(raw_data))
|
67
|
+
data[1]
|
68
|
+
else
|
69
|
+
raise ProtocolError, "could not interpret the playload: #{raw_data}"
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def process_data(data)
|
74
|
+
data.match(/^[>*\s*]*(.*)$/)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module VLC
|
2
|
+
# Root error class
|
3
|
+
class Error < StandardError; end
|
4
|
+
|
5
|
+
# Raised on connection refusal
|
6
|
+
class ConnectionRefused < Error; end
|
7
|
+
|
8
|
+
# Raised on communication errors
|
9
|
+
class ProtocolError < Error; end
|
10
|
+
|
11
|
+
# Raised on a write to a broken connection
|
12
|
+
class BrokenConnectionError < Error; end
|
13
|
+
|
14
|
+
# Raised on a write ti a disconnected connection
|
15
|
+
class NotConnectedError < Error; end
|
16
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module VLC
|
2
|
+
# @private
|
3
|
+
class NullObject
|
4
|
+
class << self
|
5
|
+
def Null?(value)
|
6
|
+
value.nil? ? NullObject.new : value
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def nil?; true; end
|
11
|
+
def blank?; true; end
|
12
|
+
def empty?; true; end
|
13
|
+
def present?; false; end
|
14
|
+
def size; 0; end
|
15
|
+
def to_a; []; end
|
16
|
+
def to_s; ""; end
|
17
|
+
def to_f; 0.0; end
|
18
|
+
def to_i; 0; end
|
19
|
+
def extract_options!; {}; end
|
20
|
+
def inspect; self.class; end
|
21
|
+
|
22
|
+
def method_missing(*args, &block) self; end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module VLC
|
2
|
+
# Manages a local VLC server in a child process
|
3
|
+
class Server
|
4
|
+
attr_accessor :host, :port, :headless
|
5
|
+
alias :headless? :headless
|
6
|
+
|
7
|
+
#
|
8
|
+
# Creates a VLC server lifecycle manager
|
9
|
+
#
|
10
|
+
# @param [String] host The ip to bind to
|
11
|
+
# @param [Integer] port the port
|
12
|
+
# @param [Boolean] headless if true VLC media player will run headless.
|
13
|
+
# i.e. without a graphical interface. Defaults to false
|
14
|
+
#
|
15
|
+
def initialize(host = 'localhost', port = 9595, headless = false)
|
16
|
+
@host, @port, @headless = host, port, headless
|
17
|
+
@pid = NullObject.new
|
18
|
+
setup_traps
|
19
|
+
end
|
20
|
+
|
21
|
+
# Queries if VLC is running
|
22
|
+
#
|
23
|
+
# @return [Boolean] true is VLC is running, false otherwise
|
24
|
+
#
|
25
|
+
def running?
|
26
|
+
not @pid.nil?
|
27
|
+
end
|
28
|
+
|
29
|
+
alias :started? :running?
|
30
|
+
|
31
|
+
# Queries if VLC is stopped
|
32
|
+
#
|
33
|
+
# @return [Boolean] true is VLC is stopped, false otherwise
|
34
|
+
#
|
35
|
+
def stopped?; not running?; end
|
36
|
+
|
37
|
+
# Starts a VLC instance in a subprocess
|
38
|
+
#
|
39
|
+
# @return [Integer] the subprocess PID or nil if the start command
|
40
|
+
# as no effect (e.g. VLC already running)
|
41
|
+
#
|
42
|
+
def start
|
43
|
+
return NullObject.new if running?
|
44
|
+
@pid = Process.fork do
|
45
|
+
STDIN.reopen "/dev/null"
|
46
|
+
STDOUT.reopen "/dev/null", "a"
|
47
|
+
STDERR.reopen "/dev/null", "a"
|
48
|
+
|
49
|
+
exec "#{headless? ? 'cvlc' : 'vlc'} --extraintf rc --rc-host #{@host}:#{@port}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Starts a VLC instance in a subprocess
|
54
|
+
#
|
55
|
+
# @return [Integer] the terminated subprocess PID or nil if the stop command
|
56
|
+
# as no effect (e.g. VLC not running)
|
57
|
+
#
|
58
|
+
def stop
|
59
|
+
return NullObject.new if not running?
|
60
|
+
|
61
|
+
Process.kill('INT', pid = @pid)
|
62
|
+
@pid = NullObject.new
|
63
|
+
pid
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
def setup_traps
|
68
|
+
trap("EXIT") { stop }
|
69
|
+
trap("INT") { stop }
|
70
|
+
trap("CLD") { stop }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
module VLC
|
2
|
+
# Local Client/Server VLC system
|
3
|
+
class System
|
4
|
+
attr_reader :client
|
5
|
+
|
6
|
+
# Creates a local VLC Client/Server system
|
7
|
+
#
|
8
|
+
# @overload initialize(host, port, options) sets the host and port
|
9
|
+
#
|
10
|
+
# @param [String] host The ip to connect to
|
11
|
+
# @param [Integer] port the port
|
12
|
+
# @param [Hash] options
|
13
|
+
# @option options [Boolean] :auto_start When false, the server lifecycle is not managed automatically and controll is passed to the developer
|
14
|
+
# @option options [Integer] :conn_retries Number of connection retries (each separated by a second) to make on auto-connect. Defaults to 5.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# vlc = VLC::System.new('10.10.0.10', 9000)
|
18
|
+
#
|
19
|
+
# @overload initialize()
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# vlc = VLC::System.new
|
23
|
+
#
|
24
|
+
# @return [VLC::Server]
|
25
|
+
#
|
26
|
+
# @raise [VLC::ConnectionRefused] if the connection fails
|
27
|
+
#
|
28
|
+
def initialize(*args)
|
29
|
+
args = NullObject.Null?(args)
|
30
|
+
opts = args.extract_options!
|
31
|
+
|
32
|
+
server = VLC::Server.new
|
33
|
+
server.headless = opts.fetch(:headless, false)
|
34
|
+
|
35
|
+
if args.size == 2
|
36
|
+
server.host = args.first.to_s
|
37
|
+
server.port = Integer(args.last)
|
38
|
+
end
|
39
|
+
@client = VLC::Client.new(server, opts)
|
40
|
+
end
|
41
|
+
|
42
|
+
def server
|
43
|
+
client.server
|
44
|
+
end
|
45
|
+
|
46
|
+
def respond_to?(method, private_methods = false)
|
47
|
+
client.respond_to?(method, private_methods) || super(method, private_methods)
|
48
|
+
end
|
49
|
+
|
50
|
+
protected
|
51
|
+
# Delegate to VLC::Client
|
52
|
+
#
|
53
|
+
def method_missing(method, *args, &block)
|
54
|
+
return super unless client.respond_to?(method)
|
55
|
+
client.send(method, *args, &block)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
data/spec/helper.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
require 'pry'
|
3
|
+
|
4
|
+
#setup simplecov
|
5
|
+
SimpleCov.start do
|
6
|
+
add_filter "/spec"
|
7
|
+
end
|
8
|
+
|
9
|
+
require File.expand_path('../../lib/vlc-client', __FILE__)
|
10
|
+
|
11
|
+
module Mocks
|
12
|
+
def mock_tcp_server(opts = {})
|
13
|
+
tcp = double()
|
14
|
+
TCPSocket.stub(:new).and_return do
|
15
|
+
if opts.fetch(:defaults, true)
|
16
|
+
tcp.should_receive(:gets).with(no_args).at_least(:twice).and_return("")
|
17
|
+
tcp.should_receive(:flush).with(no_args).any_number_of_times
|
18
|
+
tcp.should_receive(:close).with(no_args) if opts.fetch(:close, true)
|
19
|
+
end
|
20
|
+
|
21
|
+
yield(tcp) if block_given?
|
22
|
+
tcp
|
23
|
+
end
|
24
|
+
tcp
|
25
|
+
end
|
26
|
+
|
27
|
+
def mock_system_calls(opts = {})
|
28
|
+
Process.stub(:fork).and_return(1)
|
29
|
+
Process.should_receive(:kill).once.with('INT', 1) if opts.fetch(:kill, true)
|
30
|
+
end
|
31
|
+
|
32
|
+
def mock_sub_systems
|
33
|
+
mock_system_calls
|
34
|
+
mock_tcp_server
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
RSpec.configure do |cfg|
|
39
|
+
cfg.include(Mocks)
|
40
|
+
end
|
data/spec/system_spec.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
describe VLC::System do
|
2
|
+
before(:each) do
|
3
|
+
mock_system_calls(:kill => false)
|
4
|
+
end
|
5
|
+
|
6
|
+
subject { VLC::System.new }
|
7
|
+
|
8
|
+
it 'creates a self-managed VLC media client/server local system' do
|
9
|
+
mock_tcp_server(:close => false)
|
10
|
+
|
11
|
+
subject.client.should be_a(VLC::Client)
|
12
|
+
subject.server.should be_a(VLC::Server)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'delegates calls to the client' do
|
16
|
+
mock_tcp_server(:close => false).should_receive(:puts).with('is_playing').once
|
17
|
+
|
18
|
+
should respond_to(:play)
|
19
|
+
should_not be_playing
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'handles server lifecycle management to client code' do
|
23
|
+
mock_tcp_server(:close => false)
|
24
|
+
|
25
|
+
vlc = VLC::System.new('127.0.0.1', 9999, :auto_start => false)
|
26
|
+
|
27
|
+
vlc.server.should_not be_running
|
28
|
+
vlc.server.host.should eq('127.0.0.1')
|
29
|
+
vlc.server.port.should eq(9999)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
describe VLC::Client::ConnectionManagement do
|
2
|
+
let(:vlc) { VLC::Client.new(:self_managed => false) }
|
3
|
+
before(:each) { mock_tcp_server }
|
4
|
+
after(:each) { vlc.disconnect }
|
5
|
+
|
6
|
+
context 'when disconnected' do
|
7
|
+
specify { vlc.should be_disconnected }
|
8
|
+
|
9
|
+
it 'connects to VLC' do
|
10
|
+
vlc.connect
|
11
|
+
vlc.should be_connected
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'when connected' do
|
16
|
+
before(:each) { vlc.connect }
|
17
|
+
specify { vlc.should be_connected }
|
18
|
+
|
19
|
+
it 'disconnects to VLC' do
|
20
|
+
vlc.disconnect
|
21
|
+
vlc.should be_disconnected
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
describe VLC::Client::MediaControls do
|
2
|
+
let(:vlc) { VLC::Client.new(:self_managed => false) }
|
3
|
+
after(:each) { vlc.disconnect }
|
4
|
+
|
5
|
+
context 'plays media' do
|
6
|
+
it 'from filesystem' do
|
7
|
+
mock_tcp_server.should_receive(:puts).once.with('add ./media.mp3')
|
8
|
+
vlc.connect
|
9
|
+
|
10
|
+
vlc.play('./media.mp3')
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'from a file descriptor' do
|
14
|
+
File.stub(:open).and_return {
|
15
|
+
f = File.new('./LICENSE', 'r')
|
16
|
+
f.should_receive(:path).once.and_return('./media.mp3')
|
17
|
+
f
|
18
|
+
}
|
19
|
+
|
20
|
+
mock_tcp_server.should_receive(:puts).once.with('add ./media.mp3')
|
21
|
+
vlc.connect
|
22
|
+
|
23
|
+
vlc.play(File.open("./media.mp3"))
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'from web' do
|
27
|
+
mock_tcp_server.should_receive(:puts).once.with('add http://example.org/media.mp3')
|
28
|
+
vlc.connect
|
29
|
+
|
30
|
+
vlc.play('http://example.org/media.mp3')
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'raises error for unknown schemas' do
|
34
|
+
mock_tcp_server
|
35
|
+
vlc.connect
|
36
|
+
|
37
|
+
expect { vlc.play(Class.new) }.to raise_error(ArgumentError)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'when playing media' do
|
42
|
+
def tcp_mock
|
43
|
+
tcp = mock_tcp_server(:defaults => false)
|
44
|
+
|
45
|
+
tcp.should_receive(:flush).with(no_args).any_number_of_times
|
46
|
+
tcp.should_receive(:gets).with(no_args).twice.and_return("")
|
47
|
+
|
48
|
+
tcp.should_receive(:puts).once.with('add http://example.org/media.mp3')
|
49
|
+
|
50
|
+
tcp.should_receive(:close).with(no_args)
|
51
|
+
tcp
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'may stop playback' do
|
55
|
+
tcp = tcp_mock
|
56
|
+
tcp.should_receive(:puts).once.with('is_playing')
|
57
|
+
tcp.should_receive(:gets).once.with(no_args).and_return("0")
|
58
|
+
|
59
|
+
tcp.should_receive(:puts).once.with('play')
|
60
|
+
tcp.should_receive(:puts).once.with('is_playing')
|
61
|
+
tcp.should_receive(:gets).once.with(no_args).and_return("1")
|
62
|
+
|
63
|
+
tcp.should_receive(:puts).once.with("stop")
|
64
|
+
|
65
|
+
vlc.connect
|
66
|
+
vlc.play('http://example.org/media.mp3')
|
67
|
+
|
68
|
+
vlc.stop
|
69
|
+
vlc.should be_stopped
|
70
|
+
|
71
|
+
vlc.play #play current item
|
72
|
+
vlc.should be_playing
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'may pause playback' do
|
76
|
+
tcp = tcp_mock
|
77
|
+
|
78
|
+
tcp.should_receive(:puts).once.with('pause')
|
79
|
+
tcp.should_receive(:puts).once.with('is_playing')
|
80
|
+
tcp.should_receive(:gets).once.with(no_args).and_return("0")
|
81
|
+
|
82
|
+
|
83
|
+
vlc.connect
|
84
|
+
vlc.play('http://example.org/media.mp3')
|
85
|
+
|
86
|
+
vlc.pause
|
87
|
+
vlc.should be_stopped
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'may resume playback' do
|
91
|
+
tcp = tcp_mock
|
92
|
+
|
93
|
+
tcp.should_receive(:puts).once.with('pause')
|
94
|
+
tcp.should_receive(:puts).once.with('play')
|
95
|
+
tcp.should_receive(:puts).once.with('is_playing')
|
96
|
+
tcp.should_receive(:gets).once.with(no_args).and_return('1')
|
97
|
+
|
98
|
+
vlc.connect
|
99
|
+
vlc.play('http://example.org/media.mp3')
|
100
|
+
|
101
|
+
vlc.pause
|
102
|
+
vlc.play
|
103
|
+
vlc.should be_playing
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'displays the playing media title' do
|
107
|
+
tcp = tcp_mock
|
108
|
+
tcp.should_receive(:puts).once.with('get_title')
|
109
|
+
tcp.should_receive(:gets).once.and_return('test media')
|
110
|
+
tcp.should_receive(:puts).once.with('stop')
|
111
|
+
tcp.should_receive(:puts).once.with('get_title')
|
112
|
+
tcp.should_receive(:gets).once.and_return('')
|
113
|
+
|
114
|
+
vlc.connect
|
115
|
+
vlc.play('http://example.org/media.mp3')
|
116
|
+
|
117
|
+
vlc.title.should eq('test media')
|
118
|
+
vlc.stop
|
119
|
+
|
120
|
+
vlc.title.should be_empty
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'is aware of track time' do
|
124
|
+
tcp = tcp_mock
|
125
|
+
|
126
|
+
tcp.should_receive(:puts).once.with('get_time')
|
127
|
+
tcp.should_receive(:gets).once.and_return('60')
|
128
|
+
|
129
|
+
vlc.connect
|
130
|
+
vlc.play('http://example.org/media.mp3')
|
131
|
+
|
132
|
+
vlc.time.should eq(60)
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'is aware of track length' do
|
136
|
+
tcp = tcp_mock
|
137
|
+
|
138
|
+
tcp.should_receive(:puts).once.with('get_length')
|
139
|
+
tcp.should_receive(:gets).once.and_return('100')
|
140
|
+
|
141
|
+
vlc.connect
|
142
|
+
vlc.play('http://example.org/media.mp3')
|
143
|
+
|
144
|
+
vlc.length.should eq(100)
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'is aware of track progress' do
|
148
|
+
vlc.stub(:length).and_return { 0 }
|
149
|
+
vlc.stub(:time).and_return { 100 }
|
150
|
+
vlc.progress.should eq(0)
|
151
|
+
|
152
|
+
vlc.stub(:length).and_return { 100 }
|
153
|
+
vlc.stub(:time).and_return { 0 }
|
154
|
+
vlc.progress.should eq(0)
|
155
|
+
|
156
|
+
vlc.stub(:length).and_return { 100 }
|
157
|
+
vlc.stub(:time).and_return { 10 }
|
158
|
+
vlc.progress.should eq(10)
|
159
|
+
|
160
|
+
vlc.stub(:length).and_return { 100 }
|
161
|
+
vlc.stub(:time).and_return { 100 }
|
162
|
+
vlc.progress.should eq(100)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'is aware of current status' do
|
167
|
+
tcp = mock_tcp_server(:defaults => false)
|
168
|
+
tcp.should_receive(:flush).with(no_args).any_number_of_times
|
169
|
+
|
170
|
+
tcp.should_receive(:gets).with(no_args).twice.and_return("")
|
171
|
+
|
172
|
+
tcp.should_receive(:puts).once.with('is_playing')
|
173
|
+
tcp.should_receive(:gets).once.with(no_args).and_return("> > 0")
|
174
|
+
|
175
|
+
tcp.should_receive(:puts).once.with('add http://example.org/media.mp3')
|
176
|
+
|
177
|
+
tcp.should_receive(:puts).once.with('is_playing')
|
178
|
+
tcp.should_receive(:gets).once.with(no_args).and_return("> > 1")
|
179
|
+
|
180
|
+
tcp.should_receive(:close).with(no_args)
|
181
|
+
|
182
|
+
vlc.connect
|
183
|
+
|
184
|
+
vlc.should be_stopped
|
185
|
+
vlc.play('http://example.org/media.mp3')
|
186
|
+
vlc.should be_playing
|
187
|
+
end
|
188
|
+
end
|