/* * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code 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 * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.media.jfxmediaimpl; import com.sun.media.jfxmedia.Media; import com.sun.media.jfxmedia.MediaError; import com.sun.media.jfxmedia.MediaException; import com.sun.media.jfxmedia.MediaPlayer; import com.sun.media.jfxmedia.control.VideoRenderControl; import com.sun.media.jfxmedia.effects.AudioEqualizer; import com.sun.media.jfxmedia.effects.AudioSpectrum; import com.sun.media.jfxmedia.events.AudioSpectrumEvent; import com.sun.media.jfxmedia.events.AudioSpectrumListener; import com.sun.media.jfxmedia.events.BufferListener; import com.sun.media.jfxmedia.events.BufferProgressEvent; import com.sun.media.jfxmedia.events.MarkerEvent; import com.sun.media.jfxmedia.events.MarkerListener; import com.sun.media.jfxmedia.events.MediaErrorListener; import com.sun.media.jfxmedia.events.NewFrameEvent; import com.sun.media.jfxmedia.events.PlayerEvent; import com.sun.media.jfxmedia.events.PlayerStateEvent; import com.sun.media.jfxmedia.events.PlayerStateEvent.PlayerState; import com.sun.media.jfxmedia.events.PlayerStateListener; import com.sun.media.jfxmedia.events.PlayerTimeListener; import com.sun.media.jfxmedia.events.VideoFrameRateListener; import com.sun.media.jfxmedia.events.VideoRendererListener; import com.sun.media.jfxmedia.events.VideoTrackSizeListener; import com.sun.media.jfxmedia.logging.Logger; import com.sun.media.jfxmedia.track.AudioTrack; import com.sun.media.jfxmedia.track.SubtitleTrack; import com.sun.media.jfxmedia.track.Track; import com.sun.media.jfxmedia.track.Track.Encoding; import com.sun.media.jfxmedia.track.VideoResolution; import com.sun.media.jfxmedia.track.VideoTrack; import java.lang.ref.WeakReference; import java.util.*; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; /** * Base implementation of a * MediaPlayer. */ public abstract class NativeMediaPlayer implements MediaPlayer, MarkerStateListener { //***** Event IDs for PlayerStateEvent. IDs sent from native JNI layer. public final static int eventPlayerUnknown = 100; public final static int eventPlayerReady = 101; public final static int eventPlayerPlaying = 102; public final static int eventPlayerPaused = 103; public final static int eventPlayerStopped = 104; public final static int eventPlayerStalled = 105; public final static int eventPlayerFinished = 106; public final static int eventPlayerError = 107; // Nominal video frames per second. private static final int NOMINAL_VIDEO_FPS = 30; // Nanoseconds per second. public static final long ONE_SECOND = 1000000000L; /** * The * Media corresponding to the media source. */ private NativeMedia media; private VideoRenderControl videoRenderControl; private final List> errorListeners = new ArrayList<>(); private final List> playerStateListeners = new ArrayList<>(); private final List> playerTimeListeners = new ArrayList<>(); private final List> videoTrackSizeListeners = new ArrayList<>(); private final List> videoUpdateListeners = new ArrayList<>(); private final List> videoFrameRateListeners = new ArrayList<>(); private final List> markerListeners = new ArrayList<>(); private final List> bufferListeners = new ArrayList<>(); private final List> audioSpectrumListeners = new ArrayList<>(); private final List cachedStateEvents = new ArrayList<>(); private final List cachedTimeEvents = new ArrayList<>(); private final List cachedBufferEvents = new ArrayList<>(); private final List cachedErrorEvents = new ArrayList<>(); private boolean isFirstFrame = true; private NewFrameEvent firstFrameEvent = null; private double firstFrameTime; private final Object firstFrameLock = new Object(); private EventQueueThread eventLoop = new EventQueueThread(); private int frameWidth = -1; private int frameHeight = -1; private final AtomicBoolean isMediaPulseEnabled = new AtomicBoolean(false); private final Lock mediaPulseLock = new ReentrantLock(); private Timer mediaPulseTimer; private final Lock markerLock = new ReentrantLock(); private boolean checkSeek = false; private double timeBeforeSeek = 0.0; private double timeAfterSeek = 0.0; private double previousTime = 0.0; private double firedMarkerTime = -1.0; private double startTime = 0.0; private double stopTime = Double.POSITIVE_INFINITY; private boolean isStartTimeUpdated = false; private boolean isStopTimeSet = false; // --- Begin decoded frame rate fields private double encodedFrameRate = 0.0; private boolean recomputeFrameRate = true; private double previousFrameTime; private long numFramesSincePlaying; private double meanFrameDuration; private double decodedFrameRate; // --- End decoded frame rate fields private PlayerState playerState = PlayerState.UNKNOWN; private final Lock disposeLock = new ReentrantLock(); private boolean isDisposed = false; private Runnable onDispose; //************************************************************************** //***** Constructors //************************************************************************** /** * Construct a NativeMediaPlayer for the referenced clip. * * @param clip Media object * @throws IllegalArgumentException if * clip is * null. */ protected NativeMediaPlayer(NativeMedia clip) { if (clip == null) { throw new IllegalArgumentException("clip == null!"); } media = clip; videoRenderControl = new VideoRenderer(); } /** * Initialization method which must be called after construction to * initialize the internal state of the player. This method should be * invoked directly after the player is constructed. */ protected void init() { media.addMarkerStateListener(this); eventLoop.start(); } /** * Set a callback to invoke when the player is disposed. * * @param onDispose object on which to invoke {@link Runnable#run()} in * {@link #dispose()}. */ void setOnDispose(Runnable onDispose) { disposeLock.lock(); try { if (!isDisposed) { this.onDispose = onDispose; } } finally { disposeLock.unlock(); } } /** * Event to be posted to any registered {@link MediaErrorListener}s. */ private static class WarningEvent extends PlayerEvent { private final Object source; private final String message; WarningEvent(Object source, String message) { this.source = source; this.message = message; } public Object getSource() { return source; } public String getMessage() { return message; } } /** * Event to be posted to any registered (@link MediaErrorListener)s */ public static class MediaErrorEvent extends PlayerEvent { private final Object source; private final MediaError error; public MediaErrorEvent(Object source, MediaError error) { this.source = source; this.error = error; } public Object getSource() { return source; } public String getMessage() { return error.description(); } public int getErrorCode() { return error.code(); } } private static class PlayerTimeEvent extends PlayerEvent { private final double time; public PlayerTimeEvent(double time) { this.time = time; } public double getTime() { return time; } } /** * Event to be posted to any registered {@link PlayerStateListener}s. */ private static class TrackEvent extends PlayerEvent { private final Track track; TrackEvent(Track track) { this.track = track; } public Track getTrack() { return this.track; } } /** * Event to be posted to any registered {@link VideoTrackSizeListener}s. */ private static class FrameSizeChangedEvent extends PlayerEvent { private final int width; private final int height; public FrameSizeChangedEvent(int width, int height) { if (width > 0) { this.width = width; } else { this.width = 0; } if (height > 0) { this.height = height; } else { this.height = 0; } } public int getWidth() { return width; } public int getHeight() { return height; } } /** * Helper class which managers {@link VideoRendererListener}s. This allows * any registered listeners, specifically AWT and Prism, to receive video * frames. */ private class VideoRenderer implements VideoRenderControl { /** * adds the listener to the player's videoUpdate. The listener will be * called whenever a new frame of video is ready to be painted or * fetched by getData() * * @param listener the object which provides the VideoUpdateListener * callback interface */ @Override public void addVideoRendererListener(VideoRendererListener listener) { if (listener != null) { synchronized (firstFrameLock) { // If the first frame is cached, post it to the listener // directly. The lock is obtained first so the cached // frame is not cleared between the non-null test and // posting the event. if (firstFrameEvent != null) { listener.videoFrameUpdated(firstFrameEvent); } } videoUpdateListeners.add(new WeakReference<>(listener)); } } /** * removes the listener from the player. * * @param listener to be removed from the player */ @Override public void removeVideoRendererListener(VideoRendererListener listener) { if (listener != null) { for (ListIterator> it = videoUpdateListeners.listIterator(); it.hasNext();) { VideoRendererListener l = it.next().get(); if (l == null || l == listener) { it.remove(); } } } } @Override public void addVideoFrameRateListener(VideoFrameRateListener listener) { if (listener != null) { videoFrameRateListeners.add(new WeakReference<>(listener)); } } @Override public void removeVideoFrameRateListener(VideoFrameRateListener listener) { if (listener != null) { for (ListIterator> it = videoFrameRateListeners.listIterator(); it.hasNext();) { VideoFrameRateListener l = it.next().get(); if (l == null || l == listener) { it.remove(); } } } } @Override public int getFrameWidth() { return frameWidth; } @Override public int getFrameHeight() { return frameHeight; } } //***** EventQueueThread Helper Class -- Provides event handling. /** * Thread for media player event processing. The thread maintains an * internal queue of * PlayerEvents to which callers post using * postEvent(). The thread blocks until an event becomes * available on the queue, and then removes the event from the queue and * posts it to any registered listeners appropriate to the type of event. */ private class EventQueueThread extends Thread { private final BlockingQueue eventQueue = new LinkedBlockingQueue<>(); private volatile boolean stopped = false; EventQueueThread() { setName("JFXMedia Player EventQueueThread"); setDaemon(true); } @Override public void run() { while (!stopped) { try { // trying to take an event from the queue. // this method will block until an event becomes available. PlayerEvent evt = eventQueue.take(); if (!stopped) { if (evt instanceof NewFrameEvent) { try { HandleRendererEvents((NewFrameEvent) evt); } catch (Throwable t) { if (Logger.canLog(Logger.ERROR)) { Logger.logMsg(Logger.ERROR, "Caught exception in HandleRendererEvents: " + t.toString()); } } } else if (evt instanceof PlayerStateEvent) { HandleStateEvents((PlayerStateEvent) evt); } else if (evt instanceof FrameSizeChangedEvent) { HandleFrameSizeChangedEvents((FrameSizeChangedEvent) evt); } else if (evt instanceof TrackEvent) { HandleTrackEvents((TrackEvent) evt); } else if (evt instanceof MarkerEvent) { HandleMarkerEvents((MarkerEvent) evt); } else if (evt instanceof WarningEvent) { HandleWarningEvents((WarningEvent) evt); } else if (evt instanceof PlayerTimeEvent) { HandlePlayerTimeEvents((PlayerTimeEvent) evt); } else if (evt instanceof BufferProgressEvent) { HandleBufferEvents((BufferProgressEvent) evt); } else if (evt instanceof AudioSpectrumEvent) { HandleAudioSpectrumEvents((AudioSpectrumEvent) evt); } else if (evt instanceof MediaErrorEvent) { HandleErrorEvents((MediaErrorEvent) evt); } } } catch (Exception e) { // eventQueue.take() can throw InterruptedException, // also in rare case it can throw wrong // IllegalMonitorStateException // so we catch Exception // nothing to do, restart the loop unless it was properly stopped. } } eventQueue.clear(); } private void HandleRendererEvents(NewFrameEvent evt) { if (isFirstFrame) { // Cache first frame. Frames are delivered time-sequentially // so there should be no thread contention problem here. isFirstFrame = false; synchronized (firstFrameLock) { firstFrameEvent = evt; firstFrameTime = firstFrameEvent.getFrameData().getTimestamp(); firstFrameEvent.getFrameData().holdFrame(); // hold as long as we cache it, else we'll crash } } else if (firstFrameEvent != null && firstFrameTime != evt.getFrameData().getTimestamp()) { // If this branch is entered then it cannot be the first frame. // This means that the player must be in the PLAYING state as // the first frame will arrive upon completion of prerolling. // When playing, listeners should receive the current frame, // not the first frame in the stream. // Clear the cached first frame. Obtain the lock first to avoid // a race condition with a listener newly being added. synchronized (firstFrameLock) { firstFrameEvent.getFrameData().releaseFrame(); firstFrameEvent = null; } } // notify videoUpdateListeners for (ListIterator> it = videoUpdateListeners.listIterator(); it.hasNext();) { VideoRendererListener l = it.next().get(); if (l != null) { l.videoFrameUpdated(evt); } else { it.remove(); } } // done with the frame, we can release our hold now evt.getFrameData().releaseFrame(); if (!videoFrameRateListeners.isEmpty()) { // Decoded frame rate calculations. double currentFrameTime = System.nanoTime() / (double) ONE_SECOND; if (recomputeFrameRate) { // First frame in new computation sequence. recomputeFrameRate = false; previousFrameTime = currentFrameTime; numFramesSincePlaying = 1; } else { boolean fireFrameRateEvent = false; if (numFramesSincePlaying == 1) { // Second frame. Estimate the initial frame rate and // set event flag. meanFrameDuration = currentFrameTime - previousFrameTime; if (meanFrameDuration > 0.0) { decodedFrameRate = 1.0 / meanFrameDuration; fireFrameRateEvent = true; } } else { // Update decoded frame rate estimate using a moving // average over encodedFrameRate frames. double previousMeanFrameDuration = meanFrameDuration; // Determine moving average length. int movingAverageLength = encodedFrameRate != 0.0 ? ((int) (encodedFrameRate + 0.5)) : NOMINAL_VIDEO_FPS; // Claculate number of frames in current average. long numFrames = numFramesSincePlaying < movingAverageLength ? numFramesSincePlaying : movingAverageLength; // Update the mean frame duration. meanFrameDuration = ((numFrames - 1) * previousMeanFrameDuration + currentFrameTime - previousFrameTime) / numFrames; // If mean frame duration changed by more than 0.5 set // event flag. if (meanFrameDuration > 0.0 && Math.abs(decodedFrameRate - 1.0 / meanFrameDuration) > 0.5) { decodedFrameRate = 1.0 / meanFrameDuration; fireFrameRateEvent = true; } } if (fireFrameRateEvent) { // Fire event. for (ListIterator> it = videoFrameRateListeners.listIterator(); it.hasNext();) { VideoFrameRateListener l = it.next().get(); if (l != null) { l.onFrameRateChanged(decodedFrameRate); } else { it.remove(); } } } // Update running values. previousFrameTime = currentFrameTime; numFramesSincePlaying++; } } } private void HandleStateEvents(PlayerStateEvent evt) { playerState = evt.getState(); recomputeFrameRate = PlayerState.PLAYING == evt.getState(); switch (playerState) { case READY: onNativeInit(); sendFakeBufferProgressEvent(); break; case PLAYING: isMediaPulseEnabled.set(true); break; case STOPPED: case FINISHED: // Force a time update here to catch the time going to // zero for STOPPED and any trailing markers for FINISHED. doMediaPulseTask(); case PAUSED: case STALLED: case HALTED: isMediaPulseEnabled.set(false); break; default: break; } synchronized (cachedStateEvents) { if (playerStateListeners.isEmpty()) { // Cache event for processing when first listener registers. cachedStateEvents.add(evt); return; } } for (ListIterator> it = playerStateListeners.listIterator(); it.hasNext();) { PlayerStateListener listener = it.next().get(); if (listener != null) { switch (playerState) { case READY: onNativeInit(); sendFakeBufferProgressEvent(); listener.onReady(evt); break; case PLAYING: listener.onPlaying(evt); break; case PAUSED: listener.onPause(evt); break; case STOPPED: listener.onStop(evt); break; case STALLED: listener.onStall(evt); break; case FINISHED: listener.onFinish(evt); break; case HALTED: listener.onHalt(evt); break; default: break; } } else { it.remove(); } } } private void HandlePlayerTimeEvents(PlayerTimeEvent evt) { synchronized (cachedTimeEvents) { if (playerTimeListeners.isEmpty()) { // Cache event for processing when first listener registers. cachedTimeEvents.add(evt); return; } } for (ListIterator> it = playerTimeListeners.listIterator(); it.hasNext();) { PlayerTimeListener listener = it.next().get(); if (listener != null) { listener.onDurationChanged(evt.getTime()); } else { it.remove(); } } } private void HandleFrameSizeChangedEvents(FrameSizeChangedEvent evt) { frameWidth = evt.getWidth(); frameHeight = evt.getHeight(); Logger.logMsg(Logger.DEBUG, "** Frame size changed (" + frameWidth + ", " + frameHeight + ")"); for (ListIterator> it = videoTrackSizeListeners.listIterator(); it.hasNext();) { VideoTrackSizeListener listener = it.next().get(); if (listener != null) { listener.onSizeChanged(frameWidth, frameHeight); } else { it.remove(); } } } private void HandleTrackEvents(TrackEvent evt) { media.addTrack(evt.getTrack()); if (evt.getTrack() instanceof VideoTrack) { encodedFrameRate = ((VideoTrack) evt.getTrack()).getEncodedFrameRate(); } } private void HandleMarkerEvents(MarkerEvent evt) { for (ListIterator> it = markerListeners.listIterator(); it.hasNext();) { MarkerListener listener = it.next().get(); if (listener != null) { listener.onMarker(evt); } else { it.remove(); } } } private void HandleWarningEvents(WarningEvent evt) { Logger.logMsg(Logger.WARNING, evt.getSource() + evt.getMessage()); } private void HandleErrorEvents(MediaErrorEvent evt) { Logger.logMsg(Logger.ERROR, evt.getMessage()); synchronized (cachedErrorEvents) { if (errorListeners.isEmpty()) { // cache error events until at least one listener is added cachedErrorEvents.add(evt); return; } } for (ListIterator> it = errorListeners.listIterator(); it.hasNext();) { MediaErrorListener l = it.next().get(); if (l != null) { l.onError(evt.getSource(), evt.getErrorCode(), evt.getMessage()); } else { it.remove(); } } } private void HandleBufferEvents(BufferProgressEvent evt) { synchronized (cachedBufferEvents) { if (bufferListeners.isEmpty()) { // Cache event for processing when first listener registers. cachedBufferEvents.add(evt); return; } } for (ListIterator> it = bufferListeners.listIterator(); it.hasNext();) { BufferListener listener = it.next().get(); if (listener != null) { listener.onBufferProgress(evt); } else { it.remove(); } } } private void HandleAudioSpectrumEvents(AudioSpectrumEvent evt) { for (ListIterator> it = audioSpectrumListeners.listIterator(); it.hasNext();) { AudioSpectrumListener listener = it.next().get(); if (listener != null) { listener.onAudioSpectrumEvent(evt); } else { it.remove(); } } } /** * Puts an event to the EventQuery. */ public void postEvent(PlayerEvent event) { if (eventQueue != null) { eventQueue.offer(event); } } /** * Signals the thread to terminate. */ public void terminateLoop() { stopped = true; // put an event to unblock eventQueue.take() try { eventQueue.put(new PlayerEvent()); } catch(InterruptedException ex) {} } private void sendFakeBufferProgressEvent() { // Send fake 100% buffer progress event for HLS or !http protcol String contentType = media.getLocator().getContentType(); String protocol = media.getLocator().getProtocol(); if ((contentType != null && (contentType.equals(MediaUtils.CONTENT_TYPE_M3U) || contentType.equals(MediaUtils.CONTENT_TYPE_M3U8))) || (protocol != null && !protocol.equals("http") && !protocol.equals("https"))) { HandleBufferEvents(new BufferProgressEvent(getDuration(), 0, 1, 1)); } } } /** * Internal function to get called when the native player is ready. */ private synchronized void onNativeInit() { try { playerInit(); } catch (MediaException me) { sendPlayerMediaErrorEvent(me.getMediaError().code()); } } //************************************************************************** //***** MediaPlayer implementation //************************************************************************** //***** Listener (un)registration. @Override public void addMediaErrorListener(MediaErrorListener listener) { if (listener != null) { this.errorListeners.add(new WeakReference<>(listener)); synchronized (cachedErrorEvents) { if (!cachedErrorEvents.isEmpty() && !errorListeners.isEmpty()) { cachedErrorEvents.stream().forEach((evt) -> { sendPlayerEvent(evt); }); cachedErrorEvents.clear(); } } } } @Override public void removeMediaErrorListener(MediaErrorListener listener) { if (listener != null) { for (ListIterator> it = errorListeners.listIterator(); it.hasNext();) { MediaErrorListener l = it.next().get(); if (l == null || l == listener) { it.remove(); } } } } @Override public void addMediaPlayerListener(PlayerStateListener listener) { if (listener != null) { synchronized (cachedStateEvents) { if (!cachedStateEvents.isEmpty() && playerStateListeners.isEmpty()) { // Forward all cached state events to first listener to register. Iterator events = cachedStateEvents.iterator(); while (events.hasNext()) { PlayerStateEvent evt = events.next(); switch (evt.getState()) { case READY: listener.onReady(evt); break; case PLAYING: listener.onPlaying(evt); break; case PAUSED: listener.onPause(evt); break; case STOPPED: listener.onStop(evt); break; case STALLED: listener.onStall(evt); break; case FINISHED: listener.onFinish(evt); break; default: break; } } // Clear state event cache. cachedStateEvents.clear(); } playerStateListeners.add(new WeakReference(listener)); } } } @Override public void removeMediaPlayerListener(PlayerStateListener listener) { if (listener != null) { for (ListIterator> it = playerStateListeners.listIterator(); it.hasNext();) { PlayerStateListener l = it.next().get(); if (l == null || l == listener) { it.remove(); } } } } @Override public void addMediaTimeListener(PlayerTimeListener listener) { if (listener != null) { synchronized (cachedTimeEvents) { if (!cachedTimeEvents.isEmpty() && playerTimeListeners.isEmpty()) { // Forward all cached time events to first listener to register. Iterator events = cachedTimeEvents.iterator(); while (events.hasNext()) { PlayerTimeEvent evt = events.next(); listener.onDurationChanged(evt.getTime()); } // Clear time event cache. cachedTimeEvents.clear(); } else { // Let listener to know about duration double duration = getDuration(); if (duration != Double.POSITIVE_INFINITY) { listener.onDurationChanged(duration); } } playerTimeListeners.add(new WeakReference(listener)); } } } @Override public void removeMediaTimeListener(PlayerTimeListener listener) { if (listener != null) { for (ListIterator> it = playerTimeListeners.listIterator(); it.hasNext();) { PlayerTimeListener l = it.next().get(); if (l == null || l == listener) { it.remove(); } } } } @Override public void addVideoTrackSizeListener(VideoTrackSizeListener listener) { if (listener != null) { if (frameWidth != -1 && frameHeight != -1) { listener.onSizeChanged(frameWidth, frameHeight); } videoTrackSizeListeners.add(new WeakReference(listener)); } } @Override public void removeVideoTrackSizeListener(VideoTrackSizeListener listener) { if (listener != null) { for (ListIterator> it = videoTrackSizeListeners.listIterator(); it.hasNext();) { VideoTrackSizeListener l = it.next().get(); if (l == null || l == listener) { it.remove(); } } } } @Override public void addMarkerListener(MarkerListener listener) { if (listener != null) { markerListeners.add(new WeakReference(listener)); } } @Override public void removeMarkerListener(MarkerListener listener) { if (listener != null) { for (ListIterator> it = markerListeners.listIterator(); it.hasNext();) { MarkerListener l = it.next().get(); if (l == null || l == listener) { it.remove(); } } } } @Override public void addBufferListener(BufferListener listener) { if (listener != null) { synchronized (cachedBufferEvents) { if (!cachedBufferEvents.isEmpty() && bufferListeners.isEmpty()) { cachedBufferEvents.stream().forEach((evt) -> { listener.onBufferProgress(evt); }); // Clear buffer event cache. cachedBufferEvents.clear(); } bufferListeners.add(new WeakReference(listener)); } } } @Override public void removeBufferListener(BufferListener listener) { if (listener != null) { for (ListIterator> it = bufferListeners.listIterator(); it.hasNext();) { BufferListener l = it.next().get(); if (l == null || l == listener) { it.remove(); } } } } @Override public void addAudioSpectrumListener(AudioSpectrumListener listener) { if (listener != null) { audioSpectrumListeners.add(new WeakReference(listener)); } } @Override public void removeAudioSpectrumListener(AudioSpectrumListener listener) { if (listener != null) { for (ListIterator> it = audioSpectrumListeners.listIterator(); it.hasNext();) { AudioSpectrumListener l = it.next().get(); if (l == null || l == listener) { it.remove(); } } } } //***** Control functions @Override public VideoRenderControl getVideoRenderControl() { return videoRenderControl; } @Override public Media getMedia() { return media; } @Override public void setAudioSyncDelay(long delay) { try { playerSetAudioSyncDelay(delay); } catch (MediaException me) { sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); } } @Override public long getAudioSyncDelay() { try { return playerGetAudioSyncDelay(); } catch (MediaException me) { sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); } return 0; } @Override public void play() { try { if (isStartTimeUpdated) { playerSeek(startTime); } isMediaPulseEnabled.set(true); playerPlay(); } catch (MediaException me) { sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); } } @Override public void stop() { try { playerStop(); playerSeek(startTime); } catch (MediaException me) { // sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); MediaUtils.warning(this, "stop() failed!"); } } @Override public void pause() { try { playerPause(); } catch (MediaException me) { sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); } } @Override public float getRate() { try { return playerGetRate(); } catch (MediaException me) { sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); } return 0; } //***** Public properties @Override public void setRate(float rate) { try { playerSetRate(rate); } catch (MediaException me) { // sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); MediaUtils.warning(this, "setRate(" + rate + ") failed!"); } } @Override public double getPresentationTime() { try { return playerGetPresentationTime(); } catch (MediaException me) { // sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); } return -1.0; } @Override public float getVolume() { try { return playerGetVolume(); } catch (MediaException me) { sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); } return 0; } @Override public void setVolume(float vol) { if (vol < 0.0F) { vol = 0.0F; } else if (vol > 1.0F) { vol = 1.0F; } try { playerSetVolume(vol); } catch (MediaException me) { sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); } } @Override public boolean getMute() { try { return playerGetMute(); } catch (MediaException me) { sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); } return false; } /** * Enables/disable mute. If mute is enabled then disabled, the previous * volume goes into effect. */ @Override public void setMute(boolean enable) { try { playerSetMute(enable); } catch (MediaException me) { sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); } } @Override public float getBalance() { try { return playerGetBalance(); } catch (MediaException me) { sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); } return 0; } @Override public void setBalance(float bal) { if (bal < -1.0F) { bal = -1.0F; } else if (bal > 1.0F) { bal = 1.0F; } try { playerSetBalance(bal); } catch (MediaException me) { sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); } } @Override public abstract AudioEqualizer getEqualizer(); @Override public abstract AudioSpectrum getAudioSpectrum(); @Override public double getDuration() { try { return playerGetDuration(); } catch (MediaException me) { // sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); } return Double.POSITIVE_INFINITY; } /** * Gets the time within the duration of the media to start playing. */ @Override public double getStartTime() { return startTime; } /** * Sets the start time within the media to play. */ @Override public void setStartTime(double startTime) { try { markerLock.lock(); this.startTime = startTime; if (playerState != PlayerState.PLAYING && playerState != PlayerState.FINISHED && playerState != PlayerState.STOPPED) { playerSeek(startTime); } else if (playerState == PlayerState.STOPPED) { isStartTimeUpdated = true; } } finally { markerLock.unlock(); } } /** * Gets the time within the duration of the media to stop playing. */ @Override public double getStopTime() { return stopTime; } /** * Sets the stop time within the media to stop playback. */ @Override public void setStopTime(double stopTime) { try { markerLock.lock(); this.stopTime = stopTime; isStopTimeSet = true; createMediaPulse(); } finally { markerLock.unlock(); } } @Override public void seek(double streamTime) { if (playerState == PlayerState.STOPPED) { return; // No seek in stopped state } if (streamTime < 0.0) { streamTime = 0.0; } else { double duration = getDuration(); if (duration >= 0.0 && streamTime > duration) { streamTime = duration; } } if (!isMediaPulseEnabled.get()) { if ((playerState == PlayerState.PLAYING || playerState == PlayerState.PAUSED || playerState == PlayerState.FINISHED) && getStartTime() <= streamTime && streamTime <= getStopTime()) { isMediaPulseEnabled.set(true); } } markerLock.lock(); try { timeBeforeSeek = getPresentationTime(); timeAfterSeek = streamTime; checkSeek = timeBeforeSeek != timeAfterSeek; previousTime = streamTime; firedMarkerTime = -1.0; // System.out.println("seek @ "+System.currentTimeMillis()); // System.out.println("seek to "+streamTime+" previousTime "+previousTime); try { playerSeek(streamTime); } catch (MediaException me) { //sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError())); MediaUtils.warning(this, "seek(" + streamTime + ") failed!"); } } finally { markerLock.unlock(); } } protected abstract long playerGetAudioSyncDelay() throws MediaException; protected abstract void playerSetAudioSyncDelay(long delay) throws MediaException; protected abstract void playerPlay() throws MediaException; protected abstract void playerStop() throws MediaException; protected abstract void playerPause() throws MediaException; protected abstract void playerFinish() throws MediaException; protected abstract float playerGetRate() throws MediaException; protected abstract void playerSetRate(float rate) throws MediaException; protected abstract double playerGetPresentationTime() throws MediaException; protected abstract boolean playerGetMute() throws MediaException; protected abstract void playerSetMute(boolean state) throws MediaException; protected abstract float playerGetVolume() throws MediaException; protected abstract void playerSetVolume(float volume) throws MediaException; protected abstract float playerGetBalance() throws MediaException; protected abstract void playerSetBalance(float balance) throws MediaException; protected abstract double playerGetDuration() throws MediaException; protected abstract void playerSeek(double streamTime) throws MediaException; protected abstract void playerInit() throws MediaException; protected abstract void playerDispose(); /** * Retrieves the current {@link PlayerState state} of the player. * * @return the current player state. */ @Override public PlayerState getState() { return playerState; } @Override final public void dispose() { disposeLock.lock(); try { if (!isDisposed) { // Terminate event firing destroyMediaPulse(); if (eventLoop != null) { eventLoop.terminateLoop(); } synchronized (firstFrameLock) { if (firstFrameEvent != null) { firstFrameEvent.getFrameData().releaseFrame(); firstFrameEvent = null; } } // Terminate native layer playerDispose(); // Dispose media object and clear reference if (media != null) { media.dispose(); media = null; } // Clear event loop reference if (eventLoop != null) { eventLoop.eventQueue.clear(); eventLoop = null; } if (videoUpdateListeners != null) { for (ListIterator> it = videoUpdateListeners.listIterator(); it.hasNext();) { VideoRendererListener l = it.next().get(); if (l != null) { l.releaseVideoFrames(); } else { it.remove(); } } videoUpdateListeners.clear(); } if (playerStateListeners != null) { playerStateListeners.clear(); } if (videoTrackSizeListeners != null) { videoTrackSizeListeners.clear(); } if (videoFrameRateListeners != null) { videoFrameRateListeners.clear(); } if (cachedStateEvents != null) { cachedStateEvents.clear(); } if (cachedTimeEvents != null) { cachedTimeEvents.clear(); } if (cachedBufferEvents != null) { cachedBufferEvents.clear(); } if (errorListeners != null) { errorListeners.clear(); } if (playerTimeListeners != null) { playerTimeListeners.clear(); } if (markerListeners != null) { markerListeners.clear(); } if (bufferListeners != null) { bufferListeners.clear(); } if (audioSpectrumListeners != null) { audioSpectrumListeners.clear(); } if (videoRenderControl != null) { videoRenderControl = null; } if (onDispose != null) { onDispose.run(); } isDisposed = true; } } finally { disposeLock.unlock(); } } //************************************************************************** //***** Non-JNI methods called by the native layer. These methods are called //***** from the native layer via the invocation API. Their purpose is to //***** dispatch certain events to the Java layer. Each of these methods //***** posts an event on the EventQueueThread which in turn //***** forwards the event to any registered listeners. //************************************************************************** protected void sendWarning(int warningCode, String warningMessage) { if (eventLoop != null) { String message = String.format(MediaUtils.NATIVE_MEDIA_WARNING_FORMAT, warningCode); if (warningMessage != null) { message += ": " + warningMessage; } eventLoop.postEvent(new WarningEvent(this, message)); } } protected void sendPlayerEvent(PlayerEvent evt) { if (eventLoop != null) { eventLoop.postEvent(evt); } } protected void sendPlayerHaltEvent(String message, double time) { // Log the error. Since these are most likely playback engine message (e.g. GStreamer or PacketVideo), // it makes no sense to propogate it above. Logger.logMsg(Logger.ERROR, message); if (eventLoop != null) { eventLoop.postEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.HALTED, time, message)); } } protected void sendPlayerMediaErrorEvent(int errorCode) { sendPlayerEvent(new MediaErrorEvent(this, MediaError.getFromCode(errorCode))); } protected void sendPlayerStateEvent(int eventID, double time) { switch (eventID) { case eventPlayerReady: sendPlayerEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.READY, time)); break; case eventPlayerPlaying: sendPlayerEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.PLAYING, time)); break; case eventPlayerPaused: sendPlayerEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.PAUSED, time)); break; case eventPlayerStopped: sendPlayerEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.STOPPED, time)); break; case eventPlayerStalled: sendPlayerEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.STALLED, time)); break; case eventPlayerFinished: sendPlayerEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.FINISHED, time)); break; default: break; } } protected void sendNewFrameEvent(long nativeRef) { NativeVideoBuffer newFrameData = NativeVideoBuffer.createVideoBuffer(nativeRef); // createVideoBuffer puts a hold on the frame // we need to keep that hold until the event thread can process this event sendPlayerEvent(new NewFrameEvent(newFrameData)); } protected void sendFrameSizeChangedEvent(int width, int height) { sendPlayerEvent(new FrameSizeChangedEvent(width, height)); } protected void sendAudioTrack(boolean enabled, long trackID, String name, int encoding, String language, int numChannels, int channelMask, float sampleRate) { Locale locale = null; if (!language.equals("und")) { locale = new Locale(language); } Track track = new AudioTrack(enabled, trackID, name, locale, Encoding.toEncoding(encoding), numChannels, channelMask, sampleRate); TrackEvent evt = new TrackEvent(track); sendPlayerEvent(evt); } protected void sendVideoTrack(boolean enabled, long trackID, String name, int encoding, int width, int height, float frameRate, boolean hasAlphaChannel) { // No locale (currently) for video, so pass null Track track = new VideoTrack(enabled, trackID, name, null, Encoding.toEncoding(encoding), new VideoResolution(width, height), frameRate, hasAlphaChannel); TrackEvent evt = new TrackEvent(track); sendPlayerEvent(evt); } protected void sendSubtitleTrack(boolean enabled, long trackID, String name, int encoding, String language) { Locale locale = null; if (null != language) { locale = new Locale(language); } Track track = new SubtitleTrack(enabled, trackID, name, locale, Encoding.toEncoding(encoding)); sendPlayerEvent(new TrackEvent(track)); } protected void sendMarkerEvent(String name, double time) { sendPlayerEvent(new MarkerEvent(name, time)); } protected void sendDurationUpdateEvent(double duration) { sendPlayerEvent(new PlayerTimeEvent(duration)); } protected void sendBufferProgressEvent(double clipDuration, long bufferStart, long bufferStop, long bufferPosition) { sendPlayerEvent(new BufferProgressEvent(clipDuration, bufferStart, bufferStop, bufferPosition)); } protected void sendAudioSpectrumEvent(double timestamp, double duration) { sendPlayerEvent(new AudioSpectrumEvent(getAudioSpectrum(), timestamp, duration)); } @Override public void markerStateChanged(boolean hasMarkers) { if (hasMarkers) { markerLock.lock(); try { previousTime = getPresentationTime(); } finally { markerLock.unlock(); } createMediaPulse(); } else { if (!isStopTimeSet) { destroyMediaPulse(); } } } private void createMediaPulse() { mediaPulseLock.lock(); try { if (mediaPulseTimer == null) { mediaPulseTimer = new Timer(true); mediaPulseTimer.scheduleAtFixedRate(new MediaPulseTask(this), 0, 40 /* * period ms */); } } finally { mediaPulseLock.unlock(); } } private void destroyMediaPulse() { mediaPulseLock.lock(); try { if (mediaPulseTimer != null) { mediaPulseTimer.cancel(); mediaPulseTimer = null; } } finally { mediaPulseLock.unlock(); } } boolean doMediaPulseTask() { if (this.isMediaPulseEnabled.get()) { disposeLock.lock(); if (isDisposed) { disposeLock.unlock(); return false; } double thisTime = getPresentationTime(); markerLock.lock(); try { //System.out.println("Media pulse @ pts "+thisTime+" previous "+previousTime); if (checkSeek) { if (timeAfterSeek > timeBeforeSeek) { // Forward seek if (thisTime >= timeAfterSeek) { // System.out.println("bail 1"); checkSeek = false; } else { return true; } } else if (timeAfterSeek < timeBeforeSeek) { // Backward seek if (thisTime >= timeBeforeSeek) { // System.out.println("bail 2"); return true; } else { checkSeek = false; } } } Map.Entry marker = media.getNextMarker(previousTime, true); // System.out.println("marker "+marker); // System.out.println("Checking: " + previousTime + " " + thisTime + " " // + getStartTime() + " " + getStopTime() + " " // + marker.getKey()); while (marker != null) { double nextMarkerTime = marker.getKey(); if (nextMarkerTime > thisTime) { break; } else if (nextMarkerTime != firedMarkerTime && nextMarkerTime >= previousTime && nextMarkerTime >= getStartTime() && nextMarkerTime <= getStopTime()) { // System.out.println("Firing: "+previousTime+" "+thisTime+" "+ // getStartTime()+" "+getStopTime()+" "+ // nextMarkerTime); MarkerEvent evt = new MarkerEvent(marker.getValue(), nextMarkerTime); for (ListIterator> it = markerListeners.listIterator(); it.hasNext();) { MarkerListener listener = it.next().get(); if (listener != null) { listener.onMarker(evt); } else { it.remove(); } } firedMarkerTime = nextMarkerTime; } marker = media.getNextMarker(nextMarkerTime, false); } previousTime = thisTime; // Do stopTime if (isStopTimeSet && thisTime >= stopTime) { playerFinish(); } } finally { disposeLock.unlock(); markerLock.unlock(); } } return true; } /* Audio EQ and spectrum creation, used by sub-classes */ protected AudioEqualizer createNativeAudioEqualizer(long nativeRef) { return new NativeAudioEqualizer(nativeRef); } protected AudioSpectrum createNativeAudioSpectrum(long nativeRef) { return new NativeAudioSpectrum(nativeRef); } } class MediaPulseTask extends TimerTask { WeakReference playerRef; MediaPulseTask(NativeMediaPlayer player) { playerRef = new WeakReference<>(player); } @Override public void run() { final NativeMediaPlayer player = playerRef.get(); if (player != null) { if (!player.doMediaPulseTask()) { cancel(); // Stop if doMediaPulseTask() returns false. False means doMediaPulseTask() cannot continue (like after dispose).cy } } else { cancel(); } } }