/* * This file is part of VLCJ. * * VLCJ is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * VLCJ is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with VLCJ. If not, see <http://www.gnu.org/licenses/>. * * Copyright 2009-2016 Caprica Software Limited. */ package uk.co.caprica.vlcj.player.condition; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import uk.co.caprica.vlcj.player.MediaPlayer; import uk.co.caprica.vlcj.player.MediaPlayerEventAdapter; import uk.co.caprica.vlcj.player.MediaPlayerEventListener; /** * Base implementation for a component that waits for specific media player state * to occur. * <p> * Instances of this class, or its sub-classes, are <em>not</em> reusable. * <p> * This implementation works by adding a temporary event listener to an associated * media player then waiting, via {@link #await()}, for an internal synchronisation * object to trigger, via {@link #ready()} or {@link #ready(Object)} when one or * other of the event listener's implementation methods detects the desired media * player state. The temporary event listener is then removed and the {@link #await()} * method is unblocked and returns. * <p> * Commonly needed triggers are implemented for {@link #error()} and {@link #finished()}. * <p> * This facilitates a semblance of <em>synchronous</em> or sequential media player * programming. * <p> * Sub-classes have access to the associated {@link #mediaPlayer} if needed. * <p> * Sub-classes may also override {@link #onBefore()} and {@link #onAfter(Object)} to * implement behaviour that executes respectively before awaiting the condition and * after the condition state is reached. * <p> * Using {@link #onBefore()} guarantees that the media player event listener has been * registered with the media player before the condition implementation is executed. * <p> * Note that as with other {@link MediaPlayerEventListener} implementations the * event callbacks are running in a background thread. * <p> * Example: * <pre> * try { * Condition<?> playingCondition = new PlayingCondition(mediaPlayer) { * {@literal @}Override * protected void onBefore() { * mediaPlayer.play(); * } * }; * playingCondition.await(); * * // Do some interesting things, wait for some other conditions... * } * catch(UnexpectedErrorConditionException e) { * // Whatever... * } * catch(UnexpectedFinishedConditionException e) { * // Whatever... * } * </pre> * * @param <T> type of result that may be returned when the desired condition arises * * @see DefaultCondition */ public abstract class Condition<T> extends MediaPlayerEventAdapter { /** * Log. */ private final Logger logger = LoggerFactory.getLogger(Condition.class); /** * Synchronisation object used to wait until the media player state reaches * the desired condition. */ private final CountDownLatch completionLatch = new CountDownLatch(1); /** * Result status indicator. */ private final AtomicReference<ResultStatus> resultStatus = new AtomicReference<ResultStatus>(); /** * Optional result. */ private final AtomicReference<T> result = new AtomicReference<T>(); /** * Flag indicating if this condition is already finished. */ private final AtomicBoolean finished = new AtomicBoolean(); /** * Associated media player. */ protected final MediaPlayer mediaPlayer; /** * Simple flag to track whether or not this instance has been used before - * instances are <em>not</em> reusable. */ private boolean used; /** * Create a new waiter. * * @param mediaPlayer media player */ public Condition(MediaPlayer mediaPlayer) { this.mediaPlayer = mediaPlayer; // Listen for media player events mediaPlayer.addMediaPlayerEventListener(this); } /** * Wait for the required condition to occur. * * @return optional result * @throws InterruptedException if the condition was interrupted while waiting * @throws UnexpectedErrorConditionException if an unexpected error occurred * @throws UnexpectedFinishedConditionException if the condition finished unexpectedly */ public final T await() throws InterruptedException, UnexpectedErrorConditionException, UnexpectedFinishedConditionException { logger.debug("await()"); if(!used) { used = true; // Invoke the template method before waiting if(onBefore()) { // Wait for the completion latch to be triggered... completionLatch.await(); // Depending on the result status... switch(resultStatus.get()) { case NORMAL: // ...normal processing, first invoke the template method after finished onAfter(this.result.get()); // ...then return the result return result.get(); case ERROR: // ...an error occurred throw new UnexpectedErrorConditionException(); case FINISHED: // ...the media finished unexpectedly throw new UnexpectedFinishedConditionException(); default: // Can not happen throw new IllegalStateException("Unexpected result status: " + resultStatus.get()); } } else { throw new BeforeConditionAbortedException(); } } else { throw new IllegalStateException("Can not re-use Condition instances, create a new instance instead"); } } /** * Trigger method invoked by a sub-class event handler when the desired media * player state is detected. * <p> * This is a convenience for {@link #ready(Object)} when no result is needed. */ protected final void ready() { logger.debug("ready()"); ready(null); } /** * Trigger method invoked by a sub-class event handler when the desired media * player state is detected. * * @param result optional result, may be <code>null</code> */ protected final void ready(T result) { logger.debug("ready(result={})", result); if(!finished.getAndSet(true)) { logger.debug("Finished"); // Store the result this.result.set(result); // Finish waiting and release the waiter release(ResultStatus.NORMAL); } else { logger.debug("Already finished"); } } /** * Trigger method invoked by a sub-class event handler when the media player * reports an error has occurred. */ protected final void error() { logger.debug("error()"); // Finish waiting... release(ResultStatus.ERROR); } /** * Trigger method invoked by a sub-class event handler when the media player * reports the end of the media has been reached. */ protected final void finished() { logger.debug("finished()"); // Finish waiting... release(ResultStatus.FINISHED); } /** * Template method invoked after the listener has been added but before the * {@link #await()} is invoked. * * @return <code>true</code> to continue; <code>false</code> to abort */ protected boolean onBefore() { // Default implementation does nothing return true; } /** * Template method invoked after the media player state has reached the desired * condition. * * @param result optional result */ protected void onAfter(T result) { // Default implementation does nothing } /** * Release the waiter. * * @param resultStatus result status indicator */ private void release(ResultStatus resultStatus) { // Stop listening for media player events mediaPlayer.removeMediaPlayerEventListener(this); // Store the result status this.resultStatus.set(resultStatus); // Trigger the completion latch to release the waiter completionLatch.countDown(); } /** * Enumeration of result status. */ private enum ResultStatus { /** * Processing completed normally. */ NORMAL, /** * An error occurred. */ ERROR, /** * The media finished unexpectedly. */ FINISHED } }