SquirrelJME/SquirrelJME

View on GitHub
modules/media-api/src/main/java/cc/squirreljme/runtime/media/AbstractPlayer.java

Summary

Maintainability
A
0 mins
Test Coverage
// ---------------------------------------------------------------------------
// SquirrelJME
//     Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
// ---------------------------------------------------------------------------
// SquirrelJME is under the GNU General Public License v3+, or later.
// See license.mkd for licensing and copyright information.
// ---------------------------------------------------------------------------

package cc.squirreljme.runtime.media;

import cc.squirreljme.runtime.cldc.annotation.SquirrelJMEVendorApi;
import java.util.LinkedList;
import java.util.List;
import javax.microedition.media.Manager;
import javax.microedition.media.MediaException;
import javax.microedition.media.Player;
import javax.microedition.media.PlayerListener;
import javax.microedition.media.TimeBase;

/**
 * Common implementation of players.
 *
 * @since 2022/04/24
 */
@SquirrelJMEVendorApi
public abstract class AbstractPlayer
    implements Player
{
    /** The current track position. */
    @SquirrelJMEVendorApi
    protected final TrackPosition trackPosition =
        new TrackPosition();
    
    /** The mime type. */
    private final String _mime;
    
    /** Listeners available. */
    private final List<PlayerListener> _listeners =
        new LinkedList<>();
    
    /** The default time base. */
    private final TimeBase _defaultTimeBase =
        Manager.getSystemTimeBase();
    
    /** The loop counter which controls how much the audio replays. */
    @SquirrelJMEVendorApi
    volatile int _loopCounter =
        1;
    
    /** The state of the player. */
    private volatile int _state =
        Player.UNREALIZED;
    
    /** The current timebase. */
    private volatile TimeBase _currentTimebase;
    
    /** The duration of the media. */
    private volatile long _cachedDurationMicros =
        Long.MIN_VALUE;
    
    /**
     * Initializes the base player.
     * 
     * @param __mime The MIME type.
     * @throws NullPointerException On null arguments.
     * @since 2022/04/24
     */
    @SquirrelJMEVendorApi
    protected AbstractPlayer(String __mime)
        throws NullPointerException
    {
        if (__mime == null)
            throw new NullPointerException("NARG");
        
        this._mime = __mime;
    }
    
    /**
     * This is called when the player is becoming prefetched.
     * 
     * @throws MediaException If the player cannot be prefetched.
     * @since 2022/04/24
     */
    @SquirrelJMEVendorApi
    protected abstract void becomingPrefetched()
        throws MediaException;
    
    /**
     * This is called when the player is becoming realized.
     * 
     * @throws MediaException If the player cannot be realized.
     * @since 2022/04/24
     */
    @SquirrelJMEVendorApi
    protected abstract void becomingRealized()
        throws MediaException;
    
    /**
     * Indicates that the media is about to start.
     * 
     * @throws MediaException If the player could not be started.
     * @since 2022/04/24
     */
    @SquirrelJMEVendorApi
    protected abstract void becomingStarted()
        throws MediaException;
    
    /**
     * Indicates that the player is stopping.
     * 
     * @throws MediaException If the player could not be stopped.
     * @since 2022/04/24
     */
    @SquirrelJMEVendorApi
    protected abstract void becomingStopped()
        throws MediaException;
    
    /**
     * Determines the length of the media.
     * 
     * @return The media length.
     * @since 2022/04/25
     */
    @SquirrelJMEVendorApi
    protected abstract long determineDuration()
        throws MediaException;
    
    /**
     * {@inheritDoc}
     * @since 2019/04/15
     */
    @Override
    @SquirrelJMEVendorApi
    public final void addPlayerListener(PlayerListener __l)
    {
        // Ignore?
        if (__l == null)
            return;
        
        // {@squirreljme.error EA01 Player has been closed.}
        if (this.getState() == Player.CLOSED)
            throw new IllegalStateException("EA01");
        
        // Add unique listener
        List<PlayerListener> listeners = this._listeners;
        synchronized (this)
        {
            if (!listeners.contains(__l))
                listeners.add(__l);
        }
    }
    
    /**
     * Sends an event to all listeners.
     *
     * @param __key The key used.
     * @param __val The value used.
     * @since 2019/06/28
     */
    @SquirrelJMEVendorApi
    protected final void broadcastEvent(String __key, Object __val)
    {
        PlayerListener[] poke;
        
        // Get listeners to poke
        List<PlayerListener> listeners = this._listeners;
        synchronized (this)
        {
            poke = listeners.<PlayerListener>toArray(
                new PlayerListener[listeners.size()]);
        }
        
        // Poke them all
        for (PlayerListener pl : poke)
            try
            {
                pl.playerUpdate(this, __key, __val);
            }
            catch (Throwable t)
            {
                t.printStackTrace();
            }
    }
    
    /**
     * Decrement the loop count.
     *
     * @return If the loop has reached zero.
     * @since 2024/02/26
     */
    public boolean decrementLoop()
    {
        int count = this._loopCounter;
        
        if ((--count) <= 0)
        {
            this._loopCounter = 0;
            return true;
        }
        
        this._loopCounter = count;
        return false;
    }
    
    /**
     * {@inheritDoc}
     * @since 2019/04/15
     */
    @Override
    @SquirrelJMEVendorApi
    public final String getContentType()
    {
        return this._mime;
    }
    
    /**
     * {@inheritDoc}
     * @since 2022/04/25
     */
    @Override
    @SquirrelJMEVendorApi
    public final long getDuration()
        throws IllegalStateException
    {
        // {@squirreljme.error EA0g Stream closed, cannot get duration.}
        if (this.getState() == Player.CLOSED)
            throw new IllegalStateException("EA0g");
        
        // Already has been cached?
        long cachedDuration = this._cachedDurationMicros;
        if (cachedDuration != Long.MIN_VALUE)
            return cachedDuration;
        
        // Otherwise determine the duration
        long newDuration;
        try
        {
            newDuration = this.determineDuration();
            this._cachedDurationMicros = newDuration;
            
        }
        catch (MediaException e)
        {
            return Player.TIME_UNKNOWN;
        }
        
        // Indicate the duration is available now
        this.broadcastEvent(PlayerListener.DURATION_UPDATED,
            newDuration);
        
        return newDuration;
    }
    
    /**
     * {@inheritDoc}
     * @since 2019/04/15
     */
    @Override
    @SquirrelJMEVendorApi
    public final int getState()
    {
        synchronized (this)
        {
            return this._state;
        }
    }
    
    /**
     * {@inheritDoc}
     * @since 2019/04/15
     */
    @Override
    @SquirrelJMEVendorApi
    public final TimeBase getTimeBase()
    {
        // Use the default time base, if there is no current one
        TimeBase rv = this._currentTimebase;
        if (rv == null)
            return this._defaultTimeBase;
        
        return rv;
    }
    
    /**
     * {@inheritDoc}
     * @since 2019/04/15
     */
    @Override
    @SquirrelJMEVendorApi
    public final void prefetch()
        throws MediaException
    {
        int state = this.getState();
        if (state == Player.CLOSED)
            throw new IllegalStateException("EA0g");
        
        // Ignore when started or already prefetched
        if (state == Player.STARTED ||
            state == Player.PREFETCHED)
            return;
        
        // Implicit realize, if not yet realized
        if (state == Player.UNREALIZED)
            this.realize();
        
        // Now becoming prefetched
        this.becomingPrefetched();
        this.setState(Player.PREFETCHED);
    }
    
    /**
     * {@inheritDoc}
     * @since 2019/04/15
     */
    @Override
    @SquirrelJMEVendorApi
    public final void realize()
        throws MediaException
    {
        // {@squirreljme.error EA04 Player has been closed.}
        int state = this.getState();
        if (state == Player.CLOSED)
            throw new IllegalStateException("EA04");
        
        // Ignore in these states
        if (state == Player.REALIZED ||
            state == Player.PREFETCHED ||
            state == Player.STARTED)
            return;
        
        // Now becoming realized
        this.becomingRealized();
        this.setState(Player.REALIZED);
    }
    
    /**
     * {@inheritDoc}
     * @since 2019/04/15
     */
    @Override
    @SquirrelJMEVendorApi
    public final void removePlayerListener(PlayerListener __l)
    {
        // Ignore?
        if (__l == null)
            return;
        
        // {@squirreljme.error EA02 Player has been closed.}
        if (this.getState() == Player.CLOSED)
            throw new IllegalStateException("EA02");
        
        // Remove it
        synchronized (this)
        {
            this._listeners.remove(__l);
        }
    }
    
    /**
     * {@inheritDoc}
     * @since 2022/04/24
     */
    @Override
    @SquirrelJMEVendorApi
    public void setLoopCount(int __count)
        throws IllegalArgumentException, IllegalStateException
    {
        // {@squirreljme.error EA0g Invalid loop count. (The count)}
        if (__count == 0 || __count < -1)
            throw new IllegalArgumentException("EA0g " + __count);
        
        // {@squirreljme.error EA0h Cannot set the loop count when the
        // player has started or is closed.}
        int state = this.getState();
        if (state == Player.CLOSED || state == Player.STARTED)
            throw new IllegalStateException("EA0h");
        
        // Set the internal loop counter
        this._loopCounter = __count;
    }
    
    /**
     * Sets the state.
     * 
     * @param __state The state to set.
     * @throws IllegalArgumentException If the state is not valid.
     * @since 2022/04/24
     */
    @SquirrelJMEVendorApi
    protected final void setState(int __state)
        throws IllegalArgumentException
    {
        switch (__state)
        {
            case Player.CLOSED:
            case Player.PREFETCHED:
            case Player.STARTED:
            case Player.REALIZED:
            case Player.UNREALIZED:
                this._state = __state;
                break;
            
                // {@squirreljme.error EA0e Invalid state. (The state)}
            default:
                throw new IllegalArgumentException("EA0e " + __state);
        }
    }
    
    /**
     * {@inheritDoc}
     * @since 2022/04/24
     */
    @Override
    @SquirrelJMEVendorApi
    public final void setTimeBase(TimeBase __timeBase)
    {
        this._currentTimebase = __timeBase;
    }
    
    /**
     * {@inheritDoc}
     * @since 2019/04/15
     */
    @Override
    @SquirrelJMEVendorApi
    public final void start()
        throws MediaException
    {
        // {@squirreljme.error EA05 Null Player has been closed.}
        int state = this.getState();
        if (state == Player.CLOSED)
            throw new IllegalStateException("EA05");
        
        // Ignore when started
        if (state == Player.STARTED)
            return;
        
        // The player needs to be prefetched first?
        if (state == Player.UNREALIZED ||
            state == Player.REALIZED)
            this.prefetch();
        
        // Set up the track position for starting
        TrackPosition trackPosition = this.trackPosition;
        TimeBase timeBase = this.getTimeBase();
        trackPosition.timeBase = timeBase;
        trackPosition.basisMicros = timeBase.getTime() -
            trackPosition.stoppedMicros;
        
        // Is being started now
        this.becomingStarted();
        this.setState(Player.STARTED);
        
        // Send event
        this.broadcastEvent(PlayerListener.STARTED,
            timeBase.getTime());
    }
    
    /**
     * {@inheritDoc}
     * @since 2022/04/27
     */
    @Override
    @SquirrelJMEVendorApi
    public final void stop()
        throws MediaException
    {
        // {@squirreljme.error EA06 Null Player has been closed.}
        int state = this.getState();
        if (state == Player.CLOSED)
            throw new IllegalStateException("EA06");
        
        // Ignore these
        if (state == Player.UNREALIZED ||
            state == Player.REALIZED ||
            state == Player.PREFETCHED)
            return;
        
        // Send stop via media
        this.stopViaMedia();
    }
    
    /**
     * A stop occurred via media playback.
     *
     * @throws MediaException On any error.
     * @since 2024/02/26
     */
    public final void stopViaMedia()
        throws MediaException
    {    
        // Becoming stopped
        this.becomingStopped();
        
        // Make sure the state stays valid
        int state = this.getState();
        if (state != Player.CLOSED &&
            state != Player.UNREALIZED &&
            state != Player.REALIZED &&
            state != Player.PREFETCHED)
            this.setState(Player.PREFETCHED);
        
        // Send event
        this.broadcastEvent(PlayerListener.END_OF_MEDIA,
            this.getTimeBase().getTime());
        this.broadcastEvent(PlayerListener.STOPPED,
            this.getTimeBase().getTime());
    }
}