feat(app): 1:1 voice and video calls
End-to-end encrypted calls between two contacts over WebRTC. Call signalling (offer, answer, hang-up) travels as ordinary Signal-encrypted messages through the relay; media is DTLS-SRTP, peer to peer when possible and through the operator's TURN server otherwise. - Non-trickle ICE (candidates gathered for up to 3 s), 45 s ring timeout, busy handling, and a call log entry in the conversation. - The system call UI rings even when the app is closed: CallKit from a PushKit VoIP push on iOS, a full-screen incoming call from an FCM wake-up on Android. The wake-up carries no caller; the app learns who is calling from the queued offer once it is unlocked. - New permissions: camera for video, audio routing and Bluetooth headsets, network state; iOS background modes audio and voip. Push on Android: a fresh install now picks UnifiedPush when a distributor is installed and FCM otherwise, instead of leaving wake-ups off. Without a wake-up Android puts the app to sleep and messages wait until it is opened. Users can switch at any time in Settings.
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
/// Záznam hovoru v chatu („Hovor · 3:12“, „Zmeškaný hovor“). Jen lokální:
|
||||
/// každá strana si ho zapíše sama podle toho, jak hovor u ní skončil.
|
||||
enum CallOutcome {
|
||||
/// Hovor proběhl (spojil se), [CallLog.durationSeconds] je jeho délka.
|
||||
completed,
|
||||
|
||||
/// Příchozí, nikdo ho nevzal (nebo nabídka dorazila pozdě).
|
||||
missed,
|
||||
|
||||
/// Odchozí, protistrana ho nevzala do vypršení zvonění.
|
||||
unanswered,
|
||||
|
||||
/// Odmítnutý (mnou u příchozího, protistranou u odchozího).
|
||||
rejected,
|
||||
|
||||
/// Protistrana měla jiný hovor.
|
||||
busy,
|
||||
|
||||
/// Spojení se nepodařilo navázat (síť, firewall bez TURN).
|
||||
failed,
|
||||
}
|
||||
|
||||
class CallLog {
|
||||
const CallLog({
|
||||
required this.outcome,
|
||||
required this.outgoing,
|
||||
required this.video,
|
||||
this.durationSeconds = 0,
|
||||
});
|
||||
|
||||
final CallOutcome outcome;
|
||||
final bool outgoing;
|
||||
final bool video;
|
||||
final int durationSeconds;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'o': outcome.name,
|
||||
'out': outgoing,
|
||||
'vid': video,
|
||||
if (durationSeconds > 0) 'dur': durationSeconds,
|
||||
};
|
||||
|
||||
/// Neznámý výsledek z novější verze appky = null (řádek se nezobrazí,
|
||||
/// ale zpráva se načte).
|
||||
static CallLog? fromJson(Object? json) {
|
||||
if (json is! Map) return null;
|
||||
final outcome =
|
||||
CallOutcome.values.where((o) => o.name == json['o']).firstOrNull;
|
||||
if (outcome == null) return null;
|
||||
return CallLog(
|
||||
outcome: outcome,
|
||||
outgoing: json['out'] == true,
|
||||
video: json['vid'] == true,
|
||||
durationSeconds: (json['dur'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user