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 }