diff --git a/src/avwidgets.py b/src/avwidgets.py new file mode 100644 index 0000000..604768b --- /dev/null +++ b/src/avwidgets.py @@ -0,0 +1,106 @@ +from PySide import QtCore, QtGui +import widgets +import profile +import util +import pyaudio +import wave +import settings +from util import curr_directory + + +class IncomingCallWidget(widgets.CenteredWidget): + + def __init__(self, friend_number, text, name): + super(IncomingCallWidget, self).__init__() + self.setWindowFlags(QtCore.Qt.CustomizeWindowHint | QtCore.Qt.WindowTitleHint | QtCore.Qt.WindowStaysOnTopHint) + self.resize(QtCore.QSize(500, 270)) + self.avatar_label = QtGui.QLabel(self) + self.avatar_label.setGeometry(QtCore.QRect(10, 20, 64, 64)) + self.avatar_label.setScaledContents(False) + self.name = widgets.DataLabel(self) + self.name.setGeometry(QtCore.QRect(90, 20, 300, 25)) + font = QtGui.QFont() + font.setFamily("Times New Roman") + font.setPointSize(16) + font.setBold(True) + self.name.setFont(font) + self.call_type = widgets.DataLabel(self) + self.call_type.setGeometry(QtCore.QRect(90, 55, 300, 25)) + self.call_type.setFont(font) + self.accept_audio = QtGui.QPushButton(self) + self.accept_audio.setGeometry(QtCore.QRect(20, 100, 150, 150)) + self.accept_video = QtGui.QPushButton(self) + self.accept_video.setGeometry(QtCore.QRect(170, 100, 150, 150)) + self.decline = QtGui.QPushButton(self) + self.decline.setGeometry(QtCore.QRect(320, 100, 150, 150)) + pixmap = QtGui.QPixmap(util.curr_directory() + '/images/accept_audio.png') + icon = QtGui.QIcon(pixmap) + self.accept_audio.setIcon(icon) + pixmap = QtGui.QPixmap(util.curr_directory() + '/images/accept_video.png') + icon = QtGui.QIcon(pixmap) + self.accept_video.setIcon(icon) + pixmap = QtGui.QPixmap(util.curr_directory() + '/images/decline_call.png') + icon = QtGui.QIcon(pixmap) + self.decline.setIcon(icon) + self.accept_audio.setIconSize(QtCore.QSize(150, 150)) + self.accept_video.setIconSize(QtCore.QSize(140, 140)) + self.decline.setIconSize(QtCore.QSize(140, 140)) + self.accept_audio.setStyleSheet("QPushButton { border: none }") + self.accept_video.setStyleSheet("QPushButton { border: none }") + self.decline.setStyleSheet("QPushButton { border: none }") + self.setWindowTitle(text) + self.name.setText(name) + self.call_type.setText(text) + pr = profile.Profile.get_instance() + self.accept_audio.clicked.connect(lambda: pr.accept_call(friend_number, True, False) or self.stop()) + # self.accept_video.clicked.connect(lambda: pr.start_call(friend_number, True, True)) + self.decline.clicked.connect(lambda: pr.stop_call(friend_number, False) or self.stop()) + + class SoundPlay(QtCore.QThread): + + def __init__(self): + QtCore.QThread.__init__(self) + + def run(self): + class AudioFile(object): + chunk = 1024 + + def __init__(self, fl): + self.stop = False + self.wf = wave.open(fl, 'rb') + self.p = pyaudio.PyAudio() + self.stream = self.p.open( + format=self.p.get_format_from_width(self.wf.getsampwidth()), + channels=self.wf.getnchannels(), + rate=self.wf.getframerate(), + output=True + ) + + def play(self): + data = self.wf.readframes(self.chunk) + while data and not self.stop: + self.stream.write(data) + data = self.wf.readframes(self.chunk) + + def close(self): + self.stream.close() + self.p.terminate() + + self.a = AudioFile(curr_directory() + '/sounds/call.wav') + self.a.play() + self.a.close() + + if settings.Settings.get_instance()['calls_sound']: + self.thread = SoundPlay() + self.thread.start() + else: + self.thread = None + + def stop(self): + if self.thread is not None: + self.thread.a.stop = True + self.thread.wait() + self.close() + + def set_pixmap(self, pixmap): + self.avatar_label.setPixmap(pixmap) diff --git a/src/callbacks.py b/src/callbacks.py index ee38f0c..0102af6 100644 --- a/src/callbacks.py +++ b/src/callbacks.py @@ -3,6 +3,7 @@ from notifications import * from settings import Settings from profile import Profile from toxcore_enums_and_consts import * +from toxav_enums import * from tox import bin_to_string from ctypes import c_char_p, cast, pointer @@ -200,6 +201,35 @@ def file_recv_control(tox, friend_number, file_number, file_control, user_data): if file_control == TOX_FILE_CONTROL['CANCEL']: Profile.get_instance().cancel_transfer(friend_number, file_number, True) + +# ----------------------------------------------------------------------------------------------------------------- +# Callbacks - audio +# ----------------------------------------------------------------------------------------------------------------- + +def call_state(toxav, friend_number, mask, user_data): + """New call state""" + print friend_number, mask + if mask == TOXAV_FRIEND_CALL_STATE['FINISHED'] or mask == TOXAV_FRIEND_CALL_STATE['ERROR']: + invoke_in_main_thread(Profile.get_instance().stop_call, friend_number, True) + else: + Profile.get_instance().call.toxav_call_state_cb(friend_number, mask) + + +def call(toxav, friend_number, audio, video, user_data): + """Incoming call from friend""" + print friend_number, audio, video + invoke_in_main_thread(Profile.get_instance().incoming_call, audio, video, friend_number) + + +def callback_audio(toxav, friend_number, samples, audio_samples_per_channel, audio_channels_count, rate, user_data): + """New audio chunk""" + print audio_samples_per_channel, audio_channels_count, rate + Profile.get_instance().call.chunk( + ''.join(chr(x) for x in samples[:audio_samples_per_channel * 2 * audio_channels_count]), + audio_channels_count, + rate) + + # ----------------------------------------------------------------------------------------------------------------- # Callbacks - initialization # ----------------------------------------------------------------------------------------------------------------- @@ -225,3 +255,9 @@ def init_callbacks(tox, window, tray): tox.callback_file_recv_chunk(file_recv_chunk, 0) tox.callback_file_chunk_request(file_chunk_request, 0) tox.callback_file_recv_control(file_recv_control, 0) + + toxav = tox.AV + toxav.callback_call_state(call_state, 0) + toxav.callback_call(call, 0) + toxav.callback_audio_receive_frame(callback_audio, 0) + diff --git a/src/calls.py b/src/calls.py new file mode 100644 index 0000000..b85d4f5 --- /dev/null +++ b/src/calls.py @@ -0,0 +1,143 @@ +import pyaudio +import time +import threading +import settings +from toxav_enums import * +# TODO: play sound until outgoing call will be started or cancelled + +CALL_TYPE = { + 'NONE': 0, + 'AUDIO': 1, + 'VIDEO': 2 +} + + +class AV(object): + + def __init__(self, toxav): + self._toxav = toxav + self._running = True + + self._calls = {} # dict: key - friend number, value - call type + + self._audio = None + self._audio_stream = None + self._audio_thread = None + self._audio_running = False + self._out_stream = None + + self._audio_rate = 8000 + self._audio_channels = 1 + self._audio_duration = 60 + self._audio_sample_count = self._audio_rate * self._audio_channels * self._audio_duration / 1000 + + def __contains__(self, friend_number): + return friend_number in self._calls + + def __call__(self, friend_number, audio, video): + """Call friend with specified number""" + self._toxav.call(friend_number, 32 if audio else 0, 5000 if video else 0) + self._calls[friend_number] = CALL_TYPE['AUDIO'] + self.start_audio_thread() + + def finish_call(self, friend_number, by_friend=False): + + if not by_friend: + self._toxav.call_control(friend_number, TOXAV_CALL_CONTROL['CANCEL']) + if friend_number in self._calls: + del self._calls[friend_number] + if not len(self._calls): + self.stop_audio_thread() + + def stop(self): + self._running = False + self.stop_audio_thread() + + def start_audio_thread(self): + """ + Start audio sending + """ + if self._audio_thread is not None: + return + + self._audio_running = True + + self._audio = pyaudio.PyAudio() + self._audio_stream = self._audio.open(format=pyaudio.paInt16, + rate=self._audio_rate, + channels=self._audio_channels, + input=True, + input_device_index=settings.Settings().get_instance().audio['input'], + frames_per_buffer=self._audio_sample_count * 10) + + self._audio_thread = threading.Thread(target=self.send_audio) + self._audio_thread.start() + + def stop_audio_thread(self): + + if self._audio_thread is None: + return + + self._audio_running = False + + self._audio_thread.join() + + self._audio_thread = None + self._audio_stream = None + self._audio = None + + if self._out_stream is not None: + self._out_stream.stop_stream() + self._out_stream.close() + self._out_stream = None + + def chunk(self, samples, channels_count, rate): + """ + Incoming chunk + """ + + if self._out_stream is None: + self._out_stream = self._audio.open(format=pyaudio.paInt16, + channels=channels_count, + rate=rate, + output_device_index=settings.Settings().get_instance().audio['output'], + output=True) + self._out_stream.write(samples) + + def send_audio(self): + """ + This method sends audio to friends + """ + + while self._audio_running: + try: + pcm = self._audio_stream.read(self._audio_sample_count) + if pcm: + for friend in self._calls: + if self._calls[friend] & 1: + try: + self._toxav.audio_send_frame(friend, pcm, self._audio_sample_count, + self._audio_channels, self._audio_rate) + except: + pass + except: + pass + + time.sleep(0.01) + + def accept_call(self, friend_number, audio_enabled, video_enabled): + + if self._running: + self._calls[friend_number] = int(video_enabled) * 2 + int(audio_enabled) + self._toxav.answer(friend_number, 32 if audio_enabled else 0, 5000 if video_enabled else 0) + self.start_audio_thread() + + def toxav_call_state_cb(self, friend_number, state): + """ + New call state + """ + if self._running: + + if state & TOXAV_FRIEND_CALL_STATE['ACCEPTING_A']: + self._calls[friend_number] |= 1 + diff --git a/src/images/accept_audio.png b/src/images/accept_audio.png new file mode 100755 index 0000000..2fd2818 Binary files /dev/null and b/src/images/accept_audio.png differ diff --git a/src/images/accept_video.png b/src/images/accept_video.png new file mode 100755 index 0000000..2fdebe7 Binary files /dev/null and b/src/images/accept_video.png differ diff --git a/src/images/decline_call.png b/src/images/decline_call.png new file mode 100755 index 0000000..9f39789 Binary files /dev/null and b/src/images/decline_call.png differ diff --git a/src/images/finish_call.png b/src/images/finish_call.png new file mode 100755 index 0000000..a26c1fb Binary files /dev/null and b/src/images/finish_call.png differ diff --git a/src/images/incoming_call.png b/src/images/incoming_call.png new file mode 100755 index 0000000..f66fc60 Binary files /dev/null and b/src/images/incoming_call.png differ diff --git a/src/libtox.py b/src/libtox.py new file mode 100644 index 0000000..4a53562 --- /dev/null +++ b/src/libtox.py @@ -0,0 +1,33 @@ +from platform import system +from ctypes import CDLL + + +class LibToxCore(object): + + def __init__(self): + if system() == 'Linux': + # be sure that libtoxcore and libsodium are installed in your os + self._libtoxcore = CDLL('libtoxcore.so') + elif system() == 'Windows': + self._libtoxcore = CDLL('libs/libtox.dll') + else: + raise OSError('Unknown system.') + + def __getattr__(self, item): + return self._libtoxcore.__getattr__(item) + + +class LibToxAV(object): + + def __init__(self): + if system() == 'Linux': + # be sure that /usr/lib/libtoxav.so exists + self._libtoxav = CDLL('libtoxav.so') + elif system() == 'Windows': + # on Windows av api is in libtox.dll + self._libtoxav = CDLL('libs/libtox.dll') + else: + raise OSError('Unknown system.') + + def __getattr__(self, item): + return self._libtoxav.__getattr__(item) diff --git a/src/list_items.py b/src/list_items.py index 65ce2d5..05e3a3f 100644 --- a/src/list_items.py +++ b/src/list_items.py @@ -4,6 +4,7 @@ import profile from file_transfers import TOX_FILE_TRANSFER_STATE from util import curr_directory, convert_time from messages import FILE_TRANSFER_MESSAGE_STATUS +from widgets import DataLabel class MessageEdit(QtGui.QTextEdit): @@ -65,15 +66,6 @@ class MessageItem(QtGui.QWidget): self.message.setStyleSheet("QTextEdit { color: red; }") -class DataLabel(QtGui.QLabel): - - def paintEvent(self, event): - painter = QtGui.QPainter(self) - metrics = QtGui.QFontMetrics(self.font()) - text = metrics.elidedText(self.text(), QtCore.Qt.ElideRight, self.width()) - painter.drawText(self.rect(), self.alignment(), text) - - class ContactItem(QtGui.QWidget): """ Contact in friends list diff --git a/src/loginscreen.py b/src/loginscreen.py index 4aa5347..faf0034 100644 --- a/src/loginscreen.py +++ b/src/loginscreen.py @@ -1,11 +1,10 @@ # -*- coding: utf-8 -*- from PySide import QtCore, QtGui -import sys -import os +from widgets import * -class LoginScreen(QtGui.QWidget): +class LoginScreen(CenteredWidget): def __init__(self): super(LoginScreen, self).__init__() @@ -15,7 +14,6 @@ class LoginScreen(QtGui.QWidget): self.resize(400, 200) self.setMinimumSize(QtCore.QSize(400, 200)) self.setMaximumSize(QtCore.QSize(400, 200)) - self.setBaseSize(QtCore.QSize(400, 200)) self.new_profile = QtGui.QPushButton(self) self.new_profile.setGeometry(QtCore.QRect(20, 150, 171, 27)) self.new_profile.clicked.connect(self.create_profile) @@ -54,13 +52,6 @@ class LoginScreen(QtGui.QWidget): self.name = None self.retranslateUi() QtCore.QMetaObject.connectSlotsByName(self) - self.center() - - def center(self): - qr = self.frameGeometry() - cp = QtGui.QDesktopWidget().availableGeometry().center() - qr.moveCenter(cp) - self.move(qr.topLeft()) def retranslateUi(self): self.setWindowTitle(QtGui.QApplication.translate("login", "Log in", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/src/main.py b/src/main.py index 1417114..ee0f654 100644 --- a/src/main.py +++ b/src/main.py @@ -15,7 +15,7 @@ class Toxygen(object): def __init__(self): super(Toxygen, self).__init__() - self.tox = self.ms = self.init = self.mainloop = None + self.tox = self.ms = self.init = self.mainloop = self.avloop = None def main(self): """ @@ -123,15 +123,19 @@ class Toxygen(object): self.init = self.InitThread(self.tox, self.ms, self.tray) self.init.start() - # starting thread for tox iterate + # starting threads for tox iterate and toxav iterate self.mainloop = self.ToxIterateThread(self.tox) self.mainloop.start() + self.avloop = self.ToxAVIterateThread(self.tox.AV) + self.avloop.start() app.connect(app, QtCore.SIGNAL("lastWindowClosed()"), app, QtCore.SLOT("quit()")) app.exec_() self.init.stop = True self.mainloop.stop = True + self.avloop.stop = True self.mainloop.wait() self.init.wait() + self.avloop.wait() data = self.tox.get_savedata() ProfileHelper.save_profile(data) settings.close() @@ -144,8 +148,10 @@ class Toxygen(object): """ self.mainloop.stop = True self.init.stop = True + self.avloop.stop = True self.mainloop.wait() self.init.wait() + self.avloop.wait() data = self.tox.get_savedata() ProfileHelper.save_profile(data) del self.tox @@ -155,9 +161,12 @@ class Toxygen(object): self.init = self.InitThread(self.tox, self.ms, self.tray) self.init.start() - # starting thread for tox iterate + # starting threads for tox iterate and toxav iterate self.mainloop = self.ToxIterateThread(self.tox) self.mainloop.start() + + self.avloop = self.ToxAVIterateThread(self.tox.AV) + self.avloop.start() return self.tox # ----------------------------------------------------------------------------------------------------------------- @@ -209,6 +218,18 @@ class Toxygen(object): self.tox.iterate() self.msleep(self.tox.iteration_interval()) + class ToxAVIterateThread(QtCore.QThread): + + def __init__(self, toxav): + QtCore.QThread.__init__(self) + self.toxav = toxav + self.stop = False + + def run(self): + while not self.stop: + self.toxav.iterate() + self.msleep(self.toxav.iteration_interval()) + class Login(object): def __init__(self, arr): diff --git a/src/mainscreen.py b/src/mainscreen.py index ed6fb24..0dc8b9c 100644 --- a/src/mainscreen.py +++ b/src/mainscreen.py @@ -60,12 +60,14 @@ class MainWindow(QtGui.QMainWindow): self.actionAbout_program.setObjectName("actionAbout_program") self.actionSettings = QtGui.QAction(MainWindow) self.actionSettings.setObjectName("actionSettings") + self.audioSettings = QtGui.QAction(MainWindow) self.menuProfile.addAction(self.actionAdd_friend) self.menuProfile.addAction(self.actionSettings) self.menuSettings.addAction(self.actionPrivacy_settings) self.menuSettings.addAction(self.actionInterface_settings) self.menuSettings.addAction(self.actionNotifications) self.menuSettings.addAction(self.actionNetwork) + self.menuSettings.addAction(self.audioSettings) self.menuAbout.addAction(self.actionAbout_program) self.menubar.addAction(self.menuProfile.menuAction()) self.menubar.addAction(self.menuSettings.menuAction()) @@ -78,6 +80,7 @@ class MainWindow(QtGui.QMainWindow): self.actionPrivacy_settings.triggered.connect(self.privacy_settings) self.actionInterface_settings.triggered.connect(self.interface_settings) self.actionNotifications.triggered.connect(self.notification_settings) + self.audioSettings.triggered.connect(self.audio_settings) QtCore.QMetaObject.connectSlotsByName(MainWindow) def languageChange(self, *args, **kwargs): @@ -96,6 +99,7 @@ class MainWindow(QtGui.QMainWindow): self.actionNetwork.setText(QtGui.QApplication.translate("MainWindow", "Network", None, QtGui.QApplication.UnicodeUTF8)) self.actionAbout_program.setText(QtGui.QApplication.translate("MainWindow", "About program", None, QtGui.QApplication.UnicodeUTF8)) self.actionSettings.setText(QtGui.QApplication.translate("MainWindow", "Settings", None, QtGui.QApplication.UnicodeUTF8)) + self.audioSettings.setText(QtGui.QApplication.translate("MainWindow", "Audio", None, QtGui.QApplication.UnicodeUTF8)) def setup_right_bottom(self, Form): Form.setObjectName("right_bottom") @@ -202,10 +206,8 @@ class MainWindow(QtGui.QMainWindow): self.callButton = QtGui.QPushButton(Form) self.callButton.setGeometry(QtCore.QRect(550, 30, 50, 50)) self.callButton.setObjectName("callButton") - pixmap = QtGui.QPixmap(curr_directory() + '/images/call.png') - icon = QtGui.QIcon(pixmap) - self.callButton.setIcon(icon) - self.callButton.setIconSize(QtCore.QSize(50, 50)) + self.callButton.clicked.connect(self.call) + self.update_call_state('call') QtCore.QMetaObject.connectSlotsByName(Form) def setup_left_center(self, widget): @@ -271,6 +273,7 @@ class MainWindow(QtGui.QMainWindow): def closeEvent(self, *args, **kwargs): self.profile.save_history() + self.profile.close() QtGui.QApplication.closeAllWindows() # ----------------------------------------------------------------------------------------------------------------- @@ -308,8 +311,12 @@ class MainWindow(QtGui.QMainWindow): self.int_s = InterfaceSettings() self.int_s.show() + def audio_settings(self): + self.audio_s = AudioSettings() + self.audio_s.show() + # ----------------------------------------------------------------------------------------------------------------- - # Messages and file transfers + # Messages, calls and file transfers # ----------------------------------------------------------------------------------------------------------------- def send_message(self): @@ -328,6 +335,25 @@ class MainWindow(QtGui.QMainWindow): self.sw = ScreenShotWindow() self.sw.show() + def call(self): + if self.profile.is_active_online(): # active friend exists and online + self.profile.call_click(True) + + def active_call(self): + self.update_call_state('finish_call') + + def incoming_call(self): + self.update_call_state('incoming_call') + + def call_finished(self): + self.update_call_state('call') + + def update_call_state(self, fl): + pixmap = QtGui.QPixmap(curr_directory() + '/images/{}.png'.format(fl)) + icon = QtGui.QIcon(pixmap) + self.callButton.setIcon(icon) + self.callButton.setIconSize(QtCore.QSize(50, 50)) + # ----------------------------------------------------------------------------------------------------------------- # Functions which called when user open context menu in friends list # ----------------------------------------------------------------------------------------------------------------- diff --git a/src/menu.py b/src/menu.py index 344cfad..de02723 100644 --- a/src/menu.py +++ b/src/menu.py @@ -2,19 +2,8 @@ from PySide import QtCore, QtGui from settings import * from profile import Profile from util import get_style, curr_directory - - -class CenteredWidget(QtGui.QWidget): - - def __init__(self): - super(CenteredWidget, self).__init__() - self.center() - - def center(self): - qr = self.frameGeometry() - cp = QtGui.QDesktopWidget().availableGeometry().center() - qr.moveCenter(cp) - self.move(qr.topLeft()) +from widgets import CenteredWidget +import pyaudio class AddContact(CenteredWidget): @@ -442,3 +431,54 @@ class InterfaceSettings(CenteredWidget): app.installTranslator(app.translator) settings.save() + +class AudioSettings(CenteredWidget): + + def __init__(self): + super(AudioSettings, self).__init__() + self.initUI() + self.retranslateUi() + + def initUI(self): + self.setObjectName("audioSettingsForm") + self.resize(400, 150) + self.setMinimumSize(QtCore.QSize(400, 150)) + self.setMaximumSize(QtCore.QSize(400, 150)) + self.in_label = QtGui.QLabel(self) + self.in_label.setGeometry(QtCore.QRect(25, 5, 350, 20)) + self.out_label = QtGui.QLabel(self) + self.out_label.setGeometry(QtCore.QRect(25, 65, 350, 20)) + font = QtGui.QFont() + font.setPointSize(16) + font.setBold(True) + self.in_label.setFont(font) + self.out_label.setFont(font) + self.input = QtGui.QComboBox(self) + self.input.setGeometry(QtCore.QRect(25, 30, 350, 30)) + self.output = QtGui.QComboBox(self) + self.output.setGeometry(QtCore.QRect(25, 90, 350, 30)) + p = pyaudio.PyAudio() + settings = Settings.get_instance() + self.in_indexes, self.out_indexes = [], [] + for i in xrange(p.get_device_count()): + device = p.get_device_info_by_index(i) + if device["maxInputChannels"]: + self.input.addItem(unicode(device["name"])) + self.in_indexes.append(i) + if device["maxOutputChannels"]: + self.output.addItem(unicode(device["name"])) + self.out_indexes.append(i) + self.input.setCurrentIndex(self.in_indexes.index(settings.audio['input'])) + self.output.setCurrentIndex(self.out_indexes.index(settings.audio['output'])) + QtCore.QMetaObject.connectSlotsByName(self) + + def retranslateUi(self): + self.setWindowTitle(QtGui.QApplication.translate("audioSettingsForm", "Audio settings", None, QtGui.QApplication.UnicodeUTF8)) + self.in_label.setText(QtGui.QApplication.translate("audioSettingsForm", "Input device:", None, QtGui.QApplication.UnicodeUTF8)) + self.out_label.setText(QtGui.QApplication.translate("audioSettingsForm", "Output device:", None, QtGui.QApplication.UnicodeUTF8)) + + def closeEvent(self, event): + settings = Settings.get_instance() + settings.audio['input'] = self.in_indexes[self.input.currentIndex()] + settings.audio['output'] = self.out_indexes[self.output.currentIndex()] + settings.save() diff --git a/src/profile.py b/src/profile.py index 331dbf7..c2dfea7 100644 --- a/src/profile.py +++ b/src/profile.py @@ -11,6 +11,8 @@ from tox_dns import tox_dns from history import * from file_transfers import * import time +import calls +import avwidgets class Contact(object): @@ -115,6 +117,9 @@ class Contact(object): f.write(avatar) self.load_avatar() + def get_pixmap(self): + return self._widget.avatar_label.pixmap() + class Friend(Contact): """ @@ -287,6 +292,7 @@ class Profile(Contact, Singleton): self._messages = screen.messages self._tox = tox self._file_transfers = {} # dict of file transfers. key - tuple (friend_number, file_number) + self._call = calls.AV(tox.AV) # object with data about calls settings = Settings.get_instance() self._show_online = settings['show_online_friends'] screen.online_contacts.setChecked(self._show_online) @@ -374,6 +380,7 @@ class Profile(Contact, Singleton): """ :param value: number of new active friend in friend's list or None to update active user's data """ + # TODO: check if there is incoming call with friend if value is None and self._active_friend == -1: # nothing to update return if value == -1: # all friends were deleted @@ -408,6 +415,10 @@ class Profile(Contact, Singleton): else: # inline self.create_inline_item(message.get_data()) self._messages.scrollToBottom() + if value in self._call: + self._screen.active_call() + else: + self._screen.call_finished() else: friend = self._friends[self._active_friend] @@ -745,6 +756,10 @@ class Profile(Contact, Singleton): for friend in self._friends: friend.status = None + def close(self): + self._call.stop() + del self._call + # ----------------------------------------------------------------------------------------------------------------- # File transfers support # ----------------------------------------------------------------------------------------------------------------- @@ -956,6 +971,51 @@ class Profile(Contact, Singleton): for friend in filter(lambda x: x.status is not None, self._friends): self.send_avatar(friend.number) + # ----------------------------------------------------------------------------------------------------------------- + # AV support + # ----------------------------------------------------------------------------------------------------------------- + + def get_call(self): + return self._call + + call = property(get_call) + + def call_click(self, audio=True, video=False): + """User clicked audio button in main window""" + num = self.get_active_number() + if num not in self._call and self.is_active_online(): # start call + self._call(num, audio, video) + self._screen.active_call() + elif num in self._call: # finish or cancel call if you call with active friend + self.stop_call(num, False) + + def incoming_call(self, audio, video, friend_number): + friend = self.get_friend_by_number(friend_number) + if friend_number == self.get_active_number(): + self._screen.incoming_call() + # self.accept_call(friend_number, audio, video) + else: + friend.set_messages(True) + if video: + text = QtGui.QApplication.translate("incoming_call", "Incoming video call", None, QtGui.QApplication.UnicodeUTF8) + else: + text = QtGui.QApplication.translate("incoming_call", "Incoming audio call", None, QtGui.QApplication.UnicodeUTF8) + self._call_widget = avwidgets.IncomingCallWidget(friend_number, text, friend.name) + self._call_widget.set_pixmap(friend.get_pixmap()) + self._call_widget.show() + + def accept_call(self, friend_number, audio, video): + self._call.accept_call(friend_number, audio, video) + self._screen.active_call() + if hasattr(self, '_call_widget'): + del self._call_widget + + def stop_call(self, friend_number, by_friend): + self._screen.call_finished() + self._call.finish_call(friend_number, by_friend) # finish or decline call + if hasattr(self, '_call_widget'): + del self._call_widget + def tox_factory(data=None, settings=None): """ diff --git a/src/settings.py b/src/settings.py index 151b521..8cdbb7b 100644 --- a/src/settings.py +++ b/src/settings.py @@ -3,6 +3,7 @@ import json import os import locale from util import Singleton, curr_directory +import pyaudio class Settings(Singleton, dict): @@ -17,6 +18,9 @@ class Settings(Singleton, dict): else: super(self.__class__, self).__init__(Settings.get_default_settings()) self.save() + p = pyaudio.PyAudio() + self.audio = {'input': p.get_default_input_device_info()['index'], + 'output': p.get_default_output_device_info()['index']} @staticmethod def get_auto_profile(): diff --git a/src/sounds/call.wav b/src/sounds/call.wav new file mode 100755 index 0000000..6a7a8b2 Binary files /dev/null and b/src/sounds/call.wav differ diff --git a/src/tox.py b/src/tox.py index 172f98a..3752842 100644 --- a/src/tox.py +++ b/src/tox.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -from ctypes import c_char_p, Structure, CDLL, c_bool, addressof, c_int, c_size_t, POINTER, c_uint16, c_void_p, c_uint64 +from ctypes import c_char_p, Structure, c_bool, addressof, c_int, c_size_t, POINTER, c_uint16, c_void_p, c_uint64 from ctypes import create_string_buffer, ArgumentError, CFUNCTYPE, c_uint32, sizeof, c_uint8 -from platform import system from toxcore_enums_and_consts import * +from toxav import ToxAV +from libtox import LibToxCore class ToxOptions(Structure): @@ -21,20 +22,6 @@ class ToxOptions(Structure): ] -class LibToxCore(object): - def __init__(self): - if system() == 'Linux': - # be sure that libtoxcore and libsodium are installed in your os - self._libtoxcore = CDLL('libtoxcore.so') - elif system() == 'Windows': - self._libtoxcore = CDLL('libs/libtox.dll') - else: - raise OSError('Unknown system.') - - def __getattr__(self, item): - return self._libtoxcore.__getattr__(item) - - def string_to_bin(tox_id): return c_char_p(tox_id.decode('hex')) if tox_id is not None else None @@ -103,7 +90,10 @@ class Tox(object): self.file_recv_cb = None self.file_recv_chunk_cb = None + self.AV = ToxAV(self._tox_pointer) + def __del__(self): + del self.AV Tox.libtoxcore.tox_kill(self._tox_pointer) # ----------------------------------------------------------------------------------------------------------------- @@ -1406,11 +1396,3 @@ class Tox(object): return result elif tox_err_get_port == TOX_ERR_GET_PORT['NOT_BOUND']: raise RuntimeError('The instance was not bound to any port.') - - -if __name__ == '__main__': - tox = Tox(Tox.options_new()) - p = tox.get_savedata() - print type(p) - print p - del tox diff --git a/src/toxav.py b/src/toxav.py new file mode 100644 index 0000000..200ada5 --- /dev/null +++ b/src/toxav.py @@ -0,0 +1,363 @@ +from ctypes import c_int, POINTER, c_void_p, addressof, ArgumentError, c_uint32, CFUNCTYPE, c_size_t, c_uint8, c_uint16 +from ctypes import c_char_p, c_int32, c_bool, cast +from libtox import LibToxAV +from toxav_enums import * + + +class ToxAV(object): + """ + The ToxAV instance type. Each ToxAV instance can be bound to only one Tox instance, and Tox instance can have only + one ToxAV instance. One must make sure to close ToxAV instance prior closing Tox instance otherwise undefined + behaviour occurs. Upon closing of ToxAV instance, all active calls will be forcibly terminated without notifying + peers. + """ + + libtoxav = LibToxAV() + + # ----------------------------------------------------------------------------------------------------------------- + # Creation and destruction + # ----------------------------------------------------------------------------------------------------------------- + + def __init__(self, tox_pointer): + """ + Start new A/V session. There can only be only one session per Tox instance. + + :param tox_pointer: pointer to Tox instance + """ + toxav_err_new = c_int() + ToxAV.libtoxav.toxav_new.restype = POINTER(c_void_p) + self._toxav_pointer = ToxAV.libtoxav.toxav_new(tox_pointer, addressof(toxav_err_new)) + toxav_err_new = toxav_err_new.value + if toxav_err_new == TOXAV_ERR_NEW['NULL']: + raise ArgumentError('One of the arguments to the function was NULL when it was not expected.') + elif toxav_err_new == TOXAV_ERR_NEW['MALLOC']: + raise MemoryError('Memory allocation failure while trying to allocate structures required for the A/V ' + 'session.') + elif toxav_err_new == TOXAV_ERR_NEW['MULTIPLE']: + raise RuntimeError('Attempted to create a second session for the same Tox instance.') + + self.call_state_cb = None + self.audio_receive_frame_cb = None + self.video_receive_frame_cb = None + self.call_cb = None + + def __del__(self): + """ + Releases all resources associated with the A/V session. + + If any calls were ongoing, these will be forcibly terminated without notifying peers. After calling this + function, no other functions may be called and the av pointer becomes invalid. + """ + ToxAV.libtoxav.toxav_kill(self._toxav_pointer) + + def get_tox_pointer(self): + """ + Returns the Tox instance the A/V object was created for. + + :return: pointer to the Tox instance + """ + ToxAV.libtoxav.toxav_get_tox.restype = POINTER(c_void_p) + return ToxAV.libtoxav.toxav_get_tox(self._toxav_pointer) + + # ----------------------------------------------------------------------------------------------------------------- + # A/V event loop + # ----------------------------------------------------------------------------------------------------------------- + + def iteration_interval(self): + """ + Returns the interval in milliseconds when the next toxav_iterate call should be. If no call is active at the + moment, this function returns 200. + + :return: interval in milliseconds + """ + return ToxAV.libtoxav.toxav_iteration_interval(self._toxav_pointer) + + def iterate(self): + """ + Main loop for the session. This function needs to be called in intervals of toxav_iteration_interval() + milliseconds. It is best called in the separate thread from tox_iterate. + """ + ToxAV.libtoxav.toxav_iterate(self._toxav_pointer) + + # ----------------------------------------------------------------------------------------------------------------- + # Call setup + # ----------------------------------------------------------------------------------------------------------------- + + def call(self, friend_number, audio_bit_rate, video_bit_rate): + """ + Call a friend. This will start ringing the friend. + + It is the client's responsibility to stop ringing after a certain timeout, if such behaviour is desired. If the + client does not stop ringing, the library will not stop until the friend is disconnected. Audio and video + receiving are both enabled by default. + + :param friend_number: The friend number of the friend that should be called. + :param audio_bit_rate: Audio bit rate in Kb/sec. Set this to 0 to disable audio sending. + :param video_bit_rate: Video bit rate in Kb/sec. Set this to 0 to disable video sending. + :return: True on success. + """ + toxav_err_call = c_int() + result = ToxAV.libtoxav.toxav_call(self._toxav_pointer, c_uint32(friend_number), c_uint32(audio_bit_rate), + c_uint32(video_bit_rate), addressof(toxav_err_call)) + toxav_err_call = toxav_err_call.value + if toxav_err_call == TOXAV_ERR_CALL['OK']: + return bool(result) + elif toxav_err_call == TOXAV_ERR_CALL['MALLOC']: + raise MemoryError('A resource allocation error occurred while trying to create the structures required for ' + 'the call.') + elif toxav_err_call == TOXAV_ERR_CALL['SYNC']: + raise RuntimeError('Synchronization error occurred.') + elif toxav_err_call == TOXAV_ERR_CALL['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend number did not designate a valid friend.') + elif toxav_err_call == TOXAV_ERR_CALL['FRIEND_NOT_CONNECTED']: + raise ArgumentError('The friend was valid, but not currently connected.') + elif toxav_err_call == TOXAV_ERR_CALL['FRIEND_ALREADY_IN_CALL']: + raise ArgumentError('Attempted to call a friend while already in an audio or video call with them.') + elif toxav_err_call == TOXAV_ERR_CALL['INVALID_BIT_RATE']: + raise ArgumentError('Audio or video bit rate is invalid.') + + def callback_call(self, callback, user_data): + """ + Set the callback for the `call` event. Pass None to unset. + + :param callback: The function for the call callback. + + Should take pointer (c_void_p) to ToxAV object, + The friend number (c_uint32) from which the call is incoming. + True (c_bool) if friend is sending audio. + True (c_bool) if friend is sending video. + pointer (c_void_p) to user_data + :param user_data: pointer (c_void_p) to user data + """ + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_bool, c_bool, c_void_p) + self.call_cb = c_callback(callback) + ToxAV.libtoxav.toxav_callback_call(self._toxav_pointer, self.call_cb, user_data) + + def answer(self, friend_number, audio_bit_rate, video_bit_rate): + """ + Accept an incoming call. + + If answering fails for any reason, the call will still be pending and it is possible to try and answer it later. + Audio and video receiving are both enabled by default. + + :param friend_number: The friend number of the friend that is calling. + :param audio_bit_rate: Audio bit rate in Kb/sec. Set this to 0 to disable audio sending. + :param video_bit_rate: Video bit rate in Kb/sec. Set this to 0 to disable video sending. + :return: True on success. + """ + toxav_err_answer = c_int() + result = ToxAV.libtoxav.toxav_answer(self._toxav_pointer, c_uint32(friend_number), c_uint32(audio_bit_rate), + c_uint32(video_bit_rate), addressof(toxav_err_answer)) + toxav_err_answer = toxav_err_answer.value + if toxav_err_answer == TOXAV_ERR_ANSWER['OK']: + return bool(result) + elif toxav_err_answer == TOXAV_ERR_ANSWER['SYNC']: + raise RuntimeError('Synchronization error occurred.') + elif toxav_err_answer == TOXAV_ERR_ANSWER['CODEC_INITIALIZATION']: + raise RuntimeError('Failed to initialize codecs for call session. Note that codec initiation will fail if ' + 'there is no receive callback registered for either audio or video.') + elif toxav_err_answer == TOXAV_ERR_ANSWER['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend number did not designate a valid friend.') + elif toxav_err_answer == TOXAV_ERR_ANSWER['FRIEND_NOT_CALLING']: + raise ArgumentError('The friend was valid, but they are not currently trying to initiate a call. This is ' + 'also returned if this client is already in a call with the friend.') + elif toxav_err_answer == TOXAV_ERR_ANSWER['INVALID_BIT_RATE']: + raise ArgumentError('Audio or video bit rate is invalid.') + + # ----------------------------------------------------------------------------------------------------------------- + # Call state graph + # ----------------------------------------------------------------------------------------------------------------- + + def callback_call_state(self, callback, user_data): + """ + Set the callback for the `call_state` event. Pass None to unset. + + :param callback: Python function. + The function for the call_state callback. + + Should take pointer (c_void_p) to ToxAV object, + The friend number (c_uint32) for which the call state changed. + The bitmask of the new call state which is guaranteed to be different than the previous state. The state is set + to 0 when the call is paused. The bitmask represents all the activities currently performed by the friend. + pointer (c_void_p) to user_data + :param user_data: pointer (c_void_p) to user data + """ + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint32, c_void_p) + self.call_state_cb = c_callback(callback) + ToxAV.libtoxav.toxav_callback_call_state(self._toxav_pointer, self.call_state_cb, user_data) + + # ----------------------------------------------------------------------------------------------------------------- + # Call control + # ----------------------------------------------------------------------------------------------------------------- + + def call_control(self, friend_number, control): + """ + Sends a call control command to a friend. + + :param friend_number: The friend number of the friend this client is in a call with. + :param control: The control command to send. + :return: True on success. + """ + toxav_err_call_control = c_int() + result = ToxAV.libtoxav.toxav_call_control(self._toxav_pointer, c_uint32(friend_number), c_int(control), + addressof(toxav_err_call_control)) + toxav_err_call_control = toxav_err_call_control.value + if toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['OK']: + return bool(result) + elif toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['SYNC']: + raise RuntimeError('Synchronization error occurred.') + elif toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend_number passed did not designate a valid friend.') + elif toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['FRIEND_NOT_IN_CALL']: + raise RuntimeError('This client is currently not in a call with the friend. Before the call is answered, ' + 'only CANCEL is a valid control.') + elif toxav_err_call_control == TOXAV_ERR_CALL_CONTROL['INVALID_TRANSITION']: + raise RuntimeError('Happens if user tried to pause an already paused call or if trying to resume a call ' + 'that is not paused.') + + # ----------------------------------------------------------------------------------------------------------------- + # TODO Controlling bit rates + # ----------------------------------------------------------------------------------------------------------------- + + # ----------------------------------------------------------------------------------------------------------------- + # A/V sending + # ----------------------------------------------------------------------------------------------------------------- + + def audio_send_frame(self, friend_number, pcm, sample_count, channels, sampling_rate): + """ + Send an audio frame to a friend. + + The expected format of the PCM data is: [s1c1][s1c2][...][s2c1][s2c2][...]... + Meaning: sample 1 for channel 1, sample 1 for channel 2, ... + For mono audio, this has no meaning, every sample is subsequent. For stereo, this means the expected format is + LRLRLR... with samples for left and right alternating. + + :param friend_number: The friend number of the friend to which to send an audio frame. + :param pcm: An array of audio samples. The size of this array must be sample_count * channels. + :param sample_count: Number of samples in this frame. Valid numbers here are + ((sample rate) * (audio length) / 1000), where audio length can be 2.5, 5, 10, 20, 40 or 60 milliseconds. + :param channels: Number of audio channels. Sulpported values are 1 and 2. + :param sampling_rate: Audio sampling rate used in this frame. Valid sampling rates are 8000, 12000, 16000, + 24000, or 48000. + """ + toxav_err_send_frame = c_int() + result = ToxAV.libtoxav.toxav_audio_send_frame(self._toxav_pointer, c_uint32(friend_number), + cast(pcm, c_void_p), + c_size_t(sample_count), c_uint8(channels), + c_uint32(sampling_rate), addressof(toxav_err_send_frame)) + toxav_err_send_frame = toxav_err_send_frame.value + if toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['OK']: + return bool(result) + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['NULL']: + raise ArgumentError('The samples data pointer was NULL.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend_number passed did not designate a valid friend.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['FRIEND_NOT_IN_CALL']: + raise RuntimeError('This client is currently not in a call with the friend.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['SYNC']: + raise RuntimeError('Synchronization error occurred.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['INVALID']: + raise ArgumentError('One of the frame parameters was invalid. E.g. the resolution may be too small or too ' + 'large, or the audio sampling rate may be unsupported.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['PAYLOAD_TYPE_DISABLED']: + raise RuntimeError('Either friend turned off audio or video receiving or we turned off sending for the said' + 'payload.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['RTP_FAILED']: + RuntimeError('Failed to push frame through rtp interface.') + + def video_send_frame(self, friend_number, width, height, y, u, v): + """ + Send a video frame to a friend. + + Y - plane should be of size: height * width + U - plane should be of size: (height/2) * (width/2) + V - plane should be of size: (height/2) * (width/2) + + :param friend_number: The friend number of the friend to which to send a video frame. + :param width: Width of the frame in pixels. + :param height: Height of the frame in pixels. + :param y: Y (Luminance) plane data. + :param u: U (Chroma) plane data. + :param v: V (Chroma) plane data. + """ + toxav_err_send_frame = c_int() + result = ToxAV.libtoxav.toxav_video_send_frame(self._toxav_pointer, c_uint32(friend_number), c_uint16(width), + c_uint16(height), c_char_p(y), c_char_p(u), c_char_p(v), + addressof(toxav_err_send_frame)) + toxav_err_send_frame = toxav_err_send_frame.value + if toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['OK']: + return bool(result) + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['NULL']: + raise ArgumentError('One of Y, U, or V was NULL.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['FRIEND_NOT_FOUND']: + raise ArgumentError('The friend_number passed did not designate a valid friend.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['FRIEND_NOT_IN_CALL']: + raise RuntimeError('This client is currently not in a call with the friend.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['SYNC']: + raise RuntimeError('Synchronization error occurred.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['INVALID']: + raise ArgumentError('One of the frame parameters was invalid. E.g. the resolution may be too small or too ' + 'large, or the audio sampling rate may be unsupported.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['PAYLOAD_TYPE_DISABLED']: + raise RuntimeError('Either friend turned off audio or video receiving or we turned off sending for the said' + 'payload.') + elif toxav_err_send_frame == TOXAV_ERR_SEND_FRAME['RTP_FAILED']: + RuntimeError('Failed to push frame through rtp interface.') + + # ----------------------------------------------------------------------------------------------------------------- + # A/V receiving + # ----------------------------------------------------------------------------------------------------------------- + + def callback_audio_receive_frame(self, callback, user_data): + """ + Set the callback for the `audio_receive_frame` event. Pass None to unset. + + :param callback: Python function. + Function for the audio_receive_frame callback. The callback can be called multiple times per single + iteration depending on the amount of queued frames in the buffer. The received format is the same as in send + function. + + Should take pointer (c_void_p) to ToxAV object, + The friend number (c_uint32) of the friend who sent an audio frame. + An array (c_uint8) of audio samples (sample_count * channels elements). + The number (c_size_t) of audio samples per channel in the PCM array. + Number (c_uint8) of audio channels. + Sampling rate (c_uint32) used in this frame. + pointer (c_void_p) to user_data + :param user_data: pointer (c_void_p) to user data + """ + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, POINTER(c_uint8), c_size_t, c_uint8, c_uint32, c_void_p) + self.audio_receive_frame_cb = c_callback(callback) + ToxAV.libtoxav.toxav_callback_audio_receive_frame(self._toxav_pointer, self.audio_receive_frame_cb, user_data) + + def callback_video_receive_frame(self, callback, user_data): + """ + Set the callback for the `video_receive_frame` event. Pass None to unset. + + :param callback: Python function. + The function type for the video_receive_frame callback. + + Should take + toxAV pointer (c_void_p) to ToxAV object, + friend_number The friend number (c_uint32) of the friend who sent a video frame. + width Width (c_uint16) of the frame in pixels. + height Height (c_uint16) of the frame in pixels. + y + u + v Plane data (c_char_p). + The size of plane data is derived from width and height where + Y = MAX(width, abs(ystride)) * height, + U = MAX(width/2, abs(ustride)) * (height/2) and + V = MAX(width/2, abs(vstride)) * (height/2). + ystride + ustride + vstride Strides data (c_int32). Strides represent padding for each plane that may or may not be present. You must + handle strides in your image processing code. Strides are negative if the image is bottom-up + hence why you MUST abs() it when calculating plane buffer size. + user_data pointer (c_void_p) to user_data + :param user_data: pointer (c_void_p) to user data + """ + c_callback = CFUNCTYPE(None, c_void_p, c_uint32, c_uint16, c_uint16, c_char_p, c_char_p, c_char_p, c_int32, + c_int32, c_int32, c_void_p) + self.video_receive_frame_cb = c_callback(callback) + ToxAV.libtoxav.toxav_callback_video_receive_frame(self._toxav_pointer, self.video_receive_frame_cb, user_data) diff --git a/src/toxav_enums.py b/src/toxav_enums.py new file mode 100644 index 0000000..3f3977a --- /dev/null +++ b/src/toxav_enums.py @@ -0,0 +1,131 @@ +TOXAV_ERR_NEW = { + # The function returned successfully. + 'OK': 0, + # One of the arguments to the function was NULL when it was not expected. + 'NULL': 1, + # Memory allocation failure while trying to allocate structures required for the A/V session. + 'MALLOC': 2, + # Attempted to create a second session for the same Tox instance. + 'MULTIPLE': 3, +} + +TOXAV_ERR_CALL = { + # The function returned successfully. + 'OK': 0, + # A resource allocation error occurred while trying to create the structures required for the call. + 'MALLOC': 1, + # Synchronization error occurred. + 'SYNC': 2, + # The friend number did not designate a valid friend. + 'FRIEND_NOT_FOUND': 3, + # The friend was valid, but not currently connected. + 'FRIEND_NOT_CONNECTED': 4, + # Attempted to call a friend while already in an audio or video call with them. + 'FRIEND_ALREADY_IN_CALL': 5, + # Audio or video bit rate is invalid. + 'INVALID_BIT_RATE': 6, +} + +TOXAV_ERR_ANSWER = { + # The function returned successfully. + 'OK': 0, + # Synchronization error occurred. + 'SYNC': 1, + # Failed to initialize codecs for call session. Note that codec initiation will fail if there is no receive callback + # registered for either audio or video. + 'CODEC_INITIALIZATION': 2, + # The friend number did not designate a valid friend. + 'FRIEND_NOT_FOUND': 3, + # The friend was valid, but they are not currently trying to initiate a call. This is also returned if this client + # is already in a call with the friend. + 'FRIEND_NOT_CALLING': 4, + # Audio or video bit rate is invalid. + 'INVALID_BIT_RATE': 5, +} + +TOXAV_FRIEND_CALL_STATE = { + # Set by the AV core if an error occurred on the remote end or if friend timed out. This is the final state after + # which no more state transitions can occur for the call. This call state will never be triggered in combination + # with other call states. + 'ERROR': 1, + # The call has finished. This is the final state after which no more state transitions can occur for the call. This + # call state will never be triggered in combination with other call states. + 'FINISHED': 2, + # The flag that marks that friend is sending audio. + 'SENDING_A': 4, + # The flag that marks that friend is sending video. + 'SENDING_V': 8, + # The flag that marks that friend is receiving audio. + 'ACCEPTING_A': 16, + # The flag that marks that friend is receiving video. + 'ACCEPTING_V': 32, +} + +TOXAV_CALL_CONTROL = { + # Resume a previously paused call. Only valid if the pause was caused by this client, if not, this control is + # ignored. Not valid before the call is accepted. + 'RESUME': 0, + # Put a call on hold. Not valid before the call is accepted. + 'PAUSE': 1, + # Reject a call if it was not answered, yet. Cancel a call after it was answered. + 'CANCEL': 2, + # Request that the friend stops sending audio. Regardless of the friend's compliance, this will cause the + # audio_receive_frame event to stop being triggered on receiving an audio frame from the friend. + 'MUTE_AUDIO': 3, + # Calling this control will notify client to start sending audio again. + 'UNMUTE_AUDIO': 4, + # Request that the friend stops sending video. Regardless of the friend's compliance, this will cause the + # video_receive_frame event to stop being triggered on receiving a video frame from the friend. + 'HIDE_VIDEO': 5, + # Calling this control will notify client to start sending video again. + 'SHOW_VIDEO': 6, +} + +TOXAV_ERR_CALL_CONTROL = { + # The function returned successfully. + 'OK': 0, + # Synchronization error occurred. + 'SYNC': 1, + # The friend_number passed did not designate a valid friend. + 'FRIEND_NOT_FOUND': 2, + # This client is currently not in a call with the friend. Before the call is answered, only CANCEL is a valid + # control. + 'FRIEND_NOT_IN_CALL': 3, + # Happens if user tried to pause an already paused call or if trying to resume a call that is not paused. + 'INVALID_TRANSITION': 4, +} + +TOXAV_ERR_BIT_RATE_SET = { + # The function returned successfully. + 'OK': 0, + # Synchronization error occurred. + 'SYNC': 1, + # The audio bit rate passed was not one of the supported values. + 'INVALID_AUDIO_BIT_RATE': 2, + # The video bit rate passed was not one of the supported values. + 'INVALID_VIDEO_BIT_RATE': 3, + # The friend_number passed did not designate a valid friend. + 'FRIEND_NOT_FOUND': 4, + # This client is currently not in a call with the friend. + 'FRIEND_NOT_IN_CALL': 5, +} + +TOXAV_ERR_SEND_FRAME = { + # The function returned successfully. + 'OK': 0, + # In case of video, one of Y, U, or V was NULL. In case of audio, the samples data pointer was NULL. + 'NULL': 1, + # The friend_number passed did not designate a valid friend. + 'FRIEND_NOT_FOUND': 2, + # This client is currently not in a call with the friend. + 'FRIEND_NOT_IN_CALL': 3, + # Synchronization error occurred. + 'SYNC': 4, + # One of the frame parameters was invalid. E.g. the resolution may be too small or too large, or the audio sampling + # rate may be unsupported. + 'INVALID': 5, + # Either friend turned off audio or video receiving or we turned off sending for the said payload. + 'PAYLOAD_TYPE_DISABLED': 6, + # Failed to push frame through rtp interface. + 'RTP_FAILED': 7, +} diff --git a/src/widgets.py b/src/widgets.py new file mode 100644 index 0000000..28ea0b3 --- /dev/null +++ b/src/widgets.py @@ -0,0 +1,23 @@ +from PySide import QtGui, QtCore + + +class DataLabel(QtGui.QLabel): + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + metrics = QtGui.QFontMetrics(self.font()) + text = metrics.elidedText(self.text(), QtCore.Qt.ElideRight, self.width()) + painter.drawText(self.rect(), self.alignment(), text) + + +class CenteredWidget(QtGui.QWidget): + + def __init__(self): + super(CenteredWidget, self).__init__() + self.center() + + def center(self): + qr = self.frameGeometry() + cp = QtGui.QDesktopWidget().availableGeometry().center() + qr.moveCenter(cp) + self.move(qr.topLeft())