Commit 1b661828 authored by Daniel Brötzmann's avatar Daniel Brötzmann

Jingle AV: Rework GUI, fix Video stream

parent 0a9f0ed9
Pipeline #6533 passed with stages
in 4 minutes and 2 seconds
This diff is collapsed.
......@@ -271,6 +271,17 @@ def is_available(self):
return self == ClientState.AVAILABLE
class JingleState(Enum):
NULL = 'stop'
CONNECTING = 'connecting'
CONNECTION_RECEIVED = 'connection_received'
CONNECTED = 'connected'
ERROR = 'error'
def __str__(self):
return self.value
MUC_CREATION_EXAMPLES = [
(Q_('?Group chat name:Team'),
Q_('?Group chat description:Project discussion'),
......
This diff is collapsed.
......@@ -287,7 +287,7 @@ def is_ready(self):
"""
Return True when all codecs and candidates are ready (for all contents)
"""
return (any((content.is_ready() for content in self.contents.values()))
return (all((content.is_ready() for content in self.contents.values()))
and self.accepted)
def accept_session(self):
......@@ -569,7 +569,7 @@ def __on_session_initiate(self, stanza, jingle, error, action):
self.state = JingleStates.PENDING
# Send event about starting a session
self._raise_event('jingle-request-received', contents=contents[0])
self._raise_event('jingle-request-received', contents=contents)
def __broadcast(self, stanza, jingle, error, action):
"""
......@@ -605,6 +605,14 @@ def __on_session_terminate(self, stanza, jingle, error, action):
else:
# TODO
text = reason
if reason == 'decline':
self._raise_event('jingle-disconnected-received',
media=None,
reason=text)
if reason == 'success':
self._raise_event('jingle-disconnected-received',
media=None,
reason=text)
if reason == 'cancel' and self.session_type_ft:
self._raise_event('jingle-ft-cancelled-received',
media=None,
......
......@@ -180,6 +180,31 @@ def start_video(self, jid):
jingle.start_session()
return jingle.sid
def start_audio_video(self, jid):
if self.get_jingle_session(jid, media='video'):
return self.get_jingle_session(jid, media='video').sid
audio_session = self.get_jingle_session(jid, media='audio')
video_session = self.get_jingle_session(jid, media='video')
if audio_session and video_session:
return audio_session.sid
if audio_session:
video = JingleVideo(audio_session)
audio_session.add_content('video', video)
return audio_session.sid
if video_session:
audio = JingleAudio(video_session)
video_session.add_content('audio', audio)
return video_session.sid
jingle_session = JingleSession(self._con, weinitiate=True, jid=jid)
self._sessions[jingle_session.sid] = jingle_session
audio = JingleAudio(jingle_session)
video = JingleVideo(jingle_session)
jingle_session.add_content('audio', audio)
jingle_session.add_content('video', video)
jingle_session.start_session()
return jingle_session.sid
def start_file_transfer(self, jid, file_props, request=False):
logger.info("start file transfer with file: %s", file_props)
contact = app.contacts.get_contact_with_highest_priority(
......
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.18.3 -->
<interface>
<requires lib="gtk+" version="3.12"/>
<object class="GtkMessageDialog" id="voip_call_received_messagedialog">
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="border_width">5</property>
<property name="resizable">False</property>
<property name="window_position">center-on-parent</property>
<property name="type_hint">dialog</property>
<property name="skip_taskbar_hint">True</property>
<property name="message_type">question</property>
<property name="buttons">yes-no</property>
<property name="text" translatable="yes">&lt;b&gt;&lt;big&gt;Incoming call&lt;/big&gt;&lt;/b&gt;</property>
<property name="use_markup">True</property>
<signal name="close" handler="on_voip_call_received_messagedialog_close" swapped="no"/>
<signal name="destroy" handler="on_voip_call_received_messagedialog_destroy" swapped="no"/>
<signal name="response" handler="on_voip_call_received_messagedialog_response" swapped="no"/>
<child internal-child="vbox">
<object class="GtkBox" id="dialog-vbox4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="orientation">vertical</property>
<property name="spacing">2</property>
<child internal-child="action_area">
<object class="GtkButtonBox" id="dialog-action_area4">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="events">GDK_POINTER_MOTION_MASK | GDK_POINTER_MOTION_HINT_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK</property>
<property name="layout_style">end</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="pack_type">end</property>
<property name="position">0</property>
</packing>
</child>
</object>
</child>
</object>
</interface>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-camera-off"><line x1="1" y1="1" x2="23" y2="23"></line><path d="M21 21H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h3m3-3h6l2 3h4a2 2 0 0 1 2 2v9.34m-7.72-2.06a4 4 0 1 1-5.56-5.56"></path></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-camera"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>
\ No newline at end of file
......@@ -319,6 +319,19 @@ button.flat.link { padding: 0; border: 0; }
font-weight: bold;
}
@keyframes pulse {
0% { -gtk-icon-transform: scale(1.0); }
20% { -gtk-icon-transform: scale(1.1); }
35% { -gtk-icon-transform: scale(1.0); }
}
.audio-mic-animation {
animation-name: pulse;
animation-duration: 2.0s;
animation-timing-function: ease-in-out;
animation-iteration-count: infinite;
}
/* Treeview */
.adhoc-treeview { padding: 5px; }
.adhoc-scrolled { border: 1px solid; border-color:@unfocused_borders; }
......
......@@ -45,7 +45,6 @@
from gajim.gtk.util import get_icon_name
from gajim.gtk.util import get_builder
from gajim.gtk.util import get_app_window
from gajim.gtk import gstreamer
log = logging.getLogger('gajim.dialogs')
......@@ -469,119 +468,3 @@ def on_invite_button_clicked(self, widget):
def on_cancel_button_clicked(self, widget):
self.window.destroy()
class VoIPCallReceivedDialog:
instances = {} # type: Dict[Tuple[str, str], VoIPCallReceivedDialog]
def __init__(self, account, contact_jid, sid, content_types):
self.instances[(contact_jid, sid)] = self
self.account = account
self.fjid = contact_jid
self.sid = sid
self.content_types = content_types
xml = get_builder('voip_call_received_dialog.ui')
xml.connect_signals(self)
jid = app.get_jid_without_resource(self.fjid)
contact = app.contacts.get_first_contact_from_jid(account, jid)
if contact and contact.name:
self.contact_text = '%s (%s)' % (contact.name, jid)
else:
self.contact_text = contact_jid
self.dialog = xml.get_object('voip_call_received_messagedialog')
self.set_secondary_text()
self.dialog.show_all()
@classmethod
def get_dialog(cls, jid, sid):
if (jid, sid) in cls.instances:
return cls.instances[(jid, sid)]
return None
def set_secondary_text(self):
if 'audio' in self.content_types and 'video' in self.content_types:
types_text = _('an audio and video')
elif 'audio' in self.content_types:
types_text = _('an audio')
elif 'video' in self.content_types:
types_text = _('a video')
# do the substitution
self.dialog.set_property('secondary-text',
_('%(contact)s wants to start a %(type)s chat with you. Do you want '
'to answer the call?') % {'contact': self.contact_text,
'type': types_text})
def add_contents(self, content_types):
for type_ in content_types:
if type_ not in self.content_types:
self.content_types.add(type_)
self.set_secondary_text()
def remove_contents(self, content_types):
for type_ in content_types:
if type_ in self.content_types:
self.content_types.remove(type_)
if not self.content_types:
self.dialog.destroy()
else:
self.set_secondary_text()
def on_voip_call_received_messagedialog_destroy(self, dialog):
if (self.fjid, self.sid) in self.instances:
del self.instances[(self.fjid, self.sid)]
def on_voip_call_received_messagedialog_close(self, dialog):
return self.on_voip_call_received_messagedialog_response(dialog,
Gtk.ResponseType.NO)
def on_voip_call_received_messagedialog_response(self, dialog, response):
# we've got response from user, either stop connecting or accept the call
session = app.connections[self.account].get_module('Jingle').get_jingle_session(
self.fjid, self.sid)
if not session:
dialog.destroy()
return
if response == Gtk.ResponseType.YES:
#TODO: Ensure that ctrl.contact.resource == resource
jid = app.get_jid_without_resource(self.fjid)
ctrl = (app.interface.msg_win_mgr.get_control(self.fjid, self.account)
or app.interface.msg_win_mgr.get_control(jid, self.account)
or app.interface.new_chat_from_jid(self.account, jid))
# Chat control opened, update content's status
audio = session.get_content('audio')
video = session.get_content('video')
if audio and not audio.negotiated:
ctrl.set_audio_state('connecting', self.sid)
if video and not video.negotiated:
video_hbox = ctrl.xml.get_object('video_hbox')
video_hbox.set_no_show_all(False)
if app.settings.get('video_see_self'):
fixed = ctrl.xml.get_object('outgoing_fixed')
fixed.set_no_show_all(False)
video_hbox.show_all()
content = session.get_content('video')
sink_other, widget_other, _ = gstreamer.create_gtk_widget()
sink_self, widget_self, _ = gstreamer.create_gtk_widget()
ctrl.xml.incoming_viewport.add(widget_other)
ctrl.xml.outgoing_viewport.add(widget_self)
content.do_setup(sink_self, sink_other)
ctrl.set_video_state('connecting', self.sid)
# Now, accept the content/sessions.
# This should be done after the chat control is running
if not session.accepted:
session.approve_session()
for content in self.content_types:
session.approve_content(content)
else: # response==Gtk.ResponseType.NO
if not session.accepted:
session.decline_session()
else:
for content in self.content_types:
session.reject_content(content)
dialog.destroy()
......@@ -129,7 +129,7 @@ def _on_event_removed(self, event_list):
self._withdraw('gc-invitation', event.account, event.muc)
if event.type_ in ('normal', 'printed_chat', 'chat',
'printed_pm', 'pm', 'printed_marked_gc_msg',
'printed_gc_msg'):
'printed_gc_msg', 'jingle-incoming'):
self._withdraw('new-message', event.account, event.jid)
def _nec_our_status(self, event):
......@@ -187,7 +187,7 @@ def popup(self, event_type, jid, account, type_='', icon_name=None,
_('File Transfer Error'), _('File Transfer Completed'),
_('File Transfer Stopped'), _('Group Chat Invitation'),
_('Connection Failed'), _('Subscription request'),
_('Unsubscribed')):
_('Unsubscribed'), _('Incoming Call')):
if 'actions' in self._daemon_capabilities:
# Create Variant Dict
dict_ = {'account': GLib.Variant('s', account),
......
......@@ -57,7 +57,6 @@
from gajim.common.dbus import music_track
from gajim import gui_menu_builder
from gajim import dialogs
from gajim.dialog_messages import get_dialog
from gajim.chat_control_base import ChatControlBase
......@@ -82,6 +81,7 @@
from gajim.common.i18n import _
from gajim.common.client import Client
from gajim.common.const import Display
from gajim.common.const import JingleState
from gajim.common.file_props import FilesProp
......@@ -877,14 +877,16 @@ def handle_event_jingleft_cancel(self, obj):
ft.show_stopped(obj.jid, file_props, 'Peer cancelled ' +
'the transfer')
def handle_event_jingle_incoming(self, obj):
# Jingle AV handling
def handle_event_jingle_incoming(self, event):
# ('JINGLE_INCOMING', account, peer jid, sid, tuple-of-contents==(type,
# data...))
# TODO: conditional blocking if peer is not in roster
account = obj.conn.name
content_types = [obj.contents.media]
account = event.conn.name
content_types = []
for item in event.contents:
content_types.append(item.media)
# check type of jingle session
if 'audio' in content_types or 'video' in content_types:
# a voip session...
......@@ -895,74 +897,105 @@ def handle_event_jingle_incoming(self, obj):
# unknown session type... it should be declined in common/jingle.py
return
ctrl = (self.msg_win_mgr.get_control(obj.fjid, account)
or self.msg_win_mgr.get_control(obj.jid, account))
notification_event = events.JingleIncomingEvent(
event.fjid, event.sid, content_types)
ctrl = (self.msg_win_mgr.get_control(event.fjid, account)
or self.msg_win_mgr.get_control(event.jid, account))
if ctrl:
if 'audio' in content_types:
ctrl.set_audio_state('connection_received', obj.sid)
ctrl.set_jingle_state(
'audio',
JingleState.CONNECTION_RECEIVED,
event.sid)
if 'video' in content_types:
ctrl.set_video_state('connection_received', obj.sid)
dlg = dialogs.VoIPCallReceivedDialog.get_dialog(obj.fjid, obj.sid)
if dlg:
dlg.add_contents(content_types)
return
ctrl.set_jingle_state(
'video',
JingleState.CONNECTION_RECEIVED,
event.sid)
ctrl.add_call_received_message(notification_event)
if helpers.allow_popup_window(account):
dialogs.VoIPCallReceivedDialog(account, obj.fjid, obj.sid,
content_types)
app.interface.new_chat_from_jid(account, event.fjid)
ctrl.add_call_received_message(notification_event)
return
event = events.JingleIncomingEvent(obj.fjid, obj.sid, content_types)
self.add_event(account, obj.jid, event)
self.add_event(account, event.fjid, notification_event)
if helpers.allow_showing_notification(account):
# TODO: we should use another pixmap ;-)
txt = _('%s wants to start a voice chat.') % \
app.get_name_from_jid(account, obj.fjid)
event_type = _('Voice Chat Request')
heading = _('Incoming Call')
text = _(f'{app.get_name_from_jid(account, event.jid)} is calling')
app.notification.popup(
event_type, obj.fjid, account, 'jingle-incoming',
icon_name='call-start-symbolic', title=event_type, text=txt)
heading,
event.fjid,
account,
'jingle-incoming',
icon_name='call-start-symbolic',
title=heading,
text=text)
def handle_event_jingle_connected(self, obj):
def handle_event_jingle_connected(self, event):
# ('JINGLE_CONNECTED', account, (peerjid, sid, media))
if obj.media in ('audio', 'video'):
account = obj.conn.name
ctrl = (self.msg_win_mgr.get_control(obj.fjid, account)
or self.msg_win_mgr.get_control(obj.jid, account))
if event.media in ('audio', 'video'):
account = event.conn.name
ctrl = (self.msg_win_mgr.get_control(event.fjid, account)
or self.msg_win_mgr.get_control(event.jid, account))
if ctrl:
if obj.media == 'audio':
ctrl.set_audio_state('connected', obj.sid)
else:
ctrl.set_video_state('connected', obj.sid)
def handle_event_jingle_disconnected(self, obj):
con = app.connections[account]
session = con.get_module('Jingle').get_jingle_session(
event.fjid, event.sid)
if event.media == 'audio':
content = session.get_content('audio')
ctrl.set_jingle_state(
'audio',
JingleState.CONNECTED,
event.sid)
if event.media == 'video':
content = session.get_content('video')
ctrl.set_jingle_state(
'video',
JingleState.CONNECTED,
event.sid)
# Now, accept the content/sessions.
# This should be done after the chat control is running
if not session.accepted:
session.approve_session()
for content in event.media:
session.approve_content(content)
def handle_event_jingle_disconnected(self, event):
# ('JINGLE_DISCONNECTED', account, (peerjid, sid, reason))
account = obj.conn.name
ctrl = (self.msg_win_mgr.get_control(obj.fjid, account)
or self.msg_win_mgr.get_control(obj.jid, account))
account = event.conn.name
ctrl = (self.msg_win_mgr.get_control(event.fjid, account)
or self.msg_win_mgr.get_control(event.jid, account))
if ctrl:
if obj.media is None:
ctrl.stop_jingle(sid=obj.sid, reason=obj.reason)
elif obj.media == 'audio':
ctrl.set_audio_state('stop', sid=obj.sid, reason=obj.reason)
elif obj.media == 'video':
ctrl.set_video_state('stop', sid=obj.sid, reason=obj.reason)
dialog = dialogs.VoIPCallReceivedDialog.get_dialog(obj.fjid, obj.sid)
if dialog:
if obj.media is None:
dialog.dialog.destroy()
else:
dialog.remove_contents((obj.media, ))
def handle_event_jingle_error(self, obj):
if event.media is None:
ctrl.stop_jingle(sid=event.sid, reason=event.reason)
if event.media == 'audio':
ctrl.set_jingle_state(
'audio',
JingleState.NULL,
sid=event.sid,
reason=event.reason)
if event.media == 'video':
ctrl.set_jingle_state(
'video',
JingleState.NULL,
sid=event.sid,
reason=event.reason)
def handle_event_jingle_error(self, event):
# ('JINGLE_ERROR', account, (peerjid, sid, reason))
account = obj.conn.name
ctrl = (self.msg_win_mgr.get_control(obj.fjid, account)
or self.msg_win_mgr.get_control(obj.jid, account))
if ctrl and obj.sid == ctrl.jingle['audio'].sid:
ctrl.set_audio_state('error', reason=obj.reason)
account = event.conn.name
ctrl = (self.msg_win_mgr.get_control(event.fjid, account)
or self.msg_win_mgr.get_control(event.jid, account))
if ctrl and event.sid == ctrl.jingle['audio'].sid:
ctrl.set_jingle_state(
'audio',
JingleState.ERROR,
reason=event.reason)
@staticmethod
def handle_event_roster_item_exchange(obj):
......
......@@ -521,11 +521,10 @@ def get_singlechat_menu(control_id, account, jid):
('win.invite-contacts-', _('Invite Contacts…')),
('win.add-to-roster-', _('Add to Contact List…')),
('win.block-contact-', _('Block Contact…')),
('win.toggle-audio-', _('Voice Chat')),
('win.toggle-video-', _('Video Chat')),
('win.start-call-', _('Start Call…')),
('win.information-', _('Information')),
('app.browse-history', _('History')),
]
]
def build_chatstate_menu():
menu = Gio.Menu()
......
......@@ -1938,9 +1938,12 @@ def open_event(self, account, jid, event):
return True
if event.type_ == 'jingle-incoming':
dialogs.VoIPCallReceivedDialog(account, event.peerjid, event.sid,
event.content_types)
app.events.remove_events(account, jid, event)
ctrl = app.interface.msg_win_mgr.get_control(jid, account)
if ctrl:
ctrl.parent_win.set_active_tab(ctrl)
else:
ctrl = app.interface.new_chat_from_jid(account, jid)
ctrl.add_call_received_message(event)
return True
return False
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment