001/** 002 * 003 * Copyright 2003-2005 Jive Software. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.jivesoftware.smackx.jingleold.media; 018 019import java.util.ArrayList; 020import java.util.List; 021import java.util.logging.Logger; 022 023import org.jivesoftware.smack.SmackException.NotConnectedException; 024import org.jivesoftware.smack.XMPPException; 025import org.jivesoftware.smack.packet.IQ; 026 027import org.jivesoftware.smackx.jingleold.ContentNegotiator; 028import org.jivesoftware.smackx.jingleold.JingleActionEnum; 029import org.jivesoftware.smackx.jingleold.JingleException; 030import org.jivesoftware.smackx.jingleold.JingleNegotiator; 031import org.jivesoftware.smackx.jingleold.JingleNegotiatorState; 032import org.jivesoftware.smackx.jingleold.JingleSession; 033import org.jivesoftware.smackx.jingleold.listeners.JingleListener; 034import org.jivesoftware.smackx.jingleold.listeners.JingleMediaListener; 035import org.jivesoftware.smackx.jingleold.packet.Jingle; 036import org.jivesoftware.smackx.jingleold.packet.JingleContent; 037import org.jivesoftware.smackx.jingleold.packet.JingleDescription; 038import org.jivesoftware.smackx.jingleold.packet.JingleError; 039 040/** 041 * Manager for jmf descriptor negotiation. This class is responsible 042 * for managing the descriptor negotiation process, handling all the xmpp 043 * packets interchange and the stage control. handling all the xmpp packets 044 * interchange and the stage control. 045 * 046 * @author Thiago Camargo 047 */ 048@SuppressWarnings("UnusedVariable") 049public class MediaNegotiator extends JingleNegotiator { 050 051 private static final Logger LOGGER = Logger.getLogger(MediaNegotiator.class.getName()); 052 053 // private JingleSession session; // The session this negotiation 054 055 private final JingleMediaManager mediaManager; 056 057 // Local and remote payload types... 058 059 private final List<PayloadType> localAudioPts = new ArrayList<>(); 060 061 private final List<PayloadType> remoteAudioPts = new ArrayList<>(); 062 063 private PayloadType bestCommonAudioPt; 064 065 private ContentNegotiator parentNegotiator; 066 067 /** 068 * Default constructor. The constructor establishes some basic parameters, 069 * but it does not start the negotiation. For starting the negotiation, call 070 * startNegotiation. 071 * 072 * @param session TODO javadoc me please 073 * @param mediaManager the media manager. 074 * @param pts the list of payload types. 075 * @param parentNegotiator the parent content negotiator. 076 */ 077 public MediaNegotiator(JingleSession session, JingleMediaManager mediaManager, List<PayloadType> pts, 078 ContentNegotiator parentNegotiator) { 079 super(session); 080 081 this.mediaManager = mediaManager; 082 this.parentNegotiator = parentNegotiator; 083 084 bestCommonAudioPt = null; 085 086 if (pts != null) { 087 if (pts.size() > 0) { 088 localAudioPts.addAll(pts); 089 } 090 } 091 } 092 093 /** 094 * Returns the media manager for this negotiator. 095 * 096 * @return the media manager. 097 */ 098 public JingleMediaManager getMediaManager() { 099 return mediaManager; 100 } 101 102 /** 103 * Dispatch an incoming packet. The method is responsible for recognizing 104 * the stanza type and, depending on the current state, delivering the 105 * stanza to the right event handler and wait for a response. 106 * 107 * @param iq TODO javadoc me please 108 * the stanza received 109 * @return the new Jingle stanza to send. 110 * @throws XMPPException if an XMPP protocol error was received. 111 * @throws NotConnectedException if the XMPP connection is not connected. 112 * @throws InterruptedException if the calling thread was interrupted. 113 */ 114 @Override 115 public List<IQ> dispatchIncomingPacket(IQ iq, String id) throws XMPPException, NotConnectedException, InterruptedException { 116 List<IQ> responses = new ArrayList<>(); 117 IQ response = null; 118 119 if (iq.getType().equals(IQ.Type.error)) { 120 // Process errors 121 setNegotiatorState(JingleNegotiatorState.FAILED); 122 triggerMediaClosed(getBestCommonAudioPt()); 123 // This next line seems wrong, and may subvert the normal closing process. 124 throw new JingleException(iq.getError().getDescriptiveText()); 125 } else if (iq.getType().equals(IQ.Type.result)) { 126 // Process ACKs 127 if (isExpectedId(iq.getStanzaId())) { 128 receiveResult(iq); 129 removeExpectedId(iq.getStanzaId()); 130 } 131 } else if (iq instanceof Jingle) { 132 Jingle jingle = (Jingle) iq; 133 JingleActionEnum action = jingle.getAction(); 134 135 // Only act on the JingleContent sections that belong to this media negotiator. 136 for (JingleContent jingleContent : jingle.getContentsList()) { 137 if (jingleContent.getName().equals(parentNegotiator.getName())) { 138 139 JingleDescription description = jingleContent.getDescription(); 140 141 if (description != null) { 142 143 switch (action) { 144 case CONTENT_ACCEPT: 145 response = receiveContentAcceptAction(jingle, description); 146 break; 147 148 case CONTENT_MODIFY: 149 break; 150 151 case CONTENT_REMOVE: 152 break; 153 154 case SESSION_INFO: 155 response = receiveSessionInfoAction(jingle, description); 156 break; 157 158 case SESSION_INITIATE: 159 response = receiveSessionInitiateAction(jingle, description); 160 break; 161 162 case SESSION_ACCEPT: 163 response = receiveSessionAcceptAction(jingle, description); 164 break; 165 166 default: 167 break; 168 } 169 } 170 } 171 } 172 173 } 174 175 if (response != null) { 176 addExpectedId(response.getStanzaId()); 177 responses.add(response); 178 } 179 180 return responses; 181 } 182 183 /** 184 * Process the ACK of our list of codecs (our offer). 185 */ 186 private static Jingle receiveResult(IQ iq) throws XMPPException { 187 Jingle response = null; 188 189// if (!remoteAudioPts.isEmpty()) { 190// // Calculate the best common codec 191// bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts); 192// 193// // and send an accept if we have an agreement... 194// if (bestCommonAudioPt != null) { 195// response = createAcceptMessage(); 196// } else { 197// throw new JingleException(JingleError.NO_COMMON_PAYLOAD); 198// } 199// } 200 return response; 201 } 202 203 /** 204 * The other side has sent us a content-accept. The payload types in that message may not match with what 205 * we sent, but XEP-167 says that the other side should retain the order of the payload types we first sent. 206 * 207 * This means we can walk through our list, in order, until we find one from their list that matches. This 208 * will be the best payload type to use. 209 * 210 * @param jingle TODO javadoc me please 211 * @return the iq 212 * @throws NotConnectedException if the XMPP connection is not connected. 213 * @throws InterruptedException if the calling thread was interrupted. 214 */ 215 private IQ receiveContentAcceptAction(Jingle jingle, JingleDescription description) throws XMPPException, NotConnectedException, InterruptedException { 216 IQ response; 217 218 List<PayloadType> offeredPayloads = description.getAudioPayloadTypesList(); 219 bestCommonAudioPt = calculateBestCommonAudioPt(offeredPayloads); 220 221 if (bestCommonAudioPt == null) { 222 223 setNegotiatorState(JingleNegotiatorState.FAILED); 224 response = session.createJingleError(jingle, JingleError.NEGOTIATION_ERROR); 225 226 } else { 227 228 setNegotiatorState(JingleNegotiatorState.SUCCEEDED); 229 triggerMediaEstablished(getBestCommonAudioPt()); 230 LOGGER.severe("Media choice:" + getBestCommonAudioPt().getName()); 231 232 response = session.createAck(jingle); 233 } 234 235 return response; 236 } 237 238 /** 239 * Receive a session-initiate packet. 240 * @param jingle TODO javadoc me please 241 * @param description TODO javadoc me please 242 * @return the iq 243 */ 244 private IQ receiveSessionInitiateAction(Jingle jingle, JingleDescription description) { 245 IQ response = null; 246 247 List<PayloadType> offeredPayloads = description.getAudioPayloadTypesList(); 248 bestCommonAudioPt = calculateBestCommonAudioPt(offeredPayloads); 249 250 synchronized (remoteAudioPts) { 251 remoteAudioPts.addAll(offeredPayloads); 252 } 253 254 // If there are suitable/matching payload types then accept this content. 255 if (bestCommonAudioPt != null) { 256 // Let the transport negotiators sort-out connectivity and content-accept instead. 257 // response = createAudioPayloadTypesOffer(); 258 setNegotiatorState(JingleNegotiatorState.PENDING); 259 } else { 260 // Don't really know what to send here. XEP-166 is not clear. 261 setNegotiatorState(JingleNegotiatorState.FAILED); 262 } 263 264 return response; 265 } 266 267 /** 268 * A content info has been received. This is done for publishing the 269 * list of payload types... 270 * 271 * @param jingle TODO javadoc me please 272 * The input packet 273 * @return a Jingle packet 274 * @throws JingleException 275 */ 276 private IQ receiveSessionInfoAction(Jingle jingle, JingleDescription description) throws JingleException { 277 IQ response = null; 278 PayloadType oldBestCommonAudioPt = bestCommonAudioPt; 279 List<PayloadType> offeredPayloads; 280 boolean ptChange = false; 281 282 offeredPayloads = description.getAudioPayloadTypesList(); 283 if (!offeredPayloads.isEmpty()) { 284 285 synchronized (remoteAudioPts) { 286 remoteAudioPts.clear(); 287 remoteAudioPts.addAll(offeredPayloads); 288 } 289 290 // Calculate the best common codec 291 bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts); 292 if (bestCommonAudioPt != null) { 293 // and send an accept if we have an agreement... 294 ptChange = !bestCommonAudioPt.equals(oldBestCommonAudioPt); 295 if (oldBestCommonAudioPt == null || ptChange) { 296 // response = createAcceptMessage(); 297 } 298 } else { 299 throw new JingleException(JingleError.NO_COMMON_PAYLOAD); 300 } 301 } 302 303 // Parse the Jingle and get the payload accepted 304 return response; 305 } 306 307 /** 308 * A jmf description has been accepted. In this case, we must save the 309 * accepted payload type and notify any listener... 310 * 311 * @param jin TODO javadoc me please 312 * The input packet 313 * @return a Jingle packet 314 * @throws JingleException 315 */ 316 private IQ receiveSessionAcceptAction(Jingle jingle, JingleDescription description) throws JingleException { 317 IQ response = null; 318 PayloadType.Audio agreedCommonAudioPt; 319 320 if (bestCommonAudioPt == null) { 321 // Update the best common audio PT 322 bestCommonAudioPt = calculateBestCommonAudioPt(remoteAudioPts); 323 // response = createAcceptMessage(); 324 } 325 326 List<PayloadType> offeredPayloads = description.getAudioPayloadTypesList(); 327 if (!offeredPayloads.isEmpty()) { 328 if (offeredPayloads.size() == 1) { 329 agreedCommonAudioPt = (PayloadType.Audio) offeredPayloads.get(0); 330 if (bestCommonAudioPt != null) { 331 // If the accepted PT matches the best payload 332 // everything is fine 333 if (!agreedCommonAudioPt.equals(bestCommonAudioPt)) { 334 throw new JingleException(JingleError.NEGOTIATION_ERROR); 335 } 336 } 337 338 } else if (offeredPayloads.size() > 1) { 339 throw new JingleException(JingleError.MALFORMED_STANZA); 340 } 341 } 342 343 return response; 344 } 345 346 /** 347 * Return true if the content is negotiated. 348 * 349 * @return true if the content is negotiated. 350 */ 351 public boolean isEstablished() { 352 return getBestCommonAudioPt() != null; 353 } 354 355 /** 356 * Return true if the content is fully negotiated. 357 * 358 * @return true if the content is fully negotiated. 359 */ 360 public boolean isFullyEstablished() { 361 return isEstablished() && ((getNegotiatorState() == JingleNegotiatorState.SUCCEEDED) || (getNegotiatorState() == JingleNegotiatorState.FAILED)); 362 } 363 364 // Payload types 365 366 private PayloadType calculateBestCommonAudioPt(List<PayloadType> remoteAudioPts) { 367 final ArrayList<PayloadType> commonAudioPtsHere = new ArrayList<>(); 368 final ArrayList<PayloadType> commonAudioPtsThere = new ArrayList<>(); 369 PayloadType result = null; 370 371 if (!remoteAudioPts.isEmpty()) { 372 commonAudioPtsHere.addAll(localAudioPts); 373 commonAudioPtsHere.retainAll(remoteAudioPts); 374 375 commonAudioPtsThere.addAll(remoteAudioPts); 376 commonAudioPtsThere.retainAll(localAudioPts); 377 378 if (!commonAudioPtsHere.isEmpty() && !commonAudioPtsThere.isEmpty()) { 379 380 if (session.getInitiator().equals(session.getConnection().getUser())) { 381 PayloadType.Audio bestPtHere = null; 382 383 PayloadType payload = mediaManager.getPreferredPayloadType(); 384 385 if (payload != null && payload instanceof PayloadType.Audio) 386 if (commonAudioPtsHere.contains(payload)) 387 bestPtHere = (PayloadType.Audio) payload; 388 389 if (bestPtHere == null) 390 for (PayloadType payloadType : commonAudioPtsHere) 391 if (payloadType instanceof PayloadType.Audio) { 392 bestPtHere = (PayloadType.Audio) payloadType; 393 break; 394 } 395 396 result = bestPtHere; 397 } else { 398 PayloadType.Audio bestPtThere = null; 399 for (PayloadType payloadType : commonAudioPtsThere) 400 if (payloadType instanceof PayloadType.Audio) { 401 bestPtThere = (PayloadType.Audio) payloadType; 402 break; 403 } 404 405 result = bestPtThere; 406 } 407 } 408 } 409 410 return result; 411 } 412 413 /** 414 * Adds a payload type to the list of remote payloads. 415 * 416 * @param pt TODO javadoc me please 417 * the remote payload type 418 */ 419 public void addRemoteAudioPayloadType(PayloadType.Audio pt) { 420 if (pt != null) { 421 synchronized (remoteAudioPts) { 422 remoteAudioPts.add(pt); 423 } 424 } 425 } 426 427// /** 428// * Create an offer for the list of audio payload types. 429// * 430// * @return a new Jingle packet with the list of audio Payload Types 431// */ 432// private Jingle createAudioPayloadTypesOffer() { 433// 434// JingleContent jingleContent = new JingleContent(parentNegotiator.getCreator(), parentNegotiator.getName()); 435// JingleDescription audioDescr = new JingleDescription.Audio(); 436// 437// // Add the list of payloads for audio and create a 438// // JingleDescription 439// // where we announce our payloads... 440// audioDescr.addAudioPayloadTypes(localAudioPts); 441// jingleContent.setDescription(audioDescr); 442// 443// Jingle jingle = new Jingle(JingleActionEnum.CONTENT_ACCEPT); 444// jingle.addContent(jingleContent); 445// 446// return jingle; 447// } 448 449 // Predefined messages and Errors 450 451 /** 452 * Create an IQ "accept" message. 453 */ 454// private Jingle createAcceptMessage() { 455// Jingle jout = null; 456// 457// // If we have a common best codec, send an accept right now... 458// jout = new Jingle(JingleActionEnum.CONTENT_ACCEPT); 459// JingleContent content = new JingleContent(parentNegotiator.getCreator(), parentNegotiator.getName()); 460// content.setDescription(new JingleDescription.Audio(bestCommonAudioPt)); 461// jout.addContent(content); 462// 463// return jout; 464// } 465 466 // Payloads 467 468 /** 469 * Get the best common codec between both parts. 470 * 471 * @return The best common PayloadType codec. 472 */ 473 public PayloadType getBestCommonAudioPt() { 474 return bestCommonAudioPt; 475 } 476 477 // Events 478 479 /** 480 * Trigger a session established event. 481 * 482 * @param bestPt TODO javadoc me please 483 * payload type that has been agreed. 484 * @throws NotConnectedException if the XMPP connection is not connected. 485 * @throws InterruptedException if the calling thread was interrupted. 486 */ 487 protected void triggerMediaEstablished(PayloadType bestPt) throws NotConnectedException, InterruptedException { 488 List<JingleListener> listeners = getListenersList(); 489 for (JingleListener li : listeners) { 490 if (li instanceof JingleMediaListener) { 491 JingleMediaListener mli = (JingleMediaListener) li; 492 mli.mediaEstablished(bestPt); 493 } 494 } 495 } 496 497 /** 498 * Trigger a jmf closed event. 499 * 500 * @param currPt TODO javadoc me please 501 * current payload type that is cancelled. 502 */ 503 protected void triggerMediaClosed(PayloadType currPt) { 504 List<JingleListener> listeners = getListenersList(); 505 for (JingleListener li : listeners) { 506 if (li instanceof JingleMediaListener) { 507 JingleMediaListener mli = (JingleMediaListener) li; 508 mli.mediaClosed(currPt); 509 } 510 } 511 } 512 513 /** 514 * Called from above when starting a new session. 515 */ 516 @Override 517 protected void doStart() { 518 519 } 520 521 /** 522 * Terminate the jmf negotiator. 523 */ 524 @Override 525 public void close() { 526 super.close(); 527 triggerMediaClosed(getBestCommonAudioPt()); 528 } 529 530 /** 531 * Create a JingleDescription that matches this negotiator. 532 * 533 * @return the jingle description. 534 */ 535 public JingleDescription getJingleDescription() { 536 JingleDescription result = null; 537 PayloadType payloadType = getBestCommonAudioPt(); 538 if (payloadType != null) { 539 result = new JingleDescription.Audio(payloadType); 540 } else { 541 // If we haven't settled on a best payload type yet then just use the first one in our local list. 542 result = new JingleDescription.Audio(); 543 result.addAudioPayloadTypes(localAudioPts); 544 } 545 return result; 546 } 547}