1 | #!/usr/bin/env python |
---|
2 | # -*- coding: utf-8 -*- |
---|
3 | # |
---|
4 | # Copyright (c) 2009-2010, Atzm WATANABE |
---|
5 | # All rights reserved. |
---|
6 | # |
---|
7 | # Redistribution and use in source and binary forms, with or without |
---|
8 | # modification, are permitted provided that the following conditions are met: |
---|
9 | # |
---|
10 | # 1. Redistributions of source code must retain the above copyright notice, |
---|
11 | # this list of conditions and the following disclaimer. |
---|
12 | # 2. Redistributions in binary form must reproduce the above copyright |
---|
13 | # notice, this list of conditions and the following disclaimer in the |
---|
14 | # documentation and/or other materials provided with the distribution. |
---|
15 | # |
---|
16 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" |
---|
17 | # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
---|
18 | # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE |
---|
19 | # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE |
---|
20 | # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
---|
21 | # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
---|
22 | # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
---|
23 | # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
---|
24 | # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
---|
25 | # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
---|
26 | # POSSIBILITY OF SUCH DAMAGE. |
---|
27 | # |
---|
28 | # $Id$ |
---|
29 | |
---|
30 | import sys |
---|
31 | import os |
---|
32 | import os.path |
---|
33 | import glob |
---|
34 | import commands |
---|
35 | import gettext |
---|
36 | import gobject |
---|
37 | import gtk |
---|
38 | import gst |
---|
39 | |
---|
40 | from ConfigParser import SafeConfigParser, NoSectionError, NoOptionError |
---|
41 | from cStringIO import StringIO |
---|
42 | |
---|
43 | __author__ = '$Author$' |
---|
44 | __copyright__ = 'Copyright 2009-2010, Atzm WATANABE' |
---|
45 | __credits__ = [__author__] |
---|
46 | __license__ = 'BSD-2' |
---|
47 | __version__ = '$Revision$' |
---|
48 | __maintainer__ = __author__ |
---|
49 | __email__ = 'atzm@atzm.org' |
---|
50 | __status__ = 'Development' |
---|
51 | |
---|
52 | CMD_CH = 'ivtv-tune -d %s -t %s -c %d > /dev/null 2>&1' |
---|
53 | CMD_BL = 'v4l2-ctl -d %s -t %s > /dev/null 2>&1' |
---|
54 | CMD_BL_GET = 'v4l2-ctl -d %s -T' |
---|
55 | CMD_IN = 'v4l2-ctl -d %s --set-input %d > /dev/null 2>&1' |
---|
56 | CMD_IN_GET = "v4l2-ctl -d %s --get-input | awk '{print $4}'" |
---|
57 | CMD_INLST_GET = 'v4l2-ctl -d %s --list-input' |
---|
58 | CMD_CTRL_SET = 'v4l2-ctl -d %s -c %s=%d' |
---|
59 | CMD_CTRL_GET = "v4l2-ctl -d %s -C %s | awk -F': ' '{print $2}'" |
---|
60 | CMD_STND_SET = 'v4l2-ctl -d %s -s %d' |
---|
61 | CMD_STND_GET = "v4l2-ctl -d %s -S | head -1 | awk -F= '{print $2}'" |
---|
62 | CMD_STNDLST_GET = 'v4l2-ctl -d %s --list-standards' |
---|
63 | |
---|
64 | gettext.install('tvctl') |
---|
65 | |
---|
66 | config = SafeConfigParser() |
---|
67 | config.readfp( |
---|
68 | StringIO(""" |
---|
69 | [gui] |
---|
70 | channel_max: 12 |
---|
71 | channel_width: 3 |
---|
72 | """)) |
---|
73 | config.read(os.path.expanduser('~/.tvctl')) |
---|
74 | |
---|
75 | |
---|
76 | def v4l2_ctl_get(*args): |
---|
77 | return commands.getoutput(CMD_CTRL_GET % args) |
---|
78 | |
---|
79 | |
---|
80 | def v4l2_ctl_set(*args): |
---|
81 | return commands.getoutput(CMD_CTRL_SET % args) |
---|
82 | |
---|
83 | |
---|
84 | def get_inputs(*args): |
---|
85 | val = commands.getoutput(CMD_INLST_GET % args) |
---|
86 | _dict = {} |
---|
87 | |
---|
88 | for item in [v.split('\n') for v in val.split('\n\n')]: |
---|
89 | _input = _name = None |
---|
90 | |
---|
91 | for i in item: |
---|
92 | key, value = [v.strip() for v in i.split(':', 1)] |
---|
93 | key = key.lower() |
---|
94 | |
---|
95 | if key == 'input': |
---|
96 | _input = int(value) |
---|
97 | elif key == 'name': |
---|
98 | _name = value |
---|
99 | |
---|
100 | if None not in [_input, _name]: |
---|
101 | _dict[_input] = _name |
---|
102 | |
---|
103 | return _dict |
---|
104 | |
---|
105 | |
---|
106 | def get_audio_mode(*args): |
---|
107 | val = commands.getoutput(CMD_BL_GET % args) |
---|
108 | |
---|
109 | for item in [v.split('\n') for v in val.split('\n\n')]: |
---|
110 | for i in item: |
---|
111 | key, value = [v.strip() for v in i.split(':', 1)] |
---|
112 | key = key.lower() |
---|
113 | |
---|
114 | if key == 'current audio mode': |
---|
115 | return value |
---|
116 | |
---|
117 | raise RuntimeError(val) |
---|
118 | |
---|
119 | |
---|
120 | def get_standards(*args): |
---|
121 | val = commands.getoutput(CMD_STNDLST_GET % args) |
---|
122 | _list = [v.split('\n') for v in val.split('\n\n')] |
---|
123 | |
---|
124 | for item in _list: |
---|
125 | _index = _id = _name = None |
---|
126 | |
---|
127 | for i in item: |
---|
128 | key, value = [v.strip() for v in i.split(':', 1)] |
---|
129 | key = key.lower() |
---|
130 | |
---|
131 | # index is zero origin :-) |
---|
132 | if key == 'index': |
---|
133 | _index = int(value) |
---|
134 | elif key == 'id': |
---|
135 | _id = int(value, 16) |
---|
136 | elif key == 'name': |
---|
137 | _name = value |
---|
138 | |
---|
139 | if None not in [_index, _id, _name]: |
---|
140 | _list[_index] = (_id, _name) |
---|
141 | |
---|
142 | return _list |
---|
143 | |
---|
144 | |
---|
145 | class ChannelTable(gtk.Frame): |
---|
146 | def __init__(self, device='/dev/video0', |
---|
147 | max_ch=config.getint('gui', 'channel_max'), |
---|
148 | width=config.getint('gui', 'channel_width')): |
---|
149 | |
---|
150 | gtk.Frame.__init__(self, label=_('Channel')) |
---|
151 | self.set_shadow_type(gtk.SHADOW_ETCHED_OUT) |
---|
152 | self.set_border_width(3) |
---|
153 | |
---|
154 | self._tooltips = gtk.Tooltips() |
---|
155 | self._table = gtk.Table(width, max_ch / width) |
---|
156 | self._device = device |
---|
157 | self._buttons = {} |
---|
158 | |
---|
159 | _prev_radio_button = None |
---|
160 | |
---|
161 | for _ch in range(max_ch): |
---|
162 | ch = _ch + 1 |
---|
163 | col = _ch % width |
---|
164 | row = _ch / width |
---|
165 | ch = str(ch) |
---|
166 | |
---|
167 | b = gtk.RadioButton(group=_prev_radio_button, label=ch) |
---|
168 | b.set_mode(False) |
---|
169 | b.connect('toggled', self._button_toggled, ch) |
---|
170 | |
---|
171 | try: |
---|
172 | tip = config.get('channel_alias', ch) |
---|
173 | self._tooltips.set_tip(b, tip) |
---|
174 | except (NoSectionError, NoOptionError): |
---|
175 | pass |
---|
176 | |
---|
177 | self._buttons[ch] = _prev_radio_button = b |
---|
178 | self._table.attach(b, col, col + 1, row, row + 1, |
---|
179 | gtk.FILL, gtk.FILL, 2, 2) |
---|
180 | |
---|
181 | self.add(self._table) |
---|
182 | |
---|
183 | def _button_toggled(self, button, ch): |
---|
184 | if not button.get_active(): |
---|
185 | return True |
---|
186 | |
---|
187 | freq = 'japan-bcast' |
---|
188 | if config.has_option('channel_assign', ch): |
---|
189 | freq, ch = config.get('channel_assign', ch).split() |
---|
190 | |
---|
191 | os.system(CMD_CH % (self._device, freq, int(ch))) |
---|
192 | |
---|
193 | |
---|
194 | class AudioModeTable(gtk.Frame): |
---|
195 | def __init__(self, device='/dev/video0'): |
---|
196 | gtk.Frame.__init__(self, label=_('Audio Mode')) |
---|
197 | self.set_shadow_type(gtk.SHADOW_ETCHED_OUT) |
---|
198 | self.set_border_width(3) |
---|
199 | |
---|
200 | self._vbox = gtk.VBox(True, 2) |
---|
201 | self._device = device |
---|
202 | self._buttons = {} |
---|
203 | self._table = { |
---|
204 | _('Mono'): 'mono', |
---|
205 | _('Stereo'): 'stereo', |
---|
206 | _('Lang1'): 'lang1', |
---|
207 | _('Lang2'): 'lang2', |
---|
208 | _('Bilingual'): 'bilingual', |
---|
209 | } |
---|
210 | |
---|
211 | _cur_mode = get_audio_mode(device) |
---|
212 | _prev_radio_button = None |
---|
213 | |
---|
214 | for label in sorted(self._table.keys()): |
---|
215 | mode = self._table[label] |
---|
216 | b = gtk.RadioButton(group=_prev_radio_button, label=label) |
---|
217 | if mode == _cur_mode: |
---|
218 | b.set_active(True) |
---|
219 | b.set_mode(False) |
---|
220 | b.connect('toggled', self._button_toggled, mode) |
---|
221 | self._buttons[label] = _prev_radio_button = b |
---|
222 | self._vbox.pack_start(b) |
---|
223 | |
---|
224 | self.add(self._vbox) |
---|
225 | |
---|
226 | def _button_toggled(self, button, mode): |
---|
227 | if not button.get_active(): |
---|
228 | return True |
---|
229 | os.system(CMD_BL % (self._device, mode)) |
---|
230 | |
---|
231 | |
---|
232 | class InputTable(gtk.Frame): |
---|
233 | def __init__(self, device='/dev/video0'): |
---|
234 | gtk.Frame.__init__(self, label=_('Input')) |
---|
235 | self.set_shadow_type(gtk.SHADOW_ETCHED_OUT) |
---|
236 | self.set_border_width(3) |
---|
237 | |
---|
238 | self._vbox = gtk.VBox(True, 2) |
---|
239 | self._device = device |
---|
240 | self._buttons = {} |
---|
241 | self._table = get_inputs(device) |
---|
242 | |
---|
243 | _cur_input = int(commands.getoutput(CMD_IN_GET % device)) |
---|
244 | _prev_radio_button = None |
---|
245 | |
---|
246 | for num in sorted(self._table.keys()): |
---|
247 | label = self._table[num] |
---|
248 | b = gtk.RadioButton(group=_prev_radio_button, label=label) |
---|
249 | if num == _cur_input: |
---|
250 | b.set_active(True) |
---|
251 | b.set_mode(False) |
---|
252 | b.connect('toggled', self._button_toggled, num) |
---|
253 | self._buttons[label] = _prev_radio_button = b |
---|
254 | self._vbox.pack_start(b) |
---|
255 | |
---|
256 | self.add(self._vbox) |
---|
257 | |
---|
258 | def _button_toggled(self, button, num): |
---|
259 | if not button.get_active(): |
---|
260 | return True |
---|
261 | os.system(CMD_IN % (self._device, num)) |
---|
262 | |
---|
263 | |
---|
264 | class StandardsComboBox(gtk.ComboBox): |
---|
265 | def __init__(self, device='/dev/video0'): |
---|
266 | self._device = device |
---|
267 | self._standards = get_standards(device) |
---|
268 | self._list_store = gtk.ListStore(gobject.TYPE_STRING) |
---|
269 | self._cell = gtk.CellRendererText() |
---|
270 | |
---|
271 | gtk.ComboBox.__init__(self, self._list_store) |
---|
272 | self.pack_start(self._cell, True) |
---|
273 | self.add_attribute(self._cell, 'text', 0) |
---|
274 | |
---|
275 | for _id, _name in self._standards: |
---|
276 | self.append_text(_name) |
---|
277 | |
---|
278 | self._set_active_by_id( |
---|
279 | int(commands.getoutput(CMD_STND_GET % self._device), 16)) |
---|
280 | |
---|
281 | self.connect('changed', self._changed) |
---|
282 | |
---|
283 | def _set_active_by_id(self, _id): |
---|
284 | for i in range(len(self._standards)): |
---|
285 | if self._standards[i][0] == _id: |
---|
286 | self.set_active(i) |
---|
287 | break |
---|
288 | |
---|
289 | def _changed(self, combo_box): |
---|
290 | os.system(CMD_STND_SET % (self._device, combo_box.get_active())) |
---|
291 | |
---|
292 | |
---|
293 | class V4L2ControlScale(gtk.Frame): |
---|
294 | def __init__(self, label, ctrl, max_val, device='/dev/video0'): |
---|
295 | self._device = device |
---|
296 | self._label = label |
---|
297 | self._ctrl = ctrl |
---|
298 | self._max_val = max_val |
---|
299 | |
---|
300 | gtk.Frame.__init__(self, label=self._label) |
---|
301 | self.set_shadow_type(gtk.SHADOW_ETCHED_OUT) |
---|
302 | self.set_border_width(3) |
---|
303 | |
---|
304 | current = v4l2_ctl_get(self._device, self._ctrl) |
---|
305 | default = int(float(current) / self._max_val * 100) |
---|
306 | |
---|
307 | self._adj = gtk.Adjustment(default, 0, 100, 1, 10, 0) |
---|
308 | self._adj.connect('value-changed', self._adj_value_changed) |
---|
309 | |
---|
310 | self._hscale = gtk.HScale(self._adj) |
---|
311 | self._hscale.set_value_pos(gtk.POS_RIGHT) |
---|
312 | self._hscale.set_digits(0) |
---|
313 | |
---|
314 | self.vbox = gtk.VBox(False, 0) |
---|
315 | self.vbox.pack_start(self._hscale) |
---|
316 | |
---|
317 | self.add(self.vbox) |
---|
318 | |
---|
319 | def _adj_value_changed(self, adj): |
---|
320 | val = self._max_val * (adj.get_value() / 100) |
---|
321 | return v4l2_ctl_set(self._device, self._ctrl, val) |
---|
322 | |
---|
323 | |
---|
324 | class VolumeScale(V4L2ControlScale): |
---|
325 | def __init__(self, device='/dev/video0'): |
---|
326 | V4L2ControlScale.__init__(self, device=device, label=_('Volume'), |
---|
327 | ctrl='volume', max_val=65535) |
---|
328 | |
---|
329 | mute = bool(int(v4l2_ctl_get(self._device, 'mute'))) |
---|
330 | |
---|
331 | self._mute_button = gtk.CheckButton(label=_('Mute')) |
---|
332 | self._mute_button.set_active(mute) |
---|
333 | self._mute_button.connect('toggled', self._button_toggled) |
---|
334 | self.get_child().pack_start(self._mute_button) |
---|
335 | |
---|
336 | def _button_toggled(self, button): |
---|
337 | mute = int(button.get_active()) |
---|
338 | v4l2_ctl_set(self._device, 'mute', mute) |
---|
339 | |
---|
340 | |
---|
341 | class PlayButton(gtk.Button): |
---|
342 | def __init__(self, device='/dev/video0'): |
---|
343 | gtk.Button.__init__(self) |
---|
344 | |
---|
345 | self._image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, |
---|
346 | gtk.ICON_SIZE_BUTTON) |
---|
347 | self.set_property('image', self._image) |
---|
348 | |
---|
349 | self._player = gst.element_factory_make('playbin2', 'player') |
---|
350 | self._player.set_property('uri', 'file://' + device) |
---|
351 | |
---|
352 | bus = self._player.get_bus() |
---|
353 | bus.add_signal_watch() |
---|
354 | bus.connect('message', self._caught_message) |
---|
355 | |
---|
356 | self.connect('clicked', self._button_clicked) |
---|
357 | |
---|
358 | def _start(self): |
---|
359 | self._player.set_state(gst.STATE_PLAYING) |
---|
360 | self._image.set_from_stock(gtk.STOCK_MEDIA_STOP, gtk.ICON_SIZE_BUTTON) |
---|
361 | |
---|
362 | def _stop(self): |
---|
363 | self._player.set_state(gst.STATE_NULL) |
---|
364 | self._image.set_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON) |
---|
365 | |
---|
366 | def _button_clicked(self, button): |
---|
367 | if self._image.get_stock()[0] == gtk.STOCK_MEDIA_PLAY: |
---|
368 | self._start() |
---|
369 | else: |
---|
370 | self._stop() |
---|
371 | |
---|
372 | def _caught_message(self, bus, msg): |
---|
373 | if msg.type in [gst.MESSAGE_EOS, gst.MESSAGE_ERROR]: |
---|
374 | self._stop() |
---|
375 | |
---|
376 | |
---|
377 | class DeviceNotebook(gtk.Notebook): |
---|
378 | def __init__(self): |
---|
379 | gtk.Notebook.__init__(self) |
---|
380 | self._devices = glob.glob('/dev/video?') |
---|
381 | self._devices.sort() |
---|
382 | |
---|
383 | for d in self._devices: |
---|
384 | hbox = gtk.HBox(False, 5) |
---|
385 | hbox.pack_start(ChannelTable(device=d)) |
---|
386 | hbox.pack_start(AudioModeTable(device=d)) |
---|
387 | hbox.pack_start(InputTable(device=d)) |
---|
388 | |
---|
389 | hbox2 = gtk.HBox(False, 5) |
---|
390 | hbox2.pack_start(PlayButton(device=d)) |
---|
391 | hbox2.pack_start(StandardsComboBox(device=d)) |
---|
392 | |
---|
393 | volume = VolumeScale(device=d) |
---|
394 | balance = V4L2ControlScale(device=d, label=_('Balance'), |
---|
395 | ctrl='balance', max_val=65535) |
---|
396 | brightness = V4L2ControlScale(device=d, label=_('Brightness'), |
---|
397 | ctrl='brightness', max_val=255) |
---|
398 | contrast = V4L2ControlScale(device=d, label=_('Contrast'), |
---|
399 | ctrl='contrast', max_val=127) |
---|
400 | |
---|
401 | vbox = gtk.VBox(False, 0) |
---|
402 | vbox.pack_start(hbox) |
---|
403 | vbox.pack_start(hbox2) |
---|
404 | vbox.pack_start(volume) |
---|
405 | vbox.pack_start(balance) |
---|
406 | vbox.pack_start(brightness) |
---|
407 | vbox.pack_start(contrast) |
---|
408 | |
---|
409 | self.append_page(vbox, gtk.Label(os.path.basename(d))) |
---|
410 | |
---|
411 | |
---|
412 | def main(): |
---|
413 | notebook = DeviceNotebook() |
---|
414 | |
---|
415 | window = gtk.Window() |
---|
416 | window.set_title(_('TV Controller')) |
---|
417 | window.connect('destroy', lambda w: gtk.main_quit()) |
---|
418 | |
---|
419 | window.add(notebook) |
---|
420 | window.show_all() |
---|
421 | gtk.main() |
---|
422 | |
---|
423 | |
---|
424 | if __name__ == '__main__': |
---|
425 | main() |
---|