/* * Copyright (C) 2018 Intel Corporation * SPDX-License-Identifier: Apache-2.0 */ package owt.p2p; import static org.webrtc.DataChannel.State.OPEN; import static org.webrtc.PeerConnection.IceConnectionState.COMPLETED; import static org.webrtc.PeerConnection.IceConnectionState.CONNECTED; import static org.webrtc.PeerConnection.SignalingState.STABLE; import static owt.base.CheckCondition.DCHECK; import static owt.base.CheckCondition.RCHECK; import static owt.base.Const.LOG_TAG; import static owt.p2p.OwtP2PError.P2P_CLIENT_INVALID_STATE; import static owt.p2p.OwtP2PError.P2P_WEBRTC_SDP; import android.util.Log; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.webrtc.DataChannel; import org.webrtc.IceCandidate; import org.webrtc.MediaStream; import org.webrtc.PeerConnection; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.concurrent.ConcurrentHashMap; import owt.base.ActionCallback; import owt.base.AudioEncodingParameters; import owt.base.LocalStream; import owt.base.OwtError; import owt.base.PeerConnectionChannel; import owt.base.VideoEncodingParameters; final class P2PPeerConnectionChannel extends PeerConnectionChannel { // <MediaStreamId, CallbackInfo> ConcurrentHashMap<String, CallbackInfo> publishCallbacks; private Long messageId = 0L; // <MessageId, Callback> private ConcurrentHashMap<Long, ActionCallback<Void>> sendMsgCallbacks; // <LocalStream> ArrayList<LocalStream> publishedStreams; private String currentMediaStreamId; // <MediaStreamId, RemoteStream> private ConcurrentHashMap<String, RemoteStream> remoteStreams; // <MediaStreamId> private ArrayList<String> pendingAckRemoteStreams; private ArrayList<Publication> publications; private final Object negLock = new Object(); private boolean renegotiationNeeded = false; private boolean negotiating = false; private boolean streamRemovable = true; private boolean unifiedPlan = false; private boolean continualIceGathering = true; private boolean everPublished = false; P2PPeerConnectionChannel(String peerId, P2PClientConfiguration configuration, PeerConnectionChannelObserver observer) { super(peerId, configuration.rtcConfiguration, true, true, observer); publishCallbacks = new ConcurrentHashMap<>(); sendMsgCallbacks = new ConcurrentHashMap<>(); publishedStreams = new ArrayList<>(); remoteStreams = new ConcurrentHashMap<>(); pendingAckRemoteStreams = new ArrayList<>(); publications = new ArrayList<>(); for (VideoEncodingParameters parameters : configuration.videoEncodings) { if (videoCodecs == null) { videoCodecs = new ArrayList<>(); } videoCodecs.add(parameters.codec.name); } videoMaxBitrate = VideoEncodingParameters.maxBitrate; for (AudioEncodingParameters parameters : configuration.audioEncodings) { if (audioCodecs == null) { audioCodecs = new ArrayList<>(); } audioCodecs.add(parameters.codec.name); } audioMaxBitrate = AudioEncodingParameters.maxBitrate; } void publish(LocalStream localStream, ActionCallback<Publication> callback) { if (!streamRemovable && everPublished) { if (callback != null) { callback.onFailure(new OwtError(P2P_CLIENT_INVALID_STATE.value, "Cannot publish multiple streams due to the ability of peer client.")); } return; } MediaStream currentMediaStream = GetMediaStream(localStream); RCHECK(currentMediaStream); currentMediaStreamId = localStream.id(); if (publishedStreams.contains(localStream)) { if (callback != null) { callback.onFailure( new OwtError(P2P_CLIENT_INVALID_STATE.value, "Duplicated stream.")); } return; } CallbackInfo callbackInfo = new CallbackInfo(currentMediaStream, callback); if (!currentMediaStream.audioTracks.isEmpty()) { publishCallbacks.put(currentMediaStream.audioTracks.get(0).id(), callbackInfo); } if (!currentMediaStream.videoTracks.isEmpty()) { publishCallbacks.put(currentMediaStream.videoTracks.get(0).id(), callbackInfo); } publishedStreams.add(localStream); addStream(currentMediaStream); everPublished = true; // create the data channel here due to BUG1418. if (localDataChannel == null) { createDataChannel(); } } void unpublish(String mediaStreamId) { for (LocalStream localStream : publishedStreams) { if (localStream.id().equals(mediaStreamId)) { publishedStreams.remove(localStream); removeStream(mediaStreamId); break; } } } protected synchronized void dispose() { super.dispose(); for (RemoteStream remoteStream : remoteStreams.values()) { remoteStream.onEnded(); } for (Publication publication : publications) { publication.onEnded(); } publishedStreams.clear(); remoteStreams.clear(); publications.clear(); } void processTrackAck(JSONArray tracksData) throws JSONException { for (int i = 0; i < tracksData.length(); i++) { String trackId = tracksData.getString(i); CallbackInfo callbackInfo = publishCallbacks.get(trackId); if (callbackInfo != null && --callbackInfo.trackNum == 0 && callbackInfo.callback != null) { Publication publication = new Publication(callbackInfo.mediaStreamId, this); publications.add(publication); callbackInfo.callback.onSuccess(publication); } publishCallbacks.remove(trackId); } } void processDataAck(Long msgId) { if (sendMsgCallbacks.containsKey(msgId)) { sendMsgCallbacks.get(msgId).onSuccess(null); sendMsgCallbacks.remove(msgId); } } void processError(OwtError error) { for (CallbackInfo callbackInfo : publishCallbacks.values()) { if (--callbackInfo.trackNum == 0 && callbackInfo.callback != null) { callbackInfo.callback.onFailure(error); } } publishCallbacks.clear(); for (ActionCallback<Void> callback : sendMsgCallbacks.values()) { callback.onFailure(error); } sendMsgCallbacks.clear(); } // TODO: currently (v4.1) Android is compatible with all other platforms. private boolean checkCompatibility(JSONObject userInfo) { try { boolean hasCap = userInfo.has("capabilities"); JSONObject cap = hasCap ? userInfo.getJSONObject("capabilities") : null; streamRemovable = cap == null ? !userInfo.getJSONObject("runtime").getString("name").equals("Firefox") : cap.getBoolean("streamRemovable"); unifiedPlan = cap != null && cap.getBoolean("unifiedPlan"); continualIceGathering = cap != null && cap.getBoolean("continualIceGathering"); } catch (JSONException e) { DCHECK(e); } return true; } void processUserInfo(JSONObject userInfo) { // check capabilities. if (!checkCompatibility(userInfo)) { onError = true; observer.onError(key, "Incompatible", true); } } void processNegotiationRequest() { synchronized (negLock) { if (!negotiating && getSignalingState() == STABLE) { negotiating = true; renegotiationNeeded = false; createOffer(); } else { renegotiationNeeded = true; } } } PeerConnection.SignalingState getSignalingState() { return signalingState; } private void checkWaitingList() { if (renegotiationNeeded) { renegotiationNeeded = false; processNegotiationRequest(); } for (String id : pendingAckRemoteStreams) { observer.onAddStream(key, remoteStreams.get(id)); } pendingAckRemoteStreams.clear(); } void sendData(final String message, ActionCallback<Void> callback) { Long msgId = ++messageId; final JSONObject messageObj = new JSONObject(); try { messageObj.put("id", msgId); messageObj.put("data", message); } catch (JSONException e) { DCHECK(e); } if (callback != null) { sendMsgCallbacks.put(msgId, callback); } if (localDataChannel == null || localDataChannel.state() != OPEN) { queuedMessage.add(messageObj.toString()); if (localDataChannel == null) { createDataChannel(); } return; } ByteBuffer byteBuffer = ByteBuffer.wrap(messageObj.toString().getBytes(Charset.forName("UTF-8"))); DataChannel.Buffer buffer = new DataChannel.Buffer(byteBuffer, false); localDataChannel.send(buffer); } //All PeerConnection.Observer publishCallbacks should be pooled onto callbackExecutor. @Override public void onSignalingChange(final PeerConnection.SignalingState signalingState) { callbackExecutor.execute(() -> { Log.d(LOG_TAG, "onSignalingChange " + signalingState); P2PPeerConnectionChannel.this.signalingState = signalingState; if (signalingState == STABLE) { synchronized (negLock) { negotiating = false; } checkWaitingList(); } }); } @Override public void onIceConnectionChange( final PeerConnection.IceConnectionState iceConnectionState) { callbackExecutor.execute(() -> { Log.d(LOG_TAG, "onIceConnectionChange " + iceConnectionState); P2PPeerConnectionChannel.this.iceConnectionState = iceConnectionState; if (iceConnectionState == CONNECTED || iceConnectionState == COMPLETED) { checkWaitingList(); } if (iceConnectionState == PeerConnection.IceConnectionState.FAILED) { for (RemoteStream remoteStream : remoteStreams.values()) { remoteStream.onEnded(); } for (Publication publication : publications) { publication.onEnded(); } remoteStreams.clear(); publications.clear(); } }); } @Override public void onIceCandidate(final IceCandidate iceCandidate) { callbackExecutor.execute(() -> { Log.d(LOG_TAG, "onIceCandidate"); observer.onIceCandidate(key, iceCandidate); }); } @Override public void onIceCandidatesRemoved(IceCandidate[] iceCandidates) { } @Override public void onAddStream(final MediaStream mediaStream) { callbackExecutor.execute(() -> { RemoteStream remoteStream = new RemoteStream(key, mediaStream); remoteStreams.put(mediaStream.getId(), remoteStream); if (iceConnectionState == CONNECTED || iceConnectionState == COMPLETED) { observer.onAddStream(key, remoteStream); } else { pendingAckRemoteStreams.add(mediaStream.getId()); } }); } @Override public void onRemoveStream(final MediaStream mediaStream) { String id = mediaStream.getId(); callbackExecutor.execute(() -> { Log.d(LOG_TAG, "onRemoveStream"); if (remoteStreams.containsKey(id)) { remoteStreams.remove(id).onEnded(); } }); } @Override public void onRenegotiationNeeded() { callbackExecutor.execute(() -> { if (disposed()) { return; } Log.d(LOG_TAG, "onRenegotiationNeeded"); processNegotiationRequest(); }); } @Override public void onSetSuccess() { callbackExecutor.execute(() -> { if (disposed()) { return; } Log.d(LOG_TAG, "onSetSuccess "); if (signalingState == PeerConnection.SignalingState.HAVE_REMOTE_OFFER || peerConnection.getLocalDescription() == null) { createAnswer(); } else { drainRemoteCandidates(); if (currentMediaStreamId != null) { setMaxBitrate(currentMediaStreamId); currentMediaStreamId = null; } } }); } @Override public void onCreateFailure(final String error) { callbackExecutor.execute(() -> { for (CallbackInfo callbackInfo : publishCallbacks.values()) { if (callbackInfo.callback != null) { callbackInfo.callback.onFailure(new OwtError(P2P_WEBRTC_SDP.value, error)); } } publishCallbacks.clear(); observer.onError(key, error, false); }); } @Override public void onSetFailure(final String error) { callbackExecutor.execute(() -> { for (CallbackInfo callbackInfo : publishCallbacks.values()) { if (callbackInfo.callback != null) { callbackInfo.callback.onFailure(new OwtError(P2P_WEBRTC_SDP.value, error)); } } publishCallbacks.clear(); observer.onError(key, error, false); }); } static class CallbackInfo { final String mediaStreamId; final ActionCallback<Publication> callback; int trackNum; CallbackInfo(MediaStream mediaStream, ActionCallback<Publication> callback) { this.mediaStreamId = mediaStream.getId(); this.callback = callback; trackNum = mediaStream.audioTracks.size() + mediaStream.videoTracks.size(); } } }