ZaStoGram/TMessagesProj/jni/voip/tgcalls/v2wasm/ReferenceCallCore.cpp
2026-09-24 16:17:30 +04:00

663 lines
26 KiB
C++

#include "v2wasm/ReferenceCallCore.h"
#include "v2wasm/CallCoreABI.h"
#include "v2wasm/CoreBase64.h"
#include <algorithm>
#include <memory>
namespace tgcalls {
namespace v2wasm {
namespace {
constexpr int kAbiVersion = 1;
constexpr int kStatsTimerToken = 1;
constexpr int kConnectionTimerToken = 2;
constexpr int kDisconnectTimerToken = 3;
constexpr int kIceRestartMinIntervalMs = 5000;
constexpr int kConnectionFailureTimeoutMs = 20000;
constexpr int kAudioMaxBitrateBps = 32 * 1024; // stock: 32 * 1024
constexpr int kVideoMaxBitrateBps = 1200 * 1024; // stock: 1200 * 1024
std::string stringField(json11::Json const &object, std::string const &key) {
const auto &value = object[key];
return value.is_string() ? value.string_value() : std::string();
}
} // namespace
ReferenceCallCore::ReferenceCallCore(json11::Json const &config, std::function<void(json11::Json::object &&)> emit) :
_emit(std::move(emit)) {
_isOutgoing = config["isOutgoing"].bool_value();
_enableP2P = config["enableP2P"].bool_value();
for (const auto &server : config["rtcServers"].array_items()) {
_rtcServers.push_back(server);
}
SignalingFraming::Delegate framingDelegate;
framingDelegate.sendPacket = [this](std::vector<uint8_t> &&packet) {
this->emit({ {"@type", "signaling_send_packet"}, {"packetB64", base64Encode(packet)} });
};
framingDelegate.deliverMessage = [this](std::string &&message) {
// Load-bearing log line (P1/P2 wire diffs); stock counterpart logs
// outbound only, this one aids debugging.
emitLog("signaling in: " + message);
handleSignalingData(message);
};
framingDelegate.log = [this](std::string const &line) {
emitLog(line);
};
framingDelegate.nowMs = [this]() {
return _nowMs;
};
_framing = std::make_unique<SignalingFraming>(std::move(framingDelegate));
this->emit({ {"@type", "core_ready"}, {"abiVersion", kAbiVersion} });
// Policy: map RtcServers to ICE servers (stock start() lines 641–685).
json11::Json::array iceServers;
for (const auto &server : _rtcServers) {
if (server["isTcp"].bool_value()) {
continue;
}
const auto rawHost = stringField(server, "host");
if (rawHost.empty()) {
continue;
}
// Stock validates via SocketAddress::IsComplete() and brackets IPv6
// literals via HostAsURIString(); SocketAddress cannot cross the
// boundary, so replicate the URI form here.
const auto host = (rawHost.find(':') != std::string::npos)
? "[" + rawHost + "]"
: rawHost;
const auto port = std::to_string((int)server["port"].number_value());
if (server["isTurn"].bool_value()) {
iceServers.push_back(json11::Json::object{
{"urls", json11::Json::array{ "turn:" + host + ":" + port }},
{"username", stringField(server, "login")},
{"password", stringField(server, "password")},
});
} else {
iceServers.push_back(json11::Json::object{
{"urls", json11::Json::array{ "stun:" + host + ":" + port }},
{"username", ""},
{"password", ""},
});
}
}
this->emit({
{"@type", "pc_create"},
{"iceTransportsType", _enableP2P ? "all" : "relay"},
{"iceServers", std::move(iceServers)},
});
if (_isOutgoing) {
this->emit({ {"@type", "pc_create_data_channel"} });
}
this->emit({
{"@type", "pc_add_transceiver"},
{"id", "audio0"},
{"kind", "audio"},
{"direction", "sendrecv"},
{"trackSource", "microphone"},
{"sendEncodings", json11::Json::array{ json11::Json::object{ {"maxBitrateBps", kAudioMaxBitrateBps} } }},
});
// stock beginSignaling()
_didBeginNegotiation = true;
if (_isOutgoing) {
requestSetLocalDescription();
}
// stock beginLogTimer(0)
this->emit({ {"@type", "set_timer"}, {"token", kStatsTimerToken}, {"delayMs", 0} });
_connectionTimerGeneration += 1;
this->emit({ {"@type", "set_timer"}, {"token", kConnectionTimerToken}, {"generation", _connectionTimerGeneration}, {"delayMs", 1000} });
}
void ReferenceCallCore::emit(json11::Json::object &&command) {
_emit(std::move(command));
}
void ReferenceCallCore::emitLog(std::string const &message) {
emit({ {"@type", "log"}, {"message", message} });
}
void ReferenceCallCore::onEvent(json11::Json const &event) {
if (event["nowMs"].is_number()) {
_nowMs = (int64_t)event["nowMs"].number_value();
}
// Emit a baseline state record as soon as we have a real clock, so a call
// that never transitions still uploads a non-empty timeline. _nowMs is zero
// until the first event carries one, which is why this cannot live in the
// core-init path.
if (!_didEmitBaselineRecord && _nowMs != 0) {
_didEmitBaselineRecord = true;
updateNetworkState(_isConnected, _isFailed);
}
const auto type = stringField(event, "@type");
if (type == "signaling_packet") {
const auto packet = base64Decode(stringField(event, "packetB64"));
if (packet) {
_framing->receivePacket(*packet);
}
} else if (type == "pc_renegotiation_needed") {
// stock onRenegotiationNeeded delegate
if (_isMakingOffer) {
// An offer is already in flight and already covers whatever triggered
// this event - the data channel created moments ago during setup. Stock
// suppresses this via the is_negotiation_needed_ latch; the host
// forwards the legacy OnRenegotiationNeeded, which bypasses that, so we
// suppress it here. Parity with InstanceV2ReferenceImpl.
emitLog("onRenegotiationNeeded: offer already in flight, skipping");
} else if (_didBeginNegotiation) {
if (_isOutgoing || _haveRemoteDescription) {
requestSetLocalDescription();
}
} else {
emitLog("onRenegotiationNeeded: not sending local description");
}
} else if (type == "pc_ice_candidate") {
// stock sendIceCandidate: exact wire keys @type/sdp/mid/mline
json11::Json::object candidate{
{"@type", "candidate"},
{"sdp", stringField(event, "sdp")},
{"mid", stringField(event, "mid")},
{"mline", (int)event["mline"].number_value()},
};
sendSignalingMessage(std::move(candidate));
} else if (type == "pc_ice_state") {
onIceState(stringField(event, "state"));
} else if (type == "pc_signaling_state") {
_signalingState = stringField(event, "state");
} else if (type == "pc_description_created") {
if (event["ok"].bool_value()) {
const auto descType = stringField(event, "type");
const auto sdp = mungeLocalDescription(descType, stringField(event, "sdp"));
emit({ {"@type", "pc_set_local_description"}, {"type", descType}, {"sdp", sdp} });
} else {
_isMakingOffer = false;
emitLog("CreateOffer/CreateAnswer failed: " + stringField(event, "error"));
}
} else if (type == "pc_candidate_pair_changed") {
json11::Json::object connection{
{"local", event["local"]},
{"remote", event["remote"]},
};
json11::Json connectionJson(std::move(connection));
if (_currentConnection != connectionJson) {
_currentConnection = std::move(connectionJson);
updateNetworkState(_isConnected, _isFailed);
}
} else if (type == "pc_set_local_done") {
_isMakingOffer = false;
// Deviation from stock: stock sends the local description without
// checking the SLD error (InstanceV2ReferenceImpl.cpp:888-901); we
// skip the send on failure. Same below for pc_set_remote_done: stock
// flushes candidates/answers offers even on SRD failure.
if (event["ok"].bool_value()) {
_haveLocalDescription = true;
// stock doSendLocalDescription: exact wire keys @type/sdp
json11::Json::object description{
{"@type", stringField(event, "type")},
{"sdp", stringField(event, "sdp")},
};
sendSignalingMessage(std::move(description));
} else {
emitLog("SetLocalDescription failed");
}
flushPendingRemoteCandidates();
} else if (type == "pc_set_remote_done") {
_isSettingRemoteAnswerPending = false;
if (event["ok"].bool_value()) {
_haveRemoteDescription = true;
flushPendingRemoteCandidates();
if (stringField(event, "sdpType") == "offer") {
requestSetLocalDescription();
}
} else {
emitLog("SetRemoteDescription failed");
}
} else if (type == "dc_state") {
if (stringField(event, "label") == "data") {
const bool open = event["open"].bool_value();
if (open && !_isDataChannelOpen) {
_isDataChannelOpen = true;
sendMediaState();
} else if (!open) {
_isDataChannelOpen = false;
}
}
onDataChannelEvent(event);
} else if (type == "dc_message") {
if (stringField(event, "label") == "data") {
// stock feeds non-binary data-channel messages into processSignalingData
handleSignalingData(stringField(event, "data"));
}
onDataChannelEvent(event);
} else if (type == "dc_buffered" || type == "dc_channel") {
onDataChannelEvent(event);
} else if (type == "timer") {
const int token = (int)event["token"].number_value();
if (token == kStatsTimerToken) {
emit({ {"@type", "pc_get_stats"} });
emit({ {"@type", "set_timer"}, {"token", kStatsTimerToken}, {"delayMs", 1000} });
} else if (token == kConnectionTimerToken) {
// set_timer does NOT cancel a prior timer of the same token, so every
// re-arm would otherwise stack another live timer. Ignore stale ones.
if ((int)event["generation"].number_value() != _connectionTimerGeneration) {
return;
}
if (_isConnected) {
_lastDisconnectedTimestampMs = _nowMs;
} else {
// Seeded from an event-supplied clock, never the constructor: _nowMs
// is 0 until the first event carries one, and a zero seed would make
// this fire on every call after ~1s.
if (_lastDisconnectedTimestampMs == 0) {
_lastDisconnectedTimestampMs = _nowMs;
} else if (_nowMs - _lastDisconnectedTimestampMs > kConnectionFailureTimeoutMs) {
updateNetworkState(false, true);
return;
}
}
_connectionTimerGeneration += 1;
emit({ {"@type", "set_timer"}, {"token", kConnectionTimerToken}, {"generation", _connectionTimerGeneration}, {"delayMs", 1000} });
} else if (token == kDisconnectTimerToken) {
// Debounce: only report a disconnect (and re-check the failed state)
// once it has persisted for 2s, matching
// InstanceV2ReferenceImpl::updateIsConnected. Not re-armed - a fresh
// disconnect schedules its own timer via updateIsConnected.
if ((int)event["generation"].number_value() != _disconnectReportGeneration) {
return;
}
if (!_isConnected) {
updateNetworkState(_isConnected, _isFailed);
}
}
} else if (type == "stats") {
onStats(event);
} else if (type == "mute") {
const bool muted = event["muted"].bool_value();
if (_isMicrophoneMuted != muted) {
_isMicrophoneMuted = muted;
emit({ {"@type", "pc_set_track_enabled"}, {"id", "audio0"}, {"enabled", !muted} });
sendMediaState();
}
} else if (type == "battery_low") {
const bool low = event["low"].bool_value();
if (_isBatteryLow != low) {
_isBatteryLow = low;
sendMediaState();
}
} else if (type == "video_capture") {
// stock setVideoCapture: always remove, re-add for non-screencast capture
if (_hasVideoTrack) {
emit({ {"@type", "pc_remove_track"}, {"id", "video0"} });
_hasVideoTrack = false;
}
_hasVideoCapture = event["active"].bool_value() && !event["screencast"].bool_value();
if (_hasVideoCapture) {
emit({
{"@type", "pc_add_transceiver"},
{"id", "video0"},
{"kind", "video"},
{"direction", "sendrecv"},
{"trackSource", "camera"},
{"codecPreferences", json11::Json::array{ "H265", "H264" }},
{"sendEncodings", json11::Json::array{ json11::Json::object{ {"maxBitrateBps", kVideoMaxBitrateBps} } }},
});
_hasVideoTrack = true;
}
if (_didBeginNegotiation) {
sendMediaState();
requestSetLocalDescription();
}
} else if (type == "pc_track") {
if (stringField(event, "kind") == "video") {
emit({ {"@type", "pc_set_incoming_sink"}, {"mid", stringField(event, "mid")} });
}
} else if (type == "stop") {
handleStop();
} else if (type == "error") {
emitLog("host error: " + stringField(event, "message") + " (command: " + stringField(event, "command") + ")");
if (stringField(event, "command") == "pc_create") {
updateNetworkState(false, true);
}
} else {
// Unknown event: ignore (ABI rule).
}
}
void ReferenceCallCore::requestSetLocalDescription() {
// Stock sendLocalDescription() sets _isMakingOffer before its no-arg SLD
// (InstanceV2ReferenceImpl.cpp:886); the flag window now also covers the
// create step, which is strictly safer for collision detection.
_isMakingOffer = true;
// Stock's no-arg SLD creates an answer in have-remote-offer and an offer
// otherwise. _signalingState is current here: OnSignalingChange is posted
// to the media thread during SRD execution, before the SRD observer's
// posted completion, so pc_signaling_state always precedes
// pc_set_remote_done (FIFO).
const bool asAnswer = (_signalingState == "have-remote-offer" || _signalingState == "have-remote-pranswer");
emit({ {"@type", asAnswer ? "pc_create_answer" : "pc_create_offer"} });
}
void ReferenceCallCore::sendSignalingMessage(json11::Json::object &&message) {
mungeOutgoingSignalingMessage(message);
const std::string data = json11::Json(std::move(message)).dump();
// Load-bearing log line: the P1/P2 wire diffs key on it (the stock
// counterpart is InstanceV2ReferenceImpl's "sendSignalingMessage: ").
emitLog("signaling out: " + data);
_framing->sendMessage(data);
}
void ReferenceCallCore::mungeOutgoingSignalingMessage(json11::Json::object &message) {
(void)message;
}
void ReferenceCallCore::onDataChannelEvent(json11::Json const &event) {
(void)event;
}
std::string ReferenceCallCore::mungeLocalDescription(std::string const &type, std::string const &sdp) {
(void)type;
return sdp;
}
void ReferenceCallCore::onIceState(std::string const &state) {
bool isConnected = (state == "connected" || state == "completed");
// ICE 'failed' is NOT terminal. Stock InstanceV2ReferenceImpl never sets its
// failed flag from the ICE state - the 20s watchdog is its only writer - and
// instead attempts a restart. 18/19 treating it as terminal is exactly the
// state-semantics divergence that contaminates the substrate A/B.
if (state == "failed") {
maybeRestartIce();
}
updateIsConnected(isConnected);
}
void ReferenceCallCore::maybeRestartIce() {
if (_isFailed) {
return;
}
// Only the caller restarts, matching InstanceV2ReferenceImpl - if both sides
// restart on the same failure they glare.
if (!_isOutgoing) {
return;
}
if (_lastIceRestartTimestampMs != 0 && _nowMs - _lastIceRestartTimestampMs < kIceRestartMinIntervalMs) {
return;
}
_lastIceRestartTimestampMs = _nowMs;
emitLog("ICE failed; requesting restart");
emit({ {"@type", "pc_restart_ice"} });
}
void ReferenceCallCore::updateIsConnected(bool isConnected) {
if (_isConnected == isConnected) {
return;
}
_isConnected = isConnected;
if (isConnected) {
updateNetworkState(_isConnected, _isFailed);
} else {
_lastDisconnectedTimestampMs = _nowMs;
// The legacy ICE state reports a disconnect on a brief receiving timeout,
// so a blip would otherwise surface as a Reconnecting episode in the
// uploaded timeline. Only report (and log) the disconnect once it
// persists, matching InstanceV2ReferenceImpl::updateIsConnected.
_disconnectReportGeneration += 1;
emit({ {"@type", "set_timer"}, {"token", kDisconnectTimerToken}, {"generation", _disconnectReportGeneration}, {"delayMs", 2000} });
}
}
void ReferenceCallCore::onStats(json11::Json const &event) {
// stock writeStateLogRecords signal-bars heuristic
const double sendBitrateKbps = event["sendBitrateKbps"].number_value();
double bitrateNorm = _hasVideoTrack ? 600.0 : 16.0;
double adjustedQuality = sendBitrateKbps / bitrateNorm;
adjustedQuality = std::max(0.0, std::min(1.0, adjustedQuality));
emit({ {"@type", "emit_signal_bars"}, {"bars", (int)(adjustedQuality * 4.0)} });
BitrateRecord record;
record.timestampMs = _nowMs;
record.bitrateKbps = (int32_t)sendBitrateKbps;
_bitrateRecords.push_back(record);
}
void ReferenceCallCore::handleSignalingData(std::string const &data) {
std::string parsingError;
const auto json = json11::Json::parse(data, parsingError);
if (!json.is_object()) {
emitLog("Signaling: message must be an object");
return;
}
const auto type = stringField(json, "@type");
if (type.empty()) {
emitLog("Signaling: @type is missing");
return;
}
if (type == "offer" || type == "answer") {
const auto sdp = stringField(json, "sdp");
if (sdp.empty()) {
emitLog("Signaling: sdp is missing");
return;
}
handleRemoteSdp(type, sdp);
} else if (type == "candidate") {
if (!json["mid"].is_string() || !json["mline"].is_number() || !json["sdp"].is_string()) {
return;
}
json11::Json::object candidate{
{"@type", "pc_add_ice_candidate"},
{"mid", json["mid"]},
{"mline", json["mline"]},
{"sdp", json["sdp"]},
};
if (_haveLocalDescription && _haveRemoteDescription) {
emit(std::move(candidate));
} else {
_pendingRemoteCandidates.push_back(json11::Json(std::move(candidate)));
}
} else if (type == "MediaState") {
handleMediaStateMessage(json);
} else {
// Other signaling::Message kinds are not used by the reference protocol.
}
}
void ReferenceCallCore::handleRemoteSdp(std::string const &type, std::string const &sdp) {
// stock handleRemoteSdp perfect-negotiation gate, verbatim semantics
bool isReadyForOffer = !_isMakingOffer && (_signalingState == "stable" || _isSettingRemoteAnswerPending);
bool isOfferCollision = (type == "offer") && !isReadyForOffer;
bool ignoreOffer = !_isOutgoing && isOfferCollision;
if (ignoreOffer) {
emitLog("Ignoring remote sdp");
return;
}
_isSettingRemoteAnswerPending = (type == "answer");
emit({ {"@type", "pc_set_remote_description"}, {"sdpType", type}, {"sdp", sdp} });
}
void ReferenceCallCore::handleMediaStateMessage(json11::Json const &message) {
// wire keys from stock MediaStateMessage_serialize:
// muted / lowBattery / videoState / screencastState (values inactive|suspended|active)
const auto audio = message["muted"].bool_value() ? "muted" : "active";
const auto mapVideo = [](std::string const &value) -> std::string {
if (value == "suspended") {
return "paused";
} else if (value == "active") {
return "active";
}
return "inactive";
};
const auto videoState = mapVideo(stringField(message, "videoState"));
const auto screencastState = mapVideo(stringField(message, "screencastState"));
// stock: screencast overrides video when active or paused
const auto effectiveVideo = (screencastState == "active" || screencastState == "paused") ? screencastState : videoState;
emit({ {"@type", "emit_remote_media_state"}, {"audio", audio}, {"video", effectiveVideo} });
emit({ {"@type", "emit_remote_battery_low"}, {"low", message["lowBattery"].bool_value()} });
}
void ReferenceCallCore::flushPendingRemoteCandidates() {
if (_pendingRemoteCandidates.empty()) {
return;
}
if (!_haveLocalDescription || !_haveRemoteDescription) {
return;
}
for (auto &candidate : _pendingRemoteCandidates) {
json11::Json::object command = candidate.object_items();
emit(std::move(command));
}
_pendingRemoteCandidates.clear();
}
void ReferenceCallCore::sendMediaState() {
if (!_isDataChannelOpen) {
return;
}
// wire format from stock MediaStateMessage_serialize (Signaling.cpp):
// keys @type/muted/lowBattery/videoState/videoRotation/screencastState
json11::Json::object message{
{"@type", "MediaState"},
{"muted", _isMicrophoneMuted},
{"lowBattery", _isBatteryLow},
{"videoState", (_hasVideoTrack && _hasVideoCapture) ? "active" : "inactive"},
{"videoRotation", 0},
{"screencastState", "inactive"},
};
emit({ {"@type", "dc_send"}, {"data", json11::Json(std::move(message)).dump()} });
}
void ReferenceCallCore::updateNetworkState(bool isConnected, bool isFailed) {
_isConnected = isConnected;
_isFailed = isFailed;
NetworkStateRecord record;
record.timestampMs = _nowMs;
record.isConnected = _isConnected;
record.isFailed = _isFailed;
record.connection = _currentConnection;
if (_networkStateRecords.empty()
|| _networkStateRecords.back().isConnected != record.isConnected
|| _networkStateRecords.back().isFailed != record.isFailed
|| !(_networkStateRecords.back().connection == record.connection)) {
_networkStateRecords.push_back(record);
}
emitMappedState();
}
void ReferenceCallCore::emitMappedState() {
std::string mappedState;
if (_isFailed) {
mappedState = "failed";
} else if (_isConnected) {
mappedState = "established";
} else {
mappedState = "reconnecting";
}
emit({ {"@type", "emit_state"}, {"state", mappedState} });
}
void ReferenceCallCore::handleStop() {
// stock stop(): coalesce events within 5ms, then serialize "v":3 stats log
for (int i = (int)_networkStateRecords.size() - 1; i >= 1; i--) {
if (_networkStateRecords[i].timestampMs - _networkStateRecords[i - 1].timestampMs < 5) {
_networkStateRecords.erase(_networkStateRecords.begin() + i - 1);
}
}
json11::Json::array networkRecords;
int64_t baseTimestamp = 0;
for (const auto &record : _networkStateRecords) {
json11::Json::object jsonRecord;
if (baseTimestamp == 0) {
baseTimestamp = record.timestampMs;
}
jsonRecord.insert(std::make_pair("t", json11::Json(std::to_string(record.timestampMs - baseTimestamp))));
jsonRecord.insert(std::make_pair("c", json11::Json(record.isConnected ? 1 : 0)));
if (record.connection.is_object()) {
jsonRecord.insert(std::make_pair("network", record.connection));
}
if (record.isFailed) {
jsonRecord.insert(std::make_pair("failed", json11::Json(1)));
}
networkRecords.push_back(json11::Json(std::move(jsonRecord)));
}
json11::Json::array bitrateRecords;
for (const auto &record : _bitrateRecords) {
bitrateRecords.push_back(json11::Json(json11::Json::object{ {"b", record.bitrateKbps} }));
}
json11::Json::object statsLog{
{"v", 3},
{"network", std::move(networkRecords)},
{"bitrate", std::move(bitrateRecords)},
};
emit({ {"@type", "stats_log"}, {"json", json11::Json(std::move(statsLog)).dump()} });
emit({ {"@type", "close"} });
}
} // namespace v2wasm
} // namespace tgcalls
// ============================================================================
// C ABI wrappers
// ============================================================================
struct TgcallsCallCore {
std::unique_ptr<tgcalls::v2wasm::ReferenceCallCore> impl;
};
extern "C" {
TgcallsCallCore *tgcalls_core_create(const char *configJson, TgcallsCoreEmitFn emitFn, void *userData) {
if (!emitFn) {
return nullptr;
}
std::string parsingError;
const auto config = json11::Json::parse(configJson ? configJson : "", parsingError);
auto core = new TgcallsCallCore();
core->impl = std::make_unique<tgcalls::v2wasm::ReferenceCallCore>(config, [emitFn, userData](json11::Json::object &&command) {
const std::string serialized = json11::Json(std::move(command)).dump();
emitFn(userData, (const uint8_t *)serialized.data(), serialized.size());
});
return core;
}
void tgcalls_core_on_event(TgcallsCallCore *core, const uint8_t *data, size_t len) {
if (!core || !core->impl || !data) {
return;
}
std::string parsingError;
const auto event = json11::Json::parse(std::string((const char *)data, len), parsingError);
if (!event.is_object()) {
return;
}
core->impl->onEvent(event);
}
void tgcalls_core_destroy(TgcallsCallCore *core) {
delete core;
}
} // extern "C"