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