org.jitsi.service.audionotifier.AbstractSCAudioClip Maven / Gradle / Ivy
/*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jitsi.service.audionotifier;
import java.util.concurrent.*;
/**
* An abstract base implementation of {@link SCAudioClip} which is provided in
* order to aid implementers by allowing them to extend
* AbstractSCAudioClip and focus on the task of playing actual audio
* once.
*
* @author Damian Minkov
* @author Lyubomir Marinov
*/
public abstract class AbstractSCAudioClip
implements SCAudioClip
{
/**
* The thread pool used by the AbstractSCAudioClip instances in
* order to reduce the impact of thread creation/initialization.
*/
private static ExecutorService executorService;
/**
* The AudioNotifierService which has initialized this instance.
* AbstractSCAudioClip monitors its mute property/state in
* order to silence the played audio as appropriate/necessary.
*/
protected final AudioNotifierService audioNotifier;
private Runnable command;
/**
* 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
* loopCondition specified to {@link #play(int, Callable)} is
* null or its invocation fails.
*/
private int loopInterval;
/**
* The indicator which determines whether the playback of this audio is
* started.
*/
private boolean started;
/**
* The Object used for internal synchronization purposes which
* arise because this instance does the actual playback of audio in a
* separate thread.
*
* The synchronization root is exposed to extenders in case they would like
* to, for example, get notified as soon as possible when this instance gets
* stopped.
*/
protected final Object sync = new Object();
/**
* The String uri of the audio to be played by this instance.
* AbstractSCAudioClip does not use it and just remembers it in
* order to make it available to extenders.
*/
protected final String uri;
protected AbstractSCAudioClip(
String uri,
AudioNotifierService audioNotifier)
{
this.uri = uri;
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 AbstractSCAudioClip
* implementation does nothing.
*/
protected void enterRunInPlayThread()
{
}
/**
* 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
* AbstractSCAudioClip implementation does nothing.
*/
protected void enterRunOnceInPlayThread()
{
}
/**
* 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 AbstractSCAudioClip implementation does nothing.
*/
protected void exitRunInPlayThread()
{
}
/**
* 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 AbstractSCAudioClip
* implementation does nothing.
*/
protected void exitRunOnceInPlayThread()
{
}
/**
* 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()
{
return loopInterval;
}
/**
* Stops this audio without setting the isLooping property in the case of
* a looping audio. The AudioNotifier uses this method to stop the audio
* when setMute(true) is invoked. This allows us to restore all looping
* audios when the sound is restored by calling setMute(false).
*/
protected void internalStop()
{
boolean interrupted = false;
synchronized (sync)
{
started = false;
sync.notifyAll();
while (command != null)
{
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();
}
/**
* Determines whether this instance is invalid. AbstractSCAudioClip
* does not use the invalid property/state of this instance and
* merely remembers the value which was set on it by
* {@link #setInvalid(boolean)}. The default value is false i.e.
* this instance is valid by default.
*
* @return true if this instance is invalid; otherwise,
* false
*/
public boolean isInvalid()
{
return invalid;
}
/**
* Determines whether this instance plays the audio it represents in a loop.
*
* @return true if this instance plays the audio it represents in a
* loop; false, otherwise
*/
public boolean isLooping()
{
return looping;
}
/**
* Determines whether this audio is started i.e. a play method was
* invoked and no subsequent stop has been invoked yet.
*
* @return true if this audio is started; otherwise, false
*/
public boolean isStarted()
{
synchronized (sync)
{
return started;
}
}
/**
* {@inheritDoc}
*
* Delegates to {@link #play(int, Callable)} with loopInterval
* -1 and loopCondition null in order to conform
* with the contract for the behavior of this method specified by the
* interface SCAudioClip.
*/
public void play()
{
play(-1, null);
}
/**
* {@inheritDoc}
*/
public void play(int loopInterval, final Callable loopCondition)
{
if ((loopInterval >= 0) && (loopCondition == null))
loopInterval = -1;
synchronized (sync)
{
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()
{
public void run()
{
try
{
synchronized (sync)
{
/*
* We have to wait for
* play(int,Callable) 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();
}
}
}
}
};
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 Callback<Boolean> which represents
* the condition on which this audio will play more than once. If
* null, this audio will play once only. If an invocation of
* loopCondition throws a Throwable, this audio will
* discontinue playing.
*/
private void runInPlayThread(Callable loopCondition)
{
enterRunInPlayThread();
try
{
boolean interrupted = false;
while (isStarted())
{
if (audioNotifier.isMute())
{
/*
* 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;
}
}
}
else
{
enterRunOnceInPlayThread();
try
{
if (!runOnceInPlayThread())
break;
}
finally
{
exitRunOnceInPlayThread();
}
}
if(!isLooping())
break;
synchronized (sync)
{
/*
* We may have waited to acquire sync. Before beginning the
* wait for loopInterval, make sure we should continue.
*/
if (!isStarted())
break;
try
{
int loopInterval = getLoopInterval();
/*
* 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;
}
}
/*
* 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
{
exitRunInPlayThread();
}
}
/**
* Plays this audio once.
*
* @return true 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, false. The value reflects an
* implementation-specific loop condition, is not dependent on
* loopInterval and loopCondition and is combined with the
* latter in order to determine whether there will be a subsequent iteration
* of the playback loop.
*/
protected abstract boolean runOnceInPlayThread();
/**
* Sets the indicator which determines whether this instance is invalid.
* AbstractSCAudioClip does not use the invalid
* 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 false i.e. this instance is valid by default.
*
* @param invalid true to mark this instance invalid or
* false to mark it valid
*/
public void setInvalid(boolean invalid)
{
this.invalid = invalid;
}
/**
* 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 loopInterval property of this
* instance and the loopInterval and loopCondition
* parameters of {@link #play(int, Callable)} anyway.
*
* @param looping true to mark this instance that it should play
* the audio it represents in a loop; otherwise, false
*/
public void setLooping(boolean looping)
{
synchronized (sync)
{
if (this.looping != looping)
{
this.looping = looping;
sync.notifyAll();
}
}
}
/**
* 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
* loopCondition specified to {@link #play(int, Callable)} is
* null 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)
{
synchronized (sync)
{
if (this.loopInterval != loopInterval)
{
this.loopInterval = loopInterval;
sync.notifyAll();
}
}
}
/**
* {@inheritDoc}
*/
public void stop()
{
internalStop();
setLooping(false);
}
}