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}