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