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