1 /*
   2  * Copyright (c) 2010, 2019, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.media.jfxmediaimpl;
  27 
  28 import java.lang.annotation.Native;
  29 import com.sun.media.jfxmedia.Media;
  30 import com.sun.media.jfxmedia.MediaError;
  31 import com.sun.media.jfxmedia.MediaException;
  32 import com.sun.media.jfxmedia.MediaPlayer;
  33 import com.sun.media.jfxmedia.control.VideoRenderControl;
  34 import com.sun.media.jfxmedia.effects.AudioEqualizer;
  35 import com.sun.media.jfxmedia.effects.AudioSpectrum;
  36 import com.sun.media.jfxmedia.events.AudioSpectrumEvent;
  37 import com.sun.media.jfxmedia.events.AudioSpectrumListener;
  38 import com.sun.media.jfxmedia.events.BufferListener;
  39 import com.sun.media.jfxmedia.events.BufferProgressEvent;
  40 import com.sun.media.jfxmedia.events.MarkerEvent;
  41 import com.sun.media.jfxmedia.events.MarkerListener;
  42 import com.sun.media.jfxmedia.events.MediaErrorListener;
  43 import com.sun.media.jfxmedia.events.NewFrameEvent;
  44 import com.sun.media.jfxmedia.events.PlayerEvent;
  45 import com.sun.media.jfxmedia.events.PlayerStateEvent;
  46 import com.sun.media.jfxmedia.events.PlayerStateEvent.PlayerState;
  47 import com.sun.media.jfxmedia.events.PlayerStateListener;
  48 import com.sun.media.jfxmedia.events.PlayerTimeListener;
  49 import com.sun.media.jfxmedia.events.VideoFrameRateListener;
  50 import com.sun.media.jfxmedia.events.VideoRendererListener;
  51 import com.sun.media.jfxmedia.events.VideoTrackSizeListener;
  52 import com.sun.media.jfxmedia.logging.Logger;
  53 import com.sun.media.jfxmedia.track.AudioTrack;
  54 import com.sun.media.jfxmedia.track.SubtitleTrack;
  55 import com.sun.media.jfxmedia.track.Track;
  56 import com.sun.media.jfxmedia.track.Track.Encoding;
  57 import com.sun.media.jfxmedia.track.VideoResolution;
  58 import com.sun.media.jfxmedia.track.VideoTrack;
  59 import java.lang.ref.WeakReference;
  60 import java.util.*;
  61 import java.util.concurrent.BlockingQueue;
  62 import java.util.concurrent.LinkedBlockingQueue;
  63 import java.util.concurrent.atomic.AtomicBoolean;
  64 import java.util.concurrent.locks.Lock;
  65 import java.util.concurrent.locks.ReentrantLock;
  66 
  67 /**
  68  * Base implementation of a
  69  * <code>MediaPlayer</code>.
  70  */
  71 public abstract class NativeMediaPlayer implements MediaPlayer, MarkerStateListener {
  72     //***** Event IDs for PlayerStateEvent.  IDs sent from native JNI layer.
  73 
  74     @Native public final static int eventPlayerUnknown = 100;
  75     @Native public final static int eventPlayerReady = 101;
  76     @Native public final static int eventPlayerPlaying = 102;
  77     @Native public final static int eventPlayerPaused = 103;
  78     @Native public final static int eventPlayerStopped = 104;
  79     @Native public final static int eventPlayerStalled = 105;
  80     @Native public final static int eventPlayerFinished = 106;
  81     @Native public final static int eventPlayerError = 107;
  82     // Nominal video frames per second.
  83     @Native private static final int NOMINAL_VIDEO_FPS = 30;
  84     // Nanoseconds per second.
  85     @Native public static final long ONE_SECOND = 1000000000L;
  86 
  87     /**
  88      * The
  89      * <code>Media</code> corresponding to the media source.
  90      */
  91     private NativeMedia media;
  92     private VideoRenderControl videoRenderControl;
  93     private final List<WeakReference<MediaErrorListener>> errorListeners = new ArrayList<>();
  94     private final List<WeakReference<PlayerStateListener>> playerStateListeners = new ArrayList<>();
  95     private final List<WeakReference<PlayerTimeListener>> playerTimeListeners = new ArrayList<>();
  96     private final List<WeakReference<VideoTrackSizeListener>> videoTrackSizeListeners = new ArrayList<>();
  97     private final List<WeakReference<VideoRendererListener>> videoUpdateListeners = new ArrayList<>();
  98     private final List<WeakReference<VideoFrameRateListener>> videoFrameRateListeners = new ArrayList<>();
  99     private final List<WeakReference<MarkerListener>> markerListeners = new ArrayList<>();
 100     private final List<WeakReference<BufferListener>> bufferListeners = new ArrayList<>();
 101     private final List<WeakReference<AudioSpectrumListener>> audioSpectrumListeners = new ArrayList<>();
 102     private final List<PlayerStateEvent> cachedStateEvents = new ArrayList<>();
 103     private final List<PlayerTimeEvent> cachedTimeEvents = new ArrayList<>();
 104     private final List<BufferProgressEvent> cachedBufferEvents = new ArrayList<>();
 105     private final List<MediaErrorEvent> cachedErrorEvents = new ArrayList<>();
 106     private boolean isFirstFrame = true;
 107     private NewFrameEvent firstFrameEvent = null;
 108     private double firstFrameTime;
 109     private final Object firstFrameLock = new Object();
 110     private EventQueueThread eventLoop = new EventQueueThread();
 111     private int frameWidth = -1;
 112     private int frameHeight = -1;
 113     private final AtomicBoolean isMediaPulseEnabled = new AtomicBoolean(false);
 114     private final Lock mediaPulseLock = new ReentrantLock();
 115     private Timer mediaPulseTimer;
 116     private final Lock markerLock = new ReentrantLock();
 117     private boolean checkSeek = false;
 118     private double timeBeforeSeek = 0.0;
 119     private double timeAfterSeek = 0.0;
 120     private double previousTime = 0.0;
 121     private double firedMarkerTime = -1.0;
 122     private double startTime = 0.0;
 123     private double stopTime = Double.POSITIVE_INFINITY;
 124     private boolean isStartTimeUpdated = false;
 125     private boolean isStopTimeSet = false;
 126 
 127     // --- Begin decoded frame rate fields
 128     private double encodedFrameRate = 0.0;
 129     private boolean recomputeFrameRate = true;
 130     private double previousFrameTime;
 131     private long numFramesSincePlaying;
 132     private double meanFrameDuration;
 133     private double decodedFrameRate;
 134     // --- End decoded frame rate fields
 135     private PlayerState playerState = PlayerState.UNKNOWN;
 136     private final Lock disposeLock = new ReentrantLock();
 137     private boolean isDisposed = false;
 138     private Runnable onDispose;
 139 
 140     //**************************************************************************
 141     //***** Constructors
 142     //**************************************************************************
 143     /**
 144      * Construct a NativeMediaPlayer for the referenced clip.
 145      *
 146      * @param clip Media object
 147      * @throws IllegalArgumentException if
 148      * <code>clip</code> is
 149      * <code>null</code>.
 150      */
 151     protected NativeMediaPlayer(NativeMedia clip) {
 152         if (clip == null) {
 153             throw new IllegalArgumentException("clip == null!");
 154         }
 155         media = clip;
 156         videoRenderControl = new VideoRenderer();
 157     }
 158 
 159     /**
 160      * Initialization method which must be called after construction to
 161      * initialize the internal state of the player. This method should be
 162      * invoked directly after the player is constructed.
 163      */
 164     protected void init() {
 165         media.addMarkerStateListener(this);
 166         eventLoop.start();
 167     }
 168 
 169     /**
 170      * Set a callback to invoke when the player is disposed.
 171      *
 172      * @param onDispose object on which to invoke {@link Runnable#run()} in
 173      * {@link #dispose()}.
 174      */
 175     void setOnDispose(Runnable onDispose) {
 176         disposeLock.lock();
 177         try {
 178             if (!isDisposed) {
 179                 this.onDispose = onDispose;
 180             }
 181         } finally {
 182             disposeLock.unlock();
 183         }
 184     }
 185 
 186     /**
 187      * Event to be posted to any registered {@link MediaErrorListener}s.
 188      */
 189     private static class WarningEvent extends PlayerEvent {
 190 
 191         private final Object source;
 192         private final String message;
 193 
 194         WarningEvent(Object source, String message) {
 195             this.source = source;
 196             this.message = message;
 197         }
 198 
 199         public Object getSource() {
 200             return source;
 201         }
 202 
 203         public String getMessage() {
 204             return message;
 205         }
 206     }
 207 
 208     /**
 209      * Event to be posted to any registered (@link MediaErrorListener)s
 210      */
 211     public static class MediaErrorEvent extends PlayerEvent {
 212 
 213         private final Object source;
 214         private final MediaError error;
 215 
 216         public MediaErrorEvent(Object source, MediaError error) {
 217             this.source = source;
 218             this.error = error;
 219         }
 220 
 221         public Object getSource() {
 222             return source;
 223         }
 224 
 225         public String getMessage() {
 226             return error.description();
 227         }
 228 
 229         public int getErrorCode() {
 230             return error.code();
 231         }
 232     }
 233 
 234     private static class PlayerTimeEvent extends PlayerEvent {
 235 
 236         private final double time;
 237 
 238         public PlayerTimeEvent(double time) {
 239             this.time = time;
 240         }
 241 
 242         public double getTime() {
 243             return time;
 244         }
 245     }
 246 
 247     /**
 248      * Event to be posted to any registered {@link PlayerStateListener}s.
 249      */
 250     private static class TrackEvent extends PlayerEvent {
 251 
 252         private final Track track;
 253 
 254         TrackEvent(Track track) {
 255             this.track = track;
 256         }
 257 
 258         public Track getTrack() {
 259             return this.track;
 260         }
 261     }
 262 
 263     /**
 264      * Event to be posted to any registered {@link VideoTrackSizeListener}s.
 265      */
 266     private static class FrameSizeChangedEvent extends PlayerEvent {
 267 
 268         private final int width;
 269         private final int height;
 270 
 271         public FrameSizeChangedEvent(int width, int height) {
 272             if (width > 0) {
 273                 this.width = width;
 274             } else {
 275                 this.width = 0;
 276             }
 277 
 278             if (height > 0) {
 279                 this.height = height;
 280             } else {
 281                 this.height = 0;
 282             }
 283         }
 284 
 285         public int getWidth() {
 286             return width;
 287         }
 288 
 289         public int getHeight() {
 290             return height;
 291         }
 292     }
 293 
 294     /**
 295      * Helper class which managers {@link VideoRendererListener}s. This allows
 296      * any registered listeners, specifically AWT and Prism, to receive video
 297      * frames.
 298      */
 299     private class VideoRenderer implements VideoRenderControl {
 300 
 301         /**
 302          * adds the listener to the player's videoUpdate. The listener will be
 303          * called whenever a new frame of video is ready to be painted or
 304          * fetched by getData()
 305          *
 306          * @param listener the object which provides the VideoUpdateListener
 307          * callback interface
 308          */
 309         @Override
 310         public void addVideoRendererListener(VideoRendererListener listener) {
 311             if (listener != null) {
 312                 synchronized (firstFrameLock) {
 313                     // If the first frame is cached, post it to the listener
 314                     // directly. The lock is obtained first so the cached
 315                     // frame is not cleared between the non-null test and
 316                     // posting the event.
 317                     if (firstFrameEvent != null) {
 318                         listener.videoFrameUpdated(firstFrameEvent);
 319                     }
 320                 }
 321                 videoUpdateListeners.add(new WeakReference<>(listener));
 322             }
 323         }
 324 
 325         /**
 326          * removes the listener from the player.
 327          *
 328          * @param listener to be removed from the player
 329          */
 330         @Override
 331         public void removeVideoRendererListener(VideoRendererListener listener) {
 332             if (listener != null) {
 333                 for (ListIterator<WeakReference<VideoRendererListener>> it = videoUpdateListeners.listIterator(); it.hasNext();) {
 334                     VideoRendererListener l = it.next().get();
 335                     if (l == null || l == listener) {
 336                         it.remove();
 337                     }
 338                 }
 339             }
 340         }
 341 
 342         @Override
 343         public void addVideoFrameRateListener(VideoFrameRateListener listener) {
 344             if (listener != null) {
 345                 videoFrameRateListeners.add(new WeakReference<>(listener));
 346             }
 347         }
 348 
 349         @Override
 350         public void removeVideoFrameRateListener(VideoFrameRateListener listener) {
 351             if (listener != null) {
 352                 for (ListIterator<WeakReference<VideoFrameRateListener>> it = videoFrameRateListeners.listIterator(); it.hasNext();) {
 353                     VideoFrameRateListener l = it.next().get();
 354                     if (l == null || l == listener) {
 355                         it.remove();
 356                     }
 357                 }
 358             }
 359         }
 360 
 361         @Override
 362         public int getFrameWidth() {
 363             return frameWidth;
 364         }
 365 
 366         @Override
 367         public int getFrameHeight() {
 368             return frameHeight;
 369         }
 370     }
 371 
 372     //***** EventQueueThread Helper Class -- Provides event handling.
 373     /**
 374      * Thread for media player event processing. The thread maintains an
 375      * internal queue of
 376      * <code>PlayerEvent</code>s to which callers post using
 377      * <code>postEvent()</code>. The thread blocks until an event becomes
 378      * available on the queue, and then removes the event from the queue and
 379      * posts it to any registered listeners appropriate to the type of event.
 380      */
 381     private class EventQueueThread extends Thread {
 382 
 383         private final BlockingQueue<PlayerEvent> eventQueue =
 384                 new LinkedBlockingQueue<>();
 385         private volatile boolean stopped = false;
 386 
 387         EventQueueThread() {
 388             setName("JFXMedia Player EventQueueThread");
 389             setDaemon(true);
 390         }
 391 
 392         @Override
 393         public void run() {
 394             while (!stopped) {
 395                 try {
 396                     // trying to take an event from the queue.
 397                     // this method will block until an event becomes available.
 398                     PlayerEvent evt = eventQueue.take();
 399 
 400                     if (!stopped) {
 401                         if (evt instanceof NewFrameEvent) {
 402                             try {
 403                                 HandleRendererEvents((NewFrameEvent) evt);
 404                             } catch (Throwable t) {
 405                                 if (Logger.canLog(Logger.ERROR)) {
 406                                     Logger.logMsg(Logger.ERROR, "Caught exception in HandleRendererEvents: " + t.toString());
 407                                 }
 408                             }
 409                         } else if (evt instanceof PlayerStateEvent) {
 410                             HandleStateEvents((PlayerStateEvent) evt);
 411                         } else if (evt instanceof FrameSizeChangedEvent) {
 412                             HandleFrameSizeChangedEvents((FrameSizeChangedEvent) evt);
 413                         } else if (evt instanceof TrackEvent) {
 414                             HandleTrackEvents((TrackEvent) evt);
 415                         } else if (evt instanceof MarkerEvent) {
 416                             HandleMarkerEvents((MarkerEvent) evt);
 417                         } else if (evt instanceof WarningEvent) {
 418                             HandleWarningEvents((WarningEvent) evt);
 419                         } else if (evt instanceof PlayerTimeEvent) {
 420                             HandlePlayerTimeEvents((PlayerTimeEvent) evt);
 421                         } else if (evt instanceof BufferProgressEvent) {
 422                             HandleBufferEvents((BufferProgressEvent) evt);
 423                         } else if (evt instanceof AudioSpectrumEvent) {
 424                             HandleAudioSpectrumEvents((AudioSpectrumEvent) evt);
 425                         } else if (evt instanceof MediaErrorEvent) {
 426                             HandleErrorEvents((MediaErrorEvent) evt);
 427                         }
 428                     }
 429                 } catch (Exception e) {
 430                     // eventQueue.take() can throw InterruptedException,
 431                     // also in rare case it can throw wrong
 432                     // IllegalMonitorStateException
 433                     // so we catch Exception
 434                     // nothing to do, restart the loop unless it was properly stopped.
 435                 }
 436             }
 437 
 438             eventQueue.clear();
 439         }
 440 
 441         private void HandleRendererEvents(NewFrameEvent evt) {
 442             if (isFirstFrame) {
 443                 // Cache first frame. Frames are delivered time-sequentially
 444                 // so there should be no thread contention problem here.
 445                 isFirstFrame = false;
 446                 synchronized (firstFrameLock) {
 447                     firstFrameEvent = evt;
 448                     firstFrameTime = firstFrameEvent.getFrameData().getTimestamp();
 449                     firstFrameEvent.getFrameData().holdFrame(); // hold as long as we cache it, else we'll crash
 450                 }
 451             } else if (firstFrameEvent != null
 452                     && firstFrameTime != evt.getFrameData().getTimestamp()) {
 453                 // If this branch is entered then it cannot be the first frame.
 454                 // This means that the player must be in the PLAYING state as
 455                 // the first frame will arrive upon completion of prerolling.
 456                 // When playing, listeners should receive the current frame,
 457                 // not the first frame in the stream.
 458 
 459                 // Clear the cached first frame. Obtain the lock first to avoid
 460                 // a race condition with a listener newly being added.
 461                 synchronized (firstFrameLock) {
 462                     firstFrameEvent.getFrameData().releaseFrame();
 463                     firstFrameEvent = null;
 464                 }
 465             }
 466 
 467             // notify videoUpdateListeners
 468             for (ListIterator<WeakReference<VideoRendererListener>> it = videoUpdateListeners.listIterator(); it.hasNext();) {
 469                 VideoRendererListener l = it.next().get();
 470                 if (l != null) {
 471                     l.videoFrameUpdated(evt);
 472                 } else {
 473                     it.remove();
 474                 }
 475             }
 476             // done with the frame, we can release our hold now
 477             evt.getFrameData().releaseFrame();
 478 
 479             if (!videoFrameRateListeners.isEmpty()) {
 480                 // Decoded frame rate calculations.
 481                 double currentFrameTime = System.nanoTime() / (double) ONE_SECOND;
 482 
 483                 if (recomputeFrameRate) {
 484                     // First frame in new computation sequence.
 485                     recomputeFrameRate = false;
 486                     previousFrameTime = currentFrameTime;
 487                     numFramesSincePlaying = 1;
 488                 } else {
 489                     boolean fireFrameRateEvent = false;
 490 
 491                     if (numFramesSincePlaying == 1) {
 492                         // Second frame. Estimate the initial frame rate and
 493                         // set event flag.
 494                         meanFrameDuration = currentFrameTime - previousFrameTime;
 495                         if (meanFrameDuration > 0.0) {
 496                             decodedFrameRate = 1.0 / meanFrameDuration;
 497                             fireFrameRateEvent = true;
 498                         }
 499                     } else {
 500                         // Update decoded frame rate estimate using a moving
 501                         // average over encodedFrameRate frames.
 502                         double previousMeanFrameDuration = meanFrameDuration;
 503 
 504                         // Determine moving average length.
 505                         int movingAverageLength = encodedFrameRate != 0.0
 506                                 ? ((int) (encodedFrameRate + 0.5)) : NOMINAL_VIDEO_FPS;
 507 
 508                         // Claculate number of frames in current average.
 509                         long numFrames = numFramesSincePlaying < movingAverageLength
 510                                 ? numFramesSincePlaying : movingAverageLength;
 511 
 512                         // Update the mean frame duration.
 513                         meanFrameDuration = ((numFrames - 1) * previousMeanFrameDuration
 514                                 + currentFrameTime - previousFrameTime) / numFrames;
 515 
 516                         // If mean frame duration changed by more than 0.5 set
 517                         // event flag.
 518                         if (meanFrameDuration > 0.0
 519                                 && Math.abs(decodedFrameRate - 1.0 / meanFrameDuration) > 0.5) {
 520                             decodedFrameRate = 1.0 / meanFrameDuration;
 521                             fireFrameRateEvent = true;
 522                         }
 523                     }
 524 
 525                     if (fireFrameRateEvent) {
 526                         // Fire event.
 527                         for (ListIterator<WeakReference<VideoFrameRateListener>> it = videoFrameRateListeners.listIterator(); it.hasNext();) {
 528                             VideoFrameRateListener l = it.next().get();
 529                             if (l != null) {
 530                                 l.onFrameRateChanged(decodedFrameRate);
 531                             } else {
 532                                 it.remove();
 533                             }
 534                         }
 535                     }
 536 
 537                     // Update running values.
 538                     previousFrameTime = currentFrameTime;
 539                     numFramesSincePlaying++;
 540                 }
 541             }
 542         }
 543 
 544         private void HandleStateEvents(PlayerStateEvent evt) {
 545             playerState = evt.getState();
 546 
 547             recomputeFrameRate = PlayerState.PLAYING == evt.getState();
 548 
 549             switch (playerState) {
 550                 case READY:
 551                     onNativeInit();
 552                     sendFakeBufferProgressEvent();
 553                     break;
 554                 case PLAYING:
 555                     isMediaPulseEnabled.set(true);
 556                     break;
 557                 case STOPPED:
 558                 case FINISHED:
 559                     // Force a time update here to catch the time going to
 560                     // zero for STOPPED and any trailing markers for FINISHED.
 561                     doMediaPulseTask();
 562                 case PAUSED:
 563                 case STALLED:
 564                 case HALTED:
 565                     isMediaPulseEnabled.set(false);
 566                     break;
 567                 default:
 568                     break;
 569             }
 570 
 571             synchronized (cachedStateEvents) {
 572                 if (playerStateListeners.isEmpty()) {
 573                     // Cache event for processing when first listener registers.
 574                     cachedStateEvents.add(evt);
 575                     return;
 576                 }
 577             }
 578 
 579             for (ListIterator<WeakReference<PlayerStateListener>> it = playerStateListeners.listIterator(); it.hasNext();) {
 580                 PlayerStateListener listener = it.next().get();
 581                 if (listener != null) {
 582                     switch (playerState) {
 583                         case READY:
 584                             onNativeInit();
 585                             sendFakeBufferProgressEvent();
 586                             listener.onReady(evt);
 587                             break;
 588 
 589                         case PLAYING:
 590                             listener.onPlaying(evt);
 591                             break;
 592 
 593                         case PAUSED:
 594                             listener.onPause(evt);
 595                             break;
 596 
 597                         case STOPPED:
 598                             listener.onStop(evt);
 599                             break;
 600 
 601                         case STALLED:
 602                             listener.onStall(evt);
 603                             break;
 604 
 605                         case FINISHED:
 606                             listener.onFinish(evt);
 607                             break;
 608 
 609                         case HALTED:
 610                             listener.onHalt(evt);
 611                             break;
 612 
 613                         default:
 614                             break;
 615                     }
 616                 } else {
 617                     it.remove();
 618                 }
 619             }
 620         }
 621 
 622         private void HandlePlayerTimeEvents(PlayerTimeEvent evt) {
 623             synchronized (cachedTimeEvents) {
 624                 if (playerTimeListeners.isEmpty()) {
 625                     // Cache event for processing when first listener registers.
 626                     cachedTimeEvents.add(evt);
 627                     return;
 628                 }
 629             }
 630 
 631             for (ListIterator<WeakReference<PlayerTimeListener>> it = playerTimeListeners.listIterator(); it.hasNext();) {
 632                 PlayerTimeListener listener = it.next().get();
 633                 if (listener != null) {
 634                     listener.onDurationChanged(evt.getTime());
 635                 } else {
 636                     it.remove();
 637                 }
 638             }
 639         }
 640 
 641         private void HandleFrameSizeChangedEvents(FrameSizeChangedEvent evt) {
 642             frameWidth = evt.getWidth();
 643             frameHeight = evt.getHeight();
 644             Logger.logMsg(Logger.DEBUG, "** Frame size changed (" + frameWidth + ", " + frameHeight + ")");
 645             for (ListIterator<WeakReference<VideoTrackSizeListener>> it = videoTrackSizeListeners.listIterator(); it.hasNext();) {
 646                 VideoTrackSizeListener listener = it.next().get();
 647                 if (listener != null) {
 648                     listener.onSizeChanged(frameWidth, frameHeight);
 649                 } else {
 650                     it.remove();
 651                 }
 652             }
 653         }
 654 
 655         private void HandleTrackEvents(TrackEvent evt) {
 656             media.addTrack(evt.getTrack());
 657 
 658             if (evt.getTrack() instanceof VideoTrack) {
 659                 encodedFrameRate = ((VideoTrack) evt.getTrack()).getEncodedFrameRate();
 660             }
 661         }
 662 
 663         private void HandleMarkerEvents(MarkerEvent evt) {
 664             for (ListIterator<WeakReference<MarkerListener>> it = markerListeners.listIterator(); it.hasNext();) {
 665                 MarkerListener listener = it.next().get();
 666                 if (listener != null) {
 667                     listener.onMarker(evt);
 668                 } else {
 669                     it.remove();
 670                 }
 671             }
 672         }
 673 
 674         private void HandleWarningEvents(WarningEvent evt) {
 675             Logger.logMsg(Logger.WARNING, evt.getSource() + evt.getMessage());
 676         }
 677 
 678         private void HandleErrorEvents(MediaErrorEvent evt) {
 679             Logger.logMsg(Logger.ERROR, evt.getMessage());
 680 
 681             synchronized (cachedErrorEvents) {
 682                 if (errorListeners.isEmpty()) {
 683                     // cache error events until at least one listener is added
 684                     cachedErrorEvents.add(evt);
 685                     return;
 686                 }
 687             }
 688 
 689             for (ListIterator<WeakReference<MediaErrorListener>> it = errorListeners.listIterator(); it.hasNext();) {
 690                 MediaErrorListener l = it.next().get();
 691                 if (l != null) {
 692                     l.onError(evt.getSource(), evt.getErrorCode(), evt.getMessage());
 693                 } else {
 694                     it.remove();
 695                 }
 696             }
 697         }
 698 
 699         private void HandleBufferEvents(BufferProgressEvent evt) {
 700             synchronized (cachedBufferEvents) {
 701                 if (bufferListeners.isEmpty()) {
 702                     // Cache event for processing when first listener registers.
 703                     cachedBufferEvents.add(evt);
 704                     return;
 705                 }
 706             }
 707 
 708             for (ListIterator<WeakReference<BufferListener>> it = bufferListeners.listIterator(); it.hasNext();) {
 709                 BufferListener listener = it.next().get();
 710                 if (listener != null) {
 711                     listener.onBufferProgress(evt);
 712                 } else {
 713                     it.remove();
 714                 }
 715             }
 716         }
 717 
 718         private void HandleAudioSpectrumEvents(AudioSpectrumEvent evt) {
 719             for (ListIterator<WeakReference<AudioSpectrumListener>> it = audioSpectrumListeners.listIterator(); it.hasNext();) {
 720                 AudioSpectrumListener listener = it.next().get();
 721                 if (listener != null) {
 722                     listener.onAudioSpectrumEvent(evt);
 723                 } else {
 724                     it.remove();
 725                 }
 726             }
 727         }
 728 
 729         /**
 730          * Puts an event to the EventQuery.
 731          */
 732         public void postEvent(PlayerEvent event) {
 733             if (eventQueue != null) {
 734                 eventQueue.offer(event);
 735             }
 736         }
 737 
 738         /**
 739          * Signals the thread to terminate.
 740          */
 741         public void terminateLoop() {
 742             stopped = true;
 743             // put an event to unblock eventQueue.take()
 744             try {
 745                 eventQueue.put(new PlayerEvent());
 746             } catch(InterruptedException ex) {}
 747         }
 748 
 749         private void sendFakeBufferProgressEvent() {
 750             // Send fake 100% buffer progress event for HLS or !http protcol
 751             String contentType = media.getLocator().getContentType();
 752             String protocol = media.getLocator().getProtocol();
 753             if ((contentType != null && (contentType.equals(MediaUtils.CONTENT_TYPE_M3U) || contentType.equals(MediaUtils.CONTENT_TYPE_M3U8)))
 754                     || (protocol != null && !protocol.equals("http") && !protocol.equals("https"))) {
 755                 HandleBufferEvents(new BufferProgressEvent(getDuration(), 0, 1, 1));
 756             }
 757         }
 758     }
 759 
 760     /**
 761      * Internal function to get called when the native player is ready.
 762      */
 763     private synchronized void onNativeInit() {
 764         try {
 765             playerInit();
 766         } catch (MediaException me) {
 767             sendPlayerMediaErrorEvent(me.getMediaError().code());
 768         }
 769     }
 770 
 771     //**************************************************************************
 772     //***** MediaPlayer implementation
 773     //**************************************************************************
 774     //***** Listener (un)registration.
 775     @Override
 776     public void addMediaErrorListener(MediaErrorListener listener) {
 777         if (listener != null) {
 778             this.errorListeners.add(new WeakReference<>(listener));
 779 
 780             synchronized (cachedErrorEvents) {
 781                 if (!cachedErrorEvents.isEmpty() && !errorListeners.isEmpty()) {
 782                     cachedErrorEvents.stream().forEach((evt) -> {
 783                         sendPlayerEvent(evt);
 784                     });
 785                     cachedErrorEvents.clear();
 786                 }
 787             }
 788         }
 789     }
 790 
 791     @Override
 792     public void removeMediaErrorListener(MediaErrorListener listener) {
 793         if (listener != null) {
 794             for (ListIterator<WeakReference<MediaErrorListener>> it = errorListeners.listIterator(); it.hasNext();) {
 795                 MediaErrorListener l = it.next().get();
 796                 if (l == null || l == listener) {
 797                     it.remove();
 798                 }
 799             }
 800         }
 801     }
 802 
 803     @Override
 804     public void addMediaPlayerListener(PlayerStateListener listener) {
 805         if (listener != null) {
 806             synchronized (cachedStateEvents) {
 807                 if (!cachedStateEvents.isEmpty() && playerStateListeners.isEmpty()) {
 808                     // Forward all cached state events to first listener to register.
 809                     Iterator<PlayerStateEvent> events = cachedStateEvents.iterator();
 810                     while (events.hasNext()) {
 811                         PlayerStateEvent evt = events.next();
 812                         switch (evt.getState()) {
 813                             case READY:
 814                                 listener.onReady(evt);
 815                                 break;
 816                             case PLAYING:
 817                                 listener.onPlaying(evt);
 818                                 break;
 819                             case PAUSED:
 820                                 listener.onPause(evt);
 821                                 break;
 822                             case STOPPED:
 823                                 listener.onStop(evt);
 824                                 break;
 825                             case STALLED:
 826                                 listener.onStall(evt);
 827                                 break;
 828                             case FINISHED:
 829                                 listener.onFinish(evt);
 830                                 break;
 831                             case HALTED:
 832                                 listener.onHalt(evt);
 833                                 break;
 834                             default:
 835                                 break;
 836                         }
 837                     }
 838 
 839                     // Clear state event cache.
 840                     cachedStateEvents.clear();
 841                 }
 842 
 843                 playerStateListeners.add(new WeakReference(listener));
 844             }
 845         }
 846     }
 847 
 848     @Override
 849     public void removeMediaPlayerListener(PlayerStateListener listener) {
 850         if (listener != null) {
 851             for (ListIterator<WeakReference<PlayerStateListener>> it = playerStateListeners.listIterator(); it.hasNext();) {
 852                 PlayerStateListener l = it.next().get();
 853                 if (l == null || l == listener) {
 854                     it.remove();
 855                 }
 856             }
 857         }
 858     }
 859 
 860     @Override
 861     public void addMediaTimeListener(PlayerTimeListener listener) {
 862         if (listener != null) {
 863             synchronized (cachedTimeEvents) {
 864                 if (!cachedTimeEvents.isEmpty() && playerTimeListeners.isEmpty()) {
 865                     // Forward all cached time events to first listener to register.
 866                     Iterator<PlayerTimeEvent> events = cachedTimeEvents.iterator();
 867                     while (events.hasNext()) {
 868                         PlayerTimeEvent evt = events.next();
 869                         listener.onDurationChanged(evt.getTime());
 870                     }
 871 
 872                     // Clear time event cache.
 873                     cachedTimeEvents.clear();
 874                 } else {
 875                     // Let listener to know about duration
 876                     double duration = getDuration();
 877                     if (duration != Double.POSITIVE_INFINITY) {
 878                         listener.onDurationChanged(duration);
 879                     }
 880                 }
 881 
 882                 playerTimeListeners.add(new WeakReference(listener));
 883             }
 884         }
 885     }
 886 
 887     @Override
 888     public void removeMediaTimeListener(PlayerTimeListener listener) {
 889         if (listener != null) {
 890             for (ListIterator<WeakReference<PlayerTimeListener>> it = playerTimeListeners.listIterator(); it.hasNext();) {
 891                 PlayerTimeListener l = it.next().get();
 892                 if (l == null || l == listener) {
 893                     it.remove();
 894                 }
 895             }
 896         }
 897     }
 898 
 899     @Override
 900     public void addVideoTrackSizeListener(VideoTrackSizeListener listener) {
 901         if (listener != null) {
 902             if (frameWidth != -1 && frameHeight != -1) {
 903                 listener.onSizeChanged(frameWidth, frameHeight);
 904             }
 905             videoTrackSizeListeners.add(new WeakReference(listener));
 906         }
 907     }
 908 
 909     @Override
 910     public void removeVideoTrackSizeListener(VideoTrackSizeListener listener) {
 911         if (listener != null) {
 912             for (ListIterator<WeakReference<VideoTrackSizeListener>> it = videoTrackSizeListeners.listIterator(); it.hasNext();) {
 913                 VideoTrackSizeListener l = it.next().get();
 914                 if (l == null || l == listener) {
 915                     it.remove();
 916                 }
 917             }
 918         }
 919     }
 920 
 921     @Override
 922     public void addMarkerListener(MarkerListener listener) {
 923         if (listener != null) {
 924             markerListeners.add(new WeakReference(listener));
 925         }
 926     }
 927 
 928     @Override
 929     public void removeMarkerListener(MarkerListener listener) {
 930         if (listener != null) {
 931             for (ListIterator<WeakReference<MarkerListener>> it = markerListeners.listIterator(); it.hasNext();) {
 932                 MarkerListener l = it.next().get();
 933                 if (l == null || l == listener) {
 934                     it.remove();
 935                 }
 936             }
 937         }
 938     }
 939 
 940     @Override
 941     public void addBufferListener(BufferListener listener) {
 942         if (listener != null) {
 943             synchronized (cachedBufferEvents) {
 944                 if (!cachedBufferEvents.isEmpty() && bufferListeners.isEmpty()) {
 945                     cachedBufferEvents.stream().forEach((evt) -> {
 946                         listener.onBufferProgress(evt);
 947                     });
 948                     // Clear buffer event cache.
 949                     cachedBufferEvents.clear();
 950                 }
 951 
 952                 bufferListeners.add(new WeakReference(listener));
 953             }
 954         }
 955     }
 956 
 957     @Override
 958     public void removeBufferListener(BufferListener listener) {
 959         if (listener != null) {
 960             for (ListIterator<WeakReference<BufferListener>> it = bufferListeners.listIterator(); it.hasNext();) {
 961                 BufferListener l = it.next().get();
 962                 if (l == null || l == listener) {
 963                     it.remove();
 964                 }
 965             }
 966         }
 967     }
 968 
 969     @Override
 970     public void addAudioSpectrumListener(AudioSpectrumListener listener) {
 971         if (listener != null) {
 972             audioSpectrumListeners.add(new WeakReference(listener));
 973         }
 974     }
 975 
 976     @Override
 977     public void removeAudioSpectrumListener(AudioSpectrumListener listener) {
 978         if (listener != null) {
 979             for (ListIterator<WeakReference<AudioSpectrumListener>> it = audioSpectrumListeners.listIterator(); it.hasNext();) {
 980                 AudioSpectrumListener l = it.next().get();
 981                 if (l == null || l == listener) {
 982                     it.remove();
 983                 }
 984             }
 985         }
 986     }
 987 
 988     //***** Control functions
 989     @Override
 990     public VideoRenderControl getVideoRenderControl() {
 991         return videoRenderControl;
 992     }
 993 
 994     @Override
 995     public Media getMedia() {
 996         return media;
 997     }
 998 
 999     @Override
1000     public void setAudioSyncDelay(long delay) {
1001         try {
1002             playerSetAudioSyncDelay(delay);
1003         } catch (MediaException me) {
1004             sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1005         }
1006     }
1007 
1008     @Override
1009     public long getAudioSyncDelay() {
1010         try {
1011             return playerGetAudioSyncDelay();
1012         } catch (MediaException me) {
1013             sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1014         }
1015         return 0;
1016     }
1017 
1018     @Override
1019     public void play() {
1020         try {
1021             if (isStartTimeUpdated) {
1022                 playerSeek(startTime);
1023             }
1024             isMediaPulseEnabled.set(true);
1025             playerPlay();
1026         } catch (MediaException me) {
1027             sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1028         }
1029     }
1030 
1031     @Override
1032     public void stop() {
1033         try {
1034             playerStop();
1035             playerSeek(startTime);
1036         } catch (MediaException me) {
1037 //            sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1038             MediaUtils.warning(this, "stop() failed!");
1039         }
1040     }
1041 
1042     @Override
1043     public void pause() {
1044         try {
1045             playerPause();
1046         } catch (MediaException me) {
1047             sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1048         }
1049     }
1050 
1051     @Override
1052     public float getRate() {
1053         try {
1054             return playerGetRate();
1055         } catch (MediaException me) {
1056             sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1057         }
1058         return 0;
1059     }
1060 
1061     //***** Public properties
1062     @Override
1063     public void setRate(float rate) {
1064         try {
1065             playerSetRate(rate);
1066         } catch (MediaException me) {
1067 //            sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1068             MediaUtils.warning(this, "setRate(" + rate + ") failed!");
1069         }
1070     }
1071 
1072     @Override
1073     public double getPresentationTime() {
1074         try {
1075             return playerGetPresentationTime();
1076         } catch (MediaException me) {
1077 //            sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1078         }
1079         return -1.0;
1080     }
1081 
1082     @Override
1083     public float getVolume() {
1084         try {
1085             return playerGetVolume();
1086         } catch (MediaException me) {
1087             sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1088         }
1089         return 0;
1090     }
1091 
1092     @Override
1093     public void setVolume(float vol) {
1094         if (vol < 0.0F) {
1095             vol = 0.0F;
1096         } else if (vol > 1.0F) {
1097             vol = 1.0F;
1098         }
1099 
1100         try {
1101             playerSetVolume(vol);
1102         } catch (MediaException me) {
1103             sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1104         }
1105     }
1106 
1107     @Override
1108     public boolean getMute() {
1109         try {
1110             return playerGetMute();
1111         } catch (MediaException me) {
1112             sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1113         }
1114         return false;
1115     }
1116 
1117     /**
1118      * Enables/disable mute. If mute is enabled then disabled, the previous
1119      * volume goes into effect.
1120      */
1121     @Override
1122     public void setMute(boolean enable) {
1123         try {
1124             playerSetMute(enable);
1125         } catch (MediaException me) {
1126             sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1127         }
1128     }
1129 
1130     @Override
1131     public float getBalance() {
1132         try {
1133             return playerGetBalance();
1134         } catch (MediaException me) {
1135             sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1136         }
1137         return 0;
1138     }
1139 
1140     @Override
1141     public void setBalance(float bal) {
1142         if (bal < -1.0F) {
1143             bal = -1.0F;
1144         } else if (bal > 1.0F) {
1145             bal = 1.0F;
1146         }
1147 
1148         try {
1149             playerSetBalance(bal);
1150         } catch (MediaException me) {
1151             sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1152         }
1153     }
1154 
1155     @Override
1156     public abstract AudioEqualizer getEqualizer();
1157 
1158     @Override
1159     public abstract AudioSpectrum getAudioSpectrum();
1160 
1161     @Override
1162     public double getDuration() {
1163         try {
1164             return playerGetDuration();
1165         } catch (MediaException me) {
1166 //            sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1167         }
1168         return Double.POSITIVE_INFINITY;
1169     }
1170 
1171     /**
1172      * Gets the time within the duration of the media to start playing.
1173      */
1174     @Override
1175     public double getStartTime() {
1176         return startTime;
1177     }
1178 
1179     /**
1180      * Sets the start time within the media to play.
1181      */
1182     @Override
1183     public void setStartTime(double startTime) {
1184         try {
1185             markerLock.lock();
1186             this.startTime = startTime;
1187             if (playerState != PlayerState.PLAYING && playerState != PlayerState.FINISHED && playerState != PlayerState.STOPPED) {
1188                 playerSeek(startTime);
1189             } else if (playerState == PlayerState.STOPPED) {
1190                 isStartTimeUpdated = true;
1191             }
1192         } finally {
1193             markerLock.unlock();
1194         }
1195     }
1196 
1197     /**
1198      * Gets the time within the duration of the media to stop playing.
1199      */
1200     @Override
1201     public double getStopTime() {
1202         return stopTime;
1203     }
1204 
1205     /**
1206      * Sets the stop time within the media to stop playback.
1207      */
1208     @Override
1209     public void setStopTime(double stopTime) {
1210         try {
1211             markerLock.lock();
1212             this.stopTime = stopTime;
1213             isStopTimeSet = true;
1214             createMediaPulse();
1215         } finally {
1216             markerLock.unlock();
1217         }
1218     }
1219 
1220     @Override
1221     public void seek(double streamTime) {
1222         if (playerState == PlayerState.STOPPED) {
1223             return; // No seek in stopped state
1224         }
1225 
1226         if (streamTime < 0.0) {
1227             streamTime = 0.0;
1228         } else {
1229             double duration = getDuration();
1230             if (duration >= 0.0 && streamTime > duration) {
1231                 streamTime = duration;
1232             }
1233         }
1234 
1235         if (!isMediaPulseEnabled.get()) {
1236             if ((playerState == PlayerState.PLAYING
1237                     || playerState == PlayerState.PAUSED
1238                     || playerState == PlayerState.FINISHED)
1239                     && getStartTime() <= streamTime && streamTime <= getStopTime()) {
1240                 isMediaPulseEnabled.set(true);
1241             }
1242         }
1243 
1244         markerLock.lock();
1245         try {
1246             timeBeforeSeek = getPresentationTime();
1247             timeAfterSeek = streamTime;
1248             checkSeek = timeBeforeSeek != timeAfterSeek;
1249             previousTime = streamTime;
1250             firedMarkerTime = -1.0;
1251 //            System.out.println("seek @ "+System.currentTimeMillis());
1252 //            System.out.println("seek to "+streamTime+" previousTime "+previousTime);
1253 
1254             try {
1255                 playerSeek(streamTime);
1256             } catch (MediaException me) {
1257                 //sendPlayerEvent(new MediaErrorEvent(this, me.getMediaError()));
1258                 MediaUtils.warning(this, "seek(" + streamTime + ") failed!");
1259             }
1260         } finally {
1261             markerLock.unlock();
1262         }
1263     }
1264 
1265     protected abstract long playerGetAudioSyncDelay() throws MediaException;
1266 
1267     protected abstract void playerSetAudioSyncDelay(long delay) throws MediaException;
1268 
1269     protected abstract void playerPlay() throws MediaException;
1270 
1271     protected abstract void playerStop() throws MediaException;
1272 
1273     protected abstract void playerPause() throws MediaException;
1274 
1275     protected abstract void playerFinish() throws MediaException;
1276 
1277     protected abstract float playerGetRate() throws MediaException;
1278 
1279     protected abstract void playerSetRate(float rate) throws MediaException;
1280 
1281     protected abstract double playerGetPresentationTime() throws MediaException;
1282 
1283     protected abstract boolean playerGetMute() throws MediaException;
1284 
1285     protected abstract void playerSetMute(boolean state) throws MediaException;
1286 
1287     protected abstract float playerGetVolume() throws MediaException;
1288 
1289     protected abstract void playerSetVolume(float volume) throws MediaException;
1290 
1291     protected abstract float playerGetBalance() throws MediaException;
1292 
1293     protected abstract void playerSetBalance(float balance) throws MediaException;
1294 
1295     protected abstract double playerGetDuration() throws MediaException;
1296 
1297     protected abstract void playerSeek(double streamTime) throws MediaException;
1298 
1299     protected abstract void playerInit() throws MediaException;
1300 
1301     protected abstract void playerDispose();
1302 
1303     /**
1304      * Retrieves the current {@link PlayerState state} of the player.
1305      *
1306      * @return the current player state.
1307      */
1308     @Override
1309     public PlayerState getState() {
1310         return playerState;
1311     }
1312 
1313     @Override
1314     final public void dispose() {
1315         disposeLock.lock();
1316         try {
1317             if (!isDisposed) {
1318                 // Terminate event firing
1319                 destroyMediaPulse();
1320 
1321                 if (eventLoop != null) {
1322                     eventLoop.terminateLoop();
1323                     eventLoop = null;
1324                 }
1325 
1326                 synchronized (firstFrameLock) {
1327                     if (firstFrameEvent != null) {
1328                         firstFrameEvent.getFrameData().releaseFrame();
1329                         firstFrameEvent = null;
1330                     }
1331                 }
1332 
1333                 // Terminate native layer
1334                 playerDispose();
1335 
1336                 // Dispose media object and clear reference
1337                 if (media != null) {
1338                     media.dispose();
1339                     media = null;
1340                 }
1341 
1342                 if (videoUpdateListeners != null) {
1343                     for (ListIterator<WeakReference<VideoRendererListener>> it = videoUpdateListeners.listIterator(); it.hasNext();) {
1344                         VideoRendererListener l = it.next().get();
1345                         if (l != null) {
1346                             l.releaseVideoFrames();
1347                         } else {
1348                             it.remove();
1349                         }
1350                     }
1351 
1352                     videoUpdateListeners.clear();
1353                 }
1354 
1355                 if (playerStateListeners != null) {
1356                     playerStateListeners.clear();
1357                 }
1358 
1359                 if (videoTrackSizeListeners != null) {
1360                     videoTrackSizeListeners.clear();
1361                 }
1362 
1363                 if (videoFrameRateListeners != null) {
1364                     videoFrameRateListeners.clear();
1365                 }
1366 
1367                 if (cachedStateEvents != null) {
1368                     cachedStateEvents.clear();
1369                 }
1370 
1371                 if (cachedTimeEvents != null) {
1372                     cachedTimeEvents.clear();
1373                 }
1374 
1375                 if (cachedBufferEvents != null) {
1376                     cachedBufferEvents.clear();
1377                 }
1378 
1379                 if (errorListeners != null) {
1380                     errorListeners.clear();
1381                 }
1382 
1383                 if (playerTimeListeners != null) {
1384                     playerTimeListeners.clear();
1385                 }
1386 
1387                 if (markerListeners != null) {
1388                     markerListeners.clear();
1389                 }
1390 
1391                 if (bufferListeners != null) {
1392                     bufferListeners.clear();
1393                 }
1394 
1395                 if (audioSpectrumListeners != null) {
1396                     audioSpectrumListeners.clear();
1397                 }
1398 
1399                 if (videoRenderControl != null) {
1400                     videoRenderControl = null;
1401                 }
1402 
1403                 if (onDispose != null) {
1404                     onDispose.run();
1405                 }
1406 
1407                 isDisposed = true;
1408             }
1409         } finally {
1410             disposeLock.unlock();
1411         }
1412     }
1413 
1414     @Override
1415     public boolean isErrorEventCached() {
1416         synchronized (cachedErrorEvents) {
1417             if (cachedErrorEvents.isEmpty()) {
1418                 return false;
1419             } else {
1420                 return true;
1421             }
1422         }
1423     }
1424 
1425     //**************************************************************************
1426     //***** Non-JNI methods called by the native layer. These methods are called
1427     //***** from the native layer via the invocation API. Their purpose is to
1428     //***** dispatch certain events to the Java layer. Each of these methods
1429     //***** posts an event on the <code>EventQueueThread</code> which in turn
1430     //***** forwards the event to any registered listeners.
1431     //**************************************************************************
1432     protected void sendWarning(int warningCode, String warningMessage) {
1433         if (eventLoop != null) {
1434             String message = String.format(MediaUtils.NATIVE_MEDIA_WARNING_FORMAT,
1435                     warningCode);
1436             if (warningMessage != null) {
1437                 message += ": " + warningMessage;
1438             }
1439             eventLoop.postEvent(new WarningEvent(this, message));
1440         }
1441     }
1442 
1443     protected void sendPlayerEvent(PlayerEvent evt) {
1444         if (eventLoop != null) {
1445             eventLoop.postEvent(evt);
1446         }
1447     }
1448 
1449     protected void sendPlayerHaltEvent(String message, double time) {
1450         // Log the error.  Since these are most likely playback engine message (e.g. GStreamer or PacketVideo),
1451         // it makes no sense to propogate it above.
1452         Logger.logMsg(Logger.ERROR, message);
1453 
1454         if (eventLoop != null) {
1455             eventLoop.postEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.HALTED, time, message));
1456         }
1457     }
1458 
1459     protected void sendPlayerMediaErrorEvent(int errorCode) {
1460         sendPlayerEvent(new MediaErrorEvent(this, MediaError.getFromCode(errorCode)));
1461     }
1462 
1463     protected void sendPlayerStateEvent(int eventID, double time) {
1464         switch (eventID) {
1465             case eventPlayerReady:
1466                 sendPlayerEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.READY, time));
1467                 break;
1468             case eventPlayerPlaying:
1469                 sendPlayerEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.PLAYING, time));
1470                 break;
1471             case eventPlayerPaused:
1472                 sendPlayerEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.PAUSED, time));
1473                 break;
1474             case eventPlayerStopped:
1475                 sendPlayerEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.STOPPED, time));
1476                 break;
1477             case eventPlayerStalled:
1478                 sendPlayerEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.STALLED, time));
1479                 break;
1480             case eventPlayerFinished:
1481                 sendPlayerEvent(new PlayerStateEvent(PlayerStateEvent.PlayerState.FINISHED, time));
1482                 break;
1483             default:
1484                 break;
1485         }
1486     }
1487 
1488     protected void sendNewFrameEvent(long nativeRef) {
1489         NativeVideoBuffer newFrameData = NativeVideoBuffer.createVideoBuffer(nativeRef);
1490         // createVideoBuffer puts a hold on the frame
1491         // we need to keep that hold until the event thread can process this event
1492         sendPlayerEvent(new NewFrameEvent(newFrameData));
1493     }
1494 
1495     protected void sendFrameSizeChangedEvent(int width, int height) {
1496         sendPlayerEvent(new FrameSizeChangedEvent(width, height));
1497     }
1498 
1499     protected void sendAudioTrack(boolean enabled, long trackID, String name, int encoding,
1500             String language, int numChannels,
1501             int channelMask, float sampleRate) {
1502         Locale locale = null;
1503         if (!language.equals("und")) {
1504             locale = new Locale(language);
1505         }
1506 
1507         Track track = new AudioTrack(enabled, trackID, name,
1508                 locale, Encoding.toEncoding(encoding),
1509                 numChannels, channelMask, sampleRate);
1510 
1511         TrackEvent evt = new TrackEvent(track);
1512 
1513         sendPlayerEvent(evt);
1514     }
1515 
1516     protected void sendVideoTrack(boolean enabled, long trackID, String name, int encoding,
1517             int width, int height, float frameRate,
1518             boolean hasAlphaChannel) {
1519         // No locale (currently) for video, so pass null
1520         Track track = new VideoTrack(enabled, trackID, name, null,
1521                 Encoding.toEncoding(encoding),
1522                 new VideoResolution(width, height), frameRate, hasAlphaChannel);
1523 
1524         TrackEvent evt = new TrackEvent(track);
1525 
1526         sendPlayerEvent(evt);
1527     }
1528 
1529     protected void sendSubtitleTrack(boolean enabled, long trackID, String name,
1530             int encoding, String language)
1531     {
1532         Locale locale = null;
1533         if (null != language) {
1534             locale = new Locale(language);
1535         }
1536         Track track = new SubtitleTrack(enabled, trackID, name, locale,
1537                 Encoding.toEncoding(encoding));
1538 
1539         sendPlayerEvent(new TrackEvent(track));
1540     }
1541 
1542     protected void sendMarkerEvent(String name, double time) {
1543         sendPlayerEvent(new MarkerEvent(name, time));
1544     }
1545 
1546     protected void sendDurationUpdateEvent(double duration) {
1547         sendPlayerEvent(new PlayerTimeEvent(duration));
1548     }
1549 
1550     protected void sendBufferProgressEvent(double clipDuration, long bufferStart, long bufferStop, long bufferPosition) {
1551         sendPlayerEvent(new BufferProgressEvent(clipDuration, bufferStart, bufferStop, bufferPosition));
1552     }
1553 
1554     protected void sendAudioSpectrumEvent(double timestamp, double duration) {
1555         sendPlayerEvent(new AudioSpectrumEvent(getAudioSpectrum(), timestamp, duration));
1556     }
1557 
1558     @Override
1559     public void markerStateChanged(boolean hasMarkers) {
1560         if (hasMarkers) {
1561             markerLock.lock();
1562             try {
1563                 previousTime = getPresentationTime();
1564             } finally {
1565                 markerLock.unlock();
1566             }
1567             createMediaPulse();
1568         } else {
1569             if (!isStopTimeSet) {
1570                 destroyMediaPulse();
1571             }
1572         }
1573     }
1574 
1575     private void createMediaPulse() {
1576         mediaPulseLock.lock();
1577         try {
1578             if (mediaPulseTimer == null) {
1579                 mediaPulseTimer = new Timer(true);
1580                 mediaPulseTimer.scheduleAtFixedRate(new MediaPulseTask(this), 0, 40 /*
1581                          * period ms
1582                          */);
1583             }
1584         } finally {
1585             mediaPulseLock.unlock();
1586         }
1587     }
1588 
1589     private void destroyMediaPulse() {
1590         mediaPulseLock.lock();
1591         try {
1592             if (mediaPulseTimer != null) {
1593                 mediaPulseTimer.cancel();
1594                 mediaPulseTimer = null;
1595             }
1596         } finally {
1597             mediaPulseLock.unlock();
1598         }
1599     }
1600 
1601     boolean doMediaPulseTask() {
1602         if (this.isMediaPulseEnabled.get()) {
1603             disposeLock.lock();
1604 
1605             if (isDisposed) {
1606                 disposeLock.unlock();
1607                 return false;
1608             }
1609 
1610             double thisTime = getPresentationTime();
1611 
1612             markerLock.lock();
1613 
1614             try {
1615                 //System.out.println("Media pulse @ pts "+thisTime+" previous "+previousTime);
1616 
1617                 if (checkSeek) {
1618                     if (timeAfterSeek > timeBeforeSeek) {
1619                         // Forward seek
1620                         if (thisTime >= timeAfterSeek) {
1621 //                        System.out.println("bail 1");
1622                             checkSeek = false;
1623                         } else {
1624                             return true;
1625                         }
1626                     } else if (timeAfterSeek < timeBeforeSeek) {
1627                         // Backward seek
1628                         if (thisTime >= timeBeforeSeek) {
1629 //                        System.out.println("bail 2");
1630                             return true;
1631                         } else {
1632                             checkSeek = false;
1633                         }
1634                     }
1635                 }
1636 
1637                 Map.Entry<Double, String> marker = media.getNextMarker(previousTime, true);
1638 //                System.out.println("marker "+marker);
1639 //                System.out.println("Checking: " + previousTime + " " + thisTime + " "
1640 //                        + getStartTime() + " " + getStopTime() + " "
1641 //                        + marker.getKey());
1642 
1643                 while (marker != null) {
1644                     double nextMarkerTime = marker.getKey();
1645                     if (nextMarkerTime > thisTime) {
1646                         break;
1647                     } else if (nextMarkerTime != firedMarkerTime
1648                             && nextMarkerTime >= previousTime
1649                             && nextMarkerTime >= getStartTime()
1650                             && nextMarkerTime <= getStopTime()) {
1651 //                            System.out.println("Firing: "+previousTime+" "+thisTime+" "+
1652 //                                    getStartTime()+" "+getStopTime()+" "+
1653 //                                    nextMarkerTime);
1654                         MarkerEvent evt = new MarkerEvent(marker.getValue(), nextMarkerTime);
1655                         for (ListIterator<WeakReference<MarkerListener>> it = markerListeners.listIterator(); it.hasNext();) {
1656                             MarkerListener listener = it.next().get();
1657                             if (listener != null) {
1658                                 listener.onMarker(evt);
1659                             } else {
1660                                 it.remove();
1661                             }
1662                         }
1663                         firedMarkerTime = nextMarkerTime;
1664                     }
1665                     marker = media.getNextMarker(nextMarkerTime, false);
1666                 }
1667 
1668                 previousTime = thisTime;
1669 
1670                 // Do stopTime
1671                 if (isStopTimeSet && thisTime >= stopTime) {
1672                     playerFinish();
1673                 }
1674             } finally {
1675                 disposeLock.unlock();
1676                 markerLock.unlock();
1677             }
1678         }
1679 
1680         return true;
1681     }
1682 
1683     /* Audio EQ and spectrum creation, used by sub-classes */
1684     protected AudioEqualizer createNativeAudioEqualizer(long nativeRef) {
1685         return new NativeAudioEqualizer(nativeRef);
1686     }
1687 
1688     protected AudioSpectrum createNativeAudioSpectrum(long nativeRef) {
1689         return new NativeAudioSpectrum(nativeRef);
1690     }
1691 }
1692 
1693 class MediaPulseTask extends TimerTask {
1694 
1695     WeakReference<NativeMediaPlayer> playerRef;
1696 
1697     MediaPulseTask(NativeMediaPlayer player) {
1698         playerRef = new WeakReference<>(player);
1699     }
1700 
1701     @Override
1702     public void run() {
1703         final NativeMediaPlayer player = playerRef.get();
1704         if (player != null) {
1705             if (!player.doMediaPulseTask()) {
1706                 cancel(); // Stop if doMediaPulseTask() returns false. False means doMediaPulseTask() cannot continue (like after dispose).cy
1707             }
1708         } else {
1709             cancel();
1710         }
1711     }
1712 }