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