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 }