Spec-Zone .ru
спецификации, руководства, описания, API
|
001/* 002 * Copyright (c) 2010, 2012, Oracle and/or its affiliates. All rights reserved. 003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 004 * 005 * This code is free software; you can redistribute it and/or modify it 006 * under the terms of the GNU General Public License version 2 only, as 007 * published by the Free Software Foundation. Oracle designates this 008 * particular file as subject to the "Classpath" exception as provided 009 * by Oracle in the LICENSE file that accompanied this code. 010 * 011 * This code is distributed in the hope that it will be useful, but WITHOUT 012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 013 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 014 * version 2 for more details (a copy is included in the LICENSE file that 015 * accompanied this code). 016 * 017 * You should have received a copy of the GNU General Public License version 018 * 2 along with this work; if not, write to the Free Software Foundation, 019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 020 * 021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 022 * or visit www.oracle.com if you need additional information or have any 023 * questions. 024 */ 025 026package javafx.scene.media; 027 028import com.sun.media.jfxmedia.MetadataParser; 029import java.io.ByteArrayInputStream; 030import java.io.IOException; 031import java.io.FileNotFoundException; 032import java.net.URI; 033import java.net.URISyntaxException; 034import java.util.HashMap; 035import java.util.List; 036import java.util.Map; 037 038import javafx.application.Platform; 039import javafx.beans.property.ObjectProperty; 040import javafx.beans.property.ObjectPropertyBase; 041import javafx.collections.FXCollections; 042import javafx.collections.ObservableList; 043import javafx.collections.ObservableMap; 044import javafx.scene.image.Image; 045import javafx.util.Duration; 046 047import com.sun.media.jfxmedia.locator.Locator; 048import javafx.beans.property.ReadOnlyIntegerProperty; 049import javafx.beans.property.ReadOnlyIntegerWrapper; 050import javafx.beans.property.ReadOnlyObjectProperty; 051import javafx.beans.property.ReadOnlyObjectWrapper; 052import com.sun.media.jfxmedia.events.MetadataListener; 053import com.sun.media.jfxmedia.track.VideoResolution; 054 055/** 056 * The <code>Media</code> class represents a media resource. It is instantiated 057 * from the string form of a source URI. Information about the media such as 058 * duration, metadata, tracks, and video resolution may be obtained from a 059 * <code>Media</code> instance. The media information is obtained asynchronously 060 * and so not necessarily available immediately after instantiation of the class. 061 * All information should however be available if the instance has been 062 * associated with a {@link MediaPlayer} and that player has transitioned to 063 * {@link MediaPlayer.Status#READY} status. To be notified when metadata or 064 * {@link Track}s are added, observers may be registered with the collections 065 * returned by {@link #getMetadata()}and {@link #getTracks()}, respectively.</p> 066 * 067 * <p>The same <code>Media</code> object may be shared among multiple 068 * <code>MediaPlayer</code> objects. Such a shared instance might manage a single 069 * copy of the source media data to be used by all players, or it might require a 070 * separate copy of the data for each player. The choice of implementation will 071 * not however have any effect on player behavior at the interface level.</p> 072 * 073 * @see MediaPlayer 074 * @see MediaException 075 */ 076public final class Media { 077 /** 078 * A property set to a MediaException value when an error occurs. 079 * If <code>error</code> is non-<code>null</code>, then the media could not 080 * be loaded and is not usable. If {@link #onErrorProperty onError} is non-<code>null</code>, 081 * it will be invoked when the <code>error</code> property is set. 082 * 083 * @see MediaException 084 */ 085 private ReadOnlyObjectWrapper<MediaException> error; 086 087 private void setError(MediaException value) { 088 errorPropertyImpl().set(value); 089 } 090 091 /** 092 * Return any error encountered in the media. 093 * @return a {@link MediaException} or <code>null</code> if there is no error. 094 */ 095 public final MediaException getError() { 096 return error == null ? null : error.get(); 097 } 098 099 public ReadOnlyObjectProperty<MediaException> errorProperty() { 100 return errorPropertyImpl().getReadOnlyProperty(); 101 } 102 103 private ReadOnlyObjectWrapper<MediaException> errorPropertyImpl() { 104 if (error == null) { 105 error = new ReadOnlyObjectWrapper<MediaException>() { 106 107 @Override 108 protected void invalidated() { 109 if (getOnError() != null) { 110 Platform.runLater(getOnError()); 111 } 112 } 113 114 @Override 115 public Object getBean() { 116 return Media.this; 117 } 118 119 @Override 120 public String getName() { 121 return "error"; 122 } 123 }; 124 } 125 return error; 126 } 127 /** 128 * Event handler called when an error occurs. This will happen 129 * if a malformed or invalid URL is passed to the constructor or there is 130 * a problem accessing the URL. 131 */ 132 private ObjectProperty<Runnable> onError; 133 134 /** 135 * Set the event handler to be called when an error occurs. 136 * @param value the error event handler. 137 */ 138 public final void setOnError(Runnable value) { 139 onErrorProperty().set(value); 140 } 141 142 /** 143 * Retrieve the error handler to be called if an error occurs. 144 * @return the error handler or <code>null</code> if none is defined. 145 */ 146 public final Runnable getOnError() { 147 return onError == null ? null : onError.get(); 148 } 149 150 public ObjectProperty<Runnable> onErrorProperty() { 151 if (onError == null) { 152 onError = new ObjectPropertyBase<Runnable>() { 153 154 @Override 155 protected void invalidated() { 156 /* 157 * if we have an existing error condition schedule the handler to be 158 * called immediately. This way the client app does not have to perform 159 * an explicit error check. 160 */ 161 if (get() != null && getError() != null) { 162 Platform.runLater(get()); 163 } 164 } 165 166 @Override 167 public Object getBean() { 168 return Media.this; 169 } 170 171 @Override 172 public String getName() { 173 return "onError"; 174 } 175 }; 176 } 177 return onError; 178 } 179 180 private MetadataListener metadataListener = new _MetadataListener(); 181 182 /** 183 * An {@link ObservableMap} of metadata which can contain information about 184 * the media. Metadata entries use {@link String}s for keys and contain 185 * {@link Object} values. This map is unmodifiable: its contents or stored 186 * values cannot be changed. 187 */ 188 // FIXME: define standard metadata keys and the corresponding objects types 189 // FIXME: figure out how to make the entries read-only to observers, we'll 190 // need to enhance javafx.collections a bit to accomodate this 191 private ObservableMap<String, Object> metadata; 192 193 /** 194 * Retrieve the metadata contained in this media source. If there are 195 * no metadata, the returned {@link ObservableMap} will be empty. 196 * @return the metadata contained in this media source. 197 */ 198 public final ObservableMap<String, Object> getMetadata() { 199 return metadata; 200 } 201 202 private final ObservableMap<String,Object> metadataBacking; 203 /** 204 * The width in pixels of the source media. 205 * This may be zero if the media has no width, e.g., when playing audio, 206 * or if the width is currently unknown which may occur with streaming 207 * media. 208 * @see height 209 */ 210 private ReadOnlyIntegerWrapper width; 211 212 213 final void setWidth(int value) { 214 widthPropertyImpl().set(value); 215 } 216 217 /** 218 * Retrieve the width in pixels of the media. 219 * @return the media width or zero if the width is undefined or unknown. 220 */ 221 public final int getWidth() { 222 return width == null ? 0 : width.get(); 223 } 224 225 public ReadOnlyIntegerProperty widthProperty() { 226 return widthPropertyImpl().getReadOnlyProperty(); 227 } 228 229 private ReadOnlyIntegerWrapper widthPropertyImpl() { 230 if (width == null) { 231 width = new ReadOnlyIntegerWrapper(this, "width"); 232 } 233 return width; 234 } 235 /** 236 * The height in pixels of the source media. 237 * This may be zero if the media has no height, e.g., when playing audio, 238 * or if the height is currently unknown which may occur with streaming 239 * media. 240 * @see width 241 */ 242 private ReadOnlyIntegerWrapper height; 243 244 245 final void setHeight(int value) { 246 heightPropertyImpl().set(value); 247 } 248 249 /** 250 * Retrieve the height in pixels of the media. 251 * @return the media height or zero if the height is undefined or unknown. 252 */ 253 public final int getHeight() { 254 return height == null ? 0 : height.get(); 255 } 256 257 public ReadOnlyIntegerProperty heightProperty() { 258 return heightPropertyImpl().getReadOnlyProperty(); 259 } 260 261 private ReadOnlyIntegerWrapper heightPropertyImpl() { 262 if (height == null) { 263 height = new ReadOnlyIntegerWrapper(this, "height"); 264 } 265 return height; 266 } 267 /** 268 * The duration in seconds of the source media. If the media duration is 269 * unknown then this property value will be {@link Duration#UNKNOWN}. 270 */ 271 private ReadOnlyObjectWrapper<Duration> duration; 272 273 final void setDuration(Duration value) { 274 durationPropertyImpl().set(value); 275 } 276 277 /** 278 * Retrieve the duration in seconds of the media. 279 * @return the duration of the media, {@link Duration#UNKNOWN} if unknown or {@link Duration#INDEFINITE} for live streams 280 */ 281 public final Duration getDuration() { 282 return duration == null ? Duration.UNKNOWN : duration.get(); 283 } 284 285 public ReadOnlyObjectProperty<Duration> durationProperty() { 286 return durationPropertyImpl().getReadOnlyProperty(); 287 } 288 289 private ReadOnlyObjectWrapper<Duration> durationPropertyImpl() { 290 if (duration == null) { 291 duration = new ReadOnlyObjectWrapper<Duration>(this, "duration"); 292 } 293 return duration; 294 } 295 /** 296 * An <code>ObservableList</code> of tracks contained in this media object. 297 * A <code>Media</code> object can contain multiple tracks, such as a video track 298 * with several audio track. This list is unmodifiable: the contents cannot 299 * be changed. 300 * @see Track 301 */ 302 private ObservableList<Track> tracks; 303 304 /** 305 * Retrieve the tracks contained in this media source. If there are 306 * no tracks, the returned {@link ObservableList} will be empty. 307 * @return the tracks contained in this media source. 308 */ 309 public final ObservableList<Track> getTracks() { 310 return tracks; 311 } 312 private final ObservableList<Track> tracksBacking; 313 314 /** 315 * The markers defined on this media source. A marker is defined to be a 316 * mapping from a name to a point in time between the beginning and end of 317 * the media. 318 */ 319 private ObservableMap<String, Duration> markers; 320 321 /** 322 * Retrieve the markers defined on this <code>Media</code> instance. If 323 * there are no markers the returned {@link ObservableMap} will be empty. 324 * Programmatic markers may be added by inserting entries in the returned 325 * <code>Map</code>. 326 * 327 * @return the markers defined on this media source. 328 */ 329 public final ObservableMap<String, Duration> getMarkers() { 330 return markers; 331 } 332 333 /** 334 * Constructs a <code>Media</code> instance. This is the only way to 335 * specify the media source. The source must represent a valid <code>URI</code> 336 * and is immutable. Only HTTP, FILE, and JAR <code>URL</code>s are supported. If the 337 * provided URL is invalid then an exception will be thrown. If an 338 * asynchronous error occurs, the {@link #errorProperty error} property will be set. Listen 339 * to this property to be notified of any such errors. 340 * 341 * <p>If the source uses a non-blocking protocol such as FILE, then any 342 * problems which can be detected immediately will cause a <code>MediaException</code> 343 * to be thrown. Such problems include the media being inaccessible or in an 344 * unsupported format. If however a potentially blocking protocol such as 345 * HTTP is used, then the connection will be initialized asynchronously so 346 * that these sorts of errors will be signaled by setting the {@link #errorProperty error} 347 * property.</p> 348 * 349 * <p>Constraints: 350 * <ul> 351 * <li>The supplied URI must conform to RFC-2396 as required by 352 * <A href="http://download.oracle.com/javase/7/docs/api/java/net/URI.html">java.net.URI</A>.</li> 353 * <li>Only HTTP, FILE, and JAR URIs are supported.</li> 354 * </ul> 355 * 356 * <p>See <A href="http://download.oracle.com/javase/7/docs/api/java/net/URI.html">java.net.URI</A> 357 * for more information about URI formatting in general. 358 * JAR URL syntax is specified in <a href="http://download.oracle.com/javase/7/docs/api/java/net/JarURLConnection.html">java.net.JarURLConnection</A>. 359 * 360 * @param source The URI of the source media. 361 * @throws NullPointerException if the URI string is <code>null</code>. 362 * @throws IllegalArgumentException if the URI string does not conform to RFC-2396 363 * or, if appropriate, the Jar URL specification, or is in a non-compliant 364 * form which cannot be modified to a compliant form. 365 * @throws IllegalArgumentException if the URI string has a <code>null</code> 366 * scheme. 367 * @throws UnsupportedOperationException if the protocol specified for the 368 * source is not supported. 369 * @throws MediaException if the media source cannot be connected 370 * (type {@link MediaException.Type#MEDIA_INACCESSIBLE}) or is not supported 371 * (type {@link MediaException.Type#MEDIA_UNSUPPORTED}). 372 */ 373 public Media(String source) { 374 URI uri = null; 375 try { 376 // URI will throw NPE if source == null: do not catch it! 377 uri = new URI(source); 378 } catch(URISyntaxException use) { 379 throw new IllegalArgumentException(use); 380 } 381 382 Locator locator = null; 383 try { 384 locator = new com.sun.media.jfxmedia.locator.Locator(uri); 385 jfxLocator = locator; 386 if (locator.canBlock()) { 387 InitLocator locatorInit = new InitLocator(); 388 Thread t = new Thread(locatorInit); 389 t.setDaemon(true); 390 t.start(); 391 } else { 392 locator.init(); 393 runMetadataParser(); 394 } 395 } catch(URISyntaxException use) { 396 throw new IllegalArgumentException(use); 397 } catch(FileNotFoundException fnfe) { 398 throw new MediaException(MediaException.Type.MEDIA_UNAVAILABLE, fnfe.getMessage()); 399 } catch(IOException ioe) { 400 throw new MediaException(MediaException.Type.MEDIA_INACCESSIBLE, ioe.getMessage()); 401 } catch(com.sun.media.jfxmedia.MediaException me) { 402 throw new MediaException(MediaException.Type.MEDIA_UNSUPPORTED, me.getMessage()); 403 } 404 405 this.source = source; 406 407 metadataBacking = FXCollections.observableMap(new HashMap<String,Object>()); 408 metadata = FXCollections.unmodifiableObservableMap(metadataBacking); 409 410 tracksBacking = FXCollections.observableArrayList(); 411 tracks = FXCollections.unmodifiableObservableList(tracksBacking); 412 413 markers = FXCollections.observableMap(new HashMap<String,Duration>()); 414 } 415 416 private void runMetadataParser() { 417 // Create and start metadata parser 418 MetadataParser parser = null; 419 try { 420 parser = com.sun.media.jfxmedia.MediaManager.getMetadataParser(jfxLocator); 421 parser.addListener(metadataListener); 422 parser.startParser(); 423 } catch (Exception e) { 424 // Ignore it 425 } 426 this.jfxParser = parser; 427 } 428 429 /** 430 * The source URI of the media; 431 */ 432 private final String source; 433 434 /** 435 * Retrieve the source URI of the media. 436 * @return the media source URI as a {@link String}. 437 */ 438 public String getSource() { 439 return source; 440 } 441 442 /** 443 * Locator used by the jfxmedia player, MediaPlayer needs access to this 444 */ 445 private final Locator jfxLocator; 446 Locator retrieveJfxLocator() { 447 return jfxLocator; 448 } 449 450 private MetadataParser jfxParser; 451 452 private Track getTrackWithID(long trackID) { 453 for (Track track : tracksBacking) { 454 if (track.getTrackID() == trackID) { 455 return track; 456 } 457 } 458 return null; 459 } 460 461 // http://javafx-jira.kenai.com/browse/RT-24594 462 // TODO: Remove this entire method (and associated stuff) when we switch to track parsing in MetadataParser 463 void _updateMedia(com.sun.media.jfxmedia.Media _media) { 464 try { 465 List<com.sun.media.jfxmedia.track.Track> trackList = _media.getTracks(); 466 467 if (trackList != null) { 468 for (com.sun.media.jfxmedia.track.Track trackElement : trackList) { 469 long trackID = trackElement.getTrackID(); 470 if (getTrackWithID(trackID) == null) { 471 Track newTrack = null; 472 Map<String,Object> trackMetadata = new HashMap<String,Object>(); 473 if (null != trackElement.getName()) { 474 // FIXME: need constants for metadata keys (globally) 475 trackMetadata.put("name", trackElement.getName()); 476 } 477 if (null != trackElement.getLocale()) { 478 trackMetadata.put("locale", trackElement.getLocale()); 479 } 480 trackMetadata.put("encoding", trackElement.getEncodingType().toString()); 481 trackMetadata.put("enabled", Boolean.valueOf(trackElement.isEnabled())); 482 483 if (trackElement instanceof com.sun.media.jfxmedia.track.VideoTrack) { 484 com.sun.media.jfxmedia.track.VideoTrack vt = 485 (com.sun.media.jfxmedia.track.VideoTrack) trackElement; 486 487 int videoWidth = vt.getFrameSize().getWidth(); 488 int videoHeight = vt.getFrameSize().getHeight(); 489 490 // FIXME: this isn't valid when there are multiple video tracks... 491 setWidth(videoWidth); 492 setHeight(videoHeight); 493 494 trackMetadata.put("video width", Integer.valueOf(videoWidth)); 495 trackMetadata.put("video height", Integer.valueOf(videoHeight)); 496 497 newTrack = new VideoTrack(trackElement.getTrackID(), trackMetadata); 498 } else if (trackElement instanceof com.sun.media.jfxmedia.track.AudioTrack) { 499 newTrack = new AudioTrack(trackElement.getTrackID(), trackMetadata); 500 } else if (trackElement instanceof com.sun.media.jfxmedia.track.SubtitleTrack) { 501 newTrack = new SubtitleTrack(trackID, trackMetadata); 502 } 503 504 if (null != newTrack) { 505 tracksBacking.add(newTrack); 506 } 507 } 508 } 509 } 510 } catch (Exception e) { 511 // Save any async exceptions as an error. 512 setError(new MediaException(MediaException.Type.UNKNOWN, e)); 513 } 514 } 515 516 void _setError(MediaException.Type type, String message) { 517 setError(new MediaException(type, message)); 518 } 519 520 private synchronized void updateMetadata(Map<String, Object> metadata) { 521 if (metadata != null) { 522 for (Map.Entry<String,Object> entry : metadata.entrySet()) { 523 String key = entry.getKey(); 524 Object value = entry.getValue(); 525 if (key.equals(MetadataParser.IMAGE_TAG_NAME) && value instanceof byte[]) { 526 byte[] imageData = (byte[]) value; 527 Image image = new Image(new ByteArrayInputStream(imageData)); 528 if (!image.isError()) { 529 metadataBacking.put(MetadataParser.IMAGE_TAG_NAME, image); 530 } 531 } else if (key.equals(MetadataParser.DURATION_TAG_NAME) && value instanceof java.lang.Long) { 532 Duration d = new Duration((Long) value); 533 if (d != null) { 534 metadataBacking.put(MetadataParser.DURATION_TAG_NAME, d); 535 } 536 } else { 537 metadataBacking.put(key, value); 538 } 539 } 540 } 541 } 542 543 private class _MetadataListener implements MetadataListener { 544 @Override 545 public void onMetadata(final Map<String, Object> metadata) { 546 // Clean up metadata 547 Platform.runLater(new Runnable() { 548 @Override 549 public void run() { 550 updateMetadata(metadata); 551 jfxParser.removeListener(metadataListener); 552 jfxParser.stopParser(); 553 jfxParser = null; 554 } 555 }); 556 } 557 } 558 559 private class InitLocator implements Runnable { 560 561 @Override 562 public void run() { 563 try { 564 jfxLocator.init(); 565 runMetadataParser(); 566 } catch (URISyntaxException use) { 567 _setError(MediaException.Type.OPERATION_UNSUPPORTED, use.getMessage()); 568 } catch (FileNotFoundException fnfe) { 569 _setError(MediaException.Type.MEDIA_UNAVAILABLE, fnfe.getMessage()); 570 } catch (IOException ioe) { 571 _setError(MediaException.Type.MEDIA_INACCESSIBLE, ioe.getMessage()); 572 } catch (com.sun.media.jfxmedia.MediaException me) { 573 _setError(MediaException.Type.MEDIA_UNSUPPORTED, me.getMessage()); 574 } catch (Exception e) { 575 _setError(MediaException.Type.UNKNOWN, e.getMessage()); 576 } 577 } 578 } 579}