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 ---&gt; portB
063 * </p>
064 * <p>
065 * Receiving
066 * portB ---&gt; 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}