1 /* 2 * Copyright (c) 2010, 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 javafx.scene.media; 27 28 import com.sun.media.jfxmedia.MetadataParser; 29 import java.io.ByteArrayInputStream; 30 import java.io.IOException; 31 import java.io.FileNotFoundException; 32 import java.net.URI; 33 import java.net.URISyntaxException; 34 import java.util.HashMap; 35 import java.util.List; 36 import java.util.Map; 37 38 import javafx.application.Platform; 39 import javafx.beans.NamedArg; 40 import javafx.beans.property.ObjectProperty; 41 import javafx.beans.property.ObjectPropertyBase; 42 import javafx.collections.FXCollections; 43 import javafx.collections.ObservableList; 44 import javafx.collections.ObservableMap; 45 import javafx.scene.image.Image; 46 import javafx.util.Duration; 47 48 import com.sun.media.jfxmedia.locator.Locator; 49 import javafx.beans.property.ReadOnlyIntegerProperty; 50 import javafx.beans.property.ReadOnlyIntegerWrapper; 51 import javafx.beans.property.ReadOnlyObjectProperty; 52 import javafx.beans.property.ReadOnlyObjectWrapper; 53 import com.sun.media.jfxmedia.events.MetadataListener; 54 import com.sun.media.jfxmedia.track.VideoResolution; 55 56 /** 57 * The <code>Media</code> class represents a media resource. It is instantiated 58 * from the string form of a source URI. Information about the media such as 59 * duration, metadata, tracks, and video resolution may be obtained from a 60 * <code>Media</code> instance. The media information is obtained asynchronously 61 * and so not necessarily available immediately after instantiation of the class. 62 * All information should however be available if the instance has been 63 * associated with a {@link MediaPlayer} and that player has transitioned to 64 * {@link MediaPlayer.Status#READY} status. To be notified when metadata or 65 * {@link Track}s are added, observers may be registered with the collections 66 * returned by {@link #getMetadata()}and {@link #getTracks()}, respectively.</p> 67 * 68 * <p>The same <code>Media</code> object may be shared among multiple 69 * <code>MediaPlayer</code> objects. Such a shared instance might manage a single 70 * copy of the source media data to be used by all players, or it might require a 71 * separate copy of the data for each player. The choice of implementation will 72 * not however have any effect on player behavior at the interface level.</p> 73 * 74 * @see MediaPlayer 75 * @see MediaException 76 * @since JavaFX 2.0 77 */ 78 public final class Media { 79 /** 80 * A property set to a MediaException value when an error occurs. 81 * If <code>error</code> is non-<code>null</code>, then the media could not 82 * be loaded and is not usable. If {@link #onErrorProperty onError} is non-<code>null</code>, 83 * it will be invoked when the <code>error</code> property is set. 84 * 85 * @see MediaException 86 */ 87 private ReadOnlyObjectWrapper<MediaException> error; 88 89 private void setError(MediaException value) { 90 if (getError() == null) { 91 errorPropertyImpl().set(value); 92 } 93 } 94 95 /** 96 * Return any error encountered in the media. 97 * @return a {@link MediaException} or <code>null</code> if there is no error. 98 */ 99 public final MediaException getError() { 100 return error == null ? null : error.get(); 101 } 102 103 public ReadOnlyObjectProperty<MediaException> errorProperty() { 104 return errorPropertyImpl().getReadOnlyProperty(); 105 } 106 107 private ReadOnlyObjectWrapper<MediaException> errorPropertyImpl() { 108 if (error == null) { 109 error = new ReadOnlyObjectWrapper<MediaException>() { 110 111 @Override 112 protected void invalidated() { 113 if (getOnError() != null) { 114 Platform.runLater(getOnError()); 115 } 116 } 117 118 @Override 119 public Object getBean() { 120 return Media.this; 121 } 122 123 @Override 124 public String getName() { 125 return "error"; 126 } 127 }; 128 } 129 return error; 130 } 131 /** 132 * Event handler called when an error occurs. This will happen 133 * if a malformed or invalid URL is passed to the constructor or there is 134 * a problem accessing the URL. 135 */ 136 private ObjectProperty<Runnable> onError; 137 138 /** 139 * Set the event handler to be called when an error occurs. 140 * @param value the error event handler. 141 */ 142 public final void setOnError(Runnable value) { 143 onErrorProperty().set(value); 144 } 145 146 /** 147 * Retrieve the error handler to be called if an error occurs. 148 * @return the error handler or <code>null</code> if none is defined. 149 */ 150 public final Runnable getOnError() { 151 return onError == null ? null : onError.get(); 152 } 153 154 public ObjectProperty<Runnable> onErrorProperty() { 155 if (onError == null) { 156 onError = new ObjectPropertyBase<Runnable>() { 157 158 @Override 159 protected void invalidated() { 160 /* 161 * if we have an existing error condition schedule the handler to be 162 * called immediately. This way the client app does not have to perform 163 * an explicit error check. 164 */ 165 if (get() != null && getError() != null) { 166 Platform.runLater(get()); 167 } 168 } 169 170 @Override 171 public Object getBean() { 172 return Media.this; 173 } 174 175 @Override 176 public String getName() { 177 return "onError"; 178 } 179 }; 180 } 181 return onError; 182 } 183 184 private MetadataListener metadataListener = new _MetadataListener(); 185 186 /** 187 * An {@link ObservableMap} of metadata which can contain information about 188 * the media. Metadata entries use {@link String}s for keys and contain 189 * {@link Object} values. This map is unmodifiable: its contents or stored 190 * values cannot be changed. 191 */ 192 // FIXME: define standard metadata keys and the corresponding objects types 193 // FIXME: figure out how to make the entries read-only to observers, we'll 194 // need to enhance javafx.collections a bit to accomodate this 195 private ObservableMap<String, Object> metadata; 196 197 /** 198 * Retrieve the metadata contained in this media source. If there are 199 * no metadata, the returned {@link ObservableMap} will be empty. 200 * @return the metadata contained in this media source. 201 */ 202 public final ObservableMap<String, Object> getMetadata() { 203 return metadata; 204 } 205 206 private final ObservableMap<String,Object> metadataBacking = FXCollections.observableMap(new HashMap<String,Object>()); 207 /** 208 * The width in pixels of the source media. 209 * This may be zero if the media has no width, e.g., when playing audio, 210 * or if the width is currently unknown which may occur with streaming 211 * media. 212 * @see height 213 */ 214 private ReadOnlyIntegerWrapper width; 215 216 217 final void setWidth(int value) { 218 widthPropertyImpl().set(value); 219 } 220 221 /** 222 * Retrieve the width in pixels of the media. 223 * @return the media width or zero if the width is undefined or unknown. 224 */ 225 public final int getWidth() { 226 return width == null ? 0 : width.get(); 227 } 228 229 public ReadOnlyIntegerProperty widthProperty() { 230 return widthPropertyImpl().getReadOnlyProperty(); 231 } 232 233 private ReadOnlyIntegerWrapper widthPropertyImpl() { 234 if (width == null) { 235 width = new ReadOnlyIntegerWrapper(this, "width"); 236 } 237 return width; 238 } 239 /** 240 * The height in pixels of the source media. 241 * This may be zero if the media has no height, e.g., when playing audio, 242 * or if the height is currently unknown which may occur with streaming 243 * media. 244 * @see width 245 */ 246 private ReadOnlyIntegerWrapper height; 247 248 249 final void setHeight(int value) { 250 heightPropertyImpl().set(value); 251 } 252 253 /** 254 * Retrieve the height in pixels of the media. 255 * @return the media height or zero if the height is undefined or unknown. 256 */ 257 public final int getHeight() { 258 return height == null ? 0 : height.get(); 259 } 260 261 public ReadOnlyIntegerProperty heightProperty() { 262 return heightPropertyImpl().getReadOnlyProperty(); 263 } 264 265 private ReadOnlyIntegerWrapper heightPropertyImpl() { 266 if (height == null) { 267 height = new ReadOnlyIntegerWrapper(this, "height"); 268 } 269 return height; 270 } 271 /** 272 * The duration in seconds of the source media. If the media duration is 273 * unknown then this property value will be {@link Duration#UNKNOWN}. 274 */ 275 private ReadOnlyObjectWrapper<Duration> duration; 276 277 final void setDuration(Duration value) { 278 durationPropertyImpl().set(value); 279 } 280 281 /** 282 * Retrieve the duration in seconds of the media. 283 * @return the duration of the media, {@link Duration#UNKNOWN} if unknown or {@link Duration#INDEFINITE} for live streams 284 */ 285 public final Duration getDuration() { 286 return duration == null || duration.get() == null ? Duration.UNKNOWN : duration.get(); 287 } 288 289 public ReadOnlyObjectProperty<Duration> durationProperty() { 290 return durationPropertyImpl().getReadOnlyProperty(); 291 } 292 293 private ReadOnlyObjectWrapper<Duration> durationPropertyImpl() { 294 if (duration == null) { 295 duration = new ReadOnlyObjectWrapper<Duration>(this, "duration"); 296 } 297 return duration; 298 } 299 /** 300 * An <code>ObservableList</code> of tracks contained in this media object. 301 * A <code>Media</code> object can contain multiple tracks, such as a video track 302 * with several audio track. This list is unmodifiable: the contents cannot 303 * be changed. 304 * @see Track 305 */ 306 private ObservableList<Track> tracks; 307 308 /** 309 * Retrieve the tracks contained in this media source. If there are 310 * no tracks, the returned {@link ObservableList} will be empty. 311 * @return the tracks contained in this media source. 312 */ 313 public final ObservableList<Track> getTracks() { 314 return tracks; 315 } 316 private final ObservableList<Track> tracksBacking = FXCollections.observableArrayList(); 317 318 /** 319 * The markers defined on this media source. A marker is defined to be a 320 * mapping from a name to a point in time between the beginning and end of 321 * the media. 322 */ 323 private ObservableMap<String, Duration> markers = FXCollections.observableMap(new HashMap<String,Duration>()); 324 325 /** 326 * Retrieve the markers defined on this <code>Media</code> instance. If 327 * there are no markers the returned {@link ObservableMap} will be empty. 328 * Programmatic markers may be added by inserting entries in the returned 329 * <code>Map</code>. 330 * 331 * @return the markers defined on this media source. 332 */ 333 public final ObservableMap<String, Duration> getMarkers() { 334 return markers; 335 } 336 337 /** 338 * Constructs a <code>Media</code> instance. This is the only way to 339 * specify the media source. The source must represent a valid <code>URI</code> 340 * and is immutable. Only HTTP, HTTPS, FILE, and JAR <code>URL</code>s are supported. If the 341 * provided URL is invalid then an exception will be thrown. If an 342 * asynchronous error occurs, the {@link #errorProperty error} property will be set. Listen 343 * to this property to be notified of any such errors. 344 * 345 * <p>If the source uses a non-blocking protocol such as FILE, then any 346 * problems which can be detected immediately will cause a <code>MediaException</code> 347 * to be thrown. Such problems include the media being inaccessible or in an 348 * unsupported format. If however a potentially blocking protocol such as 349 * HTTP is used, then the connection will be initialized asynchronously so 350 * that these sorts of errors will be signaled by setting the {@link #errorProperty error} 351 * property.</p> 352 * 353 * <p>Constraints: 354 * <ul> 355 * <li>The supplied URI must conform to RFC-2396 as required by 356 * <A href="https://docs.oracle.com/javase/8/docs/api/java/net/URI.html">java.net.URI</A>.</li> 357 * <li>Only HTTP, HTTPS, FILE, and JAR URIs are supported.</li> 358 * </ul> 359 * 360 * <p>See <A href="https://docs.oracle.com/javase/8/docs/api/java/net/URI.html">java.net.URI</A> 361 * for more information about URI formatting in general. 362 * JAR URL syntax is specified in <a href="https://docs.oracle.com/javase/8/docs/api/java/net/JarURLConnection.html">java.net.JarURLConnection</A>. 363 * 364 * @param source The URI of the source media. 365 * @throws NullPointerException if the URI string is <code>null</code>. 366 * @throws IllegalArgumentException if the URI string does not conform to RFC-2396 367 * or, if appropriate, the Jar URL specification, or is in a non-compliant 368 * form which cannot be modified to a compliant form. 369 * @throws IllegalArgumentException if the URI string has a <code>null</code> 370 * scheme. 371 * @throws UnsupportedOperationException if the protocol specified for the 372 * source is not supported. 373 * @throws MediaException if the media source cannot be connected 374 * (type {@link MediaException.Type#MEDIA_INACCESSIBLE}) or is not supported 375 * (type {@link MediaException.Type#MEDIA_UNSUPPORTED}). 376 */ 377 public Media(@NamedArg("source") String source) { 378 this.source = source; 379 380 URI uri = null; 381 try { 382 // URI will throw NPE if source == null: do not catch it! 383 uri = new URI(source); 384 } catch(URISyntaxException use) { 385 throw new IllegalArgumentException(use); 386 } 387 388 metadata = FXCollections.unmodifiableObservableMap(metadataBacking); 389 tracks = FXCollections.unmodifiableObservableList(tracksBacking); 390 391 Locator locator = null; 392 try { 393 locator = new com.sun.media.jfxmedia.locator.Locator(uri); 394 jfxLocator = locator; 395 if (locator.canBlock()) { 396 InitLocator locatorInit = new InitLocator(); 397 Thread t = new Thread(locatorInit); 398 t.setDaemon(true); 399 t.start(); 400 } else { 401 locator.init(); 402 runMetadataParser(); 403 } 404 } catch(URISyntaxException use) { 405 throw new IllegalArgumentException(use); 406 } catch(FileNotFoundException fnfe) { 407 throw new MediaException(MediaException.Type.MEDIA_UNAVAILABLE, fnfe.getMessage()); 408 } catch(IOException ioe) { 409 throw new MediaException(MediaException.Type.MEDIA_INACCESSIBLE, ioe.getMessage()); 410 } catch(com.sun.media.jfxmedia.MediaException me) { 411 throw new MediaException(MediaException.Type.MEDIA_UNSUPPORTED, me.getMessage()); 412 } 413 } 414 415 private void runMetadataParser() { 416 try { 417 jfxParser = com.sun.media.jfxmedia.MediaManager.getMetadataParser(jfxLocator); 418 jfxParser.addListener(metadataListener); 419 jfxParser.startParser(); 420 } catch (Exception e) { 421 jfxParser = null; 422 } 423 } 424 425 /** 426 * The source URI of the media; 427 */ 428 private final String source; 429 430 /** 431 * Retrieve the source URI of the media. 432 * @return the media source URI as a {@link String}. 433 */ 434 public String getSource() { 435 return source; 436 } 437 438 /** 439 * Locator used by the jfxmedia player, MediaPlayer needs access to this 440 */ 441 private final Locator jfxLocator; 442 Locator retrieveJfxLocator() { 443 return jfxLocator; 444 } 445 446 private MetadataParser jfxParser; 447 448 private Track getTrackWithID(long trackID) { 449 for (Track track : tracksBacking) { 450 if (track.getTrackID() == trackID) { 451 return track; 452 } 453 } 454 return null; 455 } 456 457 // http://javafx-jira.kenai.com/browse/RT-24594 458 // TODO: Remove this entire method (and associated stuff) when we switch to track parsing in MetadataParser 459 void _updateMedia(com.sun.media.jfxmedia.Media _media) { 460 try { 461 List<com.sun.media.jfxmedia.track.Track> trackList = _media.getTracks(); 462 463 if (trackList != null) { 464 for (com.sun.media.jfxmedia.track.Track trackElement : trackList) { 465 long trackID = trackElement.getTrackID(); 466 if (getTrackWithID(trackID) == null) { 467 Track newTrack = null; 468 Map<String,Object> trackMetadata = new HashMap<String,Object>(); 469 if (null != trackElement.getName()) { 470 // FIXME: need constants for metadata keys (globally) 471 trackMetadata.put("name", trackElement.getName()); 472 } 473 if (null != trackElement.getLocale()) { 474 trackMetadata.put("locale", trackElement.getLocale()); 475 } 476 trackMetadata.put("encoding", trackElement.getEncodingType().toString()); 477 trackMetadata.put("enabled", Boolean.valueOf(trackElement.isEnabled())); 478 479 if (trackElement instanceof com.sun.media.jfxmedia.track.VideoTrack) { 480 com.sun.media.jfxmedia.track.VideoTrack vt = 481 (com.sun.media.jfxmedia.track.VideoTrack) trackElement; 482 483 int videoWidth = vt.getFrameSize().getWidth(); 484 int videoHeight = vt.getFrameSize().getHeight(); 485 486 // FIXME: this isn't valid when there are multiple video tracks... 487 setWidth(videoWidth); 488 setHeight(videoHeight); 489 490 trackMetadata.put("video width", Integer.valueOf(videoWidth)); 491 trackMetadata.put("video height", Integer.valueOf(videoHeight)); 492 493 newTrack = new VideoTrack(trackElement.getTrackID(), trackMetadata); 494 } else if (trackElement instanceof com.sun.media.jfxmedia.track.AudioTrack) { 495 newTrack = new AudioTrack(trackElement.getTrackID(), trackMetadata); 496 } else if (trackElement instanceof com.sun.media.jfxmedia.track.SubtitleTrack) { 497 newTrack = new SubtitleTrack(trackID, trackMetadata); 498 } 499 500 if (null != newTrack) { 501 tracksBacking.add(newTrack); 502 } 503 } 504 } 505 } 506 } catch (Exception e) { 507 // Save any async exceptions as an error. 508 setError(new MediaException(MediaException.Type.UNKNOWN, e)); 509 } 510 } 511 512 void _setError(MediaException.Type type, String message) { 513 setError(new MediaException(type, message)); 514 } 515 516 private synchronized void updateMetadata(Map<String, Object> metadata) { 517 if (metadata != null) { 518 for (Map.Entry<String,Object> entry : metadata.entrySet()) { 519 String key = entry.getKey(); 520 Object value = entry.getValue(); 521 if (key.equals(MetadataParser.IMAGE_TAG_NAME) && value instanceof byte[]) { 522 byte[] imageData = (byte[]) value; 523 Image image = new Image(new ByteArrayInputStream(imageData)); 524 if (!image.isError()) { 525 metadataBacking.put(MetadataParser.IMAGE_TAG_NAME, image); 526 } 527 } else if (key.equals(MetadataParser.DURATION_TAG_NAME) && value instanceof java.lang.Long) { 528 Duration d = new Duration((Long) value); 529 if (d != null) { 530 metadataBacking.put(MetadataParser.DURATION_TAG_NAME, d); 531 } 532 } else { 533 metadataBacking.put(key, value); 534 } 535 } 536 } 537 } 538 539 private class _MetadataListener implements MetadataListener { 540 @Override 541 public void onMetadata(final Map<String, Object> metadata) { 542 // Clean up metadata 543 Platform.runLater(() -> { 544 updateMetadata(metadata); 545 jfxParser.removeListener(metadataListener); 546 jfxParser.stopParser(); 547 jfxParser = null; 548 }); 549 } 550 } 551 552 private class InitLocator implements Runnable { 553 554 @Override 555 public void run() { 556 try { 557 jfxLocator.init(); 558 runMetadataParser(); 559 } catch (URISyntaxException use) { 560 _setError(MediaException.Type.OPERATION_UNSUPPORTED, use.getMessage()); 561 } catch (FileNotFoundException fnfe) { 562 _setError(MediaException.Type.MEDIA_UNAVAILABLE, fnfe.getMessage()); 563 } catch (IOException ioe) { 564 _setError(MediaException.Type.MEDIA_INACCESSIBLE, ioe.getMessage()); 565 } catch (com.sun.media.jfxmedia.MediaException me) { 566 _setError(MediaException.Type.MEDIA_UNSUPPORTED, me.getMessage()); 567 } catch (Exception e) { 568 _setError(MediaException.Type.UNKNOWN, e.getMessage()); 569 } 570 } 571 } 572 }