#!/usr/bin/env python # -*- coding: utf-8 -*- # # Copyright (c) 2009-2010, Atzm WATANABE # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # # $Id$ import sys import os import os.path import glob import commands import gettext import gobject import gtk import gst from ConfigParser import SafeConfigParser, NoSectionError, NoOptionError from cStringIO import StringIO __author__ = '$Author$' __copyright__ = 'Copyright 2009-2010, Atzm WATANABE' __credits__ = [__author__] __license__ = 'BSD-2' __version__ = '$Revision$' __maintainer__ = __author__ __email__ = 'atzm@atzm.org' __status__ = 'Development' CMD_CH = 'ivtv-tune -d %s -t %s -c %d > /dev/null 2>&1' CMD_BL = 'v4l2-ctl -d %s -t %s > /dev/null 2>&1' CMD_BL_GET = 'v4l2-ctl -d %s -T' CMD_IN = 'v4l2-ctl -d %s --set-input %d > /dev/null 2>&1' CMD_IN_GET = "v4l2-ctl -d %s --get-input | awk '{print $4}'" CMD_INLST_GET = 'v4l2-ctl -d %s --list-input' CMD_CTRL_SET = 'v4l2-ctl -d %s -c %s=%d' CMD_CTRL_GET = "v4l2-ctl -d %s -C %s | awk -F': ' '{print $2}'" CMD_STND_SET = 'v4l2-ctl -d %s -s %d' CMD_STND_GET = "v4l2-ctl -d %s -S | head -1 | awk -F= '{print $2}'" CMD_STNDLST_GET = 'v4l2-ctl -d %s --list-standards' gettext.install('tvctl') config = SafeConfigParser() config.readfp( StringIO(""" [gui] channel_max: 12 channel_width: 3 """)) config.read(os.path.expanduser('~/.tvctl')) def v4l2_ctl_get(*args): return commands.getoutput(CMD_CTRL_GET % args) def v4l2_ctl_set(*args): return commands.getoutput(CMD_CTRL_SET % args) def get_inputs(*args): val = commands.getoutput(CMD_INLST_GET % args) _dict = {} for item in [v.split('\n') for v in val.split('\n\n')]: _input = _name = None for i in item: key, value = [v.strip() for v in i.split(':', 1)] key = key.lower() if key == 'input': _input = int(value) elif key == 'name': _name = value if None not in [_input, _name]: _dict[_input] = _name return _dict def get_audio_mode(*args): val = commands.getoutput(CMD_BL_GET % args) for item in [v.split('\n') for v in val.split('\n\n')]: for i in item: key, value = [v.strip() for v in i.split(':', 1)] key = key.lower() if key == 'current audio mode': return value raise RuntimeError(val) def get_standards(*args): val = commands.getoutput(CMD_STNDLST_GET % args) _list = [v.split('\n') for v in val.split('\n\n')] for item in _list: _index = _id = _name = None for i in item: key, value = [v.strip() for v in i.split(':', 1)] key = key.lower() # index is zero origin :-) if key == 'index': _index = int(value) elif key == 'id': _id = int(value, 16) elif key == 'name': _name = value if None not in [_index, _id, _name]: _list[_index] = (_id, _name) return _list class ChannelTable(gtk.Frame): def __init__(self, device='/dev/video0', max_ch=config.getint('gui', 'channel_max'), width=config.getint('gui', 'channel_width')): gtk.Frame.__init__(self, label=_('Channel')) self.set_shadow_type(gtk.SHADOW_ETCHED_OUT) self.set_border_width(3) self._tooltips = gtk.Tooltips() self._table = gtk.Table(width, max_ch / width) self._device = device self._buttons = {} _prev_radio_button = None for _ch in range(max_ch): ch = _ch + 1 col = _ch % width row = _ch / width ch = str(ch) b = gtk.RadioButton(group=_prev_radio_button, label=ch) b.set_mode(False) b.connect('toggled', self._button_toggled, ch) try: tip = config.get('channel_alias', ch) self._tooltips.set_tip(b, tip) except (NoSectionError, NoOptionError): pass self._buttons[ch] = _prev_radio_button = b self._table.attach(b, col, col + 1, row, row + 1, gtk.FILL, gtk.FILL, 2, 2) self.add(self._table) def _button_toggled(self, button, ch): if not button.get_active(): return True freq = 'japan-bcast' if config.has_option('channel_assign', ch): freq, ch = config.get('channel_assign', ch).split() os.system(CMD_CH % (self._device, freq, int(ch))) class AudioModeTable(gtk.Frame): def __init__(self, device='/dev/video0'): gtk.Frame.__init__(self, label=_('Audio Mode')) self.set_shadow_type(gtk.SHADOW_ETCHED_OUT) self.set_border_width(3) self._vbox = gtk.VBox(True, 2) self._device = device self._buttons = {} self._table = { _('Mono'): 'mono', _('Stereo'): 'stereo', _('Lang1'): 'lang1', _('Lang2'): 'lang2', _('Bilingual'): 'bilingual', } _cur_mode = get_audio_mode(device) _prev_radio_button = None for label in sorted(self._table.keys()): mode = self._table[label] b = gtk.RadioButton(group=_prev_radio_button, label=label) if mode == _cur_mode: b.set_active(True) b.set_mode(False) b.connect('toggled', self._button_toggled, mode) self._buttons[label] = _prev_radio_button = b self._vbox.pack_start(b) self.add(self._vbox) def _button_toggled(self, button, mode): if not button.get_active(): return True os.system(CMD_BL % (self._device, mode)) class InputTable(gtk.Frame): def __init__(self, device='/dev/video0'): gtk.Frame.__init__(self, label=_('Input')) self.set_shadow_type(gtk.SHADOW_ETCHED_OUT) self.set_border_width(3) self._vbox = gtk.VBox(True, 2) self._device = device self._buttons = {} self._table = get_inputs(device) _cur_input = int(commands.getoutput(CMD_IN_GET % device)) _prev_radio_button = None for num in sorted(self._table.keys()): label = self._table[num] b = gtk.RadioButton(group=_prev_radio_button, label=label) if num == _cur_input: b.set_active(True) b.set_mode(False) b.connect('toggled', self._button_toggled, num) self._buttons[label] = _prev_radio_button = b self._vbox.pack_start(b) self.add(self._vbox) def _button_toggled(self, button, num): if not button.get_active(): return True os.system(CMD_IN % (self._device, num)) class StandardsComboBox(gtk.ComboBox): def __init__(self, device='/dev/video0'): self._device = device self._standards = get_standards(device) self._list_store = gtk.ListStore(gobject.TYPE_STRING) self._cell = gtk.CellRendererText() gtk.ComboBox.__init__(self, self._list_store) self.pack_start(self._cell, True) self.add_attribute(self._cell, 'text', 0) for _id, _name in self._standards: self.append_text(_name) self._set_active_by_id( int(commands.getoutput(CMD_STND_GET % self._device), 16)) self.connect('changed', self._changed) def _set_active_by_id(self, _id): for i in range(len(self._standards)): if self._standards[i][0] == _id: self.set_active(i) break def _changed(self, combo_box): os.system(CMD_STND_SET % (self._device, combo_box.get_active())) class V4L2ControlScale(gtk.Frame): def __init__(self, label, ctrl, max_val, device='/dev/video0'): self._device = device self._label = label self._ctrl = ctrl self._max_val = max_val gtk.Frame.__init__(self, label=self._label) self.set_shadow_type(gtk.SHADOW_ETCHED_OUT) self.set_border_width(3) current = v4l2_ctl_get(self._device, self._ctrl) default = int(float(current) / self._max_val * 100) self._adj = gtk.Adjustment(default, 0, 100, 1, 10, 0) self._adj.connect('value-changed', self._adj_value_changed) self._hscale = gtk.HScale(self._adj) self._hscale.set_value_pos(gtk.POS_RIGHT) self._hscale.set_digits(0) self.vbox = gtk.VBox(False, 0) self.vbox.pack_start(self._hscale) self.add(self.vbox) def _adj_value_changed(self, adj): val = self._max_val * (adj.get_value() / 100) return v4l2_ctl_set(self._device, self._ctrl, val) class VolumeScale(V4L2ControlScale): def __init__(self, device='/dev/video0'): V4L2ControlScale.__init__(self, device=device, label=_('Volume'), ctrl='volume', max_val=65535) mute = bool(int(v4l2_ctl_get(self._device, 'mute'))) self._mute_button = gtk.CheckButton(label=_('Mute')) self._mute_button.set_active(mute) self._mute_button.connect('toggled', self._button_toggled) self.get_child().pack_start(self._mute_button) def _button_toggled(self, button): mute = int(button.get_active()) v4l2_ctl_set(self._device, 'mute', mute) class PlayButton(gtk.Button): def __init__(self, device='/dev/video0'): gtk.Button.__init__(self) self._image = gtk.image_new_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON) self.set_property('image', self._image) self._player = gst.element_factory_make('playbin2', 'player') self._player.set_property('uri', 'file://' + device) bus = self._player.get_bus() bus.add_signal_watch() bus.connect('message', self._caught_message) self.connect('clicked', self._button_clicked) def _start(self): self._player.set_state(gst.STATE_PLAYING) self._image.set_from_stock(gtk.STOCK_MEDIA_STOP, gtk.ICON_SIZE_BUTTON) def _stop(self): self._player.set_state(gst.STATE_NULL) self._image.set_from_stock(gtk.STOCK_MEDIA_PLAY, gtk.ICON_SIZE_BUTTON) def _button_clicked(self, button): if self._image.get_stock()[0] == gtk.STOCK_MEDIA_PLAY: self._start() else: self._stop() def _caught_message(self, bus, msg): if msg.type in [gst.MESSAGE_EOS, gst.MESSAGE_ERROR]: self._stop() class DeviceNotebook(gtk.Notebook): def __init__(self): gtk.Notebook.__init__(self) self._devices = glob.glob('/dev/video?') self._devices.sort() for d in self._devices: hbox = gtk.HBox(False, 5) hbox.pack_start(ChannelTable(device=d)) hbox.pack_start(AudioModeTable(device=d)) hbox.pack_start(InputTable(device=d)) hbox2 = gtk.HBox(False, 5) hbox2.pack_start(PlayButton(device=d)) hbox2.pack_start(StandardsComboBox(device=d)) volume = VolumeScale(device=d) balance = V4L2ControlScale(device=d, label=_('Balance'), ctrl='balance', max_val=65535) brightness = V4L2ControlScale(device=d, label=_('Brightness'), ctrl='brightness', max_val=255) contrast = V4L2ControlScale(device=d, label=_('Contrast'), ctrl='contrast', max_val=127) vbox = gtk.VBox(False, 0) vbox.pack_start(hbox) vbox.pack_start(hbox2) vbox.pack_start(volume) vbox.pack_start(balance) vbox.pack_start(brightness) vbox.pack_start(contrast) self.append_page(vbox, gtk.Label(os.path.basename(d))) def main(): notebook = DeviceNotebook() window = gtk.Window() window.set_title(_('TV Controller')) window.connect('destroy', lambda w: gtk.main_quit()) window.add(notebook) window.show_all() gtk.main() if __name__ == '__main__': main()