vlc-client 0.0.1.beta

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