audio => develop
This commit is contained in:
parent
7b9204a88b
commit
e1db914f13
21 changed files with 1016 additions and 65 deletions
106
src/avwidgets.py
Normal file
106
src/avwidgets.py
Normal file
|
@ -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)
|
|
@ -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)
|
||||
|
||||
|
|
143
src/calls.py
Normal file
143
src/calls.py
Normal file
|
@ -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
|
||||
|
BIN
src/images/accept_audio.png
Executable file
BIN
src/images/accept_audio.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
src/images/accept_video.png
Executable file
BIN
src/images/accept_video.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 13 KiB |
BIN
src/images/decline_call.png
Executable file
BIN
src/images/decline_call.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
BIN
src/images/finish_call.png
Executable file
BIN
src/images/finish_call.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 3.7 KiB |
BIN
src/images/incoming_call.png
Executable file
BIN
src/images/incoming_call.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 1.1 KiB |
33
src/libtox.py
Normal file
33
src/libtox.py
Normal file
|
@ -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)
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
|
|
27
src/main.py
27
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):
|
||||
|
|
|
@ -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
|
||||
# -----------------------------------------------------------------------------------------------------------------
|
||||
|
|
66
src/menu.py
66
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()
|
||||
|
|
|
@ -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):
|
||||
"""
|
||||
|
|
|
@ -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():
|
||||
|
|
BIN
src/sounds/call.wav
Executable file
BIN
src/sounds/call.wav
Executable file
Binary file not shown.
30
src/tox.py
30
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
|
||||
|
|
363
src/toxav.py
Normal file
363
src/toxav.py
Normal file
|
@ -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)
|
131
src/toxav_enums.py
Normal file
131
src/toxav_enums.py
Normal file
|
@ -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,
|
||||
}
|
23
src/widgets.py
Normal file
23
src/widgets.py
Normal file
|
@ -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())
|
Loading…
Reference in a new issue