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}