package im.actor.core.modules.calls.peers;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import im.actor.core.api.ApiICEServer;
import im.actor.core.modules.ModuleContext;
import im.actor.core.modules.calls.peers.messages.RTCAdvertised;
import im.actor.core.modules.calls.peers.messages.RTCAnswer;
import im.actor.core.modules.calls.peers.messages.RTCCandidate;
import im.actor.core.modules.calls.peers.messages.RTCCloseSession;
import im.actor.core.modules.calls.peers.messages.RTCMasterAdvertised;
import im.actor.core.modules.calls.peers.messages.RTCMediaStateUpdated;
import im.actor.core.modules.calls.peers.messages.RTCNeedOffer;
import im.actor.core.modules.calls.peers.messages.RTCOffer;
import im.actor.core.modules.calls.peers.messages.RTCStart;
import im.actor.core.modules.ModuleActor;
import im.actor.runtime.actors.ask.AskMessage;
import im.actor.runtime.actors.messages.Void;
import im.actor.runtime.function.CountedReference;
import im.actor.runtime.promise.Promise;
import im.actor.runtime.webrtc.WebRTCMediaStream;
import im.actor.runtime.webrtc.WebRTCMediaTrack;
/**
* Proxy Actor for simplifying state of PeerConnection by careful peer connection initialization
* and handling case when we want to establish connection before call answering
*/
public class PeerNodeActor extends ModuleActor implements PeerConnectionCallback {
//
// Node Configuration
//
/**
* Current Node's DeviceId
*/
private final long deviceId;
/**
* Callback for a Node events
*/
private final PeerNodeCallback callback;
//
// Connection Configuration
//
private final PeerSettings ownSettings;
private PeerSettings theirSettings;
private List<ApiICEServer> iceServers;
private CountedReference<WebRTCMediaStream> ownMediaStream;
//
// Signaling State
//
/**
* Current Session Id
*/
private long currentSession = 0;
/**
* All closed sessions. Used to filter out old signaling messages
*/
private final HashSet<Long> closedSessions = new HashSet<>();
/**
* All pending sessions
*/
private final ArrayList<PendingSession> pendingSessions = new ArrayList<>();
//
// Current peer connection
//
private int CHILD_NEXT_ID = 0;
private PeerConnectionInt peerConnection;
private WebRTCMediaStream theirStream;
//
// Node State Values
//
/**
* Mean that if it can produce media tracks
*/
private boolean isEnabled = false;
/**
* State if node is connected to other peer
*/
private boolean isConnected = false;
/**
* State if node is connected, enabled and notified about all streams
*/
private boolean isStarted = false;
/**
* External node state value
*/
private PeerState state = PeerState.PENDING;
/**
* Is Node's audio enabled
*/
private boolean isAudioEnabled = true;
/**
* Is Node's video enabled
*/
private boolean isVideoEnabled = true;
public PeerNodeActor(long deviceId,
PeerSettings ownSettings,
PeerNodeCallback callback,
ModuleContext context) {
super(context);
this.deviceId = deviceId;
this.ownSettings = ownSettings;
this.callback = callback;
}
//
// Starting up peer nodes
//
// Stages:
// 0. Notify about new peer node
// 1. Waiting for master advertise
// 2. Waiting for advertise of a node
// 3. If both peers supports pre connection, create new connection
// 4. Enabling Own and Their peers
// 5. Setting own media stream
// 6. Creation of peer connection
//
@Override
public void preStart() {
callback.onPeerStateChanged(deviceId, state);
}
public void onMasterAdvertised(List<ApiICEServer> iceServers) {
if (this.iceServers == null) {
this.iceServers = iceServers;
reconfigurePeerConnectionIfNeeded();
}
}
public void onAdvertised(PeerSettings settings) {
if (this.theirSettings == null) {
this.theirSettings = settings;
reconfigurePeerConnectionIfNeeded();
}
}
public void onEnabled() {
if (!isEnabled) {
isEnabled = true;
reconfigurePeerConnectionIfNeeded();
}
}
public Promise<Void> replaceOwnStream(CountedReference<WebRTCMediaStream> mediaStream) {
if (this.ownMediaStream == null) {
this.ownMediaStream = mediaStream;
reconfigurePeerConnectionIfNeeded();
} else {
this.ownMediaStream.release();
this.ownMediaStream = mediaStream;
if (peerConnection != null) {
return peerConnection.replaceStream(mediaStream);
}
}
return Promise.success(null);
}
private void reconfigurePeerConnectionIfNeeded() {
makePeerConnectionIfNeeded();
startIfNeeded();
}
private void makePeerConnectionIfNeeded() {
if (peerConnection != null || theirSettings == null || ownMediaStream == null || this.iceServers == null) {
return;
}
if (isEnabled || (theirSettings.isPreConnectionEnabled() && ownSettings.isPreConnectionEnabled())) {
state = PeerState.CONNECTING;
callback.onPeerStateChanged(deviceId, state);
peerConnection = new PeerConnectionInt(
iceServers, ownSettings, theirSettings,
ownMediaStream, this, context(), self(), "connection/" + (CHILD_NEXT_ID++));
unstashAll();
}
}
private void startIfNeeded() {
if (isEnabled && isConnected && !isStarted) {
isStarted = true;
state = PeerState.ACTIVE;
callback.onPeerStateChanged(deviceId, state);
if (theirStream != null) {
for (WebRTCMediaTrack track : theirStream.getAudioTracks()) {
track.setEnabled(isAudioEnabled);
if (isAudioEnabled) {
callback.onTrackAdded(deviceId, track);
}
}
for (WebRTCMediaTrack track : theirStream.getVideoTracks()) {
track.setEnabled(isVideoEnabled);
if (isVideoEnabled) {
callback.onTrackAdded(deviceId, track);
}
}
}
}
}
//
// Peer callbacks
//
@Override
public void onOffer(long sessionId, String sdp) {
callback.onOffer(deviceId, sessionId, sdp);
}
@Override
public void onAnswer(long sessionId, String sdp) {
callback.onAnswer(deviceId, sessionId, sdp);
}
@Override
public void onCandidate(long sessionId, int mdpIndex, String id, String sdp) {
callback.onCandidate(deviceId, sessionId, mdpIndex, id, sdp);
}
@Override
public void onNegotiationSuccessful(long sessionId) {
callback.onNegotiationSuccessful(deviceId, sessionId);
}
@Override
public void onNegotiationNeeded(long sessionId) {
callback.onNegotiationNeeded(deviceId, sessionId);
}
@Override
public void onStreamAdded(WebRTCMediaStream stream) {
WebRTCMediaStream oldStream = theirStream;
theirStream = stream;
//
// Enable Tracks if needed
//
if (isStarted) {
for (WebRTCMediaTrack track : stream.getAudioTracks()) {
track.setEnabled(isAudioEnabled);
if (isAudioEnabled) {
callback.onTrackAdded(deviceId, track);
}
}
for (WebRTCMediaTrack track : stream.getVideoTracks()) {
track.setEnabled(isVideoEnabled);
if (isVideoEnabled) {
callback.onTrackAdded(deviceId, track);
}
}
if (oldStream != null) {
for (WebRTCMediaTrack track : oldStream.getVideoTracks()) {
callback.onTrackRemoved(deviceId, track);
}
for (WebRTCMediaTrack track : oldStream.getAudioTracks()) {
callback.onTrackRemoved(deviceId, track);
}
}
}
if (!isConnected) {
isConnected = true;
if (!isEnabled) {
state = PeerState.CONNECTED;
callback.onPeerStateChanged(deviceId, state);
} else {
// This case is handled in startIfNeeded();
}
}
startIfNeeded();
}
@Override
public void onStreamRemoved(WebRTCMediaStream stream) {
if (isAudioEnabled || isVideoEnabled) {
return;
}
//
// Remove Tracks if needed
//
if (isStarted && theirStream != null) {
for (WebRTCMediaTrack track : stream.getAudioTracks()) {
callback.onTrackRemoved(deviceId, track);
}
for (WebRTCMediaTrack track : stream.getVideoTracks()) {
callback.onTrackRemoved(deviceId, track);
}
}
}
public void onStreamStateChanged(boolean isAudioEnabled, boolean isVideoEnabled) {
if (this.isAudioEnabled != isAudioEnabled) {
this.isAudioEnabled = isAudioEnabled;
if (isStarted) {
for (WebRTCMediaTrack track : theirStream.getAudioTracks()) {
track.setEnabled(isAudioEnabled);
if (isAudioEnabled) {
callback.onTrackAdded(deviceId, track);
} else {
callback.onTrackRemoved(deviceId, track);
}
}
}
}
if (this.isVideoEnabled != isVideoEnabled) {
this.isVideoEnabled = isVideoEnabled;
if (isStarted) {
for (WebRTCMediaTrack track : theirStream.getVideoTracks()) {
track.setEnabled(isVideoEnabled);
if (isVideoEnabled) {
callback.onTrackAdded(deviceId, track);
} else {
callback.onTrackRemoved(deviceId, track);
}
}
}
}
}
public void onCloseSession(long sessionId) {
if (!closedSessions.contains(sessionId)) {
closedSessions.add(sessionId);
currentSession = 0;
// Searching for pending sessions and closing it
for (PendingSession p : pendingSessions) {
if (p.getSessionId() == sessionId) {
pendingSessions.remove(p);
break;
}
}
// Killing Peer Connection
if (peerConnection != null) {
peerConnection.kill();
peerConnection = null;
}
// Creating new peer connection
peerConnection = new PeerConnectionInt(
iceServers, ownSettings, theirSettings,
ownMediaStream, this, context(), self(), "connection/" + (CHILD_NEXT_ID++));
// Pick first pending session if available
if (pendingSessions.size() > 0) {
PendingSession p = pendingSessions.remove(0);
if (p != null) {
for (Object o : p.getMessages()) {
self().sendFirst(o, self());
}
}
}
}
}
//
// Stopping
//
@Override
public void postStop() {
if (peerConnection != null) {
peerConnection.kill();
peerConnection = null;
}
if (ownMediaStream != null) {
ownMediaStream.release();
ownMediaStream = null;
}
state = PeerState.DISPOSED;
callback.onPeerStateChanged(deviceId, state);
}
//
// Messages
//
@Override
public void onReceive(Object message) {
if (message instanceof RTCStart) {
onEnabled();
} else if (message instanceof RTCAdvertised) {
RTCAdvertised advertised = (RTCAdvertised) message;
onAdvertised(advertised.getSettings());
} else if (message instanceof ReplaceOwnStream) {
ReplaceOwnStream ownStream = (ReplaceOwnStream) message;
replaceOwnStream(ownStream.getMediaStream());
} else if (message instanceof RTCMasterAdvertised) {
RTCMasterAdvertised advertisedMaster = (RTCMasterAdvertised) message;
onMasterAdvertised(advertisedMaster.getIceServers());
} else if (message instanceof RTCNeedOffer) {
RTCNeedOffer needOffer = (RTCNeedOffer) message;
if (peerConnection != null) {
if (receiveSessionMessage(needOffer.getSessionId(), message)) {
peerConnection.onOfferNeeded(needOffer.getSessionId());
}
} else {
stash();
}
} else if (message instanceof RTCOffer) {
RTCOffer offer = (RTCOffer) message;
if (peerConnection != null) {
if (receiveSessionMessage(offer.getSessionId(), message)) {
peerConnection.onOffer(offer.getSessionId(), offer.getSdp());
}
} else {
stash();
}
} else if (message instanceof RTCAnswer) {
RTCAnswer answer = (RTCAnswer) message;
if (peerConnection != null) {
if (receiveSessionMessage(answer.getSessionId(), message)) {
peerConnection.onAnswer(answer.getSessionId(), answer.getSdp());
}
} else {
stash();
}
} else if (message instanceof RTCCandidate) {
RTCCandidate candidate = (RTCCandidate) message;
if (peerConnection != null) {
peerConnection.onCandidate(candidate.getSessionId(), candidate.getMdpIndex(), candidate.getId(), candidate.getSdp());
} else {
stash();
}
} else if (message instanceof RTCCloseSession) {
RTCCloseSession closeSession = (RTCCloseSession) message;
if (peerConnection != null) {
onCloseSession(closeSession.getSessionId());
} else {
stash();
}
} else if (message instanceof RTCMediaStateUpdated) {
RTCMediaStateUpdated stateUpdated = (RTCMediaStateUpdated) message;
onStreamStateChanged(stateUpdated.isAudioEnabled(), stateUpdated.isVideoEnabled());
} else {
super.onReceive(message);
}
}
@Override
public Promise onAsk(Object message) throws Exception {
if (message instanceof ReplaceOwnStream) {
ReplaceOwnStream ownStream = (ReplaceOwnStream) message;
return replaceOwnStream(ownStream.getMediaStream());
} else {
return super.onAsk(message);
}
}
private boolean receiveSessionMessage(long sessionId, Object msg) {
if (currentSession == sessionId || currentSession == 0) {
currentSession = sessionId;
return true;
} else {
if (!closedSessions.contains(sessionId)) {
boolean found = false;
for (PendingSession p : pendingSessions) {
if (p.getSessionId() == sessionId) {
p.getMessages().add(msg);
found = true;
}
}
if (!found) {
PendingSession p = new PendingSession(sessionId);
p.getMessages().add(msg);
pendingSessions.add(p);
}
}
return false;
}
}
public static class ReplaceOwnStream implements AskMessage<Void> {
private CountedReference<WebRTCMediaStream> mediaStream;
public ReplaceOwnStream(CountedReference<WebRTCMediaStream> mediaStream) {
this.mediaStream = mediaStream;
}
public CountedReference<WebRTCMediaStream> getMediaStream() {
return mediaStream;
}
}
private class PendingSession {
private long sessionId;
private ArrayList<Object> messages;
public PendingSession(long sessionId) {
this.sessionId = sessionId;
this.messages = new ArrayList<>();
}
public long getSessionId() {
return sessionId;
}
public ArrayList<Object> getMessages() {
return messages;
}
}
}