diff --git a/src/org/jitsi/impl/neomedia/device/AudioSystem.java b/src/org/jitsi/impl/neomedia/device/AudioSystem.java index b60e21d233c2a49f0adf7187118fc8642e58d20c..70d1bfd5f34b9509055500c0f326c58d56b6953d 100644 --- a/src/org/jitsi/impl/neomedia/device/AudioSystem.java +++ b/src/org/jitsi/impl/neomedia/device/AudioSystem.java @@ -6,15 +6,10 @@ */ package org.jitsi.impl.neomedia.device; -import java.io.*; import java.util.*; import javax.media.*; -import javax.media.format.*; -import org.jitsi.impl.neomedia.*; -import org.jitsi.service.configuration.*; -import org.jitsi.service.libjitsi.*; import org.jitsi.service.neomedia.*; public abstract class AudioSystem diff --git a/src/org/jitsi/impl/neomedia/notify/AudioNotifierServiceImpl.java b/src/org/jitsi/impl/neomedia/notify/AudioNotifierServiceImpl.java index f2a79c283805d250bb596c9f955326c28de663c9..a4131ddfee8ce0590a692837749f4b4e517be293 100644 --- a/src/org/jitsi/impl/neomedia/notify/AudioNotifierServiceImpl.java +++ b/src/org/jitsi/impl/neomedia/notify/AudioNotifierServiceImpl.java @@ -9,6 +9,7 @@ import java.beans.*; import java.net.*; import java.util.*; +import java.util.concurrent.*; import javax.media.*; @@ -19,30 +20,40 @@ import org.jitsi.service.resources.*; /** - * The implementation of the AudioNotifierService. + * The implementation of <tt>AudioNotifierService</tt>. * * @author Yana Stamcheva + * @author Lyubomir Marinov */ public class AudioNotifierServiceImpl implements AudioNotifierService, PropertyChangeListener { /** - * Map of different audio clips. + * The cache of <tt>SCAudioClip</tt> instances which we may reuse. The reuse + * is complex because a <tt>SCAudioClip</tt> may be used by a single user at + * a time. */ - private static final Map<AudioClipsKey, AbstractSCAudioClip> audioClips - = new HashMap<AudioClipsKey, AbstractSCAudioClip>(); + private Map<AudioKey, SCAudioClip> audios; /** - * If the sound is currently disabled. + * The <tt>Object</tt> which synchronizes the access to {@link #audios}. */ - private boolean isMute; + private final Object audiosSyncRoot = new Object(); /** - * Device config to look for notify device. + * The <tt>DeviceConfiguration</tt> which provides information about the + * notify and playback devices on which this instance plays + * <tt>SCAudioClip</tt>s. */ private final DeviceConfiguration deviceConfiguration; + /** + * The indicator which determined whether <tt>SCAudioClip</tt>s are to be + * played by this instance. + */ + private boolean mute; + /** * Initializes a new <tt>AudioNotifierServiceImpl</tt> instance. */ @@ -53,7 +64,26 @@ public AudioNotifierServiceImpl() .getMediaServiceImpl() .getDeviceConfiguration(); - deviceConfiguration.addPropertyChangeListener(this); + this.deviceConfiguration.addPropertyChangeListener(this); + } + + /** + * Checks whether the playback and notification configuration + * share the same device. + * @return are audio out and notifications using the same device. + */ + public boolean audioOutAndNotificationsShareSameDevice() + { + AudioSystem audioSystem = getDeviceConfiguration().getAudioSystem(); + CaptureDeviceInfo notify + = audioSystem.getDevice(AudioSystem.NOTIFY_INDEX); + CaptureDeviceInfo playback + = audioSystem.getDevice(AudioSystem.PLAYBACK_INDEX); + + return + (notify == null) + ? (playback == null) + : notify.getLocator().equals(playback.getLocator()); } /** @@ -63,7 +93,7 @@ public AudioNotifierServiceImpl() * @param uri the path where the audio file could be found * @return a newly created <tt>SCAudioClip</tt> from <tt>uri</tt> */ - public AbstractSCAudioClip createAudio(String uri) + public SCAudioClip createAudio(String uri) { return createAudio(uri, false); } @@ -76,18 +106,23 @@ public AbstractSCAudioClip createAudio(String uri) * @param playback use or not the playback device. * @return a newly created <tt>SCAudioClip</tt> from <tt>uri</tt> */ - public AbstractSCAudioClip createAudio(String uri, boolean playback) + public SCAudioClip createAudio(String uri, boolean playback) { - AbstractSCAudioClip audioClip; + SCAudioClip audio; - synchronized (audioClips) + synchronized (audiosSyncRoot) { - AudioClipsKey key = new AudioClipsKey(uri, playback); - if(audioClips.containsKey(key)) - { - audioClip = audioClips.get(key); - } - else + final AudioKey key = new AudioKey(uri, playback); + + /* + * While we want to reuse the SCAudioClip instances, they may be + * used by a single user at a time. That's why we'll forget about + * them while they are in use and we'll reclaim them when they are + * no longer in use. + */ + audio = (audios == null) ? null : audios.remove(key); + + if (audio == null) { ResourceManagementService resources = LibJitsi.getResourceManagementService(); @@ -105,7 +140,6 @@ public AbstractSCAudioClip createAudio(String uri, boolean playback) } catch (MalformedURLException e) { - //logger.error("The given uri could not be parsed.", e); return null; } } @@ -116,16 +150,21 @@ public AbstractSCAudioClip createAudio(String uri, boolean playback) = getDeviceConfiguration().getAudioSystem(); if (audioSystem == null) - audioClip = new JavaSoundClipImpl(url, this); + { + audio = new JavaSoundClipImpl(url, this); + } else if (NoneAudioSystem.LOCATOR_PROTOCOL.equalsIgnoreCase( audioSystem.getLocatorProtocol())) - audioClip = null; + { + audio = null; + } else { - audioClip + audio = new AudioSystemClipImpl( url, - this, audioSystem, + this, + audioSystem, playback); } } @@ -133,62 +172,85 @@ else if (NoneAudioSystem.LOCATOR_PROTOCOL.equalsIgnoreCase( { if (t instanceof ThreadDeath) throw (ThreadDeath) t; - // Cannot create audio to play - return null; + else + { + /* + * Could not initialize a new SCAudioClip instance to be + * played. + */ + return null; + } } - - audioClips.put(key, audioClip); } - } - - return audioClip; - } - /** - * Removes the given audio from the list of available audio clips. - * - * @param audioClip the audio to destroy - */ - public void destroyAudio(SCAudioClip audioClip) - { - synchronized (audioClips) - { - AudioClipsKey keyToRemove = null; - for(Map.Entry<AudioClipsKey, AbstractSCAudioClip> entry - : audioClips.entrySet()) + /* + * Make sure that the SCAudioClip will be reclaimed for reuse when + * it is no longer in use. + */ + if (audio != null) { - if(entry.getValue().equals(audioClip)) - { - keyToRemove = entry.getKey(); - break; - } + if (audios == null) + audios = new HashMap<AudioKey, SCAudioClip>(); + + /* + * We have to return in the Map which was active at the time the + * SCAudioClip was initialized because it may have become + * invalid if the playback or notify audio device changed. + */ + final Map<AudioKey, SCAudioClip> finalAudios = audios; + final SCAudioClip finalAudio = audio; + + audio + = new SCAudioClip() + { + @Override + protected void finalize() + throws Throwable + { + try + { + synchronized (audios) + { + finalAudios.put(key, finalAudio); + } + } + finally + { + super.finalize(); + } + } + + public void play() + { + finalAudio.play(); + } + + public void play( + int loopInterval, + Callable<Boolean> loopCondition) + { + finalAudio.play(loopInterval, loopCondition); + } + + public void stop() + { + finalAudio.stop(); + } + }; } - - audioClips.remove(keyToRemove); } + + return audio; } /** - * Enables or disables the sound in the application. If FALSE, we try to - * restore all looping sounds if any. + * The device configuration. * - * @param isMute when TRUE disables the sound, otherwise enables the sound. + * @return the deviceConfiguration */ - public void setMute(boolean isMute) + public DeviceConfiguration getDeviceConfiguration() { - this.isMute = isMute; - - for (AbstractSCAudioClip audioClip : audioClips.values()) - { - if (isMute) - { - audioClip.internalStop(); - } - else if (audioClip.isLooping()) - { - // TODO Auto-generated method stub - } - } + return deviceConfiguration; } /** @@ -197,98 +259,96 @@ else if (audioClip.isLooping()) */ public boolean isMute() { - return isMute; - } - - /** - * The device configuration. - * - * @return the deviceConfiguration - */ - public DeviceConfiguration getDeviceConfiguration() - { - return deviceConfiguration; + return mute; } /** * Listens for changes in notify device - * @param event the event that notify device has changed. + * @param ev the event that notify device has changed. */ - public void propertyChange(PropertyChangeEvent event) + public void propertyChange(PropertyChangeEvent ev) { - if (DeviceConfiguration.AUDIO_NOTIFY_DEVICE.equals( - event.getPropertyName())) + String propertyName = ev.getPropertyName(); + + if (DeviceConfiguration.AUDIO_NOTIFY_DEVICE.equals(propertyName) + || DeviceConfiguration.AUDIO_PLAYBACK_DEVICE.equals( + propertyName)) { - audioClips.clear(); + synchronized (audiosSyncRoot) + { + /* + * Make sure that the currently referenced SCAudioClips will not + * be reclaimed. + */ + audios = null; + } } } /** - * Checks whether the playback and notification configuration - * share the same device. - * @return are audio out and notifications using the same device. + * Enables or disables the sound in the application. If FALSE, we try to + * restore all looping sounds if any. + * + * @param mute when TRUE disables the sound, otherwise enables the sound. */ - public boolean audioOutAndNotificationsShareSameDevice() + public void setMute(boolean mute) { - CaptureDeviceInfo notifyInfo = - getDeviceConfiguration().getAudioSystem() - .getDevice(AudioSystem.NOTIFY_INDEX); - CaptureDeviceInfo playbackInfo = - getDeviceConfiguration().getAudioSystem() - .getDevice(AudioSystem.PLAYBACK_INDEX); - - if(notifyInfo != null && playbackInfo != null) - { - return notifyInfo.getLocator().equals(playbackInfo.getLocator()); - } + this.mute = mute; - return false; + // TODO Auto-generated method stub } /** - * Key for clips. + * Implements the key of {@link AudioNotifierServiceImpl#audios}. Combines the + * <tt>uri</tt> of the <tt>SCAudioClip</tt> with the indicator which + * determines whether the <tt>SCAudioClip</tt> in question uses the playback + * or the notify audio device. */ - private static class AudioClipsKey + private static class AudioKey { /** - * The uri. + * Is it playback? */ - private String uri; + private final boolean playback; /** - * Is it playback. + * The uri. */ - private boolean isPlayback; + final String uri; /** - * Constructs key. - * @param uri by uri - * @param playback and playback + * Initializes a new <tt>AudioKey</tt> instance. + * + * @param uri + * @param playback */ - private AudioClipsKey(String uri, boolean playback) + private AudioKey(String uri, boolean playback) { this.uri = uri; - isPlayback = playback; + this.playback = playback; } @Override public boolean equals(Object o) { - AudioClipsKey that = (AudioClipsKey) o; - - if(isPlayback != that.isPlayback) - return false; - if(uri != null ? !uri.equals(that.uri) : that.uri != null) + if (o == this) + return true; + if (o == null) return false; - return true; + AudioKey that = (AudioKey) o; + + return + (playback == that.playback) + && ((uri == null) + ? (that.uri == null) + : uri.equals(that.uri)); } @Override public int hashCode() { - return (uri != null ? uri.hashCode() : 0) - + (isPlayback ? 1 : 0); + return ((uri == null) ? 0 : uri.hashCode()) + (playback ? 1 : 0); } } } diff --git a/src/org/jitsi/impl/neomedia/notify/JavaSoundClipImpl.java b/src/org/jitsi/impl/neomedia/notify/JavaSoundClipImpl.java index 2d42a48b25b21a5fd5b89da9f0514258d7771714..dc67a062032b6fa0356a055347c24185433248c5 100644 --- a/src/org/jitsi/impl/neomedia/notify/JavaSoundClipImpl.java +++ b/src/org/jitsi/impl/neomedia/notify/JavaSoundClipImpl.java @@ -97,12 +97,17 @@ public Constructor<AudioClip> run() private final AudioClip audioClip; /** - * Creates the audio clip and initialize the listener used from the - * loop timer. + * Initializes a new <tt>JavaSoundClipImpl</tt> instance which is to play + * audio stored at a specific <tt>URL</tt> using + * <tt>java.applet.AudioClip</tt>. * - * @param url the url pointing to the audio file - * @param audioNotifier the audio notify service - * @throws IOException cannot audio clip with supplied url. + * @param url the <tt>URL</tt> at which the audio is stored and which the + * new instance is to load + * @param audioNotifier the <tt>AudioNotifierService</tt> which is + * initializing the new instance and whose <tt>mute</tt> property/state is + * to be monitored by the new instance + * @throws IOException if a <tt>java.applet.AudioClip</tt> could not be + * initialized or the audio at the specified <tt>url</tt> could not be read */ public JavaSoundClipImpl(URL url, AudioNotifierService audioNotifier) throws IOException @@ -114,9 +119,11 @@ public JavaSoundClipImpl(URL url, AudioNotifierService audioNotifier) /** * {@inheritDoc} + * + * Stops the <tt>java.applet.AudioClip</tt> wrapped by this instance. */ @Override - public void internalStop() + protected void internalStop() { try { @@ -131,6 +138,8 @@ public void internalStop() /** * {@inheritDoc} + * + * Plays the <tt>java.applet.AudioClip</tt> wrapped by this instance. */ protected boolean runOnceInPlayThread() { diff --git a/src/org/jitsi/service/audionotifier/AbstractSCAudioClip.java b/src/org/jitsi/service/audionotifier/AbstractSCAudioClip.java index 161dea63851802e0bf1df3102d99b18dcf84f55b..b2d48e0e6406c2a2ca1eb2277ca08a98031b547d 100644 --- a/src/org/jitsi/service/audionotifier/AbstractSCAudioClip.java +++ b/src/org/jitsi/service/audionotifier/AbstractSCAudioClip.java @@ -10,7 +10,10 @@ import java.util.concurrent.*; /** - * An abstract base implementation of {@link SCAudioClip}. + * An abstract base implementation of {@link SCAudioClip} which is provided in + * order to aid implementers by allowing them to extend + * <tt>AbstractSCAudioClip</tt> and focus on the task of playing actual audio + * once. * * @author Damian Minkov * @author Lyubomir Marinov @@ -18,18 +21,59 @@ public abstract class AbstractSCAudioClip implements SCAudioClip { + /** + * The thread pool used by the <tt>AbstractSCAudioClip</tt> instances in + * order to reduce the impact of thread creation/initialization. + */ + private static ExecutorService executorService; + + /** + * The <tt>AudioNotifierService</tt> which has initialized this instance. + * <tt>AbstractSCAudioClip</tt> monitors its <tt>mute</tt> property/state in + * order to silence the played audio as appropriate/necessary. + */ protected final AudioNotifierService audioNotifier; - private boolean isInvalid; + private Runnable command; - private boolean isLooping; + /** + * The indicator which determines whether this instance was marked invalid. + */ + private boolean invalid; + + /** + * The indicator which determines whether this instance plays the audio it + * represents in a loop. + */ + private boolean looping; + /** + * The interval of time in milliseconds between consecutive plays of this + * audio in a loop. If negative, this audio is played once only. If + * non-negative, this audio may still be played once only if the + * <tt>loopCondition</tt> specified to {@link #play(int, Callable)} is + * <tt>null</tt> or its invocation fails. + */ private int loopInterval; - private boolean started = false; - + /** + * The indicator which determines whether the playback of this audio is + * started. + */ + private boolean started; + + /** + * The <tt>Object</tt> used for internal synchronization purposes which + * arise because this instance does the actual playback of audio in a + * separate thread. + */ private final Object sync = new Object(); + /** + * The <tt>URL</tt> of the audio to be played by this instance. + * <tt>AbstractSCAudioClip</tt> does not use it and just remembers it in + * order to make it available to extenders. + */ protected final URL url; protected AbstractSCAudioClip( @@ -40,29 +84,62 @@ protected AbstractSCAudioClip( this.audioNotifier = audioNotifier; } + /** + * Notifies this instance that its execution in its background/separate + * thread dedicated to the playback of this audio is about to start playing + * this audio for the first time. Regardless of whether this instance is to + * be played once or multiple times in a loop, the method is called once in + * order to allow extenders/implementers to perform one-time initialization + * before this audio starts playing. The <tt>AbstractSCAudioClip</tt> + * implementation does nothing. + */ protected void enterRunInPlayThread() { - // TODO Auto-generated method stub } + /** + * Notifies this instance that its execution in its background/separate + * thread dedicated to the playback of this audio is about the start playing + * this audio once. If this audio is to be played in a loop, the method is + * invoked at the beginning of each iteration of the loop. Allows + * extenders/implementers to perform per-loop iteration initialization. The + * <tt>AbstractSCAudioClip</tt> implementation does nothing. + */ protected void enterRunOnceInPlayThread() { - // TODO Auto-generated method stub } + /** + * Notifies this instance that its execution in its background/separate + * thread dedicated to the playback of this audio is about to stop playing + * this audio once. Regardless of whether this instance is to be played once + * or multiple times in a loop, the method is called once in order to allow + * extenders/implementers to perform one-time cleanup after this audio stops + * playing. The <tt>AbstractSCAudioClip</tt> implementation does nothing. + */ protected void exitRunInPlayThread() { - // TODO Auto-generated method stub } + /** + * Notifies this instance that its execution in its background/separate + * thread dedicated to the playback of this audio is about to stop playing + * this audio. If this audio is to be played in a loop, the method is called + * at the end of each iteration of the loop. Allows extenders/implementers + * to perform per-loop iteraction cleanup. The <tt>AbstractSCAudioClip</tt> + * implementation does nothing. + */ protected void exitRunOnceInPlayThread() { - // TODO Auto-generated method stub } /** - * Returns the loop interval if this audio is looping. - * @return the loop interval if this audio is looping + * Gets the interval of time in milliseconds between consecutive plays of + * this audio. + * + * @return the interval of time in milliseconds between consecutive plays of + * this audio. If negative, this audio will not be played in a loop and will + * be played once only. */ public int getLoopInterval() { @@ -75,44 +152,84 @@ public int getLoopInterval() * when setMute(true) is invoked. This allows us to restore all looping * audios when the sound is restored by calling setMute(false). */ - public void internalStop() + protected void internalStop() { + boolean interrupted = false; + synchronized (sync) { - if (url != null && started) + started = false; + sync.notifyAll(); + + while (command != null) { - started = false; - sync.notifyAll(); + try + { + /* + * Technically, we do not need a timeout. If a notifyAll() + * is not called to wake us up, then we will likely already + * be in trouble. Anyway, use a timeout just in case. + */ + sync.wait(500); + } + catch (InterruptedException ie) + { + interrupted = true; + } } } + + if (interrupted) + Thread.currentThread().interrupt(); } /** - * Returns TRUE if this audio is invalid, FALSE otherwise. + * Determines whether this instance is invalid. <tt>AbstractSCAudioClip</tt> + * does not use the <tt>invalid</tt> property/state of this instance and + * merely remembers the value which was set on it by + * {@link #setInvalid(boolean)}. The default value is <tt>false</tt> i.e. + * this instance is valid by default. * - * @return TRUE if this audio is invalid, FALSE otherwise + * @return <tt>true</tt> if this instance is invalid; otherwise, + * <tt>false</tt> */ public boolean isInvalid() { - return isInvalid; + return invalid; } /** - * Returns TRUE if this audio is currently playing in loop, FALSE otherwise. - * @return TRUE if this audio is currently playing in loop, FALSE otherwise. + * Determines whether this instance plays the audio it represents in a loop. + * + * @param <tt>true</tt> if this instance plays the audio it represents in a + * loop; <tt>false</tt>, otherwise */ public boolean isLooping() { - return isLooping; + return looping; } + /** + * Determines whether this audio is started i.e. a <tt>play</tt> method was + * invoked and no subsequent <tt>stop</tt> has been invoked yet. + * + * @return <tt>true</tt> if this audio is started; otherwise, <tt>false</tt> + */ protected boolean isStarted() { - return started; + synchronized (sync) + { + return started; + } } /** * {@inheritDoc} + * + * Delegates to {@link #play(int, Callable)} with <tt>loopInterval</tt> + * <tt>-1</tt> and <tt>loopCondition</tt> <tt>null</tt> in order to conform + * with the contract for the behavior of this method specified by the + * interface <tt>SCAudioClip</tt>. */ public void play() { @@ -127,101 +244,220 @@ public void play(int loopInterval, final Callable<Boolean> loopCondition) if ((loopInterval >= 0) && (loopCondition == null)) loopInterval = -1; - setLoopInterval(loopInterval); - setIsLooping(loopInterval >= 0); - - if ((url != null) && !audioNotifier.isMute()) + synchronized (sync) { - started = true; - new Thread() + if (command != null) + return; + + setLoopInterval(loopInterval); + setLooping(loopInterval >= 0); + + /* + * We use a thread pool shared among all AbstractSCAudioClip + * instances in order to reduce the impact of thread + * creation/initialization. + */ + ExecutorService executorService; + + synchronized (AbstractSCAudioClip.class) + { + if (AbstractSCAudioClip.executorService == null) + { + AbstractSCAudioClip.executorService + = Executors.newCachedThreadPool(); + } + executorService = AbstractSCAudioClip.executorService; + } + + try + { + started = false; + command + = new Runnable() { - @Override public void run() { - runInPlayThread(loopCondition); + try + { + synchronized (sync) + { + /* + * We have to wait for + * play(int,Callable<Boolean>) to let go of + * sync i.e. be ready with setting up the + * whole AbstractSCAudioClip state; + * otherwise, this Runnable will most likely + * prematurely seize to exist. + */ + if (!equals(command)) + return; + } + + runInPlayThread(loopCondition); + } + finally + { + synchronized (sync) + { + if (equals(command)) + { + command = null; + started = false; + sync.notifyAll(); + } + } + } } - }.start(); + }; + executorService.execute(command); + started = true; + } + finally + { + if (!started) + command = null; + sync.notifyAll(); + } } } + /** + * Runs in a background/separate thread dedicated to the actual playback of + * this audio and plays this audio once or in a loop. + * + * @param loopCondition a <tt>Callback<Boolean></tt> which represents + * the condition on which this audio will play more than once. If + * <tt>null</tt>, this audio will play once only. If an invocation of + * <tt>loopCondition</tt> throws a <tt>Throwable</tt>, this audio will + * discontinue playing. + */ private void runInPlayThread(Callable<Boolean> loopCondition) { enterRunInPlayThread(); try { - while (started) + boolean interrupted = false; + + while (isStarted()) { - enterRunOnceInPlayThread(); - try + if (audioNotifier.isMute()) { - if (!runOnceInPlayThread()) - break; + /* + * If the AudioNotifierService has muted the sounds, we will + * have to really wait a bit in order to not fall into a + * busy wait. + */ + synchronized (sync) + { + try + { + sync.wait(500); + } + catch (InterruptedException ie) + { + interrupted = true; + } + } } - finally + else { - exitRunOnceInPlayThread(); + enterRunOnceInPlayThread(); + try + { + if (!runOnceInPlayThread()) + break; + } + finally + { + exitRunOnceInPlayThread(); + } } - if(isLooping()) + if(!isLooping()) + break; + + synchronized (sync) { - synchronized(sync) - { - if (started) - { - try - { - /* - * Only wait if longer than 0; otherwise, we - * will wait forever. - */ - if(getLoopInterval() > 0) - sync.wait(getLoopInterval()); - } - catch (InterruptedException e) - { - } - } - } + /* + * We may have waited to acquire sync. Before beginning the + * wait for loopInterval, make sure we should continue. + */ + if (!isStarted()) + break; - if (started) + try { - if (loopCondition == null) - { - /* - * The interface contract is that this audio plays - * once only if the loopCondition is null. - */ - break; - } - else - { - boolean loop = false; + int loopInterval = getLoopInterval(); - try - { - loop = loopCondition.call(); - } - catch (Throwable t) - { - if (t instanceof ThreadDeath) - throw (ThreadDeath) t; - } - if (!loop) - { - /* - * The loopCondition failed to evaluate to true - * so the loop will not continue. - */ - break; - } - } + /* + * XXX The value 0 means that this instance should loop + * playing without waiting but it means infinity to + * Object.wait(long). + */ + if (loopInterval > 0) + sync.wait(loopInterval); + } + catch (InterruptedException ie) + { + interrupted = true; } - else - break; } - else + + /* + * After this audio has been played once, loopCondition should + * be consulted to approve each subsequent iteration of the + * loop. Before invoking loopCondition which may take noticeable + * time to execute, make sure that this instance has not been + * stopped while it waited for loopInterval. + */ + if (!isStarted()) + break; + + if (loopCondition == null) + { + /* + * The interface contract is that this audio plays once + * only if the loopCondition is null. + */ + break; + } + + /* + * The contract of the SCAudioClip interface with respect to + * loopCondition is that the loop will continue only if + * loopCondition successfully and explicitly evaluates to true. + */ + boolean loop = false; + + try + { + loop = loopCondition.call(); + } + catch (Throwable t) + { + if (t instanceof ThreadDeath) + throw (ThreadDeath) t; + + /* + * If loopCondition fails to successfully and explicitly + * evaluate to true, this audio should seize to play in a + * loop. Otherwise, there is a risk that whoever requested + * this audio to be played in a loop and provided the + * loopCondition will continue to play it forever. + */ + } + if (!loop) + { + /* + * The loopCondition failed to successfully and explicitly + * evaluate to true so the loop will not continue. + */ break; + } } + + if (interrupted) + Thread.currentThread().interrupt(); } finally { @@ -229,40 +465,76 @@ private void runInPlayThread(Callable<Boolean> loopCondition) } } - protected abstract boolean runOnceInPlayThread(); - /** - * Marks this audio as invalid or not. + * Plays this audio once. * - * @param isInvalid TRUE to mark this audio as invalid, FALSE otherwise + * @return <tt>true</tt> if subsequent plays of this audio and, + * respectively, the method are to be invoked if this audio is to be played + * in a loop; otherwise, <tt>false</tt>. The value reflects an + * implementation-specific loop condition, is not dependent on + * <tt>loopInterval</tt> and <tt>loopCondition</tt> and is combined with the + * latter in order to determine whether there will be a subsequent iteration + * of the playback loop. */ - public void setInvalid(boolean isInvalid) - { - this.setIsInvalid(isInvalid); - } + protected abstract boolean runOnceInPlayThread(); /** - * @param isInvalid the isInvalid to set + * Sets the indicator which determines whether this instance is invalid. + * <tt>AbstractSCAudioClip</tt> does not use the <tt>invalid</tt> + * property/state of this instance and merely remembers the value which was + * set on it so that it can be retrieved by {@link #isInvalid()}. The + * default value is <tt>false</tt> i.e. this instance is valid by default. + * + * @param invalid <tt>true</tt> to mark this instance invalid or + * <tt>false</tt> to mark it valid */ - public void setIsInvalid(boolean isInvalid) + public void setInvalid(boolean invalid) { - this.isInvalid = isInvalid; + this.invalid = invalid; } /** - * @param isLooping the isLooping to set + * Sets the indicator which determines whether this audio is to play in a + * loop. Generally, public invocation of the method is not necessary because + * the looping is controlled by the <tt>loopInterval</tt> property of this + * instance and the <tt>loopInterval</tt> and <tt>loopCondition</tt> + * parameters of {@link #play(int, Callable)} anyway. + * + * @param <tt>true</tt> to mark this instance that it should play the audio + * it represents in a loop; otherwise, <tt>false</tt> */ - public void setIsLooping(boolean isLooping) + public void setLooping(boolean looping) { - this.isLooping = isLooping; + synchronized (sync) + { + if (this.looping != looping) + { + this.looping = looping; + sync.notifyAll(); + } + } } /** - * @param loopInterval the loopInterval to set + * Sets the interval of time in milliseconds between consecutive plays of + * this audio in a loop. If negative, this audio is played once only. If + * non-negative, this audio may still be played once only if the + * <tt>loopCondition</tt> specified to {@link #play(int, Callable)} is + * <tt>null</tt> or its invocation fails. + * + * @param loopInterval the interval of time in milliseconds between + * consecutive plays of this audio in a loop to be set on this instance */ public void setLoopInterval(int loopInterval) { - this.loopInterval = loopInterval; + synchronized (sync) + { + if (this.loopInterval != loopInterval) + { + this.loopInterval = loopInterval; + sync.notifyAll(); + } + } } /** @@ -271,6 +543,6 @@ public void setLoopInterval(int loopInterval) public void stop() { internalStop(); - setIsLooping(false); + setLooping(false); } } diff --git a/src/org/jitsi/service/audionotifier/AudioNotifierService.java b/src/org/jitsi/service/audionotifier/AudioNotifierService.java index 968ba73c15821e379694e9617f5fd3e84bb762e4..a6b09d1189e128012a46afc8286a9648a81e0a01 100644 --- a/src/org/jitsi/service/audionotifier/AudioNotifierService.java +++ b/src/org/jitsi/service/audionotifier/AudioNotifierService.java @@ -17,6 +17,13 @@ */ public interface AudioNotifierService { + /** + * Checks whether the playback and notification configuration + * share the same device. + * @return are audio out and notifications using the same device. + */ + public boolean audioOutAndNotificationsShareSameDevice(); + /** * Creates an SCAudioClip and returns it. By default using notification * device. @@ -34,11 +41,11 @@ public interface AudioNotifierService public SCAudioClip createAudio(String uri, boolean playback); /** - * Destroys the given audio. + * Specifies if currently the sound is off. * - * @param audio <tt>SCAudioClip</tt> to destroy + * @return TRUE if currently the sound is off, FALSE otherwise */ - public void destroyAudio(SCAudioClip audio); + public boolean isMute(); /** * Stops/Restores all currently playing sounds. @@ -46,18 +53,4 @@ public interface AudioNotifierService * @param isMute mute or not currently playing sounds */ public void setMute(boolean isMute); - - /** - * Specifies if currently the sound is off. - * - * @return TRUE if currently the sound is off, FALSE otherwise - */ - public boolean isMute(); - - /** - * Checks whether the playback and notification configuration - * share the same device. - * @return are audio out and notifications using the same device. - */ - public boolean audioOutAndNotificationsShareSameDevice(); } diff --git a/src/org/jitsi/service/audionotifier/SCAudioClip.java b/src/org/jitsi/service/audionotifier/SCAudioClip.java index 9699c0765efea1c3ff8d7462582dabc010d56417..5c6623dfe561e66f8c44644d4c2eb8c270d65330 100644 --- a/src/org/jitsi/service/audionotifier/SCAudioClip.java +++ b/src/org/jitsi/service/audionotifier/SCAudioClip.java @@ -9,8 +9,8 @@ import java.util.concurrent.*; /** - * SCAudioClip represents an audio clip created using the AudioNotifierService. - * Like any audio it could be played, stopped or played in loop. + * Represents an audio clip which could be played (optionally, in a loop) and + * stopped.. * * @author Yana Stamcheva * @author Lyubomir Marinov diff --git a/src/org/jitsi/util/swing/FitLayout.java b/src/org/jitsi/util/swing/FitLayout.java index 783ba57cb683900ba581ac67bcbceb6cfb28ac71..fb7615d8f3c85be00fcfdf3aaa735adeb161bfad 100644 --- a/src/org/jitsi/util/swing/FitLayout.java +++ b/src/org/jitsi/util/swing/FitLayout.java @@ -11,14 +11,13 @@ import javax.swing.*; /** - * Represents a <code>LayoutManager</code> which centers the first - * <code>Component</code> within its <code>Container</code> and, if the - * preferred size of the <code>Component</code> is larger than the size of the - * <code>Container</code>, scales the former within the bounds of the latter - * while preserving the aspect ratio. <code>FitLayout</code> is appropriate for - * <code>Container</code>s which display a single image or video - * <code>Component</code> in its entirety for which preserving the aspect ratio - * is important. + * Represents a <tt>LayoutManager</tt> which centers the first + * <tt>Component</tt> within its <tt>Container</tt> and, if the preferred size + * of the <tt>Component</tt> is larger than the size of the <tt>Container</tt>, + * scales the former within the bounds of the latter while preserving the aspect + * ratio. <tt>FitLayout</tt> is appropriate for <tt>Container</tt>s which + * display a single image or video <tt>Component</tt> in its entirety for which + * preserving the aspect ratio is important. * * @author Lyubomir Marinov */ @@ -35,14 +34,13 @@ public void addLayoutComponent(String name, Component comp) } /** - * Gets the first <code>Component</code> of a specific - * <code>Container</code> if there is such a <code>Component</code>. - * - * @param parent the <code>Container</code> to retrieve the first - * <code>Component</code> of - * @return the first <code>Component</code> of a specific - * <code>Container</code> if there is such a <code>Component</code>; - * otherwise, <tt>null</tt> + * Gets the first <tt>Component</tt> of a specific <tt>Container</tt> if + * there is such a <tt>Component</tt>. + * + * @param parent the <tt>Container</tt> to retrieve the first + * <tt>Component</tt> of + * @return the first <tt>Component</tt> of a specific <tt>Container</tt> if + * there is such a <tt>Component</tt>; otherwise, <tt>null</tt> */ protected Component getComponent(Container parent) { @@ -172,10 +170,13 @@ public Dimension minimumLayoutSize(Container parent) : new Dimension(0, 0); } - /* - * Since this LayoutManager lays out only the first Component of the - * specified parent Container, the preferred size of the Container is the - * preferred size of the mentioned Component. + /** + * {@inheritDoc} + * + * Since this <tt>LayoutManager</tt> lays out only the first + * <tt>Component</tt> of the specified parent <tt>Container</tt>, the + * preferred size of the <tt>Container</tt> is the preferred size of the + * mentioned <tt>Component</tt>. */ public Dimension preferredLayoutSize(Container parent) { @@ -187,9 +188,12 @@ public Dimension preferredLayoutSize(Container parent) : new Dimension(0, 0); } - /* - * Does nothing because this LayoutManager lays out only the first Component - * of the parent Container and thus doesn't need String associations. + /** + * {@inheritDoc} + * + * Does nothing because this <tt>LayoutManager</tt> lays out only the first + * <tt>Component</tt> of the parent <tt>Container</tt> and thus doesn't need + * any <tt>String</tt> associations. */ public void removeLayoutComponent(Component comp) { diff --git a/src/org/jitsi/util/swing/VideoLayout.java b/src/org/jitsi/util/swing/VideoLayout.java index 945657cef582762f9379626993a8e6e2f63abb92..37c96bcc71f7cff9d49421bd4dc73d374e0be13f 100644 --- a/src/org/jitsi/util/swing/VideoLayout.java +++ b/src/org/jitsi/util/swing/VideoLayout.java @@ -374,11 +374,9 @@ else if (remoteCount > 0) } /** - * Returns the minimum layout size for the given container. + * {@inheritDoc} * - * @param parent the container which minimum layout size we're looking for - * @return a Dimension containing, the minimum layout size for the given - * container + * The <tt>VideoLayout</tt> implementation of the method does nothing. */ @Override public Dimension minimumLayoutSize(Container parent)