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