001/** 002 * 003 * Copyright 2003-2006 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.mediaimpl.jmf; 018 019import java.io.IOException; 020import java.net.InetAddress; 021import java.net.UnknownHostException; 022import java.util.ArrayList; 023import java.util.List; 024import java.util.logging.Level; 025import java.util.logging.Logger; 026 027import javax.media.Codec; 028import javax.media.Controller; 029import javax.media.ControllerClosedEvent; 030import javax.media.ControllerEvent; 031import javax.media.ControllerListener; 032import javax.media.Format; 033import javax.media.MediaLocator; 034import javax.media.NoProcessorException; 035import javax.media.Processor; 036import javax.media.UnsupportedPlugInException; 037import javax.media.control.BufferControl; 038import javax.media.control.PacketSizeControl; 039import javax.media.control.TrackControl; 040import javax.media.format.AudioFormat; 041import javax.media.protocol.ContentDescriptor; 042import javax.media.protocol.DataSource; 043import javax.media.protocol.PushBufferDataSource; 044import javax.media.protocol.PushBufferStream; 045import javax.media.rtp.InvalidSessionAddressException; 046import javax.media.rtp.RTPManager; 047import javax.media.rtp.SendStream; 048import javax.media.rtp.SessionAddress; 049 050import org.jivesoftware.smackx.jingleold.media.JingleMediaSession; 051 052/** 053 * An Easy to use Audio Channel implemented using JMF. 054 * It sends and receives jmf for and from desired IPs and ports. 055 * Also has a rport Symetric behavior for better NAT Traversal. 056 * It send data from a defined port and receive data in the same port, making NAT binds easier. 057 * <p> 058 * Send from portA to portB and receive from portB in portA. 059 * </p> 060 * <p> 061 * Sending 062 * portA ---> portB 063 * </p> 064 * <p> 065 * Receiving 066 * portB ---> portA 067 * </p> 068 * <i>Transmit and Receive are interdependence. To receive you MUST transmit. </i> 069 * 070 * @author Thiago Camargo 071 */ 072public class AudioChannel { 073 074 private static final Logger LOGGER = Logger.getLogger(AudioChannel.class.getName()); 075 076 private MediaLocator locator; 077 private String localIpAddress; 078 private String remoteIpAddress; 079 private int localPort; 080 private int portBase; 081 private Format format; 082 083 private Processor processor = null; 084 private RTPManager[] rtpMgrs; 085 private DataSource dataOutput = null; 086 private AudioReceiver audioReceiver; 087 088 private List<SendStream> sendStreams = new ArrayList<>(); 089 090 private JingleMediaSession jingleMediaSession; 091 092 private boolean started = false; 093 094 /** 095 * Creates an Audio Channel for a desired jmf locator. For instance: new MediaLocator("dsound://") 096 * 097 * @param locator media locator 098 * @param localIpAddress local IP address 099 * @param remoteIpAddress remote IP address 100 * @param localPort local port number 101 * @param remotePort remote port number 102 * @param format audio format 103 * @param jingleMediaSession the jingle media session. 104 */ 105 public AudioChannel(MediaLocator locator, 106 String localIpAddress, 107 String remoteIpAddress, 108 int localPort, 109 int remotePort, 110 Format format, JingleMediaSession jingleMediaSession) { 111 112 this.locator = locator; 113 this.localIpAddress = localIpAddress; 114 this.remoteIpAddress = remoteIpAddress; 115 this.localPort = localPort; 116 this.portBase = remotePort; 117 this.format = format; 118 this.jingleMediaSession = jingleMediaSession; 119 } 120 121 /** 122 * Starts the transmission. Returns null if transmission started ok. 123 * Otherwise it returns a string with the reason why the setup failed. 124 * Starts receive also. 125 * 126 * @return result description 127 */ 128 public synchronized String start() { 129 if (started) return null; 130 131 // Create a processor for the specified jmf locator 132 String result = createProcessor(); 133 if (result != null) { 134 started = false; 135 } 136 137 // Create an RTP session to transmit the output of the 138 // processor to the specified IP address and port no. 139 result = createTransmitter(); 140 if (result != null) { 141 processor.close(); 142 processor = null; 143 started = false; 144 } 145 else { 146 started = true; 147 } 148 149 // Start the transmission 150 processor.start(); 151 152 return null; 153 } 154 155 /** 156 * Stops the transmission if already started. 157 * Stops the receiver also. 158 */ 159 public void stop() { 160 if (!started) return; 161 synchronized (this) { 162 try { 163 started = false; 164 if (processor != null) { 165 processor.stop(); 166 processor = null; 167 168 for (RTPManager rtpMgr : rtpMgrs) { 169 rtpMgr.removeReceiveStreamListener(audioReceiver); 170 rtpMgr.removeSessionListener(audioReceiver); 171 rtpMgr.removeTargets("Session ended."); 172 rtpMgr.dispose(); 173 } 174 175 sendStreams.clear(); 176 177 } 178 } 179 catch (Exception e) { 180 LOGGER.log(Level.WARNING, "exception", e); 181 } 182 } 183 } 184 185 private String createProcessor() { 186 if (locator == null) 187 return "Locator is null"; 188 189 DataSource ds; 190 191 try { 192 ds = javax.media.Manager.createDataSource(locator); 193 } 194 catch (Exception e) { 195 // Try JavaSound Locator as a last resort 196 try { 197 ds = javax.media.Manager.createDataSource(new MediaLocator("javasound://")); 198 } 199 catch (Exception ee) { 200 return "Couldn't create DataSource"; 201 } 202 } 203 204 // Try to create a processor to handle the input jmf locator 205 try { 206 processor = javax.media.Manager.createProcessor(ds); 207 } 208 catch (NoProcessorException npe) { 209 LOGGER.log(Level.WARNING, "exception", npe); 210 return "Couldn't create processor"; 211 } 212 catch (IOException ioe) { 213 LOGGER.log(Level.WARNING, "exception", ioe); 214 return "IOException creating processor"; 215 } 216 217 // Wait for it to configure 218 boolean result = waitForState(processor, Processor.Configured); 219 if (!result) { 220 return "Couldn't configure processor"; 221 } 222 223 // Get the tracks from the processor 224 TrackControl[] tracks = processor.getTrackControls(); 225 226 // Do we have at least one track? 227 if (tracks == null || tracks.length < 1) { 228 return "Couldn't find tracks in processor"; 229 } 230 231 // Set the output content descriptor to RAW_RTP 232 // This will limit the supported formats reported from 233 // Track.getSupportedFormats to only valid RTP formats. 234 ContentDescriptor cd = new ContentDescriptor(ContentDescriptor.RAW_RTP); 235 processor.setContentDescriptor(cd); 236 237 Format[] supported; 238 Format chosen = null; 239 boolean atLeastOneTrack = false; 240 241 // Program the tracks. 242 for (int i = 0; i < tracks.length; i++) { 243 if (tracks[i].isEnabled()) { 244 245 supported = tracks[i].getSupportedFormats(); 246 247 if (supported.length > 0) { 248 for (Format format : supported) { 249 if (format instanceof AudioFormat) { 250 if (this.format.matches(format)) 251 chosen = format; 252 } 253 } 254 if (chosen != null) { 255 tracks[i].setFormat(chosen); 256 LOGGER.severe("Track " + i + " is set to transmit as: " + chosen); 257 258 if (tracks[i].getFormat() instanceof AudioFormat) { 259 int packetRate = 20; 260 PacketSizeControl pktCtrl = (PacketSizeControl) processor.getControl(PacketSizeControl.class.getName()); 261 if (pktCtrl != null) { 262 try { 263 pktCtrl.setPacketSize(getPacketSize(tracks[i].getFormat(), packetRate)); 264 } 265 catch (IllegalArgumentException e) { 266 pktCtrl.setPacketSize(80); 267 // Do nothing 268 } 269 } 270 271 if (tracks[i].getFormat().getEncoding().equals(AudioFormat.ULAW_RTP)) { 272 Codec[] codec = new Codec[3]; 273 274 codec[0] = new com.ibm.media.codec.audio.rc.RCModule(); 275 codec[1] = new com.ibm.media.codec.audio.ulaw.JavaEncoder(); 276 codec[2] = new com.sun.media.codec.audio.ulaw.Packetizer(); 277 ((com.sun.media.codec.audio.ulaw.Packetizer) codec 278 [2]).setPacketSize(160); 279 280 try { 281 tracks[i].setCodecChain(codec); 282 } 283 catch (UnsupportedPlugInException e) { 284 LOGGER.log(Level.WARNING, "exception", e); 285 } 286 } 287 288 } 289 290 atLeastOneTrack = true; 291 } 292 else 293 tracks[i].setEnabled(false); 294 } 295 else 296 tracks[i].setEnabled(false); 297 } 298 } 299 300 if (!atLeastOneTrack) 301 return "Couldn't set any of the tracks to a valid RTP format"; 302 303 result = waitForState(processor, Controller.Realized); 304 if (!result) 305 return "Couldn't realize processor"; 306 307 // Get the output data source of the processor 308 dataOutput = processor.getDataOutput(); 309 310 return null; 311 } 312 313 /** 314 * Get the best stanza size for a given codec and a codec rate 315 * 316 * @param codecFormat TODO javadoc me please 317 * @param milliseconds TODO javadoc me please 318 * @return the best stanza size 319 * @throws IllegalArgumentException if an illegal argument was given. 320 */ 321 private static int getPacketSize(Format codecFormat, int milliseconds) throws IllegalArgumentException { 322 String encoding = codecFormat.getEncoding(); 323 if (encoding.equalsIgnoreCase(AudioFormat.GSM) || 324 encoding.equalsIgnoreCase(AudioFormat.GSM_RTP)) { 325 return milliseconds * 4; // 1 byte per millisecond 326 } 327 else if (encoding.equalsIgnoreCase(AudioFormat.ULAW) || 328 encoding.equalsIgnoreCase(AudioFormat.ULAW_RTP)) { 329 return milliseconds * 8; 330 } 331 else { 332 throw new IllegalArgumentException("Unknown codec type"); 333 } 334 } 335 336 /** 337 * Use the RTPManager API to create sessions for each jmf 338 * track of the processor. 339 * 340 * @return description TODO javadoc me please 341 */ 342 private String createTransmitter() { 343 344 // Cheated. Should have checked the type. 345 PushBufferDataSource pbds = (PushBufferDataSource) dataOutput; 346 PushBufferStream[] pbss = pbds.getStreams(); 347 348 rtpMgrs = new RTPManager[pbss.length]; 349 SessionAddress localAddr, destAddr; 350 InetAddress ipAddr; 351 SendStream sendStream; 352 audioReceiver = new AudioReceiver(this, jingleMediaSession); 353 int port; 354 355 for (int i = 0; i < pbss.length; i++) { 356 try { 357 rtpMgrs[i] = RTPManager.newInstance(); 358 359 port = portBase + 2 * i; 360 ipAddr = InetAddress.getByName(remoteIpAddress); 361 362 localAddr = new SessionAddress(InetAddress.getByName(this.localIpAddress), 363 localPort); 364 365 destAddr = new SessionAddress(ipAddr, port); 366 367 rtpMgrs[i].addReceiveStreamListener(audioReceiver); 368 rtpMgrs[i].addSessionListener(audioReceiver); 369 370 BufferControl bc = (BufferControl) rtpMgrs[i].getControl("javax.media.control.BufferControl"); 371 if (bc != null) { 372 int bl = 160; 373 bc.setBufferLength(bl); 374 } 375 376 try { 377 378 rtpMgrs[i].initialize(localAddr); 379 380 } 381 catch (InvalidSessionAddressException e) { 382 // In case the local address is not allowed to read, we user another local address 383 SessionAddress sessAddr = new SessionAddress(); 384 localAddr = new SessionAddress(sessAddr.getDataAddress(), 385 localPort); 386 rtpMgrs[i].initialize(localAddr); 387 } 388 389 rtpMgrs[i].addTarget(destAddr); 390 391 LOGGER.severe("Created RTP session at " + localPort + " to: " + remoteIpAddress + " " + port); 392 393 sendStream = rtpMgrs[i].createSendStream(dataOutput, i); 394 395 sendStreams.add(sendStream); 396 397 sendStream.start(); 398 399 } 400 catch (Exception e) { 401 LOGGER.log(Level.WARNING, "exception", e); 402 return e.getMessage(); 403 } 404 } 405 406 return null; 407 } 408 409 /** 410 * Set transmit activity. If the active is true, the instance should transmit. 411 * If it is set to false, the instance should pause transmit. 412 * 413 * @param active active state 414 */ 415 public void setTrasmit(boolean active) { 416 for (SendStream sendStream : sendStreams) { 417 try { 418 if (active) { 419 sendStream.start(); 420 LOGGER.fine("START"); 421 } 422 else { 423 sendStream.stop(); 424 LOGGER.fine("STOP"); 425 } 426 } 427 catch (IOException e) { 428 LOGGER.log(Level.WARNING, "exception", e); 429 } 430 431 } 432 } 433 434 /** 435 * ************************************************************* 436 * Convenience methods to handle processor's state changes. 437 * ************************************************************** 438 */ 439 440 private Integer stateLock = 0; 441 private boolean failed = false; 442 443 Integer getStateLock() { 444 return stateLock; 445 } 446 447 void setFailed() { 448 failed = true; 449 } 450 451 @SuppressWarnings("LockOnBoxedPrimitive") 452 private synchronized boolean waitForState(Processor p, int state) { 453 p.addControllerListener(new StateListener()); 454 failed = false; 455 456 // Call the required method on the processor 457 if (state == Processor.Configured) { 458 p.configure(); 459 } 460 else if (state == Processor.Realized) { 461 p.realize(); 462 } 463 464 // Wait until we get an event that confirms the 465 // success of the method, or a failure event. 466 // See StateListener inner class 467 while (p.getState() < state && !failed) { 468 synchronized (getStateLock()) { 469 try { 470 getStateLock().wait(); 471 } 472 catch (InterruptedException ie) { 473 return false; 474 } 475 } 476 } 477 478 return !failed; 479 } 480 481 /** 482 * ************************************************************* 483 * Inner Classes 484 * ************************************************************** 485 */ 486 487 class StateListener implements ControllerListener { 488 489 @SuppressWarnings("LockOnBoxedPrimitive") 490 @Override 491 public void controllerUpdate(ControllerEvent ce) { 492 493 // If there was an error during configure or 494 // realize, the processor will be closed 495 if (ce instanceof ControllerClosedEvent) 496 setFailed(); 497 498 // All controller events, send a notification 499 // to the waiting thread in waitForState method. 500 if (ce != null) { 501 synchronized (getStateLock()) { 502 getStateLock().notifyAll(); 503 } 504 } 505 } 506 } 507 508 public static void main(String[] args) { 509 510 InetAddress localhost; 511 try { 512 localhost = InetAddress.getLocalHost(); 513 514 AudioChannel audioChannel0 = new AudioChannel(new MediaLocator("javasound://8000"), localhost.getHostAddress(), localhost.getHostAddress(), 7002, 7020, new AudioFormat(AudioFormat.GSM_RTP), null); 515 AudioChannel audioChannel1 = new AudioChannel(new MediaLocator("javasound://8000"), localhost.getHostAddress(), localhost.getHostAddress(), 7020, 7002, new AudioFormat(AudioFormat.GSM_RTP), null); 516 517 audioChannel0.start(); 518 audioChannel1.start(); 519 520 try { 521 Thread.sleep(5000); 522 } 523 catch (InterruptedException e) { 524 LOGGER.log(Level.WARNING, "exception", e); 525 } 526 527 audioChannel0.setTrasmit(false); 528 audioChannel1.setTrasmit(false); 529 530 try { 531 Thread.sleep(5000); 532 } 533 catch (InterruptedException e) { 534 LOGGER.log(Level.WARNING, "exception", e); 535 } 536 537 audioChannel0.setTrasmit(true); 538 audioChannel1.setTrasmit(true); 539 540 try { 541 Thread.sleep(5000); 542 } 543 catch (InterruptedException e) { 544 LOGGER.log(Level.WARNING, "exception", e); 545 } 546 547 audioChannel0.stop(); 548 audioChannel1.stop(); 549 550 } 551 catch (UnknownHostException e) { 552 LOGGER.log(Level.WARNING, "exception", e); 553 } 554 555 } 556}