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