handle senders modification via content-modify

Dino uses this to enable/disable video when a video content is already present
This commit is contained in:
Daniel Gultsch 2023-10-10 18:42:23 +02:00
parent 8cb802e7c1
commit dbf71e5d54
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
3 changed files with 117 additions and 42 deletions

View file

@ -251,7 +251,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
final Element error = response.addChild("error"); final Element error = response.addChild("error");
error.setAttribute("type", conditionType); error.setAttribute("type", conditionType);
error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas"); error.addChild(condition, "urn:ietf:params:xml:ns:xmpp-stanzas");
error.addChild(jingleCondition, Namespace.JINGLE_ERRORS); if (jingleCondition != null) {
error.addChild(jingleCondition, Namespace.JINGLE_ERRORS);
}
account.getXmppConnection().sendIqPacket(response, null); account.getXmppConnection().sendIqPacket(response, null);
} }

View file

@ -14,8 +14,11 @@ import com.google.common.base.Throwables;
import com.google.common.collect.Collections2; import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import com.google.common.primitives.Ints; import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.FutureCallback;
@ -551,15 +554,17 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
private void receiveContentModify(final JinglePacket jinglePacket) { private void receiveContentModify(final JinglePacket jinglePacket) {
// TODO check session accepted
final Map<String, Content.Senders> modification = final Map<String, Content.Senders> modification =
Maps.transformEntries( Maps.transformEntries(
jinglePacket.getJingleContents(), (key, value) -> value.getSenders()); jinglePacket.getJingleContents(), (key, value) -> value.getSenders());
respondOk(jinglePacket); final boolean isInitiator = isInitiator();
final RtpContentMap currentOutgoing = this.outgoingContentAdd; final RtpContentMap currentOutgoing = this.outgoingContentAdd;
final RtpContentMap remoteContentMap = this.getRemoteContentMap();
final Set<String> currentOutgoingMediaIds = currentOutgoing == null ? Collections.emptySet() : currentOutgoing.contents.keySet(); final Set<String> currentOutgoingMediaIds = currentOutgoing == null ? Collections.emptySet() : currentOutgoing.contents.keySet();
Log.d(Config.LOGTAG, "receiveContentModification(" + modification + ")"); Log.d(Config.LOGTAG, "receiveContentModification(" + modification + ")");
if (currentOutgoing != null && currentOutgoingMediaIds.containsAll(modification.keySet())) { if (currentOutgoing != null && currentOutgoingMediaIds.containsAll(modification.keySet())) {
final boolean isInitiator = isInitiator(); respondOk(jinglePacket);
final RtpContentMap modifiedContentMap; final RtpContentMap modifiedContentMap;
try { try {
modifiedContentMap = currentOutgoing.modifiedSendersChecked(isInitiator, modification); modifiedContentMap = currentOutgoing.modifiedSendersChecked(isInitiator, modification);
@ -570,18 +575,72 @@ public class JingleRtpConnection extends AbstractJingleConnection
} }
this.outgoingContentAdd = modifiedContentMap; this.outgoingContentAdd = modifiedContentMap;
Log.d(Config.LOGTAG, id.account.getJid().asBareJid()+": processed content-modification for pending content-add"); Log.d(Config.LOGTAG, id.account.getJid().asBareJid()+": processed content-modification for pending content-add");
} else if (remoteContentMap != null && remoteContentMap.contents.keySet().containsAll(modification.keySet())) {
respondOk(jinglePacket);
final RtpContentMap modifiedRemoteContentMap;
try {
modifiedRemoteContentMap = remoteContentMap.modifiedSendersChecked(isInitiator, modification);
} catch (final IllegalArgumentException e) {
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage());
return;
}
final SessionDescription offer;
try {
offer = SessionDescription.of(modifiedRemoteContentMap, !isInitiator());
} catch (final IllegalArgumentException | NullPointerException e) {
Log.d(Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": unable convert offer from content-modify to SDP", e);
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage());
return;
}
Log.d(Config.LOGTAG, id.account.getJid().asBareJid()+": auto accepting content-modification");
this.autoAcceptContentModify(modifiedRemoteContentMap, offer);
} else { } else {
webRTCWrapper.close(); Log.d(Config.LOGTAG,"received unsupported content modification "+modification);
sendSessionTerminate( respondWithItemNotFound(jinglePacket);
Reason.FAILED_APPLICATION,
String.format(
"%s only supports %s as a means to modify a not yet accepted %s",
BuildConfig.APP_NAME,
JinglePacket.Action.CONTENT_MODIFY,
JinglePacket.Action.CONTENT_ADD));
} }
} }
private void autoAcceptContentModify(final RtpContentMap modifiedRemoteContentMap, final SessionDescription offer) {
this.setRemoteContentMap(modifiedRemoteContentMap);
final org.webrtc.SessionDescription sdp =
new org.webrtc.SessionDescription(
org.webrtc.SessionDescription.Type.OFFER, offer.toString());
try {
this.webRTCWrapper.setRemoteDescription(sdp).get();
// auto accept is only done when we already have tracks
final SessionDescription answer = setLocalSessionDescription();
final RtpContentMap rtpContentMap = RtpContentMap.of(answer, isInitiator());
modifyLocalContentMap(rtpContentMap);
// we do not need to send an answer but do we have to resend the candidates currently in SDP?
//resendCandidatesFromSdp(answer);
webRTCWrapper.setIsReadyToReceiveIceCandidates(true);
} catch (final Exception e) {
Log.d(Config.LOGTAG, "unable to accept content add", Throwables.getRootCause(e));
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION);
}
}
private void resendCandidatesFromSdp(final SessionDescription answer) {
final ImmutableMultimap.Builder<String, IceUdpTransportInfo.Candidate> candidateBuilder = new ImmutableMultimap.Builder<>();
for(final SessionDescription.Media media : answer.media) {
final String mid = Iterables.getFirst(media.attributes.get("mid"), null);
if (Strings.isNullOrEmpty(mid)) {
continue;
}
for(final String sdpCandidate : media.attributes.get("candidate")) {
final IceUdpTransportInfo.Candidate candidate = IceUdpTransportInfo.Candidate.fromSdpAttributeValue(sdpCandidate, null);
if (candidate != null) {
candidateBuilder.put(mid,candidate);
}
}
}
final ImmutableMultimap<String, IceUdpTransportInfo.Candidate> candidates = candidateBuilder.build();
sendTransportInfo(candidates);
}
private void receiveContentReject(final JinglePacket jinglePacket) { private void receiveContentReject(final JinglePacket jinglePacket) {
final RtpContentMap receivedContentReject; final RtpContentMap receivedContentReject;
try { try {
@ -1942,6 +2001,11 @@ public class JingleRtpConnection extends AbstractJingleConnection
send(jinglePacket); send(jinglePacket);
} }
private void sendTransportInfo(final Multimap<String, IceUdpTransportInfo.Candidate> candidates) {
// TODO send all candidates in one transport-info
}
private void send(final JinglePacket jinglePacket) { private void send(final JinglePacket jinglePacket) {
jinglePacket.setTo(id.with); jinglePacket.setTo(id.with);
xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse); xmppConnectionService.sendIqPacket(id.account, jinglePacket, this::handleIqResponse);
@ -2028,6 +2092,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait"); respondWithJingleError(jinglePacket, "out-of-order", "unexpected-request", "wait");
} }
private void respondWithItemNotFound(final JinglePacket jinglePacket) {
respondWithJingleError(jinglePacket, null, "item-not-found", "cancel");
}
void respondWithJingleError( void respondWithJingleError(
final IqPacket original, final IqPacket original,
String jingleCondition, String jingleCondition,

View file

@ -202,41 +202,46 @@ public class IceUdpTransportInfo extends GenericTransportInfo {
public static Candidate fromSdpAttribute(final String attribute, String currentUfrag) { public static Candidate fromSdpAttribute(final String attribute, String currentUfrag) {
final String[] pair = attribute.split(":", 2); final String[] pair = attribute.split(":", 2);
if (pair.length == 2 && "candidate".equals(pair[0])) { if (pair.length == 2 && "candidate".equals(pair[0])) {
final String[] segments = pair[1].split(" "); return fromSdpAttributeValue(pair[1], currentUfrag);
if (segments.length >= 6) {
final String id = UUID.randomUUID().toString();
final String foundation = segments[0];
final String component = segments[1];
final String transport = segments[2].toLowerCase(Locale.ROOT);
final String priority = segments[3];
final String connectionAddress = segments[4];
final String port = segments[5];
final HashMap<String, String> additional = new HashMap<>();
for (int i = 6; i < segments.length - 1; i = i + 2) {
additional.put(segments[i], segments[i + 1]);
}
final String ufrag = additional.get("ufrag");
if (ufrag != null && !ufrag.equals(currentUfrag)) {
return null;
}
final Candidate candidate = new Candidate();
candidate.setAttribute("component", component);
candidate.setAttribute("foundation", foundation);
candidate.setAttribute("generation", additional.get("generation"));
candidate.setAttribute("rel-addr", additional.get("raddr"));
candidate.setAttribute("rel-port", additional.get("rport"));
candidate.setAttribute("id", id);
candidate.setAttribute("ip", connectionAddress);
candidate.setAttribute("port", port);
candidate.setAttribute("priority", priority);
candidate.setAttribute("protocol", transport);
candidate.setAttribute("type", additional.get("typ"));
return candidate;
}
} }
return null; return null;
} }
public static Candidate fromSdpAttributeValue(final String value, final String currentUfrag) {
final String[] segments = value.split(" ");
if (segments.length < 6) {
return null;
}
final String id = UUID.randomUUID().toString();
final String foundation = segments[0];
final String component = segments[1];
final String transport = segments[2].toLowerCase(Locale.ROOT);
final String priority = segments[3];
final String connectionAddress = segments[4];
final String port = segments[5];
final HashMap<String, String> additional = new HashMap<>();
for (int i = 6; i < segments.length - 1; i = i + 2) {
additional.put(segments[i], segments[i + 1]);
}
final String ufrag = additional.get("ufrag");
if (currentUfrag != null && ufrag != null && !ufrag.equals(currentUfrag)) {
return null;
}
final Candidate candidate = new Candidate();
candidate.setAttribute("component", component);
candidate.setAttribute("foundation", foundation);
candidate.setAttribute("generation", additional.get("generation"));
candidate.setAttribute("rel-addr", additional.get("raddr"));
candidate.setAttribute("rel-port", additional.get("rport"));
candidate.setAttribute("id", id);
candidate.setAttribute("ip", connectionAddress);
candidate.setAttribute("port", port);
candidate.setAttribute("priority", priority);
candidate.setAttribute("protocol", transport);
candidate.setAttribute("type", additional.get("typ"));
return candidate;
}
public int getComponent() { public int getComponent() {
return getAttributeAsInt("component"); return getAttributeAsInt("component");
} }