1 /*
   2  * Copyright (c) 2011, 2018, 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.javafx.webkit.prism;
  27 
  28 import java.net.URI;
  29 import java.util.List;
  30 
  31 import com.sun.javafx.media.PrismMediaFrameHandler;
  32 import com.sun.media.jfxmedia.Media;
  33 import com.sun.media.jfxmedia.MediaManager;
  34 import com.sun.media.jfxmedia.MediaPlayer;
  35 import com.sun.media.jfxmedia.control.VideoDataBuffer;
  36 import com.sun.media.jfxmedia.events.BufferListener;
  37 import com.sun.media.jfxmedia.events.BufferProgressEvent;
  38 import com.sun.media.jfxmedia.events.MediaErrorListener;
  39 import com.sun.media.jfxmedia.events.NewFrameEvent;
  40 import com.sun.media.jfxmedia.events.PlayerStateEvent;
  41 import com.sun.media.jfxmedia.events.PlayerStateListener;
  42 import com.sun.media.jfxmedia.events.PlayerTimeListener;
  43 import com.sun.media.jfxmedia.events.VideoRendererListener;
  44 import com.sun.media.jfxmedia.events.VideoTrackSizeListener;
  45 import com.sun.media.jfxmedia.locator.Locator;
  46 import com.sun.media.jfxmedia.track.AudioTrack;
  47 import com.sun.media.jfxmedia.track.Track;
  48 import com.sun.media.jfxmedia.track.VideoTrack;
  49 import com.sun.prism.Graphics;
  50 import com.sun.prism.Texture;
  51 import com.sun.webkit.graphics.WCGraphicsContext;
  52 import com.sun.webkit.graphics.WCMediaPlayer;
  53 
  54 
  55 final class WCMediaPlayerImpl extends WCMediaPlayer
  56         implements PlayerStateListener, MediaErrorListener,
  57         VideoTrackSizeListener, BufferListener, PlayerTimeListener
  58 {
  59 
  60     // lock for fields access (player, createThread, frameHandler)
  61     private final Object lock = new Object();
  62 
  63     private volatile MediaPlayer player;
  64     private volatile CreateThread createThread;
  65     private volatile PrismMediaFrameHandler frameHandler;
  66 
  67     private final MediaFrameListener frameListener;
  68 
  69     // we need this flag to handle a case when 1st frame arrives before onReady
  70     private boolean gotFirstFrame = false;
  71 
  72     // 1: at the end (rate > 0); -1: at the begining (rate < 0)
  73     private int finished = 0;
  74 
  75     WCMediaPlayerImpl() {
  76         frameListener = new MediaFrameListener();
  77     }
  78 
  79     private MediaPlayer getPlayer() {
  80         synchronized(lock) {
  81             if (createThread != null) {
  82                 return null;
  83             }
  84             return player;
  85         }
  86     }
  87 
  88     private void setPlayer(MediaPlayer p) {
  89         synchronized (lock) {
  90             player = p;
  91             installListeners();
  92             frameHandler = PrismMediaFrameHandler.getHandler(player);
  93         }
  94 
  95         finished = 0;
  96     }
  97 
  98     private final class CreateThread extends Thread {
  99         private boolean cancelled = false;
 100         private final String url;
 101         private final String userAgent;
 102         private CreateThread(String url, String userAgent) {
 103             this.url = url;
 104             this.userAgent = userAgent;
 105             gotFirstFrame = false;
 106         }
 107 
 108         @Override
 109         public void run() {
 110             log.fine("CreateThread: started, url={0}", url);
 111 
 112             notifyNetworkStateChanged(NETWORK_STATE_LOADING);
 113             notifyReadyStateChanged(READY_STATE_HAVE_NOTHING);
 114 
 115             MediaPlayer p = null;
 116 
 117             try {
 118                 Locator locator = new Locator(new URI(url));
 119                 if (userAgent != null) {
 120                     locator.setConnectionProperty("User-Agent", userAgent);
 121                 }
 122                 locator.init();
 123                     log.fine("CreateThread: locator created");
 124 
 125                 p = MediaManager.getPlayer(locator);
 126             } catch (Exception ex) {
 127                 log.warning("CreateThread ERROR: {0}", ex.toString());
 128                 ex.printStackTrace(System.out);
 129                 onError(this, 0, ex.getMessage());
 130                 return;
 131             }
 132 
 133             synchronized (lock) {
 134                 if (cancelled) {
 135                     log.fine("CreateThread: cancelled");
 136                     p.dispose();
 137                     return;
 138                 }
 139                 createThread = null;
 140                 setPlayer(p);
 141             }
 142             log.fine("CreateThread: completed");
 143         }
 144 
 145         private void cancel() {
 146             synchronized (lock) {
 147                 cancelled = true;
 148             }
 149         }
 150     }
 151 
 152 
 153     protected void load(String url, String userAgent) {
 154         synchronized (lock) {
 155             if (createThread != null) {
 156                 createThread.cancel();
 157             }
 158             disposePlayer();
 159             createThread = new CreateThread(url, userAgent);
 160         }
 161         // fx media player does not support loading only metadata,
 162         // so handle PRELOAD_METADATA as PRELOAD_AUTO (start loading)
 163         if (getPreload() != PRELOAD_NONE) {
 164             createThread.start();
 165         }
 166     }
 167 
 168     protected void cancelLoad() {
 169         synchronized (lock) {
 170             if (createThread != null) {
 171                 createThread.cancel();
 172             }
 173         }
 174         MediaPlayer p = getPlayer();
 175         if (p != null) {
 176             p.stop();
 177         }
 178         notifyNetworkStateChanged(NETWORK_STATE_EMPTY);
 179         notifyReadyStateChanged(READY_STATE_HAVE_NOTHING);
 180     }
 181 
 182     protected void disposePlayer() {
 183         MediaPlayer old;
 184         synchronized (lock) {
 185             removeListeners();
 186             old = player;
 187             player = null;
 188             if (frameHandler != null) {
 189                 frameHandler.releaseTextures();
 190                 frameHandler = null;
 191             }
 192         }
 193         if (old != null) {
 194             old.stop();
 195             old.dispose();
 196             old = null;
 197             if (frameListener != null) {
 198                 frameListener.releaseVideoFrames();
 199             }
 200         }
 201     }
 202 
 203     private void installListeners() {
 204         if (null != player) {
 205             player.addMediaPlayerListener(this);
 206             player.addMediaErrorListener(this);
 207             player.addVideoTrackSizeListener(this);
 208             player.addBufferListener(this);
 209             player.getVideoRenderControl().addVideoRendererListener(frameListener);
 210         }
 211     }
 212 
 213     private void removeListeners() {
 214         if (null != player) {
 215             player.removeMediaPlayerListener(this);
 216             player.removeMediaErrorListener(this);
 217             player.removeVideoTrackSizeListener(this);
 218             player.removeBufferListener(this);
 219             player.getVideoRenderControl().removeVideoRendererListener(frameListener);
 220         }
 221     }
 222 
 223     protected void prepareToPlay() {
 224         synchronized (lock) {
 225             if (player == null) {
 226                 // Only start the thread if it has been created but not yet started.
 227                 Thread t = createThread;
 228                 if (t != null && t.getState() == Thread.State.NEW) {
 229                     t.start();
 230                 }
 231             }
 232         }
 233     }
 234 
 235     protected void play() {
 236         MediaPlayer p = getPlayer();
 237         if (p != null) {
 238             p.play();
 239             // workaround: webkit doesn't like late notifications
 240             notifyPaused(false);
 241         }
 242     }
 243 
 244     protected void pause() {
 245         MediaPlayer p = getPlayer();
 246         if (p != null) {
 247             p.pause();
 248             // workaround: webkit doesn't like late notifications
 249             notifyPaused(true);
 250         }
 251     }
 252 
 253     protected float getCurrentTime() {
 254         MediaPlayer p = getPlayer();
 255         if (p == null) {
 256             return 0f;
 257         }
 258         return finished == 0 ? (float)p.getPresentationTime()
 259                 : finished > 0 ? (float)p.getDuration()
 260                 : 0f;
 261     }
 262 
 263     protected void seek(float time) {
 264         MediaPlayer p = getPlayer();
 265         if (p != null) {
 266             finished = 0;
 267             if (getReadyState() >= READY_STATE_HAVE_METADATA) {
 268                 notifySeeking(true, READY_STATE_HAVE_METADATA);
 269             } else {
 270                 notifySeeking(true, READY_STATE_HAVE_NOTHING);
 271             }
 272             p.seek(time);
 273 
 274             // fx media doesn't have a notification about seek completeness
 275             // while seeking fx player returns 0 as current time
 276             final float seekTime = time;
 277             Thread seekCompletedThread = new Thread(new Runnable() {
 278                 public void run() {
 279                     while (isSeeking()) {
 280                         MediaPlayer p = getPlayer();
 281                         if (p == null) {
 282                             break;
 283                         }
 284                         double cur = p.getPresentationTime();
 285                         if (seekTime < 0.01 || Math.abs(cur) >= 0.01) {
 286                             notifySeeking(false, READY_STATE_HAVE_ENOUGH_DATA);
 287                             break;
 288                         }
 289                         try {
 290                             Thread.sleep(10);
 291                         } catch (InterruptedException ex) {
 292                         }
 293                     }
 294                 }
 295             });
 296             seekCompletedThread.setDaemon(true);
 297             seekCompletedThread.start();
 298         }
 299     }
 300 
 301     protected void setRate(float rate) {
 302         MediaPlayer p = getPlayer();
 303         if (p != null) {
 304             p.setRate(rate);
 305         }
 306     }
 307 
 308     protected void setVolume(float volume) {
 309         MediaPlayer p = getPlayer();
 310         if (p != null) {
 311             p.setVolume(volume);
 312         }
 313     }
 314 
 315     protected void setMute(boolean mute) {
 316         MediaPlayer p = getPlayer();
 317         if (p != null) {
 318             p.setMute(mute);
 319         }
 320     }
 321 
 322     protected void setSize(int w, int h) {
 323         // nothing to do
 324     }
 325 
 326     protected void setPreservesPitch(boolean preserve) {
 327         // nothing to do
 328     }
 329 
 330     protected void renderCurrentFrame(WCGraphicsContext gc, int x, int y, int w, int h) {
 331         // TODO: need a render lock in MediaFrameHandler
 332         synchronized (lock) {
 333             renderImpl(gc, x, y, w, h);
 334         }
 335     }
 336 
 337 
 338     private void renderImpl(WCGraphicsContext gc, int x, int y, int w, int h) {
 339         log.finer(">>(Prism)renderImpl");
 340         Graphics g = (Graphics)gc.getPlatformGraphics();
 341 
 342         Texture texture = null;
 343         VideoDataBuffer currentFrame = frameListener.getLatestFrame();
 344 
 345         if (null != currentFrame) {
 346             if (null != frameHandler) {
 347                 texture = frameHandler.getTexture(g, currentFrame);
 348             }
 349             currentFrame.releaseFrame();
 350         }
 351 
 352         if (texture != null) {
 353             g.drawTexture(texture,
 354                     x, y, x + w, y + h,
 355                     0f, 0f, texture.getContentWidth(), texture.getContentHeight());
 356             texture.unlock();
 357         } else {
 358             log.finest("  (Prism)renderImpl, texture is null, draw black rect");
 359             gc.fillRect(x, y, w, h, 0xFF000000);
 360         }
 361         log.finer("<<(Prism)renderImpl");
 362     }
 363 
 364     // PlayerStateListener
 365     @Override
 366     public void onReady(PlayerStateEvent pse) {
 367         MediaPlayer p = getPlayer();
 368         log.fine("onReady");
 369         Media media = p.getMedia();
 370         boolean hasVideo = false;
 371         boolean hasAudio = false;
 372         if (media != null) {
 373             List<Track> tracks = media.getTracks();
 374             if (tracks != null) {
 375                 log.fine("{0} track(s) detected:", tracks.size());
 376                 for (Track track : tracks) {
 377                     if (track instanceof VideoTrack) {
 378                         hasVideo = true;
 379                     } else if (track instanceof AudioTrack) {
 380                         hasAudio = true;
 381                     }
 382                     log.fine("track: {0}", track);
 383                 }
 384             } else {
 385                 log.warning("onReady, tracks IS NULL");
 386             }
 387         } else {
 388             log.warning("onReady, media IS NULL");
 389         }
 390         log.fine("onReady, hasVideo:{0}, hasAudio: {1}", new Object[]{hasVideo, hasAudio});
 391         notifyReady(hasVideo, hasAudio, (float)p.getDuration());
 392 
 393         // if we have no video, report READY_STATE_HAVE_ENOUGH_DATA right now
 394         if (!hasVideo) {
 395             notifyReadyStateChanged(READY_STATE_HAVE_ENOUGH_DATA);
 396         } else {
 397             if (getReadyState() < READY_STATE_HAVE_METADATA) {
 398                 if (gotFirstFrame) {
 399                     notifyReadyStateChanged(READY_STATE_HAVE_ENOUGH_DATA);
 400                 } else {
 401                     notifyReadyStateChanged(READY_STATE_HAVE_METADATA);
 402                 }
 403             }
 404         }
 405     }
 406 
 407     @Override
 408     public void onPlaying(PlayerStateEvent pse) {
 409         log.fine("onPlaying");
 410         notifyPaused(false);
 411     }
 412 
 413     @Override
 414     public void onPause(PlayerStateEvent pse) {
 415         log.fine("onPause, time: {0}", pse.getTime());
 416         notifyPaused(true);
 417     }
 418 
 419     @Override
 420     public void onStop(PlayerStateEvent pse) {
 421         log.fine("onStop");
 422         notifyPaused(true);
 423     }
 424 
 425     @Override
 426     public void onStall(PlayerStateEvent pse) {
 427         log.fine("onStall");
 428     }
 429 
 430     @Override
 431     public void onFinish(PlayerStateEvent pse) {
 432         MediaPlayer p = getPlayer();
 433         if (p != null) {
 434             finished = p.getRate() > 0 ? 1 : -1;
 435             log.fine("onFinish, time: {0}", pse.getTime());
 436             notifyFinished();
 437         }
 438     }
 439 
 440     @Override
 441     public void onHalt(PlayerStateEvent pse) {
 442         log.fine("onHalt");
 443     }
 444 
 445     // MediaErrorListener
 446     @Override
 447     public void onError(Object source, int errCode, String message) {
 448         //MediaPlayer p = getPlayer();
 449         log.warning("onError, errCode={0}, msg={1}", new Object[]{errCode, message});
 450         // TODO: parse errCode to detect NETWORK_STATE_FORMAT_ERROR/
 451         // NETWORK_STATE_NETWORK_ERROR/NETWORK_STATE_DECODE_ERROR
 452         notifyNetworkStateChanged(NETWORK_STATE_NETWORK_ERROR);
 453         notifyReadyStateChanged(READY_STATE_HAVE_NOTHING);
 454     }
 455 
 456     //PlayerTimeListener
 457     @Override
 458     public void onDurationChanged(double duration) {
 459         log.fine("onDurationChanged, duration={0}", duration);
 460         notifyDurationChanged((float)duration);
 461     }
 462 
 463     // VideoTrackSizeListener
 464     @Override
 465     public void onSizeChanged(int width, int height) {
 466         //MediaPlayer p = getPlayer();
 467         log.fine("onSizeChanged, new size = {0} x {1}", new Object[]{width, height});
 468         notifySizeChanged(width, height);
 469     }
 470 
 471     private void notifyFrameArrived() {
 472         if (!gotFirstFrame) {
 473             // this is the first frame
 474             // don't set HAVE_ENOUGH_DATA state before onReady
 475             if (getReadyState() >= READY_STATE_HAVE_METADATA) {
 476                 notifyReadyStateChanged(READY_STATE_HAVE_ENOUGH_DATA);
 477             }
 478             gotFirstFrame = true;
 479         }
 480         if (finished != 0) {
 481             log.fine("notifyFrameArrived (after finished) time: {0}", getPlayer().getPresentationTime());
 482         }
 483         notifyNewFrame();
 484     }
 485 
 486     private float bufferedStart = 0f;
 487     private float bufferedEnd   = 0f;
 488     private boolean buffering   = false;
 489 
 490     private void updateBufferingStatus() {
 491         int newNetworkState =
 492                 buffering ? NETWORK_STATE_LOADING
 493                 : bufferedStart > 0 ? NETWORK_STATE_IDLE : NETWORK_STATE_LOADED;
 494         log.fine("updateBufferingStatus, buffered: [{0} - {1}], buffering = {2}",
 495             new Object[]{bufferedStart, bufferedEnd, buffering});
 496         notifyNetworkStateChanged(newNetworkState);
 497     }
 498 
 499     // BufferListener
 500     @Override
 501     public void onBufferProgress(BufferProgressEvent event) {
 502         /* event (in the current API):
 503          * double getDuration(): duration of the movie (seconds);
 504          * long getBufferStart(): start of the buffered data (bytes)
 505          * long getBufferStop(): end of the movie (bytes)
 506          * long getBufferPosition(): end of the buffered data (bytes)
 507          */
 508         // if duration is not yet known, we cannot calculate buffered ranges
 509         if (event.getDuration() < 0) {
 510             return;
 511         }
 512         double bytes2seconds = event.getDuration() / (double)event.getBufferStop();
 513         bufferedStart = (float)(bytes2seconds * event.getBufferStart());
 514         bufferedEnd = (float)(bytes2seconds * event.getBufferPosition());
 515         buffering = event.getBufferPosition() < event.getBufferStop();
 516 
 517         float ranges[] = new float[2];
 518         ranges[0] = bufferedStart;
 519         ranges[1] = bufferedEnd;
 520         int bytesLoaded = (int)(event.getBufferPosition() - event.getBufferStart());
 521         log.finer("onBufferProgress, "
 522                 + "bufferStart={0}, bufferStop={1}, bufferPos={2}, duration={3}; "
 523                 + "notify range [{4},[5]], bytesLoaded: {6}",
 524                 new Object[]{event.getBufferStart(), event.getBufferStop(),
 525                              event.getBufferPosition(), event.getDuration(),
 526                              ranges[0], ranges[1], bytesLoaded});
 527         notifyBufferChanged(ranges, bytesLoaded);
 528         updateBufferingStatus();
 529     }
 530 
 531     /* Inner class that will listen for new frames from the jfxmedia player and
 532      * manage our own texture cache to remove the dependency on
 533      * PrismMediaFrameHandler
 534      */
 535     private final class MediaFrameListener implements VideoRendererListener {
 536         private final Object frameLock = new Object();
 537         private VideoDataBuffer currentFrame;
 538         private VideoDataBuffer nextFrame;
 539 
 540         public void videoFrameUpdated(NewFrameEvent nfe) {
 541             synchronized (frameLock) {
 542                 if (null != nextFrame) {
 543                     nextFrame.releaseFrame();
 544                 }
 545                 nextFrame = nfe.getFrameData();
 546                 if (null != nextFrame) {
 547                     nextFrame.holdFrame();
 548                 }
 549             }
 550 
 551             // and finally notify the base player that we have a new frame
 552             notifyFrameArrived();
 553         }
 554 
 555         public void releaseVideoFrames() {
 556             synchronized (frameLock) {
 557                 if (null != nextFrame) {
 558                     nextFrame.releaseFrame();
 559                     nextFrame = null;
 560                 }
 561 
 562                 if (null != currentFrame) {
 563                     currentFrame.releaseFrame();
 564                     currentFrame = null;
 565                 }
 566             }
 567         }
 568 
 569         public VideoDataBuffer getLatestFrame() {
 570             synchronized (frameLock) {
 571                 if (null != nextFrame) {
 572                     if (null != currentFrame) {
 573                         currentFrame.releaseFrame();
 574                     }
 575                     currentFrame = nextFrame;
 576                     nextFrame = null;
 577                 }
 578 
 579                 // avoid premature release
 580                 if (null != currentFrame) {
 581                     currentFrame.holdFrame();
 582                 }
 583                 return currentFrame;
 584             }
 585         }
 586     }
 587 }