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