Merge remote-tracking branch 'origin/feature/calls'
2
.github/workflows/build.yml
vendored
|
@ -6,7 +6,7 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- run: sudo apt-get update
|
- run: sudo apt-get update
|
||||||
- run: sudo apt-get install -y build-essential gettext cmake valac libgee-0.8-dev libsqlite3-dev libgtk-3-dev libnotify-dev libgpgme-dev libsoup2.4-dev libgcrypt20-dev libqrencode-dev libgspell-1-dev
|
- run: sudo apt-get install -y build-essential gettext cmake valac libgee-0.8-dev libsqlite3-dev libgtk-3-dev libnotify-dev libgpgme-dev libsoup2.4-dev libgcrypt20-dev libqrencode-dev libgspell-1-dev libnice-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libsrtp2-dev libwebrtc-audio-processing-dev
|
||||||
- run: ./configure --with-tests --with-libsignal-in-tree
|
- run: ./configure --with-tests --with-libsignal-in-tree
|
||||||
- run: make
|
- run: make
|
||||||
- run: build/xmpp-vala-test
|
- run: build/xmpp-vala-test
|
||||||
|
|
|
@ -2,16 +2,16 @@ cmake_minimum_required(VERSION 3.3)
|
||||||
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
|
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
|
||||||
include(ComputeVersion)
|
include(ComputeVersion)
|
||||||
if (NOT VERSION_FOUND)
|
if (NOT VERSION_FOUND)
|
||||||
project(Dino LANGUAGES C)
|
project(Dino LANGUAGES C CXX)
|
||||||
elseif (VERSION_IS_RELEASE)
|
elseif (VERSION_IS_RELEASE)
|
||||||
project(Dino VERSION ${VERSION_FULL} LANGUAGES C)
|
project(Dino VERSION ${VERSION_FULL} LANGUAGES C CXX)
|
||||||
else ()
|
else ()
|
||||||
project(Dino LANGUAGES C)
|
project(Dino LANGUAGES C CXX)
|
||||||
set(PROJECT_VERSION ${VERSION_FULL})
|
set(PROJECT_VERSION ${VERSION_FULL})
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
# Prepare Plugins
|
# Prepare Plugins
|
||||||
set(DEFAULT_PLUGINS omemo;openpgp;http-files)
|
set(DEFAULT_PLUGINS omemo;openpgp;http-files;ice;rtp)
|
||||||
foreach (plugin ${DEFAULT_PLUGINS})
|
foreach (plugin ${DEFAULT_PLUGINS})
|
||||||
if ("$CACHE{DINO_PLUGIN_ENABLED_${plugin}}" STREQUAL "")
|
if ("$CACHE{DINO_PLUGIN_ENABLED_${plugin}}" STREQUAL "")
|
||||||
if (NOT DEFINED DINO_PLUGIN_ENABLED_${plugin}})
|
if (NOT DEFINED DINO_PLUGIN_ENABLED_${plugin}})
|
||||||
|
@ -96,6 +96,7 @@ macro(AddCFlagIfSupported list flag)
|
||||||
endif ()
|
endif ()
|
||||||
endmacro()
|
endmacro()
|
||||||
|
|
||||||
|
|
||||||
if ("Ninja" STREQUAL ${CMAKE_GENERATOR})
|
if ("Ninja" STREQUAL ${CMAKE_GENERATOR})
|
||||||
AddCFlagIfSupported(CMAKE_C_FLAGS -fdiagnostics-color)
|
AddCFlagIfSupported(CMAKE_C_FLAGS -fdiagnostics-color)
|
||||||
endif ()
|
endif ()
|
||||||
|
@ -105,6 +106,7 @@ AddCFlagIfSupported(CMAKE_C_FLAGS -Wall)
|
||||||
AddCFlagIfSupported(CMAKE_C_FLAGS -Wextra)
|
AddCFlagIfSupported(CMAKE_C_FLAGS -Wextra)
|
||||||
AddCFlagIfSupported(CMAKE_C_FLAGS -Werror=format-security)
|
AddCFlagIfSupported(CMAKE_C_FLAGS -Werror=format-security)
|
||||||
AddCFlagIfSupported(CMAKE_C_FLAGS -Wno-duplicate-decl-specifier)
|
AddCFlagIfSupported(CMAKE_C_FLAGS -Wno-duplicate-decl-specifier)
|
||||||
|
AddCFlagIfSupported(CMAKE_C_FLAGS -fno-omit-frame-pointer)
|
||||||
|
|
||||||
if (NOT VALA_WARN)
|
if (NOT VALA_WARN)
|
||||||
set(VALA_WARN "conversion")
|
set(VALA_WARN "conversion")
|
||||||
|
|
13
cmake/FindGnuTLS.cmake
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
include(PkgConfigWithFallback)
|
||||||
|
find_pkg_config_with_fallback(GnuTLS
|
||||||
|
PKG_CONFIG_NAME gnutls
|
||||||
|
LIB_NAMES gnutls
|
||||||
|
INCLUDE_NAMES gnutls/gnutls.h
|
||||||
|
INCLUDE_DIR_SUFFIXES gnutls gnutls/include
|
||||||
|
DEPENDS GLib
|
||||||
|
)
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(GnuTLS
|
||||||
|
REQUIRED_VARS GnuTLS_LIBRARY
|
||||||
|
VERSION_VAR GnuTLS_VERSION)
|
12
cmake/FindGst.cmake
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
include(PkgConfigWithFallback)
|
||||||
|
find_pkg_config_with_fallback(Gst
|
||||||
|
PKG_CONFIG_NAME gstreamer-1.0
|
||||||
|
LIB_NAMES gstreamer-1.0
|
||||||
|
INCLUDE_NAMES gst/gst.h
|
||||||
|
INCLUDE_DIR_SUFFIXES gstreamer-1.0 gstreamer-1.0/include
|
||||||
|
)
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(Gst
|
||||||
|
REQUIRED_VARS Gst_LIBRARY
|
||||||
|
VERSION_VAR Gst_VERSION)
|
14
cmake/FindGstApp.cmake
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
include(PkgConfigWithFallback)
|
||||||
|
find_pkg_config_with_fallback(GstApp
|
||||||
|
PKG_CONFIG_NAME gstreamer-app-1.0
|
||||||
|
LIB_NAMES gstapp
|
||||||
|
LIB_DIR_HINTS gstreamer-1.0
|
||||||
|
INCLUDE_NAMES gst/app/app.h
|
||||||
|
INCLUDE_DIR_SUFFIXES gstreamer-1.0 gstreamer-1.0/include gstreamer-app-1.0 gstreamer-app-1.0/include
|
||||||
|
DEPENDS Gst
|
||||||
|
)
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(GstApp
|
||||||
|
REQUIRED_VARS GstApp_LIBRARY
|
||||||
|
VERSION_VAR GstApp_VERSION)
|
14
cmake/FindGstAudio.cmake
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
include(PkgConfigWithFallback)
|
||||||
|
find_pkg_config_with_fallback(GstAudio
|
||||||
|
PKG_CONFIG_NAME gstreamer-audio-1.0
|
||||||
|
LIB_NAMES gstaudio
|
||||||
|
LIB_DIR_HINTS gstreamer-1.0
|
||||||
|
INCLUDE_NAMES gst/audio/audio.h
|
||||||
|
INCLUDE_DIR_SUFFIXES gstreamer-1.0 gstreamer-1.0/include gstreamer-audio-1.0 gstreamer-audio-1.0/include
|
||||||
|
DEPENDS Gst
|
||||||
|
)
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(GstAudio
|
||||||
|
REQUIRED_VARS GstAudio_LIBRARY
|
||||||
|
VERSION_VAR GstAudio_VERSION)
|
14
cmake/FindGstRtp.cmake
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
include(PkgConfigWithFallback)
|
||||||
|
find_pkg_config_with_fallback(GstRtp
|
||||||
|
PKG_CONFIG_NAME gstreamer-rtp-1.0
|
||||||
|
LIB_NAMES gstrtp
|
||||||
|
LIB_DIR_HINTS gstreamer-1.0
|
||||||
|
INCLUDE_NAMES gst/rtp/rtp.h
|
||||||
|
INCLUDE_DIR_SUFFIXES gstreamer-1.0 gstreamer-1.0/include gstreamer-rtp-1.0 gstreamer-rtp-1.0/include
|
||||||
|
DEPENDS Gst
|
||||||
|
)
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(GstRtp
|
||||||
|
REQUIRED_VARS GstRtp_LIBRARY
|
||||||
|
VERSION_VAR GstRtp_VERSION)
|
14
cmake/FindGstVideo.cmake
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
include(PkgConfigWithFallback)
|
||||||
|
find_pkg_config_with_fallback(GstVideo
|
||||||
|
PKG_CONFIG_NAME gstreamer-video-1.0
|
||||||
|
LIB_NAMES gstvideo
|
||||||
|
LIB_DIR_HINTS gstreamer-1.0
|
||||||
|
INCLUDE_NAMES gst/video/video.h
|
||||||
|
INCLUDE_DIR_SUFFIXES gstreamer-1.0 gstreamer-1.0/include gstreamer-video-1.0 gstreamer-video-1.0/include
|
||||||
|
DEPENDS Gst
|
||||||
|
)
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(GstVideo
|
||||||
|
REQUIRED_VARS GstVideo_LIBRARY
|
||||||
|
VERSION_VAR GstVideo_VERSION)
|
13
cmake/FindNice.cmake
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
include(PkgConfigWithFallback)
|
||||||
|
find_pkg_config_with_fallback(Nice
|
||||||
|
PKG_CONFIG_NAME nice
|
||||||
|
LIB_NAMES nice
|
||||||
|
INCLUDE_NAMES nice.h
|
||||||
|
INCLUDE_DIR_SUFFIXES nice nice/include
|
||||||
|
DEPENDS GIO
|
||||||
|
)
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(Nice
|
||||||
|
REQUIRED_VARS Nice_LIBRARY
|
||||||
|
VERSION_VAR Nice_VERSION)
|
12
cmake/FindSrtp2.cmake
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
include(PkgConfigWithFallback)
|
||||||
|
find_pkg_config_with_fallback(Srtp2
|
||||||
|
PKG_CONFIG_NAME libsrtp2
|
||||||
|
LIB_NAMES srtp2
|
||||||
|
INCLUDE_NAMES srtp2/srtp.h
|
||||||
|
INCLUDE_DIR_SUFFIXES srtp2 srtp2/include
|
||||||
|
)
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(Srtp2
|
||||||
|
REQUIRED_VARS Srtp2_LIBRARY
|
||||||
|
VERSION_VAR Srtp2_VERSION)
|
12
cmake/FindWebRTCAudioProcessing.cmake
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
include(PkgConfigWithFallback)
|
||||||
|
find_pkg_config_with_fallback(WebRTCAudioProcessing
|
||||||
|
PKG_CONFIG_NAME webrtc-audio-processing
|
||||||
|
LIB_NAMES webrtc_audio_processing
|
||||||
|
INCLUDE_NAMES webrtc/modules/audio_processing/include/audio_processing.h
|
||||||
|
INCLUDE_DIR_SUFFIXES webrtc-audio-processing webrtc_audio_processing
|
||||||
|
)
|
||||||
|
|
||||||
|
include(FindPackageHandleStandardArgs)
|
||||||
|
find_package_handle_standard_args(WebRTCAudioProcessing
|
||||||
|
REQUIRED_VARS WebRTCAudioProcessing_LIBRARY
|
||||||
|
VERSION_VAR WebRTCAudioProcessing_VERSION)
|
|
@ -10,7 +10,7 @@ function(find_pkg_config_with_fallback name)
|
||||||
endif(PKG_CONFIG_FOUND)
|
endif(PKG_CONFIG_FOUND)
|
||||||
|
|
||||||
if (${name}_PKG_CONFIG_FOUND)
|
if (${name}_PKG_CONFIG_FOUND)
|
||||||
# Found via pkg-config, using it's result values
|
# Found via pkg-config, using its result values
|
||||||
set(${name}_FOUND ${${name}_PKG_CONFIG_FOUND})
|
set(${name}_FOUND ${${name}_PKG_CONFIG_FOUND})
|
||||||
|
|
||||||
# Try to find real file name of libraries
|
# Try to find real file name of libraries
|
||||||
|
|
|
@ -15,6 +15,7 @@ SOURCES
|
||||||
src/dbus/upower.vala
|
src/dbus/upower.vala
|
||||||
|
|
||||||
src/entity/account.vala
|
src/entity/account.vala
|
||||||
|
src/entity/call.vala
|
||||||
src/entity/conversation.vala
|
src/entity/conversation.vala
|
||||||
src/entity/encryption.vala
|
src/entity/encryption.vala
|
||||||
src/entity/file_transfer.vala
|
src/entity/file_transfer.vala
|
||||||
|
@ -27,6 +28,8 @@ SOURCES
|
||||||
|
|
||||||
src/service/avatar_manager.vala
|
src/service/avatar_manager.vala
|
||||||
src/service/blocking_manager.vala
|
src/service/blocking_manager.vala
|
||||||
|
src/service/call_store.vala
|
||||||
|
src/service/calls.vala
|
||||||
src/service/chat_interaction.vala
|
src/service/chat_interaction.vala
|
||||||
src/service/connection_manager.vala
|
src/service/connection_manager.vala
|
||||||
src/service/content_item_store.vala
|
src/service/content_item_store.vala
|
||||||
|
|
|
@ -39,6 +39,8 @@ public interface Application : GLib.Application {
|
||||||
AvatarManager.start(stream_interactor, db);
|
AvatarManager.start(stream_interactor, db);
|
||||||
RosterManager.start(stream_interactor, db);
|
RosterManager.start(stream_interactor, db);
|
||||||
FileManager.start(stream_interactor, db);
|
FileManager.start(stream_interactor, db);
|
||||||
|
Calls.start(stream_interactor, db);
|
||||||
|
CallStore.start(stream_interactor, db);
|
||||||
ContentItemStore.start(stream_interactor, db);
|
ContentItemStore.start(stream_interactor, db);
|
||||||
ChatInteraction.start(stream_interactor);
|
ChatInteraction.start(stream_interactor);
|
||||||
NotificationEvents.start(stream_interactor);
|
NotificationEvents.start(stream_interactor);
|
||||||
|
|
133
libdino/src/entity/call.vala
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
using Xmpp;
|
||||||
|
|
||||||
|
namespace Dino.Entities {
|
||||||
|
|
||||||
|
public class Call : Object {
|
||||||
|
|
||||||
|
public const bool DIRECTION_OUTGOING = true;
|
||||||
|
public const bool DIRECTION_INCOMING = false;
|
||||||
|
|
||||||
|
public enum State {
|
||||||
|
RINGING,
|
||||||
|
ESTABLISHING,
|
||||||
|
IN_PROGRESS,
|
||||||
|
OTHER_DEVICE_ACCEPTED,
|
||||||
|
ENDED,
|
||||||
|
DECLINED,
|
||||||
|
MISSED,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
|
||||||
|
public int id { get; set; default=-1; }
|
||||||
|
public Account account { get; set; }
|
||||||
|
public Jid counterpart { get; set; }
|
||||||
|
public Jid ourpart { get; set; }
|
||||||
|
public Jid? from {
|
||||||
|
get { return direction == DIRECTION_OUTGOING ? ourpart : counterpart; }
|
||||||
|
}
|
||||||
|
public Jid? to {
|
||||||
|
get { return direction == DIRECTION_OUTGOING ? counterpart : ourpart; }
|
||||||
|
}
|
||||||
|
public bool direction { get; set; }
|
||||||
|
public DateTime time { get; set; }
|
||||||
|
public DateTime local_time { get; set; }
|
||||||
|
public DateTime end_time { get; set; }
|
||||||
|
public Encryption encryption { get; set; default=Encryption.NONE; }
|
||||||
|
|
||||||
|
public State state { get; set; }
|
||||||
|
|
||||||
|
private Database? db;
|
||||||
|
|
||||||
|
public Call.from_row(Database db, Qlite.Row row) throws InvalidJidError {
|
||||||
|
this.db = db;
|
||||||
|
|
||||||
|
id = row[db.call.id];
|
||||||
|
account = db.get_account_by_id(row[db.call.account_id]);
|
||||||
|
|
||||||
|
counterpart = db.get_jid_by_id(row[db.call.counterpart_id]);
|
||||||
|
string counterpart_resource = row[db.call.counterpart_resource];
|
||||||
|
if (counterpart_resource != null) counterpart = counterpart.with_resource(counterpart_resource);
|
||||||
|
|
||||||
|
string our_resource = row[db.call.our_resource];
|
||||||
|
if (our_resource != null) {
|
||||||
|
ourpart = account.bare_jid.with_resource(our_resource);
|
||||||
|
} else {
|
||||||
|
ourpart = account.bare_jid;
|
||||||
|
}
|
||||||
|
direction = row[db.call.direction];
|
||||||
|
time = new DateTime.from_unix_utc(row[db.call.time]);
|
||||||
|
local_time = new DateTime.from_unix_utc(row[db.call.local_time]);
|
||||||
|
end_time = new DateTime.from_unix_utc(row[db.call.end_time]);
|
||||||
|
encryption = (Encryption) row[db.call.encryption];
|
||||||
|
state = (State) row[db.call.state];
|
||||||
|
|
||||||
|
notify.connect(on_update);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void persist(Database db) {
|
||||||
|
if (id != -1) return;
|
||||||
|
|
||||||
|
this.db = db;
|
||||||
|
Qlite.InsertBuilder builder = db.call.insert()
|
||||||
|
.value(db.call.account_id, account.id)
|
||||||
|
.value(db.call.counterpart_id, db.get_jid_id(counterpart))
|
||||||
|
.value(db.call.counterpart_resource, counterpart.resourcepart)
|
||||||
|
.value(db.call.our_resource, ourpart.resourcepart)
|
||||||
|
.value(db.call.direction, direction)
|
||||||
|
.value(db.call.time, (long) time.to_unix())
|
||||||
|
.value(db.call.local_time, (long) local_time.to_unix())
|
||||||
|
.value(db.call.encryption, encryption)
|
||||||
|
.value(db.call.state, State.ENDED); // No point in persisting states that can't survive a restart
|
||||||
|
if (end_time != null) {
|
||||||
|
builder.value(db.call.end_time, (long) end_time.to_unix());
|
||||||
|
} else {
|
||||||
|
builder.value(db.call.end_time, (long) local_time.to_unix());
|
||||||
|
}
|
||||||
|
id = (int) builder.perform();
|
||||||
|
|
||||||
|
notify.connect(on_update);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool equals(Call c) {
|
||||||
|
return equals_func(this, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool equals_func(Call c1, Call c2) {
|
||||||
|
if (c1.id == c2.id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static uint hash_func(Call call) {
|
||||||
|
return (uint)call.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_update(Object o, ParamSpec sp) {
|
||||||
|
Qlite.UpdateBuilder update_builder = db.call.update().with(db.call.id, "=", id);
|
||||||
|
switch (sp.name) {
|
||||||
|
case "counterpart":
|
||||||
|
update_builder.set(db.call.counterpart_id, db.get_jid_id(counterpart));
|
||||||
|
update_builder.set(db.call.counterpart_resource, counterpart.resourcepart); break;
|
||||||
|
case "ourpart":
|
||||||
|
update_builder.set(db.call.our_resource, ourpart.resourcepart); break;
|
||||||
|
case "direction":
|
||||||
|
update_builder.set(db.call.direction, direction); break;
|
||||||
|
case "time":
|
||||||
|
update_builder.set(db.call.time, (long) time.to_unix()); break;
|
||||||
|
case "local-time":
|
||||||
|
update_builder.set(db.call.local_time, (long) local_time.to_unix()); break;
|
||||||
|
case "end-time":
|
||||||
|
update_builder.set(db.call.end_time, (long) end_time.to_unix()); break;
|
||||||
|
case "encryption":
|
||||||
|
update_builder.set(db.call.encryption, encryption); break;
|
||||||
|
case "state":
|
||||||
|
// No point in persisting states that can't survive a restart
|
||||||
|
if (state == State.RINGING || state == State.ESTABLISHING || state == State.IN_PROGRESS) return;
|
||||||
|
update_builder.set(db.call.state, state);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
update_builder.perform();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,9 @@ namespace Dino.Entities {
|
||||||
public enum Encryption {
|
public enum Encryption {
|
||||||
NONE,
|
NONE,
|
||||||
PGP,
|
PGP,
|
||||||
OMEMO
|
OMEMO,
|
||||||
|
DTLS_SRTP,
|
||||||
|
SRTP,
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -29,6 +29,16 @@ public interface EncryptionListEntry : Object {
|
||||||
public abstract Object? get_encryption_icon(Entities.Conversation conversation, ContentItem content_item);
|
public abstract Object? get_encryption_icon(Entities.Conversation conversation, ContentItem content_item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public interface CallEncryptionEntry : Object {
|
||||||
|
public abstract CallEncryptionWidget? get_widget(Account account, Xmpp.Xep.Jingle.ContentEncryption encryption);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface CallEncryptionWidget : Object {
|
||||||
|
public abstract string? get_title();
|
||||||
|
public abstract bool show_keys();
|
||||||
|
public abstract string? get_icon_name();
|
||||||
|
}
|
||||||
|
|
||||||
public abstract class AccountSettingsEntry : Object {
|
public abstract class AccountSettingsEntry : Object {
|
||||||
public abstract string id { get; }
|
public abstract string id { get; }
|
||||||
public virtual Priority priority { get { return Priority.DEFAULT; } }
|
public virtual Priority priority { get { return Priority.DEFAULT; } }
|
||||||
|
@ -84,6 +94,33 @@ public abstract interface ConversationAdditionPopulator : ConversationItemPopula
|
||||||
public virtual void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
|
public virtual void populate_timespan(Conversation conversation, DateTime from, DateTime to) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public abstract interface VideoCallPlugin : Object {
|
||||||
|
|
||||||
|
public abstract bool supports(string media);
|
||||||
|
// Video widget
|
||||||
|
public abstract VideoCallWidget? create_widget(WidgetType type);
|
||||||
|
|
||||||
|
// Devices
|
||||||
|
public signal void devices_changed(string media, bool incoming);
|
||||||
|
public abstract Gee.List<MediaDevice> get_devices(string media, bool incoming);
|
||||||
|
public abstract MediaDevice? get_device(Xmpp.Xep.JingleRtp.Stream stream, bool incoming);
|
||||||
|
public abstract void set_pause(Xmpp.Xep.JingleRtp.Stream stream, bool pause);
|
||||||
|
public abstract void set_device(Xmpp.Xep.JingleRtp.Stream stream, MediaDevice? device);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract interface VideoCallWidget : Object {
|
||||||
|
public signal void resolution_changed(uint width, uint height);
|
||||||
|
public abstract void display_stream(Xmpp.Xep.JingleRtp.Stream stream); // TODO: Multi participant
|
||||||
|
public abstract void display_device(MediaDevice device);
|
||||||
|
public abstract void detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract interface MediaDevice : Object {
|
||||||
|
public abstract string id { get; }
|
||||||
|
public abstract string display_name { get; }
|
||||||
|
public abstract string detail_name { get; }
|
||||||
|
}
|
||||||
|
|
||||||
public abstract interface NotificationPopulator : Object {
|
public abstract interface NotificationPopulator : Object {
|
||||||
public abstract string id { get; }
|
public abstract string id { get; }
|
||||||
public abstract void init(Conversation conversation, NotificationCollection summary, WidgetType type);
|
public abstract void init(Conversation conversation, NotificationCollection summary, WidgetType type);
|
||||||
|
|
|
@ -26,7 +26,7 @@ public class Loader : Object {
|
||||||
this.search_paths = app.search_path_generator.get_plugin_paths();
|
this.search_paths = app.search_path_generator.get_plugin_paths();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loadAll() throws Error {
|
public void load_all() throws Error {
|
||||||
if (Module.supported() == false) {
|
if (Module.supported() == false) {
|
||||||
throw new Error(-1, 0, "Plugins are not supported");
|
throw new Error(-1, 0, "Plugins are not supported");
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ namespace Dino.Plugins {
|
||||||
|
|
||||||
public class Registry {
|
public class Registry {
|
||||||
internal ArrayList<EncryptionListEntry> encryption_list_entries = new ArrayList<EncryptionListEntry>();
|
internal ArrayList<EncryptionListEntry> encryption_list_entries = new ArrayList<EncryptionListEntry>();
|
||||||
|
internal HashMap<string, CallEncryptionEntry> call_encryption_entries = new HashMap<string, CallEncryptionEntry>();
|
||||||
internal ArrayList<AccountSettingsEntry> account_settings_entries = new ArrayList<AccountSettingsEntry>();
|
internal ArrayList<AccountSettingsEntry> account_settings_entries = new ArrayList<AccountSettingsEntry>();
|
||||||
internal ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>();
|
internal ArrayList<ContactDetailsProvider> contact_details_entries = new ArrayList<ContactDetailsProvider>();
|
||||||
internal Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>();
|
internal Map<string, TextCommand> text_commands = new HashMap<string, TextCommand>();
|
||||||
|
@ -12,6 +13,7 @@ public class Registry {
|
||||||
internal Gee.Collection<ConversationTitlebarEntry> conversation_titlebar_entries = new Gee.TreeSet<ConversationTitlebarEntry>((a, b) => {
|
internal Gee.Collection<ConversationTitlebarEntry> conversation_titlebar_entries = new Gee.TreeSet<ConversationTitlebarEntry>((a, b) => {
|
||||||
return (int)(a.order - b.order);
|
return (int)(a.order - b.order);
|
||||||
});
|
});
|
||||||
|
public VideoCallPlugin? video_call_plugin;
|
||||||
|
|
||||||
public bool register_encryption_list_entry(EncryptionListEntry entry) {
|
public bool register_encryption_list_entry(EncryptionListEntry entry) {
|
||||||
lock(encryption_list_entries) {
|
lock(encryption_list_entries) {
|
||||||
|
@ -24,6 +26,13 @@ public class Registry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool register_call_entryption_entry(string ns, CallEncryptionEntry entry) {
|
||||||
|
lock (call_encryption_entries) {
|
||||||
|
call_encryption_entries[ns] = entry;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public bool register_account_settings_entry(AccountSettingsEntry entry) {
|
public bool register_account_settings_entry(AccountSettingsEntry entry) {
|
||||||
lock(account_settings_entries) {
|
lock(account_settings_entries) {
|
||||||
foreach(var e in account_settings_entries) {
|
foreach(var e in account_settings_entries) {
|
||||||
|
|
61
libdino/src/service/call_store.vala
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
using Xmpp;
|
||||||
|
using Gee;
|
||||||
|
using Qlite;
|
||||||
|
|
||||||
|
using Dino.Entities;
|
||||||
|
|
||||||
|
namespace Dino {
|
||||||
|
|
||||||
|
public class CallStore : StreamInteractionModule, Object {
|
||||||
|
public static ModuleIdentity<CallStore> IDENTITY = new ModuleIdentity<CallStore>("call_store");
|
||||||
|
public string id { get { return IDENTITY.id; } }
|
||||||
|
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
private Database db;
|
||||||
|
|
||||||
|
private WeakMap<int, Call> calls_by_db_id = new WeakMap<int, Call>();
|
||||||
|
|
||||||
|
public static void start(StreamInteractor stream_interactor, Database db) {
|
||||||
|
CallStore m = new CallStore(stream_interactor, db);
|
||||||
|
stream_interactor.add_module(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CallStore(StreamInteractor stream_interactor, Database db) {
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add_call(Call call, Conversation conversation) {
|
||||||
|
call.persist(db);
|
||||||
|
cache_call(call);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Call? get_call_by_id(int id) {
|
||||||
|
Call? call = calls_by_db_id[id];
|
||||||
|
if (call != null) {
|
||||||
|
return call;
|
||||||
|
}
|
||||||
|
|
||||||
|
RowOption row_option = db.call.select().with(db.call.id, "=", id).row();
|
||||||
|
|
||||||
|
return create_call_from_row_opt(row_option);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Call? create_call_from_row_opt(RowOption row_opt) {
|
||||||
|
if (!row_opt.is_present()) return null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Call call = new Call.from_row(db, row_opt.inner);
|
||||||
|
cache_call(call);
|
||||||
|
return call;
|
||||||
|
} catch (InvalidJidError e) {
|
||||||
|
warning("Got message with invalid Jid: %s", e.message);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void cache_call(Call call) {
|
||||||
|
calls_by_db_id[call.id] = call;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
686
libdino/src/service/calls.vala
Normal file
|
@ -0,0 +1,686 @@
|
||||||
|
using Gee;
|
||||||
|
|
||||||
|
using Xmpp;
|
||||||
|
using Dino.Entities;
|
||||||
|
|
||||||
|
namespace Dino {
|
||||||
|
|
||||||
|
public class Calls : StreamInteractionModule, Object {
|
||||||
|
|
||||||
|
public signal void call_incoming(Call call, Conversation conversation, bool video);
|
||||||
|
public signal void call_outgoing(Call call, Conversation conversation);
|
||||||
|
|
||||||
|
public signal void call_terminated(Call call, string? reason_name, string? reason_text);
|
||||||
|
public signal void counterpart_ringing(Call call);
|
||||||
|
public signal void counterpart_sends_video_updated(Call call, bool mute);
|
||||||
|
public signal void info_received(Call call, Xep.JingleRtp.CallSessionInfo session_info);
|
||||||
|
public signal void encryption_updated(Call call, Xep.Jingle.ContentEncryption? audio_encryption, Xep.Jingle.ContentEncryption? video_encryption, bool same);
|
||||||
|
|
||||||
|
public signal void stream_created(Call call, string media);
|
||||||
|
|
||||||
|
public static ModuleIdentity<Calls> IDENTITY = new ModuleIdentity<Calls>("calls");
|
||||||
|
public string id { get { return IDENTITY.id; } }
|
||||||
|
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
private Database db;
|
||||||
|
|
||||||
|
private HashMap<Account, HashMap<Call, string>> sid_by_call = new HashMap<Account, HashMap<Call, string>>(Account.hash_func, Account.equals_func);
|
||||||
|
private HashMap<Account, HashMap<string, Call>> call_by_sid = new HashMap<Account, HashMap<string, Call>>(Account.hash_func, Account.equals_func);
|
||||||
|
public HashMap<Call, Xep.Jingle.Session> sessions = new HashMap<Call, Xep.Jingle.Session>(Call.hash_func, Call.equals_func);
|
||||||
|
|
||||||
|
public HashMap<Account, Call> jmi_call = new HashMap<Account, Call>(Account.hash_func, Account.equals_func);
|
||||||
|
public HashMap<Account, string> jmi_sid = new HashMap<Account, string>(Account.hash_func, Account.equals_func);
|
||||||
|
public HashMap<Account, bool> jmi_video = new HashMap<Account, bool>(Account.hash_func, Account.equals_func);
|
||||||
|
|
||||||
|
private HashMap<Call, bool> counterpart_sends_video = new HashMap<Call, bool>(Call.hash_func, Call.equals_func);
|
||||||
|
private HashMap<Call, bool> we_should_send_video = new HashMap<Call, bool>(Call.hash_func, Call.equals_func);
|
||||||
|
private HashMap<Call, bool> we_should_send_audio = new HashMap<Call, bool>(Call.hash_func, Call.equals_func);
|
||||||
|
|
||||||
|
private HashMap<Call, Xep.JingleRtp.Parameters> audio_content_parameter = new HashMap<Call, Xep.JingleRtp.Parameters>(Call.hash_func, Call.equals_func);
|
||||||
|
private HashMap<Call, Xep.JingleRtp.Parameters> video_content_parameter = new HashMap<Call, Xep.JingleRtp.Parameters>(Call.hash_func, Call.equals_func);
|
||||||
|
private HashMap<Call, Xep.Jingle.Content> audio_content = new HashMap<Call, Xep.Jingle.Content>(Call.hash_func, Call.equals_func);
|
||||||
|
private HashMap<Call, Xep.Jingle.Content> video_content = new HashMap<Call, Xep.Jingle.Content>(Call.hash_func, Call.equals_func);
|
||||||
|
private HashMap<Call, HashMap<string, Xep.Jingle.ContentEncryption>> video_encryptions = new HashMap<Call, HashMap<string, Xep.Jingle.ContentEncryption>>(Call.hash_func, Call.equals_func);
|
||||||
|
private HashMap<Call, HashMap<string, Xep.Jingle.ContentEncryption>> audio_encryptions = new HashMap<Call, HashMap<string, Xep.Jingle.ContentEncryption>>(Call.hash_func, Call.equals_func);
|
||||||
|
|
||||||
|
public static void start(StreamInteractor stream_interactor, Database db) {
|
||||||
|
Calls m = new Calls(stream_interactor, db);
|
||||||
|
stream_interactor.add_module(m);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Calls(StreamInteractor stream_interactor, Database db) {
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
this.db = db;
|
||||||
|
|
||||||
|
stream_interactor.account_added.connect(on_account_added);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Xep.JingleRtp.Stream? get_video_stream(Call call) {
|
||||||
|
if (video_content_parameter.has_key(call)) {
|
||||||
|
return video_content_parameter[call].stream;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Xep.JingleRtp.Stream? get_audio_stream(Call call) {
|
||||||
|
if (audio_content_parameter.has_key(call)) {
|
||||||
|
return audio_content_parameter[call].stream;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Call? initiate_call(Conversation conversation, bool video) {
|
||||||
|
Call call = new Call();
|
||||||
|
call.direction = Call.DIRECTION_OUTGOING;
|
||||||
|
call.account = conversation.account;
|
||||||
|
call.counterpart = conversation.counterpart;
|
||||||
|
call.ourpart = conversation.account.full_jid;
|
||||||
|
call.time = call.local_time = call.end_time = new DateTime.now_utc();
|
||||||
|
call.state = Call.State.RINGING;
|
||||||
|
|
||||||
|
stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation);
|
||||||
|
|
||||||
|
we_should_send_video[call] = video;
|
||||||
|
we_should_send_audio[call] = true;
|
||||||
|
|
||||||
|
Gee.List<Jid> call_resources = yield get_call_resources(conversation);
|
||||||
|
|
||||||
|
bool do_jmi = false;
|
||||||
|
Jid? jid_for_direct = null;
|
||||||
|
if (yield contains_jmi_resources(conversation.account, call_resources)) {
|
||||||
|
do_jmi = true;
|
||||||
|
} else if (!call_resources.is_empty) {
|
||||||
|
jid_for_direct = call_resources[0];
|
||||||
|
} else if (has_jmi_resources(conversation)) {
|
||||||
|
do_jmi = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (do_jmi) {
|
||||||
|
XmppStream? stream = stream_interactor.get_stream(conversation.account);
|
||||||
|
jmi_call[conversation.account] = call;
|
||||||
|
jmi_video[conversation.account] = video;
|
||||||
|
jmi_sid[conversation.account] = Xmpp.random_uuid();
|
||||||
|
|
||||||
|
call_by_sid[call.account][jmi_sid[conversation.account]] = call;
|
||||||
|
|
||||||
|
var descriptions = new ArrayList<StanzaNode>();
|
||||||
|
descriptions.add(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "audio"));
|
||||||
|
if (video) {
|
||||||
|
descriptions.add(new StanzaNode.build("description", Xep.JingleRtp.NS_URI).add_self_xmlns().put_attribute("media", "video"));
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.get_module(Xmpp.Xep.JingleMessageInitiation.Module.IDENTITY).send_session_propose_to_peer(stream, conversation.counterpart, jmi_sid[call.account], descriptions);
|
||||||
|
} else if (jid_for_direct != null) {
|
||||||
|
yield call_resource(conversation.account, jid_for_direct, call, video);
|
||||||
|
}
|
||||||
|
|
||||||
|
conversation.last_active = call.time;
|
||||||
|
call_outgoing(call, conversation);
|
||||||
|
|
||||||
|
return call;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void call_resource(Account account, Jid full_jid, Call call, bool video, string? sid = null) {
|
||||||
|
XmppStream? stream = stream_interactor.get_stream(account);
|
||||||
|
if (stream == null) return;
|
||||||
|
|
||||||
|
Xep.Jingle.Session session = yield stream.get_module(Xep.JingleRtp.Module.IDENTITY).start_call(stream, full_jid, video, sid);
|
||||||
|
sessions[call] = session;
|
||||||
|
sid_by_call[call.account][call] = session.sid;
|
||||||
|
|
||||||
|
connect_session_signals(call, session);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void end_call(Conversation conversation, Call call) {
|
||||||
|
XmppStream? stream = stream_interactor.get_stream(call.account);
|
||||||
|
if (stream == null) return;
|
||||||
|
|
||||||
|
if (call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) {
|
||||||
|
sessions[call].terminate(Xep.Jingle.ReasonElement.SUCCESS, null, "success");
|
||||||
|
call.state = Call.State.ENDED;
|
||||||
|
} else if (call.state == Call.State.RINGING) {
|
||||||
|
if (sessions.has_key(call)) {
|
||||||
|
sessions[call].terminate(Xep.Jingle.ReasonElement.CANCEL, null, "cancel");
|
||||||
|
} else {
|
||||||
|
// Only a JMI so far
|
||||||
|
stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_retract_to_peer(stream, call.counterpart, jmi_sid[call.account]);
|
||||||
|
}
|
||||||
|
call.state = Call.State.MISSED;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
call.end_time = new DateTime.now_utc();
|
||||||
|
|
||||||
|
remove_call_from_datastructures(call);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void accept_call(Call call) {
|
||||||
|
call.state = Call.State.ESTABLISHING;
|
||||||
|
|
||||||
|
if (sessions.has_key(call)) {
|
||||||
|
foreach (Xep.Jingle.Content content in sessions[call].contents) {
|
||||||
|
content.accept();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Only a JMI so far
|
||||||
|
Account account = call.account;
|
||||||
|
string sid = sid_by_call[call.account][call];
|
||||||
|
XmppStream stream = stream_interactor.get_stream(account);
|
||||||
|
if (stream == null) return;
|
||||||
|
|
||||||
|
jmi_call[account] = call;
|
||||||
|
jmi_sid[account] = sid;
|
||||||
|
jmi_video[account] = we_should_send_video[call];
|
||||||
|
|
||||||
|
stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_accept_to_self(stream, sid);
|
||||||
|
stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_proceed_to_peer(stream, call.counterpart, sid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reject_call(Call call) {
|
||||||
|
call.state = Call.State.DECLINED;
|
||||||
|
|
||||||
|
if (sessions.has_key(call)) {
|
||||||
|
foreach (Xep.Jingle.Content content in sessions[call].contents) {
|
||||||
|
content.reject();
|
||||||
|
}
|
||||||
|
remove_call_from_datastructures(call);
|
||||||
|
} else {
|
||||||
|
// Only a JMI so far
|
||||||
|
XmppStream stream = stream_interactor.get_stream(call.account);
|
||||||
|
if (stream == null) return;
|
||||||
|
|
||||||
|
string sid = sid_by_call[call.account][call];
|
||||||
|
stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_reject_to_peer(stream, call.counterpart, sid);
|
||||||
|
stream.get_module(Xep.JingleMessageInitiation.Module.IDENTITY).send_session_reject_to_self(stream, sid);
|
||||||
|
remove_call_from_datastructures(call);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void mute_own_audio(Call call, bool mute) {
|
||||||
|
we_should_send_audio[call] = !mute;
|
||||||
|
|
||||||
|
Xep.JingleRtp.Stream stream = audio_content_parameter[call].stream;
|
||||||
|
// The user might mute audio before a feed was created. The feed will be muted as soon as it has been created.
|
||||||
|
if (stream == null) return;
|
||||||
|
|
||||||
|
// Inform our counterpart that we (un)muted our audio
|
||||||
|
stream_interactor.module_manager.get_module(call.account, Xep.JingleRtp.Module.IDENTITY).session_info_type.send_mute(sessions[call], mute, "audio");
|
||||||
|
|
||||||
|
// Start/Stop sending audio data
|
||||||
|
Application.get_default().plugin_registry.video_call_plugin.set_pause(stream, mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void mute_own_video(Call call, bool mute) {
|
||||||
|
we_should_send_video[call] = !mute;
|
||||||
|
|
||||||
|
if (!sessions.has_key(call)) {
|
||||||
|
// Call hasn't been established yet
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Xep.JingleRtp.Module rtp_module = stream_interactor.module_manager.get_module(call.account, Xep.JingleRtp.Module.IDENTITY);
|
||||||
|
|
||||||
|
if (video_content_parameter.has_key(call) &&
|
||||||
|
video_content_parameter[call].stream != null &&
|
||||||
|
sessions[call].senders_include_us(video_content[call].senders)) {
|
||||||
|
// A video feed has already been established
|
||||||
|
|
||||||
|
// Start/Stop sending video data
|
||||||
|
Xep.JingleRtp.Stream stream = video_content_parameter[call].stream;
|
||||||
|
if (stream != null) {
|
||||||
|
// TODO maybe the user muted video before the feed was created...
|
||||||
|
Application.get_default().plugin_registry.video_call_plugin.set_pause(stream, mute);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inform our counterpart that we started/stopped our video
|
||||||
|
rtp_module.session_info_type.send_mute(sessions[call], mute, "video");
|
||||||
|
} else if (!mute) {
|
||||||
|
// Need to start a new video feed
|
||||||
|
XmppStream stream = stream_interactor.get_stream(call.account);
|
||||||
|
rtp_module.add_outgoing_video_content.begin(stream, sessions[call], (_, res) => {
|
||||||
|
if (video_content_parameter[call] == null) {
|
||||||
|
Xep.Jingle.Content content = rtp_module.add_outgoing_video_content.end(res);
|
||||||
|
Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
|
||||||
|
if (rtp_content_parameter != null) {
|
||||||
|
connect_content_signals(call, content, rtp_content_parameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// If video_feed == null && !mute we're trying to mute a non-existant feed. It will be muted as soon as it is created.
|
||||||
|
}
|
||||||
|
|
||||||
|
public async bool can_do_audio_calls_async(Conversation conversation) {
|
||||||
|
if (!can_do_audio_calls()) return false;
|
||||||
|
return (yield get_call_resources(conversation)).size > 0 || has_jmi_resources(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool can_do_audio_calls() {
|
||||||
|
Plugins.VideoCallPlugin? plugin = Application.get_default().plugin_registry.video_call_plugin;
|
||||||
|
if (plugin == null) return false;
|
||||||
|
|
||||||
|
return plugin.supports("audio");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async bool can_do_video_calls_async(Conversation conversation) {
|
||||||
|
if (!can_do_video_calls()) return false;
|
||||||
|
return (yield get_call_resources(conversation)).size > 0 || has_jmi_resources(conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool can_do_video_calls() {
|
||||||
|
Plugins.VideoCallPlugin? plugin = Application.get_default().plugin_registry.video_call_plugin;
|
||||||
|
if (plugin == null) return false;
|
||||||
|
|
||||||
|
return plugin.supports("video");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Gee.List<Jid> get_call_resources(Conversation conversation) {
|
||||||
|
ArrayList<Jid> ret = new ArrayList<Jid>();
|
||||||
|
|
||||||
|
XmppStream? stream = stream_interactor.get_stream(conversation.account);
|
||||||
|
if (stream == null) return ret;
|
||||||
|
|
||||||
|
Gee.List<Jid>? full_jids = stream.get_flag(Presence.Flag.IDENTITY).get_resources(conversation.counterpart);
|
||||||
|
if (full_jids == null) return ret;
|
||||||
|
|
||||||
|
foreach (Jid full_jid in full_jids) {
|
||||||
|
bool supports_rtc = yield stream.get_module(Xep.JingleRtp.Module.IDENTITY).is_available(stream, full_jid);
|
||||||
|
if (!supports_rtc) continue;
|
||||||
|
ret.add(full_jid);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async bool contains_jmi_resources(Account account, Gee.List<Jid> full_jids) {
|
||||||
|
XmppStream? stream = stream_interactor.get_stream(account);
|
||||||
|
if (stream == null) return false;
|
||||||
|
|
||||||
|
foreach (Jid full_jid in full_jids) {
|
||||||
|
bool does_jmi = yield stream_interactor.get_module(EntityInfo.IDENTITY).has_feature(account, full_jid, Xep.JingleMessageInitiation.NS_URI);
|
||||||
|
if (does_jmi) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool has_jmi_resources(Conversation conversation) {
|
||||||
|
int64 jmi_resources = db.entity.select()
|
||||||
|
.with(db.entity.jid_id, "=", db.get_jid_id(conversation.counterpart))
|
||||||
|
.join_with(db.entity_feature, db.entity.caps_hash, db.entity_feature.entity)
|
||||||
|
.with(db.entity_feature.feature, "=", Xep.JingleMessageInitiation.NS_URI)
|
||||||
|
.count();
|
||||||
|
return jmi_resources > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool should_we_send_video(Call call) {
|
||||||
|
return we_should_send_video[call];
|
||||||
|
}
|
||||||
|
|
||||||
|
public Jid? is_call_in_progress() {
|
||||||
|
foreach (Call call in sessions.keys) {
|
||||||
|
if (call.state == Call.State.IN_PROGRESS || call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) {
|
||||||
|
return call.counterpart;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_incoming_call(Account account, Xep.Jingle.Session session) {
|
||||||
|
if (!can_do_audio_calls()) {
|
||||||
|
warning("Incoming call but no call support detected. Ignoring.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool counterpart_wants_video = false;
|
||||||
|
foreach (Xep.Jingle.Content content in session.contents) {
|
||||||
|
Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
|
||||||
|
if (rtp_content_parameter == null) continue;
|
||||||
|
if (rtp_content_parameter.media == "video" && session.senders_include_us(content.senders)) {
|
||||||
|
counterpart_wants_video = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session might have already been accepted via Jingle Message Initiation
|
||||||
|
bool already_accepted = jmi_sid.has_key(account) &&
|
||||||
|
jmi_sid[account] == session.sid && jmi_call[account].account.equals(account) &&
|
||||||
|
jmi_call[account].counterpart.equals_bare(session.peer_full_jid) &&
|
||||||
|
jmi_video[account] == counterpart_wants_video;
|
||||||
|
|
||||||
|
Call? call = null;
|
||||||
|
if (already_accepted) {
|
||||||
|
call = jmi_call[account];
|
||||||
|
} else {
|
||||||
|
call = create_received_call(account, session.peer_full_jid, account.full_jid, counterpart_wants_video);
|
||||||
|
}
|
||||||
|
sessions[call] = session;
|
||||||
|
|
||||||
|
call_by_sid[account][session.sid] = call;
|
||||||
|
sid_by_call[account][call] = session.sid;
|
||||||
|
|
||||||
|
connect_session_signals(call, session);
|
||||||
|
|
||||||
|
if (already_accepted) {
|
||||||
|
accept_call(call);
|
||||||
|
} else {
|
||||||
|
stream_interactor.module_manager.get_module(account, Xep.JingleRtp.Module.IDENTITY).session_info_type.send_ringing(session);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Call create_received_call(Account account, Jid from, Jid to, bool video_requested) {
|
||||||
|
Call call = new Call();
|
||||||
|
if (from.equals_bare(account.bare_jid)) {
|
||||||
|
// Call requested by another of our devices
|
||||||
|
call.direction = Call.DIRECTION_OUTGOING;
|
||||||
|
call.ourpart = from;
|
||||||
|
call.counterpart = to;
|
||||||
|
} else {
|
||||||
|
call.direction = Call.DIRECTION_INCOMING;
|
||||||
|
call.ourpart = account.full_jid;
|
||||||
|
call.counterpart = from;
|
||||||
|
}
|
||||||
|
call.account = account;
|
||||||
|
call.time = call.local_time = call.end_time = new DateTime.now_utc();
|
||||||
|
call.state = Call.State.RINGING;
|
||||||
|
|
||||||
|
Conversation conversation = stream_interactor.get_module(ConversationManager.IDENTITY).create_conversation(call.counterpart.bare_jid, account, Conversation.Type.CHAT);
|
||||||
|
|
||||||
|
stream_interactor.get_module(CallStore.IDENTITY).add_call(call, conversation);
|
||||||
|
|
||||||
|
conversation.last_active = call.time;
|
||||||
|
|
||||||
|
we_should_send_video[call] = video_requested;
|
||||||
|
we_should_send_audio[call] = true;
|
||||||
|
|
||||||
|
if (call.direction == Call.DIRECTION_INCOMING) {
|
||||||
|
call_incoming(call, conversation, video_requested);
|
||||||
|
} else {
|
||||||
|
call_outgoing(call, conversation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return call;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_incoming_content_add(XmppStream stream, Call call, Xep.Jingle.Session session, Xep.Jingle.Content content) {
|
||||||
|
Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
|
||||||
|
|
||||||
|
if (rtp_content_parameter == null) {
|
||||||
|
content.reject();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Our peer shouldn't tell us to start sending, that's for us to initiate
|
||||||
|
if (session.senders_include_us(content.senders)) {
|
||||||
|
if (session.senders_include_counterpart(content.senders)) {
|
||||||
|
// If our peer wants to send, let them
|
||||||
|
content.modify(session.we_initiated ? Xep.Jingle.Senders.RESPONDER : Xep.Jingle.Senders.INITIATOR);
|
||||||
|
} else {
|
||||||
|
// If only we're supposed to send, reject
|
||||||
|
content.reject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connect_content_signals(call, content, rtp_content_parameter);
|
||||||
|
content.accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_call_terminated(Call call, bool we_terminated, string? reason_name, string? reason_text) {
|
||||||
|
if (call.state == Call.State.RINGING || call.state == Call.State.IN_PROGRESS || call.state == Call.State.ESTABLISHING) {
|
||||||
|
call.end_time = new DateTime.now_utc();
|
||||||
|
}
|
||||||
|
if (call.state == Call.State.IN_PROGRESS) {
|
||||||
|
call.state = Call.State.ENDED;
|
||||||
|
} else if (call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) {
|
||||||
|
if (reason_name == Xep.Jingle.ReasonElement.DECLINE) {
|
||||||
|
call.state = Call.State.DECLINED;
|
||||||
|
} else {
|
||||||
|
call.state = Call.State.FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
call_terminated(call, reason_name, reason_text);
|
||||||
|
remove_call_from_datastructures(call);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_stream_created(Call call, string media, Xep.JingleRtp.Stream stream) {
|
||||||
|
if (media == "video" && stream.receiving) {
|
||||||
|
counterpart_sends_video[call] = true;
|
||||||
|
video_content_parameter[call].connection_ready.connect((status) => {
|
||||||
|
counterpart_sends_video_updated(call, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
stream_created(call, media);
|
||||||
|
|
||||||
|
// Outgoing audio/video might have been muted in the meanwhile.
|
||||||
|
if (media == "video" && !we_should_send_video[call]) {
|
||||||
|
mute_own_video(call, true);
|
||||||
|
} else if (media == "audio" && !we_should_send_audio[call]) {
|
||||||
|
mute_own_audio(call, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_counterpart_mute_update(Call call, bool mute, string? media) {
|
||||||
|
if (!call.equals(call)) return;
|
||||||
|
|
||||||
|
if (media == "video") {
|
||||||
|
counterpart_sends_video[call] = !mute;
|
||||||
|
counterpart_sends_video_updated(call, mute);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connect_session_signals(Call call, Xep.Jingle.Session session) {
|
||||||
|
session.terminated.connect((stream, we_terminated, reason_name, reason_text) =>
|
||||||
|
on_call_terminated(call, we_terminated, reason_name, reason_text)
|
||||||
|
);
|
||||||
|
session.additional_content_add_incoming.connect((session,stream, content) =>
|
||||||
|
on_incoming_content_add(stream, call, session, content)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (Xep.Jingle.Content content in session.contents) {
|
||||||
|
Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
|
||||||
|
if (rtp_content_parameter == null) continue;
|
||||||
|
|
||||||
|
connect_content_signals(call, content, rtp_content_parameter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connect_content_signals(Call call, Xep.Jingle.Content content, Xep.JingleRtp.Parameters rtp_content_parameter) {
|
||||||
|
if (rtp_content_parameter.media == "audio") {
|
||||||
|
audio_content[call] = content;
|
||||||
|
audio_content_parameter[call] = rtp_content_parameter;
|
||||||
|
} else if (rtp_content_parameter.media == "video") {
|
||||||
|
video_content[call] = content;
|
||||||
|
video_content_parameter[call] = rtp_content_parameter;
|
||||||
|
}
|
||||||
|
|
||||||
|
rtp_content_parameter.stream_created.connect((stream) => on_stream_created(call, rtp_content_parameter.media, stream));
|
||||||
|
rtp_content_parameter.connection_ready.connect((status) => on_connection_ready(call, content, rtp_content_parameter.media));
|
||||||
|
|
||||||
|
content.senders_modify_incoming.connect((content, proposed_senders) => {
|
||||||
|
if (content.session.senders_include_us(content.senders) != content.session.senders_include_us(proposed_senders)) {
|
||||||
|
warning("counterpart set us to (not)sending %s. ignoring", content.content_name);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!content.session.senders_include_counterpart(content.senders) && content.session.senders_include_counterpart(proposed_senders)) {
|
||||||
|
// Counterpart wants to start sending. Ok.
|
||||||
|
content.accept_content_modify(proposed_senders);
|
||||||
|
on_counterpart_mute_update(call, false, "video");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_connection_ready(Call call, Xep.Jingle.Content content, string media) {
|
||||||
|
if (call.state == Call.State.RINGING || call.state == Call.State.ESTABLISHING) {
|
||||||
|
call.state = Call.State.IN_PROGRESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media == "audio") {
|
||||||
|
audio_encryptions[call] = content.encryptions;
|
||||||
|
} else if (media == "video") {
|
||||||
|
video_encryptions[call] = content.encryptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((audio_encryptions.has_key(call) && audio_encryptions[call].is_empty) || (video_encryptions.has_key(call) && video_encryptions[call].is_empty)) {
|
||||||
|
call.encryption = Encryption.NONE;
|
||||||
|
encryption_updated(call, null, null, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HashMap<string, Xep.Jingle.ContentEncryption> encryptions = audio_encryptions[call] ?? video_encryptions[call];
|
||||||
|
|
||||||
|
Xep.Jingle.ContentEncryption? omemo_encryption = null, dtls_encryption = null, srtp_encryption = null;
|
||||||
|
foreach (string encr_name in encryptions.keys) {
|
||||||
|
if (video_encryptions.has_key(call) && !video_encryptions[call].has_key(encr_name)) continue;
|
||||||
|
|
||||||
|
var encryption = encryptions[encr_name];
|
||||||
|
if (encryption.encryption_ns == "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification") {
|
||||||
|
omemo_encryption = encryption;
|
||||||
|
} else if (encryption.encryption_ns == Xep.JingleIceUdp.DTLS_NS_URI) {
|
||||||
|
dtls_encryption = encryption;
|
||||||
|
} else if (encryption.encryption_name == "SRTP") {
|
||||||
|
srtp_encryption = encryption;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (omemo_encryption != null && dtls_encryption != null) {
|
||||||
|
call.encryption = Encryption.OMEMO;
|
||||||
|
Xep.Jingle.ContentEncryption? video_encryption = video_encryptions.has_key(call) ? video_encryptions[call]["http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification"] : null;
|
||||||
|
omemo_encryption.peer_key = dtls_encryption.peer_key;
|
||||||
|
omemo_encryption.our_key = dtls_encryption.our_key;
|
||||||
|
encryption_updated(call, omemo_encryption, video_encryption, true);
|
||||||
|
} else if (dtls_encryption != null) {
|
||||||
|
call.encryption = Encryption.DTLS_SRTP;
|
||||||
|
Xep.Jingle.ContentEncryption? video_encryption = video_encryptions.has_key(call) ? video_encryptions[call][Xep.JingleIceUdp.DTLS_NS_URI] : null;
|
||||||
|
bool same = true;
|
||||||
|
if (video_encryption != null && dtls_encryption.peer_key.length == video_encryption.peer_key.length) {
|
||||||
|
for (int i = 0; i < dtls_encryption.peer_key.length; i++) {
|
||||||
|
if (dtls_encryption.peer_key[i] != video_encryption.peer_key[i]) { same = false; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
encryption_updated(call, dtls_encryption, video_encryption, same);
|
||||||
|
} else if (srtp_encryption != null) {
|
||||||
|
call.encryption = Encryption.SRTP;
|
||||||
|
encryption_updated(call, srtp_encryption, video_encryptions[call]["SRTP"], false);
|
||||||
|
} else {
|
||||||
|
call.encryption = Encryption.NONE;
|
||||||
|
encryption_updated(call, null, null, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void remove_call_from_datastructures(Call call) {
|
||||||
|
string? sid = sid_by_call[call.account][call];
|
||||||
|
sid_by_call[call.account].unset(call);
|
||||||
|
if (sid != null) call_by_sid[call.account].unset(sid);
|
||||||
|
|
||||||
|
sessions.unset(call);
|
||||||
|
|
||||||
|
counterpart_sends_video.unset(call);
|
||||||
|
we_should_send_video.unset(call);
|
||||||
|
we_should_send_audio.unset(call);
|
||||||
|
|
||||||
|
audio_content_parameter.unset(call);
|
||||||
|
video_content_parameter.unset(call);
|
||||||
|
audio_content.unset(call);
|
||||||
|
video_content.unset(call);
|
||||||
|
audio_encryptions.unset(call);
|
||||||
|
video_encryptions.unset(call);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_account_added(Account account) {
|
||||||
|
call_by_sid[account] = new HashMap<string, Call>();
|
||||||
|
sid_by_call[account] = new HashMap<Call, string>();
|
||||||
|
|
||||||
|
Xep.Jingle.Module jingle_module = stream_interactor.module_manager.get_module(account, Xep.Jingle.Module.IDENTITY);
|
||||||
|
jingle_module.session_initiate_received.connect((stream, session) => {
|
||||||
|
foreach (Xep.Jingle.Content content in session.contents) {
|
||||||
|
Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
|
||||||
|
if (rtp_content_parameter != null) {
|
||||||
|
on_incoming_call(account, session);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var session_info_type = stream_interactor.module_manager.get_module(account, Xep.JingleRtp.Module.IDENTITY).session_info_type;
|
||||||
|
session_info_type.mute_update_received.connect((session,mute, name) => {
|
||||||
|
if (!call_by_sid[account].has_key(session.sid)) return;
|
||||||
|
Call call = call_by_sid[account][session.sid];
|
||||||
|
|
||||||
|
foreach (Xep.Jingle.Content content in session.contents) {
|
||||||
|
if (name == null || content.content_name == name) {
|
||||||
|
Xep.JingleRtp.Parameters? rtp_content_parameter = content.content_params as Xep.JingleRtp.Parameters;
|
||||||
|
if (rtp_content_parameter != null) {
|
||||||
|
on_counterpart_mute_update(call, mute, rtp_content_parameter.media);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
session_info_type.info_received.connect((session, session_info) => {
|
||||||
|
if (!call_by_sid[account].has_key(session.sid)) return;
|
||||||
|
Call call = call_by_sid[account][session.sid];
|
||||||
|
|
||||||
|
info_received(call, session_info);
|
||||||
|
});
|
||||||
|
|
||||||
|
Xep.JingleMessageInitiation.Module mi_module = stream_interactor.module_manager.get_module(account, Xep.JingleMessageInitiation.Module.IDENTITY);
|
||||||
|
mi_module.session_proposed.connect((from, to, sid, descriptions) => {
|
||||||
|
if (!can_do_audio_calls()) {
|
||||||
|
warning("Incoming call but no call support detected. Ignoring.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool audio_requested = descriptions.any_match((description) => description.ns_uri == Xep.JingleRtp.NS_URI && description.get_attribute("media") == "audio");
|
||||||
|
bool video_requested = descriptions.any_match((description) => description.ns_uri == Xep.JingleRtp.NS_URI && description.get_attribute("media") == "video");
|
||||||
|
if (!audio_requested && !video_requested) return;
|
||||||
|
Call call = create_received_call(account, from, to, video_requested);
|
||||||
|
call_by_sid[account][sid] = call;
|
||||||
|
sid_by_call[account][call] = sid;
|
||||||
|
});
|
||||||
|
mi_module.session_accepted.connect((from, sid) => {
|
||||||
|
if (!call_by_sid[account].has_key(sid)) return;
|
||||||
|
|
||||||
|
if (from.equals_bare(account.bare_jid)) { // Carboned message from our account
|
||||||
|
// Ignore carbon from ourselves
|
||||||
|
if (from.equals(account.full_jid)) return;
|
||||||
|
|
||||||
|
Call call = call_by_sid[account][sid];
|
||||||
|
call.state = Call.State.OTHER_DEVICE_ACCEPTED;
|
||||||
|
remove_call_from_datastructures(call);
|
||||||
|
} else if (from.equals_bare(call_by_sid[account][sid].counterpart)) { // Message from our peer
|
||||||
|
// We proposed the call
|
||||||
|
if (jmi_sid.has_key(account) && jmi_sid[account] == sid) {
|
||||||
|
call_resource.begin(account, from, jmi_call[account], jmi_video[account], jmi_sid[account]);
|
||||||
|
jmi_call.unset(account);
|
||||||
|
jmi_sid.unset(account);
|
||||||
|
jmi_video.unset(account);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mi_module.session_rejected.connect((from, to, sid) => {
|
||||||
|
if (!call_by_sid[account].has_key(sid)) return;
|
||||||
|
Call call = call_by_sid[account][sid];
|
||||||
|
|
||||||
|
bool outgoing_reject = call.direction == Call.DIRECTION_OUTGOING && from.equals_bare(call.counterpart);
|
||||||
|
bool incoming_reject = call.direction == Call.DIRECTION_INCOMING && from.equals_bare(account.bare_jid);
|
||||||
|
if (!(outgoing_reject || incoming_reject)) return;
|
||||||
|
|
||||||
|
call.state = Call.State.DECLINED;
|
||||||
|
remove_call_from_datastructures(call);
|
||||||
|
call_terminated(call, null, null);
|
||||||
|
});
|
||||||
|
mi_module.session_retracted.connect((from, to, sid) => {
|
||||||
|
if (!call_by_sid[account].has_key(sid)) return;
|
||||||
|
Call call = call_by_sid[account][sid];
|
||||||
|
|
||||||
|
bool outgoing_retract = call.direction == Call.DIRECTION_OUTGOING && from.equals_bare(call.counterpart);
|
||||||
|
bool incoming_retract = call.direction == Call.DIRECTION_INCOMING && from.equals_bare(account.bare_jid);
|
||||||
|
if (!(outgoing_retract || incoming_retract)) return;
|
||||||
|
|
||||||
|
call.state = Call.State.MISSED;
|
||||||
|
remove_call_from_datastructures(call);
|
||||||
|
call_terminated(call, null, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ namespace Dino {
|
||||||
public class ConnectionManager : Object {
|
public class ConnectionManager : Object {
|
||||||
|
|
||||||
public signal void stream_opened(Account account, XmppStream stream);
|
public signal void stream_opened(Account account, XmppStream stream);
|
||||||
|
public signal void stream_attached_modules(Account account, XmppStream stream);
|
||||||
public signal void connection_state_changed(Account account, ConnectionState state);
|
public signal void connection_state_changed(Account account, ConnectionState state);
|
||||||
public signal void connection_error(Account account, ConnectionError error);
|
public signal void connection_error(Account account, ConnectionError error);
|
||||||
|
|
||||||
|
@ -169,7 +170,7 @@ public class ConnectionManager : Object {
|
||||||
public async void disconnect_account(Account account) {
|
public async void disconnect_account(Account account) {
|
||||||
if (connections.has_key(account)) {
|
if (connections.has_key(account)) {
|
||||||
make_offline(account);
|
make_offline(account);
|
||||||
connections[account].disconnect_account();
|
connections[account].disconnect_account.begin();
|
||||||
connections.unset(account);
|
connections.unset(account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -225,6 +226,7 @@ public class ConnectionManager : Object {
|
||||||
|
|
||||||
connections[account].established = new DateTime.now_utc();
|
connections[account].established = new DateTime.now_utc();
|
||||||
stream.attached_modules.connect((stream) => {
|
stream.attached_modules.connect((stream) => {
|
||||||
|
stream_attached_modules(account, stream);
|
||||||
change_connection_state(account, ConnectionState.CONNECTED);
|
change_connection_state(account, ConnectionState.CONNECTED);
|
||||||
});
|
});
|
||||||
stream.get_module(Sasl.Module.IDENTITY).received_auth_failure.connect((stream, node) => {
|
stream.get_module(Sasl.Module.IDENTITY).received_auth_failure.connect((stream, node) => {
|
||||||
|
@ -348,7 +350,9 @@ public class ConnectionManager : Object {
|
||||||
foreach (Account account in connections.keys) {
|
foreach (Account account in connections.keys) {
|
||||||
try {
|
try {
|
||||||
make_offline(account);
|
make_offline(account);
|
||||||
yield connections[account].stream.disconnect();
|
if (connections[account].stream != null) {
|
||||||
|
yield connections[account].stream.disconnect();
|
||||||
|
}
|
||||||
} catch (Error e) {
|
} catch (Error e) {
|
||||||
debug("Error disconnecting stream %p: %s", connections[account].stream, e.message);
|
debug("Error disconnecting stream %p: %s", connections[account].stream, e.message);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ public class ContentItemStore : StreamInteractionModule, Object {
|
||||||
stream_interactor.get_module(FileManager.IDENTITY).received_file.connect(insert_file_transfer);
|
stream_interactor.get_module(FileManager.IDENTITY).received_file.connect(insert_file_transfer);
|
||||||
stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect(announce_message);
|
stream_interactor.get_module(MessageProcessor.IDENTITY).message_received.connect(announce_message);
|
||||||
stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(announce_message);
|
stream_interactor.get_module(MessageProcessor.IDENTITY).message_sent.connect(announce_message);
|
||||||
|
stream_interactor.get_module(Calls.IDENTITY).call_incoming.connect(insert_call);
|
||||||
|
stream_interactor.get_module(Calls.IDENTITY).call_outgoing.connect(insert_call);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init(Conversation conversation, ContentItemCollection item_collection) {
|
public void init(Conversation conversation, ContentItemCollection item_collection) {
|
||||||
|
@ -51,7 +53,6 @@ public class ContentItemStore : StreamInteractionModule, Object {
|
||||||
Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(foreign_id, conversation);
|
Message? message = stream_interactor.get_module(MessageStorage.IDENTITY).get_message_by_id(foreign_id, conversation);
|
||||||
if (message != null) {
|
if (message != null) {
|
||||||
var message_item = new MessageItem(message, conversation, row[db.content_item.id]);
|
var message_item = new MessageItem(message, conversation, row[db.content_item.id]);
|
||||||
message_item.time = time;
|
|
||||||
items.add(message_item);
|
items.add(message_item);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -66,6 +67,13 @@ public class ContentItemStore : StreamInteractionModule, Object {
|
||||||
items.add(file_item);
|
items.add(file_item);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 3:
|
||||||
|
Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(foreign_id);
|
||||||
|
if (call != null) {
|
||||||
|
var call_item = new CallItem(call, conversation, row[db.content_item.id]);
|
||||||
|
items.add(call_item);
|
||||||
|
}
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,6 +185,15 @@ public class ContentItemStore : StreamInteractionModule, Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void insert_call(Call call, Conversation conversation) {
|
||||||
|
CallItem item = new CallItem(call, conversation, -1);
|
||||||
|
item.id = db.add_content_item(conversation, call.time, call.local_time, 3, call.id, false);
|
||||||
|
if (collection_conversations.has_key(conversation)) {
|
||||||
|
collection_conversations.get(conversation).insert_item(item);
|
||||||
|
}
|
||||||
|
new_item(item, conversation);
|
||||||
|
}
|
||||||
|
|
||||||
public bool get_item_hide(ContentItem content_item) {
|
public bool get_item_hide(ContentItem content_item) {
|
||||||
return db.content_item.row_with(db.content_item.id, content_item.id)[db.content_item.hide, false];
|
return db.content_item.row_with(db.content_item.id, content_item.id)[db.content_item.hide, false];
|
||||||
}
|
}
|
||||||
|
@ -296,4 +313,20 @@ public class FileItem : ContentItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class CallItem : ContentItem {
|
||||||
|
public const string TYPE = "call";
|
||||||
|
|
||||||
|
public Call call;
|
||||||
|
public Conversation conversation;
|
||||||
|
|
||||||
|
public CallItem(Call call, Conversation conversation, int id) {
|
||||||
|
base(id, TYPE, call.from, call.time, call.encryption, Message.Marked.NONE);
|
||||||
|
|
||||||
|
this.call = call;
|
||||||
|
this.conversation = conversation;
|
||||||
|
|
||||||
|
call.bind_property("encryption", this, "encryption");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ using Dino.Entities;
|
||||||
namespace Dino {
|
namespace Dino {
|
||||||
|
|
||||||
public class Database : Qlite.Database {
|
public class Database : Qlite.Database {
|
||||||
private const int VERSION = 19;
|
private const int VERSION = 21;
|
||||||
|
|
||||||
public class AccountTable : Table {
|
public class AccountTable : Table {
|
||||||
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||||
|
@ -155,6 +155,25 @@ public class Database : Qlite.Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class CallTable : Table {
|
||||||
|
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||||
|
public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
|
||||||
|
public Column<int> counterpart_id = new Column.Integer("counterpart_id") { not_null = true };
|
||||||
|
public Column<string> counterpart_resource = new Column.Text("counterpart_resource");
|
||||||
|
public Column<string> our_resource = new Column.Text("our_resource");
|
||||||
|
public Column<bool> direction = new Column.BoolInt("direction") { not_null = true };
|
||||||
|
public Column<long> time = new Column.Long("time") { not_null = true };
|
||||||
|
public Column<long> local_time = new Column.Long("local_time") { not_null = true };
|
||||||
|
public Column<long> end_time = new Column.Long("end_time");
|
||||||
|
public Column<int> encryption = new Column.Integer("encryption") { min_version=21 };
|
||||||
|
public Column<int> state = new Column.Integer("state");
|
||||||
|
|
||||||
|
internal CallTable(Database db) {
|
||||||
|
base(db, "call");
|
||||||
|
init({id, account_id, counterpart_id, counterpart_resource, our_resource, direction, time, local_time, end_time, encryption, state});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public class ConversationTable : Table {
|
public class ConversationTable : Table {
|
||||||
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
public Column<int> id = new Column.Integer("id") { primary_key = true, auto_increment = true };
|
||||||
public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
|
public Column<int> account_id = new Column.Integer("account_id") { not_null = true };
|
||||||
|
@ -275,6 +294,7 @@ public class Database : Qlite.Database {
|
||||||
public MessageCorrectionTable message_correction { get; private set; }
|
public MessageCorrectionTable message_correction { get; private set; }
|
||||||
public RealJidTable real_jid { get; private set; }
|
public RealJidTable real_jid { get; private set; }
|
||||||
public FileTransferTable file_transfer { get; private set; }
|
public FileTransferTable file_transfer { get; private set; }
|
||||||
|
public CallTable call { get; private set; }
|
||||||
public ConversationTable conversation { get; private set; }
|
public ConversationTable conversation { get; private set; }
|
||||||
public AvatarTable avatar { get; private set; }
|
public AvatarTable avatar { get; private set; }
|
||||||
public EntityIdentityTable entity_identity { get; private set; }
|
public EntityIdentityTable entity_identity { get; private set; }
|
||||||
|
@ -298,6 +318,7 @@ public class Database : Qlite.Database {
|
||||||
message_correction = new MessageCorrectionTable(this);
|
message_correction = new MessageCorrectionTable(this);
|
||||||
real_jid = new RealJidTable(this);
|
real_jid = new RealJidTable(this);
|
||||||
file_transfer = new FileTransferTable(this);
|
file_transfer = new FileTransferTable(this);
|
||||||
|
call = new CallTable(this);
|
||||||
conversation = new ConversationTable(this);
|
conversation = new ConversationTable(this);
|
||||||
avatar = new AvatarTable(this);
|
avatar = new AvatarTable(this);
|
||||||
entity_identity = new EntityIdentityTable(this);
|
entity_identity = new EntityIdentityTable(this);
|
||||||
|
@ -306,7 +327,7 @@ public class Database : Qlite.Database {
|
||||||
mam_catchup = new MamCatchupTable(this);
|
mam_catchup = new MamCatchupTable(this);
|
||||||
settings = new SettingsTable(this);
|
settings = new SettingsTable(this);
|
||||||
conversation_settings = new ConversationSettingsTable(this);
|
conversation_settings = new ConversationSettingsTable(this);
|
||||||
init({ account, jid, entity, content_item, message, message_correction, real_jid, file_transfer, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, settings, conversation_settings });
|
init({ account, jid, entity, content_item, message, message_correction, real_jid, file_transfer, call, conversation, avatar, entity_identity, entity_feature, roster, mam_catchup, settings, conversation_settings });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
exec("PRAGMA journal_mode = WAL");
|
exec("PRAGMA journal_mode = WAL");
|
||||||
|
|
|
@ -40,6 +40,9 @@ public class EntityInfo : StreamInteractionModule, Object {
|
||||||
entity_caps_hashes[account.bare_jid.domain_jid] = hash;
|
entity_caps_hashes[account.bare_jid.domain_jid] = hash;
|
||||||
});
|
});
|
||||||
stream_interactor.module_manager.initialize_account_modules.connect(initialize_modules);
|
stream_interactor.module_manager.initialize_account_modules.connect(initialize_modules);
|
||||||
|
|
||||||
|
remove_old_entities();
|
||||||
|
Timeout.add_seconds(60 * 60, () => { remove_old_entities(); return true; });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Gee.Set<Identity>? get_identities(Account account, Jid jid) {
|
public async Gee.Set<Identity>? get_identities(Account account, Jid jid) {
|
||||||
|
@ -94,26 +97,30 @@ public class EntityInfo : StreamInteractionModule, Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void on_received_available_presence(Account account, Presence.Stanza presence) {
|
private void on_received_available_presence(Account account, Presence.Stanza presence) {
|
||||||
bool is_gc = stream_interactor.get_module(MucManager.IDENTITY).is_groupchat(presence.from.bare_jid, account);
|
bool is_gc = stream_interactor.get_module(MucManager.IDENTITY).might_be_groupchat(presence.from.bare_jid, account);
|
||||||
if (is_gc) return;
|
if (is_gc) return;
|
||||||
|
|
||||||
string? caps_hash = EntityCapabilities.get_caps_hash(presence);
|
string? caps_hash = EntityCapabilities.get_caps_hash(presence);
|
||||||
if (caps_hash == null) return;
|
if (caps_hash == null) return;
|
||||||
|
|
||||||
/* TODO check might_be_groupchat before storing
|
|
||||||
db.entity.upsert()
|
db.entity.upsert()
|
||||||
.value(db.entity.account_id, account.id, true)
|
.value(db.entity.account_id, account.id, true)
|
||||||
.value(db.entity.jid_id, db.get_jid_id(presence.from), true)
|
.value(db.entity.jid_id, db.get_jid_id(presence.from), true)
|
||||||
.value(db.entity.resource, presence.from.resourcepart, true)
|
.value(db.entity.resource, presence.from.resourcepart, true)
|
||||||
.value(db.entity.last_seen, (long)(new DateTime.now_local()).to_unix())
|
.value(db.entity.last_seen, (long)(new DateTime.now_local()).to_unix())
|
||||||
.value(db.entity.caps_hash, caps_hash)
|
.value(db.entity.caps_hash, caps_hash)
|
||||||
.perform();*/
|
.perform();
|
||||||
|
|
||||||
if (caps_hash != null) {
|
if (caps_hash != null) {
|
||||||
entity_caps_hashes[presence.from] = caps_hash;
|
entity_caps_hashes[presence.from] = caps_hash;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void remove_old_entities() {
|
||||||
|
long timestamp = (long)(new DateTime.now_local().add_days(-14)).to_unix();
|
||||||
|
db.entity.delete().with(db.entity.last_seen, "<", timestamp).perform();
|
||||||
|
}
|
||||||
|
|
||||||
private void store_features(string entity, Gee.List<string> features) {
|
private void store_features(string entity, Gee.List<string> features) {
|
||||||
if (entity_features.has_key(entity)) return;
|
if (entity_features.has_key(entity)) return;
|
||||||
|
|
||||||
|
|
|
@ -103,7 +103,7 @@ public class JingleFileProvider : FileProvider, Object {
|
||||||
throw new FileReceiveError.DOWNLOAD_FAILED("Transfer data not available anymore");
|
throw new FileReceiveError.DOWNLOAD_FAILED("Transfer data not available anymore");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
jingle_file_transfer.accept(stream);
|
yield jingle_file_transfer.accept(stream);
|
||||||
} catch (IOError e) {
|
} catch (IOError e) {
|
||||||
throw new FileReceiveError.DOWNLOAD_FAILED("Establishing connection did not work");
|
throw new FileReceiveError.DOWNLOAD_FAILED("Establishing connection did not work");
|
||||||
}
|
}
|
||||||
|
@ -202,8 +202,11 @@ public class JingleFileSender : FileSender, Object {
|
||||||
if (stream == null) throw new FileSendError.UPLOAD_FAILED("No stream available");
|
if (stream == null) throw new FileSendError.UPLOAD_FAILED("No stream available");
|
||||||
JingleFileEncryptionHelper? helper = JingleFileHelperRegistry.instance.get_encryption_helper(file_transfer.encryption);
|
JingleFileEncryptionHelper? helper = JingleFileHelperRegistry.instance.get_encryption_helper(file_transfer.encryption);
|
||||||
bool must_encrypt = helper != null && yield helper.can_encrypt(conversation, file_transfer);
|
bool must_encrypt = helper != null && yield helper.can_encrypt(conversation, file_transfer);
|
||||||
|
// TODO(hrxi): Prioritization of transports (and resources?).
|
||||||
foreach (Jid full_jid in stream.get_flag(Presence.Flag.IDENTITY).get_resources(conversation.counterpart)) {
|
foreach (Jid full_jid in stream.get_flag(Presence.Flag.IDENTITY).get_resources(conversation.counterpart)) {
|
||||||
// TODO(hrxi): Prioritization of transports (and resources?).
|
if (full_jid.equals(stream.get_flag(Bind.Flag.IDENTITY).my_jid)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (!yield stream.get_module(Xep.JingleFileTransfer.Module.IDENTITY).is_available(stream, full_jid)) {
|
if (!yield stream.get_module(Xep.JingleFileTransfer.Module.IDENTITY).is_available(stream, full_jid)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
|
@ -331,7 +331,7 @@ public class MessageProcessor : StreamInteractionModule, Object {
|
||||||
if (conversation == null) return;
|
if (conversation == null) return;
|
||||||
|
|
||||||
// MAM state database update
|
// MAM state database update
|
||||||
Xep.MessageArchiveManagement.MessageFlag mam_flag = Xep.MessageArchiveManagement.MessageFlag.get_flag(message_stanza);
|
Xep.MessageArchiveManagement.MessageFlag? mam_flag = Xep.MessageArchiveManagement.MessageFlag.get_flag(message_stanza);
|
||||||
if (mam_flag == null) {
|
if (mam_flag == null) {
|
||||||
if (current_catchup_id.has_key(account)) {
|
if (current_catchup_id.has_key(account)) {
|
||||||
string? stanza_id = UniqueStableStanzaIDs.get_stanza_id(message_stanza, account.bare_jid);
|
string? stanza_id = UniqueStableStanzaIDs.get_stanza_id(message_stanza, account.bare_jid);
|
||||||
|
|
|
@ -79,6 +79,7 @@ public class ModuleManager {
|
||||||
module_map[account].add(new Xep.Jet.Module());
|
module_map[account].add(new Xep.Jet.Module());
|
||||||
module_map[account].add(new Xep.LastMessageCorrection.Module());
|
module_map[account].add(new Xep.LastMessageCorrection.Module());
|
||||||
module_map[account].add(new Xep.DirectMucInvitations.Module());
|
module_map[account].add(new Xep.DirectMucInvitations.Module());
|
||||||
|
module_map[account].add(new Xep.JingleMessageInitiation.Module());
|
||||||
initialize_account_modules(account, module_map[account]);
|
initialize_account_modules(account, module_map[account]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,12 +24,15 @@ public class NotificationEvents : StreamInteractionModule, Object {
|
||||||
|
|
||||||
stream_interactor.get_module(ContentItemStore.IDENTITY).new_item.connect(on_content_item_received);
|
stream_interactor.get_module(ContentItemStore.IDENTITY).new_item.connect(on_content_item_received);
|
||||||
stream_interactor.get_module(PresenceManager.IDENTITY).received_subscription_request.connect(on_received_subscription_request);
|
stream_interactor.get_module(PresenceManager.IDENTITY).received_subscription_request.connect(on_received_subscription_request);
|
||||||
|
|
||||||
stream_interactor.get_module(MucManager.IDENTITY).invite_received.connect(on_invite_received);
|
stream_interactor.get_module(MucManager.IDENTITY).invite_received.connect(on_invite_received);
|
||||||
stream_interactor.get_module(MucManager.IDENTITY).voice_request_received.connect((account, room_jid, from_jid, nick) => {
|
stream_interactor.get_module(MucManager.IDENTITY).voice_request_received.connect((account, room_jid, from_jid, nick) => {
|
||||||
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(room_jid, account, Conversation.Type.GROUPCHAT);
|
Conversation? conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(room_jid, account, Conversation.Type.GROUPCHAT);
|
||||||
if (conversation == null) return;
|
if (conversation == null) return;
|
||||||
notifier.notify_voice_request.begin(conversation, from_jid);
|
notifier.notify_voice_request.begin(conversation, from_jid);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
stream_interactor.get_module(Calls.IDENTITY).call_incoming.connect(on_call_incoming);
|
||||||
stream_interactor.connection_manager.connection_error.connect((account, error) => notifier.notify_connection_error.begin(account, error));
|
stream_interactor.connection_manager.connection_error.connect((account, error) => notifier.notify_connection_error.begin(account, error));
|
||||||
stream_interactor.get_module(ChatInteraction.IDENTITY).focused_in.connect((conversation) => {
|
stream_interactor.get_module(ChatInteraction.IDENTITY).focused_in.connect((conversation) => {
|
||||||
notifier.retract_content_item_notifications.begin();
|
notifier.retract_content_item_notifications.begin();
|
||||||
|
@ -91,6 +94,9 @@ public class NotificationEvents : StreamInteractionModule, Object {
|
||||||
notifier.notify_file.begin(file_transfer, conversation, is_image, conversation_display_name, participant_display_name);
|
notifier.notify_file.begin(file_transfer, conversation, is_image, conversation_display_name, participant_display_name);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case CallItem.TYPE:
|
||||||
|
// handled in `on_call_incoming`
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,6 +107,17 @@ public class NotificationEvents : StreamInteractionModule, Object {
|
||||||
notifier.notify_subscription_request.begin(conversation);
|
notifier.notify_subscription_request.begin(conversation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void on_call_incoming(Call call, Conversation conversation, bool video) {
|
||||||
|
string conversation_display_name = get_conversation_display_name(stream_interactor, conversation, null);
|
||||||
|
|
||||||
|
notifier.notify_call.begin(call, conversation, video, conversation_display_name);
|
||||||
|
call.notify["state"].connect(() => {
|
||||||
|
if (call.state != Call.State.RINGING) {
|
||||||
|
notifier.retract_call_notification.begin(call, conversation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void on_invite_received(Account account, Jid room_jid, Jid from_jid, string? password, string? reason) {
|
private void on_invite_received(Account account, Jid room_jid, Jid from_jid, string? password, string? reason) {
|
||||||
string inviter_display_name;
|
string inviter_display_name;
|
||||||
if (room_jid.equals_bare(from_jid)) {
|
if (room_jid.equals_bare(from_jid)) {
|
||||||
|
@ -119,6 +136,8 @@ public interface NotificationProvider : Object {
|
||||||
|
|
||||||
public abstract async void notify_message(Message message, Conversation conversation, string conversation_display_name, string? participant_display_name);
|
public abstract async void notify_message(Message message, Conversation conversation, string conversation_display_name, string? participant_display_name);
|
||||||
public abstract async void notify_file(FileTransfer file_transfer, Conversation conversation, bool is_image, string conversation_display_name, string? participant_display_name);
|
public abstract async void notify_file(FileTransfer file_transfer, Conversation conversation, bool is_image, string conversation_display_name, string? participant_display_name);
|
||||||
|
public abstract async void notify_call(Call call, Conversation conversation, bool video, string conversation_display_name);
|
||||||
|
public abstract async void retract_call_notification(Call call, Conversation conversation);
|
||||||
public abstract async void notify_subscription_request(Conversation conversation);
|
public abstract async void notify_subscription_request(Conversation conversation);
|
||||||
public abstract async void notify_connection_error(Account account, ConnectionManager.ConnectionError error);
|
public abstract async void notify_connection_error(Account account, ConnectionManager.ConnectionError error);
|
||||||
public abstract async void notify_muc_invite(Account account, Jid room_jid, Jid from_jid, string inviter_display_name);
|
public abstract async void notify_muc_invite(Account account, Jid room_jid, Jid from_jid, string inviter_display_name);
|
||||||
|
|
|
@ -11,7 +11,7 @@ public class StreamInteractor : Object {
|
||||||
public signal void account_removed(Account account);
|
public signal void account_removed(Account account);
|
||||||
public signal void stream_resumed(Account account, XmppStream stream);
|
public signal void stream_resumed(Account account, XmppStream stream);
|
||||||
public signal void stream_negotiated(Account account, XmppStream stream);
|
public signal void stream_negotiated(Account account, XmppStream stream);
|
||||||
public signal void attached_modules(Account account, XmppStream stream);
|
public signal void stream_attached_modules(Account account, XmppStream stream);
|
||||||
|
|
||||||
public ModuleManager module_manager;
|
public ModuleManager module_manager;
|
||||||
public ConnectionManager connection_manager;
|
public ConnectionManager connection_manager;
|
||||||
|
@ -22,6 +22,9 @@ public class StreamInteractor : Object {
|
||||||
connection_manager = new ConnectionManager(module_manager);
|
connection_manager = new ConnectionManager(module_manager);
|
||||||
|
|
||||||
connection_manager.stream_opened.connect(on_stream_opened);
|
connection_manager.stream_opened.connect(on_stream_opened);
|
||||||
|
connection_manager.stream_attached_modules.connect((account, stream) => {
|
||||||
|
stream_attached_modules(account, stream);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void connect_account(Account account) {
|
public void connect_account(Account account) {
|
||||||
|
|
|
@ -36,7 +36,7 @@ namespace Dino {
|
||||||
return participant.bare_jid.to_string();
|
return participant.bare_jid.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? get_real_display_name(StreamInteractor stream_interactor, Account account, Jid jid, string? self_word = null) {
|
public static string? get_real_display_name(StreamInteractor stream_interactor, Account account, Jid jid, string? self_word = null) {
|
||||||
if (jid.equals_bare(account.bare_jid)) {
|
if (jid.equals_bare(account.bare_jid)) {
|
||||||
if (self_word != null || account.alias == null || account.alias.length == 0) {
|
if (self_word != null || account.alias == null || account.alias.length == 0) {
|
||||||
return self_word;
|
return self_word;
|
||||||
|
@ -50,7 +50,7 @@ namespace Dino {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string get_groupchat_display_name(StreamInteractor stream_interactor, Account account, Jid jid) {
|
public static string get_groupchat_display_name(StreamInteractor stream_interactor, Account account, Jid jid) {
|
||||||
MucManager muc_manager = stream_interactor.get_module(MucManager.IDENTITY);
|
MucManager muc_manager = stream_interactor.get_module(MucManager.IDENTITY);
|
||||||
string? room_name = muc_manager.get_room_name(account, jid);
|
string? room_name = muc_manager.get_room_name(account, jid);
|
||||||
if (room_name != null && room_name != jid.localpart) {
|
if (room_name != null && room_name != jid.localpart) {
|
||||||
|
@ -72,7 +72,7 @@ namespace Dino {
|
||||||
return jid.to_string();
|
return jid.to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string get_occupant_display_name(StreamInteractor stream_interactor, Conversation conversation, Jid jid, string? self_word = null, bool muc_real_name = false) {
|
public static string get_occupant_display_name(StreamInteractor stream_interactor, Conversation conversation, Jid jid, string? self_word = null, bool muc_real_name = false) {
|
||||||
if (muc_real_name) {
|
if (muc_real_name) {
|
||||||
MucManager muc_manager = stream_interactor.get_module(MucManager.IDENTITY);
|
MucManager muc_manager = stream_interactor.get_module(MucManager.IDENTITY);
|
||||||
if (muc_manager.is_private_room(conversation.account, jid.bare_jid)) {
|
if (muc_manager.is_private_room(conversation.account, jid.bare_jid)) {
|
||||||
|
|
|
@ -5,6 +5,8 @@ gettext_compile(${GETTEXT_PACKAGE} SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/po TAR
|
||||||
|
|
||||||
find_packages(MAIN_PACKAGES REQUIRED
|
find_packages(MAIN_PACKAGES REQUIRED
|
||||||
Gee
|
Gee
|
||||||
|
Gst
|
||||||
|
GstVideo
|
||||||
GLib
|
GLib
|
||||||
GModule
|
GModule
|
||||||
GObject
|
GObject
|
||||||
|
@ -21,7 +23,14 @@ set(RESOURCE_LIST
|
||||||
icons/dino-emoticon-symbolic.svg
|
icons/dino-emoticon-symbolic.svg
|
||||||
icons/dino-qr-code-symbolic.svg
|
icons/dino-qr-code-symbolic.svg
|
||||||
icons/dino-security-high-symbolic.svg
|
icons/dino-security-high-symbolic.svg
|
||||||
|
icons/dino-microphone-off-symbolic.svg
|
||||||
|
icons/dino-microphone-symbolic.svg
|
||||||
icons/dino-party-popper-symbolic.svg
|
icons/dino-party-popper-symbolic.svg
|
||||||
|
icons/dino-phone-hangup-symbolic.svg
|
||||||
|
icons/dino-phone-in-talk-symbolic.svg
|
||||||
|
icons/dino-phone-missed-symbolic.svg
|
||||||
|
icons/dino-phone-ring-symbolic.svg
|
||||||
|
icons/dino-phone-symbolic.svg
|
||||||
icons/dino-status-away.svg
|
icons/dino-status-away.svg
|
||||||
icons/dino-status-chat.svg
|
icons/dino-status-chat.svg
|
||||||
icons/dino-status-dnd.svg
|
icons/dino-status-dnd.svg
|
||||||
|
@ -29,6 +38,8 @@ set(RESOURCE_LIST
|
||||||
icons/im.dino.Dino.svg
|
icons/im.dino.Dino.svg
|
||||||
icons/im.dino.Dino-symbolic.svg
|
icons/im.dino.Dino-symbolic.svg
|
||||||
icons/dino-tick-symbolic.svg
|
icons/dino-tick-symbolic.svg
|
||||||
|
icons/dino-video-off-symbolic.svg
|
||||||
|
icons/dino-video-symbolic.svg
|
||||||
|
|
||||||
icons/dino-device-desktop-symbolic.svg
|
icons/dino-device-desktop-symbolic.svg
|
||||||
icons/dino-device-phone-symbolic.svg
|
icons/dino-device-phone-symbolic.svg
|
||||||
|
@ -46,6 +57,8 @@ set(RESOURCE_LIST
|
||||||
add_conversation/conference_details_fragment.ui
|
add_conversation/conference_details_fragment.ui
|
||||||
add_conversation/list_row.ui
|
add_conversation/list_row.ui
|
||||||
add_conversation/select_jid_fragment.ui
|
add_conversation/select_jid_fragment.ui
|
||||||
|
|
||||||
|
call_widget.ui
|
||||||
chat_input.ui
|
chat_input.ui
|
||||||
contact_details_dialog.ui
|
contact_details_dialog.ui
|
||||||
conversation_list_titlebar.ui
|
conversation_list_titlebar.ui
|
||||||
|
@ -124,6 +137,13 @@ SOURCES
|
||||||
src/ui/add_conversation/select_contact_dialog.vala
|
src/ui/add_conversation/select_contact_dialog.vala
|
||||||
src/ui/add_conversation/select_jid_fragment.vala
|
src/ui/add_conversation/select_jid_fragment.vala
|
||||||
|
|
||||||
|
src/ui/call_window/audio_settings_popover.vala
|
||||||
|
src/ui/call_window/call_bottom_bar.vala
|
||||||
|
src/ui/call_window/call_encryption_button.vala
|
||||||
|
src/ui/call_window/call_window.vala
|
||||||
|
src/ui/call_window/call_window_controller.vala
|
||||||
|
src/ui/call_window/video_settings_popover.vala
|
||||||
|
|
||||||
src/ui/chat_input/chat_input_controller.vala
|
src/ui/chat_input/chat_input_controller.vala
|
||||||
src/ui/chat_input/chat_text_view.vala
|
src/ui/chat_input/chat_text_view.vala
|
||||||
src/ui/chat_input/edit_history.vala
|
src/ui/chat_input/edit_history.vala
|
||||||
|
@ -142,6 +162,7 @@ SOURCES
|
||||||
src/ui/conversation_selector/conversation_selector_row.vala
|
src/ui/conversation_selector/conversation_selector_row.vala
|
||||||
src/ui/conversation_selector/conversation_selector.vala
|
src/ui/conversation_selector/conversation_selector.vala
|
||||||
|
|
||||||
|
src/ui/conversation_content_view/call_widget.vala
|
||||||
src/ui/conversation_content_view/chat_state_populator.vala
|
src/ui/conversation_content_view/chat_state_populator.vala
|
||||||
src/ui/conversation_content_view/content_populator.vala
|
src/ui/conversation_content_view/content_populator.vala
|
||||||
src/ui/conversation_content_view/conversation_item_skeleton.vala
|
src/ui/conversation_content_view/conversation_item_skeleton.vala
|
||||||
|
@ -153,6 +174,7 @@ SOURCES
|
||||||
src/ui/conversation_content_view/message_widget.vala
|
src/ui/conversation_content_view/message_widget.vala
|
||||||
src/ui/conversation_content_view/subscription_notification.vala
|
src/ui/conversation_content_view/subscription_notification.vala
|
||||||
|
|
||||||
|
src/ui/conversation_titlebar/call_entry.vala
|
||||||
src/ui/conversation_titlebar/menu_entry.vala
|
src/ui/conversation_titlebar/menu_entry.vala
|
||||||
src/ui/conversation_titlebar/occupants_entry.vala
|
src/ui/conversation_titlebar/occupants_entry.vala
|
||||||
src/ui/conversation_titlebar/search_entry.vala
|
src/ui/conversation_titlebar/search_entry.vala
|
||||||
|
|
111
main/data/call_widget.ui
Normal file
|
@ -0,0 +1,111 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<interface>
|
||||||
|
<template class="DinoUiCallWidget">
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<style>
|
||||||
|
<class name="call-box-outer"/>
|
||||||
|
</style>
|
||||||
|
<child>
|
||||||
|
<object class="DinoUiSizingBin">
|
||||||
|
<property name="target-width">350</property>
|
||||||
|
<property name="max-width">350</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="halign">fill</property>
|
||||||
|
<property name="hexpand">true</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="spacing">10</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<style>
|
||||||
|
<class name="call-box"/>
|
||||||
|
</style>
|
||||||
|
<child>
|
||||||
|
<object class="GtkImage" id="image">
|
||||||
|
<property name="icon-size">5</property>
|
||||||
|
<property name="opacity">0.7</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="title_label">
|
||||||
|
<property name="ellipsize">middle</property>
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="yalign">0</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="subtitle_label">
|
||||||
|
<property name="xalign">0</property>
|
||||||
|
<property name="yalign">1</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<attributes>
|
||||||
|
<attribute name="scale" value="0.8"/>
|
||||||
|
</attributes>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkRevealer" id="incoming_call_revealer">
|
||||||
|
<property name="transition-type">slide-down</property>
|
||||||
|
<property name="transition-duration">200</property>
|
||||||
|
<property name="reveal-child">True</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<style>
|
||||||
|
<class name="incoming-call-box"/>
|
||||||
|
</style>
|
||||||
|
<child>
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="halign">end</property>
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="spacing">5</property>
|
||||||
|
<property name="margin">10</property>
|
||||||
|
<property name="hexpand">True</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="reject_call_button">
|
||||||
|
<property name="label" translatable="yes">Reject</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<style>
|
||||||
|
<class name="destructive-action"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
|
<object class="GtkButton" id="accept_call_button">
|
||||||
|
<property name="label" translatable="yes">Accept</property>
|
||||||
|
<property name="visible">True</property>
|
||||||
|
<style>
|
||||||
|
<class name="suggested-action"/>
|
||||||
|
</style>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</template>
|
||||||
|
</interface>
|
1
main/data/icons/dino-microphone-off-symbolic.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M19,11C19,12.19 18.66,13.3 18.1,14.28L16.87,13.05C17.14,12.43 17.3,11.74 17.3,11H19M15,11.16L9,5.18V5A3,3 0 0,1 12,2A3,3 0 0,1 15,5V11L15,11.16M4.27,3L21,19.73L19.73,21L15.54,16.81C14.77,17.27 13.91,17.58 13,17.72V21H11V17.72C7.72,17.23 5,14.41 5,11H6.7C6.7,14 9.24,16.1 12,16.1C12.81,16.1 13.6,15.91 14.31,15.58L12.65,13.92L12,14A3,3 0 0,1 9,11V10.28L3,4.27L4.27,3Z" /></svg>
|
After Width: | Height: | Size: 661 B |
1
main/data/icons/dino-microphone-symbolic.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z" /></svg>
|
After Width: | Height: | Size: 476 B |
1
main/data/icons/dino-phone-hangup-symbolic.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M12,9C10.4,9 8.85,9.25 7.4,9.72V12.82C7.4,13.22 7.17,13.56 6.84,13.72C5.86,14.21 4.97,14.84 4.17,15.57C4,15.75 3.75,15.86 3.5,15.86C3.2,15.86 2.95,15.74 2.77,15.56L0.29,13.08C0.11,12.9 0,12.65 0,12.38C0,12.1 0.11,11.85 0.29,11.67C3.34,8.77 7.46,7 12,7C16.54,7 20.66,8.77 23.71,11.67C23.89,11.85 24,12.1 24,12.38C24,12.65 23.89,12.9 23.71,13.08L21.23,15.56C21.05,15.74 20.8,15.86 20.5,15.86C20.25,15.86 20,15.75 19.82,15.57C19.03,14.84 18.14,14.21 17.16,13.72C16.83,13.56 16.6,13.22 16.6,12.82V9.72C15.15,9.25 13.6,9 12,9Z" /></svg>
|
After Width: | Height: | Size: 816 B |
1
main/data/icons/dino-phone-in-talk-symbolic.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M15,12H17A5,5 0 0,0 12,7V9A3,3 0 0,1 15,12M19,12H21C21,7 16.97,3 12,3V5C15.86,5 19,8.13 19,12M20,15.5C18.75,15.5 17.55,15.3 16.43,14.93C16.08,14.82 15.69,14.9 15.41,15.18L13.21,17.38C10.38,15.94 8.06,13.62 6.62,10.79L8.82,8.59C9.1,8.31 9.18,7.92 9.07,7.57C8.7,6.45 8.5,5.25 8.5,4A1,1 0 0,0 7.5,3H4A1,1 0 0,0 3,4A17,17 0 0,0 20,21A1,1 0 0,0 21,20V16.5A1,1 0 0,0 20,15.5Z" /></svg>
|
After Width: | Height: | Size: 664 B |
1
main/data/icons/dino-phone-missed-symbolic.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M23.71,16.67C20.66,13.77 16.54,12 12,12C7.46,12 3.34,13.77 0.29,16.67C0.11,16.85 0,17.1 0,17.38C0,17.65 0.11,17.9 0.29,18.08L2.77,20.56C2.95,20.74 3.2,20.86 3.5,20.86C3.75,20.86 4,20.75 4.18,20.57C4.97,19.83 5.86,19.21 6.84,18.72C7.17,18.56 7.4,18.22 7.4,17.82V14.72C8.85,14.25 10.39,14 12,14C13.6,14 15.15,14.25 16.6,14.72V17.82C16.6,18.22 16.83,18.56 17.16,18.72C18.14,19.21 19.03,19.83 19.82,20.57C20,20.75 20.25,20.86 20.5,20.86C20.8,20.86 21.05,20.74 21.23,20.56L23.71,18.08C23.89,17.9 24,17.65 24,17.38C24,17.1 23.89,16.85 23.71,16.67M6.5,5.5L12,11L19,4L18,3L12,9L7.5,4.5H11V3H5V9H6.5V5.5Z" /></svg>
|
After Width: | Height: | Size: 890 B |
1
main/data/icons/dino-phone-ring-symbolic.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M23.71 16.67C20.66 13.78 16.54 12 12 12S3.34 13.78.29 16.67c-.18.18-.29.43-.29.71 0 .28.11.53.29.71l2.48 2.48c.18.18.43.29.71.29.27 0 .52-.11.7-.28.79-.74 1.69-1.36 2.66-1.85.33-.16.56-.5.56-.9v-3.1c1.45-.48 3-.73 4.6-.73s3.15.25 4.6.72v3.1c0 .39.23.74.56.9.98.49 1.87 1.12 2.66 1.85.18.18.43.28.7.28.28 0 .53-.11.71-.29l2.48-2.48c.18-.18.29-.43.29-.71a.99.99 0 0 0-.29-.7zM21.16 6.26l-1.41-1.41-3.56 3.55 1.41 1.41s3.45-3.52 3.56-3.55zM13 2h-2v5h2V2zM6.4 9.81L7.81 8.4 4.26 4.84 2.84 6.26c.11.03 3.56 3.55 3.56 3.55z" /></svg>
|
After Width: | Height: | Size: 812 B |
1
main/data/icons/dino-phone-symbolic.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M6.62,10.79C8.06,13.62 10.38,15.94 13.21,17.38L15.41,15.18C15.69,14.9 16.08,14.82 16.43,14.93C17.55,15.3 18.75,15.5 20,15.5A1,1 0 0,1 21,16.5V20A1,1 0 0,1 20,21A17,17 0 0,1 3,4A1,1 0 0,1 4,3H7.5A1,1 0 0,1 8.5,4C8.5,5.25 8.7,6.45 9.07,7.57C9.18,7.92 9.1,8.31 8.82,8.59L6.62,10.79Z" /></svg>
|
After Width: | Height: | Size: 574 B |
1
main/data/icons/dino-video-off-symbolic.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M3.27,2L2,3.27L4.73,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16C16.2,18 16.39,17.92 16.54,17.82L19.73,21L21,19.73M21,6.5L17,10.5V7A1,1 0 0,0 16,6H9.82L21,17.18V6.5Z" /></svg>
|
After Width: | Height: | Size: 454 B |
1
main/data/icons/dino-video-symbolic.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M17,10.5V7A1,1 0 0,0 16,6H4A1,1 0 0,0 3,7V17A1,1 0 0,0 4,18H16A1,1 0 0,0 17,17V13.5L21,17.5V6.5L17,10.5Z" /></svg>
|
After Width: | Height: | Size: 399 B |
|
@ -86,16 +86,26 @@ window.dino-main .dino-conversation .message-box.edit-mode:hover {
|
||||||
background: alpha(@theme_selected_bg_color, 0.12);
|
background: alpha(@theme_selected_bg_color, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.dino-main .file-box-outer {
|
window.dino-main .file-box-outer,
|
||||||
|
window.dino-main .call-box-outer {
|
||||||
background: @theme_base_color;
|
background: @theme_base_color;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
border: 1px solid alpha(@theme_fg_color, 0.1);
|
border: 1px solid alpha(@theme_fg_color, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
window.dino-main .file-box {
|
window.dino-main .file-box,
|
||||||
|
window.dino-main .call-box {
|
||||||
margin: 12px 16px 12px 12px;
|
margin: 12px 16px 12px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.dino-main .call-box-outer.incoming {
|
||||||
|
border-color: alpha(@theme_selected_bg_color, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.dino-main .incoming-call-box {
|
||||||
|
background: alpha(@theme_selected_bg_color, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
window.dino-main .dino-sidebar > frame.collapsed {
|
window.dino-main .dino-sidebar > frame.collapsed {
|
||||||
border-bottom: 1px solid @borders;
|
border-bottom: 1px solid @borders;
|
||||||
}
|
}
|
||||||
|
@ -204,3 +214,103 @@ box.dino-input-error label.input-status-highlight-once {
|
||||||
animation-iteration-count: 1;
|
animation-iteration-count: 1;
|
||||||
animation-name: input-error-highlight;
|
animation-name: input-error-highlight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Call window */
|
||||||
|
|
||||||
|
.dino-call-window .titlebar {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window headerbar, .call-button {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window .titlebutton.close:hover {
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border-color: rgba(255,255,255,0);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window button.call-button {
|
||||||
|
outline: 0;
|
||||||
|
border-radius: 1000px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window button.white-button {
|
||||||
|
color: #1d1c1d;
|
||||||
|
background: rgba(255,255,255,0.85);
|
||||||
|
border: lightgrey;
|
||||||
|
}
|
||||||
|
.dino-call-window button.white-button:hover {
|
||||||
|
background: rgba(255,255,255,1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window button.transparent-white-button {
|
||||||
|
color: white;
|
||||||
|
background: rgba(255,255,255,0.15);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
.dino-call-window button.transparent-white-button:hover {
|
||||||
|
background: rgba(255,255,255,0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window button.call-mediadevice-settings-button {
|
||||||
|
border-radius: 1000px;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 3px;
|
||||||
|
margin: 2px;
|
||||||
|
transition-duration: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window button.call-mediadevice-settings-button:hover,
|
||||||
|
.dino-call-window button.call-mediadevice-settings-button:checked { /* Effect that makes the button slightly larger on hover :) */
|
||||||
|
border-radius: 1000px;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 5px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window .encryption-box {
|
||||||
|
color: rgba(255,255,255,0.7);
|
||||||
|
border-radius: 5px;
|
||||||
|
background: rgba(0,0,0,0.5);
|
||||||
|
padding: 0px;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window .encryption-box.unencrypted {
|
||||||
|
color: @error_color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window .encryption-box:hover {
|
||||||
|
background: rgba(20,20,20,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window .call-header-bar {
|
||||||
|
background: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0));
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window .call-header-bar,
|
||||||
|
.dino-call-window .call-header-bar image {
|
||||||
|
color: #ededec;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window .call-bottom-bar {
|
||||||
|
background: linear-gradient(rgba(0,0,0,0), rgba(0,0,0,0.3));
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window .video-placeholder-box {
|
||||||
|
background-color: #212121;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dino-call-window .text-no-controls {
|
||||||
|
background: white;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
}
|
|
@ -17,7 +17,7 @@ void main(string[] args) {
|
||||||
Gtk.init(ref args);
|
Gtk.init(ref args);
|
||||||
Dino.Ui.Application app = new Dino.Ui.Application() { search_path_generator=search_path_generator };
|
Dino.Ui.Application app = new Dino.Ui.Application() { search_path_generator=search_path_generator };
|
||||||
Plugins.Loader loader = new Plugins.Loader(app);
|
Plugins.Loader loader = new Plugins.Loader(app);
|
||||||
loader.loadAll();
|
loader.load_all();
|
||||||
|
|
||||||
app.run(args);
|
app.run(args);
|
||||||
loader.shutdown();
|
loader.shutdown();
|
||||||
|
|
|
@ -199,6 +199,24 @@ public class Dino.Ui.Application : Gtk.Application, Dino.Application {
|
||||||
dialog.present();
|
dialog.present();
|
||||||
});
|
});
|
||||||
add_action(open_shortcuts_action);
|
add_action(open_shortcuts_action);
|
||||||
|
|
||||||
|
SimpleAction accept_call_action = new SimpleAction("accept-call", VariantType.INT32);
|
||||||
|
accept_call_action.activate.connect((variant) => {
|
||||||
|
Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(variant.get_int32());
|
||||||
|
stream_interactor.get_module(Calls.IDENTITY).accept_call(call);
|
||||||
|
|
||||||
|
var call_window = new CallWindow();
|
||||||
|
call_window.controller = new CallWindowController(call_window, call, stream_interactor);
|
||||||
|
call_window.present();
|
||||||
|
});
|
||||||
|
add_action(accept_call_action);
|
||||||
|
|
||||||
|
SimpleAction deny_call_action = new SimpleAction("deny-call", VariantType.INT32);
|
||||||
|
deny_call_action.activate.connect((variant) => {
|
||||||
|
Call? call = stream_interactor.get_module(CallStore.IDENTITY).get_call_by_id(variant.get_int32());
|
||||||
|
stream_interactor.get_module(Calls.IDENTITY).reject_call(call);
|
||||||
|
});
|
||||||
|
add_action(deny_call_action);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool use_csd() {
|
public bool use_csd() {
|
||||||
|
|
127
main/src/ui/call_window/audio_settings_popover.vala
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
using Gee;
|
||||||
|
using Gtk;
|
||||||
|
using Dino.Entities;
|
||||||
|
|
||||||
|
public class Dino.Ui.AudioSettingsPopover : Gtk.Popover {
|
||||||
|
|
||||||
|
public signal void microphone_selected(Plugins.MediaDevice device);
|
||||||
|
public signal void speaker_selected(Plugins.MediaDevice device);
|
||||||
|
|
||||||
|
public Plugins.MediaDevice? current_microphone_device { get; set; }
|
||||||
|
public Plugins.MediaDevice? current_speaker_device { get; set; }
|
||||||
|
|
||||||
|
private HashMap<ListBoxRow, Plugins.MediaDevice> row_microphone_device = new HashMap<ListBoxRow, Plugins.MediaDevice>();
|
||||||
|
private HashMap<ListBoxRow, Plugins.MediaDevice> row_speaker_device = new HashMap<ListBoxRow, Plugins.MediaDevice>();
|
||||||
|
|
||||||
|
public AudioSettingsPopover() {
|
||||||
|
Box box = new Box(Orientation.VERTICAL, 15) { margin=18, visible=true };
|
||||||
|
box.add(create_microphone_box());
|
||||||
|
box.add(create_speaker_box());
|
||||||
|
|
||||||
|
this.add(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Widget create_microphone_box() {
|
||||||
|
Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
|
||||||
|
Gee.List<Plugins.MediaDevice> devices = call_plugin.get_devices("audio", false);
|
||||||
|
|
||||||
|
Box micro_box = new Box(Orientation.VERTICAL, 10) { visible=true };
|
||||||
|
micro_box.add(new Label("<b>" + _("Microphones") + "</b>") { use_markup=true, xalign=0, visible=true, can_focus=true /* grab initial focus*/ });
|
||||||
|
|
||||||
|
if (devices.size == 0) {
|
||||||
|
micro_box.add(new Label("No microphones found."));
|
||||||
|
} else {
|
||||||
|
ListBox micro_list_box = new ListBox() { activate_on_single_click=true, selection_mode=SelectionMode.SINGLE, visible=true };
|
||||||
|
micro_list_box.set_header_func(listbox_header_func);
|
||||||
|
Frame micro_frame = new Frame(null) { visible=true };
|
||||||
|
micro_frame.add(micro_list_box);
|
||||||
|
foreach (Plugins.MediaDevice device in devices) {
|
||||||
|
Label label = new Label(device.display_name) { xalign=0, visible=true };
|
||||||
|
Image image = new Image.from_icon_name("object-select-symbolic", IconSize.BUTTON) { visible=true };
|
||||||
|
if (current_microphone_device == null || current_microphone_device.id != device.id) {
|
||||||
|
image.opacity = 0;
|
||||||
|
}
|
||||||
|
this.notify["current-microphone-device"].connect(() => {
|
||||||
|
if (current_microphone_device == null || current_microphone_device.id != device.id) {
|
||||||
|
image.opacity = 0;
|
||||||
|
} else {
|
||||||
|
image.opacity = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Box device_box = new Box(Orientation.HORIZONTAL, 0) { spacing=7, margin=7, visible=true };
|
||||||
|
device_box.add(image);
|
||||||
|
device_box.add(label);
|
||||||
|
ListBoxRow list_box_row = new ListBoxRow() { visible=true };
|
||||||
|
list_box_row.add(device_box);
|
||||||
|
micro_list_box.add(list_box_row);
|
||||||
|
|
||||||
|
row_microphone_device[list_box_row] = device;
|
||||||
|
}
|
||||||
|
micro_list_box.row_activated.connect((row) => {
|
||||||
|
if (!row_microphone_device.has_key(row)) return;
|
||||||
|
microphone_selected(row_microphone_device[row]);
|
||||||
|
micro_list_box.unselect_row(row);
|
||||||
|
});
|
||||||
|
micro_box.add(micro_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
return micro_box;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Widget create_speaker_box() {
|
||||||
|
Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
|
||||||
|
Gee.List<Plugins.MediaDevice> devices = call_plugin.get_devices("audio", true);
|
||||||
|
|
||||||
|
Box speaker_box = new Box(Orientation.VERTICAL, 10) { visible=true };
|
||||||
|
speaker_box.add(new Label("<b>" + _("Speakers") +"</b>") { use_markup=true, xalign=0, visible=true });
|
||||||
|
|
||||||
|
if (devices.size == 0) {
|
||||||
|
speaker_box.add(new Label("No speakers found."));
|
||||||
|
} else {
|
||||||
|
ListBox speaker_list_box = new ListBox() { activate_on_single_click=true, selection_mode=SelectionMode.SINGLE, visible=true };
|
||||||
|
speaker_list_box.set_header_func(listbox_header_func);
|
||||||
|
speaker_list_box.row_selected.connect((row) => {
|
||||||
|
|
||||||
|
});
|
||||||
|
Frame speaker_frame = new Frame(null) { visible=true };
|
||||||
|
speaker_frame.add(speaker_list_box);
|
||||||
|
foreach (Plugins.MediaDevice device in devices) {
|
||||||
|
Label label = new Label(device.display_name) { xalign=0, visible=true };
|
||||||
|
Image image = new Image.from_icon_name("object-select-symbolic", IconSize.BUTTON) { visible=true };
|
||||||
|
if (current_speaker_device == null || current_speaker_device.id != device.id) {
|
||||||
|
image.opacity = 0;
|
||||||
|
}
|
||||||
|
this.notify["current-speaker-device"].connect(() => {
|
||||||
|
if (current_speaker_device == null || current_speaker_device.id != device.id) {
|
||||||
|
image.opacity = 0;
|
||||||
|
} else {
|
||||||
|
image.opacity = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Box device_box = new Box(Orientation.HORIZONTAL, 0) { spacing=7, margin=7, visible=true };
|
||||||
|
device_box.add(image);
|
||||||
|
device_box.add(label);
|
||||||
|
ListBoxRow list_box_row = new ListBoxRow() { visible=true };
|
||||||
|
list_box_row.add(device_box);
|
||||||
|
speaker_list_box.add(list_box_row);
|
||||||
|
|
||||||
|
row_speaker_device[list_box_row] = device;
|
||||||
|
}
|
||||||
|
speaker_list_box.row_activated.connect((row) => {
|
||||||
|
if (!row_speaker_device.has_key(row)) return;
|
||||||
|
speaker_selected(row_speaker_device[row]);
|
||||||
|
speaker_list_box.unselect_row(row);
|
||||||
|
});
|
||||||
|
speaker_box.add(speaker_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
return speaker_box;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void listbox_header_func(ListBoxRow row, ListBoxRow? before_row) {
|
||||||
|
if (row.get_header() == null && before_row != null) {
|
||||||
|
row.set_header(new Separator(Orientation.HORIZONTAL));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
164
main/src/ui/call_window/call_bottom_bar.vala
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
using Dino.Entities;
|
||||||
|
using Gtk;
|
||||||
|
using Pango;
|
||||||
|
|
||||||
|
public class Dino.Ui.CallBottomBar : Gtk.Box {
|
||||||
|
|
||||||
|
public signal void hang_up();
|
||||||
|
|
||||||
|
public bool audio_enabled { get; set; }
|
||||||
|
public bool video_enabled { get; set; }
|
||||||
|
|
||||||
|
public static IconSize ICON_SIZE_MEDIADEVICE_BUTTON = Gtk.icon_size_register("im.dino.Dino.CALL_MEDIADEVICE_BUTTON", 10, 10);
|
||||||
|
|
||||||
|
public string counterpart_display_name { get; set; }
|
||||||
|
|
||||||
|
private Button audio_button = new Button() { height_request=45, width_request=45, halign=Align.START, valign=Align.START, visible=true };
|
||||||
|
private Overlay audio_button_overlay = new Overlay() { visible=true };
|
||||||
|
private Image audio_image = new Image() { visible=true };
|
||||||
|
private MenuButton audio_settings_button = new MenuButton() { halign=Align.END, valign=Align.END };
|
||||||
|
public AudioSettingsPopover? audio_settings_popover;
|
||||||
|
|
||||||
|
private Button video_button = new Button() { height_request=45, width_request=45, halign=Align.START, valign=Align.START, visible=true };
|
||||||
|
private Overlay video_button_overlay = new Overlay() { visible=true };
|
||||||
|
private Image video_image = new Image() { visible=true };
|
||||||
|
private MenuButton video_settings_button = new MenuButton() { halign=Align.END, valign=Align.END };
|
||||||
|
public VideoSettingsPopover? video_settings_popover;
|
||||||
|
|
||||||
|
public CallEntryptionButton encryption_button = new CallEntryptionButton() { relief=ReliefStyle.NONE, height_request=30, width_request=30, margin_start=20, margin_bottom=25, halign=Align.START, valign=Align.END };
|
||||||
|
|
||||||
|
private Label label = new Label("") { margin=20, halign=Align.CENTER, valign=Align.CENTER, wrap=true, wrap_mode=Pango.WrapMode.WORD_CHAR, hexpand=true, visible=true };
|
||||||
|
private Stack stack = new Stack() { visible=true };
|
||||||
|
|
||||||
|
public CallBottomBar() {
|
||||||
|
Object(orientation:Orientation.HORIZONTAL, spacing:0);
|
||||||
|
|
||||||
|
Overlay default_control = new Overlay() { visible=true };
|
||||||
|
default_control.add_overlay(encryption_button);
|
||||||
|
|
||||||
|
Box main_buttons = new Box(Orientation.HORIZONTAL, 20) { margin_start=40, margin_end=40, margin=20, halign=Align.CENTER, hexpand=true, visible=true };
|
||||||
|
|
||||||
|
audio_button.add(audio_image);
|
||||||
|
audio_button.get_style_context().add_class("call-button");
|
||||||
|
audio_button.clicked.connect(() => { audio_enabled = !audio_enabled; });
|
||||||
|
audio_button.margin_end = audio_button.margin_bottom = 5; // space for the small settings button
|
||||||
|
audio_button_overlay.add(audio_button);
|
||||||
|
audio_button_overlay.add_overlay(audio_settings_button);
|
||||||
|
audio_settings_button.set_image(new Image.from_icon_name("go-up-symbolic", ICON_SIZE_MEDIADEVICE_BUTTON) { visible=true });
|
||||||
|
audio_settings_button.get_style_context().add_class("call-mediadevice-settings-button");
|
||||||
|
audio_settings_button.use_popover = true;
|
||||||
|
main_buttons.add(audio_button_overlay);
|
||||||
|
|
||||||
|
video_button.add(video_image);
|
||||||
|
video_button.get_style_context().add_class("call-button");
|
||||||
|
video_button.clicked.connect(() => { video_enabled = !video_enabled; });
|
||||||
|
video_button.margin_end = video_button.margin_bottom = 5;
|
||||||
|
video_button_overlay.add(video_button);
|
||||||
|
video_button_overlay.add_overlay(video_settings_button);
|
||||||
|
video_settings_button.set_image(new Image.from_icon_name("go-up-symbolic", ICON_SIZE_MEDIADEVICE_BUTTON) { visible=true });
|
||||||
|
video_settings_button.get_style_context().add_class("call-mediadevice-settings-button");
|
||||||
|
video_settings_button.use_popover = true;
|
||||||
|
main_buttons.add(video_button_overlay);
|
||||||
|
|
||||||
|
Button button_hang = new Button.from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR) { height_request=45, width_request=45, halign=Align.START, valign=Align.START, visible=true };
|
||||||
|
button_hang.get_style_context().add_class("call-button");
|
||||||
|
button_hang.get_style_context().add_class("destructive-action");
|
||||||
|
button_hang.clicked.connect(() => hang_up());
|
||||||
|
main_buttons.add(button_hang);
|
||||||
|
|
||||||
|
default_control.add(main_buttons);
|
||||||
|
|
||||||
|
label.get_style_context().add_class("text-no-controls");
|
||||||
|
|
||||||
|
stack.add_named(default_control, "control-buttons");
|
||||||
|
stack.add_named(label, "label");
|
||||||
|
this.add(stack);
|
||||||
|
|
||||||
|
this.notify["audio-enabled"].connect(on_audio_enabled_changed);
|
||||||
|
this.notify["video-enabled"].connect(on_video_enabled_changed);
|
||||||
|
|
||||||
|
audio_enabled = true;
|
||||||
|
video_enabled = false;
|
||||||
|
|
||||||
|
on_audio_enabled_changed();
|
||||||
|
on_video_enabled_changed();
|
||||||
|
|
||||||
|
this.get_style_context().add_class("call-bottom-bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
public AudioSettingsPopover? show_audio_device_choices(bool show) {
|
||||||
|
audio_settings_button.visible = show;
|
||||||
|
if (audio_settings_popover != null) audio_settings_popover.visible = false;
|
||||||
|
if (!show) return null;
|
||||||
|
|
||||||
|
audio_settings_popover = new AudioSettingsPopover();
|
||||||
|
|
||||||
|
audio_settings_button.popover = audio_settings_popover;
|
||||||
|
|
||||||
|
audio_settings_popover.set_relative_to(audio_settings_button);
|
||||||
|
audio_settings_popover.microphone_selected.connect(() => { audio_settings_button.active = false; });
|
||||||
|
audio_settings_popover.speaker_selected.connect(() => { audio_settings_button.active = false; });
|
||||||
|
|
||||||
|
return audio_settings_popover;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show_audio_device_error() {
|
||||||
|
audio_settings_button.set_image(new Image.from_icon_name("dialog-warning-symbolic", IconSize.BUTTON) { visible=true });
|
||||||
|
Util.force_error_color(audio_settings_button);
|
||||||
|
}
|
||||||
|
|
||||||
|
public VideoSettingsPopover? show_video_device_choices(bool show) {
|
||||||
|
video_settings_button.visible = show;
|
||||||
|
if (video_settings_popover != null) video_settings_popover.visible = false;
|
||||||
|
if (!show) return null;
|
||||||
|
|
||||||
|
video_settings_popover = new VideoSettingsPopover();
|
||||||
|
|
||||||
|
|
||||||
|
video_settings_button.popover = video_settings_popover;
|
||||||
|
|
||||||
|
video_settings_popover.set_relative_to(video_settings_button);
|
||||||
|
video_settings_popover.camera_selected.connect(() => { video_settings_button.active = false; });
|
||||||
|
|
||||||
|
return video_settings_popover;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show_video_device_error() {
|
||||||
|
video_settings_button.set_image(new Image.from_icon_name("dialog-warning-symbolic", IconSize.BUTTON) { visible=true });
|
||||||
|
Util.force_error_color(video_settings_button);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void on_audio_enabled_changed() {
|
||||||
|
if (audio_enabled) {
|
||||||
|
audio_image.set_from_icon_name("dino-microphone-symbolic", IconSize.LARGE_TOOLBAR);
|
||||||
|
audio_button.get_style_context().add_class("white-button");
|
||||||
|
audio_button.get_style_context().remove_class("transparent-white-button");
|
||||||
|
} else {
|
||||||
|
audio_image.set_from_icon_name("dino-microphone-off-symbolic", IconSize.LARGE_TOOLBAR);
|
||||||
|
audio_button.get_style_context().remove_class("white-button");
|
||||||
|
audio_button.get_style_context().add_class("transparent-white-button");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void on_video_enabled_changed() {
|
||||||
|
if (video_enabled) {
|
||||||
|
video_image.set_from_icon_name("dino-video-symbolic", IconSize.LARGE_TOOLBAR);
|
||||||
|
video_button.get_style_context().add_class("white-button");
|
||||||
|
video_button.get_style_context().remove_class("transparent-white-button");
|
||||||
|
|
||||||
|
} else {
|
||||||
|
video_image.set_from_icon_name("dino-video-off-symbolic", IconSize.LARGE_TOOLBAR);
|
||||||
|
video_button.get_style_context().remove_class("white-button");
|
||||||
|
video_button.get_style_context().add_class("transparent-white-button");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show_counterpart_ended(string text) {
|
||||||
|
stack.set_visible_child_name("label");
|
||||||
|
label.label = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool is_menu_active() {
|
||||||
|
return video_settings_button.active || audio_settings_button.active || encryption_button.active;
|
||||||
|
}
|
||||||
|
}
|
77
main/src/ui/call_window/call_encryption_button.vala
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
using Dino.Entities;
|
||||||
|
using Gtk;
|
||||||
|
using Pango;
|
||||||
|
|
||||||
|
public class Dino.Ui.CallEntryptionButton : MenuButton {
|
||||||
|
|
||||||
|
private Image encryption_image = new Image.from_icon_name("changes-allow-symbolic", IconSize.BUTTON) { visible=true };
|
||||||
|
|
||||||
|
construct {
|
||||||
|
add(encryption_image);
|
||||||
|
get_style_context().add_class("encryption-box");
|
||||||
|
this.set_popover(popover);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_icon(bool encrypted, string? icon_name) {
|
||||||
|
this.visible = true;
|
||||||
|
|
||||||
|
if (encrypted) {
|
||||||
|
encryption_image.set_from_icon_name(icon_name ?? "changes-prevent-symbolic", IconSize.BUTTON);
|
||||||
|
get_style_context().remove_class("unencrypted");
|
||||||
|
} else {
|
||||||
|
encryption_image.set_from_icon_name(icon_name ?? "changes-allow-symbolic", IconSize.BUTTON);
|
||||||
|
get_style_context().add_class("unencrypted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_info(string? title, bool show_keys, Xmpp.Xep.Jingle.ContentEncryption? audio_encryption, Xmpp.Xep.Jingle.ContentEncryption? video_encryption) {
|
||||||
|
Popover popover = new Popover(this);
|
||||||
|
this.set_popover(popover);
|
||||||
|
|
||||||
|
if (audio_encryption == null) {
|
||||||
|
popover.add(new Label("This call is unencrypted.") { margin=10, visible=true } );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (title != null && !show_keys) {
|
||||||
|
popover.add(new Label(title) { use_markup=true, margin=10, visible=true } );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Box box = new Box(Orientation.VERTICAL, 10) { margin=10, visible=true };
|
||||||
|
box.add(new Label("<b>%s</b>".printf(title ?? "This call is end-to-end encrypted.")) { use_markup=true, xalign=0, visible=true });
|
||||||
|
|
||||||
|
if (video_encryption == null) {
|
||||||
|
box.add(create_media_encryption_grid(audio_encryption));
|
||||||
|
} else {
|
||||||
|
box.add(new Label("<b>Audio</b>") { use_markup=true, xalign=0, visible=true });
|
||||||
|
box.add(create_media_encryption_grid(audio_encryption));
|
||||||
|
box.add(new Label("<b>Video</b>") { use_markup=true, xalign=0, visible=true });
|
||||||
|
box.add(create_media_encryption_grid(video_encryption));
|
||||||
|
}
|
||||||
|
popover.add(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Grid create_media_encryption_grid(Xmpp.Xep.Jingle.ContentEncryption? encryption) {
|
||||||
|
Grid ret = new Grid() { row_spacing=3, column_spacing=5, visible=true };
|
||||||
|
if (encryption.peer_key.length > 0) {
|
||||||
|
ret.attach(new Label("Peer call key") { xalign=0, visible=true }, 1, 2, 1, 1);
|
||||||
|
ret.attach(new Label("<span font_family='monospace'>" + format_fingerprint(encryption.peer_key) + "</span>") { use_markup=true, max_width_chars=25, ellipsize=EllipsizeMode.MIDDLE, xalign=0, hexpand=true, visible=true }, 2, 2, 1, 1);
|
||||||
|
}
|
||||||
|
if (encryption.our_key.length > 0) {
|
||||||
|
ret.attach(new Label("Your call key") { xalign=0, visible=true }, 1, 3, 1, 1);
|
||||||
|
ret.attach(new Label("<span font_family='monospace'>" + format_fingerprint(encryption.our_key) + "</span>") { use_markup=true, max_width_chars=25, ellipsize=EllipsizeMode.MIDDLE, xalign=0, hexpand=true, visible=true }, 2, 3, 1, 1);
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string format_fingerprint(uint8[] fingerprint) {
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < fingerprint.length; i++) {
|
||||||
|
sb.append("%02x".printf(fingerprint[i]));
|
||||||
|
if (i < fingerprint.length - 1) {
|
||||||
|
sb.append(":");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.str;
|
||||||
|
}
|
||||||
|
}
|
260
main/src/ui/call_window/call_window.vala
Normal file
|
@ -0,0 +1,260 @@
|
||||||
|
using Dino.Entities;
|
||||||
|
using Gtk;
|
||||||
|
|
||||||
|
namespace Dino.Ui {
|
||||||
|
|
||||||
|
public class CallWindow : Gtk.Window {
|
||||||
|
public string counterpart_display_name { get; set; }
|
||||||
|
|
||||||
|
// TODO should find another place for this
|
||||||
|
public CallWindowController controller;
|
||||||
|
|
||||||
|
public Overlay overlay = new Overlay() { visible=true };
|
||||||
|
public EventBox event_box = new EventBox() { visible=true };
|
||||||
|
public CallBottomBar bottom_bar = new CallBottomBar() { visible=true };
|
||||||
|
public Revealer bottom_bar_revealer = new Revealer() { valign=Align.END, transition_type=RevealerTransitionType.CROSSFADE, transition_duration=200, visible=true };
|
||||||
|
public HeaderBar header_bar = new HeaderBar() { show_close_button=true, visible=true };
|
||||||
|
public Revealer header_bar_revealer = new Revealer() { valign=Align.START, transition_type=RevealerTransitionType.CROSSFADE, transition_duration=200, visible=true };
|
||||||
|
public Stack stack = new Stack() { visible=true };
|
||||||
|
public Box own_video_box = new Box(Orientation.HORIZONTAL, 0) { expand=true, visible=true };
|
||||||
|
private Widget? own_video = null;
|
||||||
|
private Box? own_video_border = new Box(Orientation.HORIZONTAL, 0) { expand=true }; // hack to draw a border around our own video, since we apparently can't draw a border around the Gst widget
|
||||||
|
|
||||||
|
private int own_video_width = 150;
|
||||||
|
private int own_video_height = 100;
|
||||||
|
|
||||||
|
private bool hide_controll_elements = false;
|
||||||
|
private uint hide_controll_handler = 0;
|
||||||
|
private Widget? main_widget = null;
|
||||||
|
|
||||||
|
construct {
|
||||||
|
header_bar.get_style_context().add_class("call-header-bar");
|
||||||
|
header_bar_revealer.add(header_bar);
|
||||||
|
|
||||||
|
this.get_style_context().add_class("dino-call-window");
|
||||||
|
|
||||||
|
bottom_bar_revealer.add(bottom_bar);
|
||||||
|
|
||||||
|
overlay.add_overlay(own_video_box);
|
||||||
|
overlay.add_overlay(own_video_border);
|
||||||
|
overlay.add_overlay(bottom_bar_revealer);
|
||||||
|
overlay.add_overlay(header_bar_revealer);
|
||||||
|
|
||||||
|
event_box.add(overlay);
|
||||||
|
add(event_box);
|
||||||
|
|
||||||
|
Util.force_css(own_video_border, "* { border: 1px solid #616161; background-color: transparent; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallWindow() {
|
||||||
|
event_box.events |= Gdk.EventMask.POINTER_MOTION_MASK;
|
||||||
|
event_box.events |= Gdk.EventMask.ENTER_NOTIFY_MASK;
|
||||||
|
event_box.events |= Gdk.EventMask.LEAVE_NOTIFY_MASK;
|
||||||
|
|
||||||
|
this.bind_property("counterpart-display-name", header_bar, "title", BindingFlags.SYNC_CREATE);
|
||||||
|
this.bind_property("counterpart-display-name", bottom_bar, "counterpart-display-name", BindingFlags.SYNC_CREATE);
|
||||||
|
|
||||||
|
event_box.motion_notify_event.connect(reveal_control_elements);
|
||||||
|
event_box.enter_notify_event.connect(reveal_control_elements);
|
||||||
|
event_box.leave_notify_event.connect(reveal_control_elements);
|
||||||
|
this.configure_event.connect(reveal_control_elements); // upon resizing
|
||||||
|
this.configure_event.connect(update_own_video_position);
|
||||||
|
|
||||||
|
this.set_titlebar(new OutsideHeaderBar(this.header_bar) { visible=true });
|
||||||
|
|
||||||
|
reveal_control_elements();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_video_fallback(StreamInteractor stream_interactor, Conversation conversation) {
|
||||||
|
hide_controll_elements = false;
|
||||||
|
|
||||||
|
Box box = new Box(Orientation.HORIZONTAL, 0) { visible=true };
|
||||||
|
box.get_style_context().add_class("video-placeholder-box");
|
||||||
|
AvatarImage avatar = new AvatarImage() { hexpand=true, vexpand=true, halign=Align.CENTER, valign=Align.CENTER, height=100, width=100, visible=true };
|
||||||
|
avatar.set_conversation(stream_interactor, conversation);
|
||||||
|
box.add(avatar);
|
||||||
|
|
||||||
|
set_new_main_widget(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_video(Widget widget) {
|
||||||
|
hide_controll_elements = true;
|
||||||
|
|
||||||
|
widget.visible = true;
|
||||||
|
set_new_main_widget(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_own_video(Widget? widget_) {
|
||||||
|
own_video_box.foreach((widget) => { own_video_box.remove(widget); });
|
||||||
|
|
||||||
|
own_video = widget_;
|
||||||
|
if (own_video == null) {
|
||||||
|
own_video = new Box(Orientation.HORIZONTAL, 0) { expand=true };
|
||||||
|
}
|
||||||
|
own_video.visible = true;
|
||||||
|
own_video.width_request = 150;
|
||||||
|
own_video.height_request = 100;
|
||||||
|
own_video_box.add(own_video);
|
||||||
|
|
||||||
|
own_video_border.visible = true;
|
||||||
|
|
||||||
|
update_own_video_position();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_own_video_ratio(int width, int height) {
|
||||||
|
if (width / height > 150 / 100) {
|
||||||
|
this.own_video_width = 150;
|
||||||
|
this.own_video_height = height * 150 / width;
|
||||||
|
} else {
|
||||||
|
this.own_video_width = width * 100 / height;
|
||||||
|
this.own_video_height = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
own_video.width_request = own_video_width;
|
||||||
|
own_video.height_request = own_video_height;
|
||||||
|
|
||||||
|
update_own_video_position();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unset_own_video() {
|
||||||
|
own_video_box.foreach((widget) => { own_video_box.remove(widget); });
|
||||||
|
|
||||||
|
own_video_border.visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_test_video() {
|
||||||
|
hide_controll_elements = true;
|
||||||
|
|
||||||
|
var pipeline = new Gst.Pipeline(null);
|
||||||
|
var src = Gst.ElementFactory.make("videotestsrc", null);
|
||||||
|
pipeline.add(src);
|
||||||
|
Gst.Video.Sink sink = (Gst.Video.Sink) Gst.ElementFactory.make("gtksink", null);
|
||||||
|
Gtk.Widget widget;
|
||||||
|
sink.get("widget", out widget);
|
||||||
|
widget.unparent();
|
||||||
|
pipeline.add(sink);
|
||||||
|
src.link(sink);
|
||||||
|
widget.visible = true;
|
||||||
|
|
||||||
|
pipeline.set_state(Gst.State.PLAYING);
|
||||||
|
|
||||||
|
sink.get_static_pad("sink").notify["caps"].connect(() => {
|
||||||
|
int width, height;
|
||||||
|
sink.get_static_pad("sink").caps.get_structure(0).get_int("width", out width);
|
||||||
|
sink.get_static_pad("sink").caps.get_structure(0).get_int("height", out height);
|
||||||
|
widget.width_request = width;
|
||||||
|
widget.height_request = height;
|
||||||
|
});
|
||||||
|
|
||||||
|
set_new_main_widget(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void set_new_main_widget(Widget widget) {
|
||||||
|
if (main_widget != null) overlay.remove(main_widget);
|
||||||
|
overlay.add(widget);
|
||||||
|
main_widget = widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_status(string state) {
|
||||||
|
switch (state) {
|
||||||
|
case "requested":
|
||||||
|
header_bar.subtitle = _("Calling…");
|
||||||
|
break;
|
||||||
|
case "ringing":
|
||||||
|
header_bar.subtitle = _("Ringing…");
|
||||||
|
break;
|
||||||
|
case "establishing":
|
||||||
|
header_bar.subtitle = _("Connecting…");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
header_bar.subtitle = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show_counterpart_ended(string? reason_name, string? reason_text) {
|
||||||
|
hide_controll_elements = false;
|
||||||
|
reveal_control_elements();
|
||||||
|
|
||||||
|
string text = "";
|
||||||
|
if (reason_name == Xmpp.Xep.Jingle.ReasonElement.SUCCESS) {
|
||||||
|
text = _("%s ended the call").printf(counterpart_display_name);
|
||||||
|
} else if (reason_name == Xmpp.Xep.Jingle.ReasonElement.DECLINE || reason_name == Xmpp.Xep.Jingle.ReasonElement.BUSY) {
|
||||||
|
text = _("%s declined the call").printf(counterpart_display_name);
|
||||||
|
} else {
|
||||||
|
text = "The call has been terminated: " + (reason_name ?? "") + " " + (reason_text ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
bottom_bar.show_counterpart_ended(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool reveal_control_elements() {
|
||||||
|
if (!bottom_bar_revealer.child_revealed) {
|
||||||
|
bottom_bar_revealer.set_reveal_child(true);
|
||||||
|
header_bar_revealer.set_reveal_child(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hide_controll_handler != 0) {
|
||||||
|
Source.remove(hide_controll_handler);
|
||||||
|
hide_controll_handler = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hide_controll_elements) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
hide_controll_handler = Timeout.add_seconds(3, () => {
|
||||||
|
if (!hide_controll_elements) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bottom_bar.is_menu_active()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
header_bar_revealer.set_reveal_child(false);
|
||||||
|
bottom_bar_revealer.set_reveal_child(false);
|
||||||
|
hide_controll_handler = 0;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool update_own_video_position() {
|
||||||
|
if (own_video == null) return false;
|
||||||
|
|
||||||
|
int width, height;
|
||||||
|
this.get_size(out width,out height);
|
||||||
|
|
||||||
|
own_video.margin_end = own_video.margin_bottom = own_video_border.margin_end = own_video_border.margin_bottom = 20;
|
||||||
|
own_video.margin_start = own_video_border.margin_start = width - own_video_width - 20;
|
||||||
|
own_video.margin_top = own_video_border.margin_top = height - own_video_height - 20;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hack to make the CallHeaderBar feel like a HeaderBar (right click menu, double click, ..) although it isn't set as headerbar.
|
||||||
|
* OutsideHeaderBar is set as a headerbar and it doesn't take any space, but claims to take space (which is actually taken by CallHeaderBar).
|
||||||
|
*/
|
||||||
|
public class OutsideHeaderBar : Gtk.Box {
|
||||||
|
HeaderBar header_bar;
|
||||||
|
|
||||||
|
public OutsideHeaderBar(HeaderBar header_bar) {
|
||||||
|
this.header_bar = header_bar;
|
||||||
|
|
||||||
|
size_allocate.connect_after(on_header_bar_size_allocate);
|
||||||
|
header_bar.size_allocate.connect(on_header_bar_size_allocate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void on_header_bar_size_allocate() {
|
||||||
|
Allocation header_bar_alloc;
|
||||||
|
header_bar.get_allocation(out header_bar_alloc);
|
||||||
|
|
||||||
|
Allocation alloc;
|
||||||
|
get_allocation(out alloc);
|
||||||
|
alloc.height = header_bar_alloc.height;
|
||||||
|
set_allocation(alloc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
254
main/src/ui/call_window/call_window_controller.vala
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
using Dino.Entities;
|
||||||
|
using Gtk;
|
||||||
|
|
||||||
|
public class Dino.Ui.CallWindowController : Object {
|
||||||
|
|
||||||
|
private CallWindow call_window;
|
||||||
|
private Call call;
|
||||||
|
private Conversation conversation;
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
private Calls calls;
|
||||||
|
private Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
|
||||||
|
|
||||||
|
private Plugins.VideoCallWidget? own_video = null;
|
||||||
|
private Plugins.VideoCallWidget? counterpart_video = null;
|
||||||
|
private int window_height = -1;
|
||||||
|
private int window_width = -1;
|
||||||
|
private bool window_size_changed = false;
|
||||||
|
|
||||||
|
public CallWindowController(CallWindow call_window, Call call, StreamInteractor stream_interactor) {
|
||||||
|
this.call_window = call_window;
|
||||||
|
this.call = call;
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
|
||||||
|
this.calls = stream_interactor.get_module(Calls.IDENTITY);
|
||||||
|
this.conversation = stream_interactor.get_module(ConversationManager.IDENTITY).get_conversation(call.counterpart.bare_jid, call.account, Conversation.Type.CHAT);
|
||||||
|
this.own_video = call_plugin.create_widget(Plugins.WidgetType.GTK);
|
||||||
|
this.counterpart_video = call_plugin.create_widget(Plugins.WidgetType.GTK);
|
||||||
|
|
||||||
|
call_window.counterpart_display_name = Util.get_conversation_display_name(stream_interactor, conversation);
|
||||||
|
call_window.set_default_size(704, 528); // 640x480 * 1.1
|
||||||
|
call_window.set_video_fallback(stream_interactor, conversation);
|
||||||
|
|
||||||
|
this.call_window.bottom_bar.video_enabled = calls.should_we_send_video(call);
|
||||||
|
|
||||||
|
if (call.direction == Call.DIRECTION_INCOMING) {
|
||||||
|
call_window.set_status("establishing");
|
||||||
|
} else {
|
||||||
|
call_window.set_status("requested");
|
||||||
|
}
|
||||||
|
|
||||||
|
call_window.bottom_bar.hang_up.connect(() => {
|
||||||
|
calls.end_call(conversation, call);
|
||||||
|
call_window.close();
|
||||||
|
call_window.destroy();
|
||||||
|
this.dispose();
|
||||||
|
});
|
||||||
|
call_window.destroy.connect(() => {
|
||||||
|
calls.end_call(conversation, call);
|
||||||
|
this.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
call_window.bottom_bar.notify["audio-enabled"].connect(() => {
|
||||||
|
calls.mute_own_audio(call, !call_window.bottom_bar.audio_enabled);
|
||||||
|
});
|
||||||
|
call_window.bottom_bar.notify["video-enabled"].connect(() => {
|
||||||
|
calls.mute_own_video(call, !call_window.bottom_bar.video_enabled);
|
||||||
|
update_own_video();
|
||||||
|
});
|
||||||
|
|
||||||
|
calls.counterpart_sends_video_updated.connect((call, mute) => {
|
||||||
|
if (!this.call.equals(call)) return;
|
||||||
|
|
||||||
|
if (mute) {
|
||||||
|
call_window.set_video_fallback(stream_interactor, conversation);
|
||||||
|
counterpart_video.detach();
|
||||||
|
} else {
|
||||||
|
if (!(counterpart_video is Widget)) return;
|
||||||
|
Widget widget = (Widget) counterpart_video;
|
||||||
|
call_window.set_video(widget);
|
||||||
|
counterpart_video.display_stream(calls.get_video_stream(call));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
calls.info_received.connect((call, session_info) => {
|
||||||
|
if (!this.call.equals(call)) return;
|
||||||
|
if (session_info == Xmpp.Xep.JingleRtp.CallSessionInfo.RINGING) {
|
||||||
|
call_window.set_status("ringing");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
calls.encryption_updated.connect((call, audio_encryption, video_encryption, same) => {
|
||||||
|
if (!this.call.equals(call)) return;
|
||||||
|
|
||||||
|
string? title = null;
|
||||||
|
string? icon_name = null;
|
||||||
|
bool show_keys = true;
|
||||||
|
Plugins.Registry registry = Dino.Application.get_default().plugin_registry;
|
||||||
|
Plugins.CallEncryptionEntry? encryption_entry = audio_encryption != null ? registry.call_encryption_entries[audio_encryption.encryption_ns] : null;
|
||||||
|
if (encryption_entry != null) {
|
||||||
|
Plugins.CallEncryptionWidget? encryption_widgets = encryption_entry.get_widget(call.account, audio_encryption);
|
||||||
|
if (encryption_widgets != null) {
|
||||||
|
title = encryption_widgets.get_title();
|
||||||
|
icon_name = encryption_widgets.get_icon_name();
|
||||||
|
show_keys = encryption_widgets.show_keys();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
call_window.bottom_bar.encryption_button.set_info(title, show_keys, audio_encryption, same ? null :video_encryption);
|
||||||
|
call_window.bottom_bar.encryption_button.set_icon(audio_encryption != null, icon_name);
|
||||||
|
});
|
||||||
|
|
||||||
|
own_video.resolution_changed.connect((width, height) => {
|
||||||
|
if (width == 0 || height == 0) return;
|
||||||
|
call_window.set_own_video_ratio((int)width, (int)height);
|
||||||
|
});
|
||||||
|
counterpart_video.resolution_changed.connect((width, height) => {
|
||||||
|
if (window_size_changed) return;
|
||||||
|
if (width == 0 || height == 0) return;
|
||||||
|
if (width > height) {
|
||||||
|
call_window.resize(704, (int) (height * 704 / width));
|
||||||
|
} else {
|
||||||
|
call_window.resize((int) (width * 704 / height), 704);
|
||||||
|
}
|
||||||
|
capture_window_size();
|
||||||
|
});
|
||||||
|
call_window.configure_event.connect((event) => {
|
||||||
|
if (window_width == -1 || window_height == -1) return false;
|
||||||
|
int current_height = this.call_window.get_allocated_height();
|
||||||
|
int current_width = this.call_window.get_allocated_width();
|
||||||
|
if (window_width != current_width || window_height != current_height) {
|
||||||
|
debug("Call window size changed by user. Disabling auto window-to-video size adaptation. %i->%i x %i->%i", window_width, current_width, window_height, current_height);
|
||||||
|
window_size_changed = true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
call_window.realize.connect(() => {
|
||||||
|
capture_window_size();
|
||||||
|
});
|
||||||
|
|
||||||
|
call.notify["state"].connect(on_call_state_changed);
|
||||||
|
calls.call_terminated.connect(on_call_terminated);
|
||||||
|
|
||||||
|
update_own_video();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void capture_window_size() {
|
||||||
|
Allocation allocation;
|
||||||
|
this.call_window.get_allocation(out allocation);
|
||||||
|
this.window_height = this.call_window.get_allocated_height();
|
||||||
|
this.window_width = this.call_window.get_allocated_width();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_call_state_changed() {
|
||||||
|
if (call.state == Call.State.IN_PROGRESS) {
|
||||||
|
call_window.set_status("");
|
||||||
|
call_plugin.devices_changed.connect((media, incoming) => {
|
||||||
|
if (media == "audio") update_audio_device_choices();
|
||||||
|
if (media == "video") update_video_device_choices();
|
||||||
|
});
|
||||||
|
|
||||||
|
update_audio_device_choices();
|
||||||
|
update_video_device_choices();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_call_terminated(Call call, string? reason_name, string? reason_text) {
|
||||||
|
call_window.show_counterpart_ended(reason_name, reason_text);
|
||||||
|
Timeout.add_seconds(3, () => {
|
||||||
|
call.notify["state"].disconnect(on_call_state_changed);
|
||||||
|
calls.call_terminated.disconnect(on_call_terminated);
|
||||||
|
|
||||||
|
|
||||||
|
call_window.close();
|
||||||
|
call_window.destroy();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update_audio_device_choices() {
|
||||||
|
if (call_plugin.get_devices("audio", true).size == 0 || call_plugin.get_devices("audio", false).size == 0) {
|
||||||
|
call_window.bottom_bar.show_audio_device_error();
|
||||||
|
} /*else if (call_plugin.get_devices("audio", true).size == 1 && call_plugin.get_devices("audio", false).size == 1) {
|
||||||
|
call_window.bottom_bar.show_audio_device_choices(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AudioSettingsPopover? audio_settings_popover = call_window.bottom_bar.show_audio_device_choices(true);
|
||||||
|
update_current_audio_device(audio_settings_popover);
|
||||||
|
|
||||||
|
audio_settings_popover.microphone_selected.connect((device) => {
|
||||||
|
call_plugin.set_device(calls.get_audio_stream(call), device);
|
||||||
|
update_current_audio_device(audio_settings_popover);
|
||||||
|
});
|
||||||
|
audio_settings_popover.speaker_selected.connect((device) => {
|
||||||
|
call_plugin.set_device(calls.get_audio_stream(call), device);
|
||||||
|
update_current_audio_device(audio_settings_popover);
|
||||||
|
});
|
||||||
|
calls.stream_created.connect((call, media) => {
|
||||||
|
if (media == "audio") {
|
||||||
|
update_current_audio_device(audio_settings_popover);
|
||||||
|
}
|
||||||
|
});*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update_current_audio_device(AudioSettingsPopover audio_settings_popover) {
|
||||||
|
Xmpp.Xep.JingleRtp.Stream stream = calls.get_audio_stream(call);
|
||||||
|
if (stream != null) {
|
||||||
|
audio_settings_popover.current_microphone_device = call_plugin.get_device(stream, false);
|
||||||
|
audio_settings_popover.current_speaker_device = call_plugin.get_device(stream, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update_video_device_choices() {
|
||||||
|
int device_count = call_plugin.get_devices("video", false).size;
|
||||||
|
|
||||||
|
if (device_count == 0) {
|
||||||
|
call_window.bottom_bar.show_video_device_error();
|
||||||
|
} /*else if (device_count == 1 || calls.get_video_stream(call) == null) {
|
||||||
|
call_window.bottom_bar.show_video_device_choices(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoSettingsPopover? video_settings_popover = call_window.bottom_bar.show_video_device_choices(true);
|
||||||
|
update_current_video_device(video_settings_popover);
|
||||||
|
|
||||||
|
video_settings_popover.camera_selected.connect((device) => {
|
||||||
|
call_plugin.set_device(calls.get_video_stream(call), device);
|
||||||
|
update_current_video_device(video_settings_popover);
|
||||||
|
own_video.display_device(device);
|
||||||
|
});
|
||||||
|
calls.stream_created.connect((call, media) => {
|
||||||
|
if (media == "video") {
|
||||||
|
update_current_video_device(video_settings_popover);
|
||||||
|
}
|
||||||
|
});*/
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update_current_video_device(VideoSettingsPopover video_settings_popover) {
|
||||||
|
Xmpp.Xep.JingleRtp.Stream stream = calls.get_video_stream(call);
|
||||||
|
if (stream != null) {
|
||||||
|
video_settings_popover.current_device = call_plugin.get_device(stream, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update_own_video() {
|
||||||
|
if (this.call_window.bottom_bar.video_enabled) {
|
||||||
|
Gee.List<Plugins.MediaDevice> devices = call_plugin.get_devices("video", false);
|
||||||
|
if (!(own_video is Widget) || devices.is_empty) {
|
||||||
|
call_window.set_own_video(null);
|
||||||
|
} else {
|
||||||
|
Widget widget = (Widget) own_video;
|
||||||
|
call_window.set_own_video(widget);
|
||||||
|
own_video.display_device(devices.first());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
own_video.detach();
|
||||||
|
call_window.unset_own_video();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void dispose() {
|
||||||
|
base.dispose();
|
||||||
|
call.notify["state"].disconnect(on_call_state_changed);
|
||||||
|
calls.call_terminated.disconnect(on_call_terminated);
|
||||||
|
}
|
||||||
|
}
|
73
main/src/ui/call_window/video_settings_popover.vala
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
using Gee;
|
||||||
|
using Gtk;
|
||||||
|
using Dino.Entities;
|
||||||
|
|
||||||
|
public class Dino.Ui.VideoSettingsPopover : Gtk.Popover {
|
||||||
|
|
||||||
|
public signal void camera_selected(Plugins.MediaDevice device);
|
||||||
|
|
||||||
|
public Plugins.MediaDevice? current_device { get; set; }
|
||||||
|
|
||||||
|
private HashMap<ListBoxRow, Plugins.MediaDevice> row_device = new HashMap<ListBoxRow, Plugins.MediaDevice>();
|
||||||
|
|
||||||
|
public VideoSettingsPopover() {
|
||||||
|
Box box = new Box(Orientation.VERTICAL, 15) { margin=18, visible=true };
|
||||||
|
box.add(create_camera_box());
|
||||||
|
|
||||||
|
this.add(box);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Widget create_camera_box() {
|
||||||
|
Plugins.VideoCallPlugin call_plugin = Dino.Application.get_default().plugin_registry.video_call_plugin;
|
||||||
|
Gee.List<Plugins.MediaDevice> devices = call_plugin.get_devices("video", false);
|
||||||
|
|
||||||
|
Box camera_box = new Box(Orientation.VERTICAL, 10) { visible=true };
|
||||||
|
camera_box.add(new Label("<b>" + _("Cameras") + "</b>") { use_markup=true, xalign=0, visible=true, can_focus=true /* grab initial focus*/ });
|
||||||
|
|
||||||
|
if (devices.size == 0) {
|
||||||
|
camera_box.add(new Label("No cameras found.") { visible=true });
|
||||||
|
} else {
|
||||||
|
ListBox list_box = new ListBox() { activate_on_single_click=true, selection_mode=SelectionMode.SINGLE, visible=true };
|
||||||
|
list_box.set_header_func(listbox_header_func);
|
||||||
|
Frame frame = new Frame(null) { visible=true };
|
||||||
|
frame.add(list_box);
|
||||||
|
foreach (Plugins.MediaDevice device in devices) {
|
||||||
|
Label label = new Label(device.display_name) { xalign=0, visible=true };
|
||||||
|
Image image = new Image.from_icon_name("object-select-symbolic", IconSize.BUTTON) { visible=true };
|
||||||
|
if (current_device == null || current_device.id != device.id) {
|
||||||
|
image.opacity = 0;
|
||||||
|
}
|
||||||
|
this.notify["current-device"].connect(() => {
|
||||||
|
if (current_device == null || current_device.id != device.id) {
|
||||||
|
image.opacity = 0;
|
||||||
|
} else {
|
||||||
|
image.opacity = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Box device_box = new Box(Orientation.HORIZONTAL, 0) { spacing=7, margin=7, visible=true };
|
||||||
|
device_box.add(image);
|
||||||
|
device_box.add(label);
|
||||||
|
ListBoxRow list_box_row = new ListBoxRow() { visible=true };
|
||||||
|
list_box_row.add(device_box);
|
||||||
|
list_box.add(list_box_row);
|
||||||
|
|
||||||
|
row_device[list_box_row] = device;
|
||||||
|
}
|
||||||
|
list_box.row_activated.connect((row) => {
|
||||||
|
if (!row_device.has_key(row)) return;
|
||||||
|
camera_selected(row_device[row]);
|
||||||
|
list_box.unselect_row(row);
|
||||||
|
});
|
||||||
|
camera_box.add(frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
return camera_box;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void listbox_header_func(ListBoxRow row, ListBoxRow? before_row) {
|
||||||
|
if (row.get_header() == null && before_row != null) {
|
||||||
|
row.set_header(new Separator(Orientation.HORIZONTAL));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
215
main/src/ui/conversation_content_view/call_widget.vala
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
using Gee;
|
||||||
|
using Gdk;
|
||||||
|
using Gtk;
|
||||||
|
using Pango;
|
||||||
|
|
||||||
|
using Dino.Entities;
|
||||||
|
|
||||||
|
namespace Dino.Ui {
|
||||||
|
|
||||||
|
public class CallMetaItem : ConversationSummary.ContentMetaItem {
|
||||||
|
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
|
||||||
|
public CallMetaItem(ContentItem content_item, StreamInteractor stream_interactor) {
|
||||||
|
base(content_item);
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Object? get_widget(Plugins.WidgetType type) {
|
||||||
|
CallItem call_item = content_item as CallItem;
|
||||||
|
return new CallWidget(stream_interactor, call_item.call, call_item.conversation) { visible=true };
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Gee.List<Plugins.MessageAction>? get_item_actions(Plugins.WidgetType type) { return null; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[GtkTemplate (ui = "/im/dino/Dino/call_widget.ui")]
|
||||||
|
public class CallWidget : SizeRequestBox {
|
||||||
|
|
||||||
|
[GtkChild] public Image image;
|
||||||
|
[GtkChild] public Label title_label;
|
||||||
|
[GtkChild] public Label subtitle_label;
|
||||||
|
[GtkChild] public Revealer incoming_call_revealer;
|
||||||
|
[GtkChild] public Button accept_call_button;
|
||||||
|
[GtkChild] public Button reject_call_button;
|
||||||
|
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
private Call call;
|
||||||
|
private Conversation conversation;
|
||||||
|
public Call.State call_state { get; set; } // needs to be public for binding
|
||||||
|
private uint time_update_handler_id = 0;
|
||||||
|
|
||||||
|
construct {
|
||||||
|
margin_top = 4;
|
||||||
|
size_request_mode = SizeRequestMode.HEIGHT_FOR_WIDTH;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CallWidget(StreamInteractor stream_interactor, Call call, Conversation conversation) {
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
this.call = call;
|
||||||
|
this.conversation = conversation;
|
||||||
|
|
||||||
|
size_allocate.connect((allocation) => {
|
||||||
|
if (allocation.height > parent.get_allocated_height()) {
|
||||||
|
Idle.add(() => { parent.queue_resize(); return false; });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
call.bind_property("state", this, "call-state");
|
||||||
|
this.notify["call-state"].connect(update_widget);
|
||||||
|
|
||||||
|
accept_call_button.clicked.connect(() => {
|
||||||
|
stream_interactor.get_module(Calls.IDENTITY).accept_call(call);
|
||||||
|
|
||||||
|
var call_window = new CallWindow();
|
||||||
|
call_window.controller = new CallWindowController(call_window, call, stream_interactor);
|
||||||
|
call_window.present();
|
||||||
|
});
|
||||||
|
|
||||||
|
reject_call_button.clicked.connect(() => {
|
||||||
|
stream_interactor.get_module(Calls.IDENTITY).reject_call(call);
|
||||||
|
});
|
||||||
|
|
||||||
|
update_widget();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update_widget() {
|
||||||
|
incoming_call_revealer.reveal_child = false;
|
||||||
|
incoming_call_revealer.get_style_context().remove_class("incoming");
|
||||||
|
|
||||||
|
switch (call.state) {
|
||||||
|
case Call.State.RINGING:
|
||||||
|
image.set_from_icon_name("dino-phone-ring-symbolic", IconSize.LARGE_TOOLBAR);
|
||||||
|
if (call.direction == Call.DIRECTION_INCOMING) {
|
||||||
|
bool video = stream_interactor.get_module(Calls.IDENTITY).should_we_send_video(call);
|
||||||
|
title_label.label = video ? _("Video call incoming") : _("Call incoming");
|
||||||
|
subtitle_label.label = "Ring ring…!";
|
||||||
|
incoming_call_revealer.reveal_child = true;
|
||||||
|
incoming_call_revealer.get_style_context().add_class("incoming");
|
||||||
|
} else {
|
||||||
|
title_label.label = _("Establishing call");
|
||||||
|
subtitle_label.label = "Ring ring…?";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Call.State.ESTABLISHING:
|
||||||
|
image.set_from_icon_name("dino-phone-ring-symbolic", IconSize.LARGE_TOOLBAR);
|
||||||
|
if (call.direction == Call.DIRECTION_INCOMING) {
|
||||||
|
bool video = stream_interactor.get_module(Calls.IDENTITY).should_we_send_video(call);
|
||||||
|
title_label.label = video ? _("Video call establishing") : _("Call establishing");
|
||||||
|
subtitle_label.label = "Connecting…";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Call.State.IN_PROGRESS:
|
||||||
|
image.set_from_icon_name("dino-phone-in-talk-symbolic", IconSize.LARGE_TOOLBAR);
|
||||||
|
title_label.label = _("Call in progress…");
|
||||||
|
string duration = get_duration_string((new DateTime.now_utc()).difference(call.local_time));
|
||||||
|
subtitle_label.label = _("Started %s ago").printf(duration);
|
||||||
|
|
||||||
|
time_update_handler_id = Timeout.add_seconds(get_next_time_change() + 1, () => {
|
||||||
|
Source.remove(time_update_handler_id);
|
||||||
|
time_update_handler_id = 0;
|
||||||
|
update_widget();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Call.State.OTHER_DEVICE_ACCEPTED:
|
||||||
|
image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR);
|
||||||
|
title_label.label = call.direction == Call.DIRECTION_INCOMING ? _("Incoming call") : _("Outgoing call");
|
||||||
|
subtitle_label.label = _("You handled this call on another device");
|
||||||
|
|
||||||
|
break;
|
||||||
|
case Call.State.ENDED:
|
||||||
|
image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR);
|
||||||
|
title_label.label = _("Call ended");
|
||||||
|
string formated_end = Util.format_time(call.end_time, _("%H∶%M"), _("%l∶%M %p"));
|
||||||
|
string duration = get_duration_string(call.end_time.difference(call.local_time));
|
||||||
|
subtitle_label.label = _("Ended at %s").printf(formated_end) +
|
||||||
|
" · " +
|
||||||
|
_("Lasted for %s").printf(duration);
|
||||||
|
break;
|
||||||
|
case Call.State.MISSED:
|
||||||
|
image.set_from_icon_name("dino-phone-missed-symbolic", IconSize.LARGE_TOOLBAR);
|
||||||
|
title_label.label = _("Call missed");
|
||||||
|
string who = null;
|
||||||
|
if (call.direction == Call.DIRECTION_INCOMING) {
|
||||||
|
who = "You";
|
||||||
|
} else {
|
||||||
|
who = Util.get_participant_display_name(stream_interactor, conversation, call.to);
|
||||||
|
}
|
||||||
|
subtitle_label.label = "%s missed this call".printf(who);
|
||||||
|
break;
|
||||||
|
case Call.State.DECLINED:
|
||||||
|
image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR);
|
||||||
|
title_label.label = _("Call declined");
|
||||||
|
string who = null;
|
||||||
|
if (call.direction == Call.DIRECTION_INCOMING) {
|
||||||
|
who = "You";
|
||||||
|
} else {
|
||||||
|
who = Util.get_participant_display_name(stream_interactor, conversation, call.to);
|
||||||
|
}
|
||||||
|
subtitle_label.label = "%s declined this call".printf(who);
|
||||||
|
break;
|
||||||
|
case Call.State.FAILED:
|
||||||
|
image.set_from_icon_name("dino-phone-hangup-symbolic", IconSize.LARGE_TOOLBAR);
|
||||||
|
title_label.label = _("Call failed");
|
||||||
|
subtitle_label.label = "Call failed to establish";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string get_duration_string(TimeSpan duration) {
|
||||||
|
DateTime a = new DateTime.now_utc();
|
||||||
|
DateTime b = new DateTime.now_utc();
|
||||||
|
a.difference(b);
|
||||||
|
|
||||||
|
TimeSpan remainder_duration = duration;
|
||||||
|
|
||||||
|
int hours = (int) Math.floor(remainder_duration / TimeSpan.HOUR);
|
||||||
|
remainder_duration -= hours * TimeSpan.HOUR;
|
||||||
|
|
||||||
|
int minutes = (int) Math.floor(remainder_duration / TimeSpan.MINUTE);
|
||||||
|
remainder_duration -= minutes * TimeSpan.MINUTE;
|
||||||
|
|
||||||
|
string ret = "";
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
ret += n("%i hour", "%i hours", hours).printf(hours);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minutes > 0) {
|
||||||
|
if (ret.length > 0) {
|
||||||
|
ret += " ";
|
||||||
|
}
|
||||||
|
ret += n("%i minute", "%i minutes", minutes).printf(minutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret.length > 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _("seconds");
|
||||||
|
}
|
||||||
|
|
||||||
|
private int get_next_time_change() {
|
||||||
|
DateTime now = new DateTime.now_local();
|
||||||
|
DateTime item_time = call.local_time;
|
||||||
|
|
||||||
|
if (now.get_second() < item_time.get_second()) {
|
||||||
|
return item_time.get_second() - now.get_second();
|
||||||
|
} else {
|
||||||
|
return 60 - (now.get_second() - item_time.get_second());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void dispose() {
|
||||||
|
base.dispose();
|
||||||
|
|
||||||
|
if (time_update_handler_id != 0) {
|
||||||
|
Source.remove(time_update_handler_id);
|
||||||
|
time_update_handler_id = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -68,7 +68,10 @@ public class ContentProvider : ContentItemCollection, Object {
|
||||||
return new MessageMetaItem(content_item, stream_interactor);
|
return new MessageMetaItem(content_item, stream_interactor);
|
||||||
} else if (content_item.type_ == FileItem.TYPE) {
|
} else if (content_item.type_ == FileItem.TYPE) {
|
||||||
return new FileMetaItem(content_item, stream_interactor);
|
return new FileMetaItem(content_item, stream_interactor);
|
||||||
|
} else if (content_item.type_ == CallItem.TYPE) {
|
||||||
|
return new CallMetaItem(content_item, stream_interactor);
|
||||||
}
|
}
|
||||||
|
critical("Got unknown content item type %s", content_item.type_);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,6 +88,7 @@ public abstract class ContentMetaItem : Plugins.MetaConversationItem {
|
||||||
this.mark = content_item.mark;
|
this.mark = content_item.mark;
|
||||||
|
|
||||||
content_item.bind_property("mark", this, "mark");
|
content_item.bind_property("mark", this, "mark");
|
||||||
|
content_item.bind_property("encryption", this, "encryption");
|
||||||
|
|
||||||
this.can_merge = true;
|
this.can_merge = true;
|
||||||
this.requires_avatar = true;
|
this.requires_avatar = true;
|
||||||
|
|
|
@ -104,7 +104,7 @@ public class ItemMetaDataHeader : Box {
|
||||||
[GtkChild] public Label dot_label;
|
[GtkChild] public Label dot_label;
|
||||||
[GtkChild] public Label time_label;
|
[GtkChild] public Label time_label;
|
||||||
public Image received_image = new Image() { opacity=0.4 };
|
public Image received_image = new Image() { opacity=0.4 };
|
||||||
public Image? unencrypted_image = null;
|
public Widget? encryption_image = null;
|
||||||
|
|
||||||
public static IconSize ICON_SIZE_HEADER = Gtk.icon_size_register("im.dino.Dino.HEADER_ICON", 17, 12);
|
public static IconSize ICON_SIZE_HEADER = Gtk.icon_size_register("im.dino.Dino.HEADER_ICON", 17, 12);
|
||||||
|
|
||||||
|
@ -124,27 +124,9 @@ public class ItemMetaDataHeader : Box {
|
||||||
update_name_label();
|
update_name_label();
|
||||||
name_label.style_updated.connect(update_name_label);
|
name_label.style_updated.connect(update_name_label);
|
||||||
|
|
||||||
Application app = GLib.Application.get_default() as Application;
|
conversation.notify["encryption"].connect(update_unencrypted_icon);
|
||||||
|
item.notify["encryption"].connect(update_encryption_icon);
|
||||||
ContentMetaItem ci = item as ContentMetaItem;
|
update_encryption_icon();
|
||||||
if (ci != null) {
|
|
||||||
foreach(var e in app.plugin_registry.encryption_list_entries) {
|
|
||||||
if (e.encryption == item.encryption) {
|
|
||||||
Object? w = e.get_encryption_icon(conversation, ci.content_item);
|
|
||||||
if (w != null) {
|
|
||||||
this.add(w as Widget);
|
|
||||||
} else {
|
|
||||||
Image image = new Image.from_icon_name("dino-changes-prevent-symbolic", ICON_SIZE_HEADER) { opacity=0.4, visible = true };
|
|
||||||
this.add(image);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (item.encryption == Encryption.NONE) {
|
|
||||||
conversation.notify["encryption"].connect(update_unencrypted_icon);
|
|
||||||
update_unencrypted_icon();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.add(received_image);
|
this.add(received_image);
|
||||||
|
|
||||||
|
@ -157,17 +139,51 @@ public class ItemMetaDataHeader : Box {
|
||||||
update_received_mark();
|
update_received_mark();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void update_encryption_icon() {
|
||||||
|
Application app = GLib.Application.get_default() as Application;
|
||||||
|
|
||||||
|
ContentMetaItem ci = item as ContentMetaItem;
|
||||||
|
if (item.encryption != Encryption.NONE && ci != null) {
|
||||||
|
Widget? widget = null;
|
||||||
|
foreach(var e in app.plugin_registry.encryption_list_entries) {
|
||||||
|
if (e.encryption == item.encryption) {
|
||||||
|
widget = e.get_encryption_icon(conversation, ci.content_item) as Widget;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (widget == null) {
|
||||||
|
widget = new Image.from_icon_name("dino-changes-prevent-symbolic", ICON_SIZE_HEADER) { opacity=0.4, visible = true };
|
||||||
|
}
|
||||||
|
update_encryption_image(widget);
|
||||||
|
}
|
||||||
|
if (item.encryption == Encryption.NONE) {
|
||||||
|
update_unencrypted_icon();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void update_unencrypted_icon() {
|
private void update_unencrypted_icon() {
|
||||||
if (conversation.encryption != Encryption.NONE && unencrypted_image == null) {
|
if (item.encryption != Encryption.NONE) return;
|
||||||
unencrypted_image = new Image() { opacity=0.4, visible = true };
|
|
||||||
unencrypted_image.set_from_icon_name("dino-changes-allowed-symbolic", ICON_SIZE_HEADER);
|
if (conversation.encryption != Encryption.NONE && encryption_image == null) {
|
||||||
unencrypted_image.tooltip_text = _("Unencrypted");
|
Image image = new Image() { opacity=0.4, visible = true };
|
||||||
this.add(unencrypted_image);
|
image.set_from_icon_name("dino-changes-allowed-symbolic", ICON_SIZE_HEADER);
|
||||||
this.reorder_child(unencrypted_image, 3);
|
image.tooltip_text = _("Unencrypted");
|
||||||
Util.force_error_color(unencrypted_image);
|
update_encryption_image(image);
|
||||||
} else if (conversation.encryption == Encryption.NONE && unencrypted_image != null) {
|
Util.force_error_color(image);
|
||||||
this.remove(unencrypted_image);
|
} else if (conversation.encryption == Encryption.NONE && encryption_image != null) {
|
||||||
unencrypted_image = null;
|
update_encryption_image(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update_encryption_image(Widget? widget) {
|
||||||
|
if (encryption_image != null) {
|
||||||
|
this.remove(encryption_image);
|
||||||
|
encryption_image = null;
|
||||||
|
}
|
||||||
|
if (widget != null) {
|
||||||
|
this.add(widget);
|
||||||
|
this.reorder_child(widget, 3);
|
||||||
|
encryption_image = widget;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,9 +32,6 @@ public class FileWidget : SizeRequestBox {
|
||||||
DEFAULT
|
DEFAULT
|
||||||
}
|
}
|
||||||
|
|
||||||
private const int MAX_HEIGHT = 300;
|
|
||||||
private const int MAX_WIDTH = 600;
|
|
||||||
|
|
||||||
private StreamInteractor stream_interactor;
|
private StreamInteractor stream_interactor;
|
||||||
private FileTransfer file_transfer;
|
private FileTransfer file_transfer;
|
||||||
public FileTransfer.State file_transfer_state { get; set; }
|
public FileTransfer.State file_transfer_state { get; set; }
|
||||||
|
|
|
@ -198,6 +198,14 @@ public class ConversationSelectorRow : ListBoxRow {
|
||||||
message_label.label = (file_is_image ? _("Image received") : _("File received") );
|
message_label.label = (file_is_image ? _("Image received") : _("File received") );
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case CallItem.TYPE:
|
||||||
|
CallItem call_item = (CallItem) last_content_item;
|
||||||
|
Call call = call_item.call;
|
||||||
|
|
||||||
|
nick_label.label = call.direction == Call.DIRECTION_OUTGOING ? _("Me") + ": " : "";
|
||||||
|
message_label.attributes.insert(attr_style_new(Pango.Style.ITALIC));
|
||||||
|
message_label.label = call.direction == Call.DIRECTION_OUTGOING ? _("Outgoing call") : _("Incoming call");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
nick_label.visible = true;
|
nick_label.visible = true;
|
||||||
message_label.visible = true;
|
message_label.visible = true;
|
||||||
|
|
132
main/src/ui/conversation_titlebar/call_entry.vala
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
using Xmpp;
|
||||||
|
using Gtk;
|
||||||
|
using Gee;
|
||||||
|
|
||||||
|
using Dino.Entities;
|
||||||
|
|
||||||
|
namespace Dino.Ui {
|
||||||
|
|
||||||
|
public class CallTitlebarEntry : Plugins.ConversationTitlebarEntry, Object {
|
||||||
|
public string id { get { return "call"; } }
|
||||||
|
|
||||||
|
public CallButton call_button;
|
||||||
|
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
|
||||||
|
public CallTitlebarEntry(StreamInteractor stream_interactor) {
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
|
||||||
|
call_button = new CallButton(stream_interactor) { tooltip_text=_("Start call") };
|
||||||
|
call_button.set_image(new Gtk.Image.from_icon_name("dino-phone-symbolic", Gtk.IconSize.MENU) { visible=true });
|
||||||
|
}
|
||||||
|
|
||||||
|
public double order { get { return 4; } }
|
||||||
|
public Plugins.ConversationTitlebarWidget? get_widget(Plugins.WidgetType type) {
|
||||||
|
if (type == Plugins.WidgetType.GTK) {
|
||||||
|
return call_button;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CallButton : Plugins.ConversationTitlebarWidget, Gtk.MenuButton {
|
||||||
|
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
private Conversation conversation;
|
||||||
|
|
||||||
|
private ModelButton audio_button = new ModelButton() { text="Audio call", visible=true };
|
||||||
|
private ModelButton video_button = new ModelButton() { text="Video call", visible=true };
|
||||||
|
|
||||||
|
public CallButton(StreamInteractor stream_interactor) {
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
|
||||||
|
use_popover = true;
|
||||||
|
image = new Gtk.Image.from_icon_name("dino-phone-symbolic", Gtk.IconSize.MENU) { visible=true };
|
||||||
|
|
||||||
|
Gtk.PopoverMenu popover_menu = new Gtk.PopoverMenu();
|
||||||
|
Box box = new Box(Orientation.VERTICAL, 0) { margin=10, visible=true };
|
||||||
|
audio_button.clicked.connect(() => {
|
||||||
|
stream_interactor.get_module(Calls.IDENTITY).initiate_call.begin(conversation, false, (_, res) => {
|
||||||
|
Call call = stream_interactor.get_module(Calls.IDENTITY).initiate_call.end(res);
|
||||||
|
open_call_window(call);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
box.add(audio_button);
|
||||||
|
|
||||||
|
video_button.clicked.connect(() => {
|
||||||
|
stream_interactor.get_module(Calls.IDENTITY).initiate_call.begin(conversation, true, (_, res) => {
|
||||||
|
Call call = stream_interactor.get_module(Calls.IDENTITY).initiate_call.end(res);
|
||||||
|
open_call_window(call);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
box.add(video_button);
|
||||||
|
popover_menu.add(box);
|
||||||
|
|
||||||
|
popover = popover_menu;
|
||||||
|
|
||||||
|
clicked.connect(() => {
|
||||||
|
popover_menu.visible = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
stream_interactor.get_module(Calls.IDENTITY).call_incoming.connect((call, conversation) => {
|
||||||
|
update_button_state();
|
||||||
|
});
|
||||||
|
|
||||||
|
stream_interactor.get_module(Calls.IDENTITY).call_terminated.connect((call) => {
|
||||||
|
update_button_state();
|
||||||
|
});
|
||||||
|
stream_interactor.get_module(PresenceManager.IDENTITY).show_received.connect((jid, account) => {
|
||||||
|
if (this.conversation.counterpart.equals_bare(jid) && this.conversation.account.equals(account)) {
|
||||||
|
update_visibility.begin();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stream_interactor.connection_manager.connection_state_changed.connect((account, state) => {
|
||||||
|
update_visibility.begin();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void open_call_window(Call call) {
|
||||||
|
var call_window = new CallWindow();
|
||||||
|
var call_controller = new CallWindowController(call_window, call, stream_interactor);
|
||||||
|
call_window.controller = call_controller;
|
||||||
|
call_window.present();
|
||||||
|
|
||||||
|
update_button_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
public new void set_conversation(Conversation conversation) {
|
||||||
|
this.conversation = conversation;
|
||||||
|
|
||||||
|
update_visibility.begin();
|
||||||
|
update_button_state();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void update_button_state() {
|
||||||
|
Jid? call_counterpart = stream_interactor.get_module(Calls.IDENTITY).is_call_in_progress();
|
||||||
|
this.sensitive = call_counterpart == null;
|
||||||
|
|
||||||
|
if (call_counterpart != null && call_counterpart.equals_bare(conversation.counterpart)) {
|
||||||
|
this.set_image(new Gtk.Image.from_icon_name("dino-phone-in-talk-symbolic", Gtk.IconSize.MENU) { visible=true });
|
||||||
|
} else {
|
||||||
|
this.set_image(new Gtk.Image.from_icon_name("dino-phone-symbolic", Gtk.IconSize.MENU) { visible=true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void update_visibility() {
|
||||||
|
if (conversation.type_ == Conversation.Type.CHAT) {
|
||||||
|
Conversation conv_bak = conversation;
|
||||||
|
bool audio_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_audio_calls_async(conversation);
|
||||||
|
bool video_works = yield stream_interactor.get_module(Calls.IDENTITY).can_do_video_calls_async(conversation);
|
||||||
|
if (conv_bak != conversation) return;
|
||||||
|
|
||||||
|
visible = audio_works;
|
||||||
|
video_button.visible = video_works;
|
||||||
|
} else {
|
||||||
|
visible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public new void unset_conversation() { }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -87,6 +87,7 @@ public class ConversationViewController : Object {
|
||||||
app.plugin_registry.register_contact_titlebar_entry(new MenuEntry(stream_interactor));
|
app.plugin_registry.register_contact_titlebar_entry(new MenuEntry(stream_interactor));
|
||||||
app.plugin_registry.register_contact_titlebar_entry(search_menu_entry);
|
app.plugin_registry.register_contact_titlebar_entry(search_menu_entry);
|
||||||
app.plugin_registry.register_contact_titlebar_entry(new OccupantsEntry(stream_interactor));
|
app.plugin_registry.register_contact_titlebar_entry(new OccupantsEntry(stream_interactor));
|
||||||
|
app.plugin_registry.register_contact_titlebar_entry(new CallTitlebarEntry(stream_interactor));
|
||||||
foreach(var entry in app.plugin_registry.conversation_titlebar_entries) {
|
foreach(var entry in app.plugin_registry.conversation_titlebar_entries) {
|
||||||
titlebar.insert_entry(entry);
|
titlebar.insert_entry(entry);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object {
|
||||||
private HashMap<Conversation, uint32> content_notifications = new HashMap<Conversation, uint32>(Conversation.hash_func, Conversation.equals_func);
|
private HashMap<Conversation, uint32> content_notifications = new HashMap<Conversation, uint32>(Conversation.hash_func, Conversation.equals_func);
|
||||||
private HashMap<Conversation, Gee.List<uint32>> conversation_notifications = new HashMap<Conversation, Gee.List<uint32>>(Conversation.hash_func, Conversation.equals_func);
|
private HashMap<Conversation, Gee.List<uint32>> conversation_notifications = new HashMap<Conversation, Gee.List<uint32>>(Conversation.hash_func, Conversation.equals_func);
|
||||||
private HashMap<uint32, HashMap<string, ListenerFuncWrapper>> action_listeners = new HashMap<uint32, HashMap<string, ListenerFuncWrapper>>();
|
private HashMap<uint32, HashMap<string, ListenerFuncWrapper>> action_listeners = new HashMap<uint32, HashMap<string, ListenerFuncWrapper>>();
|
||||||
|
private HashMap<Call, uint32> call_notifications = new HashMap<Call, uint32>(Call.hash_func, Call.equals_func);
|
||||||
|
|
||||||
private FreeDesktopNotifier(StreamInteractor stream_interactor) {
|
private FreeDesktopNotifier(StreamInteractor stream_interactor) {
|
||||||
this.stream_interactor = stream_interactor;
|
this.stream_interactor = stream_interactor;
|
||||||
|
@ -110,6 +111,43 @@ public class Dino.Ui.FreeDesktopNotifier : NotificationProvider, Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async void notify_call(Call call, Conversation conversation, bool video, string conversation_display_name) {
|
||||||
|
string summary = Markup.escape_text(conversation_display_name);
|
||||||
|
string body = video ? _("Incoming video call") : _("Incoming call");
|
||||||
|
|
||||||
|
HashTable<string, Variant> hash_table = new HashTable<string, Variant>(null, null);
|
||||||
|
hash_table["image-path"] = "call-start-symbolic";
|
||||||
|
hash_table["sound-name"] = new Variant.string("phone-incoming-call");
|
||||||
|
hash_table["urgency"] = new Variant.byte(2);
|
||||||
|
string[] actions = new string[] {"default", "Open conversation", "reject", _("Reject"), "accept", _("Accept")};
|
||||||
|
try {
|
||||||
|
uint32 notification_id = dbus_notifications.notify("Dino", 0, "", summary, body, actions, hash_table, 0);
|
||||||
|
call_notifications[call] = notification_id;
|
||||||
|
|
||||||
|
add_action_listener(notification_id, "default", () => {
|
||||||
|
GLib.Application.get_default().activate_action("open-conversation", new Variant.int32(conversation.id));
|
||||||
|
});
|
||||||
|
add_action_listener(notification_id, "reject", () => {
|
||||||
|
GLib.Application.get_default().activate_action("deny-call", new Variant.int32(call.id));
|
||||||
|
});
|
||||||
|
add_action_listener(notification_id, "accept", () => {
|
||||||
|
GLib.Application.get_default().activate_action("accept-call", new Variant.int32(call.id));
|
||||||
|
});
|
||||||
|
} catch (Error e) {
|
||||||
|
warning("Failed showing subscription request notification: %s", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void retract_call_notification(Call call, Conversation conversation) {
|
||||||
|
if (!call_notifications.has_key(call)) return;
|
||||||
|
uint32 notification_id = call_notifications[call];
|
||||||
|
try {
|
||||||
|
dbus_notifications.close_notification(notification_id);
|
||||||
|
action_listeners.unset(notification_id);
|
||||||
|
call_notifications.unset(call);
|
||||||
|
} catch (Error e) { }
|
||||||
|
}
|
||||||
|
|
||||||
public async void notify_subscription_request(Conversation conversation) {
|
public async void notify_subscription_request(Conversation conversation) {
|
||||||
string summary = _("Subscription request");
|
string summary = _("Subscription request");
|
||||||
string body = Markup.escape_text(conversation.counterpart.to_string());
|
string body = Markup.escape_text(conversation.counterpart.to_string());
|
||||||
|
|
|
@ -65,6 +65,25 @@ namespace Dino.Ui {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async void notify_call(Call call, Conversation conversation, bool video, string conversation_display_name) {
|
||||||
|
Notification notification = new Notification(conversation_display_name);
|
||||||
|
string body = _("Incoming call");
|
||||||
|
notification.set_body(body);
|
||||||
|
notification.set_urgent(true);
|
||||||
|
|
||||||
|
notification.set_icon(new ThemedIcon.from_names(new string[] {"call-start-symbolic"}));
|
||||||
|
|
||||||
|
notification.set_default_action_and_target_value("app.open-conversation", new Variant.int32(conversation.id));
|
||||||
|
notification.add_button_with_target_value(_("Deny"), "app.deny-call", new Variant.int32(call.id));
|
||||||
|
notification.add_button_with_target_value(_("Accept"), "app.accept-call", new Variant.int32(call.id));
|
||||||
|
|
||||||
|
GLib.Application.get_default().send_notification(call.id.to_string(), notification);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void retract_call_notification(Call call, Conversation conversation) {
|
||||||
|
GLib.Application.get_default().withdraw_notification(call.id.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
public async void notify_subscription_request(Conversation conversation) {
|
public async void notify_subscription_request(Conversation conversation) {
|
||||||
Notification notification = new Notification(_("Subscription request"));
|
Notification notification = new Notification(_("Subscription request"));
|
||||||
notification.set_body(conversation.counterpart.to_string());
|
notification.set_body(conversation.counterpart.to_string());
|
||||||
|
|
|
@ -122,15 +122,15 @@ public static string get_participant_display_name(StreamInteractor stream_intera
|
||||||
return Dino.get_participant_display_name(stream_interactor, conversation, participant, me_is_me ? _("Me") : null);
|
return Dino.get_participant_display_name(stream_interactor, conversation, participant, me_is_me ? _("Me") : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? get_real_display_name(StreamInteractor stream_interactor, Account account, Jid jid, bool me_is_me = false) {
|
public static string? get_real_display_name(StreamInteractor stream_interactor, Account account, Jid jid, bool me_is_me = false) {
|
||||||
return Dino.get_real_display_name(stream_interactor, account, jid, me_is_me ? _("Me") : null);
|
return Dino.get_real_display_name(stream_interactor, account, jid, me_is_me ? _("Me") : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string get_groupchat_display_name(StreamInteractor stream_interactor, Account account, Jid jid) {
|
public static string get_groupchat_display_name(StreamInteractor stream_interactor, Account account, Jid jid) {
|
||||||
return Dino.get_groupchat_display_name(stream_interactor, account, jid);
|
return Dino.get_groupchat_display_name(stream_interactor, account, jid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string get_occupant_display_name(StreamInteractor stream_interactor, Conversation conversation, Jid jid, bool me_is_me = false, bool muc_real_name = false) {
|
public static string get_occupant_display_name(StreamInteractor stream_interactor, Conversation conversation, Jid jid, bool me_is_me = false, bool muc_real_name = false) {
|
||||||
return Dino.get_occupant_display_name(stream_interactor, conversation, jid, me_is_me ? _("Me") : null);
|
return Dino.get_occupant_display_name(stream_interactor, conversation, jid, me_is_me ? _("Me") : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,6 +194,15 @@ public static bool is_24h_format() {
|
||||||
return is24h == 1;
|
return is24h == 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static string format_time(DateTime datetime, string format_24h, string format_12h) {
|
||||||
|
string format = Util.is_24h_format() ? format_24h : format_12h;
|
||||||
|
if (!get_charset(null)) {
|
||||||
|
// No UTF-8 support, use simple colon for time instead
|
||||||
|
format = format.replace("∶", ":");
|
||||||
|
}
|
||||||
|
return datetime.format(format);
|
||||||
|
}
|
||||||
|
|
||||||
public static Regex get_url_regex() {
|
public static Regex get_url_regex() {
|
||||||
if (URL_REGEX == null) {
|
if (URL_REGEX == null) {
|
||||||
URL_REGEX = /\b(((http|ftp)s?:\/\/|(ircs?|xmpp|mailto|sms|smsto|mms|tel|geo|openpgp4fpr|im|news|nntp|sip|ssh|bitcoin|sftp|magnet|vnc|urn):)\S+)/;
|
URL_REGEX = /\b(((http|ftp)s?:\/\/|(ircs?|xmpp|mailto|sms|smsto|mms|tel|geo|openpgp4fpr|im|news|nntp|sip|ssh|bitcoin|sftp|magnet|vnc|urn):)\S+)/;
|
||||||
|
|
|
@ -2,6 +2,14 @@ if(DINO_PLUGIN_ENABLED_http-files)
|
||||||
add_subdirectory(http-files)
|
add_subdirectory(http-files)
|
||||||
endif(DINO_PLUGIN_ENABLED_http-files)
|
endif(DINO_PLUGIN_ENABLED_http-files)
|
||||||
|
|
||||||
|
if(DINO_PLUGIN_ENABLED_ice)
|
||||||
|
add_subdirectory(ice)
|
||||||
|
endif(DINO_PLUGIN_ENABLED_ice)
|
||||||
|
|
||||||
|
if(DINO_PLUGIN_ENABLED_rtp)
|
||||||
|
add_subdirectory(rtp)
|
||||||
|
endif(DINO_PLUGIN_ENABLED_rtp)
|
||||||
|
|
||||||
if(DINO_PLUGIN_ENABLED_openpgp)
|
if(DINO_PLUGIN_ENABLED_openpgp)
|
||||||
add_subdirectory(gpgme-vala)
|
add_subdirectory(gpgme-vala)
|
||||||
add_subdirectory(openpgp)
|
add_subdirectory(openpgp)
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
find_package(GCrypt REQUIRED)
|
find_package(GCrypt REQUIRED)
|
||||||
|
find_package(Srtp2 REQUIRED)
|
||||||
find_packages(CRYPTO_VALA_PACKAGES REQUIRED
|
find_packages(CRYPTO_VALA_PACKAGES REQUIRED
|
||||||
GLib
|
GLib
|
||||||
GObject
|
GObject
|
||||||
|
@ -10,8 +11,11 @@ SOURCES
|
||||||
"src/cipher.vala"
|
"src/cipher.vala"
|
||||||
"src/cipher_converter.vala"
|
"src/cipher_converter.vala"
|
||||||
"src/error.vala"
|
"src/error.vala"
|
||||||
|
"src/random.vala"
|
||||||
|
"src/srtp.vala"
|
||||||
CUSTOM_VAPIS
|
CUSTOM_VAPIS
|
||||||
"${CMAKE_CURRENT_SOURCE_DIR}/vapi/gcrypt.vapi"
|
"${CMAKE_CURRENT_SOURCE_DIR}/vapi/gcrypt.vapi"
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/vapi/libsrtp2.vapi"
|
||||||
PACKAGES
|
PACKAGES
|
||||||
${CRYPTO_VALA_PACKAGES}
|
${CRYPTO_VALA_PACKAGES}
|
||||||
GENERATE_VAPI
|
GENERATE_VAPI
|
||||||
|
@ -20,9 +24,9 @@ GENERATE_HEADER
|
||||||
crypto-vala
|
crypto-vala
|
||||||
)
|
)
|
||||||
|
|
||||||
set(CFLAGS ${VALA_CFLAGS} -I${CMAKE_CURRENT_SOURCE_DIR}/src)
|
set(CFLAGS ${VALA_CFLAGS})
|
||||||
add_definitions(${CFLAGS})
|
add_definitions(${CFLAGS})
|
||||||
add_library(crypto-vala STATIC ${CRYPTO_VALA_C})
|
add_library(crypto-vala STATIC ${CRYPTO_VALA_C})
|
||||||
target_link_libraries(crypto-vala ${CRYPTO_VALA_PACKAGES} gcrypt)
|
target_link_libraries(crypto-vala ${CRYPTO_VALA_PACKAGES} gcrypt libsrtp2)
|
||||||
set_property(TARGET crypto-vala PROPERTY POSITION_INDEPENDENT_CODE ON)
|
set_property(TARGET crypto-vala PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,9 @@ namespace Crypto {
|
||||||
|
|
||||||
public errordomain Error {
|
public errordomain Error {
|
||||||
ILLEGAL_ARGUMENTS,
|
ILLEGAL_ARGUMENTS,
|
||||||
GCRYPT
|
GCRYPT,
|
||||||
|
AUTHENTICATION_FAILED,
|
||||||
|
UNKNOWN
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void may_throw_gcrypt_error(GCrypt.Error e) throws Error {
|
internal void may_throw_gcrypt_error(GCrypt.Error e) throws Error {
|
||||||
|
|
5
plugins/crypto-vala/src/random.vala
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
namespace Crypto {
|
||||||
|
public static void randomize(uint8[] buffer) {
|
||||||
|
GCrypt.Random.randomize(buffer);
|
||||||
|
}
|
||||||
|
}
|
122
plugins/crypto-vala/src/srtp.vala
Normal file
|
@ -0,0 +1,122 @@
|
||||||
|
using Srtp;
|
||||||
|
|
||||||
|
public class Crypto.Srtp {
|
||||||
|
public const string AES_CM_128_HMAC_SHA1_80 = "AES_CM_128_HMAC_SHA1_80";
|
||||||
|
public const string AES_CM_128_HMAC_SHA1_32 = "AES_CM_128_HMAC_SHA1_32";
|
||||||
|
public const string F8_128_HMAC_SHA1_80 = "F8_128_HMAC_SHA1_80";
|
||||||
|
|
||||||
|
public class Session {
|
||||||
|
public bool has_encrypt { get; private set; default = false; }
|
||||||
|
public bool has_decrypt { get; private set; default = false; }
|
||||||
|
|
||||||
|
private Context encrypt_context;
|
||||||
|
private Context decrypt_context;
|
||||||
|
|
||||||
|
static construct {
|
||||||
|
init();
|
||||||
|
install_log_handler(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void log(LogLevel level, string msg) {
|
||||||
|
print(@"SRTP[$level]: $msg\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
public Session() {
|
||||||
|
Context.create(out encrypt_context, null);
|
||||||
|
Context.create(out decrypt_context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint8[] encrypt_rtp(uint8[] data) throws Error {
|
||||||
|
uint8[] buf = new uint8[data.length + MAX_TRAILER_LEN];
|
||||||
|
Memory.copy(buf, data, data.length);
|
||||||
|
int buf_use = data.length;
|
||||||
|
ErrorStatus res = encrypt_context.protect(buf, ref buf_use);
|
||||||
|
if (res != ErrorStatus.ok) {
|
||||||
|
throw new Error.UNKNOWN(@"SRTP encrypt failed: $res");
|
||||||
|
}
|
||||||
|
uint8[] ret = new uint8[buf_use];
|
||||||
|
GLib.Memory.copy(ret, buf, buf_use);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint8[] decrypt_rtp(uint8[] data) throws Error {
|
||||||
|
uint8[] buf = new uint8[data.length];
|
||||||
|
Memory.copy(buf, data, data.length);
|
||||||
|
int buf_use = data.length;
|
||||||
|
ErrorStatus res = decrypt_context.unprotect(buf, ref buf_use);
|
||||||
|
switch (res) {
|
||||||
|
case ErrorStatus.auth_fail:
|
||||||
|
throw new Error.AUTHENTICATION_FAILED("SRTP packet failed the message authentication check");
|
||||||
|
case ErrorStatus.ok:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error.UNKNOWN(@"SRTP decrypt failed: $res");
|
||||||
|
}
|
||||||
|
uint8[] ret = new uint8[buf_use];
|
||||||
|
GLib.Memory.copy(ret, buf, buf_use);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint8[] encrypt_rtcp(uint8[] data) throws Error {
|
||||||
|
uint8[] buf = new uint8[data.length + MAX_TRAILER_LEN + 4];
|
||||||
|
Memory.copy(buf, data, data.length);
|
||||||
|
int buf_use = data.length;
|
||||||
|
ErrorStatus res = encrypt_context.protect_rtcp(buf, ref buf_use);
|
||||||
|
if (res != ErrorStatus.ok) {
|
||||||
|
throw new Error.UNKNOWN(@"SRTCP encrypt failed: $res");
|
||||||
|
}
|
||||||
|
uint8[] ret = new uint8[buf_use];
|
||||||
|
GLib.Memory.copy(ret, buf, buf_use);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint8[] decrypt_rtcp(uint8[] data) throws Error {
|
||||||
|
uint8[] buf = new uint8[data.length];
|
||||||
|
Memory.copy(buf, data, data.length);
|
||||||
|
int buf_use = data.length;
|
||||||
|
ErrorStatus res = decrypt_context.unprotect_rtcp(buf, ref buf_use);
|
||||||
|
switch (res) {
|
||||||
|
case ErrorStatus.auth_fail:
|
||||||
|
throw new Error.AUTHENTICATION_FAILED("SRTCP packet failed the message authentication check");
|
||||||
|
case ErrorStatus.ok:
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error.UNKNOWN(@"SRTP decrypt failed: $res");
|
||||||
|
}
|
||||||
|
uint8[] ret = new uint8[buf_use];
|
||||||
|
GLib.Memory.copy(ret, buf, buf_use);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Policy create_policy(string profile) {
|
||||||
|
Policy policy = Policy();
|
||||||
|
switch (profile) {
|
||||||
|
case AES_CM_128_HMAC_SHA1_80:
|
||||||
|
policy.rtp.set_aes_cm_128_hmac_sha1_80();
|
||||||
|
policy.rtcp.set_aes_cm_128_hmac_sha1_80();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return policy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_encryption_key(string profile, uint8[] key, uint8[] salt) {
|
||||||
|
Policy policy = create_policy(profile);
|
||||||
|
policy.ssrc.type = SsrcType.any_outbound;
|
||||||
|
policy.key = new uint8[key.length + salt.length];
|
||||||
|
Memory.copy(policy.key, key, key.length);
|
||||||
|
Memory.copy(((uint8*)policy.key) + key.length, salt, salt.length);
|
||||||
|
encrypt_context.add_stream(ref policy);
|
||||||
|
has_encrypt = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_decryption_key(string profile, uint8[] key, uint8[] salt) {
|
||||||
|
Policy policy = create_policy(profile);
|
||||||
|
policy.ssrc.type = SsrcType.any_inbound;
|
||||||
|
policy.key = new uint8[key.length + salt.length];
|
||||||
|
Memory.copy(policy.key, key, key.length);
|
||||||
|
Memory.copy(((uint8*)policy.key) + key.length, salt, salt.length);
|
||||||
|
decrypt_context.add_stream(ref policy);
|
||||||
|
has_decrypt = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
115
plugins/crypto-vala/vapi/libsrtp2.vapi
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
[CCode (cheader_filename = "srtp2/srtp.h")]
|
||||||
|
namespace Srtp {
|
||||||
|
public const uint MAX_TRAILER_LEN;
|
||||||
|
|
||||||
|
public static ErrorStatus init();
|
||||||
|
public static ErrorStatus shutdown();
|
||||||
|
|
||||||
|
[Compact]
|
||||||
|
[CCode (cname = "srtp_ctx_t", cprefix = "srtp_", free_function = "srtp_dealloc")]
|
||||||
|
public class Context {
|
||||||
|
public static ErrorStatus create(out Context session, Policy? policy);
|
||||||
|
|
||||||
|
public ErrorStatus protect([CCode (type = "void*", array_length = false)] uint8[] rtp, ref int len);
|
||||||
|
public ErrorStatus unprotect([CCode (type = "void*", array_length = false)] uint8[] rtp, ref int len);
|
||||||
|
|
||||||
|
public ErrorStatus protect_rtcp([CCode (type = "void*", array_length = false)] uint8[] rtcp, ref int len);
|
||||||
|
public ErrorStatus unprotect_rtcp([CCode (type = "void*", array_length = false)] uint8[] rtcp, ref int len);
|
||||||
|
|
||||||
|
public ErrorStatus add_stream(ref Policy policy);
|
||||||
|
public ErrorStatus update_stream(ref Policy policy);
|
||||||
|
public ErrorStatus remove_stream(uint ssrc);
|
||||||
|
public ErrorStatus update(ref Policy policy);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "srtp_ssrc_t")]
|
||||||
|
public struct Ssrc {
|
||||||
|
public SsrcType type;
|
||||||
|
public uint value;
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "srtp_ssrc_type_t", cprefix = "ssrc_")]
|
||||||
|
public enum SsrcType {
|
||||||
|
undefined, specific, any_inbound, any_outbound
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "srtp_policy_t", destroy_function = "")]
|
||||||
|
public struct Policy {
|
||||||
|
public Ssrc ssrc;
|
||||||
|
public CryptoPolicy rtp;
|
||||||
|
public CryptoPolicy rtcp;
|
||||||
|
[CCode (array_length = false)]
|
||||||
|
public uint8[] key;
|
||||||
|
public ulong num_master_keys;
|
||||||
|
public ulong window_size;
|
||||||
|
public int allow_repeat_tx;
|
||||||
|
[CCode (array_length_cname = "enc_xtn_hdr_count")]
|
||||||
|
public int[] enc_xtn_hdr;
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "srtp_crypto_policy_t")]
|
||||||
|
public struct CryptoPolicy {
|
||||||
|
public CipherType cipher_type;
|
||||||
|
public int cipher_key_len;
|
||||||
|
public AuthType auth_type;
|
||||||
|
public int auth_key_len;
|
||||||
|
public int auth_tag_len;
|
||||||
|
public SecurityServices sec_serv;
|
||||||
|
|
||||||
|
public void set_aes_cm_128_hmac_sha1_80();
|
||||||
|
public void set_aes_cm_128_hmac_sha1_32();
|
||||||
|
public void set_aes_cm_128_null_auth();
|
||||||
|
public void set_aes_cm_192_hmac_sha1_32();
|
||||||
|
public void set_aes_cm_192_hmac_sha1_80();
|
||||||
|
public void set_aes_cm_192_null_auth();
|
||||||
|
public void set_aes_cm_256_hmac_sha1_32();
|
||||||
|
public void set_aes_cm_256_hmac_sha1_80();
|
||||||
|
public void set_aes_cm_256_null_auth();
|
||||||
|
public void set_aes_gcm_128_16_auth();
|
||||||
|
public void set_aes_gcm_128_8_auth();
|
||||||
|
public void set_aes_gcm_128_8_only_auth();
|
||||||
|
public void set_aes_gcm_256_16_auth();
|
||||||
|
public void set_aes_gcm_256_8_auth();
|
||||||
|
public void set_aes_gcm_256_8_only_auth();
|
||||||
|
public void set_null_cipher_hmac_null();
|
||||||
|
public void set_null_cipher_hmac_sha1_80();
|
||||||
|
|
||||||
|
public void set_rtp_default();
|
||||||
|
public void set_rtcp_default();
|
||||||
|
|
||||||
|
public void set_from_profile_for_rtp(Profile profile);
|
||||||
|
public void set_from_profile_for_rtcp(Profile profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "srtp_profile_t", cprefix = "srtp_profile_")]
|
||||||
|
public enum Profile {
|
||||||
|
reserved, aes128_cm_sha1_80, aes128_cm_sha1_32, null_sha1_80, null_sha1_32, aead_aes_128_gcm, aead_aes_256_gcm
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "srtp_cipher_type_id_t")]
|
||||||
|
public struct CipherType : uint32 {}
|
||||||
|
|
||||||
|
[CCode (cname = "srtp_auth_type_id_t")]
|
||||||
|
public struct AuthType : uint32 {}
|
||||||
|
|
||||||
|
[CCode (cname = "srtp_sec_serv_t", cprefix = "sec_serv_")]
|
||||||
|
public enum SecurityServices {
|
||||||
|
none, conf, auth, conf_and_auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "srtp_err_status_t", cprefix = "srtp_err_status_", has_type_id = false)]
|
||||||
|
public enum ErrorStatus {
|
||||||
|
ok, fail, bad_param, alloc_fail, dealloc_fail, init_fail, terminus, auth_fail, cipher_fail, replay_fail, algo_fail, no_such_op, no_ctx, cant_check, key_expired, socket_err, signal_err, nonce_bad, encode_err, semaphore_err, pfkey_err, bad_mki, pkt_idx_old, pkt_idx_adv
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "srtp_log_level_t", cprefix = "srtp_log_level_", has_type_id = false)]
|
||||||
|
public enum LogLevel {
|
||||||
|
error, warning, info, debug
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "srtp_log_handler_func_t")]
|
||||||
|
public delegate void LogHandler(LogLevel level, string msg);
|
||||||
|
|
||||||
|
public static ErrorStatus install_log_handler(LogHandler func);
|
||||||
|
|
||||||
|
}
|
|
@ -81,12 +81,6 @@ public class HttpFileSender : FileSender, Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async long get_max_file_size(Account account) {
|
|
||||||
lock (max_file_sizes) {
|
|
||||||
return max_file_sizes[account];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void transfer_more_bytes(InputStream stream, Soup.MessageBody body) {
|
private static void transfer_more_bytes(InputStream stream, Soup.MessageBody body) {
|
||||||
uint8[] bytes = new uint8[4096];
|
uint8[] bytes = new uint8[4096];
|
||||||
ssize_t read = stream.read(bytes);
|
ssize_t read = stream.read(bytes);
|
||||||
|
|
36
plugins/ice/CMakeLists.txt
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
find_package(Nice 0.1.15 REQUIRED)
|
||||||
|
find_package(GnuTLS REQUIRED)
|
||||||
|
find_packages(ICE_PACKAGES REQUIRED
|
||||||
|
Gee
|
||||||
|
GLib
|
||||||
|
GModule
|
||||||
|
GObject
|
||||||
|
GTK3
|
||||||
|
)
|
||||||
|
|
||||||
|
vala_precompile(ICE_VALA_C
|
||||||
|
SOURCES
|
||||||
|
src/dtls_srtp.vala
|
||||||
|
src/module.vala
|
||||||
|
src/plugin.vala
|
||||||
|
src/transport_parameters.vala
|
||||||
|
src/util.vala
|
||||||
|
src/register_plugin.vala
|
||||||
|
CUSTOM_VAPIS
|
||||||
|
${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
|
||||||
|
${CMAKE_BINARY_DIR}/exports/dino.vapi
|
||||||
|
${CMAKE_BINARY_DIR}/exports/qlite.vapi
|
||||||
|
${CMAKE_BINARY_DIR}/exports/crypto-vala.vapi
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/vapi/nice.vapi
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/vapi/gnutls.vapi
|
||||||
|
PACKAGES
|
||||||
|
${ICE_PACKAGES}
|
||||||
|
)
|
||||||
|
|
||||||
|
add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="ice")
|
||||||
|
add_library(ice SHARED ${ICE_VALA_C})
|
||||||
|
target_link_libraries(ice libdino crypto-vala ${ICE_PACKAGES} nice gnutls)
|
||||||
|
set_target_properties(ice PROPERTIES PREFIX "")
|
||||||
|
set_target_properties(ice PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/)
|
||||||
|
|
||||||
|
install(TARGETS ice ${PLUGIN_INSTALL})
|
356
plugins/ice/src/dtls_srtp.vala
Normal file
|
@ -0,0 +1,356 @@
|
||||||
|
using GnuTLS;
|
||||||
|
|
||||||
|
namespace Dino.Plugins.Ice.DtlsSrtp {
|
||||||
|
|
||||||
|
public class CredentialsCapsule {
|
||||||
|
public uint8[] own_fingerprint;
|
||||||
|
public X509.Certificate[] own_cert;
|
||||||
|
public X509.PrivateKey private_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Handler {
|
||||||
|
|
||||||
|
public signal void send_data(uint8[] data);
|
||||||
|
|
||||||
|
public bool ready { get {
|
||||||
|
return srtp_session.has_encrypt && srtp_session.has_decrypt;
|
||||||
|
}}
|
||||||
|
|
||||||
|
public Mode mode { get; set; default = Mode.CLIENT; }
|
||||||
|
public uint8[] own_fingerprint { get; private set; }
|
||||||
|
public uint8[] peer_fingerprint { get; set; }
|
||||||
|
public string peer_fp_algo { get; set; }
|
||||||
|
|
||||||
|
private CredentialsCapsule credentials;
|
||||||
|
private Cond buffer_cond = Cond();
|
||||||
|
private Mutex buffer_mutex = Mutex();
|
||||||
|
private Gee.LinkedList<Bytes> buffer_queue = new Gee.LinkedList<Bytes>();
|
||||||
|
|
||||||
|
private bool running = false;
|
||||||
|
private bool stop = false;
|
||||||
|
private bool restart = false;
|
||||||
|
|
||||||
|
private Crypto.Srtp.Session srtp_session = new Crypto.Srtp.Session();
|
||||||
|
|
||||||
|
public Handler.with_cert(CredentialsCapsule creds) {
|
||||||
|
this.credentials = creds;
|
||||||
|
this.own_fingerprint = creds.own_fingerprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint8[]? process_incoming_data(uint component_id, uint8[] data) {
|
||||||
|
if (srtp_session.has_decrypt) {
|
||||||
|
try {
|
||||||
|
if (component_id == 1) {
|
||||||
|
if (data.length >= 2 && data[1] >= 192 && data[1] < 224) {
|
||||||
|
return srtp_session.decrypt_rtcp(data);
|
||||||
|
}
|
||||||
|
return srtp_session.decrypt_rtp(data);
|
||||||
|
}
|
||||||
|
if (component_id == 2) return srtp_session.decrypt_rtcp(data);
|
||||||
|
} catch (Error e) {
|
||||||
|
warning("%s (%d)", e.message, e.code);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (component_id == 1) {
|
||||||
|
on_data_rec(data);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint8[]? process_outgoing_data(uint component_id, uint8[] data) {
|
||||||
|
if (srtp_session.has_encrypt) {
|
||||||
|
try {
|
||||||
|
if (component_id == 1) {
|
||||||
|
if (data.length >= 2 && data[1] >= 192 && data[1] < 224) {
|
||||||
|
return srtp_session.encrypt_rtcp(data);
|
||||||
|
}
|
||||||
|
return srtp_session.encrypt_rtp(data);
|
||||||
|
}
|
||||||
|
if (component_id == 2) return srtp_session.encrypt_rtcp(data);
|
||||||
|
} catch (Error e) {
|
||||||
|
warning("%s (%d)", e.message, e.code);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void on_data_rec(owned uint8[] data) {
|
||||||
|
buffer_mutex.lock();
|
||||||
|
buffer_queue.add(new Bytes.take(data));
|
||||||
|
buffer_cond.signal();
|
||||||
|
buffer_mutex.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static CredentialsCapsule generate_credentials() throws GLib.Error {
|
||||||
|
int err = 0;
|
||||||
|
|
||||||
|
X509.PrivateKey private_key = X509.PrivateKey.create();
|
||||||
|
err = private_key.generate(PKAlgorithm.RSA, 2048);
|
||||||
|
throw_if_error(err);
|
||||||
|
|
||||||
|
var start_time = new DateTime.now_local().add_days(1);
|
||||||
|
var end_time = start_time.add_days(2);
|
||||||
|
|
||||||
|
X509.Certificate cert = X509.Certificate.create();
|
||||||
|
cert.set_key(private_key);
|
||||||
|
cert.set_version(1);
|
||||||
|
cert.set_activation_time ((time_t) start_time.to_unix ());
|
||||||
|
cert.set_expiration_time ((time_t) end_time.to_unix ());
|
||||||
|
|
||||||
|
uint32 serial = 1;
|
||||||
|
cert.set_serial(&serial, sizeof(uint32));
|
||||||
|
|
||||||
|
cert.sign(cert, private_key);
|
||||||
|
|
||||||
|
uint8[] own_fingerprint = get_fingerprint(cert, DigestAlgorithm.SHA256);
|
||||||
|
X509.Certificate[] own_cert = new X509.Certificate[] { (owned)cert };
|
||||||
|
|
||||||
|
var creds = new CredentialsCapsule();
|
||||||
|
creds.own_fingerprint = own_fingerprint;
|
||||||
|
creds.own_cert = (owned) own_cert;
|
||||||
|
creds.private_key = (owned) private_key;
|
||||||
|
|
||||||
|
return creds;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void stop_dtls_connection() {
|
||||||
|
buffer_mutex.lock();
|
||||||
|
stop = true;
|
||||||
|
buffer_cond.signal();
|
||||||
|
buffer_mutex.unlock();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Xmpp.Xep.Jingle.ContentEncryption? setup_dtls_connection() {
|
||||||
|
buffer_mutex.lock();
|
||||||
|
if (stop) {
|
||||||
|
restart = true;
|
||||||
|
buffer_mutex.unlock();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (running || ready) {
|
||||||
|
buffer_mutex.unlock();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
running = true;
|
||||||
|
restart = false;
|
||||||
|
buffer_mutex.unlock();
|
||||||
|
|
||||||
|
InitFlags server_or_client = mode == Mode.SERVER ? InitFlags.SERVER : InitFlags.CLIENT;
|
||||||
|
debug("Setting up DTLS connection. We're %s", mode.to_string());
|
||||||
|
|
||||||
|
CertificateCredentials cert_cred = CertificateCredentials.create();
|
||||||
|
int err = cert_cred.set_x509_key(credentials.own_cert, credentials.private_key);
|
||||||
|
throw_if_error(err);
|
||||||
|
|
||||||
|
Session? session = Session.create(server_or_client | InitFlags.DATAGRAM);
|
||||||
|
session.enable_heartbeat(1);
|
||||||
|
session.set_srtp_profile_direct("SRTP_AES128_CM_HMAC_SHA1_80");
|
||||||
|
session.set_credentials(GnuTLS.CredentialsType.CERTIFICATE, cert_cred);
|
||||||
|
session.server_set_request(CertificateRequest.REQUEST);
|
||||||
|
session.set_priority_from_string("NORMAL:!VERS-TLS-ALL:+VERS-DTLS-ALL:+CTYPE-CLI-X509");
|
||||||
|
|
||||||
|
session.set_transport_pointer(this);
|
||||||
|
session.set_pull_function(pull_function);
|
||||||
|
session.set_pull_timeout_function(pull_timeout_function);
|
||||||
|
session.set_push_function(push_function);
|
||||||
|
session.set_verify_function(verify_function);
|
||||||
|
|
||||||
|
Thread<int> thread = new Thread<int> (null, () => {
|
||||||
|
DateTime maximum_time = new DateTime.now_utc().add_seconds(20);
|
||||||
|
do {
|
||||||
|
err = session.handshake();
|
||||||
|
|
||||||
|
DateTime current_time = new DateTime.now_utc();
|
||||||
|
if (maximum_time.compare(current_time) < 0) {
|
||||||
|
warning("DTLS handshake timeouted");
|
||||||
|
err = ErrorCode.APPLICATION_ERROR_MIN + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (stop) {
|
||||||
|
debug("DTLS handshake stopped");
|
||||||
|
err = ErrorCode.APPLICATION_ERROR_MIN + 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} while (err < 0 && !((ErrorCode)err).is_fatal());
|
||||||
|
Idle.add(setup_dtls_connection.callback);
|
||||||
|
return err;
|
||||||
|
});
|
||||||
|
yield;
|
||||||
|
err = thread.join();
|
||||||
|
buffer_mutex.lock();
|
||||||
|
if (stop) {
|
||||||
|
stop = false;
|
||||||
|
running = false;
|
||||||
|
bool restart = restart;
|
||||||
|
buffer_mutex.unlock();
|
||||||
|
if (restart) {
|
||||||
|
debug("Restarting DTLS handshake");
|
||||||
|
return yield setup_dtls_connection();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
buffer_mutex.unlock();
|
||||||
|
if (err != ErrorCode.SUCCESS) {
|
||||||
|
warning("DTLS handshake failed: %s", ((ErrorCode)err).to_string());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8[] km = new uint8[150];
|
||||||
|
Datum? client_key, client_salt, server_key, server_salt;
|
||||||
|
session.get_srtp_keys(km, km.length, out client_key, out client_salt, out server_key, out server_salt);
|
||||||
|
if (client_key == null || client_salt == null || server_key == null || server_salt == null) {
|
||||||
|
warning("SRTP client/server key/salt null");
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("Finished DTLS connection. We're %s", mode.to_string());
|
||||||
|
if (mode == Mode.SERVER) {
|
||||||
|
srtp_session.set_encryption_key(Crypto.Srtp.AES_CM_128_HMAC_SHA1_80, server_key.extract(), server_salt.extract());
|
||||||
|
srtp_session.set_decryption_key(Crypto.Srtp.AES_CM_128_HMAC_SHA1_80, client_key.extract(), client_salt.extract());
|
||||||
|
} else {
|
||||||
|
srtp_session.set_encryption_key(Crypto.Srtp.AES_CM_128_HMAC_SHA1_80, client_key.extract(), client_salt.extract());
|
||||||
|
srtp_session.set_decryption_key(Crypto.Srtp.AES_CM_128_HMAC_SHA1_80, server_key.extract(), server_salt.extract());
|
||||||
|
}
|
||||||
|
return new Xmpp.Xep.Jingle.ContentEncryption() { encryption_ns=Xmpp.Xep.JingleIceUdp.DTLS_NS_URI, encryption_name = "DTLS-SRTP", our_key=credentials.own_fingerprint, peer_key=peer_fingerprint };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ssize_t pull_function(void* transport_ptr, uint8[] buffer) {
|
||||||
|
Handler self = transport_ptr as Handler;
|
||||||
|
|
||||||
|
self.buffer_mutex.lock();
|
||||||
|
while (self.buffer_queue.size == 0) {
|
||||||
|
self.buffer_cond.wait(self.buffer_mutex);
|
||||||
|
if (self.stop) {
|
||||||
|
self.buffer_mutex.unlock();
|
||||||
|
debug("DTLS handshake pull_function stopped");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Bytes data = self.buffer_queue.remove_at(0);
|
||||||
|
self.buffer_mutex.unlock();
|
||||||
|
|
||||||
|
uint8[] data_uint8 = Bytes.unref_to_data((owned) data);
|
||||||
|
Memory.copy(buffer, data_uint8, data_uint8.length);
|
||||||
|
|
||||||
|
// The callback should return 0 on connection termination, a positive number indicating the number of bytes received, and -1 on error.
|
||||||
|
return (ssize_t)data_uint8.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int pull_timeout_function(void* transport_ptr, uint ms) {
|
||||||
|
Handler self = transport_ptr as Handler;
|
||||||
|
|
||||||
|
int64 end_time = get_monotonic_time() + ms * 1000;
|
||||||
|
|
||||||
|
self.buffer_mutex.lock();
|
||||||
|
while (self.buffer_queue.size == 0) {
|
||||||
|
self.buffer_cond.wait_until(self.buffer_mutex, end_time);
|
||||||
|
if (self.stop) {
|
||||||
|
self.buffer_mutex.unlock();
|
||||||
|
debug("DTLS handshake pull_timeout_function stopped");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (get_monotonic_time() > end_time) {
|
||||||
|
self.buffer_mutex.unlock();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.buffer_mutex.unlock();
|
||||||
|
|
||||||
|
// The callback should return 0 on timeout, a positive number if data can be received, and -1 on error.
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ssize_t push_function(void* transport_ptr, uint8[] buffer) {
|
||||||
|
Handler self = transport_ptr as Handler;
|
||||||
|
self.send_data(buffer);
|
||||||
|
|
||||||
|
// The callback should return a positive number indicating the bytes sent, and -1 on error.
|
||||||
|
return (ssize_t)buffer.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int verify_function(Session session) {
|
||||||
|
Handler self = session.get_transport_pointer() as Handler;
|
||||||
|
try {
|
||||||
|
bool valid = self.verify_peer_cert(session);
|
||||||
|
if (!valid) {
|
||||||
|
warning("DTLS certificate invalid. Aborting handshake.");
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
} catch (Error e) {
|
||||||
|
warning("Error during DTLS certificate validation: %s. Aborting handshake.", e.message);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The callback function should return 0 for the handshake to continue or non-zero to terminate.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool verify_peer_cert(Session session) throws GLib.Error {
|
||||||
|
unowned Datum[] cert_datums = session.get_peer_certificates();
|
||||||
|
if (cert_datums.length == 0) {
|
||||||
|
warning("No peer certs");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cert_datums.length > 1) warning("More than one peer cert");
|
||||||
|
|
||||||
|
X509.Certificate peer_cert = X509.Certificate.create();
|
||||||
|
peer_cert.import(ref cert_datums[0], CertificateFormat.DER);
|
||||||
|
|
||||||
|
DigestAlgorithm algo;
|
||||||
|
switch (peer_fp_algo) {
|
||||||
|
case "sha-256":
|
||||||
|
algo = DigestAlgorithm.SHA256;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
warning("Unkown peer fingerprint algorithm: %s", peer_fp_algo);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8[] real_peer_fp = get_fingerprint(peer_cert, algo);
|
||||||
|
|
||||||
|
if (real_peer_fp.length != this.peer_fingerprint.length) {
|
||||||
|
warning("Fingerprint lengths not equal %i vs %i", real_peer_fp.length, peer_fingerprint.length);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < real_peer_fp.length; i++) {
|
||||||
|
if (real_peer_fp[i] != this.peer_fingerprint[i]) {
|
||||||
|
warning("First cert in peer cert list doesn't equal advertised one: %s vs %s", format_fingerprint(real_peer_fp), format_fingerprint(peer_fingerprint));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint8[] get_fingerprint(X509.Certificate certificate, DigestAlgorithm digest_algo) {
|
||||||
|
uint8[] buf = new uint8[512];
|
||||||
|
size_t buf_out_size = 512;
|
||||||
|
certificate.get_fingerprint(digest_algo, buf, ref buf_out_size);
|
||||||
|
|
||||||
|
uint8[] ret = new uint8[buf_out_size];
|
||||||
|
for (int i = 0; i < buf_out_size; i++) {
|
||||||
|
ret[i] = buf[i];
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string format_fingerprint(uint8[] fingerprint) {
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
for (int i = 0; i < fingerprint.length; i++) {
|
||||||
|
sb.append("%02x".printf(fingerprint[i]));
|
||||||
|
if (i < fingerprint.length - 1) {
|
||||||
|
sb.append(":");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.str;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public enum Mode {
|
||||||
|
CLIENT, SERVER
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
55
plugins/ice/src/module.vala
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
using Gee;
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
|
||||||
|
public class Dino.Plugins.Ice.Module : JingleIceUdp.Module {
|
||||||
|
|
||||||
|
public string? stun_ip = null;
|
||||||
|
public uint stun_port = 0;
|
||||||
|
public string? turn_ip = null;
|
||||||
|
public Xep.ExternalServiceDiscovery.Service? turn_service = null;
|
||||||
|
|
||||||
|
private weak Nice.Agent? agent;
|
||||||
|
private HashMap<string, DtlsSrtp.CredentialsCapsule> cerds = new HashMap<string, DtlsSrtp.CredentialsCapsule>();
|
||||||
|
|
||||||
|
private Nice.Agent get_agent() {
|
||||||
|
Nice.Agent? agent = this.agent;
|
||||||
|
if (agent == null) {
|
||||||
|
agent = new Nice.Agent(MainContext.@default(), Nice.Compatibility.RFC5245);
|
||||||
|
if (stun_ip != null) {
|
||||||
|
agent.stun_server = stun_ip;
|
||||||
|
agent.stun_server_port = stun_port;
|
||||||
|
}
|
||||||
|
agent.ice_tcp = false;
|
||||||
|
agent.set_software("Dino");
|
||||||
|
agent.weak_ref(agent_unweak);
|
||||||
|
this.agent = agent;
|
||||||
|
debug("STUN server for libnice %s %u", agent.stun_server, agent.stun_server_port);
|
||||||
|
}
|
||||||
|
return agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Jingle.TransportParameters create_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid) {
|
||||||
|
DtlsSrtp.CredentialsCapsule? cred = get_create_credentials(local_full_jid, peer_full_jid);
|
||||||
|
return new TransportParameters(get_agent(), cred, turn_service, turn_ip, components, local_full_jid, peer_full_jid);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Jingle.TransportParameters parse_transport_parameters(XmppStream stream, uint8 components, Jid local_full_jid, Jid peer_full_jid, StanzaNode transport) throws Jingle.IqError {
|
||||||
|
DtlsSrtp.CredentialsCapsule? cred = get_create_credentials(local_full_jid, peer_full_jid);
|
||||||
|
return new TransportParameters(get_agent(), cred, turn_service, turn_ip, components, local_full_jid, peer_full_jid, transport);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DtlsSrtp.CredentialsCapsule? get_create_credentials(Jid local_full_jid, Jid peer_full_jid) {
|
||||||
|
string from_to_id = local_full_jid.to_string() + peer_full_jid.to_string();
|
||||||
|
try {
|
||||||
|
if (!cerds.has_key(from_to_id)) cerds[from_to_id] = DtlsSrtp.Handler.generate_credentials();
|
||||||
|
} catch (Error e) {
|
||||||
|
warning("Error creating dtls credentials: %s", e.message);
|
||||||
|
}
|
||||||
|
return cerds[from_to_id];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void agent_unweak() {
|
||||||
|
this.agent = null;
|
||||||
|
}
|
||||||
|
}
|
71
plugins/ice/src/plugin.vala
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
using Gee;
|
||||||
|
using Dino.Entities;
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
|
||||||
|
private extern const size_t NICE_ADDRESS_STRING_LEN;
|
||||||
|
|
||||||
|
public class Dino.Plugins.Ice.Plugin : RootInterface, Object {
|
||||||
|
public Dino.Application app;
|
||||||
|
|
||||||
|
public void registered(Dino.Application app) {
|
||||||
|
Nice.debug_enable(true);
|
||||||
|
this.app = app;
|
||||||
|
app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => {
|
||||||
|
list.add(new Module());
|
||||||
|
});
|
||||||
|
app.stream_interactor.stream_attached_modules.connect((account, stream) => {
|
||||||
|
stream.get_module(Socks5Bytestreams.Module.IDENTITY).set_local_ip_address_handler(get_local_ip_addresses);
|
||||||
|
});
|
||||||
|
app.stream_interactor.stream_negotiated.connect(on_stream_negotiated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void on_stream_negotiated(Account account, XmppStream stream) {
|
||||||
|
Module? ice_udp_module = stream.get_module(JingleIceUdp.Module.IDENTITY) as Module;
|
||||||
|
if (ice_udp_module == null) return;
|
||||||
|
Gee.List<Xep.ExternalServiceDiscovery.Service> services = yield ExternalServiceDiscovery.request_services(stream);
|
||||||
|
foreach (Xep.ExternalServiceDiscovery.Service service in services) {
|
||||||
|
if (service.transport == "udp" && (service.ty == "stun" || service.ty == "turn")) {
|
||||||
|
InetAddress ip = yield lookup_ipv4_addess(service.host);
|
||||||
|
if (ip == null) continue;
|
||||||
|
|
||||||
|
if (service.ty == "stun") {
|
||||||
|
debug("Server offers STUN server: %s:%u, resolved to %s", service.host, service.port, ip.to_string());
|
||||||
|
ice_udp_module.stun_ip = ip.to_string();
|
||||||
|
ice_udp_module.stun_port = service.port;
|
||||||
|
} else if (service.ty == "turn") {
|
||||||
|
debug("Server offers TURN server: %s:%u, resolved to %s", service.host, service.port, ip.to_string());
|
||||||
|
ice_udp_module.turn_ip = ip.to_string();
|
||||||
|
ice_udp_module.turn_service = service;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ice_udp_module.stun_ip == null) {
|
||||||
|
InetAddress ip = yield lookup_ipv4_addess("stun.l.google.com");
|
||||||
|
if (ip == null) return;
|
||||||
|
|
||||||
|
debug("Using fallback STUN server: stun.l.google.com:19302, resolved to %s", ip.to_string());
|
||||||
|
|
||||||
|
ice_udp_module.stun_ip = ip.to_string();
|
||||||
|
ice_udp_module.stun_port = 19302;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
|
|
||||||
|
private async InetAddress? lookup_ipv4_addess(string host) {
|
||||||
|
try {
|
||||||
|
Resolver resolver = Resolver.get_default();
|
||||||
|
GLib.List<GLib.InetAddress>? ips = yield resolver.lookup_by_name_async(host);
|
||||||
|
foreach (GLib.InetAddress ina in ips) {
|
||||||
|
if (ina.get_family() != SocketFamily.IPV4) continue;
|
||||||
|
return ina;
|
||||||
|
}
|
||||||
|
} catch (Error e) {
|
||||||
|
warning("Failed looking up IP address of %s", host);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
3
plugins/ice/src/register_plugin.vala
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
public Type register_plugin(Module module) {
|
||||||
|
return typeof (Dino.Plugins.Ice.Plugin);
|
||||||
|
}
|
345
plugins/ice/src/transport_parameters.vala
Normal file
|
@ -0,0 +1,345 @@
|
||||||
|
using Gee;
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
|
||||||
|
|
||||||
|
public class Dino.Plugins.Ice.TransportParameters : JingleIceUdp.IceUdpTransportParameters {
|
||||||
|
private Nice.Agent agent;
|
||||||
|
private uint stream_id;
|
||||||
|
private bool we_want_connection;
|
||||||
|
private bool remote_credentials_set;
|
||||||
|
private Map<uint8, DatagramConnection> connections = new HashMap<uint8, DatagramConnection>();
|
||||||
|
private DtlsSrtp.Handler? dtls_srtp_handler;
|
||||||
|
|
||||||
|
private class DatagramConnection : Jingle.DatagramConnection {
|
||||||
|
private Nice.Agent agent;
|
||||||
|
private DtlsSrtp.Handler? dtls_srtp_handler;
|
||||||
|
private uint stream_id;
|
||||||
|
private string? error;
|
||||||
|
private ulong sent;
|
||||||
|
private ulong sent_reported;
|
||||||
|
private ulong recv;
|
||||||
|
private ulong recv_reported;
|
||||||
|
private ulong datagram_received_id;
|
||||||
|
|
||||||
|
public DatagramConnection(Nice.Agent agent, DtlsSrtp.Handler? dtls_srtp_handler, uint stream_id, uint8 component_id) {
|
||||||
|
this.agent = agent;
|
||||||
|
this.dtls_srtp_handler = dtls_srtp_handler;
|
||||||
|
this.stream_id = stream_id;
|
||||||
|
this.component_id = component_id;
|
||||||
|
this.datagram_received_id = this.datagram_received.connect((datagram) => {
|
||||||
|
recv += datagram.length;
|
||||||
|
if (recv > recv_reported + 100000) {
|
||||||
|
debug("Received %lu bytes via stream %u component %u", recv, stream_id, component_id);
|
||||||
|
recv_reported = recv;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async void terminate(bool we_terminated, string? reason_string = null, string? reason_text = null) {
|
||||||
|
yield base.terminate(we_terminated, reason_string, reason_text);
|
||||||
|
this.disconnect(datagram_received_id);
|
||||||
|
agent = null;
|
||||||
|
dtls_srtp_handler = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void send_datagram(Bytes datagram) {
|
||||||
|
if (this.agent != null && is_component_ready(agent, stream_id, component_id)) {
|
||||||
|
uint8[] encrypted_data = null;
|
||||||
|
if (dtls_srtp_handler != null) {
|
||||||
|
encrypted_data = dtls_srtp_handler.process_outgoing_data(component_id, datagram.get_data());
|
||||||
|
if (encrypted_data == null) return;
|
||||||
|
}
|
||||||
|
agent.send(stream_id, component_id, encrypted_data ?? datagram.get_data());
|
||||||
|
sent += datagram.length;
|
||||||
|
if (sent > sent_reported + 100000) {
|
||||||
|
debug("Sent %lu bytes via stream %u component %u", sent, stream_id, component_id);
|
||||||
|
sent_reported = sent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public TransportParameters(Nice.Agent agent, DtlsSrtp.CredentialsCapsule? credentials, Xep.ExternalServiceDiscovery.Service? turn_service, string? turn_ip, uint8 components, Jid local_full_jid, Jid peer_full_jid, StanzaNode? node = null) {
|
||||||
|
base(components, local_full_jid, peer_full_jid, node);
|
||||||
|
this.we_want_connection = (node == null);
|
||||||
|
this.agent = agent;
|
||||||
|
|
||||||
|
if (this.peer_fingerprint != null || !incoming) {
|
||||||
|
dtls_srtp_handler = setup_dtls(this, credentials);
|
||||||
|
own_fingerprint = dtls_srtp_handler.own_fingerprint;
|
||||||
|
if (incoming) {
|
||||||
|
own_setup = "active";
|
||||||
|
dtls_srtp_handler.mode = DtlsSrtp.Mode.CLIENT;
|
||||||
|
dtls_srtp_handler.peer_fingerprint = peer_fingerprint;
|
||||||
|
dtls_srtp_handler.peer_fp_algo = peer_fp_algo;
|
||||||
|
} else {
|
||||||
|
own_setup = "actpass";
|
||||||
|
dtls_srtp_handler.mode = DtlsSrtp.Mode.SERVER;
|
||||||
|
dtls_srtp_handler.setup_dtls_connection.begin((_, res) => {
|
||||||
|
var content_encryption = dtls_srtp_handler.setup_dtls_connection.end(res);
|
||||||
|
if (content_encryption != null) {
|
||||||
|
this.content.encryptions[content_encryption.encryption_ns] = content_encryption;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.candidate_gathering_done.connect(on_candidate_gathering_done);
|
||||||
|
agent.initial_binding_request_received.connect(on_initial_binding_request_received);
|
||||||
|
agent.component_state_changed.connect(on_component_state_changed);
|
||||||
|
agent.new_selected_pair_full.connect(on_new_selected_pair_full);
|
||||||
|
agent.new_candidate_full.connect(on_new_candidate);
|
||||||
|
|
||||||
|
agent.controlling_mode = !incoming;
|
||||||
|
stream_id = agent.add_stream(components);
|
||||||
|
|
||||||
|
if (turn_ip != null) {
|
||||||
|
for (uint8 component_id = 1; component_id <= components; component_id++) {
|
||||||
|
agent.set_relay_info(stream_id, component_id, turn_ip, turn_service.port, turn_service.username, turn_service.password, Nice.RelayType.UDP);
|
||||||
|
debug("TURN info (component %i) %s:%u", component_id, turn_ip, turn_service.port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
string ufrag;
|
||||||
|
string pwd;
|
||||||
|
agent.get_local_credentials(stream_id, out ufrag, out pwd);
|
||||||
|
init(ufrag, pwd);
|
||||||
|
|
||||||
|
for (uint8 component_id = 1; component_id <= components; component_id++) {
|
||||||
|
// We don't properly get local candidates before this call
|
||||||
|
agent.attach_recv(stream_id, component_id, MainContext.@default(), on_recv);
|
||||||
|
}
|
||||||
|
|
||||||
|
agent.gather_candidates(stream_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static DtlsSrtp.Handler setup_dtls(TransportParameters tp, DtlsSrtp.CredentialsCapsule credentials) {
|
||||||
|
var weak_self = WeakRef(tp);
|
||||||
|
DtlsSrtp.Handler dtls_srtp = new DtlsSrtp.Handler.with_cert(credentials);
|
||||||
|
dtls_srtp.send_data.connect((data) => {
|
||||||
|
TransportParameters self = (TransportParameters) weak_self.get();
|
||||||
|
if (self != null) self.agent.send(self.stream_id, 1, data);
|
||||||
|
});
|
||||||
|
return dtls_srtp;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_candidate_gathering_done(uint stream_id) {
|
||||||
|
if (stream_id != this.stream_id) return;
|
||||||
|
debug("on_candidate_gathering_done in %u", stream_id);
|
||||||
|
|
||||||
|
for (uint8 i = 1; i <= components; i++) {
|
||||||
|
foreach (unowned Nice.Candidate nc in agent.get_local_candidates(stream_id, i)) {
|
||||||
|
if (nc.transport == Nice.CandidateTransport.UDP) {
|
||||||
|
JingleIceUdp.Candidate? candidate = candidate_to_jingle(nc);
|
||||||
|
if (candidate == null) continue;
|
||||||
|
debug("Local candidate summary: %s", agent.generate_local_candidate_sdp(nc));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_new_candidate(Nice.Candidate nc) {
|
||||||
|
if (nc.stream_id != stream_id) return;
|
||||||
|
JingleIceUdp.Candidate? candidate = candidate_to_jingle(nc);
|
||||||
|
if (candidate == null) return;
|
||||||
|
|
||||||
|
if (nc.transport == Nice.CandidateTransport.UDP) {
|
||||||
|
// Execution was in the agent thread before
|
||||||
|
add_local_candidate_threadsafe(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void handle_transport_accept(StanzaNode transport) throws Jingle.IqError {
|
||||||
|
debug("on_transport_accept from %s", peer_full_jid.to_string());
|
||||||
|
base.handle_transport_accept(transport);
|
||||||
|
|
||||||
|
if (dtls_srtp_handler != null && peer_fingerprint != null) {
|
||||||
|
dtls_srtp_handler.peer_fingerprint = peer_fingerprint;
|
||||||
|
dtls_srtp_handler.peer_fp_algo = peer_fp_algo;
|
||||||
|
if (peer_setup == "passive") {
|
||||||
|
dtls_srtp_handler.mode = DtlsSrtp.Mode.CLIENT;
|
||||||
|
dtls_srtp_handler.stop_dtls_connection();
|
||||||
|
dtls_srtp_handler.setup_dtls_connection.begin((_, res) => {
|
||||||
|
var content_encryption = dtls_srtp_handler.setup_dtls_connection.end(res);
|
||||||
|
if (content_encryption != null) {
|
||||||
|
this.content.encryptions[content_encryption.encryption_ns] = content_encryption;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dtls_srtp_handler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void handle_transport_info(StanzaNode transport) throws Jingle.IqError {
|
||||||
|
debug("on_transport_info from %s", peer_full_jid.to_string());
|
||||||
|
base.handle_transport_info(transport);
|
||||||
|
|
||||||
|
if (!we_want_connection) return;
|
||||||
|
|
||||||
|
if (remote_ufrag != null && remote_pwd != null && !remote_credentials_set) {
|
||||||
|
agent.set_remote_credentials(stream_id, remote_ufrag, remote_pwd);
|
||||||
|
remote_credentials_set = true;
|
||||||
|
}
|
||||||
|
for (uint8 i = 1; i <= components; i++) {
|
||||||
|
SList<Nice.Candidate> candidates = new SList<Nice.Candidate>();
|
||||||
|
foreach (JingleIceUdp.Candidate candidate in remote_candidates) {
|
||||||
|
if (candidate.component == i) {
|
||||||
|
candidates.append(candidate_to_nice(candidate));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int new_candidates = agent.set_remote_candidates(stream_id, i, candidates);
|
||||||
|
debug("Updated to %i remote candidates for candidate %u via transport info", new_candidates, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void create_transport_connection(XmppStream stream, Jingle.Content content) {
|
||||||
|
debug("create_transport_connection: %s", content.session.sid);
|
||||||
|
debug("local_credentials: %s %s", local_ufrag, local_pwd);
|
||||||
|
debug("remote_credentials: %s %s", remote_ufrag, remote_pwd);
|
||||||
|
debug("expected incoming credentials: %s %s", local_ufrag + ":" + remote_ufrag, local_pwd);
|
||||||
|
debug("expected outgoing credentials: %s %s", remote_ufrag + ":" + local_ufrag, remote_pwd);
|
||||||
|
|
||||||
|
we_want_connection = true;
|
||||||
|
|
||||||
|
if (remote_ufrag != null && remote_pwd != null && !remote_credentials_set) {
|
||||||
|
agent.set_remote_credentials(stream_id, remote_ufrag, remote_pwd);
|
||||||
|
remote_credentials_set = true;
|
||||||
|
}
|
||||||
|
for (uint8 i = 1; i <= components; i++) {
|
||||||
|
SList<Nice.Candidate> candidates = new SList<Nice.Candidate>();
|
||||||
|
foreach (JingleIceUdp.Candidate candidate in remote_candidates) {
|
||||||
|
if (candidate.ip.has_prefix("fe80::")) continue;
|
||||||
|
if (candidate.component == i) {
|
||||||
|
candidates.append(candidate_to_nice(candidate));
|
||||||
|
debug("remote candidate: %s", agent.generate_local_candidate_sdp(candidate_to_nice(candidate)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int new_candidates = agent.set_remote_candidates(stream_id, i, candidates);
|
||||||
|
debug("Initiated component %u with %i remote candidates", i, new_candidates);
|
||||||
|
|
||||||
|
connections[i] = new DatagramConnection(agent, dtls_srtp_handler, stream_id, i);
|
||||||
|
content.set_transport_connection(connections[i], i);
|
||||||
|
}
|
||||||
|
|
||||||
|
base.create_transport_connection(stream, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_component_state_changed(uint stream_id, uint component_id, uint state) {
|
||||||
|
if (stream_id != this.stream_id) return;
|
||||||
|
debug("stream %u component %u state changed to %s", stream_id, component_id, agent.get_component_state(stream_id, component_id).to_string());
|
||||||
|
may_consider_ready(stream_id, component_id);
|
||||||
|
if (incoming && dtls_srtp_handler != null && !dtls_srtp_handler.ready && is_component_ready(agent, stream_id, component_id) && dtls_srtp_handler.mode == DtlsSrtp.Mode.CLIENT) {
|
||||||
|
dtls_srtp_handler.setup_dtls_connection.begin((_, res) => {
|
||||||
|
Jingle.ContentEncryption? encryption = dtls_srtp_handler.setup_dtls_connection.end(res);
|
||||||
|
if (encryption != null) {
|
||||||
|
this.content.encryptions[encryption.encryption_ns] = encryption;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void may_consider_ready(uint stream_id, uint component_id) {
|
||||||
|
if (stream_id != this.stream_id) return;
|
||||||
|
if (connections.has_key((uint8) component_id) && !connections[(uint8)component_id].ready && is_component_ready(agent, stream_id, component_id) && (dtls_srtp_handler == null || dtls_srtp_handler.ready)) {
|
||||||
|
connections[(uint8)component_id].ready = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_initial_binding_request_received(uint stream_id) {
|
||||||
|
if (stream_id != this.stream_id) return;
|
||||||
|
debug("initial_binding_request_received");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_new_selected_pair_full(uint stream_id, uint component_id, Nice.Candidate p1, Nice.Candidate p2) {
|
||||||
|
if (stream_id != this.stream_id) return;
|
||||||
|
debug("new_selected_pair_full %u [%s, %s]", component_id, agent.generate_local_candidate_sdp(p1), agent.generate_local_candidate_sdp(p2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_recv(Nice.Agent agent, uint stream_id, uint component_id, uint8[] data) {
|
||||||
|
if (stream_id != this.stream_id) return;
|
||||||
|
uint8[] decrypt_data = null;
|
||||||
|
if (dtls_srtp_handler != null) {
|
||||||
|
decrypt_data = dtls_srtp_handler.process_incoming_data(component_id, data);
|
||||||
|
if (decrypt_data == null) return;
|
||||||
|
}
|
||||||
|
may_consider_ready(stream_id, component_id);
|
||||||
|
if (connections.has_key((uint8) component_id)) {
|
||||||
|
if (!connections[(uint8) component_id].ready) {
|
||||||
|
debug("on_recv stream %u component %u when state %s", stream_id, component_id, agent.get_component_state(stream_id, component_id).to_string());
|
||||||
|
}
|
||||||
|
connections[(uint8) component_id].datagram_received(new Bytes(decrypt_data ?? data));
|
||||||
|
} else {
|
||||||
|
debug("on_recv stream %u component %u length %u", stream_id, component_id, data.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Nice.Candidate candidate_to_nice(JingleIceUdp.Candidate c) {
|
||||||
|
Nice.CandidateType type;
|
||||||
|
switch (c.type_) {
|
||||||
|
case JingleIceUdp.Candidate.Type.HOST: type = Nice.CandidateType.HOST; break;
|
||||||
|
case JingleIceUdp.Candidate.Type.PRFLX: type = Nice.CandidateType.PEER_REFLEXIVE; break;
|
||||||
|
case JingleIceUdp.Candidate.Type.RELAY: type = Nice.CandidateType.RELAYED; break;
|
||||||
|
case JingleIceUdp.Candidate.Type.SRFLX: type = Nice.CandidateType.SERVER_REFLEXIVE; break;
|
||||||
|
default: assert_not_reached();
|
||||||
|
}
|
||||||
|
|
||||||
|
Nice.Candidate candidate = new Nice.Candidate(type);
|
||||||
|
candidate.component_id = c.component;
|
||||||
|
char[] foundation = new char[Nice.CANDIDATE_MAX_FOUNDATION];
|
||||||
|
Memory.copy(foundation, c.foundation.data, size_t.min(c.foundation.length, Nice.CANDIDATE_MAX_FOUNDATION - 1));
|
||||||
|
candidate.foundation = foundation;
|
||||||
|
candidate.addr = Nice.Address();
|
||||||
|
candidate.addr.init();
|
||||||
|
candidate.addr.set_from_string(c.ip);
|
||||||
|
candidate.addr.set_port(c.port);
|
||||||
|
candidate.priority = c.priority;
|
||||||
|
if (c.rel_addr != null) {
|
||||||
|
candidate.base_addr = Nice.Address();
|
||||||
|
candidate.base_addr.init();
|
||||||
|
candidate.base_addr.set_from_string(c.rel_addr);
|
||||||
|
candidate.base_addr.set_port(c.rel_port);
|
||||||
|
}
|
||||||
|
candidate.transport = Nice.CandidateTransport.UDP;
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JingleIceUdp.Candidate? candidate_to_jingle(Nice.Candidate nc) {
|
||||||
|
JingleIceUdp.Candidate candidate = new JingleIceUdp.Candidate();
|
||||||
|
switch (nc.type) {
|
||||||
|
case Nice.CandidateType.HOST: candidate.type_ = JingleIceUdp.Candidate.Type.HOST; break;
|
||||||
|
case Nice.CandidateType.PEER_REFLEXIVE: candidate.type_ = JingleIceUdp.Candidate.Type.PRFLX; break;
|
||||||
|
case Nice.CandidateType.RELAYED: candidate.type_ = JingleIceUdp.Candidate.Type.RELAY; break;
|
||||||
|
case Nice.CandidateType.SERVER_REFLEXIVE: candidate.type_ = JingleIceUdp.Candidate.Type.SRFLX; break;
|
||||||
|
default: assert_not_reached();
|
||||||
|
}
|
||||||
|
candidate.component = (uint8) nc.component_id;
|
||||||
|
candidate.foundation = ((string)nc.foundation).dup();
|
||||||
|
candidate.generation = 0;
|
||||||
|
candidate.id = Random.next_int().to_string("%08x"); // TODO
|
||||||
|
|
||||||
|
char[] res = new char[NICE_ADDRESS_STRING_LEN];
|
||||||
|
nc.addr.to_string(res);
|
||||||
|
candidate.ip = (string) res;
|
||||||
|
candidate.network = 0; // TODO
|
||||||
|
candidate.port = (uint16) nc.addr.get_port();
|
||||||
|
candidate.priority = nc.priority;
|
||||||
|
candidate.protocol = "udp";
|
||||||
|
if (nc.base_addr.is_valid() && !nc.base_addr.equal(nc.addr)) {
|
||||||
|
res = new char[NICE_ADDRESS_STRING_LEN];
|
||||||
|
nc.base_addr.to_string(res);
|
||||||
|
candidate.rel_addr = (string) res;
|
||||||
|
candidate.rel_port = (uint16) nc.base_addr.get_port();
|
||||||
|
}
|
||||||
|
if (candidate.ip.has_prefix("fe80::")) return null;
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void dispose() {
|
||||||
|
base.dispose();
|
||||||
|
agent = null;
|
||||||
|
dtls_srtp_handler = null;
|
||||||
|
connections.clear();
|
||||||
|
}
|
||||||
|
}
|
18
plugins/ice/src/util.vala
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
using Gee;
|
||||||
|
|
||||||
|
namespace Dino.Plugins.Ice {
|
||||||
|
|
||||||
|
internal static bool is_component_ready(Nice.Agent agent, uint stream_id, uint component_id) {
|
||||||
|
var state = agent.get_component_state(stream_id, component_id);
|
||||||
|
return state == Nice.ComponentState.CONNECTED || state == Nice.ComponentState.READY;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal Gee.List<string> get_local_ip_addresses() {
|
||||||
|
Gee.List<string> result = new ArrayList<string>();
|
||||||
|
foreach (string ip_address in Nice.interfaces_get_local_ips(false)) {
|
||||||
|
result.add(ip_address);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
419
plugins/ice/vapi/gnutls.vapi
Normal file
|
@ -0,0 +1,419 @@
|
||||||
|
[CCode (cprefix = "gnutls_", lower_case_cprefix = "gnutls_", cheader_filename = "gnutls/gnutls.h")]
|
||||||
|
namespace GnuTLS {
|
||||||
|
|
||||||
|
public int global_init();
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_pull_func", has_target = false)]
|
||||||
|
public delegate ssize_t PullFunc(void* transport_ptr, [CCode (ctype = "void*", array_length_type="size_t")] uint8[] array);
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_pull_timeout_func", has_target = false)]
|
||||||
|
public delegate int PullTimeoutFunc(void* transport_ptr, uint ms);
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_push_func", has_target = false)]
|
||||||
|
public delegate ssize_t PushFunc(void* transport_ptr, [CCode (ctype = "void*", array_length_type="size_t")] uint8[] array);
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_certificate_verify_function", has_target = false)]
|
||||||
|
public delegate int VerifyFunc(Session session);
|
||||||
|
|
||||||
|
[Compact]
|
||||||
|
[CCode (cname = "struct gnutls_session_int", free_function = "gnutls_deinit")]
|
||||||
|
public class Session {
|
||||||
|
|
||||||
|
public static Session? create(int con_end) throws GLib.Error {
|
||||||
|
Session result;
|
||||||
|
var ret = init(out result, con_end);
|
||||||
|
throw_if_error(ret);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_init")]
|
||||||
|
private static int init(out Session session, int con_end);
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_transport_set_push_function")]
|
||||||
|
public void set_push_function(PushFunc func);
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_transport_set_pull_function")]
|
||||||
|
public void set_pull_function(PullFunc func);
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_transport_set_pull_timeout_function")]
|
||||||
|
public void set_pull_timeout_function(PullTimeoutFunc func);
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_transport_set_ptr")]
|
||||||
|
public void set_transport_pointer(void* ptr);
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_transport_get_ptr")]
|
||||||
|
public void* get_transport_pointer();
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_heartbeat_enable")]
|
||||||
|
public int enable_heartbeat(uint type);
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_certificate_server_set_request")]
|
||||||
|
public void server_set_request(CertificateRequest req);
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_credentials_set")]
|
||||||
|
public int set_credentials_(CredentialsType type, void* cred);
|
||||||
|
[CCode (cname = "gnutls_credentials_set_")]
|
||||||
|
public void set_credentials(CredentialsType type, void* cred) throws GLib.Error {
|
||||||
|
int err = set_credentials_(type, cred);
|
||||||
|
throw_if_error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_priority_set_direct")]
|
||||||
|
public int set_priority_from_string_(string priority, out unowned string err_pos = null);
|
||||||
|
[CCode (cname = "gnutls_priority_set_direct_")]
|
||||||
|
public void set_priority_from_string(string priority, out unowned string err_pos = null) throws GLib.Error {
|
||||||
|
int err = set_priority_from_string_(priority, out err_pos);
|
||||||
|
throw_if_error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_srtp_set_profile_direct")]
|
||||||
|
public int set_srtp_profile_direct_(string profiles, out unowned string err_pos = null);
|
||||||
|
[CCode (cname = "gnutls_srtp_set_profile_direct_")]
|
||||||
|
public void set_srtp_profile_direct(string profiles, out unowned string err_pos = null) throws GLib.Error {
|
||||||
|
int err = set_srtp_profile_direct_(profiles, out err_pos);
|
||||||
|
throw_if_error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_transport_set_int")]
|
||||||
|
public void transport_set_int(int fd);
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_handshake")]
|
||||||
|
public int handshake();
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_srtp_get_keys")]
|
||||||
|
public int get_srtp_keys_(void *key_material, uint32 key_material_size, out Datum client_key, out Datum client_salt, out Datum server_key, out Datum server_salt);
|
||||||
|
[CCode (cname = "gnutls_srtp_get_keys_")]
|
||||||
|
public void get_srtp_keys(void *key_material, uint32 key_material_size, out Datum client_key, out Datum client_salt, out Datum server_key, out Datum server_salt) throws GLib.Error {
|
||||||
|
get_srtp_keys_(key_material, key_material_size, out client_key, out client_salt, out server_key, out server_salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_certificate_get_peers", array_length_type = "unsigned int")]
|
||||||
|
public unowned Datum[]? get_peer_certificates();
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_session_set_verify_function")]
|
||||||
|
public void set_verify_function(VerifyFunc func);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Compact]
|
||||||
|
[CCode (cname = "struct gnutls_certificate_credentials_st", free_function = "gnutls_certificate_free_credentials", cprefix = "gnutls_certificate_")]
|
||||||
|
public class CertificateCredentials {
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_certificate_allocate_credentials")]
|
||||||
|
private static int allocate(out CertificateCredentials credentials);
|
||||||
|
|
||||||
|
public static CertificateCredentials create() throws GLib.Error {
|
||||||
|
CertificateCredentials result;
|
||||||
|
var ret = allocate (out result);
|
||||||
|
throw_if_error(ret);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void get_x509_crt(uint index, [CCode (array_length_type = "unsigned int")] out unowned X509.Certificate[] x509_ca_list);
|
||||||
|
|
||||||
|
public int set_x509_key(X509.Certificate[] cert_list, X509.PrivateKey key);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cheader_filename = "gnutls/x509.h", cprefix = "GNUTLS_")]
|
||||||
|
namespace X509 {
|
||||||
|
|
||||||
|
[Compact]
|
||||||
|
[CCode (cname = "struct gnutls_x509_crt_int", cprefix = "gnutls_x509_crt_", free_function = "gnutls_x509_crt_deinit")]
|
||||||
|
public class Certificate {
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_x509_crt_init")]
|
||||||
|
private static int init (out Certificate cert);
|
||||||
|
public static Certificate create() throws GLib.Error {
|
||||||
|
Certificate result;
|
||||||
|
var ret = init (out result);
|
||||||
|
throw_if_error(ret);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_x509_crt_import")]
|
||||||
|
public int import_(ref Datum data, CertificateFormat format);
|
||||||
|
[CCode (cname = "gnutls_x509_crt_import_")]
|
||||||
|
public void import(ref Datum data, CertificateFormat format) throws GLib.Error {
|
||||||
|
int err = import_(ref data, format);
|
||||||
|
throw_if_error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_x509_crt_set_version")]
|
||||||
|
public int set_version_(uint version);
|
||||||
|
[CCode (cname = "gnutls_x509_crt_set_version_")]
|
||||||
|
public void set_version(uint version) throws GLib.Error {
|
||||||
|
int err = set_version_(version);
|
||||||
|
throw_if_error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_x509_crt_set_key")]
|
||||||
|
public int set_key_(PrivateKey key);
|
||||||
|
[CCode (cname = "gnutls_x509_crt_set_key_")]
|
||||||
|
public void set_key(PrivateKey key) throws GLib.Error {
|
||||||
|
int err = set_key_(key);
|
||||||
|
throw_if_error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_x509_crt_set_activation_time")]
|
||||||
|
public int set_activation_time_(time_t act_time);
|
||||||
|
[CCode (cname = "gnutls_x509_crt_set_activation_time_")]
|
||||||
|
public void set_activation_time(time_t act_time) throws GLib.Error {
|
||||||
|
int err = set_activation_time_(act_time);
|
||||||
|
throw_if_error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_x509_crt_set_expiration_time")]
|
||||||
|
public int set_expiration_time_(time_t exp_time);
|
||||||
|
[CCode (cname = "gnutls_x509_crt_set_expiration_time_")]
|
||||||
|
public void set_expiration_time(time_t exp_time) throws GLib.Error {
|
||||||
|
int err = set_expiration_time_(exp_time);
|
||||||
|
throw_if_error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_x509_crt_set_serial")]
|
||||||
|
public int set_serial_(void* serial, size_t serial_size);
|
||||||
|
[CCode (cname = "gnutls_x509_crt_set_serial_")]
|
||||||
|
public void set_serial(void* serial, size_t serial_size) throws GLib.Error {
|
||||||
|
int err = set_serial_(serial, serial_size);
|
||||||
|
throw_if_error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_x509_crt_sign")]
|
||||||
|
public int sign_(Certificate issuer, PrivateKey issuer_key);
|
||||||
|
[CCode (cname = "gnutls_x509_crt_sign_")]
|
||||||
|
public void sign(Certificate issuer, PrivateKey issuer_key) throws GLib.Error {
|
||||||
|
int err = sign_(issuer, issuer_key);
|
||||||
|
throw_if_error(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_x509_crt_get_fingerprint")]
|
||||||
|
public int get_fingerprint_(DigestAlgorithm algo, void* buf, ref size_t buf_size);
|
||||||
|
[CCode (cname = "gnutls_x509_crt_get_fingerprint_")]
|
||||||
|
public void get_fingerprint(DigestAlgorithm algo, void* buf, ref size_t buf_size) throws GLib.Error {
|
||||||
|
int err = get_fingerprint_(algo, buf, ref buf_size);
|
||||||
|
throw_if_error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Compact]
|
||||||
|
[CCode (cname = "struct gnutls_x509_privkey_int", cprefix = "gnutls_x509_privkey_", free_function = "gnutls_x509_privkey_deinit")]
|
||||||
|
public class PrivateKey {
|
||||||
|
private static int init (out PrivateKey key);
|
||||||
|
public static PrivateKey create () throws GLib.Error {
|
||||||
|
PrivateKey result;
|
||||||
|
var ret = init (out result);
|
||||||
|
throw_if_error(ret);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int generate(PKAlgorithm algo, uint bits, uint flags = 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_certificate_request_t", cprefix = "GNUTLS_CERT_", has_type_id = false)]
|
||||||
|
public enum CertificateRequest {
|
||||||
|
IGNORE,
|
||||||
|
REQUEST,
|
||||||
|
REQUIRE
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_pk_algorithm_t", cprefix = "GNUTLS_PK_", has_type_id = false)]
|
||||||
|
public enum PKAlgorithm {
|
||||||
|
UNKNOWN,
|
||||||
|
RSA,
|
||||||
|
DSA;
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_digest_algorithm_t", cprefix = "GNUTLS_DIG_", has_type_id = false)]
|
||||||
|
public enum DigestAlgorithm {
|
||||||
|
NULL,
|
||||||
|
MD5,
|
||||||
|
SHA1,
|
||||||
|
RMD160,
|
||||||
|
MD2,
|
||||||
|
SHA224,
|
||||||
|
SHA256,
|
||||||
|
SHA384,
|
||||||
|
SHA512;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
[CCode (cname = "gnutls_init_flags_t", cprefix = "GNUTLS_", has_type_id = false)]
|
||||||
|
public enum InitFlags {
|
||||||
|
SERVER,
|
||||||
|
CLIENT,
|
||||||
|
DATAGRAM
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_credentials_type_t", cprefix = "GNUTLS_CRD_", has_type_id = false)]
|
||||||
|
public enum CredentialsType {
|
||||||
|
CERTIFICATE,
|
||||||
|
ANON,
|
||||||
|
SRP,
|
||||||
|
PSK,
|
||||||
|
IA
|
||||||
|
}
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_x509_crt_fmt_t", cprefix = "GNUTLS_X509_FMT_", has_type_id = false)]
|
||||||
|
public enum CertificateFormat {
|
||||||
|
DER,
|
||||||
|
PEM
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
[CCode (cname = "gnutls_certificate_status_t", cprefix = "GNUTLS_CERT_", has_type_id = false)]
|
||||||
|
public enum CertificateStatus {
|
||||||
|
INVALID, // will be set if the certificate was not verified.
|
||||||
|
REVOKED, // in X.509 this will be set only if CRLs are checked
|
||||||
|
SIGNER_NOT_FOUND,
|
||||||
|
SIGNER_NOT_CA,
|
||||||
|
INSECURE_ALGORITHM
|
||||||
|
}
|
||||||
|
|
||||||
|
[SimpleType]
|
||||||
|
[CCode (cname = "gnutls_datum_t", has_type_id = false)]
|
||||||
|
public struct Datum {
|
||||||
|
public uint8* data;
|
||||||
|
public uint size;
|
||||||
|
|
||||||
|
public uint8[] extract() {
|
||||||
|
uint8[] ret = new uint8[size];
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
ret[i] = data[i];
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gnutls error codes. The mapping to a TLS alert is also shown in comments.
|
||||||
|
[CCode (cname = "int", cprefix = "GNUTLS_E_", lower_case_cprefix = "gnutls_error_", has_type_id = false)]
|
||||||
|
public enum ErrorCode {
|
||||||
|
SUCCESS,
|
||||||
|
UNKNOWN_COMPRESSION_ALGORITHM,
|
||||||
|
UNKNOWN_CIPHER_TYPE,
|
||||||
|
LARGE_PACKET,
|
||||||
|
UNSUPPORTED_VERSION_PACKET, // GNUTLS_A_PROTOCOL_VERSION
|
||||||
|
UNEXPECTED_PACKET_LENGTH, // GNUTLS_A_RECORD_OVERFLOW
|
||||||
|
INVALID_SESSION,
|
||||||
|
FATAL_ALERT_RECEIVED,
|
||||||
|
UNEXPECTED_PACKET, // GNUTLS_A_UNEXPECTED_MESSAGE
|
||||||
|
WARNING_ALERT_RECEIVED,
|
||||||
|
ERROR_IN_FINISHED_PACKET,
|
||||||
|
UNEXPECTED_HANDSHAKE_PACKET,
|
||||||
|
UNKNOWN_CIPHER_SUITE, // GNUTLS_A_HANDSHAKE_FAILURE
|
||||||
|
UNWANTED_ALGORITHM,
|
||||||
|
MPI_SCAN_FAILED,
|
||||||
|
DECRYPTION_FAILED, // GNUTLS_A_DECRYPTION_FAILED, GNUTLS_A_BAD_RECORD_MAC
|
||||||
|
MEMORY_ERROR,
|
||||||
|
DECOMPRESSION_FAILED, // GNUTLS_A_DECOMPRESSION_FAILURE
|
||||||
|
COMPRESSION_FAILED,
|
||||||
|
AGAIN,
|
||||||
|
EXPIRED,
|
||||||
|
DB_ERROR,
|
||||||
|
SRP_PWD_ERROR,
|
||||||
|
INSUFFICIENT_CREDENTIALS,
|
||||||
|
HASH_FAILED,
|
||||||
|
BASE64_DECODING_ERROR,
|
||||||
|
MPI_PRINT_FAILED,
|
||||||
|
REHANDSHAKE, // GNUTLS_A_NO_RENEGOTIATION
|
||||||
|
GOT_APPLICATION_DATA,
|
||||||
|
RECORD_LIMIT_REACHED,
|
||||||
|
ENCRYPTION_FAILED,
|
||||||
|
PK_ENCRYPTION_FAILED,
|
||||||
|
PK_DECRYPTION_FAILED,
|
||||||
|
PK_SIGN_FAILED,
|
||||||
|
X509_UNSUPPORTED_CRITICAL_EXTENSION,
|
||||||
|
KEY_USAGE_VIOLATION,
|
||||||
|
NO_CERTIFICATE_FOUND, // GNUTLS_A_BAD_CERTIFICATE
|
||||||
|
INVALID_REQUEST,
|
||||||
|
SHORT_MEMORY_BUFFER,
|
||||||
|
INTERRUPTED,
|
||||||
|
PUSH_ERROR,
|
||||||
|
PULL_ERROR,
|
||||||
|
RECEIVED_ILLEGAL_PARAMETER, // GNUTLS_A_ILLEGAL_PARAMETER
|
||||||
|
REQUESTED_DATA_NOT_AVAILABLE,
|
||||||
|
PKCS1_WRONG_PAD,
|
||||||
|
RECEIVED_ILLEGAL_EXTENSION,
|
||||||
|
INTERNAL_ERROR,
|
||||||
|
DH_PRIME_UNACCEPTABLE,
|
||||||
|
FILE_ERROR,
|
||||||
|
TOO_MANY_EMPTY_PACKETS,
|
||||||
|
UNKNOWN_PK_ALGORITHM,
|
||||||
|
// returned if libextra functionality was requested but
|
||||||
|
// gnutls_global_init_extra() was not called.
|
||||||
|
|
||||||
|
INIT_LIBEXTRA,
|
||||||
|
LIBRARY_VERSION_MISMATCH,
|
||||||
|
// returned if you need to generate temporary RSA
|
||||||
|
// parameters. These are needed for export cipher suites.
|
||||||
|
|
||||||
|
NO_TEMPORARY_RSA_PARAMS,
|
||||||
|
LZO_INIT_FAILED,
|
||||||
|
NO_COMPRESSION_ALGORITHMS,
|
||||||
|
NO_CIPHER_SUITES,
|
||||||
|
OPENPGP_GETKEY_FAILED,
|
||||||
|
PK_SIG_VERIFY_FAILED,
|
||||||
|
ILLEGAL_SRP_USERNAME,
|
||||||
|
SRP_PWD_PARSING_ERROR,
|
||||||
|
NO_TEMPORARY_DH_PARAMS,
|
||||||
|
// For certificate and key stuff
|
||||||
|
|
||||||
|
ASN1_ELEMENT_NOT_FOUND,
|
||||||
|
ASN1_IDENTIFIER_NOT_FOUND,
|
||||||
|
ASN1_DER_ERROR,
|
||||||
|
ASN1_VALUE_NOT_FOUND,
|
||||||
|
ASN1_GENERIC_ERROR,
|
||||||
|
ASN1_VALUE_NOT_VALID,
|
||||||
|
ASN1_TAG_ERROR,
|
||||||
|
ASN1_TAG_IMPLICIT,
|
||||||
|
ASN1_TYPE_ANY_ERROR,
|
||||||
|
ASN1_SYNTAX_ERROR,
|
||||||
|
ASN1_DER_OVERFLOW,
|
||||||
|
OPENPGP_UID_REVOKED,
|
||||||
|
CERTIFICATE_ERROR,
|
||||||
|
CERTIFICATE_KEY_MISMATCH,
|
||||||
|
UNSUPPORTED_CERTIFICATE_TYPE, // GNUTLS_A_UNSUPPORTED_CERTIFICATE
|
||||||
|
X509_UNKNOWN_SAN,
|
||||||
|
OPENPGP_FINGERPRINT_UNSUPPORTED,
|
||||||
|
X509_UNSUPPORTED_ATTRIBUTE,
|
||||||
|
UNKNOWN_HASH_ALGORITHM,
|
||||||
|
UNKNOWN_PKCS_CONTENT_TYPE,
|
||||||
|
UNKNOWN_PKCS_BAG_TYPE,
|
||||||
|
INVALID_PASSWORD,
|
||||||
|
MAC_VERIFY_FAILED, // for PKCS #12 MAC
|
||||||
|
CONSTRAINT_ERROR,
|
||||||
|
WARNING_IA_IPHF_RECEIVED,
|
||||||
|
WARNING_IA_FPHF_RECEIVED,
|
||||||
|
IA_VERIFY_FAILED,
|
||||||
|
UNKNOWN_ALGORITHM,
|
||||||
|
BASE64_ENCODING_ERROR,
|
||||||
|
INCOMPATIBLE_CRYPTO_LIBRARY,
|
||||||
|
INCOMPATIBLE_LIBTASN1_LIBRARY,
|
||||||
|
OPENPGP_KEYRING_ERROR,
|
||||||
|
X509_UNSUPPORTED_OID,
|
||||||
|
RANDOM_FAILED,
|
||||||
|
BASE64_UNEXPECTED_HEADER_ERROR,
|
||||||
|
OPENPGP_SUBKEY_ERROR,
|
||||||
|
CRYPTO_ALREADY_REGISTERED,
|
||||||
|
HANDSHAKE_TOO_LARGE,
|
||||||
|
UNIMPLEMENTED_FEATURE,
|
||||||
|
APPLICATION_ERROR_MAX, // -65000
|
||||||
|
APPLICATION_ERROR_MIN; // -65500
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_error_is_fatal")]
|
||||||
|
public bool is_fatal();
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_perror")]
|
||||||
|
public void print();
|
||||||
|
|
||||||
|
[CCode (cname = "gnutls_strerror")]
|
||||||
|
public unowned string to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void throw_if_error(int err_int) throws GLib.Error {
|
||||||
|
ErrorCode error = (ErrorCode)err_int;
|
||||||
|
if (error != ErrorCode.SUCCESS) {
|
||||||
|
throw new GLib.Error(-1, error, "%s%s", error.to_string(), error.is_fatal() ? " fatal" : "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
plugins/ice/vapi/metadata/Nice-0.1.metadata
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
Nice cheader_filename="nice.h"
|
||||||
|
Address.to_string.dst type="char[]"
|
||||||
|
Agent.new_reliable#constructor name="create_reliable"
|
||||||
|
Agent.attach_recv skip=false
|
||||||
|
Agent.send.buf type="uint8[]" array_length_idx=2
|
||||||
|
AgentRecvFunc.buf type="uint8[]" array_length_idx=3
|
||||||
|
PseudoTcpCallbacks#record skip
|
||||||
|
PseudoTcpSocket#class skip
|
||||||
|
|
||||||
|
# Not yet supported by vapigen
|
||||||
|
# Candidate copy_function="nice_candidate_copy" free_function="nice_candidate_free" type_id=""
|
386
plugins/ice/vapi/nice.vapi
Normal file
|
@ -0,0 +1,386 @@
|
||||||
|
/* nice.vapi generated by vapigen, do not modify. */
|
||||||
|
|
||||||
|
[CCode (cprefix = "Nice", gir_namespace = "Nice", gir_version = "0.1", lower_case_cprefix = "nice_")]
|
||||||
|
namespace Nice {
|
||||||
|
[CCode (cheader_filename = "nice.h", type_id = "nice_agent_get_type ()")]
|
||||||
|
public class Agent : GLib.Object {
|
||||||
|
[CCode (has_construct_function = false)]
|
||||||
|
public Agent (GLib.MainContext ctx, Nice.Compatibility compat);
|
||||||
|
public bool add_local_address (Nice.Address addr);
|
||||||
|
public uint add_stream (uint n_components);
|
||||||
|
public bool attach_recv (uint stream_id, uint component_id, GLib.MainContext ctx, Nice.AgentRecvFunc func);
|
||||||
|
[Version (since = "0.1.16")]
|
||||||
|
public async void close_async ();
|
||||||
|
[CCode (cname = "nice_agent_new_reliable", has_construct_function = false)]
|
||||||
|
[Version (since = "0.0.11")]
|
||||||
|
public Agent.create_reliable (GLib.MainContext ctx, Nice.Compatibility compat);
|
||||||
|
[Version (since = "0.1.6")]
|
||||||
|
public bool forget_relays (uint stream_id, uint component_id);
|
||||||
|
[CCode (has_construct_function = false)]
|
||||||
|
[Version (since = "0.1.15")]
|
||||||
|
public Agent.full (GLib.MainContext ctx, Nice.Compatibility compat, Nice.AgentOption flags);
|
||||||
|
public bool gather_candidates (uint stream_id);
|
||||||
|
[Version (since = "0.1.4")]
|
||||||
|
public string generate_local_candidate_sdp (Nice.Candidate candidate);
|
||||||
|
[Version (since = "0.1.4")]
|
||||||
|
public string generate_local_sdp ();
|
||||||
|
[Version (since = "0.1.4")]
|
||||||
|
public string generate_local_stream_sdp (uint stream_id, bool include_non_ice);
|
||||||
|
[Version (since = "0.1.8")]
|
||||||
|
public Nice.ComponentState get_component_state (uint stream_id, uint component_id);
|
||||||
|
public Nice.Candidate get_default_local_candidate (uint stream_id, uint component_id);
|
||||||
|
[Version (since = "0.1.5")]
|
||||||
|
public GLib.IOStream get_io_stream (uint stream_id, uint component_id);
|
||||||
|
public GLib.SList<Nice.Candidate> get_local_candidates (uint stream_id, uint component_id);
|
||||||
|
public bool get_local_credentials (uint stream_id, out string ufrag, out string pwd);
|
||||||
|
public GLib.SList<Nice.Candidate> get_remote_candidates (uint stream_id, uint component_id);
|
||||||
|
public bool get_selected_pair (uint stream_id, uint component_id, Nice.Candidate local, Nice.Candidate remote);
|
||||||
|
[Version (since = "0.1.5")]
|
||||||
|
public GLib.Socket? get_selected_socket (uint stream_id, uint component_id);
|
||||||
|
[Version (since = "0.1.4")]
|
||||||
|
public unowned string get_stream_name (uint stream_id);
|
||||||
|
[Version (since = "0.1.4")]
|
||||||
|
public Nice.Candidate parse_remote_candidate_sdp (uint stream_id, string sdp);
|
||||||
|
[Version (since = "0.1.4")]
|
||||||
|
public int parse_remote_sdp (string sdp);
|
||||||
|
[Version (since = "0.1.4")]
|
||||||
|
public GLib.SList<Nice.Candidate> parse_remote_stream_sdp (uint stream_id, string sdp, string ufrag, string pwd);
|
||||||
|
[Version (since = "0.1.16")]
|
||||||
|
public bool peer_candidate_gathering_done (uint stream_id);
|
||||||
|
[Version (since = "0.1.5")]
|
||||||
|
public ssize_t recv (uint stream_id, uint component_id, [CCode (array_length_cname = "buf_len", array_length_pos = 3.5, array_length_type = "gsize")] out unowned uint8[] buf, GLib.Cancellable? cancellable = null) throws GLib.Error;
|
||||||
|
[Version (since = "0.1.5")]
|
||||||
|
public int recv_messages (uint stream_id, uint component_id, [CCode (array_length_cname = "n_messages", array_length_pos = 3.5, array_length_type = "guint")] out unowned Nice.InputMessage[] messages, GLib.Cancellable? cancellable = null) throws GLib.Error;
|
||||||
|
[Version (since = "0.1.5")]
|
||||||
|
public int recv_messages_nonblocking (uint stream_id, uint component_id, [CCode (array_length_cname = "n_messages", array_length_pos = 3.5, array_length_type = "guint")] out unowned Nice.InputMessage[] messages, GLib.Cancellable? cancellable = null) throws GLib.Error;
|
||||||
|
[Version (since = "0.1.5")]
|
||||||
|
public ssize_t recv_nonblocking (uint stream_id, uint component_id, [CCode (array_length_cname = "buf_len", array_length_pos = 3.5, array_length_type = "gsize")] out unowned uint8[] buf, GLib.Cancellable? cancellable = null) throws GLib.Error;
|
||||||
|
public void remove_stream (uint stream_id);
|
||||||
|
public bool restart ();
|
||||||
|
[Version (since = "0.1.6")]
|
||||||
|
public bool restart_stream (uint stream_id);
|
||||||
|
public int send (uint stream_id, uint component_id, [CCode (array_length_cname = "len", array_length_pos = 2.5, array_length_type = "guint", type = "const gchar*")] uint8[] buf);
|
||||||
|
[Version (since = "0.1.5")]
|
||||||
|
public int send_messages_nonblocking (uint stream_id, uint component_id, [CCode (array_length_cname = "n_messages", array_length_pos = 3.5, array_length_type = "guint")] Nice.OutputMessage[] messages, GLib.Cancellable? cancellable = null) throws GLib.Error;
|
||||||
|
public bool set_local_credentials (uint stream_id, string ufrag, string pwd);
|
||||||
|
public void set_port_range (uint stream_id, uint component_id, uint min_port, uint max_port);
|
||||||
|
public bool set_relay_info (uint stream_id, uint component_id, string server_ip, uint server_port, string username, string password, Nice.RelayType type);
|
||||||
|
public int set_remote_candidates (uint stream_id, uint component_id, GLib.SList<Nice.Candidate> candidates);
|
||||||
|
public bool set_remote_credentials (uint stream_id, string ufrag, string pwd);
|
||||||
|
public bool set_selected_pair (uint stream_id, uint component_id, string lfoundation, string rfoundation);
|
||||||
|
public bool set_selected_remote_candidate (uint stream_id, uint component_id, Nice.Candidate candidate);
|
||||||
|
[Version (since = "0.0.10")]
|
||||||
|
public void set_software (string software);
|
||||||
|
[Version (since = "0.1.4")]
|
||||||
|
public bool set_stream_name (uint stream_id, string name);
|
||||||
|
[Version (since = "0.0.9")]
|
||||||
|
public void set_stream_tos (uint stream_id, int tos);
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.1.8")]
|
||||||
|
public bool bytestream_tcp { get; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
public uint compatibility { get; construct; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
public bool controlling_mode { get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.1.14")]
|
||||||
|
public bool force_relay { get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
public bool full_mode { get; construct; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.1.8")]
|
||||||
|
public bool ice_tcp { get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.1.16")]
|
||||||
|
public bool ice_trickle { get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.1.8")]
|
||||||
|
public bool ice_udp { get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.1.8")]
|
||||||
|
public bool keepalive_conncheck { get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
public void* main_context { get; construct; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
public uint max_connectivity_checks { get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.0.4")]
|
||||||
|
public string proxy_ip { owned get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.0.4")]
|
||||||
|
public string proxy_password { owned get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.0.4")]
|
||||||
|
public uint proxy_port { get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.0.4")]
|
||||||
|
public uint proxy_type { get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.0.4")]
|
||||||
|
public string proxy_username { owned get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.0.11")]
|
||||||
|
public bool reliable { get; construct; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.1.15")]
|
||||||
|
public uint stun_initial_timeout { get; set construct; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.1.15")]
|
||||||
|
public uint stun_max_retransmissions { get; set construct; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
public uint stun_pacing_timer { get; set construct; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.1.15")]
|
||||||
|
public uint stun_reliable_timeout { get; set construct; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
public string stun_server { owned get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
public uint stun_server_port { get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
public bool support_renomination { get; set; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.0.7")]
|
||||||
|
public bool upnp { get; set construct; }
|
||||||
|
[NoAccessorMethod]
|
||||||
|
[Version (since = "0.0.7")]
|
||||||
|
public uint upnp_timeout { get; set construct; }
|
||||||
|
public signal void candidate_gathering_done (uint stream_id);
|
||||||
|
public signal void component_state_changed (uint stream_id, uint component_id, uint state);
|
||||||
|
public signal void initial_binding_request_received (uint stream_id);
|
||||||
|
[Version (deprecated = true, deprecated_since = "0.1.8")]
|
||||||
|
public signal void new_candidate (uint stream_id, uint component_id, string foundation);
|
||||||
|
[Version (since = "0.1.8")]
|
||||||
|
public signal void new_candidate_full (Nice.Candidate candidate);
|
||||||
|
[Version (deprecated = true, deprecated_since = "0.1.8")]
|
||||||
|
public signal void new_remote_candidate (uint stream_id, uint component_id, string foundation);
|
||||||
|
[Version (since = "0.1.8")]
|
||||||
|
public signal void new_remote_candidate_full (Nice.Candidate candidate);
|
||||||
|
[Version (deprecated = true, deprecated_since = "0.1.8")]
|
||||||
|
public signal void new_selected_pair (uint stream_id, uint component_id, string lfoundation, string rfoundation);
|
||||||
|
[Version (since = "0.1.8")]
|
||||||
|
public signal void new_selected_pair_full (uint stream_id, uint component_id, Nice.Candidate lcandidate, Nice.Candidate rcandidate);
|
||||||
|
[Version (since = "0.0.11")]
|
||||||
|
public signal void reliable_transport_writable (uint stream_id, uint component_id);
|
||||||
|
[Version (since = "0.1.5")]
|
||||||
|
public signal void streams_removed ([CCode (array_length = false, array_null_terminated = true)] uint[] stream_ids);
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", copy_function = "nice_candidate_copy", free_function = "nice_candidate_free")]
|
||||||
|
[Compact]
|
||||||
|
public class Candidate {
|
||||||
|
public Nice.Address addr;
|
||||||
|
public Nice.Address base_addr;
|
||||||
|
public uint component_id;
|
||||||
|
[CCode (array_length = false)]
|
||||||
|
public weak char foundation[33];
|
||||||
|
public weak string password;
|
||||||
|
public uint32 priority;
|
||||||
|
public void* sockptr;
|
||||||
|
public uint stream_id;
|
||||||
|
public Nice.CandidateTransport transport;
|
||||||
|
public Nice.TurnServer turn;
|
||||||
|
public Nice.CandidateType type;
|
||||||
|
public weak string username;
|
||||||
|
[CCode (has_construct_function = false)]
|
||||||
|
public Candidate (Nice.CandidateType type);
|
||||||
|
public Nice.Candidate copy ();
|
||||||
|
[Version (since = "0.1.15")]
|
||||||
|
public bool equal_target (Nice.Candidate candidate2);
|
||||||
|
public void free ();
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", has_type_id = false)]
|
||||||
|
public struct Address {
|
||||||
|
[CCode (cname = "s.addr")]
|
||||||
|
public void* s_addr;
|
||||||
|
[CCode (cname = "s.ip4")]
|
||||||
|
public void* s_ip4;
|
||||||
|
[CCode (cname = "s.ip6")]
|
||||||
|
public void* s_ip6;
|
||||||
|
public void copy_to_sockaddr (void* sin);
|
||||||
|
public bool equal (Nice.Address b);
|
||||||
|
[Version (since = "0.1.8")]
|
||||||
|
public bool equal_no_port (Nice.Address b);
|
||||||
|
public void free ();
|
||||||
|
public uint get_port ();
|
||||||
|
public void init ();
|
||||||
|
public int ip_version ();
|
||||||
|
public bool is_private ();
|
||||||
|
public bool is_valid ();
|
||||||
|
public void set_from_sockaddr (void* sin);
|
||||||
|
public bool set_from_string (string str);
|
||||||
|
public void set_ipv4 (uint32 addr_ipv4);
|
||||||
|
public void set_ipv6 (uint8 addr_ipv6);
|
||||||
|
public void set_port (uint port);
|
||||||
|
public void to_string ([CCode (array_length = false, type = "gchar*")] char[] dst);
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", has_type_id = false)]
|
||||||
|
[Version (since = "0.1.5")]
|
||||||
|
public struct InputMessage {
|
||||||
|
[CCode (array_length_cname = "n_buffers")]
|
||||||
|
public weak GLib.InputVector[] buffers;
|
||||||
|
public int n_buffers;
|
||||||
|
public Nice.Address from;
|
||||||
|
public size_t length;
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", has_type_id = false)]
|
||||||
|
[Version (since = "0.1.5")]
|
||||||
|
public struct OutputMessage {
|
||||||
|
[CCode (array_length_cname = "n_buffers")]
|
||||||
|
public weak GLib.OutputVector[] buffers;
|
||||||
|
public int n_buffers;
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "TurnServer", has_type_id = false)]
|
||||||
|
public struct TurnServer {
|
||||||
|
public int ref_count;
|
||||||
|
public Nice.Address server;
|
||||||
|
public weak string username;
|
||||||
|
public weak string password;
|
||||||
|
public Nice.RelayType type;
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cprefix = "NICE_AGENT_OPTION_", has_type_id = false)]
|
||||||
|
[Flags]
|
||||||
|
[Version (since = "0.1.15")]
|
||||||
|
public enum AgentOption {
|
||||||
|
REGULAR_NOMINATION,
|
||||||
|
RELIABLE,
|
||||||
|
LITE_MODE,
|
||||||
|
ICE_TRICKLE,
|
||||||
|
SUPPORT_RENOMINATION
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cprefix = "NICE_CANDIDATE_TRANSPORT_", has_type_id = false)]
|
||||||
|
public enum CandidateTransport {
|
||||||
|
UDP,
|
||||||
|
TCP_ACTIVE,
|
||||||
|
TCP_PASSIVE,
|
||||||
|
TCP_SO
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cprefix = "NICE_CANDIDATE_TYPE_", has_type_id = false)]
|
||||||
|
public enum CandidateType {
|
||||||
|
HOST,
|
||||||
|
SERVER_REFLEXIVE,
|
||||||
|
PEER_REFLEXIVE,
|
||||||
|
RELAYED
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cprefix = "NICE_COMPATIBILITY_", has_type_id = false)]
|
||||||
|
public enum Compatibility {
|
||||||
|
RFC5245,
|
||||||
|
DRAFT19,
|
||||||
|
GOOGLE,
|
||||||
|
MSN,
|
||||||
|
WLM2009,
|
||||||
|
OC2007,
|
||||||
|
OC2007R2,
|
||||||
|
LAST
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cprefix = "NICE_COMPONENT_STATE_", has_type_id = false)]
|
||||||
|
public enum ComponentState {
|
||||||
|
DISCONNECTED,
|
||||||
|
GATHERING,
|
||||||
|
CONNECTING,
|
||||||
|
CONNECTED,
|
||||||
|
READY,
|
||||||
|
FAILED,
|
||||||
|
LAST;
|
||||||
|
[Version (since = "0.1.6")]
|
||||||
|
public unowned string to_string ();
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cprefix = "NICE_COMPONENT_TYPE_", has_type_id = false)]
|
||||||
|
public enum ComponentType {
|
||||||
|
RTP,
|
||||||
|
RTCP
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cprefix = "NICE_NOMINATION_MODE_", has_type_id = false)]
|
||||||
|
[Version (since = "0.1.15")]
|
||||||
|
public enum NominationMode {
|
||||||
|
REGULAR,
|
||||||
|
AGGRESSIVE
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cprefix = "NICE_PROXY_TYPE_", has_type_id = false)]
|
||||||
|
[Version (since = "0.0.4")]
|
||||||
|
public enum ProxyType {
|
||||||
|
NONE,
|
||||||
|
SOCKS5,
|
||||||
|
HTTP,
|
||||||
|
LAST
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "PseudoTcpDebugLevel", cprefix = "PSEUDO_TCP_DEBUG_", has_type_id = false)]
|
||||||
|
[Version (since = "0.0.11")]
|
||||||
|
public enum PseudoTcpDebugLevel {
|
||||||
|
NONE,
|
||||||
|
NORMAL,
|
||||||
|
VERBOSE
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "PseudoTcpShutdown", cprefix = "PSEUDO_TCP_SHUTDOWN_", has_type_id = false)]
|
||||||
|
[Version (since = "0.1.8")]
|
||||||
|
public enum PseudoTcpShutdown {
|
||||||
|
RD,
|
||||||
|
WR,
|
||||||
|
RDWR
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "PseudoTcpState", cprefix = "PSEUDO_TCP_", has_type_id = false)]
|
||||||
|
[Version (since = "0.0.11")]
|
||||||
|
public enum PseudoTcpState {
|
||||||
|
LISTEN,
|
||||||
|
SYN_SENT,
|
||||||
|
SYN_RECEIVED,
|
||||||
|
ESTABLISHED,
|
||||||
|
CLOSED,
|
||||||
|
FIN_WAIT_1,
|
||||||
|
FIN_WAIT_2,
|
||||||
|
CLOSING,
|
||||||
|
TIME_WAIT,
|
||||||
|
CLOSE_WAIT,
|
||||||
|
LAST_ACK
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "PseudoTcpWriteResult", cprefix = "WR_", has_type_id = false)]
|
||||||
|
[Version (since = "0.0.11")]
|
||||||
|
public enum PseudoTcpWriteResult {
|
||||||
|
SUCCESS,
|
||||||
|
TOO_LARGE,
|
||||||
|
FAIL
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", cprefix = "NICE_RELAY_TYPE_TURN_", has_type_id = false)]
|
||||||
|
public enum RelayType {
|
||||||
|
UDP,
|
||||||
|
TCP,
|
||||||
|
TLS
|
||||||
|
}
|
||||||
|
[CCode (cheader_filename = "nice.h", instance_pos = 4.9)]
|
||||||
|
public delegate void AgentRecvFunc (Nice.Agent agent, uint stream_id, uint component_id, [CCode (array_length_cname = "len", array_length_pos = 3.5, array_length_type = "guint", type = "gchar*")] uint8[] buf);
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "NICE_AGENT_MAX_REMOTE_CANDIDATES")]
|
||||||
|
public const int AGENT_MAX_REMOTE_CANDIDATES;
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "NICE_CANDIDATE_DIRECTION_MS_PREF_ACTIVE")]
|
||||||
|
public const int CANDIDATE_DIRECTION_MS_PREF_ACTIVE;
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "NICE_CANDIDATE_DIRECTION_MS_PREF_PASSIVE")]
|
||||||
|
public const int CANDIDATE_DIRECTION_MS_PREF_PASSIVE;
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "NICE_CANDIDATE_MAX_FOUNDATION")]
|
||||||
|
public const int CANDIDATE_MAX_FOUNDATION;
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "NICE_CANDIDATE_TRANSPORT_MS_PREF_TCP")]
|
||||||
|
public const int CANDIDATE_TRANSPORT_MS_PREF_TCP;
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "NICE_CANDIDATE_TRANSPORT_MS_PREF_UDP")]
|
||||||
|
public const int CANDIDATE_TRANSPORT_MS_PREF_UDP;
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "NICE_CANDIDATE_TYPE_PREF_HOST")]
|
||||||
|
public const int CANDIDATE_TYPE_PREF_HOST;
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "NICE_CANDIDATE_TYPE_PREF_NAT_ASSISTED")]
|
||||||
|
public const int CANDIDATE_TYPE_PREF_NAT_ASSISTED;
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "NICE_CANDIDATE_TYPE_PREF_PEER_REFLEXIVE")]
|
||||||
|
public const int CANDIDATE_TYPE_PREF_PEER_REFLEXIVE;
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "NICE_CANDIDATE_TYPE_PREF_RELAYED")]
|
||||||
|
public const int CANDIDATE_TYPE_PREF_RELAYED;
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "NICE_CANDIDATE_TYPE_PREF_RELAYED_UDP")]
|
||||||
|
public const int CANDIDATE_TYPE_PREF_RELAYED_UDP;
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "NICE_CANDIDATE_TYPE_PREF_SERVER_REFLEXIVE")]
|
||||||
|
public const int CANDIDATE_TYPE_PREF_SERVER_REFLEXIVE;
|
||||||
|
[CCode (cheader_filename = "nice.h")]
|
||||||
|
public static void debug_disable (bool with_stun);
|
||||||
|
[CCode (cheader_filename = "nice.h")]
|
||||||
|
public static void debug_enable (bool with_stun);
|
||||||
|
[CCode (cheader_filename = "nice.h")]
|
||||||
|
public static string? interfaces_get_ip_for_interface (string interface_name);
|
||||||
|
[CCode (cheader_filename = "nice.h")]
|
||||||
|
public static GLib.List<string> interfaces_get_local_interfaces ();
|
||||||
|
[CCode (cheader_filename = "nice.h")]
|
||||||
|
public static GLib.List<string> interfaces_get_local_ips (bool include_loopback);
|
||||||
|
[CCode (cheader_filename = "nice.h", cname = "pseudo_tcp_set_debug_level")]
|
||||||
|
[Version (since = "0.0.11")]
|
||||||
|
public static void pseudo_tcp_set_debug_level (Nice.PseudoTcpDebugLevel level);
|
||||||
|
}
|
|
@ -3,13 +3,13 @@ find_package(Gettext)
|
||||||
include(${GETTEXT_USE_FILE})
|
include(${GETTEXT_USE_FILE})
|
||||||
gettext_compile(${GETTEXT_PACKAGE} SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/po TARGET_NAME ${GETTEXT_PACKAGE}-translations)
|
gettext_compile(${GETTEXT_PACKAGE} SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/po TARGET_NAME ${GETTEXT_PACKAGE}-translations)
|
||||||
|
|
||||||
|
find_package(Qrencode REQUIRED)
|
||||||
find_packages(OMEMO_PACKAGES REQUIRED
|
find_packages(OMEMO_PACKAGES REQUIRED
|
||||||
Gee
|
Gee
|
||||||
GLib
|
GLib
|
||||||
GModule
|
GModule
|
||||||
GObject
|
GObject
|
||||||
GTK3
|
GTK3
|
||||||
Qrencode
|
|
||||||
)
|
)
|
||||||
|
|
||||||
set(RESOURCE_LIST
|
set(RESOURCE_LIST
|
||||||
|
@ -29,6 +29,7 @@ compile_gresources(
|
||||||
|
|
||||||
vala_precompile(OMEMO_VALA_C
|
vala_precompile(OMEMO_VALA_C
|
||||||
SOURCES
|
SOURCES
|
||||||
|
src/dtls_srtp_verification_draft.vala
|
||||||
src/plugin.vala
|
src/plugin.vala
|
||||||
src/register_plugin.vala
|
src/register_plugin.vala
|
||||||
src/trust_level.vala
|
src/trust_level.vala
|
||||||
|
@ -39,7 +40,8 @@ SOURCES
|
||||||
src/jingle/jet_omemo.vala
|
src/jingle/jet_omemo.vala
|
||||||
|
|
||||||
src/logic/database.vala
|
src/logic/database.vala
|
||||||
src/logic/encrypt_state.vala
|
src/logic/decrypt.vala
|
||||||
|
src/logic/encrypt.vala
|
||||||
src/logic/manager.vala
|
src/logic/manager.vala
|
||||||
src/logic/pre_key_store.vala
|
src/logic/pre_key_store.vala
|
||||||
src/logic/session_store.vala
|
src/logic/session_store.vala
|
||||||
|
@ -53,6 +55,7 @@ SOURCES
|
||||||
src/ui/account_settings_entry.vala
|
src/ui/account_settings_entry.vala
|
||||||
src/ui/account_settings_widget.vala
|
src/ui/account_settings_widget.vala
|
||||||
src/ui/bad_messages_populator.vala
|
src/ui/bad_messages_populator.vala
|
||||||
|
src/ui/call_encryption_entry.vala
|
||||||
src/ui/contact_details_provider.vala
|
src/ui/contact_details_provider.vala
|
||||||
src/ui/contact_details_dialog.vala
|
src/ui/contact_details_dialog.vala
|
||||||
src/ui/device_notification_populator.vala
|
src/ui/device_notification_populator.vala
|
||||||
|
@ -66,18 +69,17 @@ CUSTOM_VAPIS
|
||||||
${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
|
${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
|
||||||
${CMAKE_BINARY_DIR}/exports/qlite.vapi
|
${CMAKE_BINARY_DIR}/exports/qlite.vapi
|
||||||
${CMAKE_BINARY_DIR}/exports/dino.vapi
|
${CMAKE_BINARY_DIR}/exports/dino.vapi
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/vapi/libqrencode.vapi
|
||||||
PACKAGES
|
PACKAGES
|
||||||
${OMEMO_PACKAGES}
|
${OMEMO_PACKAGES}
|
||||||
GRESOURCES
|
GRESOURCES
|
||||||
${OMEMO_GRESOURCES_XML}
|
${OMEMO_GRESOURCES_XML}
|
||||||
OPTIONS
|
|
||||||
--vapidir=${CMAKE_CURRENT_SOURCE_DIR}/vapi
|
|
||||||
)
|
)
|
||||||
|
|
||||||
add_definitions(${VALA_CFLAGS} -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\" -DG_LOG_DOMAIN="OMEMO")
|
add_definitions(${VALA_CFLAGS} -DGETTEXT_PACKAGE=\"${GETTEXT_PACKAGE}\" -DLOCALE_INSTALL_DIR=\"${LOCALE_INSTALL_DIR}\" -DG_LOG_DOMAIN="OMEMO")
|
||||||
add_library(omemo SHARED ${OMEMO_VALA_C} ${OMEMO_GRESOURCES_TARGET})
|
add_library(omemo SHARED ${OMEMO_VALA_C} ${OMEMO_GRESOURCES_TARGET})
|
||||||
add_dependencies(omemo ${GETTEXT_PACKAGE}-translations)
|
add_dependencies(omemo ${GETTEXT_PACKAGE}-translations)
|
||||||
target_link_libraries(omemo libdino signal-protocol-vala crypto-vala ${OMEMO_PACKAGES})
|
target_link_libraries(omemo libdino signal-protocol-vala crypto-vala ${OMEMO_PACKAGES} libqrencode)
|
||||||
set_target_properties(omemo PROPERTIES PREFIX "")
|
set_target_properties(omemo PROPERTIES PREFIX "")
|
||||||
set_target_properties(omemo PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/)
|
set_target_properties(omemo PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/)
|
||||||
|
|
||||||
|
|
195
plugins/omemo/src/dtls_srtp_verification_draft.vala
Normal file
|
@ -0,0 +1,195 @@
|
||||||
|
using Signal;
|
||||||
|
using Gee;
|
||||||
|
using Xmpp;
|
||||||
|
|
||||||
|
namespace Dino.Plugins.Omemo.DtlsSrtpVerificationDraft {
|
||||||
|
public const string NS_URI = "http://gultsch.de/xmpp/drafts/omemo/dlts-srtp-verification";
|
||||||
|
|
||||||
|
public class StreamModule : XmppStreamModule {
|
||||||
|
|
||||||
|
public static Xmpp.ModuleIdentity<StreamModule> IDENTITY = new Xmpp.ModuleIdentity<StreamModule>(NS_URI, "dtls_srtp_omemo_verification_draft");
|
||||||
|
|
||||||
|
private VerificationSendListener send_listener = new VerificationSendListener();
|
||||||
|
private HashMap<string, int> device_id_by_jingle_sid = new HashMap<string, int>();
|
||||||
|
private HashMap<string, Gee.List<string>> content_names_by_jingle_sid = new HashMap<string, Gee.List<string>>();
|
||||||
|
|
||||||
|
private void on_preprocess_incoming_iq_set_get(XmppStream stream, Xmpp.Iq.Stanza iq) {
|
||||||
|
if (iq.type_ != Iq.Stanza.TYPE_SET) return;
|
||||||
|
|
||||||
|
Gee.List<StanzaNode> content_nodes = iq.stanza.get_deep_subnodes(Xep.Jingle.NS_URI + ":jingle", Xep.Jingle.NS_URI + ":content");
|
||||||
|
if (content_nodes.size == 0) return;
|
||||||
|
|
||||||
|
string? jingle_sid = iq.stanza.get_deep_attribute(Xep.Jingle.NS_URI + ":jingle", "sid");
|
||||||
|
if (jingle_sid == null) return;
|
||||||
|
|
||||||
|
Xep.Omemo.OmemoDecryptor decryptor = stream.get_module(Xep.Omemo.OmemoDecryptor.IDENTITY);
|
||||||
|
|
||||||
|
foreach (StanzaNode content_node in content_nodes) {
|
||||||
|
string? content_name = content_node.get_attribute("name");
|
||||||
|
if (content_name == null) continue;
|
||||||
|
StanzaNode? transport_node = content_node.get_subnode("transport", Xep.JingleIceUdp.NS_URI);
|
||||||
|
if (transport_node == null) continue;
|
||||||
|
StanzaNode? fingerprint_node = transport_node.get_subnode("fingerprint", NS_URI);
|
||||||
|
if (fingerprint_node == null) continue;
|
||||||
|
StanzaNode? encrypted_node = fingerprint_node.get_subnode("encrypted", Omemo.NS_URI);
|
||||||
|
if (encrypted_node == null) continue;
|
||||||
|
|
||||||
|
Xep.Omemo.ParsedData? parsed_data = decryptor.parse_node(encrypted_node);
|
||||||
|
if (parsed_data == null || parsed_data.ciphertext == null) continue;
|
||||||
|
|
||||||
|
if (device_id_by_jingle_sid.has_key(jingle_sid) && device_id_by_jingle_sid[jingle_sid] != parsed_data.sid) {
|
||||||
|
warning("Expected DTLS fingerprint to be OMEMO encrypted from %s %d, but it was from %d", iq.from.to_string(), device_id_by_jingle_sid[jingle_sid], parsed_data.sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Bytes encr_key in parsed_data.our_potential_encrypted_keys.keys) {
|
||||||
|
parsed_data.is_prekey = parsed_data.our_potential_encrypted_keys[encr_key];
|
||||||
|
parsed_data.encrypted_key = encr_key.get_data();
|
||||||
|
|
||||||
|
try {
|
||||||
|
uint8[] key = decryptor.decrypt_key(parsed_data, iq.from.bare_jid);
|
||||||
|
string cleartext = decryptor.decrypt(parsed_data.ciphertext, key, parsed_data.iv);
|
||||||
|
|
||||||
|
StanzaNode new_fingerprint_node = new StanzaNode.build("fingerprint", Xep.JingleIceUdp.DTLS_NS_URI).add_self_xmlns()
|
||||||
|
.put_node(new StanzaNode.text(cleartext));
|
||||||
|
string? hash_attr = fingerprint_node.get_attribute("hash", NS_URI);
|
||||||
|
string? setup_attr = fingerprint_node.get_attribute("setup", NS_URI);
|
||||||
|
if (hash_attr != null) new_fingerprint_node.put_attribute("hash", hash_attr);
|
||||||
|
if (setup_attr != null) new_fingerprint_node.put_attribute("setup", setup_attr);
|
||||||
|
transport_node.put_node(new_fingerprint_node);
|
||||||
|
|
||||||
|
device_id_by_jingle_sid[jingle_sid] = parsed_data.sid;
|
||||||
|
if (!content_names_by_jingle_sid.has_key(content_name)) {
|
||||||
|
content_names_by_jingle_sid[content_name] = new ArrayList<string>();
|
||||||
|
}
|
||||||
|
content_names_by_jingle_sid[content_name].add(content_name);
|
||||||
|
|
||||||
|
stream.get_flag(Xep.Jingle.Flag.IDENTITY).get_session.begin(jingle_sid, (_, res) => {
|
||||||
|
Xep.Jingle.Session? session = stream.get_flag(Xep.Jingle.Flag.IDENTITY).get_session.end(res);
|
||||||
|
if (session == null || !session.contents_map.has_key(content_name)) return;
|
||||||
|
var encryption = new OmemoContentEncryption() { encryption_ns=NS_URI, encryption_name="OMEMO", our_key=new uint8[0], peer_key=new uint8[0], sid=device_id_by_jingle_sid[jingle_sid], jid=iq.from.bare_jid };
|
||||||
|
session.contents_map[content_name].encryptions[NS_URI] = encryption;
|
||||||
|
|
||||||
|
if (iq.stanza.get_deep_attribute(Xep.Jingle.NS_URI + ":jingle", "action") == "session-accept") {
|
||||||
|
session.additional_content_add_incoming.connect(on_content_add_received);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
break;
|
||||||
|
} catch (Error e) {
|
||||||
|
debug("Decrypting message from %s/%d failed: %s", iq.from.bare_jid.to_string(), parsed_data.sid, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_preprocess_outgoing_iq_set_get(XmppStream stream, Xmpp.Iq.Stanza iq) {
|
||||||
|
if (iq.type_ != Iq.Stanza.TYPE_SET) return;
|
||||||
|
|
||||||
|
StanzaNode? jingle_node = iq.stanza.get_subnode("jingle", Xep.Jingle.NS_URI);
|
||||||
|
if (jingle_node == null) return;
|
||||||
|
|
||||||
|
string? sid = jingle_node.get_attribute("sid", Xep.Jingle.NS_URI);
|
||||||
|
if (sid == null || !device_id_by_jingle_sid.has_key(sid)) return;
|
||||||
|
|
||||||
|
Gee.List<StanzaNode> content_nodes = jingle_node.get_subnodes("content", Xep.Jingle.NS_URI);
|
||||||
|
if (content_nodes.size == 0) return;
|
||||||
|
|
||||||
|
foreach (StanzaNode content_node in content_nodes) {
|
||||||
|
StanzaNode? transport_node = content_node.get_subnode("transport", Xep.JingleIceUdp.NS_URI);
|
||||||
|
if (transport_node == null) continue;
|
||||||
|
StanzaNode? fingerprint_node = transport_node.get_subnode("fingerprint", Xep.JingleIceUdp.DTLS_NS_URI);
|
||||||
|
if (fingerprint_node == null) continue;
|
||||||
|
string fingerprint = fingerprint_node.get_deep_string_content();
|
||||||
|
|
||||||
|
Xep.Omemo.OmemoEncryptor encryptor = stream.get_module(Xep.Omemo.OmemoEncryptor.IDENTITY);
|
||||||
|
Xep.Omemo.EncryptionData enc_data = encryptor.encrypt_plaintext(fingerprint);
|
||||||
|
encryptor.encrypt_key(enc_data, iq.to.bare_jid, device_id_by_jingle_sid[sid]);
|
||||||
|
|
||||||
|
StanzaNode new_fingerprint_node = new StanzaNode.build("fingerprint", NS_URI).add_self_xmlns().put_node(enc_data.get_encrypted_node());
|
||||||
|
string? hash_attr = fingerprint_node.get_attribute("hash", Xep.JingleIceUdp.DTLS_NS_URI);
|
||||||
|
string? setup_attr = fingerprint_node.get_attribute("setup", Xep.JingleIceUdp.DTLS_NS_URI);
|
||||||
|
if (hash_attr != null) new_fingerprint_node.put_attribute("hash", hash_attr);
|
||||||
|
if (setup_attr != null) new_fingerprint_node.put_attribute("setup", setup_attr);
|
||||||
|
transport_node.put_node(new_fingerprint_node);
|
||||||
|
|
||||||
|
transport_node.sub_nodes.remove(fingerprint_node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_message_received(XmppStream stream, Xmpp.MessageStanza message) {
|
||||||
|
StanzaNode? proceed_node = message.stanza.get_subnode("proceed", Xep.JingleMessageInitiation.NS_URI);
|
||||||
|
if (proceed_node == null) return;
|
||||||
|
|
||||||
|
string? jingle_sid = proceed_node.get_attribute("id");
|
||||||
|
if (jingle_sid == null) return;
|
||||||
|
|
||||||
|
StanzaNode? device_node = proceed_node.get_subnode("device", NS_URI);
|
||||||
|
if (device_node == null) return;
|
||||||
|
|
||||||
|
int device_id = device_node.get_attribute_int("id", -1);
|
||||||
|
if (device_id == -1) return;
|
||||||
|
|
||||||
|
device_id_by_jingle_sid[jingle_sid] = device_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_session_initiate_received(XmppStream stream, Xep.Jingle.Session session) {
|
||||||
|
if (device_id_by_jingle_sid.has_key(session.sid)) {
|
||||||
|
foreach (Xep.Jingle.Content content in session.contents) {
|
||||||
|
on_content_add_received(stream, content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session.additional_content_add_incoming.connect(on_content_add_received);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_content_add_received(XmppStream stream, Xep.Jingle.Content content) {
|
||||||
|
if (!content_names_by_jingle_sid.has_key(content.session.sid) || content_names_by_jingle_sid[content.session.sid].contains(content.content_name)) {
|
||||||
|
var encryption = new OmemoContentEncryption() { encryption_ns=NS_URI, encryption_name="OMEMO", our_key=new uint8[0], peer_key=new uint8[0], sid=device_id_by_jingle_sid[content.session.sid], jid=content.peer_full_jid.bare_jid };
|
||||||
|
content.encryptions[encryption.encryption_ns] = encryption;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void attach(XmppStream stream) {
|
||||||
|
stream.get_module(Xmpp.MessageModule.IDENTITY).received_message.connect(on_message_received);
|
||||||
|
stream.get_module(Xmpp.MessageModule.IDENTITY).send_pipeline.connect(send_listener);
|
||||||
|
stream.get_module(Xmpp.Iq.Module.IDENTITY).preprocess_incoming_iq_set_get.connect(on_preprocess_incoming_iq_set_get);
|
||||||
|
stream.get_module(Xmpp.Iq.Module.IDENTITY).preprocess_outgoing_iq_set_get.connect(on_preprocess_outgoing_iq_set_get);
|
||||||
|
stream.get_module(Xep.Jingle.Module.IDENTITY).session_initiate_received.connect(on_session_initiate_received);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void detach(XmppStream stream) {
|
||||||
|
stream.get_module(Xmpp.MessageModule.IDENTITY).received_message.disconnect(on_message_received);
|
||||||
|
stream.get_module(Xmpp.MessageModule.IDENTITY).send_pipeline.disconnect(send_listener);
|
||||||
|
stream.get_module(Xmpp.Iq.Module.IDENTITY).preprocess_incoming_iq_set_get.disconnect(on_preprocess_incoming_iq_set_get);
|
||||||
|
stream.get_module(Xmpp.Iq.Module.IDENTITY).preprocess_outgoing_iq_set_get.disconnect(on_preprocess_outgoing_iq_set_get);
|
||||||
|
stream.get_module(Xep.Jingle.Module.IDENTITY).session_initiate_received.disconnect(on_session_initiate_received);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string get_ns() { return NS_URI; }
|
||||||
|
|
||||||
|
public override string get_id() { return IDENTITY.id; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class VerificationSendListener : StanzaListener<MessageStanza> {
|
||||||
|
|
||||||
|
private const string[] after_actions_const = {};
|
||||||
|
|
||||||
|
public override string action_group { get { return "REWRITE_NODES"; } }
|
||||||
|
public override string[] after_actions { get { return after_actions_const; } }
|
||||||
|
|
||||||
|
public override async bool run(XmppStream stream, MessageStanza message) {
|
||||||
|
StanzaNode? proceed_node = message.stanza.get_subnode("proceed", Xep.JingleMessageInitiation.NS_URI);
|
||||||
|
if (proceed_node == null) return false;
|
||||||
|
|
||||||
|
StanzaNode device_node = new StanzaNode.build("device", NS_URI).add_self_xmlns()
|
||||||
|
.put_attribute("id", stream.get_module(Omemo.StreamModule.IDENTITY).store.local_registration_id.to_string());
|
||||||
|
proceed_node.put_node(device_node);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OmemoContentEncryption : Xep.Jingle.ContentEncryption {
|
||||||
|
public Jid jid { get; set; }
|
||||||
|
public int sid { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,18 +7,15 @@ using Xmpp;
|
||||||
using Xmpp.Xep;
|
using Xmpp.Xep;
|
||||||
|
|
||||||
namespace Dino.Plugins.JetOmemo {
|
namespace Dino.Plugins.JetOmemo {
|
||||||
|
|
||||||
private const string NS_URI = "urn:xmpp:jingle:jet-omemo:0";
|
private const string NS_URI = "urn:xmpp:jingle:jet-omemo:0";
|
||||||
private const string AES_128_GCM_URI = "urn:xmpp:ciphers:aes-128-gcm-nopadding";
|
private const string AES_128_GCM_URI = "urn:xmpp:ciphers:aes-128-gcm-nopadding";
|
||||||
|
|
||||||
public class Module : XmppStreamModule, Jet.EnvelopEncoding {
|
public class Module : XmppStreamModule, Jet.EnvelopEncoding {
|
||||||
public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0396_jet_omemo");
|
public static Xmpp.ModuleIdentity<Module> IDENTITY = new Xmpp.ModuleIdentity<Module>(NS_URI, "0396_jet_omemo");
|
||||||
private Omemo.Plugin plugin;
|
|
||||||
const uint KEY_SIZE = 16;
|
const uint KEY_SIZE = 16;
|
||||||
const uint IV_SIZE = 12;
|
const uint IV_SIZE = 12;
|
||||||
|
|
||||||
public Module(Omemo.Plugin plugin) {
|
|
||||||
this.plugin = plugin;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void attach(XmppStream stream) {
|
public override void attach(XmppStream stream) {
|
||||||
if (stream.get_module(Jet.Module.IDENTITY) != null) {
|
if (stream.get_module(Jet.Module.IDENTITY) != null) {
|
||||||
stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
|
stream.get_module(ServiceDiscovery.Module.IDENTITY).add_feature(stream, NS_URI);
|
||||||
|
@ -44,71 +41,38 @@ public class Module : XmppStreamModule, Jet.EnvelopEncoding {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Jet.TransportSecret decode_envolop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws Jingle.IqError {
|
public Jet.TransportSecret decode_envolop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, StanzaNode security) throws Jingle.IqError {
|
||||||
Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store;
|
|
||||||
StanzaNode? encrypted = security.get_subnode("encrypted", Omemo.NS_URI);
|
StanzaNode? encrypted = security.get_subnode("encrypted", Omemo.NS_URI);
|
||||||
if (encrypted == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing encrypted element");
|
if (encrypted == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing encrypted element");
|
||||||
StanzaNode? header = encrypted.get_subnode("header", Omemo.NS_URI);
|
|
||||||
if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing header element");
|
|
||||||
string? iv_node = header.get_deep_string_content("iv");
|
|
||||||
if (header == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: missing iv element");
|
|
||||||
uint8[] iv = Base64.decode((!)iv_node);
|
|
||||||
foreach (StanzaNode key_node in header.get_subnodes("key")) {
|
|
||||||
if (key_node.get_attribute_int("rid") == store.local_registration_id) {
|
|
||||||
string? key_node_content = key_node.get_string_content();
|
|
||||||
|
|
||||||
uint8[] key;
|
Xep.Omemo.OmemoDecryptor decryptor = stream.get_module(Xep.Omemo.OmemoDecryptor.IDENTITY);
|
||||||
Address address = new Address(peer_full_jid.bare_jid.to_string(), header.get_attribute_int("sid"));
|
|
||||||
if (key_node.get_attribute_bool("prekey")) {
|
|
||||||
PreKeySignalMessage msg = Omemo.Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content));
|
|
||||||
SessionCipher cipher = store.create_session_cipher(address);
|
|
||||||
key = cipher.decrypt_pre_key_signal_message(msg);
|
|
||||||
} else {
|
|
||||||
SignalMessage msg = Omemo.Plugin.get_context().deserialize_signal_message(Base64.decode((!)key_node_content));
|
|
||||||
SessionCipher cipher = store.create_session_cipher(address);
|
|
||||||
key = cipher.decrypt_signal_message(msg);
|
|
||||||
}
|
|
||||||
address.device_id = 0; // TODO: Hack to have address obj live longer
|
|
||||||
|
|
||||||
uint8[] authtag = null;
|
Xmpp.Xep.Omemo.ParsedData? data = decryptor.parse_node(encrypted);
|
||||||
if (key.length >= 32) {
|
if (data == null) throw new Jingle.IqError.BAD_REQUEST("Invalid JET-OMEMO envelop: bad encrypted element");
|
||||||
int authtaglength = key.length - 16;
|
|
||||||
authtag = new uint8[authtaglength];
|
foreach (Bytes encr_key in data.our_potential_encrypted_keys.keys) {
|
||||||
uint8[] new_key = new uint8[16];
|
data.is_prekey = data.our_potential_encrypted_keys[encr_key];
|
||||||
Memory.copy(authtag, (uint8*)key + 16, 16);
|
data.encrypted_key = encr_key.get_data();
|
||||||
Memory.copy(new_key, key, 16);
|
|
||||||
key = new_key;
|
try {
|
||||||
}
|
uint8[] key = decryptor.decrypt_key(data, peer_full_jid.bare_jid);
|
||||||
// TODO: authtag?
|
return new Jet.TransportSecret(key, data.iv);
|
||||||
return new Jet.TransportSecret(key, iv);
|
} catch (GLib.Error e) {
|
||||||
|
debug("Decrypting JET key from %s/%d failed: %s", peer_full_jid.bare_jid.to_string(), data.sid, e.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new Jingle.IqError.NOT_ACCEPTABLE("Not encrypted for targeted device");
|
throw new Jingle.IqError.NOT_ACCEPTABLE("Not encrypted for targeted device");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void encode_envelop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Jet.SecurityParameters security_params, StanzaNode security) {
|
public void encode_envelop(XmppStream stream, Jid local_full_jid, Jid peer_full_jid, Jet.SecurityParameters security_params, StanzaNode security) {
|
||||||
ArrayList<Account> accounts = plugin.app.stream_interactor.get_accounts();
|
|
||||||
Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store;
|
Store store = stream.get_module(Omemo.StreamModule.IDENTITY).store;
|
||||||
Account? account = null;
|
|
||||||
foreach (Account compare in accounts) {
|
|
||||||
if (compare.bare_jid.equals_bare(local_full_jid)) {
|
|
||||||
account = compare;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (account == null) {
|
|
||||||
// TODO
|
|
||||||
critical("Sending from offline account %s", local_full_jid.to_string());
|
|
||||||
}
|
|
||||||
|
|
||||||
StanzaNode header_node;
|
var encryption_data = new Xep.Omemo.EncryptionData(store.local_registration_id);
|
||||||
StanzaNode encrypted_node = new StanzaNode.build("encrypted", Omemo.NS_URI).add_self_xmlns()
|
encryption_data.iv = security_params.secret.initialization_vector;
|
||||||
.put_node(header_node = new StanzaNode.build("header", Omemo.NS_URI)
|
encryption_data.keytag = security_params.secret.transport_key;
|
||||||
.put_attribute("sid", store.local_registration_id.to_string())
|
Xep.Omemo.OmemoEncryptor encryptor = stream.get_module(Xep.Omemo.OmemoEncryptor.IDENTITY);
|
||||||
.put_node(new StanzaNode.build("iv", Omemo.NS_URI)
|
encryptor.encrypt_key_to_recipient(stream, encryption_data, peer_full_jid.bare_jid);
|
||||||
.put_node(new StanzaNode.text(Base64.encode(security_params.secret.initialization_vector)))));
|
|
||||||
|
|
||||||
plugin.trust_manager.encrypt_key(header_node, security_params.secret.transport_key, local_full_jid.bare_jid, new ArrayList<Jid>.wrap(new Jid[] {peer_full_jid.bare_jid}), stream, account);
|
security.put_node(encryption_data.get_encrypted_node());
|
||||||
security.put_node(encrypted_node);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override string get_ns() { return NS_URI; }
|
public override string get_ns() { return NS_URI; }
|
||||||
|
|
211
plugins/omemo/src/logic/decrypt.vala
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
using Dino.Entities;
|
||||||
|
using Qlite;
|
||||||
|
using Gee;
|
||||||
|
using Signal;
|
||||||
|
using Xmpp;
|
||||||
|
|
||||||
|
namespace Dino.Plugins.Omemo {
|
||||||
|
|
||||||
|
public class OmemoDecryptor : Xep.Omemo.OmemoDecryptor {
|
||||||
|
|
||||||
|
private Account account;
|
||||||
|
private Store store;
|
||||||
|
private Database db;
|
||||||
|
private StreamInteractor stream_interactor;
|
||||||
|
private TrustManager trust_manager;
|
||||||
|
|
||||||
|
public override uint32 own_device_id { get { return store.local_registration_id; }}
|
||||||
|
|
||||||
|
public OmemoDecryptor(Account account, StreamInteractor stream_interactor, TrustManager trust_manager, Database db, Store store) {
|
||||||
|
this.account = account;
|
||||||
|
this.stream_interactor = stream_interactor;
|
||||||
|
this.trust_manager = trust_manager;
|
||||||
|
this.db = db;
|
||||||
|
this.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool decrypt_message(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
||||||
|
StanzaNode? encrypted_node = stanza.stanza.get_subnode("encrypted", NS_URI);
|
||||||
|
if (encrypted_node == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false;
|
||||||
|
|
||||||
|
if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == NS_URI) {
|
||||||
|
message.body = "[This message is OMEMO encrypted]"; // TODO temporary
|
||||||
|
}
|
||||||
|
if (!Plugin.ensure_context()) return false;
|
||||||
|
int identity_id = db.identity.get_id(conversation.account.id);
|
||||||
|
|
||||||
|
MessageFlag flag = new MessageFlag();
|
||||||
|
stanza.add_flag(flag);
|
||||||
|
|
||||||
|
Xep.Omemo.ParsedData? data = parse_node(encrypted_node);
|
||||||
|
if (data == null || data.ciphertext == null) return false;
|
||||||
|
|
||||||
|
|
||||||
|
foreach (Bytes encr_key in data.our_potential_encrypted_keys.keys) {
|
||||||
|
data.is_prekey = data.our_potential_encrypted_keys[encr_key];
|
||||||
|
data.encrypted_key = encr_key.get_data();
|
||||||
|
Gee.List<Jid> possible_jids = get_potential_message_jids(message, data, identity_id);
|
||||||
|
if (possible_jids.size == 0) {
|
||||||
|
debug("Received message from unknown entity with device id %d", data.sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (Jid possible_jid in possible_jids) {
|
||||||
|
try {
|
||||||
|
uint8[] key = decrypt_key(data, possible_jid);
|
||||||
|
string cleartext = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, data.iv, data.ciphertext));
|
||||||
|
|
||||||
|
// If we figured out which real jid a message comes from due to decryption working, save it
|
||||||
|
if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) {
|
||||||
|
message.real_jid = possible_jid;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.body = cleartext;
|
||||||
|
message.encryption = Encryption.OMEMO;
|
||||||
|
|
||||||
|
trust_manager.message_device_id_map[message] = data.sid;
|
||||||
|
return true;
|
||||||
|
} catch (Error e) {
|
||||||
|
debug("Decrypting message from %s/%d failed: %s", possible_jid.to_string(), data.sid, e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
encrypted_node.get_deep_string_content("payload") != null && // Ratchet forwarding doesn't contain payload and might not include us, which is ok
|
||||||
|
data.our_potential_encrypted_keys.size == 0 && // The message was not encrypted to us
|
||||||
|
stream_interactor.module_manager.get_module(message.account, StreamModule.IDENTITY).store.local_registration_id != data.sid // Message from this device. Never encrypted to itself.
|
||||||
|
) {
|
||||||
|
db.identity_meta.update_last_message_undecryptable(identity_id, data.sid, message.time);
|
||||||
|
trust_manager.bad_message_state_updated(conversation.account, message.from, data.sid);
|
||||||
|
}
|
||||||
|
|
||||||
|
debug("Received OMEMO encryped message that could not be decrypted.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gee.List<Jid> get_potential_message_jids(Entities.Message message, Xmpp.Xep.Omemo.ParsedData data, int identity_id) {
|
||||||
|
Gee.List<Jid> possible_jids = new ArrayList<Jid>();
|
||||||
|
if (message.type_ == Message.Type.CHAT) {
|
||||||
|
possible_jids.add(message.from.bare_jid);
|
||||||
|
} else {
|
||||||
|
if (message.real_jid != null) {
|
||||||
|
possible_jids.add(message.real_jid.bare_jid);
|
||||||
|
} else if (data.is_prekey) {
|
||||||
|
// pre key messages do store the identity key, so we can use that to find the real jid
|
||||||
|
PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(data.encrypted_key);
|
||||||
|
string identity_key = Base64.encode(msg.identity_key.serialize());
|
||||||
|
foreach (Row row in db.identity_meta.get_with_device_id(identity_id, data.sid).with(db.identity_meta.identity_key_public_base64, "=", identity_key)) {
|
||||||
|
try {
|
||||||
|
possible_jids.add(new Jid(row[db.identity_meta.address_name]));
|
||||||
|
} catch (InvalidJidError e) {
|
||||||
|
warning("Ignoring invalid jid from database: %s", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If we don't know the device name (MUC history w/o MAM), test decryption with all keys with fitting device id
|
||||||
|
foreach (Row row in db.identity_meta.get_with_device_id(identity_id, data.sid)) {
|
||||||
|
try {
|
||||||
|
possible_jids.add(new Jid(row[db.identity_meta.address_name]));
|
||||||
|
} catch (InvalidJidError e) {
|
||||||
|
warning("Ignoring invalid jid from database: %s", e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return possible_jids;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override uint8[] decrypt_key(Xmpp.Xep.Omemo.ParsedData data, Jid from_jid) throws GLib.Error {
|
||||||
|
int sid = data.sid;
|
||||||
|
uint8[] ciphertext = data.ciphertext;
|
||||||
|
uint8[] encrypted_key = data.encrypted_key;
|
||||||
|
|
||||||
|
Address address = new Address(from_jid.to_string(), sid);
|
||||||
|
uint8[] key;
|
||||||
|
|
||||||
|
if (data.is_prekey) {
|
||||||
|
int identity_id = db.identity.get_id(account.id);
|
||||||
|
PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(encrypted_key);
|
||||||
|
string identity_key = Base64.encode(msg.identity_key.serialize());
|
||||||
|
|
||||||
|
bool ok = update_db_for_prekey(identity_id, identity_key, from_jid, sid);
|
||||||
|
if (!ok) return null;
|
||||||
|
|
||||||
|
debug("Starting new session for decryption with device from %s/%d", from_jid.to_string(), sid);
|
||||||
|
SessionCipher cipher = store.create_session_cipher(address);
|
||||||
|
key = cipher.decrypt_pre_key_signal_message(msg);
|
||||||
|
// TODO: Finish session
|
||||||
|
} else {
|
||||||
|
debug("Continuing session for decryption with device from %s/%d", from_jid.to_string(), sid);
|
||||||
|
SignalMessage msg = Plugin.get_context().deserialize_signal_message(encrypted_key);
|
||||||
|
SessionCipher cipher = store.create_session_cipher(address);
|
||||||
|
key = cipher.decrypt_signal_message(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.length >= 32) {
|
||||||
|
int authtaglength = key.length - 16;
|
||||||
|
uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength];
|
||||||
|
uint8[] new_key = new uint8[16];
|
||||||
|
Memory.copy(new_ciphertext, ciphertext, ciphertext.length);
|
||||||
|
Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength);
|
||||||
|
Memory.copy(new_key, key, 16);
|
||||||
|
data.ciphertext = new_ciphertext;
|
||||||
|
key = new_key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string decrypt(uint8[] ciphertext, uint8[] key, uint8[] iv) throws GLib.Error {
|
||||||
|
return arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext));
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool update_db_for_prekey(int identity_id, string identity_key, Jid from_jid, int sid) {
|
||||||
|
Row? device = db.identity_meta.get_device(identity_id, from_jid.to_string(), sid);
|
||||||
|
if (device != null && device[db.identity_meta.identity_key_public_base64] != null) {
|
||||||
|
if (device[db.identity_meta.identity_key_public_base64] != identity_key) {
|
||||||
|
critical("Tried to use a different identity key for a known device id.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug("Learn new device from incoming message from %s/%d", from_jid.to_string(), sid);
|
||||||
|
bool blind_trust = db.trust.get_blind_trust(identity_id, from_jid.to_string(), true);
|
||||||
|
if (db.identity_meta.insert_device_session(identity_id, from_jid.to_string(), sid, identity_key, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) {
|
||||||
|
critical("Failed learning a device.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
XmppStream? stream = stream_interactor.get_stream(account);
|
||||||
|
if (device == null && stream != null) {
|
||||||
|
stream.get_module(StreamModule.IDENTITY).request_user_devicelist.begin(stream, from_jid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string arr_to_str(uint8[] arr) {
|
||||||
|
// null-terminate the array
|
||||||
|
uint8[] rarr = new uint8[arr.length+1];
|
||||||
|
Memory.copy(rarr, arr, arr.length);
|
||||||
|
return (string)rarr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DecryptMessageListener : MessageListener {
|
||||||
|
public string[] after_actions_const = new string[]{ };
|
||||||
|
public override string action_group { get { return "DECRYPT"; } }
|
||||||
|
public override string[] after_actions { get { return after_actions_const; } }
|
||||||
|
|
||||||
|
private HashMap<Account, OmemoDecryptor> decryptors;
|
||||||
|
|
||||||
|
public DecryptMessageListener(HashMap<Account, OmemoDecryptor> decryptors) {
|
||||||
|
this.decryptors = decryptors;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
||||||
|
decryptors[message.account].decrypt_message(message, stanza, conversation);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
131
plugins/omemo/src/logic/encrypt.vala
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
using Gee;
|
||||||
|
using Signal;
|
||||||
|
using Dino.Entities;
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep.Omemo;
|
||||||
|
|
||||||
|
namespace Dino.Plugins.Omemo {
|
||||||
|
|
||||||
|
public class OmemoEncryptor : Xep.Omemo.OmemoEncryptor {
|
||||||
|
|
||||||
|
private Account account;
|
||||||
|
private Store store;
|
||||||
|
private TrustManager trust_manager;
|
||||||
|
|
||||||
|
public override uint32 own_device_id { get { return store.local_registration_id; }}
|
||||||
|
|
||||||
|
public OmemoEncryptor(Account account, TrustManager trust_manager, Store store) {
|
||||||
|
this.account = account;
|
||||||
|
this.trust_manager = trust_manager;
|
||||||
|
this.store = store;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Xep.Omemo.EncryptionData encrypt_plaintext(string plaintext) throws GLib.Error {
|
||||||
|
const uint KEY_SIZE = 16;
|
||||||
|
const uint IV_SIZE = 12;
|
||||||
|
|
||||||
|
//Create a key and use it to encrypt the message
|
||||||
|
uint8[] key = new uint8[KEY_SIZE];
|
||||||
|
Plugin.get_context().randomize(key);
|
||||||
|
uint8[] iv = new uint8[IV_SIZE];
|
||||||
|
Plugin.get_context().randomize(iv);
|
||||||
|
|
||||||
|
uint8[] aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, plaintext.data);
|
||||||
|
uint8[] ciphertext = aes_encrypt_result[0:aes_encrypt_result.length - 16];
|
||||||
|
uint8[] tag = aes_encrypt_result[aes_encrypt_result.length - 16:aes_encrypt_result.length];
|
||||||
|
uint8[] keytag = new uint8[key.length + tag.length];
|
||||||
|
Memory.copy(keytag, key, key.length);
|
||||||
|
Memory.copy((uint8*)keytag + key.length, tag, tag.length);
|
||||||
|
|
||||||
|
var ret = new Xep.Omemo.EncryptionData(own_device_id);
|
||||||
|
ret.ciphertext = ciphertext;
|
||||||
|
ret.keytag = keytag;
|
||||||
|
ret.iv = iv;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
public EncryptState encrypt(MessageStanza message, Jid self_jid, Gee.List<Jid> recipients, XmppStream stream) {
|
||||||
|
|
||||||
|
EncryptState status = new EncryptState();
|
||||||
|
if (!Plugin.ensure_context()) return status;
|
||||||
|
if (message.to == null) return status;
|
||||||
|
|
||||||
|
try {
|
||||||
|
EncryptionData enc_data = encrypt_plaintext(message.body);
|
||||||
|
status = encrypt_key_to_recipients(enc_data, self_jid, recipients, stream);
|
||||||
|
|
||||||
|
message.stanza.put_node(enc_data.get_encrypted_node());
|
||||||
|
Xep.ExplicitEncryption.add_encryption_tag_to_message(message, NS_URI, "OMEMO");
|
||||||
|
message.body = "[This message is OMEMO encrypted]";
|
||||||
|
status.encrypted = true;
|
||||||
|
} catch (Error e) {
|
||||||
|
warning(@"Signal error while encrypting message: $(e.message)\n");
|
||||||
|
message.body = "[OMEMO encryption failed]";
|
||||||
|
status.encrypted = false;
|
||||||
|
}
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal EncryptState encrypt_key_to_recipients(EncryptionData enc_data, Jid self_jid, Gee.List<Jid> recipients, XmppStream stream) throws Error {
|
||||||
|
EncryptState status = new EncryptState();
|
||||||
|
|
||||||
|
//Check we have the bundles and device lists needed to send the message
|
||||||
|
if (!trust_manager.is_known_address(account, self_jid)) return status;
|
||||||
|
status.own_list = true;
|
||||||
|
status.own_devices = trust_manager.get_trusted_devices(account, self_jid).size;
|
||||||
|
status.other_waiting_lists = 0;
|
||||||
|
status.other_devices = 0;
|
||||||
|
foreach (Jid recipient in recipients) {
|
||||||
|
if (!trust_manager.is_known_address(account, recipient)) {
|
||||||
|
status.other_waiting_lists++;
|
||||||
|
}
|
||||||
|
if (status.other_waiting_lists > 0) return status;
|
||||||
|
status.other_devices += trust_manager.get_trusted_devices(account, recipient).size;
|
||||||
|
}
|
||||||
|
if (status.own_devices == 0 || status.other_devices == 0) return status;
|
||||||
|
|
||||||
|
|
||||||
|
//Encrypt the key for each recipient's device individually
|
||||||
|
foreach (Jid recipient in recipients) {
|
||||||
|
EncryptionResult enc_res = encrypt_key_to_recipient(stream, enc_data, recipient);
|
||||||
|
status.add_result(enc_res, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt the key for each own device
|
||||||
|
EncryptionResult enc_res = encrypt_key_to_recipient(stream, enc_data, self_jid);
|
||||||
|
status.add_result(enc_res, true);
|
||||||
|
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override EncryptionResult encrypt_key_to_recipient(XmppStream stream, Xep.Omemo.EncryptionData enc_data, Jid recipient) throws GLib.Error {
|
||||||
|
var result = new EncryptionResult();
|
||||||
|
StreamModule module = stream.get_module(StreamModule.IDENTITY);
|
||||||
|
|
||||||
|
foreach(int32 device_id in trust_manager.get_trusted_devices(account, recipient)) {
|
||||||
|
if (module.is_ignored_device(recipient, device_id)) {
|
||||||
|
result.lost++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
encrypt_key(enc_data, recipient, device_id);
|
||||||
|
result.success++;
|
||||||
|
} catch (Error e) {
|
||||||
|
if (e.code == ErrorCode.UNKNOWN) result.unknown++;
|
||||||
|
else result.failure++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void encrypt_key(Xep.Omemo.EncryptionData encryption_data, Jid jid, int32 device_id) throws GLib.Error {
|
||||||
|
Address address = new Address(jid.to_string(), device_id);
|
||||||
|
SessionCipher cipher = store.create_session_cipher(address);
|
||||||
|
CiphertextMessage device_key = cipher.encrypt(encryption_data.keytag);
|
||||||
|
address.device_id = 0;
|
||||||
|
debug("Created encrypted key for %s/%d", jid.to_string(), device_id);
|
||||||
|
|
||||||
|
encryption_data.add_device_key(device_id, device_key.serialized, device_key.type == CiphertextType.PREKEY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,24 +0,0 @@
|
||||||
namespace Dino.Plugins.Omemo {
|
|
||||||
|
|
||||||
public class EncryptState {
|
|
||||||
public bool encrypted { get; internal set; }
|
|
||||||
public int other_devices { get; internal set; }
|
|
||||||
public int other_success { get; internal set; }
|
|
||||||
public int other_lost { get; internal set; }
|
|
||||||
public int other_unknown { get; internal set; }
|
|
||||||
public int other_failure { get; internal set; }
|
|
||||||
public int other_waiting_lists { get; internal set; }
|
|
||||||
|
|
||||||
public int own_devices { get; internal set; }
|
|
||||||
public int own_success { get; internal set; }
|
|
||||||
public int own_lost { get; internal set; }
|
|
||||||
public int own_unknown { get; internal set; }
|
|
||||||
public int own_failure { get; internal set; }
|
|
||||||
public bool own_list { get; internal set; }
|
|
||||||
|
|
||||||
public string to_string() {
|
|
||||||
return @"EncryptState (encrypted=$encrypted, other=(devices=$other_devices, success=$other_success, lost=$other_lost, unknown=$other_unknown, failure=$other_failure, waiting_lists=$other_waiting_lists, own=(devices=$own_devices, success=$own_success, lost=$own_lost, unknown=$own_unknown, failure=$own_failure, list=$own_list))";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -13,11 +13,12 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
private StreamInteractor stream_interactor;
|
private StreamInteractor stream_interactor;
|
||||||
private Database db;
|
private Database db;
|
||||||
private TrustManager trust_manager;
|
private TrustManager trust_manager;
|
||||||
|
private HashMap<Account, OmemoEncryptor> encryptors;
|
||||||
private Map<Entities.Message, MessageState> message_states = new HashMap<Entities.Message, MessageState>(Entities.Message.hash_func, Entities.Message.equals_func);
|
private Map<Entities.Message, MessageState> message_states = new HashMap<Entities.Message, MessageState>(Entities.Message.hash_func, Entities.Message.equals_func);
|
||||||
|
|
||||||
private class MessageState {
|
private class MessageState {
|
||||||
public Entities.Message msg { get; private set; }
|
public Entities.Message msg { get; private set; }
|
||||||
public EncryptState last_try { get; private set; }
|
public Xep.Omemo.EncryptState last_try { get; private set; }
|
||||||
public int waiting_other_sessions { get; set; }
|
public int waiting_other_sessions { get; set; }
|
||||||
public int waiting_own_sessions { get; set; }
|
public int waiting_own_sessions { get; set; }
|
||||||
public bool waiting_own_devicelist { get; set; }
|
public bool waiting_own_devicelist { get; set; }
|
||||||
|
@ -26,11 +27,11 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
public bool will_send_now { get; private set; }
|
public bool will_send_now { get; private set; }
|
||||||
public bool active_send_attempt { get; set; }
|
public bool active_send_attempt { get; set; }
|
||||||
|
|
||||||
public MessageState(Entities.Message msg, EncryptState last_try) {
|
public MessageState(Entities.Message msg, Xep.Omemo.EncryptState last_try) {
|
||||||
update_from_encrypt_status(msg, last_try);
|
update_from_encrypt_status(msg, last_try);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update_from_encrypt_status(Entities.Message msg, EncryptState new_try) {
|
public void update_from_encrypt_status(Entities.Message msg, Xep.Omemo.EncryptState new_try) {
|
||||||
this.msg = msg;
|
this.msg = msg;
|
||||||
this.last_try = new_try;
|
this.last_try = new_try;
|
||||||
this.waiting_other_sessions = new_try.other_unknown;
|
this.waiting_other_sessions = new_try.other_unknown;
|
||||||
|
@ -59,10 +60,11 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Manager(StreamInteractor stream_interactor, Database db, TrustManager trust_manager) {
|
private Manager(StreamInteractor stream_interactor, Database db, TrustManager trust_manager, HashMap<Account, OmemoEncryptor> encryptors) {
|
||||||
this.stream_interactor = stream_interactor;
|
this.stream_interactor = stream_interactor;
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.trust_manager = trust_manager;
|
this.trust_manager = trust_manager;
|
||||||
|
this.encryptors = encryptors;
|
||||||
|
|
||||||
stream_interactor.stream_negotiated.connect(on_stream_negotiated);
|
stream_interactor.stream_negotiated.connect(on_stream_negotiated);
|
||||||
stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(on_pre_message_send);
|
stream_interactor.get_module(MessageProcessor.IDENTITY).pre_message_send.connect(on_pre_message_send);
|
||||||
|
@ -125,7 +127,7 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
}
|
}
|
||||||
|
|
||||||
//Attempt to encrypt the message
|
//Attempt to encrypt the message
|
||||||
EncryptState enc_state = trust_manager.encrypt(message_stanza, conversation.account.bare_jid, recipients, stream, conversation.account);
|
Xep.Omemo.EncryptState enc_state = encryptors[conversation.account].encrypt(message_stanza, conversation.account.bare_jid, recipients, stream);
|
||||||
MessageState state;
|
MessageState state;
|
||||||
lock (message_states) {
|
lock (message_states) {
|
||||||
if (message_states.has_key(message)) {
|
if (message_states.has_key(message)) {
|
||||||
|
@ -411,8 +413,8 @@ public class Manager : StreamInteractionModule, Object {
|
||||||
return true; // TODO wait for stream?
|
return true; // TODO wait for stream?
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void start(StreamInteractor stream_interactor, Database db, TrustManager trust_manager) {
|
public static void start(StreamInteractor stream_interactor, Database db, TrustManager trust_manager, HashMap<Account, OmemoEncryptor> encryptors) {
|
||||||
Manager m = new Manager(stream_interactor, db, trust_manager);
|
Manager m = new Manager(stream_interactor, db, trust_manager, encryptors);
|
||||||
stream_interactor.add_module(m);
|
stream_interactor.add_module(m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,18 +12,15 @@ public class TrustManager {
|
||||||
|
|
||||||
private StreamInteractor stream_interactor;
|
private StreamInteractor stream_interactor;
|
||||||
private Database db;
|
private Database db;
|
||||||
private DecryptMessageListener decrypt_message_listener;
|
|
||||||
private TagMessageListener tag_message_listener;
|
private TagMessageListener tag_message_listener;
|
||||||
|
|
||||||
private HashMap<Message, int> message_device_id_map = new HashMap<Message, int>(Message.hash_func, Message.equals_func);
|
public HashMap<Message, int> message_device_id_map = new HashMap<Message, int>(Message.hash_func, Message.equals_func);
|
||||||
|
|
||||||
public TrustManager(StreamInteractor stream_interactor, Database db) {
|
public TrustManager(StreamInteractor stream_interactor, Database db) {
|
||||||
this.stream_interactor = stream_interactor;
|
this.stream_interactor = stream_interactor;
|
||||||
this.db = db;
|
this.db = db;
|
||||||
|
|
||||||
decrypt_message_listener = new DecryptMessageListener(stream_interactor, this, db, message_device_id_map);
|
|
||||||
tag_message_listener = new TagMessageListener(stream_interactor, this, db, message_device_id_map);
|
tag_message_listener = new TagMessageListener(stream_interactor, this, db, message_device_id_map);
|
||||||
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(decrypt_message_listener);
|
|
||||||
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(tag_message_listener);
|
stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(tag_message_listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,127 +66,6 @@ public class TrustManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private StanzaNode create_encrypted_key_node(uint8[] key, Address address, Store store) throws GLib.Error {
|
|
||||||
SessionCipher cipher = store.create_session_cipher(address);
|
|
||||||
CiphertextMessage device_key = cipher.encrypt(key);
|
|
||||||
debug("Created encrypted key for %s/%d", address.name, address.device_id);
|
|
||||||
StanzaNode key_node = new StanzaNode.build("key", NS_URI)
|
|
||||||
.put_attribute("rid", address.device_id.to_string())
|
|
||||||
.put_node(new StanzaNode.text(Base64.encode(device_key.serialized)));
|
|
||||||
if (device_key.type == CiphertextType.PREKEY) key_node.put_attribute("prekey", "true");
|
|
||||||
return key_node;
|
|
||||||
}
|
|
||||||
|
|
||||||
internal EncryptState encrypt_key(StanzaNode header_node, uint8[] keytag, Jid self_jid, Gee.List<Jid> recipients, XmppStream stream, Account account) throws Error {
|
|
||||||
EncryptState status = new EncryptState();
|
|
||||||
StreamModule module = stream.get_module(StreamModule.IDENTITY);
|
|
||||||
|
|
||||||
//Check we have the bundles and device lists needed to send the message
|
|
||||||
if (!is_known_address(account, self_jid)) return status;
|
|
||||||
status.own_list = true;
|
|
||||||
status.own_devices = get_trusted_devices(account, self_jid).size;
|
|
||||||
status.other_waiting_lists = 0;
|
|
||||||
status.other_devices = 0;
|
|
||||||
foreach (Jid recipient in recipients) {
|
|
||||||
if (!is_known_address(account, recipient)) {
|
|
||||||
status.other_waiting_lists++;
|
|
||||||
}
|
|
||||||
if (status.other_waiting_lists > 0) return status;
|
|
||||||
status.other_devices += get_trusted_devices(account, recipient).size;
|
|
||||||
}
|
|
||||||
if (status.own_devices == 0 || status.other_devices == 0) return status;
|
|
||||||
|
|
||||||
|
|
||||||
//Encrypt the key for each recipient's device individually
|
|
||||||
Address address = new Address("", 0);
|
|
||||||
foreach (Jid recipient in recipients) {
|
|
||||||
foreach(int32 device_id in get_trusted_devices(account, recipient)) {
|
|
||||||
if (module.is_ignored_device(recipient, device_id)) {
|
|
||||||
status.other_lost++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
address.name = recipient.bare_jid.to_string();
|
|
||||||
address.device_id = (int) device_id;
|
|
||||||
StanzaNode key_node = create_encrypted_key_node(keytag, address, module.store);
|
|
||||||
header_node.put_node(key_node);
|
|
||||||
status.other_success++;
|
|
||||||
} catch (Error e) {
|
|
||||||
if (e.code == ErrorCode.UNKNOWN) status.other_unknown++;
|
|
||||||
else status.other_failure++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Encrypt the key for each own device
|
|
||||||
address.name = self_jid.bare_jid.to_string();
|
|
||||||
foreach(int32 device_id in get_trusted_devices(account, self_jid)) {
|
|
||||||
if (module.is_ignored_device(self_jid, device_id)) {
|
|
||||||
status.own_lost++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (device_id != module.store.local_registration_id) {
|
|
||||||
address.device_id = (int) device_id;
|
|
||||||
try {
|
|
||||||
StanzaNode key_node = create_encrypted_key_node(keytag, address, module.store);
|
|
||||||
header_node.put_node(key_node);
|
|
||||||
status.own_success++;
|
|
||||||
} catch (Error e) {
|
|
||||||
if (e.code == ErrorCode.UNKNOWN) status.own_unknown++;
|
|
||||||
else status.own_failure++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EncryptState encrypt(MessageStanza message, Jid self_jid, Gee.List<Jid> recipients, XmppStream stream, Account account) {
|
|
||||||
const uint KEY_SIZE = 16;
|
|
||||||
const uint IV_SIZE = 12;
|
|
||||||
EncryptState status = new EncryptState();
|
|
||||||
if (!Plugin.ensure_context()) return status;
|
|
||||||
if (message.to == null) return status;
|
|
||||||
|
|
||||||
StreamModule module = stream.get_module(StreamModule.IDENTITY);
|
|
||||||
|
|
||||||
try {
|
|
||||||
//Create a key and use it to encrypt the message
|
|
||||||
uint8[] key = new uint8[KEY_SIZE];
|
|
||||||
Plugin.get_context().randomize(key);
|
|
||||||
uint8[] iv = new uint8[IV_SIZE];
|
|
||||||
Plugin.get_context().randomize(iv);
|
|
||||||
|
|
||||||
uint8[] aes_encrypt_result = aes_encrypt(Cipher.AES_GCM_NOPADDING, key, iv, message.body.data);
|
|
||||||
uint8[] ciphertext = aes_encrypt_result[0:aes_encrypt_result.length-16];
|
|
||||||
uint8[] tag = aes_encrypt_result[aes_encrypt_result.length-16:aes_encrypt_result.length];
|
|
||||||
uint8[] keytag = new uint8[key.length + tag.length];
|
|
||||||
Memory.copy(keytag, key, key.length);
|
|
||||||
Memory.copy((uint8*)keytag + key.length, tag, tag.length);
|
|
||||||
|
|
||||||
StanzaNode header_node;
|
|
||||||
StanzaNode encrypted_node = new StanzaNode.build("encrypted", NS_URI).add_self_xmlns()
|
|
||||||
.put_node(header_node = new StanzaNode.build("header", NS_URI)
|
|
||||||
.put_attribute("sid", module.store.local_registration_id.to_string())
|
|
||||||
.put_node(new StanzaNode.build("iv", NS_URI)
|
|
||||||
.put_node(new StanzaNode.text(Base64.encode(iv)))))
|
|
||||||
.put_node(new StanzaNode.build("payload", NS_URI)
|
|
||||||
.put_node(new StanzaNode.text(Base64.encode(ciphertext))));
|
|
||||||
|
|
||||||
status = encrypt_key(header_node, keytag, self_jid, recipients, stream, account);
|
|
||||||
|
|
||||||
message.stanza.put_node(encrypted_node);
|
|
||||||
Xep.ExplicitEncryption.add_encryption_tag_to_message(message, NS_URI, "OMEMO");
|
|
||||||
message.body = "[This message is OMEMO encrypted]";
|
|
||||||
status.encrypted = true;
|
|
||||||
} catch (Error e) {
|
|
||||||
warning(@"Signal error while encrypting message: $(e.message)\n");
|
|
||||||
message.body = "[OMEMO encryption failed]";
|
|
||||||
status.encrypted = false;
|
|
||||||
}
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool is_known_address(Account account, Jid jid) {
|
public bool is_known_address(Account account, Jid jid) {
|
||||||
int identity_id = db.identity.get_id(account.id);
|
int identity_id = db.identity.get_id(account.id);
|
||||||
if (identity_id < 0) return false;
|
if (identity_id < 0) return false;
|
||||||
|
@ -260,182 +136,6 @@ public class TrustManager {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DecryptMessageListener : MessageListener {
|
|
||||||
public string[] after_actions_const = new string[]{ };
|
|
||||||
public override string action_group { get { return "DECRYPT"; } }
|
|
||||||
public override string[] after_actions { get { return after_actions_const; } }
|
|
||||||
|
|
||||||
private StreamInteractor stream_interactor;
|
|
||||||
private TrustManager trust_manager;
|
|
||||||
private Database db;
|
|
||||||
private HashMap<Message, int> message_device_id_map;
|
|
||||||
|
|
||||||
public DecryptMessageListener(StreamInteractor stream_interactor, TrustManager trust_manager, Database db, HashMap<Message, int> message_device_id_map) {
|
|
||||||
this.stream_interactor = stream_interactor;
|
|
||||||
this.trust_manager = trust_manager;
|
|
||||||
this.db = db;
|
|
||||||
this.message_device_id_map = message_device_id_map;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override async bool run(Entities.Message message, Xmpp.MessageStanza stanza, Conversation conversation) {
|
|
||||||
StreamModule module = stream_interactor.module_manager.get_module(conversation.account, StreamModule.IDENTITY);
|
|
||||||
Store store = module.store;
|
|
||||||
|
|
||||||
StanzaNode? _encrypted = stanza.stanza.get_subnode("encrypted", NS_URI);
|
|
||||||
if (_encrypted == null || MessageFlag.get_flag(stanza) != null || stanza.from == null) return false;
|
|
||||||
StanzaNode encrypted = (!)_encrypted;
|
|
||||||
if (message.body == null && Xep.ExplicitEncryption.get_encryption_tag(stanza) == NS_URI) {
|
|
||||||
message.body = "[This message is OMEMO encrypted]"; // TODO temporary
|
|
||||||
};
|
|
||||||
if (!Plugin.ensure_context()) return false;
|
|
||||||
int identity_id = db.identity.get_id(conversation.account.id);
|
|
||||||
MessageFlag flag = new MessageFlag();
|
|
||||||
stanza.add_flag(flag);
|
|
||||||
StanzaNode? _header = encrypted.get_subnode("header");
|
|
||||||
if (_header == null) return false;
|
|
||||||
StanzaNode header = (!)_header;
|
|
||||||
int sid = header.get_attribute_int("sid");
|
|
||||||
if (sid <= 0) return false;
|
|
||||||
|
|
||||||
var our_nodes = new ArrayList<StanzaNode>();
|
|
||||||
foreach (StanzaNode key_node in header.get_subnodes("key")) {
|
|
||||||
debug("Is ours? %d =? %u", key_node.get_attribute_int("rid"), store.local_registration_id);
|
|
||||||
if (key_node.get_attribute_int("rid") == store.local_registration_id) {
|
|
||||||
our_nodes.add(key_node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string? payload = encrypted.get_deep_string_content("payload");
|
|
||||||
string? iv_node = header.get_deep_string_content("iv");
|
|
||||||
|
|
||||||
foreach (StanzaNode key_node in our_nodes) {
|
|
||||||
string? key_node_content = key_node.get_string_content();
|
|
||||||
if (payload == null || iv_node == null || key_node_content == null) continue;
|
|
||||||
uint8[] key;
|
|
||||||
uint8[] ciphertext = Base64.decode((!)payload);
|
|
||||||
uint8[] iv = Base64.decode((!)iv_node);
|
|
||||||
Gee.List<Jid> possible_jids = new ArrayList<Jid>();
|
|
||||||
if (conversation.type_ == Conversation.Type.CHAT) {
|
|
||||||
possible_jids.add(stanza.from.bare_jid);
|
|
||||||
} else {
|
|
||||||
Jid? real_jid = message.real_jid;
|
|
||||||
if (real_jid != null) {
|
|
||||||
possible_jids.add(real_jid.bare_jid);
|
|
||||||
} else if (key_node.get_attribute_bool("prekey")) {
|
|
||||||
// pre key messages do store the identity key, so we can use that to find the real jid
|
|
||||||
PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content));
|
|
||||||
string identity_key = Base64.encode(msg.identity_key.serialize());
|
|
||||||
foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid).with(db.identity_meta.identity_key_public_base64, "=", identity_key)) {
|
|
||||||
try {
|
|
||||||
possible_jids.add(new Jid(row[db.identity_meta.address_name]));
|
|
||||||
} catch (InvalidJidError e) {
|
|
||||||
warning("Ignoring invalid jid from database: %s", e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (possible_jids.size != 1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If we don't know the device name (MUC history w/o MAM), test decryption with all keys with fitting device id
|
|
||||||
foreach (Row row in db.identity_meta.get_with_device_id(identity_id, sid)) {
|
|
||||||
try {
|
|
||||||
possible_jids.add(new Jid(row[db.identity_meta.address_name]));
|
|
||||||
} catch (InvalidJidError e) {
|
|
||||||
warning("Ignoring invalid jid from database: %s", e.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (possible_jids.size == 0) {
|
|
||||||
debug("Received message from unknown entity with device id %d", sid);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (Jid possible_jid in possible_jids) {
|
|
||||||
try {
|
|
||||||
Address address = new Address(possible_jid.to_string(), sid);
|
|
||||||
if (key_node.get_attribute_bool("prekey")) {
|
|
||||||
Row? device = db.identity_meta.get_device(identity_id, possible_jid.to_string(), sid);
|
|
||||||
PreKeySignalMessage msg = Plugin.get_context().deserialize_pre_key_signal_message(Base64.decode((!)key_node_content));
|
|
||||||
string identity_key = Base64.encode(msg.identity_key.serialize());
|
|
||||||
if (device != null && device[db.identity_meta.identity_key_public_base64] != null) {
|
|
||||||
if (device[db.identity_meta.identity_key_public_base64] != identity_key) {
|
|
||||||
critical("Tried to use a different identity key for a known device id.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
debug("Learn new device from incoming message from %s/%d", possible_jid.to_string(), sid);
|
|
||||||
bool blind_trust = db.trust.get_blind_trust(identity_id, possible_jid.to_string(), true);
|
|
||||||
if (db.identity_meta.insert_device_session(identity_id, possible_jid.to_string(), sid, identity_key, blind_trust ? TrustLevel.TRUSTED : TrustLevel.UNKNOWN) < 0) {
|
|
||||||
critical("Failed learning a device.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
XmppStream? stream = stream_interactor.get_stream(conversation.account);
|
|
||||||
if (device == null && stream != null) {
|
|
||||||
module.request_user_devicelist.begin(stream, possible_jid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
debug("Starting new session for decryption with device from %s/%d", possible_jid.to_string(), sid);
|
|
||||||
SessionCipher cipher = store.create_session_cipher(address);
|
|
||||||
key = cipher.decrypt_pre_key_signal_message(msg);
|
|
||||||
// TODO: Finish session
|
|
||||||
} else {
|
|
||||||
debug("Continuing session for decryption with device from %s/%d", possible_jid.to_string(), sid);
|
|
||||||
SignalMessage msg = Plugin.get_context().deserialize_signal_message(Base64.decode((!)key_node_content));
|
|
||||||
SessionCipher cipher = store.create_session_cipher(address);
|
|
||||||
key = cipher.decrypt_signal_message(msg);
|
|
||||||
}
|
|
||||||
//address.device_id = 0; // TODO: Hack to have address obj live longer
|
|
||||||
|
|
||||||
if (key.length >= 32) {
|
|
||||||
int authtaglength = key.length - 16;
|
|
||||||
uint8[] new_ciphertext = new uint8[ciphertext.length + authtaglength];
|
|
||||||
uint8[] new_key = new uint8[16];
|
|
||||||
Memory.copy(new_ciphertext, ciphertext, ciphertext.length);
|
|
||||||
Memory.copy((uint8*)new_ciphertext + ciphertext.length, (uint8*)key + 16, authtaglength);
|
|
||||||
Memory.copy(new_key, key, 16);
|
|
||||||
ciphertext = new_ciphertext;
|
|
||||||
key = new_key;
|
|
||||||
}
|
|
||||||
|
|
||||||
message.body = arr_to_str(aes_decrypt(Cipher.AES_GCM_NOPADDING, key, iv, ciphertext));
|
|
||||||
message_device_id_map[message] = address.device_id;
|
|
||||||
message.encryption = Encryption.OMEMO;
|
|
||||||
flag.decrypted = true;
|
|
||||||
} catch (Error e) {
|
|
||||||
debug("Decrypting message from %s/%d failed: %s", possible_jid.to_string(), sid, e.message);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we figured out which real jid a message comes from due to decryption working, save it
|
|
||||||
if (conversation.type_ == Conversation.Type.GROUPCHAT && message.real_jid == null) {
|
|
||||||
message.real_jid = possible_jid;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
payload != null && // Ratchet forwarding doesn't contain payload and might not include us, which is ok
|
|
||||||
our_nodes.size == 0 && // The message was not encrypted to us
|
|
||||||
module.store.local_registration_id != sid // Message from this device. Never encrypted to itself.
|
|
||||||
) {
|
|
||||||
db.identity_meta.update_last_message_undecryptable(identity_id, sid, message.time);
|
|
||||||
trust_manager.bad_message_state_updated(conversation.account, message.from, sid);
|
|
||||||
}
|
|
||||||
|
|
||||||
debug("Received OMEMO encryped message that could not be decrypted.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string arr_to_str(uint8[] arr) {
|
|
||||||
// null-terminate the array
|
|
||||||
uint8[] rarr = new uint8[arr.length+1];
|
|
||||||
Memory.copy(rarr, arr, arr.length);
|
|
||||||
return (string)rarr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Gee;
|
||||||
using Dino.Entities;
|
using Dino.Entities;
|
||||||
|
|
||||||
extern const string GETTEXT_PACKAGE;
|
extern const string GETTEXT_PACKAGE;
|
||||||
|
@ -20,6 +21,7 @@ public class Plugin : RootInterface, Object {
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (Error e) {
|
} catch (Error e) {
|
||||||
|
warning("Error initializing Signal Context %s", e.message);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,6 +35,8 @@ public class Plugin : RootInterface, Object {
|
||||||
public DeviceNotificationPopulator device_notification_populator;
|
public DeviceNotificationPopulator device_notification_populator;
|
||||||
public OwnNotifications own_notifications;
|
public OwnNotifications own_notifications;
|
||||||
public TrustManager trust_manager;
|
public TrustManager trust_manager;
|
||||||
|
public HashMap<Account, OmemoDecryptor> decryptors = new HashMap<Account, OmemoDecryptor>(Account.hash_func, Account.equals_func);
|
||||||
|
public HashMap<Account, OmemoEncryptor> encryptors = new HashMap<Account, OmemoEncryptor>(Account.hash_func, Account.equals_func);
|
||||||
|
|
||||||
public void registered(Dino.Application app) {
|
public void registered(Dino.Application app) {
|
||||||
ensure_context();
|
ensure_context();
|
||||||
|
@ -43,22 +47,32 @@ public class Plugin : RootInterface, Object {
|
||||||
this.contact_details_provider = new ContactDetailsProvider(this);
|
this.contact_details_provider = new ContactDetailsProvider(this);
|
||||||
this.device_notification_populator = new DeviceNotificationPopulator(this, this.app.stream_interactor);
|
this.device_notification_populator = new DeviceNotificationPopulator(this, this.app.stream_interactor);
|
||||||
this.trust_manager = new TrustManager(this.app.stream_interactor, this.db);
|
this.trust_manager = new TrustManager(this.app.stream_interactor, this.db);
|
||||||
|
|
||||||
this.app.plugin_registry.register_encryption_list_entry(list_entry);
|
this.app.plugin_registry.register_encryption_list_entry(list_entry);
|
||||||
this.app.plugin_registry.register_account_settings_entry(settings_entry);
|
this.app.plugin_registry.register_account_settings_entry(settings_entry);
|
||||||
this.app.plugin_registry.register_contact_details_entry(contact_details_provider);
|
this.app.plugin_registry.register_contact_details_entry(contact_details_provider);
|
||||||
this.app.plugin_registry.register_notification_populator(device_notification_populator);
|
this.app.plugin_registry.register_notification_populator(device_notification_populator);
|
||||||
this.app.plugin_registry.register_conversation_addition_populator(new BadMessagesPopulator(this.app.stream_interactor, this));
|
this.app.plugin_registry.register_conversation_addition_populator(new BadMessagesPopulator(this.app.stream_interactor, this));
|
||||||
|
this.app.plugin_registry.register_call_entryption_entry(DtlsSrtpVerificationDraft.NS_URI, new CallEncryptionEntry(db));
|
||||||
|
|
||||||
this.app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => {
|
this.app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => {
|
||||||
list.add(new StreamModule());
|
Signal.Store signal_store = Plugin.get_context().create_store();
|
||||||
list.add(new JetOmemo.Module(this));
|
list.add(new StreamModule(signal_store));
|
||||||
|
decryptors[account] = new OmemoDecryptor(account, app.stream_interactor, trust_manager, db, signal_store);
|
||||||
|
list.add(decryptors[account]);
|
||||||
|
encryptors[account] = new OmemoEncryptor(account, trust_manager,signal_store);
|
||||||
|
list.add(encryptors[account]);
|
||||||
|
list.add(new JetOmemo.Module());
|
||||||
|
list.add(new DtlsSrtpVerificationDraft.StreamModule());
|
||||||
this.own_notifications = new OwnNotifications(this, this.app.stream_interactor, account);
|
this.own_notifications = new OwnNotifications(this, this.app.stream_interactor, account);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.stream_interactor.get_module(MessageProcessor.IDENTITY).received_pipeline.connect(new DecryptMessageListener(decryptors));
|
||||||
app.stream_interactor.get_module(FileManager.IDENTITY).add_file_decryptor(new OmemoFileDecryptor());
|
app.stream_interactor.get_module(FileManager.IDENTITY).add_file_decryptor(new OmemoFileDecryptor());
|
||||||
app.stream_interactor.get_module(FileManager.IDENTITY).add_file_encryptor(new OmemoFileEncryptor());
|
app.stream_interactor.get_module(FileManager.IDENTITY).add_file_encryptor(new OmemoFileEncryptor());
|
||||||
JingleFileHelperRegistry.instance.add_encryption_helper(Encryption.OMEMO, new JetOmemo.EncryptionHelper(app.stream_interactor));
|
JingleFileHelperRegistry.instance.add_encryption_helper(Encryption.OMEMO, new JetOmemo.EncryptionHelper(app.stream_interactor));
|
||||||
|
|
||||||
Manager.start(this.app.stream_interactor, db, trust_manager);
|
Manager.start(this.app.stream_interactor, db, trust_manager, encryptors);
|
||||||
|
|
||||||
SimpleAction own_keys_action = new SimpleAction("own-keys", VariantType.INT32);
|
SimpleAction own_keys_action = new SimpleAction("own-keys", VariantType.INT32);
|
||||||
own_keys_action.activate.connect((variant) => {
|
own_keys_action.activate.connect((variant) => {
|
||||||
|
|
|
@ -25,10 +25,8 @@ public class StreamModule : XmppStreamModule {
|
||||||
public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle);
|
public signal void bundle_fetched(Jid jid, int device_id, Bundle bundle);
|
||||||
public signal void bundle_fetch_failed(Jid jid, int device_id);
|
public signal void bundle_fetch_failed(Jid jid, int device_id);
|
||||||
|
|
||||||
public StreamModule() {
|
public StreamModule(Store store) {
|
||||||
if (Plugin.ensure_context()) {
|
this.store = store;
|
||||||
this.store = Plugin.get_context().create_store();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void attach(XmppStream stream) {
|
public override void attach(XmppStream stream) {
|
||||||
|
|
57
plugins/omemo/src/ui/call_encryption_entry.vala
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
using Dino.Entities;
|
||||||
|
using Gtk;
|
||||||
|
using Qlite;
|
||||||
|
using Xmpp;
|
||||||
|
|
||||||
|
namespace Dino.Plugins.Omemo {
|
||||||
|
|
||||||
|
public class CallEncryptionEntry : Plugins.CallEncryptionEntry, Object {
|
||||||
|
private Database db;
|
||||||
|
|
||||||
|
public CallEncryptionEntry(Database db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Plugins.CallEncryptionWidget? get_widget(Account account, Xmpp.Xep.Jingle.ContentEncryption encryption) {
|
||||||
|
DtlsSrtpVerificationDraft.OmemoContentEncryption? omemo_encryption = encryption as DtlsSrtpVerificationDraft.OmemoContentEncryption;
|
||||||
|
if (omemo_encryption == null) return null;
|
||||||
|
|
||||||
|
int identity_id = db.identity.get_id(account.id);
|
||||||
|
Row? device = db.identity_meta.get_device(identity_id, omemo_encryption.jid.to_string(), omemo_encryption.sid);
|
||||||
|
if (device == null) return null;
|
||||||
|
TrustLevel trust = (TrustLevel) device[db.identity_meta.trust_level];
|
||||||
|
|
||||||
|
return new CallEncryptionWidget(trust);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CallEncryptionWidget : Plugins.CallEncryptionWidget, Object {
|
||||||
|
|
||||||
|
string? title = null;
|
||||||
|
string? icon = null;
|
||||||
|
bool should_show_keys = false;
|
||||||
|
|
||||||
|
public CallEncryptionWidget(TrustLevel trust) {
|
||||||
|
if (trust == TrustLevel.VERIFIED) {
|
||||||
|
title = "This call is <b>encrypted and verified</b> with OMEMO.";
|
||||||
|
icon = "dino-security-high-symbolic";
|
||||||
|
should_show_keys = false;
|
||||||
|
} else {
|
||||||
|
title = "This call is encrypted with OMEMO.";
|
||||||
|
should_show_keys = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? get_title() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? get_icon_name() {
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool show_keys() {
|
||||||
|
return should_show_keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
plugins/rtp/CMakeLists.txt
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
find_package(GstRtp REQUIRED)
|
||||||
|
find_package(WebRTCAudioProcessing 0.2)
|
||||||
|
find_packages(RTP_PACKAGES REQUIRED
|
||||||
|
Gee
|
||||||
|
GLib
|
||||||
|
GModule
|
||||||
|
GnuTLS
|
||||||
|
GObject
|
||||||
|
GTK3
|
||||||
|
Gst
|
||||||
|
GstApp
|
||||||
|
GstAudio
|
||||||
|
)
|
||||||
|
|
||||||
|
if(Gst_VERSION VERSION_GREATER "1.16")
|
||||||
|
set(RTP_DEFINITIONS GST_1_16)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(WebRTCAudioProcessing_VERSION GREATER "0.4")
|
||||||
|
message(STATUS "Ignoring WebRTCAudioProcessing, only versions < 0.4 supported so far")
|
||||||
|
unset(WebRTCAudioProcessing_FOUND)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(WebRTCAudioProcessing_FOUND)
|
||||||
|
set(RTP_DEFINITIONS ${RTP_DEFINITIONS} WITH_VOICE_PROCESSOR)
|
||||||
|
set(RTP_VOICE_PROCESSOR_VALA src/voice_processor.vala)
|
||||||
|
set(RTP_VOICE_PROCESSOR_CXX src/voice_processor_native.cpp)
|
||||||
|
set(RTP_VOICE_PROCESSOR_LIB webrtc-audio-processing)
|
||||||
|
else()
|
||||||
|
message(STATUS "WebRTCAudioProcessing not found, build without voice pre-processing!")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
vala_precompile(RTP_VALA_C
|
||||||
|
SOURCES
|
||||||
|
src/codec_util.vala
|
||||||
|
src/device.vala
|
||||||
|
src/module.vala
|
||||||
|
src/plugin.vala
|
||||||
|
src/stream.vala
|
||||||
|
src/video_widget.vala
|
||||||
|
src/register_plugin.vala
|
||||||
|
${RTP_VOICE_PROCESSOR_VALA}
|
||||||
|
CUSTOM_VAPIS
|
||||||
|
${CMAKE_BINARY_DIR}/exports/crypto-vala.vapi
|
||||||
|
${CMAKE_BINARY_DIR}/exports/xmpp-vala.vapi
|
||||||
|
${CMAKE_BINARY_DIR}/exports/dino.vapi
|
||||||
|
${CMAKE_BINARY_DIR}/exports/qlite.vapi
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/vapi/gstreamer-rtp-1.0.vapi
|
||||||
|
PACKAGES
|
||||||
|
${RTP_PACKAGES}
|
||||||
|
DEFINITIONS
|
||||||
|
${RTP_DEFINITIONS}
|
||||||
|
)
|
||||||
|
|
||||||
|
add_definitions(${VALA_CFLAGS} -DG_LOG_DOMAIN="rtp" -I${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||||
|
add_library(rtp SHARED ${RTP_VALA_C} ${RTP_VOICE_PROCESSOR_CXX})
|
||||||
|
target_link_libraries(rtp libdino crypto-vala ${RTP_PACKAGES} gstreamer-rtp-1.0 ${RTP_VOICE_PROCESSOR_LIB})
|
||||||
|
set_target_properties(rtp PROPERTIES PREFIX "")
|
||||||
|
set_target_properties(rtp PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/plugins/)
|
||||||
|
|
||||||
|
install(TARGETS rtp ${PLUGIN_INSTALL})
|
307
plugins/rtp/src/codec_util.vala
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
using Gee;
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
|
||||||
|
public class Dino.Plugins.Rtp.CodecUtil {
|
||||||
|
private Set<string> supported_elements = new HashSet<string>();
|
||||||
|
private Set<string> unsupported_elements = new HashSet<string>();
|
||||||
|
|
||||||
|
public static Gst.Caps get_caps(string media, JingleRtp.PayloadType payload_type, bool incoming) {
|
||||||
|
Gst.Caps caps = new Gst.Caps.simple("application/x-rtp",
|
||||||
|
"media", typeof(string), media,
|
||||||
|
"payload", typeof(int), payload_type.id);
|
||||||
|
//"channels", typeof(int), payloadType.channels,
|
||||||
|
//"max-ptime", typeof(int), payloadType.maxptime);
|
||||||
|
unowned Gst.Structure s = caps.get_structure(0);
|
||||||
|
if (payload_type.clockrate != 0) {
|
||||||
|
s.set("clock-rate", typeof(int), payload_type.clockrate);
|
||||||
|
}
|
||||||
|
if (payload_type.name != null) {
|
||||||
|
s.set("encoding-name", typeof(string), payload_type.name.up());
|
||||||
|
}
|
||||||
|
if (incoming) {
|
||||||
|
foreach (JingleRtp.RtcpFeedback rtcp_fb in payload_type.rtcp_fbs) {
|
||||||
|
if (rtcp_fb.subtype == null) {
|
||||||
|
s.set(@"rtcp-fb-$(rtcp_fb.type_)", typeof(bool), true);
|
||||||
|
} else {
|
||||||
|
s.set(@"rtcp-fb-$(rtcp_fb.type_)-$(rtcp_fb.subtype)", typeof(bool), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return caps;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_codec_from_payload(string media, JingleRtp.PayloadType payload_type) {
|
||||||
|
if (payload_type.name != null) return payload_type.name.down();
|
||||||
|
if (media == "audio") {
|
||||||
|
switch (payload_type.id) {
|
||||||
|
case 0:
|
||||||
|
return "pcmu";
|
||||||
|
case 8:
|
||||||
|
return "pcma";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_media_type_from_payload(string media, JingleRtp.PayloadType payload_type) {
|
||||||
|
return get_media_type(media, get_codec_from_payload(media, payload_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_media_type(string media, string? codec) {
|
||||||
|
if (codec == null) return null;
|
||||||
|
if (media == "audio") {
|
||||||
|
switch (codec) {
|
||||||
|
case "pcma":
|
||||||
|
return "audio/x-alaw";
|
||||||
|
case "pcmu":
|
||||||
|
return "audio/x-mulaw";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return @"$media/x-$codec";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_rtp_pay_element_name_from_payload(string media, JingleRtp.PayloadType payload_type) {
|
||||||
|
return get_pay_candidate(media, get_codec_from_payload(media, payload_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_pay_candidate(string media, string? codec) {
|
||||||
|
if (codec == null) return null;
|
||||||
|
return @"rtp$(codec)pay";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_rtp_depay_element_name_from_payload(string media, JingleRtp.PayloadType payload_type) {
|
||||||
|
return get_depay_candidate(media, get_codec_from_payload(media, payload_type));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_depay_candidate(string media, string? codec) {
|
||||||
|
if (codec == null) return null;
|
||||||
|
return @"rtp$(codec)depay";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string[] get_encode_candidates(string media, string? codec) {
|
||||||
|
if (codec == null) return new string[0];
|
||||||
|
if (media == "audio") {
|
||||||
|
switch (codec) {
|
||||||
|
case "opus":
|
||||||
|
return new string[] {"opusenc"};
|
||||||
|
case "speex":
|
||||||
|
return new string[] {"speexenc"};
|
||||||
|
case "pcma":
|
||||||
|
return new string[] {"alawenc"};
|
||||||
|
case "pcmu":
|
||||||
|
return new string[] {"mulawenc"};
|
||||||
|
}
|
||||||
|
} else if (media == "video") {
|
||||||
|
switch (codec) {
|
||||||
|
case "h264":
|
||||||
|
return new string[] {/*"msdkh264enc", */"vaapih264enc", "x264enc"};
|
||||||
|
case "vp9":
|
||||||
|
return new string[] {/*"msdkvp9enc", */"vaapivp9enc" /*, "vp9enc" */};
|
||||||
|
case "vp8":
|
||||||
|
return new string[] {/*"msdkvp8enc", */"vaapivp8enc", "vp8enc"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new string[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string[] get_decode_candidates(string media, string? codec) {
|
||||||
|
if (codec == null) return new string[0];
|
||||||
|
if (media == "audio") {
|
||||||
|
switch (codec) {
|
||||||
|
case "opus":
|
||||||
|
return new string[] {"opusdec"};
|
||||||
|
case "speex":
|
||||||
|
return new string[] {"speexdec"};
|
||||||
|
case "pcma":
|
||||||
|
return new string[] {"alawdec"};
|
||||||
|
case "pcmu":
|
||||||
|
return new string[] {"mulawdec"};
|
||||||
|
}
|
||||||
|
} else if (media == "video") {
|
||||||
|
switch (codec) {
|
||||||
|
case "h264":
|
||||||
|
return new string[] {/*"msdkh264dec", */"vaapih264dec"};
|
||||||
|
case "vp9":
|
||||||
|
return new string[] {/*"msdkvp9dec", */"vaapivp9dec", "vp9dec"};
|
||||||
|
case "vp8":
|
||||||
|
return new string[] {/*"msdkvp8dec", */"vaapivp8dec", "vp8dec"};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new string[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_encode_prefix(string media, string codec, string encode, JingleRtp.PayloadType? payload_type) {
|
||||||
|
if (encode == "msdkh264enc") return "video/x-raw,format=NV12 ! ";
|
||||||
|
if (encode == "vaapih264enc") return "video/x-raw,format=NV12 ! ";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_encode_args(string media, string codec, string encode, JingleRtp.PayloadType? payload_type) {
|
||||||
|
// H264
|
||||||
|
if (encode == "msdkh264enc") return @" rate-control=vbr";
|
||||||
|
if (encode == "vaapih264enc") return @" tune=low-power";
|
||||||
|
if (encode == "x264enc") return @" byte-stream=1 profile=baseline speed-preset=ultrafast tune=zerolatency";
|
||||||
|
|
||||||
|
// VP8
|
||||||
|
if (encode == "msdkvp8enc") return " rate-control=vbr";
|
||||||
|
if (encode == "vaapivp8enc") return " rate-control=vbr";
|
||||||
|
if (encode == "vp8enc") return " deadline=1 error-resilient=1";
|
||||||
|
|
||||||
|
// OPUS
|
||||||
|
if (encode == "opusenc") {
|
||||||
|
if (payload_type != null && payload_type.parameters.has("useinbandfec", "1")) return " audio-type=voice inband-fec=true";
|
||||||
|
return " audio-type=voice";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_encode_suffix(string media, string codec, string encode, JingleRtp.PayloadType? payload_type) {
|
||||||
|
// H264
|
||||||
|
if (media == "video" && codec == "h264") return " ! video/x-h264,profile=constrained-baseline ! h264parse";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint update_bitrate(string media, JingleRtp.PayloadType payload_type, Gst.Element encode_element, uint bitrate) {
|
||||||
|
Gst.Bin? encode_bin = encode_element as Gst.Bin;
|
||||||
|
if (encode_bin == null) return 0;
|
||||||
|
string? codec = get_codec_from_payload(media, payload_type);
|
||||||
|
string? encode_name = get_encode_element_name(media, codec);
|
||||||
|
if (encode_name == null) return 0;
|
||||||
|
Gst.Element encode = encode_bin.get_by_name(@"$(encode_bin.name)_encode");
|
||||||
|
|
||||||
|
bitrate = uint.min(2048000, bitrate);
|
||||||
|
|
||||||
|
switch (encode_name) {
|
||||||
|
case "msdkh264enc":
|
||||||
|
case "vaapih264enc":
|
||||||
|
case "x264enc":
|
||||||
|
case "msdkvp8enc":
|
||||||
|
case "vaapivp8enc":
|
||||||
|
bitrate = uint.min(2048000, bitrate);
|
||||||
|
encode.set("bitrate", bitrate);
|
||||||
|
return bitrate;
|
||||||
|
case "vp8enc":
|
||||||
|
bitrate = uint.min(2147483, bitrate);
|
||||||
|
encode.set("target-bitrate", bitrate * 1000);
|
||||||
|
return bitrate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_decode_prefix(string media, string codec, string decode, JingleRtp.PayloadType? payload_type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_decode_args(string media, string codec, string decode, JingleRtp.PayloadType? payload_type) {
|
||||||
|
if (decode == "opusdec" && payload_type != null && payload_type.parameters.has("useinbandfec", "1")) return " use-inband-fec=true";
|
||||||
|
if (decode == "vaapivp9dec" || decode == "vaapivp8dec" || decode == "vaapih264dec") return " max-errors=100";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_decode_suffix(string media, string codec, string encode, JingleRtp.PayloadType? payload_type) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? get_depay_args(string media, string codec, string encode, JingleRtp.PayloadType? payload_type) {
|
||||||
|
if (codec == "vp8") return " wait-for-keyframe=true";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool is_element_supported(string element_name) {
|
||||||
|
if (unsupported_elements.contains(element_name)) return false;
|
||||||
|
if (supported_elements.contains(element_name)) return true;
|
||||||
|
var test_element = Gst.ElementFactory.make(element_name, @"test-$element_name");
|
||||||
|
if (test_element != null) {
|
||||||
|
supported_elements.add(element_name);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
debug("%s is not supported on this platform", element_name);
|
||||||
|
unsupported_elements.add(element_name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? get_encode_element_name(string media, string? codec) {
|
||||||
|
if (!is_element_supported(get_pay_element_name(media, codec))) return null;
|
||||||
|
foreach (string candidate in get_encode_candidates(media, codec)) {
|
||||||
|
if (is_element_supported(candidate)) return candidate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? get_pay_element_name(string media, string? codec) {
|
||||||
|
string candidate = get_pay_candidate(media, codec);
|
||||||
|
if (is_element_supported(candidate)) return candidate;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? get_decode_element_name(string media, string? codec) {
|
||||||
|
foreach (string candidate in get_decode_candidates(media, codec)) {
|
||||||
|
if (is_element_supported(candidate)) return candidate;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? get_depay_element_name(string media, string? codec) {
|
||||||
|
string candidate = get_depay_candidate(media, codec);
|
||||||
|
if (is_element_supported(candidate)) return candidate;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void mark_element_unsupported(string element_name) {
|
||||||
|
unsupported_elements.add(element_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? get_decode_bin_description(string media, string? codec, JingleRtp.PayloadType? payload_type, string? element_name = null, string? name = null) {
|
||||||
|
if (codec == null) return null;
|
||||||
|
string base_name = name ?? @"encode-$codec-$(Random.next_int())";
|
||||||
|
string depay = get_depay_element_name(media, codec);
|
||||||
|
string decode = element_name ?? get_decode_element_name(media, codec);
|
||||||
|
if (depay == null || decode == null) return null;
|
||||||
|
string decode_prefix = get_decode_prefix(media, codec, decode, payload_type) ?? "";
|
||||||
|
string decode_args = get_decode_args(media, codec, decode, payload_type) ?? "";
|
||||||
|
string decode_suffix = get_decode_suffix(media, codec, decode, payload_type) ?? "";
|
||||||
|
string depay_args = get_depay_args(media, codec, decode, payload_type) ?? "";
|
||||||
|
string resample = media == "audio" ? @" ! audioresample name=$(base_name)_resample" : "";
|
||||||
|
return @"$depay$depay_args name=$(base_name)_rtp_depay ! $decode_prefix$decode$decode_args name=$(base_name)_$(codec)_decode$decode_suffix ! $(media)convert name=$(base_name)_convert$resample";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gst.Element? get_decode_bin(string media, JingleRtp.PayloadType payload_type, string? name = null) {
|
||||||
|
string? codec = get_codec_from_payload(media, payload_type);
|
||||||
|
string base_name = name ?? @"encode-$codec-$(Random.next_int())";
|
||||||
|
string? desc = get_decode_bin_description(media, codec, payload_type, null, base_name);
|
||||||
|
if (desc == null) return null;
|
||||||
|
debug("Pipeline to decode %s %s: %s", media, codec, desc);
|
||||||
|
Gst.Element bin = Gst.parse_bin_from_description(desc, true);
|
||||||
|
bin.name = name;
|
||||||
|
return bin;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? get_encode_bin_description(string media, string? codec, JingleRtp.PayloadType? payload_type, string? element_name = null, string? name = null) {
|
||||||
|
if (codec == null) return null;
|
||||||
|
string base_name = name ?? @"encode_$(codec)_$(Random.next_int())";
|
||||||
|
string pay = get_pay_element_name(media, codec);
|
||||||
|
string encode = element_name ?? get_encode_element_name(media, codec);
|
||||||
|
if (pay == null || encode == null) return null;
|
||||||
|
string encode_prefix = get_encode_prefix(media, codec, encode, payload_type) ?? "";
|
||||||
|
string encode_args = get_encode_args(media, codec, encode, payload_type) ?? "";
|
||||||
|
string encode_suffix = get_encode_suffix(media, codec, encode, payload_type) ?? "";
|
||||||
|
string resample = media == "audio" ? @" ! audioresample name=$(base_name)_resample" : "";
|
||||||
|
return @"$(media)convert name=$(base_name)_convert$resample ! $encode_prefix$encode$encode_args name=$(base_name)_encode$encode_suffix ! $pay pt=$(payload_type != null ? payload_type.id : 96) name=$(base_name)_rtp_pay";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gst.Element? get_encode_bin(string media, JingleRtp.PayloadType payload_type, string? name = null) {
|
||||||
|
string? codec = get_codec_from_payload(media, payload_type);
|
||||||
|
string base_name = name ?? @"encode_$(codec)_$(Random.next_int())";
|
||||||
|
string? desc = get_encode_bin_description(media, codec, payload_type, null, base_name);
|
||||||
|
if (desc == null) return null;
|
||||||
|
debug("Pipeline to encode %s %s: %s", media, codec, desc);
|
||||||
|
Gst.Element bin = Gst.parse_bin_from_description(desc, true);
|
||||||
|
bin.name = name;
|
||||||
|
return bin;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
272
plugins/rtp/src/device.vala
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
public class Dino.Plugins.Rtp.Device : MediaDevice, Object {
|
||||||
|
public Plugin plugin { get; private set; }
|
||||||
|
public Gst.Device device { get; private set; }
|
||||||
|
|
||||||
|
private string device_name;
|
||||||
|
public string id { get {
|
||||||
|
return device_name;
|
||||||
|
}}
|
||||||
|
private string device_display_name;
|
||||||
|
public string display_name { get {
|
||||||
|
return device_display_name;
|
||||||
|
}}
|
||||||
|
public string detail_name { get {
|
||||||
|
return device.properties.get_string("alsa.card_name") ?? device.properties.get_string("alsa.id") ?? id;
|
||||||
|
}}
|
||||||
|
public Gst.Pipeline pipe { get {
|
||||||
|
return plugin.pipe;
|
||||||
|
}}
|
||||||
|
public string? media { get {
|
||||||
|
if (device.device_class.has_prefix("Audio/")) {
|
||||||
|
return "audio";
|
||||||
|
} else if (device.device_class.has_prefix("Video/")) {
|
||||||
|
return "video";
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
public bool is_source { get {
|
||||||
|
return device.device_class.has_suffix("/Source");
|
||||||
|
}}
|
||||||
|
public bool is_sink { get {
|
||||||
|
return device.device_class.has_suffix("/Sink");
|
||||||
|
}}
|
||||||
|
|
||||||
|
private Gst.Element element;
|
||||||
|
private Gst.Element tee;
|
||||||
|
private Gst.Element dsp;
|
||||||
|
private Gst.Element mixer;
|
||||||
|
private Gst.Element filter;
|
||||||
|
private Gst.Element rate;
|
||||||
|
private int links = 0;
|
||||||
|
|
||||||
|
public Device(Plugin plugin, Gst.Device device) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
update(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool matches(Gst.Device device) {
|
||||||
|
if (this.device.name == device.name) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(Gst.Device device) {
|
||||||
|
this.device = device;
|
||||||
|
this.device_name = device.name;
|
||||||
|
this.device_display_name = device.display_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gst.Element? link_sink() {
|
||||||
|
if (element == null) create();
|
||||||
|
links++;
|
||||||
|
if (mixer != null) return mixer;
|
||||||
|
if (is_sink && media == "audio") return filter;
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gst.Element? link_source() {
|
||||||
|
if (element == null) create();
|
||||||
|
links++;
|
||||||
|
if (tee != null) return tee;
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlink() {
|
||||||
|
if (links <= 0) {
|
||||||
|
critical("Link count below zero.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
links--;
|
||||||
|
if (links == 0) {
|
||||||
|
destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Gst.Caps get_best_caps() {
|
||||||
|
if (media == "audio") {
|
||||||
|
return Gst.Caps.from_string("audio/x-raw,rate=48000,channels=1");
|
||||||
|
} else if (media == "video" && device.caps.get_size() > 0) {
|
||||||
|
int best_index = 0;
|
||||||
|
Value? best_fraction = null;
|
||||||
|
int best_fps = 0;
|
||||||
|
int best_width = 0;
|
||||||
|
int best_height = 0;
|
||||||
|
for (int i = 0; i < device.caps.get_size(); i++) {
|
||||||
|
unowned Gst.Structure? that = device.caps.get_structure(i);
|
||||||
|
if (!that.has_name("video/x-raw")) continue;
|
||||||
|
int num = 0, den = 0, width = 0, height = 0;
|
||||||
|
if (!that.has_field("framerate")) continue;
|
||||||
|
Value framerate = that.get_value("framerate");
|
||||||
|
if (framerate.type() == typeof(Gst.Fraction)) {
|
||||||
|
num = Gst.Value.get_fraction_numerator(framerate);
|
||||||
|
den = Gst.Value.get_fraction_denominator(framerate);
|
||||||
|
} else if (framerate.type() == typeof(Gst.ValueList)) {
|
||||||
|
for(uint j = 0; j < Gst.ValueList.get_size(framerate); j++) {
|
||||||
|
Value fraction = Gst.ValueList.get_value(framerate, j);
|
||||||
|
int in_num = Gst.Value.get_fraction_numerator(fraction);
|
||||||
|
int in_den = Gst.Value.get_fraction_denominator(fraction);
|
||||||
|
int fps = den > 0 ? (num/den) : 0;
|
||||||
|
int in_fps = in_den > 0 ? (in_num/in_den) : 0;
|
||||||
|
if (in_fps > fps) {
|
||||||
|
best_fraction = fraction;
|
||||||
|
num = in_num;
|
||||||
|
den = in_den;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debug("Unknown type for framerate: %s", framerate.type_name());
|
||||||
|
}
|
||||||
|
if (den == 0) continue;
|
||||||
|
if (!that.has_field("width") || !that.get_int("width", out width)) continue;
|
||||||
|
if (!that.has_field("height") || !that.get_int("height", out height)) continue;
|
||||||
|
int fps = num/den;
|
||||||
|
if (best_fps < fps || best_fps == fps && best_width < width || best_fps == fps && best_width == width && best_height < height) {
|
||||||
|
best_fps = fps;
|
||||||
|
best_width = width;
|
||||||
|
best_height = height;
|
||||||
|
best_index = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Gst.Caps res = caps_copy_nth(device.caps, best_index);
|
||||||
|
unowned Gst.Structure? that = res.get_structure(0);
|
||||||
|
Value framerate = that.get_value("framerate");
|
||||||
|
if (framerate.type() == typeof(Gst.ValueList)) {
|
||||||
|
that.set_value("framerate", best_fraction);
|
||||||
|
}
|
||||||
|
debug("Selected caps %s", res.to_string());
|
||||||
|
return res;
|
||||||
|
} else if (device.caps.get_size() > 0) {
|
||||||
|
return caps_copy_nth(device.caps, 0);
|
||||||
|
} else {
|
||||||
|
return new Gst.Caps.any();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backport from gst_caps_copy_nth added in GStreamer 1.16
|
||||||
|
private static Gst.Caps caps_copy_nth(Gst.Caps source, uint index) {
|
||||||
|
Gst.Caps target = new Gst.Caps.empty();
|
||||||
|
target.flags = source.flags;
|
||||||
|
target.append_structure_full(source.get_structure(index).copy(), source.get_features(index).copy());
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void create() {
|
||||||
|
debug("Creating device %s", id);
|
||||||
|
plugin.pause();
|
||||||
|
element = device.create_element(id);
|
||||||
|
pipe.add(element);
|
||||||
|
if (is_source) {
|
||||||
|
element.@set("do-timestamp", true);
|
||||||
|
filter = Gst.ElementFactory.make("capsfilter", @"caps_filter_$id");
|
||||||
|
filter.@set("caps", get_best_caps());
|
||||||
|
pipe.add(filter);
|
||||||
|
element.link(filter);
|
||||||
|
#if WITH_VOICE_PROCESSOR
|
||||||
|
if (media == "audio" && plugin.echoprobe != null) {
|
||||||
|
dsp = new VoiceProcessor(plugin.echoprobe as EchoProbe, element as Gst.Audio.StreamVolume);
|
||||||
|
dsp.name = @"dsp_$id";
|
||||||
|
pipe.add(dsp);
|
||||||
|
filter.link(dsp);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
tee = Gst.ElementFactory.make("tee", @"tee_$id");
|
||||||
|
tee.@set("allow-not-linked", true);
|
||||||
|
pipe.add(tee);
|
||||||
|
(dsp ?? filter).link(tee);
|
||||||
|
}
|
||||||
|
if (is_sink) {
|
||||||
|
element.@set("async", false);
|
||||||
|
element.@set("sync", false);
|
||||||
|
}
|
||||||
|
if (is_sink && media == "audio") {
|
||||||
|
filter = Gst.ElementFactory.make("capsfilter", @"caps_filter_$id");
|
||||||
|
filter.@set("caps", get_best_caps());
|
||||||
|
pipe.add(filter);
|
||||||
|
if (plugin.echoprobe != null) {
|
||||||
|
rate = Gst.ElementFactory.make("audiorate", @"rate_$id");
|
||||||
|
rate.@set("tolerance", 100000000);
|
||||||
|
pipe.add(rate);
|
||||||
|
filter.link(rate);
|
||||||
|
rate.link(plugin.echoprobe);
|
||||||
|
plugin.echoprobe.link(element);
|
||||||
|
} else {
|
||||||
|
filter.link(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plugin.unpause();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void destroy() {
|
||||||
|
if (mixer != null) {
|
||||||
|
if (is_sink && media == "audio" && plugin.echoprobe != null) {
|
||||||
|
plugin.echoprobe.unlink(mixer);
|
||||||
|
}
|
||||||
|
int linked_sink_pads = 0;
|
||||||
|
mixer.foreach_sink_pad((_, pad) => {
|
||||||
|
if (pad.is_linked()) linked_sink_pads++;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (linked_sink_pads > 0) {
|
||||||
|
warning("%s-mixer still has %i sink pads while being destroyed", id, linked_sink_pads);
|
||||||
|
}
|
||||||
|
mixer.set_locked_state(true);
|
||||||
|
mixer.set_state(Gst.State.NULL);
|
||||||
|
mixer.unlink(element);
|
||||||
|
pipe.remove(mixer);
|
||||||
|
mixer = null;
|
||||||
|
} else if (is_sink && media == "audio") {
|
||||||
|
if (filter != null) {
|
||||||
|
filter.set_locked_state(true);
|
||||||
|
filter.set_state(Gst.State.NULL);
|
||||||
|
filter.unlink(rate ?? ((Gst.Element)plugin.echoprobe) ?? element);
|
||||||
|
pipe.remove(filter);
|
||||||
|
filter = null;
|
||||||
|
}
|
||||||
|
if (rate != null) {
|
||||||
|
rate.set_locked_state(true);
|
||||||
|
rate.set_state(Gst.State.NULL);
|
||||||
|
rate.unlink(plugin.echoprobe);
|
||||||
|
pipe.remove(rate);
|
||||||
|
rate = null;
|
||||||
|
}
|
||||||
|
if (plugin.echoprobe != null) {
|
||||||
|
plugin.echoprobe.unlink(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
element.set_locked_state(true);
|
||||||
|
element.set_state(Gst.State.NULL);
|
||||||
|
if (filter != null) element.unlink(filter);
|
||||||
|
else if (is_source) element.unlink(tee);
|
||||||
|
pipe.remove(element);
|
||||||
|
element = null;
|
||||||
|
if (filter != null) {
|
||||||
|
filter.set_locked_state(true);
|
||||||
|
filter.set_state(Gst.State.NULL);
|
||||||
|
filter.unlink(dsp ?? tee);
|
||||||
|
pipe.remove(filter);
|
||||||
|
filter = null;
|
||||||
|
}
|
||||||
|
if (dsp != null) {
|
||||||
|
dsp.set_locked_state(true);
|
||||||
|
dsp.set_state(Gst.State.NULL);
|
||||||
|
dsp.unlink(tee);
|
||||||
|
pipe.remove(dsp);
|
||||||
|
dsp = null;
|
||||||
|
}
|
||||||
|
if (tee != null) {
|
||||||
|
int linked_src_pads = 0;
|
||||||
|
tee.foreach_src_pad((_, pad) => {
|
||||||
|
if (pad.is_linked()) linked_src_pads++;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (linked_src_pads != 0) {
|
||||||
|
warning("%s-tee still has %d src pads while being destroyed", id, linked_src_pads);
|
||||||
|
}
|
||||||
|
tee.set_locked_state(true);
|
||||||
|
tee.set_state(Gst.State.NULL);
|
||||||
|
pipe.remove(tee);
|
||||||
|
tee = null;
|
||||||
|
}
|
||||||
|
debug("Destroyed device %s", id);
|
||||||
|
}
|
||||||
|
}
|
237
plugins/rtp/src/module.vala
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
using Gee;
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
|
||||||
|
public class Dino.Plugins.Rtp.Module : JingleRtp.Module {
|
||||||
|
private Set<string> supported_codecs = new HashSet<string>();
|
||||||
|
private Set<string> unsupported_codecs = new HashSet<string>();
|
||||||
|
public Plugin plugin { get; private set; }
|
||||||
|
public CodecUtil codec_util { get {
|
||||||
|
return plugin.codec_util;
|
||||||
|
}}
|
||||||
|
|
||||||
|
public Module(Plugin plugin) {
|
||||||
|
base();
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async bool pipeline_works(string media, string element_desc) {
|
||||||
|
var supported = false;
|
||||||
|
string pipeline_desc = @"$(media)testsrc is-live=true ! $element_desc ! appsink name=output";
|
||||||
|
try {
|
||||||
|
var pipeline = Gst.parse_launch(pipeline_desc);
|
||||||
|
var output = (pipeline as Gst.Bin).get_by_name("output") as Gst.App.Sink;
|
||||||
|
SourceFunc callback = pipeline_works.callback;
|
||||||
|
var finished = false;
|
||||||
|
output.emit_signals = true;
|
||||||
|
output.new_sample.connect(() => {
|
||||||
|
if (!finished) {
|
||||||
|
finished = true;
|
||||||
|
supported = true;
|
||||||
|
Idle.add(() => {
|
||||||
|
callback();
|
||||||
|
return Source.REMOVE;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Gst.FlowReturn.EOS;
|
||||||
|
});
|
||||||
|
pipeline.bus.add_watch(Priority.DEFAULT, (_, message) => {
|
||||||
|
if (message.type == Gst.MessageType.ERROR && !finished) {
|
||||||
|
Error e;
|
||||||
|
string d;
|
||||||
|
message.parse_error(out e, out d);
|
||||||
|
debug("pipeline [%s] failed: %s", pipeline_desc, e.message);
|
||||||
|
debug(d);
|
||||||
|
finished = true;
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
Timeout.add(2000, () => {
|
||||||
|
if (!finished) {
|
||||||
|
finished = true;
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
return Source.REMOVE;
|
||||||
|
});
|
||||||
|
pipeline.set_state(Gst.State.PLAYING);
|
||||||
|
yield;
|
||||||
|
pipeline.set_state(Gst.State.NULL);
|
||||||
|
} catch (Error e) {
|
||||||
|
debug("pipeline [%s] failed: %s", pipeline_desc, e.message);
|
||||||
|
}
|
||||||
|
return supported;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async bool is_payload_supported(string media, JingleRtp.PayloadType payload_type) {
|
||||||
|
string? codec = CodecUtil.get_codec_from_payload(media, payload_type);
|
||||||
|
if (codec == null) return false;
|
||||||
|
if (unsupported_codecs.contains(codec)) return false;
|
||||||
|
if (supported_codecs.contains(codec)) return true;
|
||||||
|
|
||||||
|
string? encode_element = codec_util.get_encode_element_name(media, codec);
|
||||||
|
string? decode_element = codec_util.get_decode_element_name(media, codec);
|
||||||
|
if (encode_element == null || decode_element == null) {
|
||||||
|
debug("No suitable encoder or decoder found for %s", codec);
|
||||||
|
unsupported_codecs.add(codec);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string encode_bin = codec_util.get_encode_bin_description(media, codec, null, encode_element);
|
||||||
|
while (!(yield pipeline_works(media, encode_bin))) {
|
||||||
|
debug("%s not suited for encoding %s", encode_element, codec);
|
||||||
|
codec_util.mark_element_unsupported(encode_element);
|
||||||
|
encode_element = codec_util.get_encode_element_name(media, codec);
|
||||||
|
if (encode_element == null) {
|
||||||
|
debug("No suitable encoder found for %s", codec);
|
||||||
|
unsupported_codecs.add(codec);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
encode_bin = codec_util.get_encode_bin_description(media, codec, null, encode_element);
|
||||||
|
}
|
||||||
|
debug("using %s to encode %s", encode_element, codec);
|
||||||
|
|
||||||
|
string decode_bin = codec_util.get_decode_bin_description(media, codec, null, decode_element);
|
||||||
|
while (!(yield pipeline_works(media, @"$encode_bin ! $decode_bin"))) {
|
||||||
|
debug("%s not suited for decoding %s", decode_element, codec);
|
||||||
|
codec_util.mark_element_unsupported(decode_element);
|
||||||
|
decode_element = codec_util.get_decode_element_name(media, codec);
|
||||||
|
if (decode_element == null) {
|
||||||
|
debug("No suitable decoder found for %s", codec);
|
||||||
|
unsupported_codecs.add(codec);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
decode_bin = codec_util.get_decode_bin_description(media, codec, null, decode_element);
|
||||||
|
}
|
||||||
|
debug("using %s to decode %s", decode_element, codec);
|
||||||
|
|
||||||
|
supported_codecs.add(codec);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool is_header_extension_supported(string media, JingleRtp.HeaderExtension ext) {
|
||||||
|
if (media == "video" && ext.uri == "urn:3gpp:video-orientation") return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Gee.List<JingleRtp.HeaderExtension> get_suggested_header_extensions(string media) {
|
||||||
|
Gee.List<JingleRtp.HeaderExtension> exts = new ArrayList<JingleRtp.HeaderExtension>();
|
||||||
|
if (media == "video") {
|
||||||
|
exts.add(new JingleRtp.HeaderExtension(1, "urn:3gpp:video-orientation"));
|
||||||
|
}
|
||||||
|
return exts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void add_if_supported(Gee.List<JingleRtp.PayloadType> list, string media, JingleRtp.PayloadType payload_type) {
|
||||||
|
if (yield is_payload_supported(media, payload_type)) {
|
||||||
|
list.add(payload_type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Gee.List<JingleRtp.PayloadType> get_supported_payloads(string media) {
|
||||||
|
Gee.List<JingleRtp.PayloadType> list = new ArrayList<JingleRtp.PayloadType>(JingleRtp.PayloadType.equals_func);
|
||||||
|
if (media == "audio") {
|
||||||
|
var opus = new JingleRtp.PayloadType() { channels = 2, clockrate = 48000, name = "opus", id = 99 };
|
||||||
|
opus.parameters["useinbandfec"] = "1";
|
||||||
|
var speex32 = new JingleRtp.PayloadType() { channels = 1, clockrate = 32000, name = "speex", id = 100 };
|
||||||
|
var speex16 = new JingleRtp.PayloadType() { channels = 1, clockrate = 16000, name = "speex", id = 101 };
|
||||||
|
var speex8 = new JingleRtp.PayloadType() { channels = 1, clockrate = 8000, name = "speex", id = 102 };
|
||||||
|
var pcmu = new JingleRtp.PayloadType() { channels = 1, clockrate = 8000, name = "PCMU", id = 0 };
|
||||||
|
var pcma = new JingleRtp.PayloadType() { channels = 1, clockrate = 8000, name = "PCMA", id = 8 };
|
||||||
|
yield add_if_supported(list, media, opus);
|
||||||
|
yield add_if_supported(list, media, speex32);
|
||||||
|
yield add_if_supported(list, media, speex16);
|
||||||
|
yield add_if_supported(list, media, speex8);
|
||||||
|
yield add_if_supported(list, media, pcmu);
|
||||||
|
yield add_if_supported(list, media, pcma);
|
||||||
|
} else if (media == "video") {
|
||||||
|
var h264 = new JingleRtp.PayloadType() { clockrate = 90000, name = "H264", id = 96 };
|
||||||
|
var vp9 = new JingleRtp.PayloadType() { clockrate = 90000, name = "VP9", id = 97 };
|
||||||
|
var vp8 = new JingleRtp.PayloadType() { clockrate = 90000, name = "VP8", id = 98 };
|
||||||
|
var rtcp_fbs = new ArrayList<JingleRtp.RtcpFeedback>();
|
||||||
|
rtcp_fbs.add(new JingleRtp.RtcpFeedback("goog-remb"));
|
||||||
|
rtcp_fbs.add(new JingleRtp.RtcpFeedback("ccm", "fir"));
|
||||||
|
rtcp_fbs.add(new JingleRtp.RtcpFeedback("nack"));
|
||||||
|
rtcp_fbs.add(new JingleRtp.RtcpFeedback("nack", "pli"));
|
||||||
|
h264.rtcp_fbs.add_all(rtcp_fbs);
|
||||||
|
vp9.rtcp_fbs.add_all(rtcp_fbs);
|
||||||
|
vp8.rtcp_fbs.add_all(rtcp_fbs);
|
||||||
|
yield add_if_supported(list, media, h264);
|
||||||
|
yield add_if_supported(list, media, vp9);
|
||||||
|
yield add_if_supported(list, media, vp8);
|
||||||
|
} else {
|
||||||
|
warning("Unsupported media type: %s", media);
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async JingleRtp.PayloadType? pick_payload_type(string media, Gee.List<JingleRtp.PayloadType> payloads) {
|
||||||
|
if (media == "audio") {
|
||||||
|
foreach (JingleRtp.PayloadType type in payloads) {
|
||||||
|
if (yield is_payload_supported(media, type)) return adjust_payload_type(media, type.clone());
|
||||||
|
}
|
||||||
|
} else if (media == "video") {
|
||||||
|
// We prefer H.264 (best support for hardware acceleration and good overall codec quality)
|
||||||
|
JingleRtp.PayloadType? h264 = payloads.first_match((it) => it.name.up() == "H264");
|
||||||
|
if (h264 != null && yield is_payload_supported(media, h264)) return adjust_payload_type(media, h264.clone());
|
||||||
|
// Take first of the list that we do support otherwise
|
||||||
|
foreach (JingleRtp.PayloadType type in payloads) {
|
||||||
|
if (yield is_payload_supported(media, type)) return adjust_payload_type(media, type.clone());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warning("Unsupported media type: %s", media);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public JingleRtp.PayloadType adjust_payload_type(string media, JingleRtp.PayloadType type) {
|
||||||
|
var iter = type.rtcp_fbs.iterator();
|
||||||
|
while (iter.next()) {
|
||||||
|
var fb = iter.@get();
|
||||||
|
switch (fb.type_) {
|
||||||
|
case "goog-remb":
|
||||||
|
if (fb.subtype != null) iter.remove();
|
||||||
|
break;
|
||||||
|
case "ccm":
|
||||||
|
if (fb.subtype != "fir") iter.remove();
|
||||||
|
break;
|
||||||
|
case "nack":
|
||||||
|
if (fb.subtype != null && fb.subtype != "pli") iter.remove();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
iter.remove();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override JingleRtp.Stream create_stream(Jingle.Content content) {
|
||||||
|
return plugin.open_stream(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void close_stream(JingleRtp.Stream stream) {
|
||||||
|
var rtp_stream = stream as Rtp.Stream;
|
||||||
|
plugin.close_stream(rtp_stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override JingleRtp.Crypto? generate_local_crypto() {
|
||||||
|
uint8[] key_and_salt = new uint8[30];
|
||||||
|
Crypto.randomize(key_and_salt);
|
||||||
|
return JingleRtp.Crypto.create(JingleRtp.Crypto.AES_CM_128_HMAC_SHA1_80, key_and_salt);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override JingleRtp.Crypto? pick_remote_crypto(Gee.List<JingleRtp.Crypto> cryptos) {
|
||||||
|
foreach (JingleRtp.Crypto crypto in cryptos) {
|
||||||
|
if (crypto.is_valid) return crypto;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override JingleRtp.Crypto? pick_local_crypto(JingleRtp.Crypto? remote) {
|
||||||
|
if (remote == null || !remote.is_valid) return null;
|
||||||
|
uint8[] key_and_salt = new uint8[30];
|
||||||
|
Crypto.randomize(key_and_salt);
|
||||||
|
return remote.rekey(key_and_salt);
|
||||||
|
}
|
||||||
|
}
|
39
plugins/rtp/src/participant.vala
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
using Gee;
|
||||||
|
using Xmpp;
|
||||||
|
|
||||||
|
public class Dino.Plugins.Rtp.Participant {
|
||||||
|
public Jid full_jid { get; private set; }
|
||||||
|
|
||||||
|
protected Gst.Pipeline pipe;
|
||||||
|
private Map<Stream, uint32> ssrcs = new HashMap<Stream, uint32>();
|
||||||
|
|
||||||
|
public Participant(Gst.Pipeline pipe, Jid full_jid) {
|
||||||
|
this.pipe = pipe;
|
||||||
|
this.full_jid = full_jid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint32 get_ssrc(Stream stream) {
|
||||||
|
if (ssrcs.has_key(stream)) {
|
||||||
|
return ssrcs[stream];
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_ssrc(Stream stream, uint32 ssrc) {
|
||||||
|
if (ssrcs.has_key(stream)) {
|
||||||
|
warning("Learning ssrc %ul for %s in %s when it is already known as %ul", ssrc, full_jid.to_string(), stream.to_string(), ssrcs[stream]);
|
||||||
|
} else {
|
||||||
|
stream.on_destroy.connect(unset_ssrc);
|
||||||
|
}
|
||||||
|
ssrcs[stream] = ssrc;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unset_ssrc(Stream stream) {
|
||||||
|
ssrcs.unset(stream);
|
||||||
|
stream.on_destroy.disconnect(unset_ssrc);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string to_string() {
|
||||||
|
return @"participant $full_jid";
|
||||||
|
}
|
||||||
|
}
|
449
plugins/rtp/src/plugin.vala
Normal file
|
@ -0,0 +1,449 @@
|
||||||
|
using Gee;
|
||||||
|
using Xmpp;
|
||||||
|
using Xmpp.Xep;
|
||||||
|
|
||||||
|
public class Dino.Plugins.Rtp.Plugin : RootInterface, VideoCallPlugin, Object {
|
||||||
|
public Dino.Application app { get; private set; }
|
||||||
|
public CodecUtil codec_util { get; private set; }
|
||||||
|
public Gst.DeviceMonitor device_monitor { get; private set; }
|
||||||
|
public Gst.Pipeline pipe { get; private set; }
|
||||||
|
public Gst.Bin rtpbin { get; private set; }
|
||||||
|
public Gst.Element echoprobe { get; private set; }
|
||||||
|
|
||||||
|
private Gee.List<Stream> streams = new ArrayList<Stream>();
|
||||||
|
private Gee.List<Device> devices = new ArrayList<Device>();
|
||||||
|
// private Gee.List<Participant> participants = new ArrayList<Participant>();
|
||||||
|
|
||||||
|
public void registered(Dino.Application app) {
|
||||||
|
this.app = app;
|
||||||
|
this.codec_util = new CodecUtil();
|
||||||
|
app.startup.connect(startup);
|
||||||
|
app.add_option_group(Gst.init_get_option_group());
|
||||||
|
app.stream_interactor.module_manager.initialize_account_modules.connect((account, list) => {
|
||||||
|
list.add(new Module(this));
|
||||||
|
});
|
||||||
|
app.plugin_registry.video_call_plugin = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int pause_count = 0;
|
||||||
|
public void pause() {
|
||||||
|
// if (pause_count == 0) {
|
||||||
|
// debug("Pausing pipe for modifications");
|
||||||
|
// pipe.set_state(Gst.State.PAUSED);
|
||||||
|
// }
|
||||||
|
pause_count++;
|
||||||
|
}
|
||||||
|
public void unpause() {
|
||||||
|
pause_count--;
|
||||||
|
if (pause_count == 0) {
|
||||||
|
debug("Continue pipe after modifications");
|
||||||
|
pipe.set_state(Gst.State.PLAYING);
|
||||||
|
}
|
||||||
|
if (pause_count < 0) warning("Pause count below zero!");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startup() {
|
||||||
|
device_monitor = new Gst.DeviceMonitor();
|
||||||
|
device_monitor.show_all = true;
|
||||||
|
device_monitor.get_bus().add_watch(Priority.DEFAULT, on_device_monitor_message);
|
||||||
|
device_monitor.start();
|
||||||
|
foreach (Gst.Device device in device_monitor.get_devices()) {
|
||||||
|
if (device.properties.has_name("pipewire-proplist") && device.device_class.has_prefix("Audio/")) continue;
|
||||||
|
if (device.properties.get_string("device.class") == "monitor") continue;
|
||||||
|
if (devices.any_match((it) => it.matches(device))) continue;
|
||||||
|
devices.add(new Device(this, device));
|
||||||
|
}
|
||||||
|
|
||||||
|
pipe = new Gst.Pipeline(null);
|
||||||
|
|
||||||
|
// RTP
|
||||||
|
rtpbin = Gst.ElementFactory.make("rtpbin", null) as Gst.Bin;
|
||||||
|
if (rtpbin == null) {
|
||||||
|
warning("RTP not supported");
|
||||||
|
pipe = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rtpbin.pad_added.connect(on_rtp_pad_added);
|
||||||
|
rtpbin.@set("latency", 100);
|
||||||
|
rtpbin.@set("do-lost", true);
|
||||||
|
rtpbin.@set("do-sync-event", true);
|
||||||
|
rtpbin.@set("drop-on-latency", true);
|
||||||
|
rtpbin.connect("signal::request-pt-map", request_pt_map, this);
|
||||||
|
pipe.add(rtpbin);
|
||||||
|
|
||||||
|
#if WITH_VOICE_PROCESSOR
|
||||||
|
// Audio echo probe
|
||||||
|
echoprobe = new EchoProbe();
|
||||||
|
if (echoprobe != null) pipe.add(echoprobe);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Pipeline
|
||||||
|
pipe.auto_flush_bus = true;
|
||||||
|
pipe.bus.add_watch(GLib.Priority.DEFAULT, (_, message) => {
|
||||||
|
on_pipe_bus_message(message);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
pipe.set_state(Gst.State.PLAYING);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Gst.Caps? request_pt_map(Gst.Element rtpbin, uint session, uint pt, Plugin plugin) {
|
||||||
|
debug("request-pt-map");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_rtp_pad_added(Gst.Pad pad) {
|
||||||
|
debug("pad added: %s", pad.name);
|
||||||
|
if (pad.name.has_prefix("recv_rtp_src_")) {
|
||||||
|
string[] split = pad.name.split("_");
|
||||||
|
uint8 rtpid = (uint8)int.parse(split[3]);
|
||||||
|
foreach (Stream stream in streams) {
|
||||||
|
if (stream.rtpid == rtpid) {
|
||||||
|
stream.on_ssrc_pad_added(split[4], pad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pad.name.has_prefix("send_rtp_src_")) {
|
||||||
|
string[] split = pad.name.split("_");
|
||||||
|
uint8 rtpid = (uint8)int.parse(split[3]);
|
||||||
|
debug("pad %s for stream %hhu", pad.name, rtpid);
|
||||||
|
foreach (Stream stream in streams) {
|
||||||
|
if (stream.rtpid == rtpid) {
|
||||||
|
stream.on_send_rtp_src_added(pad);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_pipe_bus_message(Gst.Message message) {
|
||||||
|
switch (message.type) {
|
||||||
|
case Gst.MessageType.ERROR:
|
||||||
|
Error error;
|
||||||
|
string str;
|
||||||
|
message.parse_error(out error, out str);
|
||||||
|
warning("Error in pipeline: %s", error.message);
|
||||||
|
debug(str);
|
||||||
|
break;
|
||||||
|
case Gst.MessageType.WARNING:
|
||||||
|
Error error;
|
||||||
|
string str;
|
||||||
|
message.parse_warning(out error, out str);
|
||||||
|
warning("Warning in pipeline: %s", error.message);
|
||||||
|
debug(str);
|
||||||
|
break;
|
||||||
|
case Gst.MessageType.CLOCK_LOST:
|
||||||
|
debug("Clock lost. Restarting");
|
||||||
|
pipe.set_state(Gst.State.READY);
|
||||||
|
pipe.set_state(Gst.State.PLAYING);
|
||||||
|
break;
|
||||||
|
case Gst.MessageType.STATE_CHANGED:
|
||||||
|
// Ignore
|
||||||
|
break;
|
||||||
|
case Gst.MessageType.STREAM_STATUS:
|
||||||
|
Gst.StreamStatusType status;
|
||||||
|
Gst.Element owner;
|
||||||
|
message.parse_stream_status(out status, out owner);
|
||||||
|
if (owner != null) {
|
||||||
|
debug("%s stream changed status to %s", owner.name, status.to_string());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Gst.MessageType.ELEMENT:
|
||||||
|
unowned Gst.Structure struc = message.get_structure();
|
||||||
|
if (struc != null && message.src is Gst.Element) {
|
||||||
|
debug("Message from %s in pipeline: %s", ((Gst.Element)message.src).name, struc.to_string());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case Gst.MessageType.NEW_CLOCK:
|
||||||
|
debug("New clock.");
|
||||||
|
break;
|
||||||
|
case Gst.MessageType.TAG:
|
||||||
|
// Ignore
|
||||||
|
break;
|
||||||
|
case Gst.MessageType.QOS:
|
||||||
|
// Ignore
|
||||||
|
break;
|
||||||
|
case Gst.MessageType.LATENCY:
|
||||||
|
if (message.src != null && message.src.name != null && message.src is Gst.Element) {
|
||||||
|
Gst.Query latency_query = new Gst.Query.latency();
|
||||||
|
if (((Gst.Element)message.src).query(latency_query)) {
|
||||||
|
bool live;
|
||||||
|
Gst.ClockTime min_latency, max_latency;
|
||||||
|
latency_query.parse_latency(out live, out min_latency, out max_latency);
|
||||||
|
debug("Latency message from %s: live=%s, min_latency=%s, max_latency=%s", message.src.name, live.to_string(), min_latency.to_string(), max_latency.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
debug("Pipe bus message: %s", message.type.to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool on_device_monitor_message(Gst.Bus bus, Gst.Message message) {
|
||||||
|
Gst.Device old_device = null;
|
||||||
|
Gst.Device device = null;
|
||||||
|
Device old = null;
|
||||||
|
switch (message.type) {
|
||||||
|
case Gst.MessageType.DEVICE_ADDED:
|
||||||
|
message.parse_device_added(out device);
|
||||||
|
if (device.properties.has_name("pipewire-proplist") && device.device_class.has_prefix("Audio/")) return Source.CONTINUE;
|
||||||
|
if (device.properties.get_string("device.class") == "monitor") return Source.CONTINUE;
|
||||||
|
if (devices.any_match((it) => it.matches(device))) return Source.CONTINUE;
|
||||||
|
devices.add(new Device(this, device));
|
||||||
|
break;
|
||||||
|
#if GST_1_16
|
||||||
|
case Gst.MessageType.DEVICE_CHANGED:
|
||||||
|
message.parse_device_changed(out device, out old_device);
|
||||||
|
if (device.properties.has_name("pipewire-proplist") && device.device_class.has_prefix("Audio/")) return Source.CONTINUE;
|
||||||
|
if (device.properties.get_string("device.class") == "monitor") return Source.CONTINUE;
|
||||||
|
old = devices.first_match((it) => it.matches(old_device));
|
||||||
|
if (old != null) old.update(device);
|
||||||
|
break;
|
||||||
|
#endif
|
||||||
|
case Gst.MessageType.DEVICE_REMOVED:
|
||||||
|
message.parse_device_removed(out device);
|
||||||
|
if (device.properties.has_name("pipewire-proplist") && device.device_class.has_prefix("Audio/")) return Source.CONTINUE;
|
||||||
|
if (device.properties.get_string("device.class") == "monitor") return Source.CONTINUE;
|
||||||
|
old = devices.first_match((it) => it.matches(device));
|
||||||
|
if (old != null) devices.remove(old);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (device != null) {
|
||||||
|
switch (device.device_class) {
|
||||||
|
case "Audio/Source":
|
||||||
|
devices_changed("audio", false);
|
||||||
|
break;
|
||||||
|
case "Audio/Sink":
|
||||||
|
devices_changed("audio", true);
|
||||||
|
break;
|
||||||
|
case "Video/Source":
|
||||||
|
devices_changed("video", false);
|
||||||
|
break;
|
||||||
|
case "Video/Sink":
|
||||||
|
devices_changed("video", true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Source.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint8 next_free_id() {
|
||||||
|
uint8 rtpid = 0;
|
||||||
|
while (streams.size < 100 && streams.any_match((stream) => stream.rtpid == rtpid)) {
|
||||||
|
rtpid++;
|
||||||
|
}
|
||||||
|
return rtpid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// public Participant get_participant(Jid full_jid, bool self) {
|
||||||
|
// foreach (Participant participant in participants) {
|
||||||
|
// if (participant.full_jid.equals(full_jid)) {
|
||||||
|
// return participant;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// Participant participant;
|
||||||
|
// if (self) {
|
||||||
|
// participant = new SelfParticipant(pipe, full_jid);
|
||||||
|
// } else {
|
||||||
|
// participant = new Participant(pipe, full_jid);
|
||||||
|
// }
|
||||||
|
// participants.add(participant);
|
||||||
|
// return participant;
|
||||||
|
// }
|
||||||
|
|
||||||
|
public Stream open_stream(Xmpp.Xep.Jingle.Content content) {
|
||||||
|
var content_params = content.content_params as Xmpp.Xep.JingleRtp.Parameters;
|
||||||
|
if (content_params == null) return null;
|
||||||
|
Stream stream;
|
||||||
|
if (content_params.media == "video") {
|
||||||
|
stream = new VideoStream(this, content);
|
||||||
|
} else {
|
||||||
|
stream = new Stream(this, content);
|
||||||
|
}
|
||||||
|
streams.add(stream);
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close_stream(Stream stream) {
|
||||||
|
streams.remove(stream);
|
||||||
|
stream.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void shutdown() {
|
||||||
|
device_monitor.stop();
|
||||||
|
pipe.set_state(Gst.State.NULL);
|
||||||
|
rtpbin = null;
|
||||||
|
pipe = null;
|
||||||
|
Gst.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool supports(string media) {
|
||||||
|
if (rtpbin == null) return false;
|
||||||
|
|
||||||
|
if (media == "audio") {
|
||||||
|
if (get_devices("audio", false).is_empty) return false;
|
||||||
|
if (get_devices("audio", true).is_empty) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media == "video") {
|
||||||
|
if (Gst.ElementFactory.make("gtksink", null) == null) return false;
|
||||||
|
if (get_devices("video", false).is_empty) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VideoCallWidget? create_widget(WidgetType type) {
|
||||||
|
if (type == WidgetType.GTK) {
|
||||||
|
return new VideoWidget(this);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gee.List<MediaDevice> get_devices(string media, bool incoming) {
|
||||||
|
if (media == "video" && !incoming) {
|
||||||
|
return get_video_sources();
|
||||||
|
}
|
||||||
|
|
||||||
|
ArrayList<MediaDevice> result = new ArrayList<MediaDevice>();
|
||||||
|
foreach (Device device in devices) {
|
||||||
|
if (device.media == media && (incoming && device.is_sink || !incoming && device.is_source)) {
|
||||||
|
result.add(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (media == "audio") {
|
||||||
|
// Reorder sources
|
||||||
|
result.sort((media_left, media_right) => {
|
||||||
|
Device left = media_left as Device;
|
||||||
|
Device right = media_right as Device;
|
||||||
|
if (left == null) return 1;
|
||||||
|
if (right == null) return -1;
|
||||||
|
|
||||||
|
bool left_is_pipewire = left.device.properties.has_name("pipewire-proplist");
|
||||||
|
bool right_is_pipewire = right.device.properties.has_name("pipewire-proplist");
|
||||||
|
|
||||||
|
bool left_is_default = false;
|
||||||
|
left.device.properties.get_boolean("is-default", out left_is_default);
|
||||||
|
bool right_is_default = false;
|
||||||
|
right.device.properties.get_boolean("is-default", out right_is_default);
|
||||||
|
|
||||||
|
// Prefer pipewire
|
||||||
|
if (left_is_pipewire && !right_is_pipewire) return -1;
|
||||||
|
if (right_is_pipewire && !left_is_pipewire) return 1;
|
||||||
|
|
||||||
|
// Prefer pulse audio default device
|
||||||
|
if (left_is_default && !right_is_default) return -1;
|
||||||
|
if (right_is_default && !left_is_default) return 1;
|
||||||
|
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Gee.List<MediaDevice> get_video_sources() {
|
||||||
|
ArrayList<MediaDevice> pipewire_devices = new ArrayList<MediaDevice>();
|
||||||
|
ArrayList<MediaDevice> other_devices = new ArrayList<MediaDevice>();
|
||||||
|
|
||||||
|
foreach (Device device in devices) {
|
||||||
|
if (device.media != "video") continue;
|
||||||
|
if (device.is_sink) continue;
|
||||||
|
|
||||||
|
bool is_color = false;
|
||||||
|
for (int i = 0; i < device.device.caps.get_size(); i++) {
|
||||||
|
unowned Gst.Structure structure = device.device.caps.get_structure(i);
|
||||||
|
if (structure.has_field("format") && !structure.get_string("format").has_prefix("GRAY")) {
|
||||||
|
is_color = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow grey-scale devices
|
||||||
|
if (!is_color) continue;
|
||||||
|
|
||||||
|
if (device.device.properties.has_name("pipewire-proplist")) {
|
||||||
|
pipewire_devices.add(device);
|
||||||
|
} else {
|
||||||
|
other_devices.add(device);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have any pipewire devices, present only those. Don't want duplicated devices from pipewire and video for linux.
|
||||||
|
ArrayList<MediaDevice> devices = pipewire_devices.size > 0 ? pipewire_devices : other_devices;
|
||||||
|
|
||||||
|
// Reorder sources
|
||||||
|
devices.sort((media_left, media_right) => {
|
||||||
|
Device left = media_left as Device;
|
||||||
|
Device right = media_right as Device;
|
||||||
|
if (left == null) return 1;
|
||||||
|
if (right == null) return -1;
|
||||||
|
|
||||||
|
int left_fps = 0;
|
||||||
|
for (int i = 0; i < left.device.caps.get_size(); i++) {
|
||||||
|
unowned Gst.Structure structure = left.device.caps.get_structure(i);
|
||||||
|
int num = 0, den = 0;
|
||||||
|
if (structure.has_field("framerate") && structure.get_fraction("framerate", out num, out den)) left_fps = int.max(left_fps, num / den);
|
||||||
|
}
|
||||||
|
|
||||||
|
int right_fps = 0;
|
||||||
|
for (int i = 0; i < left.device.caps.get_size(); i++) {
|
||||||
|
unowned Gst.Structure structure = left.device.caps.get_structure(i);
|
||||||
|
int num = 0, den = 0;
|
||||||
|
if (structure.has_field("framerate") && structure.get_fraction("framerate", out num, out den)) right_fps = int.max(right_fps, num / den);
|
||||||
|
}
|
||||||
|
|
||||||
|
// More FPS is better
|
||||||
|
if (left_fps > right_fps) return -1;
|
||||||
|
if (right_fps > left_fps) return 1;
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Device? get_preferred_device(string media, bool incoming) {
|
||||||
|
foreach (MediaDevice media_device in get_devices(media, incoming)) {
|
||||||
|
Device? device = media_device as Device;
|
||||||
|
if (device != null) return device;
|
||||||
|
}
|
||||||
|
warning("No preferred device for %s %s. Media will not be processed.", incoming ? "incoming" : "outgoing", media);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaDevice? get_device(Xmpp.Xep.JingleRtp.Stream stream, bool incoming) {
|
||||||
|
Stream plugin_stream = stream as Stream;
|
||||||
|
if (plugin_stream == null) return null;
|
||||||
|
if (incoming) {
|
||||||
|
return plugin_stream.output_device ?? get_preferred_device(stream.media, incoming);
|
||||||
|
} else {
|
||||||
|
return plugin_stream.input_device ?? get_preferred_device(stream.media, incoming);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dump_dot() {
|
||||||
|
string name = @"pipe-$(pipe.clock.get_time())-$(pipe.current_state)";
|
||||||
|
Gst.Debug.bin_to_dot_file(pipe, Gst.DebugGraphDetails.ALL, name);
|
||||||
|
debug("Stored pipe details as %s", name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_pause(Xmpp.Xep.JingleRtp.Stream stream, bool pause) {
|
||||||
|
Stream plugin_stream = stream as Stream;
|
||||||
|
if (plugin_stream == null) return;
|
||||||
|
if (pause) {
|
||||||
|
plugin_stream.pause();
|
||||||
|
} else {
|
||||||
|
plugin_stream.unpause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_device(Xmpp.Xep.JingleRtp.Stream stream, MediaDevice? device) {
|
||||||
|
Device real_device = device as Device;
|
||||||
|
Stream plugin_stream = stream as Stream;
|
||||||
|
if (real_device == null || plugin_stream == null) return;
|
||||||
|
if (real_device.is_source) {
|
||||||
|
plugin_stream.input_device = real_device;
|
||||||
|
} else if (real_device.is_sink) {
|
||||||
|
plugin_stream.output_device = real_device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
3
plugins/rtp/src/register_plugin.vala
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
public Type register_plugin(Module module) {
|
||||||
|
return typeof (Dino.Plugins.Rtp.Plugin);
|
||||||
|
}
|
681
plugins/rtp/src/stream.vala
Normal file
|
@ -0,0 +1,681 @@
|
||||||
|
using Gee;
|
||||||
|
using Xmpp;
|
||||||
|
|
||||||
|
public class Dino.Plugins.Rtp.Stream : Xmpp.Xep.JingleRtp.Stream {
|
||||||
|
public uint8 rtpid { get; private set; }
|
||||||
|
|
||||||
|
public Plugin plugin { get; private set; }
|
||||||
|
public Gst.Pipeline pipe { get {
|
||||||
|
return plugin.pipe;
|
||||||
|
}}
|
||||||
|
public Gst.Element rtpbin { get {
|
||||||
|
return plugin.rtpbin;
|
||||||
|
}}
|
||||||
|
public CodecUtil codec_util { get {
|
||||||
|
return plugin.codec_util;
|
||||||
|
}}
|
||||||
|
private Gst.App.Sink send_rtp;
|
||||||
|
private Gst.App.Sink send_rtcp;
|
||||||
|
private Gst.App.Src recv_rtp;
|
||||||
|
private Gst.App.Src recv_rtcp;
|
||||||
|
private Gst.Element encode;
|
||||||
|
private Gst.RTP.BasePayload encode_pay;
|
||||||
|
private Gst.Element decode;
|
||||||
|
private Gst.RTP.BaseDepayload decode_depay;
|
||||||
|
private Gst.Element input;
|
||||||
|
private Gst.Element output;
|
||||||
|
private Gst.Element session;
|
||||||
|
|
||||||
|
private Device _input_device;
|
||||||
|
public Device input_device { get { return _input_device; } set {
|
||||||
|
if (!paused) {
|
||||||
|
if (this._input_device != null) {
|
||||||
|
this._input_device.unlink();
|
||||||
|
this._input_device = null;
|
||||||
|
}
|
||||||
|
set_input(value != null ? value.link_source() : null);
|
||||||
|
}
|
||||||
|
this._input_device = value;
|
||||||
|
}}
|
||||||
|
private Device _output_device;
|
||||||
|
public Device output_device { get { return _output_device; } set {
|
||||||
|
if (output != null) remove_output(output);
|
||||||
|
if (value != null) add_output(value.link_sink());
|
||||||
|
this._output_device = value;
|
||||||
|
}}
|
||||||
|
|
||||||
|
public bool created { get; private set; default = false; }
|
||||||
|
public bool paused { get; private set; default = false; }
|
||||||
|
private bool push_recv_data = false;
|
||||||
|
private string participant_ssrc = null;
|
||||||
|
|
||||||
|
private Gst.Pad recv_rtcp_sink_pad;
|
||||||
|
private Gst.Pad recv_rtp_sink_pad;
|
||||||
|
private Gst.Pad recv_rtp_src_pad;
|
||||||
|
private Gst.Pad send_rtcp_src_pad;
|
||||||
|
private Gst.Pad send_rtp_sink_pad;
|
||||||
|
private Gst.Pad send_rtp_src_pad;
|
||||||
|
|
||||||
|
private Crypto.Srtp.Session? crypto_session = new Crypto.Srtp.Session();
|
||||||
|
|
||||||
|
public Stream(Plugin plugin, Xmpp.Xep.Jingle.Content content) {
|
||||||
|
base(content);
|
||||||
|
this.plugin = plugin;
|
||||||
|
this.rtpid = plugin.next_free_id();
|
||||||
|
|
||||||
|
content.notify["senders"].connect_after(on_senders_changed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void on_senders_changed() {
|
||||||
|
if (sending && input == null) {
|
||||||
|
input_device = plugin.get_preferred_device(media, false);
|
||||||
|
}
|
||||||
|
if (receiving && output == null) {
|
||||||
|
output_device = plugin.get_preferred_device(media, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void create() {
|
||||||
|
plugin.pause();
|
||||||
|
|
||||||
|
// Create i/o if needed
|
||||||
|
|
||||||
|
if (input == null && input_device == null && sending) {
|
||||||
|
input_device = plugin.get_preferred_device(media, false);
|
||||||
|
}
|
||||||
|
if (output == null && output_device == null && receiving && media == "audio") {
|
||||||
|
output_device = plugin.get_preferred_device(media, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create app elements
|
||||||
|
send_rtp = Gst.ElementFactory.make("appsink", @"rtp_sink_$rtpid") as Gst.App.Sink;
|
||||||
|
send_rtp.async = false;
|
||||||
|
send_rtp.caps = CodecUtil.get_caps(media, payload_type, false);
|
||||||
|
send_rtp.emit_signals = true;
|
||||||
|
send_rtp.sync = false;
|
||||||
|
send_rtp.new_sample.connect(on_new_sample);
|
||||||
|
pipe.add(send_rtp);
|
||||||
|
|
||||||
|
send_rtcp = Gst.ElementFactory.make("appsink", @"rtcp_sink_$rtpid") as Gst.App.Sink;
|
||||||
|
send_rtcp.async = false;
|
||||||
|
send_rtcp.caps = new Gst.Caps.empty_simple("application/x-rtcp");
|
||||||
|
send_rtcp.emit_signals = true;
|
||||||
|
send_rtcp.sync = false;
|
||||||
|
send_rtcp.new_sample.connect(on_new_sample);
|
||||||
|
pipe.add(send_rtcp);
|
||||||
|
|
||||||
|
recv_rtp = Gst.ElementFactory.make("appsrc", @"rtp_src_$rtpid") as Gst.App.Src;
|
||||||
|
recv_rtp.caps = CodecUtil.get_caps(media, payload_type, true);
|
||||||
|
recv_rtp.do_timestamp = true;
|
||||||
|
recv_rtp.format = Gst.Format.TIME;
|
||||||
|
recv_rtp.is_live = true;
|
||||||
|
pipe.add(recv_rtp);
|
||||||
|
|
||||||
|
recv_rtcp = Gst.ElementFactory.make("appsrc", @"rtcp_src_$rtpid") as Gst.App.Src;
|
||||||
|
recv_rtcp.caps = new Gst.Caps.empty_simple("application/x-rtcp");
|
||||||
|
recv_rtcp.do_timestamp = true;
|
||||||
|
recv_rtcp.format = Gst.Format.TIME;
|
||||||
|
recv_rtcp.is_live = true;
|
||||||
|
pipe.add(recv_rtcp);
|
||||||
|
|
||||||
|
// Connect RTCP
|
||||||
|
send_rtcp_src_pad = rtpbin.get_request_pad(@"send_rtcp_src_$rtpid");
|
||||||
|
send_rtcp_src_pad.link(send_rtcp.get_static_pad("sink"));
|
||||||
|
recv_rtcp_sink_pad = rtpbin.get_request_pad(@"recv_rtcp_sink_$rtpid");
|
||||||
|
recv_rtcp.get_static_pad("src").link(recv_rtcp_sink_pad);
|
||||||
|
|
||||||
|
// Connect input
|
||||||
|
encode = codec_util.get_encode_bin(media, payload_type, @"encode_$rtpid");
|
||||||
|
encode_pay = (Gst.RTP.BasePayload)((Gst.Bin)encode).get_by_name(@"encode_$(rtpid)_rtp_pay");
|
||||||
|
pipe.add(encode);
|
||||||
|
send_rtp_sink_pad = rtpbin.get_request_pad(@"send_rtp_sink_$rtpid");
|
||||||
|
encode.get_static_pad("src").link(send_rtp_sink_pad);
|
||||||
|
if (input != null) {
|
||||||
|
input.link(encode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect output
|
||||||
|
decode = codec_util.get_decode_bin(media, payload_type, @"decode_$rtpid");
|
||||||
|
decode_depay = (Gst.RTP.BaseDepayload)((Gst.Bin)encode).get_by_name(@"decode_$(rtpid)_rtp_depay");
|
||||||
|
pipe.add(decode);
|
||||||
|
if (output != null) {
|
||||||
|
decode.link(output);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect RTP
|
||||||
|
recv_rtp_sink_pad = rtpbin.get_request_pad(@"recv_rtp_sink_$rtpid");
|
||||||
|
recv_rtp.get_static_pad("src").link(recv_rtp_sink_pad);
|
||||||
|
|
||||||
|
created = true;
|
||||||
|
push_recv_data = true;
|
||||||
|
plugin.unpause();
|
||||||
|
|
||||||
|
GLib.Signal.emit_by_name(rtpbin, "get-session", rtpid, out session);
|
||||||
|
if (session != null && payload_type.rtcp_fbs.any_match((it) => it.type_ == "goog-remb")) {
|
||||||
|
Object internal_session;
|
||||||
|
session.@get("internal-session", out internal_session);
|
||||||
|
if (internal_session != null) {
|
||||||
|
internal_session.connect("signal::on-feedback-rtcp", on_feedback_rtcp, this);
|
||||||
|
}
|
||||||
|
Timeout.add(1000, () => remb_adjust());
|
||||||
|
}
|
||||||
|
if (media == "video") {
|
||||||
|
codec_util.update_bitrate(media, payload_type, encode, 256);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint remb = 256;
|
||||||
|
private int last_packets_lost = -1;
|
||||||
|
private uint64 last_packets_received;
|
||||||
|
private uint64 last_octets_received;
|
||||||
|
private bool remb_adjust() {
|
||||||
|
unowned Gst.Structure? stats;
|
||||||
|
if (session == null) {
|
||||||
|
debug("Session for %u finished, turning off remb adjustment", rtpid);
|
||||||
|
return Source.REMOVE;
|
||||||
|
}
|
||||||
|
session.get("stats", out stats);
|
||||||
|
if (stats == null) {
|
||||||
|
warning("No stats for session %u", rtpid);
|
||||||
|
return Source.REMOVE;
|
||||||
|
}
|
||||||
|
unowned ValueArray? source_stats;
|
||||||
|
stats.get("source-stats", typeof(ValueArray), out source_stats);
|
||||||
|
if (source_stats == null) {
|
||||||
|
warning("No source-stats for session %u", rtpid);
|
||||||
|
return Source.REMOVE;
|
||||||
|
}
|
||||||
|
foreach (Value value in source_stats.values) {
|
||||||
|
unowned Gst.Structure source_stat = (Gst.Structure) value.get_boxed();
|
||||||
|
uint ssrc;
|
||||||
|
if (!source_stat.get_uint("ssrc", out ssrc)) continue;
|
||||||
|
if (ssrc.to_string() == participant_ssrc) {
|
||||||
|
int packets_lost;
|
||||||
|
uint64 packets_received, octets_received;
|
||||||
|
source_stat.get_int("packets-lost", out packets_lost);
|
||||||
|
source_stat.get_uint64("packets-received", out packets_received);
|
||||||
|
source_stat.get_uint64("octets-received", out octets_received);
|
||||||
|
int new_lost = packets_lost - last_packets_lost;
|
||||||
|
uint64 new_received = packets_received - last_packets_received;
|
||||||
|
uint64 new_octets = octets_received - last_octets_received;
|
||||||
|
if (new_received == 0) continue;
|
||||||
|
last_packets_lost = packets_lost;
|
||||||
|
last_packets_received = packets_received;
|
||||||
|
last_octets_received = octets_received;
|
||||||
|
double loss_rate = (double)new_lost / (double)(new_lost + new_received);
|
||||||
|
if (new_lost <= 0 || loss_rate < 0.02) {
|
||||||
|
remb = (uint)(1.08 * (double)remb);
|
||||||
|
} else if (loss_rate > 0.1) {
|
||||||
|
remb = (uint)((1.0 - 0.5 * loss_rate) * (double)remb);
|
||||||
|
}
|
||||||
|
remb = uint.max(remb, (uint)((new_octets * 8) / 1000));
|
||||||
|
remb = uint.max(16, remb); // Never go below 16
|
||||||
|
uint8[] data = new uint8[] {
|
||||||
|
143, 206, 0, 5,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
'R', 'E', 'M', 'B',
|
||||||
|
1, 0, 0, 0,
|
||||||
|
0, 0, 0, 0
|
||||||
|
};
|
||||||
|
data[4] = (uint8)((encode_pay.ssrc >> 24) & 0xff);
|
||||||
|
data[5] = (uint8)((encode_pay.ssrc >> 16) & 0xff);
|
||||||
|
data[6] = (uint8)((encode_pay.ssrc >> 8) & 0xff);
|
||||||
|
data[7] = (uint8)(encode_pay.ssrc & 0xff);
|
||||||
|
uint8 br_exp = 0;
|
||||||
|
uint32 br_mant = remb * 1000;
|
||||||
|
uint8 bits = (uint8)Math.log2(br_mant);
|
||||||
|
if (bits > 16) {
|
||||||
|
br_exp = (uint8)bits - 16;
|
||||||
|
br_mant = br_mant >> br_exp;
|
||||||
|
}
|
||||||
|
data[17] = (uint8)((br_exp << 2) | ((br_mant >> 16) & 0x3));
|
||||||
|
data[18] = (uint8)((br_mant >> 8) & 0xff);
|
||||||
|
data[19] = (uint8)(br_mant & 0xff);
|
||||||
|
data[20] = (uint8)((ssrc >> 24) & 0xff);
|
||||||
|
data[21] = (uint8)((ssrc >> 16) & 0xff);
|
||||||
|
data[22] = (uint8)((ssrc >> 8) & 0xff);
|
||||||
|
data[23] = (uint8)(ssrc & 0xff);
|
||||||
|
encrypt_and_send_rtcp(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Source.CONTINUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void on_feedback_rtcp(Gst.Element session, uint type, uint fbtype, uint sender_ssrc, uint media_ssrc, Gst.Buffer? fci, Stream self) {
|
||||||
|
if (type == 206 && fbtype == 15 && fci != null && sender_ssrc.to_string() == self.participant_ssrc) {
|
||||||
|
// https://tools.ietf.org/html/draft-alvestrand-rmcat-remb-03
|
||||||
|
uint8[] data;
|
||||||
|
fci.extract_dup(0, fci.get_size(), out data);
|
||||||
|
if (data[0] != 'R' || data[1] != 'E' || data[2] != 'M' || data[3] != 'B') return;
|
||||||
|
uint8 br_exp = data[5] >> 2;
|
||||||
|
uint32 br_mant = (((uint32)data[5] & 0x3) << 16) + ((uint32)data[6] << 8) + (uint32)data[7];
|
||||||
|
uint bitrate = (br_mant << br_exp) / 1000;
|
||||||
|
self.codec_util.update_bitrate(self.media, self.payload_type, self.encode, bitrate * 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepare_local_crypto() {
|
||||||
|
if (local_crypto != null && local_crypto.is_valid && !crypto_session.has_encrypt) {
|
||||||
|
crypto_session.set_encryption_key(local_crypto.crypto_suite, local_crypto.key, local_crypto.salt);
|
||||||
|
debug("Setting up encryption with key params %s", local_crypto.key_params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Gst.FlowReturn on_new_sample(Gst.App.Sink sink) {
|
||||||
|
if (sink == null) {
|
||||||
|
debug("Sink is null");
|
||||||
|
return Gst.FlowReturn.EOS;
|
||||||
|
}
|
||||||
|
Gst.Sample sample = sink.pull_sample();
|
||||||
|
Gst.Buffer buffer = sample.get_buffer();
|
||||||
|
uint8[] data;
|
||||||
|
buffer.extract_dup(0, buffer.get_size(), out data);
|
||||||
|
prepare_local_crypto();
|
||||||
|
if (sink == send_rtp) {
|
||||||
|
if (crypto_session.has_encrypt) {
|
||||||
|
data = crypto_session.encrypt_rtp(data);
|
||||||
|
}
|
||||||
|
on_send_rtp_data(new Bytes.take((owned) data));
|
||||||
|
} else if (sink == send_rtcp) {
|
||||||
|
encrypt_and_send_rtcp((owned) data);
|
||||||
|
} else {
|
||||||
|
warning("unknown sample");
|
||||||
|
}
|
||||||
|
return Gst.FlowReturn.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void encrypt_and_send_rtcp(owned uint8[] data) {
|
||||||
|
if (crypto_session.has_encrypt) {
|
||||||
|
data = crypto_session.encrypt_rtcp(data);
|
||||||
|
}
|
||||||
|
if (rtcp_mux) {
|
||||||
|
on_send_rtp_data(new Bytes.take((owned) data));
|
||||||
|
} else {
|
||||||
|
on_send_rtcp_data(new Bytes.take((owned) data));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Gst.PadProbeReturn drop_probe() {
|
||||||
|
return Gst.PadProbeReturn.DROP;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void destroy() {
|
||||||
|
// Stop network communication
|
||||||
|
push_recv_data = false;
|
||||||
|
recv_rtp.end_of_stream();
|
||||||
|
recv_rtcp.end_of_stream();
|
||||||
|
send_rtp.new_sample.disconnect(on_new_sample);
|
||||||
|
send_rtcp.new_sample.disconnect(on_new_sample);
|
||||||
|
|
||||||
|
// Disconnect input device
|
||||||
|
if (input != null) {
|
||||||
|
input.unlink(encode);
|
||||||
|
input = null;
|
||||||
|
}
|
||||||
|
if (this._input_device != null) {
|
||||||
|
if (!paused) this._input_device.unlink();
|
||||||
|
this._input_device = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect encode
|
||||||
|
encode.set_locked_state(true);
|
||||||
|
encode.set_state(Gst.State.NULL);
|
||||||
|
encode.get_static_pad("src").unlink(send_rtp_sink_pad);
|
||||||
|
pipe.remove(encode);
|
||||||
|
encode = null;
|
||||||
|
encode_pay = null;
|
||||||
|
|
||||||
|
// Disconnect RTP sending
|
||||||
|
if (send_rtp_src_pad != null) {
|
||||||
|
send_rtp_src_pad.add_probe(Gst.PadProbeType.BLOCK, drop_probe);
|
||||||
|
send_rtp_src_pad.unlink(send_rtp.get_static_pad("sink"));
|
||||||
|
}
|
||||||
|
send_rtp.set_locked_state(true);
|
||||||
|
send_rtp.set_state(Gst.State.NULL);
|
||||||
|
pipe.remove(send_rtp);
|
||||||
|
send_rtp = null;
|
||||||
|
|
||||||
|
// Disconnect decode
|
||||||
|
if (recv_rtp_src_pad != null) {
|
||||||
|
recv_rtp_src_pad.add_probe(Gst.PadProbeType.BLOCK, drop_probe);
|
||||||
|
recv_rtp_src_pad.unlink(decode.get_static_pad("sink"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect RTP receiving
|
||||||
|
recv_rtp.set_locked_state(true);
|
||||||
|
recv_rtp.set_state(Gst.State.NULL);
|
||||||
|
recv_rtp.get_static_pad("src").unlink(recv_rtp_sink_pad);
|
||||||
|
pipe.remove(recv_rtp);
|
||||||
|
recv_rtp = null;
|
||||||
|
|
||||||
|
// Disconnect output
|
||||||
|
if (output != null) {
|
||||||
|
decode.unlink(output);
|
||||||
|
}
|
||||||
|
decode.set_locked_state(true);
|
||||||
|
decode.set_state(Gst.State.NULL);
|
||||||
|
pipe.remove(decode);
|
||||||
|
decode = null;
|
||||||
|
decode_depay = null;
|
||||||
|
output = null;
|
||||||
|
|
||||||
|
// Disconnect output device
|
||||||
|
if (this._output_device != null) {
|
||||||
|
this._output_device.unlink();
|
||||||
|
this._output_device = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disconnect RTCP receiving
|
||||||
|
recv_rtcp.get_static_pad("src").unlink(recv_rtcp_sink_pad);
|
||||||
|
recv_rtcp.set_locked_state(true);
|
||||||
|
recv_rtcp.set_state(Gst.State.NULL);
|
||||||
|
pipe.remove(recv_rtcp);
|
||||||
|
recv_rtcp = null;
|
||||||
|
|
||||||
|
// Disconnect RTCP sending
|
||||||
|
send_rtcp_src_pad.unlink(send_rtcp.get_static_pad("sink"));
|
||||||
|
send_rtcp.set_locked_state(true);
|
||||||
|
send_rtcp.set_state(Gst.State.NULL);
|
||||||
|
pipe.remove(send_rtcp);
|
||||||
|
send_rtcp = null;
|
||||||
|
|
||||||
|
// Release rtp pads
|
||||||
|
rtpbin.release_request_pad(send_rtp_sink_pad);
|
||||||
|
send_rtp_sink_pad = null;
|
||||||
|
rtpbin.release_request_pad(recv_rtp_sink_pad);
|
||||||
|
recv_rtp_sink_pad = null;
|
||||||
|
rtpbin.release_request_pad(recv_rtcp_sink_pad);
|
||||||
|
recv_rtcp_sink_pad = null;
|
||||||
|
rtpbin.release_request_pad(send_rtcp_src_pad);
|
||||||
|
send_rtcp_src_pad = null;
|
||||||
|
send_rtp_src_pad = null;
|
||||||
|
recv_rtp_src_pad = null;
|
||||||
|
|
||||||
|
session = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepare_remote_crypto() {
|
||||||
|
if (remote_crypto != null && remote_crypto.is_valid && !crypto_session.has_decrypt) {
|
||||||
|
crypto_session.set_decryption_key(remote_crypto.crypto_suite, remote_crypto.key, remote_crypto.salt);
|
||||||
|
debug("Setting up decryption with key params %s", remote_crypto.key_params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint16 previous_video_orientation_degree = uint16.MAX;
|
||||||
|
public signal void video_orientation_changed(uint16 degree);
|
||||||
|
|
||||||
|
public override void on_recv_rtp_data(Bytes bytes) {
|
||||||
|
if (rtcp_mux && bytes.length >= 2 && bytes.get(1) >= 192 && bytes.get(1) < 224) {
|
||||||
|
on_recv_rtcp_data(bytes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prepare_remote_crypto();
|
||||||
|
uint8[] data = bytes.get_data();
|
||||||
|
if (crypto_session.has_decrypt) {
|
||||||
|
try {
|
||||||
|
data = crypto_session.decrypt_rtp(data);
|
||||||
|
} catch (Error e) {
|
||||||
|
warning("%s (%d)", e.message, e.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (push_recv_data) {
|
||||||
|
Gst.Buffer buffer = new Gst.Buffer.wrapped((owned) data);
|
||||||
|
Gst.RTP.Buffer rtp_buffer;
|
||||||
|
if (Gst.RTP.Buffer.map(buffer, Gst.MapFlags.READ, out rtp_buffer)) {
|
||||||
|
if (rtp_buffer.get_extension()) {
|
||||||
|
Xmpp.Xep.JingleRtp.HeaderExtension? ext = header_extensions.first_match((it) => it.uri == "urn:3gpp:video-orientation");
|
||||||
|
if (ext != null) {
|
||||||
|
unowned uint8[] extension_data;
|
||||||
|
if (rtp_buffer.get_extension_onebyte_header(ext.id, 0, out extension_data) && extension_data.length == 1) {
|
||||||
|
bool camera = (extension_data[0] & 0x8) > 0;
|
||||||
|
bool flip = (extension_data[0] & 0x4) > 0;
|
||||||
|
uint8 rotation = extension_data[0] & 0x3;
|
||||||
|
uint16 rotation_degree = uint16.MAX;
|
||||||
|
switch(rotation) {
|
||||||
|
case 0: rotation_degree = 0; break;
|
||||||
|
case 1: rotation_degree = 90; break;
|
||||||
|
case 2: rotation_degree = 180; break;
|
||||||
|
case 3: rotation_degree = 270; break;
|
||||||
|
}
|
||||||
|
if (rotation_degree != previous_video_orientation_degree) {
|
||||||
|
video_orientation_changed(rotation_degree);
|
||||||
|
previous_video_orientation_degree = rotation_degree;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rtp_buffer.unmap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: VAPI file in Vala < 0.49.1 has a bug that results in broken ownership of buffer in push_buffer()
|
||||||
|
// We workaround by using the plain signal. The signal unfortunately will cause an unnecessary copy of
|
||||||
|
// the underlying buffer, so and some point we should move over to the new version (once we require
|
||||||
|
// Vala >= 0.50)
|
||||||
|
#if FIXED_APPSRC_PUSH_BUFFER_IN_VAPI
|
||||||
|
recv_rtp.push_buffer((owned) buffer);
|
||||||
|
#else
|
||||||
|
Gst.FlowReturn ret;
|
||||||
|
GLib.Signal.emit_by_name(recv_rtp, "push-buffer", buffer, out ret);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void on_recv_rtcp_data(Bytes bytes) {
|
||||||
|
prepare_remote_crypto();
|
||||||
|
uint8[] data = bytes.get_data();
|
||||||
|
if (crypto_session.has_decrypt) {
|
||||||
|
try {
|
||||||
|
data = crypto_session.decrypt_rtcp(data);
|
||||||
|
} catch (Error e) {
|
||||||
|
warning("%s (%d)", e.message, e.code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (push_recv_data) {
|
||||||
|
Gst.Buffer buffer = new Gst.Buffer.wrapped((owned) data);
|
||||||
|
// See above
|
||||||
|
#if FIXED_APPSRC_PUSH_BUFFER_IN_VAPI
|
||||||
|
recv_rtcp.push_buffer((owned) buffer);
|
||||||
|
#else
|
||||||
|
Gst.FlowReturn ret;
|
||||||
|
GLib.Signal.emit_by_name(recv_rtcp, "push-buffer", buffer, out ret);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void on_rtp_ready() {
|
||||||
|
// If full frame has been sent before the connection was ready, the counterpart would only display our video after the next full frame.
|
||||||
|
// Send a full frame to let the counterpart display our video asap
|
||||||
|
rtpbin.send_event(new Gst.Event.custom(
|
||||||
|
Gst.EventType.CUSTOM_UPSTREAM,
|
||||||
|
new Gst.Structure("GstForceKeyUnit", "all-headers", typeof(bool), true, null))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void on_rtcp_ready() {
|
||||||
|
int rtp_session_id = (int) rtpid;
|
||||||
|
uint64 max_delay = int.MAX;
|
||||||
|
Object rtp_session;
|
||||||
|
bool rtp_sent;
|
||||||
|
GLib.Signal.emit_by_name(rtpbin, "get-internal-session", rtp_session_id, out rtp_session);
|
||||||
|
GLib.Signal.emit_by_name(rtp_session, "send-rtcp-full", max_delay, out rtp_sent);
|
||||||
|
debug("RTCP is ready, resending rtcp: %s", rtp_sent.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void on_ssrc_pad_added(string ssrc, Gst.Pad pad) {
|
||||||
|
debug("New ssrc %s with pad %s", ssrc, pad.name);
|
||||||
|
if (participant_ssrc != null && participant_ssrc != ssrc) {
|
||||||
|
warning("Got second ssrc on stream (old: %s, new: %s), ignoring", participant_ssrc, ssrc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
participant_ssrc = ssrc;
|
||||||
|
recv_rtp_src_pad = pad;
|
||||||
|
if (decode != null) {
|
||||||
|
plugin.pause();
|
||||||
|
debug("Link %s to %s decode for %s", recv_rtp_src_pad.name, media, name);
|
||||||
|
recv_rtp_src_pad.link(decode.get_static_pad("sink"));
|
||||||
|
plugin.unpause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void on_send_rtp_src_added(Gst.Pad pad) {
|
||||||
|
send_rtp_src_pad = pad;
|
||||||
|
if (send_rtp != null) {
|
||||||
|
plugin.pause();
|
||||||
|
debug("Link %s to %s send_rtp for %s", send_rtp_src_pad.name, media, name);
|
||||||
|
send_rtp_src_pad.link(send_rtp.get_static_pad("sink"));
|
||||||
|
plugin.unpause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void set_input(Gst.Element? input) {
|
||||||
|
set_input_and_pause(input, paused);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void set_input_and_pause(Gst.Element? input, bool paused) {
|
||||||
|
if (created && this.input != null) {
|
||||||
|
this.input.unlink(encode);
|
||||||
|
this.input = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.input = input;
|
||||||
|
this.paused = paused;
|
||||||
|
|
||||||
|
if (created && sending && !paused && input != null) {
|
||||||
|
plugin.pause();
|
||||||
|
input.link(encode);
|
||||||
|
plugin.unpause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void pause() {
|
||||||
|
if (paused) return;
|
||||||
|
set_input_and_pause(null, true);
|
||||||
|
if (input_device != null) input_device.unlink();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unpause() {
|
||||||
|
if (!paused) return;
|
||||||
|
set_input_and_pause(input_device != null ? input_device.link_source() : null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong block_probe_handler_id = 0;
|
||||||
|
public virtual void add_output(Gst.Element element) {
|
||||||
|
if (output != null) {
|
||||||
|
critical("add_output() invoked more than once");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.output = element;
|
||||||
|
if (created) {
|
||||||
|
plugin.pause();
|
||||||
|
decode.link(element);
|
||||||
|
if (block_probe_handler_id != 0) {
|
||||||
|
decode.get_static_pad("src").remove_probe(block_probe_handler_id);
|
||||||
|
}
|
||||||
|
plugin.unpause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void remove_output(Gst.Element element) {
|
||||||
|
if (output != element) {
|
||||||
|
critical("remove_output() invoked without prior add_output()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (created) {
|
||||||
|
block_probe_handler_id = decode.get_static_pad("src").add_probe(Gst.PadProbeType.BLOCK, drop_probe);
|
||||||
|
decode.unlink(element);
|
||||||
|
}
|
||||||
|
if (this._output_device != null) {
|
||||||
|
this._output_device.unlink();
|
||||||
|
this._output_device = null;
|
||||||
|
}
|
||||||
|
this.output = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Dino.Plugins.Rtp.VideoStream : Stream {
|
||||||
|
private Gee.List<Gst.Element> outputs = new ArrayList<Gst.Element>();
|
||||||
|
private Gst.Element output_tee;
|
||||||
|
private Gst.Element rotate;
|
||||||
|
private ulong video_orientation_changed_handler;
|
||||||
|
|
||||||
|
public VideoStream(Plugin plugin, Xmpp.Xep.Jingle.Content content) {
|
||||||
|
base(plugin, content);
|
||||||
|
if (media != "video") critical("VideoStream created for non-video media");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void create() {
|
||||||
|
video_orientation_changed_handler = video_orientation_changed.connect(on_video_orientation_changed);
|
||||||
|
plugin.pause();
|
||||||
|
rotate = Gst.ElementFactory.make("videoflip", @"video_rotate_$rtpid");
|
||||||
|
pipe.add(rotate);
|
||||||
|
output_tee = Gst.ElementFactory.make("tee", @"video_tee_$rtpid");
|
||||||
|
output_tee.@set("allow-not-linked", true);
|
||||||
|
pipe.add(output_tee);
|
||||||
|
rotate.link(output_tee);
|
||||||
|
add_output(rotate);
|
||||||
|
base.create();
|
||||||
|
foreach (Gst.Element output in outputs) {
|
||||||
|
output_tee.link(output);
|
||||||
|
}
|
||||||
|
plugin.unpause();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void on_video_orientation_changed(uint16 degree) {
|
||||||
|
if (rotate != null) {
|
||||||
|
switch (degree) {
|
||||||
|
case 0:
|
||||||
|
rotate.@set("method", 0);
|
||||||
|
break;
|
||||||
|
case 90:
|
||||||
|
rotate.@set("method", 1);
|
||||||
|
break;
|
||||||
|
case 180:
|
||||||
|
rotate.@set("method", 2);
|
||||||
|
break;
|
||||||
|
case 270:
|
||||||
|
rotate.@set("method", 3);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void destroy() {
|
||||||
|
foreach (Gst.Element output in outputs) {
|
||||||
|
output_tee.unlink(output);
|
||||||
|
}
|
||||||
|
base.destroy();
|
||||||
|
rotate.set_locked_state(true);
|
||||||
|
rotate.set_state(Gst.State.NULL);
|
||||||
|
rotate.unlink(output_tee);
|
||||||
|
pipe.remove(rotate);
|
||||||
|
rotate = null;
|
||||||
|
output_tee.set_locked_state(true);
|
||||||
|
output_tee.set_state(Gst.State.NULL);
|
||||||
|
pipe.remove(output_tee);
|
||||||
|
output_tee = null;
|
||||||
|
disconnect(video_orientation_changed_handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void add_output(Gst.Element element) {
|
||||||
|
if (element == output_tee || element == rotate) {
|
||||||
|
base.add_output(element);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
outputs.add(element);
|
||||||
|
if (output_tee != null) {
|
||||||
|
output_tee.link(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void remove_output(Gst.Element element) {
|
||||||
|
if (element == output_tee || element == rotate) {
|
||||||
|
base.remove_output(element);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
outputs.remove(element);
|
||||||
|
if (output_tee != null) {
|
||||||
|
output_tee.unlink(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
110
plugins/rtp/src/video_widget.vala
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
public class Dino.Plugins.Rtp.VideoWidget : Gtk.Bin, Dino.Plugins.VideoCallWidget {
|
||||||
|
private static uint last_id = 0;
|
||||||
|
|
||||||
|
public uint id { get; private set; }
|
||||||
|
public Gst.Element element { get; private set; }
|
||||||
|
public Gtk.Widget widget { get; private set; }
|
||||||
|
|
||||||
|
public Plugin plugin { get; private set; }
|
||||||
|
public Gst.Pipeline pipe { get {
|
||||||
|
return plugin.pipe;
|
||||||
|
}}
|
||||||
|
|
||||||
|
private bool attached;
|
||||||
|
private Device? connected_device;
|
||||||
|
private Stream? connected_stream;
|
||||||
|
private Gst.Element convert;
|
||||||
|
|
||||||
|
public VideoWidget(Plugin plugin) {
|
||||||
|
this.plugin = plugin;
|
||||||
|
|
||||||
|
id = last_id++;
|
||||||
|
element = Gst.ElementFactory.make("gtksink", @"video_widget_$id");
|
||||||
|
if (element != null) {
|
||||||
|
Gtk.Widget widget;
|
||||||
|
element.@get("widget", out widget);
|
||||||
|
element.@set("async", false);
|
||||||
|
element.@set("sync", false);
|
||||||
|
this.widget = widget;
|
||||||
|
add(widget);
|
||||||
|
widget.visible = true;
|
||||||
|
|
||||||
|
// Listen for resolution changes
|
||||||
|
element.get_static_pad("sink").notify["caps"].connect(() => {
|
||||||
|
if (element.get_static_pad("sink").caps == null) return;
|
||||||
|
|
||||||
|
int width, height;
|
||||||
|
element.get_static_pad("sink").caps.get_structure(0).get_int("width", out width);
|
||||||
|
element.get_static_pad("sink").caps.get_structure(0).get_int("height", out height);
|
||||||
|
resolution_changed(width, height);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
warning("Could not create GTK video sink. Won't display videos.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void display_stream(Xmpp.Xep.JingleRtp.Stream stream) {
|
||||||
|
if (element == null) return;
|
||||||
|
detach();
|
||||||
|
if (stream.media != "video") return;
|
||||||
|
connected_stream = stream as Stream;
|
||||||
|
if (connected_stream == null) return;
|
||||||
|
plugin.pause();
|
||||||
|
pipe.add(element);
|
||||||
|
convert = Gst.parse_bin_from_description(@"videoconvert name=video_widget_$(id)_convert", true);
|
||||||
|
convert.name = @"video_widget_$(id)_prepare";
|
||||||
|
pipe.add(convert);
|
||||||
|
convert.link(element);
|
||||||
|
connected_stream.add_output(convert);
|
||||||
|
element.set_locked_state(false);
|
||||||
|
plugin.unpause();
|
||||||
|
attached = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void display_device(MediaDevice media_device) {
|
||||||
|
if (element == null) return;
|
||||||
|
detach();
|
||||||
|
connected_device = media_device as Device;
|
||||||
|
if (connected_device == null) return;
|
||||||
|
plugin.pause();
|
||||||
|
pipe.add(element);
|
||||||
|
convert = Gst.parse_bin_from_description(@"videoflip method=horizontal-flip name=video_widget_$(id)_flip ! videoconvert name=video_widget_$(id)_convert", true);
|
||||||
|
convert.name = @"video_widget_$(id)_prepare";
|
||||||
|
pipe.add(convert);
|
||||||
|
convert.link(element);
|
||||||
|
connected_device.link_source().link(convert);
|
||||||
|
element.set_locked_state(false);
|
||||||
|
plugin.unpause();
|
||||||
|
attached = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void detach() {
|
||||||
|
if (element == null) return;
|
||||||
|
if (attached) {
|
||||||
|
if (connected_stream != null) {
|
||||||
|
connected_stream.remove_output(convert);
|
||||||
|
connected_stream = null;
|
||||||
|
}
|
||||||
|
if (connected_device != null) {
|
||||||
|
connected_device.link_source().unlink(element);
|
||||||
|
connected_device.unlink(); // We get a new ref to recover the element, so unlink twice
|
||||||
|
connected_device.unlink();
|
||||||
|
connected_device = null;
|
||||||
|
}
|
||||||
|
convert.set_locked_state(true);
|
||||||
|
convert.set_state(Gst.State.NULL);
|
||||||
|
pipe.remove(convert);
|
||||||
|
convert = null;
|
||||||
|
element.set_locked_state(true);
|
||||||
|
element.set_state(Gst.State.NULL);
|
||||||
|
pipe.remove(element);
|
||||||
|
attached = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void dispose() {
|
||||||
|
detach();
|
||||||
|
widget = null;
|
||||||
|
element = null;
|
||||||
|
}
|
||||||
|
}
|
176
plugins/rtp/src/voice_processor.vala
Normal file
|
@ -0,0 +1,176 @@
|
||||||
|
using Gst;
|
||||||
|
|
||||||
|
namespace Dino.Plugins.Rtp {
|
||||||
|
public static extern Buffer adjust_to_running_time(Base.Transform transform, Buffer buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Dino.Plugins.Rtp.EchoProbe : Audio.Filter {
|
||||||
|
private static StaticPadTemplate sink_template = {"sink", PadDirection.SINK, PadPresence.ALWAYS, {null, "audio/x-raw,rate=48000,channels=1,layout=interleaved,format=S16LE"}};
|
||||||
|
private static StaticPadTemplate src_template = {"src", PadDirection.SRC, PadPresence.ALWAYS, {null, "audio/x-raw,rate=48000,channels=1,layout=interleaved,format=S16LE"}};
|
||||||
|
public Audio.Info audio_info { get; private set; }
|
||||||
|
public signal void on_new_buffer(Buffer buffer);
|
||||||
|
private uint period_samples;
|
||||||
|
private uint period_size;
|
||||||
|
private Base.Adapter adapter = new Base.Adapter();
|
||||||
|
|
||||||
|
static construct {
|
||||||
|
add_static_pad_template(sink_template);
|
||||||
|
add_static_pad_template(src_template);
|
||||||
|
set_static_metadata("Acoustic Echo Canceller probe", "Generic/Audio", "Gathers playback buffers for echo cancellation", "Dino Team <contact@dino.im>");
|
||||||
|
}
|
||||||
|
|
||||||
|
construct {
|
||||||
|
set_passthrough(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool setup(Audio.Info info) {
|
||||||
|
audio_info = info;
|
||||||
|
period_samples = info.rate / 100; // 10ms buffers
|
||||||
|
period_size = period_samples * info.bpf;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override FlowReturn transform_ip(Buffer buf) {
|
||||||
|
lock (adapter) {
|
||||||
|
adapter.push(adjust_to_running_time(this, buf));
|
||||||
|
while (adapter.available() > period_size) {
|
||||||
|
on_new_buffer(adapter.take_buffer(period_size));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FlowReturn.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool stop() {
|
||||||
|
adapter.clear();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Dino.Plugins.Rtp.VoiceProcessor : Audio.Filter {
|
||||||
|
private static StaticPadTemplate sink_template = {"sink", PadDirection.SINK, PadPresence.ALWAYS, {null, "audio/x-raw,rate=48000,channels=1,layout=interleaved,format=S16LE"}};
|
||||||
|
private static StaticPadTemplate src_template = {"src", PadDirection.SRC, PadPresence.ALWAYS, {null, "audio/x-raw,rate=48000,channels=1,layout=interleaved,format=S16LE"}};
|
||||||
|
public Audio.Info audio_info { get; private set; }
|
||||||
|
private ulong process_outgoing_buffer_handler_id;
|
||||||
|
private uint adjust_delay_timeout_id;
|
||||||
|
private uint period_samples;
|
||||||
|
private uint period_size;
|
||||||
|
private Base.Adapter adapter = new Base.Adapter();
|
||||||
|
private EchoProbe? echo_probe;
|
||||||
|
private Audio.StreamVolume? stream_volume;
|
||||||
|
private ClockTime last_reverse;
|
||||||
|
private void* native;
|
||||||
|
|
||||||
|
static construct {
|
||||||
|
add_static_pad_template(sink_template);
|
||||||
|
add_static_pad_template(src_template);
|
||||||
|
set_static_metadata("Voice Processor (AGC, AEC, filters, etc.)", "Generic/Audio", "Pre-processes voice with WebRTC Audio Processing Library", "Dino Team <contact@dino.im>");
|
||||||
|
}
|
||||||
|
|
||||||
|
construct {
|
||||||
|
set_passthrough(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public VoiceProcessor(EchoProbe? echo_probe = null, Audio.StreamVolume? stream_volume = null) {
|
||||||
|
this.echo_probe = echo_probe;
|
||||||
|
this.stream_volume = stream_volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static extern void* init_native(int stream_delay);
|
||||||
|
private static extern void setup_native(void* native);
|
||||||
|
private static extern void destroy_native(void* native);
|
||||||
|
private static extern void analyze_reverse_stream(void* native, Audio.Info info, Buffer buffer);
|
||||||
|
private static extern void process_stream(void* native, Audio.Info info, Buffer buffer);
|
||||||
|
private static extern void adjust_stream_delay(void* native);
|
||||||
|
private static extern void notify_gain_level(void* native, int gain_level);
|
||||||
|
private static extern int get_suggested_gain_level(void* native);
|
||||||
|
private static extern bool get_stream_has_voice(void* native);
|
||||||
|
|
||||||
|
public override bool setup(Audio.Info info) {
|
||||||
|
debug("VoiceProcessor.setup(%s)", info.to_caps().to_string());
|
||||||
|
audio_info = info;
|
||||||
|
period_samples = info.rate / 100; // 10ms buffers
|
||||||
|
period_size = period_samples * info.bpf;
|
||||||
|
adapter.clear();
|
||||||
|
setup_native(native);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool start() {
|
||||||
|
native = init_native(150);
|
||||||
|
if (process_outgoing_buffer_handler_id == 0 && echo_probe != null) {
|
||||||
|
process_outgoing_buffer_handler_id = echo_probe.on_new_buffer.connect(process_outgoing_buffer);
|
||||||
|
}
|
||||||
|
if (stream_volume == null && sinkpad.get_peer() != null && sinkpad.get_peer().get_parent_element() is Audio.StreamVolume) {
|
||||||
|
stream_volume = sinkpad.get_peer().get_parent_element() as Audio.StreamVolume;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool adjust_delay() {
|
||||||
|
if (native != null) {
|
||||||
|
adjust_stream_delay(native);
|
||||||
|
return Source.CONTINUE;
|
||||||
|
} else {
|
||||||
|
adjust_delay_timeout_id = 0;
|
||||||
|
return Source.REMOVE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void process_outgoing_buffer(Buffer buffer) {
|
||||||
|
if (buffer.pts != uint64.MAX) {
|
||||||
|
last_reverse = buffer.pts;
|
||||||
|
}
|
||||||
|
analyze_reverse_stream(native, echo_probe.audio_info, buffer);
|
||||||
|
if (adjust_delay_timeout_id == 0 && echo_probe != null) {
|
||||||
|
adjust_delay_timeout_id = Timeout.add(1000, adjust_delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override FlowReturn submit_input_buffer(bool is_discont, Buffer input) {
|
||||||
|
lock (adapter) {
|
||||||
|
if (is_discont) {
|
||||||
|
adapter.clear();
|
||||||
|
}
|
||||||
|
adapter.push(adjust_to_running_time(this, input));
|
||||||
|
}
|
||||||
|
return FlowReturn.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override FlowReturn generate_output(out Buffer output_buffer) {
|
||||||
|
lock (adapter) {
|
||||||
|
if (adapter.available() >= period_size) {
|
||||||
|
output_buffer = (Gst.Buffer) adapter.take_buffer(period_size).make_writable();
|
||||||
|
int old_gain_level = 0;
|
||||||
|
if (stream_volume != null) {
|
||||||
|
old_gain_level = (int) (stream_volume.get_volume(Audio.StreamVolumeFormat.LINEAR) * 255.0);
|
||||||
|
notify_gain_level(native, old_gain_level);
|
||||||
|
}
|
||||||
|
process_stream(native, audio_info, output_buffer);
|
||||||
|
if (stream_volume != null) {
|
||||||
|
int new_gain_level = get_suggested_gain_level(native);
|
||||||
|
if (old_gain_level != new_gain_level) {
|
||||||
|
debug("Gain: %i -> %i", old_gain_level, new_gain_level);
|
||||||
|
stream_volume.set_volume(Audio.StreamVolumeFormat.LINEAR, ((double)new_gain_level) / 255.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return FlowReturn.OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool stop() {
|
||||||
|
if (process_outgoing_buffer_handler_id != 0) {
|
||||||
|
echo_probe.disconnect(process_outgoing_buffer_handler_id);
|
||||||
|
process_outgoing_buffer_handler_id = 0;
|
||||||
|
}
|
||||||
|
if (adjust_delay_timeout_id != 0) {
|
||||||
|
Source.remove(adjust_delay_timeout_id);
|
||||||
|
adjust_delay_timeout_id = 0;
|
||||||
|
}
|
||||||
|
adapter.clear();
|
||||||
|
destroy_native(native);
|
||||||
|
native = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
148
plugins/rtp/src/voice_processor_native.cpp
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
#include <algorithm>
|
||||||
|
#include <gst/gst.h>
|
||||||
|
#include <gst/audio/audio.h>
|
||||||
|
#include <webrtc/modules/audio_processing/include/audio_processing.h>
|
||||||
|
#include <webrtc/modules/interface/module_common_types.h>
|
||||||
|
#include <webrtc/system_wrappers/include/trace.h>
|
||||||
|
|
||||||
|
#define SAMPLE_RATE 48000
|
||||||
|
#define SAMPLE_CHANNELS 1
|
||||||
|
|
||||||
|
struct _DinoPluginsRtpVoiceProcessorNative {
|
||||||
|
webrtc::AudioProcessing *apm;
|
||||||
|
gint stream_delay;
|
||||||
|
gint last_median;
|
||||||
|
gint last_poor_delays;
|
||||||
|
};
|
||||||
|
|
||||||
|
extern "C" void *dino_plugins_rtp_adjust_to_running_time(GstBaseTransform *transform, GstBuffer *buffer) {
|
||||||
|
GstBuffer *copy = gst_buffer_copy(buffer);
|
||||||
|
GST_BUFFER_PTS(copy) = gst_segment_to_running_time(&transform->segment, GST_FORMAT_TIME, GST_BUFFER_PTS(buffer));
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void *dino_plugins_rtp_voice_processor_init_native(gint stream_delay) {
|
||||||
|
_DinoPluginsRtpVoiceProcessorNative *native = new _DinoPluginsRtpVoiceProcessorNative();
|
||||||
|
webrtc::Config config;
|
||||||
|
config.Set<webrtc::ExtendedFilter>(new webrtc::ExtendedFilter(true));
|
||||||
|
config.Set<webrtc::ExperimentalAgc>(new webrtc::ExperimentalAgc(true, 85));
|
||||||
|
native->apm = webrtc::AudioProcessing::Create(config);
|
||||||
|
native->stream_delay = stream_delay;
|
||||||
|
native->last_median = 0;
|
||||||
|
native->last_poor_delays = 0;
|
||||||
|
return native;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void dino_plugins_rtp_voice_processor_setup_native(void *native_ptr) {
|
||||||
|
_DinoPluginsRtpVoiceProcessorNative *native = (_DinoPluginsRtpVoiceProcessorNative *) native_ptr;
|
||||||
|
webrtc::AudioProcessing *apm = native->apm;
|
||||||
|
webrtc::ProcessingConfig pconfig;
|
||||||
|
pconfig.streams[webrtc::ProcessingConfig::kInputStream] =
|
||||||
|
webrtc::StreamConfig(SAMPLE_RATE, SAMPLE_CHANNELS, false);
|
||||||
|
pconfig.streams[webrtc::ProcessingConfig::kOutputStream] =
|
||||||
|
webrtc::StreamConfig(SAMPLE_RATE, SAMPLE_CHANNELS, false);
|
||||||
|
pconfig.streams[webrtc::ProcessingConfig::kReverseInputStream] =
|
||||||
|
webrtc::StreamConfig(SAMPLE_RATE, SAMPLE_CHANNELS, false);
|
||||||
|
pconfig.streams[webrtc::ProcessingConfig::kReverseOutputStream] =
|
||||||
|
webrtc::StreamConfig(SAMPLE_RATE, SAMPLE_CHANNELS, false);
|
||||||
|
apm->Initialize(pconfig);
|
||||||
|
apm->high_pass_filter()->Enable(true);
|
||||||
|
apm->echo_cancellation()->enable_drift_compensation(false);
|
||||||
|
apm->echo_cancellation()->set_suppression_level(webrtc::EchoCancellation::kModerateSuppression);
|
||||||
|
apm->echo_cancellation()->enable_delay_logging(true);
|
||||||
|
apm->echo_cancellation()->Enable(true);
|
||||||
|
apm->noise_suppression()->set_level(webrtc::NoiseSuppression::kModerate);
|
||||||
|
apm->noise_suppression()->Enable(true);
|
||||||
|
apm->gain_control()->set_analog_level_limits(0, 255);
|
||||||
|
apm->gain_control()->set_mode(webrtc::GainControl::kAdaptiveAnalog);
|
||||||
|
apm->gain_control()->set_target_level_dbfs(3);
|
||||||
|
apm->gain_control()->set_compression_gain_db(9);
|
||||||
|
apm->gain_control()->enable_limiter(true);
|
||||||
|
apm->gain_control()->Enable(true);
|
||||||
|
apm->voice_detection()->set_likelihood(webrtc::VoiceDetection::Likelihood::kLowLikelihood);
|
||||||
|
apm->voice_detection()->Enable(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void
|
||||||
|
dino_plugins_rtp_voice_processor_analyze_reverse_stream(void *native_ptr, GstAudioInfo *info, GstBuffer *buffer) {
|
||||||
|
_DinoPluginsRtpVoiceProcessorNative *native = (_DinoPluginsRtpVoiceProcessorNative *) native_ptr;
|
||||||
|
webrtc::StreamConfig config(SAMPLE_RATE, SAMPLE_CHANNELS, false);
|
||||||
|
webrtc::AudioProcessing *apm = native->apm;
|
||||||
|
|
||||||
|
GstMapInfo map;
|
||||||
|
gst_buffer_map(buffer, &map, GST_MAP_READ);
|
||||||
|
|
||||||
|
webrtc::AudioFrame frame;
|
||||||
|
frame.num_channels_ = info->channels;
|
||||||
|
frame.sample_rate_hz_ = info->rate;
|
||||||
|
frame.samples_per_channel_ = gst_buffer_get_size(buffer) / info->bpf;
|
||||||
|
memcpy(frame.data_, map.data, frame.samples_per_channel_ * info->bpf);
|
||||||
|
|
||||||
|
int err = apm->AnalyzeReverseStream(&frame);
|
||||||
|
if (err < 0) g_warning("voice_processor_native.cpp: ProcessReverseStream %i", err);
|
||||||
|
|
||||||
|
gst_buffer_unmap(buffer, &map);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void dino_plugins_rtp_voice_processor_notify_gain_level(void *native_ptr, gint gain_level) {
|
||||||
|
_DinoPluginsRtpVoiceProcessorNative *native = (_DinoPluginsRtpVoiceProcessorNative *) native_ptr;
|
||||||
|
webrtc::AudioProcessing *apm = native->apm;
|
||||||
|
apm->gain_control()->set_stream_analog_level(gain_level);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" gint dino_plugins_rtp_voice_processor_get_suggested_gain_level(void *native_ptr) {
|
||||||
|
_DinoPluginsRtpVoiceProcessorNative *native = (_DinoPluginsRtpVoiceProcessorNative *) native_ptr;
|
||||||
|
webrtc::AudioProcessing *apm = native->apm;
|
||||||
|
return apm->gain_control()->stream_analog_level();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" bool dino_plugins_rtp_voice_processor_get_stream_has_voice(void *native_ptr) {
|
||||||
|
_DinoPluginsRtpVoiceProcessorNative *native = (_DinoPluginsRtpVoiceProcessorNative *) native_ptr;
|
||||||
|
webrtc::AudioProcessing *apm = native->apm;
|
||||||
|
return apm->voice_detection()->stream_has_voice();
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void dino_plugins_rtp_voice_processor_adjust_stream_delay(void *native_ptr) {
|
||||||
|
_DinoPluginsRtpVoiceProcessorNative *native = (_DinoPluginsRtpVoiceProcessorNative *) native_ptr;
|
||||||
|
webrtc::AudioProcessing *apm = native->apm;
|
||||||
|
int median, std, poor_delays;
|
||||||
|
float fraction_poor_delays;
|
||||||
|
apm->echo_cancellation()->GetDelayMetrics(&median, &std, &fraction_poor_delays);
|
||||||
|
poor_delays = (int)(fraction_poor_delays * 100.0);
|
||||||
|
if (fraction_poor_delays < 0 || (native->last_median == median && native->last_poor_delays == poor_delays)) return;
|
||||||
|
g_debug("voice_processor_native.cpp: Stream delay metrics: median=%i std=%i poor_delays=%i%%", median, std, poor_delays);
|
||||||
|
native->last_median = median;
|
||||||
|
native->last_poor_delays = poor_delays;
|
||||||
|
if (poor_delays > 90) {
|
||||||
|
native->stream_delay = std::min(std::max(0, native->stream_delay + std::min(48, std::max(median, -48))), 384);
|
||||||
|
g_debug("voice_processor_native.cpp: set stream_delay=%i", native->stream_delay);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void
|
||||||
|
dino_plugins_rtp_voice_processor_process_stream(void *native_ptr, GstAudioInfo *info, GstBuffer *buffer) {
|
||||||
|
_DinoPluginsRtpVoiceProcessorNative *native = (_DinoPluginsRtpVoiceProcessorNative *) native_ptr;
|
||||||
|
webrtc::StreamConfig config(SAMPLE_RATE, SAMPLE_CHANNELS, false);
|
||||||
|
webrtc::AudioProcessing *apm = native->apm;
|
||||||
|
|
||||||
|
GstMapInfo map;
|
||||||
|
gst_buffer_map(buffer, &map, GST_MAP_READWRITE);
|
||||||
|
|
||||||
|
webrtc::AudioFrame frame;
|
||||||
|
frame.num_channels_ = info->channels;
|
||||||
|
frame.sample_rate_hz_ = info->rate;
|
||||||
|
frame.samples_per_channel_ = info->rate / 100;
|
||||||
|
memcpy(frame.data_, map.data, frame.samples_per_channel_ * info->bpf);
|
||||||
|
|
||||||
|
apm->set_stream_delay_ms(native->stream_delay);
|
||||||
|
int err = apm->ProcessStream(&frame);
|
||||||
|
if (err >= 0) memcpy(map.data, frame.data_, frame.samples_per_channel_ * info->bpf);
|
||||||
|
if (err < 0) g_warning("voice_processor_native.cpp: ProcessStream %i", err);
|
||||||
|
|
||||||
|
gst_buffer_unmap(buffer, &map);
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" void dino_plugins_rtp_voice_processor_destroy_native(void *native_ptr) {
|
||||||
|
_DinoPluginsRtpVoiceProcessorNative *native = (_DinoPluginsRtpVoiceProcessorNative *) native_ptr;
|
||||||
|
delete native;
|
||||||
|
}
|