/* * SIP Communicator, the OpenSource Java VoIP and Instant Messaging client. * * Distributable under LGPL license. * See terms of license at gnu.org. */ package net.java.sip.communicator.impl.neomedia; import java.io.*; import java.util.*; import javax.media.*; import javax.media.protocol.*; import net.java.sip.communicator.impl.neomedia.device.*; import net.java.sip.communicator.service.neomedia.*; import net.java.sip.communicator.service.neomedia.MediaException; // disambiguation import net.java.sip.communicator.util.*; /** * The call recording implementation. * Provides the capability to start and stop call recording. * * @author Dmitri Melnikov * @author Lubomir Marinov */ public class RecorderImpl implements Recorder { /** * The list of formats in which RecorderImpl instances support * recording media. */ static final String[] SUPPORTED_FORMATS = new String[] { SoundFileUtils.aif, SoundFileUtils.au, SoundFileUtils.gsm, SoundFileUtils.mp3, SoundFileUtils.wav }; /** * The AudioMixerMediaDevice which is to be or which is already * being recorded by this Recorder. */ private final AudioMixerMediaDevice device; /** * The MediaDeviceSession is used to create an output data source. */ private MediaDeviceSession deviceSession; /** * The List of Recorder.Listeners interested in * notifications from this Recorder. */ private final List listeners = new ArrayList(); /** * DataSink used to save the output data. */ private DataSink sink; /** * Constructs the RecorderImpl with the provided session. * * @param device device that can create a session that provides the output * data source */ public RecorderImpl(AudioMixerMediaDevice device) { if (device == null) throw new NullPointerException("device"); this.device = device; } /** * Adds a new Recorder.Listener to the list of listeners interested * in notifications from this Recorder. * * @param listener the new Recorder.Listener to be added to the * list of listeners interested in notifications from this Recorder * @see Recorder#addListener(Recorder.Listener) */ public void addListener(Recorder.Listener listener) { if (listener == null) throw new NullPointerException("listener"); synchronized (listeners) { if (!listeners.contains(listener)) listeners.add(listener); } } /** * Returns a content descriptor to create a recording session with. * * @param format the format that corresponding to the content descriptor * @return content descriptor * @throws IllegalArgumentException if the specified format is not * a supported recording format */ private ContentDescriptor getContentDescriptor(String format) throws IllegalArgumentException { String type; if (SoundFileUtils.wav.equalsIgnoreCase(format)) type = FileTypeDescriptor.WAVE; else if (SoundFileUtils.mp3.equalsIgnoreCase(format)) type = FileTypeDescriptor.MPEG_AUDIO; else if (SoundFileUtils.gsm.equalsIgnoreCase(format)) type = FileTypeDescriptor.GSM; else if (SoundFileUtils.au.equalsIgnoreCase(format)) type = FileTypeDescriptor.BASIC_AUDIO; else if (SoundFileUtils.aif.equalsIgnoreCase(format)) type = FileTypeDescriptor.AIFF; else { throw new IllegalArgumentException( format + " is not a supported recording format."); } return new ContentDescriptor(type); } /** * Gets a list of the formats in which this Recorder supports * recording media. * * @return a List of the formats in which this Recorder * supports recording media * @see Recorder#getSupportedFormats() */ public List getSupportedFormats() { return Arrays.asList(SUPPORTED_FORMATS); } /** * Removes a existing Recorder.Listener from the list of listeners * interested in notifications from this Recorder. * * @param listener the existing Recorder.Listener to be removed * from the list of listeners interested in notifications from this * Recorder * @see Recorder#removeListener(Recorder.Listener) */ public void removeListener(Recorder.Listener listener) { if (listener != null) { synchronized (listeners) { listeners.remove(listener); } } } /** * Starts the recording of the media associated with this Recorder * (e.g. the media being sent and received in a Call) into a file * with a specific name. * * @param format the format into which the media associated with this * Recorder is to be recorded into the specified file * @param filename the name of the file into which the media associated with * this Recorder is to be recorded * @throws IOException if anything goes wrong with the input and/or output * performed by this Recorder * @throws MediaException if anything else goes wrong while starting the * recording of media performed by this Recorder * @see Recorder#start(String, String) */ public void start(String format, String filename) throws IOException, MediaException { if (this.sink == null) { if (format == null) throw new NullPointerException("format"); if (filename == null) throw new NullPointerException("filename"); /* * A file without an extension may not only turn out to be a touch * more difficult to play but is suspected to also cause an * exception inside of JMF. */ int extensionBeginIndex = filename.lastIndexOf('.'); if (extensionBeginIndex < 0) filename += '.' + format; else if (extensionBeginIndex == filename.length() - 1) filename += format; MediaDeviceSession deviceSession = device.createSession(); try { deviceSession.setContentDescriptor(getContentDescriptor(format)); /* * This RecorderImpl will use deviceSession to get a hold of the * media being set to the remote peers associated with the same * AudioMixerMediaDevice i.e. this RecorderImpl needs * deviceSession to only capture and not play back. */ deviceSession.start(MediaDirection.SENDONLY); this.deviceSession = deviceSession; } finally { if (this.deviceSession == null) { throw new MediaException( "Failed to create MediaDeviceSession from" + " AudioMixerMediaDevice for the purposes of" + " recording"); } } Throwable exception = null; try { DataSource outputDataSource = deviceSession.getOutputDataSource(); DataSink sink = Manager.createDataSink( outputDataSource, new MediaLocator("file:" + filename)); sink.open(); sink.start(); this.sink = sink; } catch (NoDataSinkException ndsex) { exception = ndsex; } finally { if ((this.sink == null) || (exception != null)) { stop(); throw new MediaException( "Failed to start recording into file " + filename, exception); } } } } /** * Stops the recording of the media associated with this Recorder * (e.g. the media being sent and received in a Call) if it has * been started and prepares this Recorder for garbage collection. * * @see Recorder#stop() */ public void stop() { if (deviceSession != null) { deviceSession.close(); deviceSession = null; } if (sink != null) { sink.close(); sink = null; /* * RecorderImpl creates the sink upon start() and it does it only if * it is null so this RecorderImpl has really stopped only if it has * managed to close() the (existing) sink. Notify the registered * listeners. */ Recorder.Listener[] listeners; synchronized (this.listeners) { listeners = this.listeners.toArray( new Recorder.Listener[this.listeners.size()]); } for (Recorder.Listener listener : listeners) listener.recorderStopped(this); } } }