/* * Jitsi, the OpenSource Java VoIP and Instant Messaging client. * * Copyright @ 2015 Atlassian Pty Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package net.java.sip.communicator.impl.gui.main.call; import java.awt.*; import java.beans.*; import java.net.*; import java.util.*; import java.util.List; import javax.swing.*; import javax.swing.text.*; import net.java.sip.communicator.impl.gui.*; import net.java.sip.communicator.plugin.desktoputil.*; import net.java.sip.communicator.service.protocol.*; import net.java.sip.communicator.service.protocol.media.*; import net.java.sip.communicator.util.*; import org.ice4j.ice.*; import org.jitsi.service.neomedia.*; import org.jitsi.service.neomedia.stats.*; import org.jitsi.service.resources.*; import org.jitsi.util.*; import com.explodingpixels.macwidgets.*; import java.security.cert.*; import javax.swing.event.*; /** * The frame displaying the statistical information for a telephony conference. * * @author Vincent Lucas * @author Yana Stamcheva */ public class CallInfoFrame implements CallTitleListener, PropertyChangeListener, HyperlinkListener { /** * The telephony conference to compute and display the statistics of. */ private CallConference callConference; /** * The call info window. */ private final JDialog callInfoWindow; /** * The information text pane. */ private final JEditorPane infoTextPane; /** * The font color. */ private String fontColor; /** * The resource management service. */ private final ResourceManagementService resources = GuiActivator.getResources(); /** * Indicates if the info window has any text to display. */ private boolean hasCallInfo; /** * Dummy URL to indicate that the certificate should be displayed. */ private final String CERTIFICATE_URL = "jitsi://viewCertificate"; /** * Creates a new frame containing the statistical information for a specific * telephony conference. * * @param callConference the telephony conference to compute and display the * statistics of */ public CallInfoFrame(CallConference callConference) { this.callConference = callConference; JScrollPane scrollPane = new JScrollPane(); scrollPane.setOpaque(false); scrollPane.getViewport().setOpaque(false); infoTextPane = createGeneralInfoPane(); Caret caret = infoTextPane.getCaret(); if (caret instanceof DefaultCaret) { ((DefaultCaret) caret).setUpdatePolicy(DefaultCaret.NEVER_UPDATE); } scrollPane.getViewport().add(infoTextPane); callInfoWindow = createCallInfoWindow( GuiActivator.getResources().getI18NString( "service.gui.callinfo.TECHNICAL_CALL_INFO")); callInfoWindow.getContentPane().add(scrollPane); hasCallInfo = this.constructCallInfo(); } /** * Creates different types of windows depending on the operating system. * * @param title the title of the created window */ private JDialog createCallInfoWindow( String title) { JDialog callInfoWindow = null; if (OSUtils.IS_MAC) { HudWindow window = new HudWindow(); JDialog dialog = window.getJDialog(); dialog.setTitle(title); callInfoWindow = window.getJDialog(); callInfoWindow.setResizable(true); fontColor = "FFFFFF"; } else { SIPCommDialog dialog = new SIPCommDialog(false); callInfoWindow = dialog; callInfoWindow.setTitle(title); fontColor = "000000"; } return callInfoWindow; } /** * Create call info text pane. * * @return the created call info text pane */ private JEditorPane createGeneralInfoPane() { JEditorPane infoTextPane = new JEditorPane(); /* * Make JEditorPane respect our default font because we will be using it * to just display text. */ infoTextPane.putClientProperty( JEditorPane.HONOR_DISPLAY_PROPERTIES, true); infoTextPane.setOpaque(false); infoTextPane.setEditable(false); infoTextPane.setContentType("text/html"); infoTextPane.addHyperlinkListener(this); return infoTextPane; } /** * Returns an HTML string corresponding to the given labelText and infoText, * that could be easily added to the information text pane. * * @param labelText the label text that would be shown in bold * @param infoText the info text that would be shown in plain text * @return the newly constructed HTML string */ private String getLineString(String labelText, String infoText) { return "" + labelText + " : " + infoText + "
"; } /** * Constructs the call info text. * @return true if call info could be found, false otherwise */ private boolean constructCallInfo() { StringBuffer stringBuffer = new StringBuffer(); stringBuffer.append( "

" + ""); stringBuffer.append(getLineString(resources.getI18NString( "service.gui.callinfo.CALL_INFORMATION"), "")); List calls = callConference.getCalls(); /* * TODO A telephony conference may consist of a single Call with * multiple CallPeers but it may as well consist of multiple Calls. */ if (calls.size() <= 0) { return false; } else { Call aCall = calls.get(0); stringBuffer.append( getLineString( resources.getI18NString( "service.gui.callinfo.CALL_IDENTITY"), aCall.getProtocolProvider().getAccountID() .getDisplayName())); int callPeerCount = callConference.getCallPeerCount(); if (callPeerCount > 1) { stringBuffer.append( getLineString(resources.getI18NString( "service.gui.callinfo.PEER_COUNT"), String.valueOf(callPeerCount))); } boolean isConfFocus = callConference.isConferenceFocus(); if (isConfFocus) { stringBuffer.append(getLineString( resources.getI18NString( "service.gui.callinfo.IS_CONFERENCE_FOCUS"), String.valueOf(isConfFocus))); } TransportProtocol preferredTransport = aCall.getProtocolProvider().getTransportProtocol(); if (preferredTransport != TransportProtocol.UNKNOWN) stringBuffer.append(getLineString( resources.getI18NString("service.gui.callinfo.CALL_TRANSPORT"), preferredTransport.toString())); final OperationSetTLS opSetTls = aCall.getProtocolProvider() .getOperationSet(OperationSetTLS.class); if (opSetTls != null) { stringBuffer.append(getLineString( resources.getI18NString( "service.gui.callinfo.TLS_PROTOCOL"), opSetTls.getProtocol())); stringBuffer.append(getLineString( resources.getI18NString( "service.gui.callinfo.TLS_CIPHER_SUITE"), opSetTls.getCipherSuite())); stringBuffer.append("") .append(resources.getI18NString( "service.gui.callinfo.VIEW_CERTIFICATE")) .append("
"); } constructCallPeersInfo(stringBuffer); stringBuffer.append("

"); infoTextPane.setText(stringBuffer.toString()); infoTextPane.revalidate(); infoTextPane.repaint(); return true; } } /** * Constructs call peers' info. * * @param stringBuffer the StringBuffer, where call peer info will * be added */ private void constructCallPeersInfo(StringBuffer stringBuffer) { for (CallPeer callPeer : callConference.getCallPeers()) { if(callPeer instanceof MediaAwareCallPeer) { ((MediaAwareCallPeer) callPeer) .getMediaHandler() .addPropertyChangeListener(this); } stringBuffer.append("
"); constructPeerInfo(callPeer, stringBuffer); } } /** * Constructs peer info. * * @param callPeer the CallPeer, for which we'll construct the info * @param stringBuffer the StringBuffer, where call peer info will * be added */ private void constructPeerInfo(CallPeer callPeer, StringBuffer stringBuffer) { stringBuffer.append(getLineString(callPeer.getAddress(), "")); if(callPeer.getCallDurationStartTime() != CallPeer.CALL_DURATION_START_TIME_UNKNOWN) { Date startTime = new Date(callPeer.getCallDurationStartTime()); stringBuffer.append(getLineString( resources.getI18NString("service.gui.callinfo.CALL_DURATION"), GuiUtils.formatTime(startTime.getTime(), System.currentTimeMillis()))); } if(callPeer instanceof MediaAwareCallPeer) { CallPeerMediaHandler callPeerMediaHandler = ((MediaAwareCallPeer) callPeer).getMediaHandler(); if(callPeerMediaHandler != null) { MediaStream mediaStream = callPeerMediaHandler.getStream(MediaType.AUDIO); if (mediaStream != null && mediaStream.isStarted()) { stringBuffer.append("
"); stringBuffer.append(getLineString(resources.getI18NString( "service.gui.callinfo.AUDIO_INFO"), "")); this.appendStreamEncryptionMethod( stringBuffer, callPeerMediaHandler, mediaStream, MediaType.AUDIO); constructAudioVideoInfo( callPeerMediaHandler, mediaStream, stringBuffer, MediaType.AUDIO); } mediaStream = callPeerMediaHandler.getStream(MediaType.VIDEO); if (mediaStream != null && mediaStream.isStarted()) { stringBuffer.append("
"); stringBuffer.append(getLineString(resources.getI18NString( "service.gui.callinfo.VIDEO_INFO"), "")); this.appendStreamEncryptionMethod( stringBuffer, callPeerMediaHandler, mediaStream, MediaType.VIDEO); constructAudioVideoInfo( callPeerMediaHandler, mediaStream, stringBuffer, MediaType.VIDEO); } stringBuffer.append("
"); // ICE state String iceState = callPeerMediaHandler.getICEState(); if(iceState != null && !iceState.equals("Terminated")) { stringBuffer.append(getLineString( resources.getI18NString( "service.gui.callinfo.ICE_STATE"), resources.getI18NString( "service.gui.callinfo.ICE_STATE." + iceState.toUpperCase()))); } stringBuffer.append("
"); // Total harvesting time. long harvestingTime = callPeerMediaHandler.getTotalHarvestingTime(); if(harvestingTime != 0) { stringBuffer.append(getLineString(resources.getI18NString( "service.gui.callinfo.TOTAL_HARVESTING_TIME" ), harvestingTime + " " + resources.getI18NString( "service.gui.callinfo.HARVESTING_MS_FOR") + " " + callPeerMediaHandler.getNbHarvesting() + " " + resources.getI18NString( "service.gui.callinfo.HARVESTS"))); } // Current harvester time if ICE agent is harvesting. String[] harvesterNames = { "GoogleTurnCandidateHarvester", "GoogleTurnSSLCandidateHarvester", "HostCandidateHarvester", "JingleNodesHarvester", "StunCandidateHarvester", "TurnCandidateHarvester", "UPNPHarvester" }; for(int i = 0; i < harvesterNames.length; ++i) { harvestingTime = callPeerMediaHandler.getHarvestingTime( harvesterNames[i]); if(harvestingTime != 0) { stringBuffer.append(getLineString( resources.getI18NString( "service.gui.callinfo.HARVESTING_TIME") + " " + harvesterNames[i], harvestingTime + " " + resources.getI18NString( "service.gui.callinfo.HARVESTING_MS_FOR" ) + " " + callPeerMediaHandler.getNbHarvesting( harvesterNames[i]) + " " + resources.getI18NString( "service.gui.callinfo.HARVESTS"))); } } } } } /** * Constructs audio video peer info. * * @param callPeerMediaHandler The CallPeerMadiaHandler containing * the AUDIO/VIDEO stream. * @param mediaStream the MediaStream that gives us access to * audio video info * @param stringBuffer the StringBuffer, where call peer info will * be added * @param mediaType The media type used to determine which stream of the * media handler must returns it encryption method. */ private void constructAudioVideoInfo( CallPeerMediaHandler callPeerMediaHandler, MediaStream mediaStream, StringBuffer stringBuffer, MediaType mediaType) { MediaStreamStats2 mediaStreamStats = mediaStream.getMediaStreamStats(); if(mediaStreamStats == null) return; mediaStreamStats.updateStats(); if(mediaType == MediaType.VIDEO) { Dimension downloadVideoSize = mediaStreamStats.getDownloadVideoSize(); Dimension uploadVideoSize = mediaStreamStats.getUploadVideoSize(); // Checks that at least one video stream is active. if(downloadVideoSize != null || uploadVideoSize != null) { stringBuffer.append( getLineString(resources.getI18NString( "service.gui.callinfo.VIDEO_SIZE"), "↓ " + this.videoSizeToString(downloadVideoSize) + " ↑ " + this.videoSizeToString(uploadVideoSize))); } // Otherwise, quit the stats for this video stream. else { return; } } stringBuffer.append( getLineString( resources.getI18NString("service.gui.callinfo.CODEC"), mediaStreamStats.getEncoding() + " / " + mediaStreamStats.getEncodingClockRate() + " Hz")); boolean displayedIpPort = false; // ICE candidate type String iceCandidateExtendedType = callPeerMediaHandler.getICECandidateExtendedType( mediaType.toString()); if(iceCandidateExtendedType != null) { stringBuffer.append(getLineString(resources.getI18NString( "service.gui.callinfo.ICE_CANDIDATE_EXTENDED_TYPE"), iceCandidateExtendedType)); displayedIpPort = true; } // Local host address InetSocketAddress iceLocalHostAddress = callPeerMediaHandler.getICELocalHostAddress(mediaType.toString()); if(iceLocalHostAddress != null) { stringBuffer.append(getLineString(resources.getI18NString( "service.gui.callinfo.ICE_LOCAL_HOST_ADDRESS"), iceLocalHostAddress.getAddress().getHostAddress() + "/" + iceLocalHostAddress.getPort())); displayedIpPort = true; } // Local reflexive address InetSocketAddress iceLocalReflexiveAddress = callPeerMediaHandler.getICELocalReflexiveAddress( mediaType.toString()); if(iceLocalReflexiveAddress != null) { stringBuffer.append(getLineString(resources.getI18NString( "service.gui.callinfo.ICE_LOCAL_REFLEXIVE_ADDRESS"), iceLocalReflexiveAddress.getAddress() .getHostAddress() + "/" + iceLocalReflexiveAddress.getPort())); displayedIpPort = true; } // Local relayed address InetSocketAddress iceLocalRelayedAddress = callPeerMediaHandler.getICELocalRelayedAddress( mediaType.toString()); if(iceLocalRelayedAddress != null) { stringBuffer.append(getLineString(resources.getI18NString( "service.gui.callinfo.ICE_LOCAL_RELAYED_ADDRESS"), iceLocalRelayedAddress.getAddress() .getHostAddress() + "/" + iceLocalRelayedAddress.getPort())); displayedIpPort = true; } // Remote relayed address InetSocketAddress iceRemoteRelayedAddress = callPeerMediaHandler.getICERemoteRelayedAddress( mediaType.toString()); if(iceRemoteRelayedAddress != null) { stringBuffer.append(getLineString(resources.getI18NString( "service.gui.callinfo.ICE_REMOTE_RELAYED_ADDRESS"), iceRemoteRelayedAddress.getAddress() .getHostAddress() + "/" + iceRemoteRelayedAddress.getPort())); displayedIpPort = true; } // Remote reflexive address InetSocketAddress iceRemoteReflexiveAddress = callPeerMediaHandler.getICERemoteReflexiveAddress( mediaType.toString()); if(iceRemoteReflexiveAddress != null) { stringBuffer.append(getLineString(resources.getI18NString( "service.gui.callinfo.ICE_REMOTE_REFLEXIVE_ADDRESS"), iceRemoteReflexiveAddress.getAddress() .getHostAddress() + "/" + iceRemoteReflexiveAddress.getPort())); displayedIpPort = true; } // Remote host address InetSocketAddress iceRemoteHostAddress = callPeerMediaHandler.getICERemoteHostAddress(mediaType.toString()); if(iceRemoteHostAddress != null) { stringBuffer.append(getLineString(resources.getI18NString( "service.gui.callinfo.ICE_REMOTE_HOST_ADDRESS"), iceRemoteHostAddress.getAddress().getHostAddress() + "/" + iceRemoteHostAddress.getPort())); displayedIpPort = true; } // If the stream does not use ICE, then show the transport IP/port. if(!displayedIpPort) { stringBuffer.append( getLineString( resources.getI18NString("service.gui.callinfo.LOCAL_IP"), mediaStreamStats.getLocalIPAddress() + " / " + String.valueOf(mediaStreamStats.getLocalPort()))); stringBuffer.append( getLineString( resources.getI18NString("service.gui.callinfo.REMOTE_IP"), mediaStreamStats.getRemoteIPAddress() + " / " + String.valueOf(mediaStreamStats.getRemotePort()))); } stringBuffer.append( getLineString( resources.getI18NString( "service.gui.callinfo.BANDWITH"), "↓ " + (int) mediaStreamStats.getReceiveStats().getBitrate()/1024 + " Kbps " + " ↑ " + (int) mediaStreamStats.getSendStats().getBitrate()/1024 + " Kbps")); stringBuffer.append( getLineString( resources.getI18NString("service.gui.callinfo.LOSS_RATE"), "↓" + (int) (mediaStreamStats.getReceiveStats().getLossRate() * 100) + "% ↑ " + (int) (mediaStreamStats.getSendStats().getLossRate() * 100) + "%")); stringBuffer.append( getLineString( resources.getI18NString( "service.gui.callinfo.DECODED_WITH_FEC"), String.valueOf(mediaStreamStats.getNbFec()))); stringBuffer.append(getLineString( resources.getI18NString( "service.gui.callinfo.DISCARDED_PERCENT"), String.valueOf((int)mediaStreamStats.getPercentDiscarded() + "%"))); stringBuffer.append(getLineString( resources.getI18NString("service.gui.callinfo.DISCARDED_TOTAL"), String.valueOf(mediaStreamStats.getNbDiscarded()) + " (" + mediaStreamStats.getNbDiscardedLate() + " late, " + mediaStreamStats.getNbDiscardedFull() + " full, " + mediaStreamStats.getNbDiscardedShrink() + " shrink, " + mediaStreamStats.getNbDiscardedReset() + " reset)")); stringBuffer.append(getLineString( resources.getI18NString( "service.gui.callinfo.ADAPTIVE_JITTER_BUFFER"), mediaStreamStats.isAdaptiveBufferEnabled() ? "enabled" : "disabled")); stringBuffer.append(getLineString( resources.getI18NString( "service.gui.callinfo.JITTER_BUFFER_DELAY"), "~" + mediaStreamStats.getJitterBufferDelayMs() + "ms; currently in queue: " + mediaStreamStats.getPacketQueueCountPackets() + "/" + mediaStreamStats.getPacketQueueSize() + " packets")); long sendRttMs = mediaStreamStats.getSendStats().getRtt(); long recvRttMs = mediaStreamStats.getReceiveStats().getRtt(); if(recvRttMs != -1 || sendRttMs != -1) { stringBuffer.append( getLineString(resources.getI18NString( "service.gui.callinfo.RTT"), (recvRttMs != -1 ? "↓ " + recvRttMs + " ms" : "") + (sendRttMs != -1 ? "↑ " + sendRttMs + " ms" : ""))); } stringBuffer.append( getLineString(resources.getI18NString( "service.gui.callinfo.JITTER"), "↓ " + (int) mediaStreamStats.getReceiveStats().getJitter() + " ms ↑ " + (int) mediaStreamStats.getSendStats().getJitter() + " ms")); } /** * Called when the title of the given CallPanel changes. * * @param callContainer the CallContainer, which title has changed */ public void callTitleChanged(CallPanel callContainer) { String selectedText = infoTextPane.getSelectedText(); // If there's a selection do not update call info, otherwise the user // would not be able to copy the selected text. if (selectedText != null && selectedText.length() > 0) return; hasCallInfo = this.constructCallInfo(); } /** * Shows/hides the corresponding window. * * @param isVisible true to show the window, false to * hide it */ public void setVisible(boolean isVisible) { if (isVisible) { callInfoWindow.pack(); callInfoWindow.setPreferredSize(new Dimension(300, 450)); callInfoWindow.setSize(300, 450); callInfoWindow.setLocationRelativeTo(null); } callInfoWindow.setVisible(isVisible); } /** * Indicates if the corresponding window is visible. * * @return true if the window is visible, false - * otherwise */ public boolean isVisible() { return callInfoWindow.isVisible(); } /** * Indicates if the call info window has any text to display * * @return true if the window contains call info, * false otherwise */ public boolean hasCallInfo() { return hasCallInfo; } /** * Disposes the corresponding window. */ public void dispose() { callInfoWindow.dispose(); } /** * Appends to the string buffer the stream encryption method (null, MIKEY, * SDES, ZRTP) used for a given media stream (type AUDIO or VIDEO). * * @param stringBuffer The string buffer containing the call information * statistics. * @param callPeerMediaHandler The media handler containing the different * media streams. * @param mediaStream the MediaStream that gives us access to * audio/video info. * @param mediaType The media type used to determine which stream of the * media handler must returns it encryption method. */ private void appendStreamEncryptionMethod( StringBuffer stringBuffer, CallPeerMediaHandler callPeerMediaHandler, MediaStream mediaStream, MediaType mediaType) { String transportProtocolString = ""; StreamConnector.Protocol transportProtocol = mediaStream.getTransportProtocol(); if(transportProtocol != null) { transportProtocolString = transportProtocol.toString(); } String rtpType; SrtpControl srtpControl = callPeerMediaHandler.getEncryptionMethod(mediaType); // If the stream is secured. if(srtpControl != null) { String info; if (srtpControl instanceof ZrtpControl) { info = "ZRTP " + ((ZrtpControl)srtpControl).getCipherString(); } else { info = srtpControl.getSrtpControlType().toString(); } rtpType = resources.getI18NString( "service.gui.callinfo.MEDIA_STREAM_SRTP") + " (" + resources.getI18NString( "service.gui.callinfo.KEY_EXCHANGE_PROTOCOL") + ": " + info + ")"; } // If the stream is not secured. else { rtpType = resources.getI18NString( "service.gui.callinfo.MEDIA_STREAM_RTP"); } // Appends the encryption status String. stringBuffer.append(getLineString( resources.getI18NString( "service.gui.callinfo.MEDIA_STREAM_TRANSPORT_PROTOCOL"), transportProtocolString + " / " + rtpType)); } /** * Listen for ice property change to trigger call info update. * @param evt the event for state change. */ public void propertyChange(PropertyChangeEvent evt) { if(evt.getPropertyName().equals(Agent.PROPERTY_ICE_PROCESSING_STATE)) { callTitleChanged(null); } } /** * Converts a video size Dimension into its String representation. * * @param videoSize The video size Dimension, containing the width and the * hieght of the video. * * @return The String representation of the video width and height, or a * String with "Not Available (N.A.)" if the videoSize is null. */ private String videoSizeToString(Dimension videoSize) { if(videoSize == null) { return resources.getI18NString("service.gui.callinfo.NA"); } return ((int) videoSize.getWidth()) + " x " + ((int) videoSize.getHeight()); } /** * Invoked when user clicks a link in the editor pane. * @param e the event */ public void hyperlinkUpdate(HyperlinkEvent e) { // Handle "View certificate" link if (e.getEventType() == HyperlinkEvent.EventType.ACTIVATED && CERTIFICATE_URL.equals(e.getDescription())) { List calls = callConference.getCalls(); if (!calls.isEmpty()) { Call aCall = calls.get(0); Certificate[] chain = aCall.getProtocolProvider() .getOperationSet(OperationSetTLS.class) .getServerCertificates(); ViewCertificateFrame certFrame = new ViewCertificateFrame(chain, null, resources.getI18NString( "service.gui.callinfo.TLS_CERTIFICATE_CONTENT")); certFrame.setVisible(true); } } } }