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}