vlc-client 0.0.1.beta

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,10 @@
1
+ module VLC
2
+ class Client
3
+ module VideoControls
4
+ # Toggles fullscreen on/off
5
+ def fullscreen
6
+ @connection.write('fullscreen')
7
+ end
8
+ end
9
+ end
10
+ end
@@ -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,12 @@
1
+ # @private
2
+ module Core
3
+ module Ext
4
+ module Array
5
+ def extract_options!
6
+ last.is_a?(::Hash) ? pop : {}
7
+ end unless method_defined?(:extract_options!)
8
+ end
9
+ end
10
+ end
11
+
12
+ Array.send(:include, Core::Ext::Array)
@@ -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
@@ -0,0 +1,3 @@
1
+ module VLC
2
+ VERSION = "0.0.1.beta"
3
+ 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
@@ -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