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