/* * Copyright 1990-2009 Sun Microsystems, Inc. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License version * 2 only, as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License version 2 for more details (a copy is * included at /legal/license.txt). * * You should have received a copy of the GNU General Public License * version 2 along with this work; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa * Clara, CA 95054 or visit www.sun.com if you need additional * information or have any questions. */ package com.sun.perseus.model; import org.w3c.dom.DOMException; import org.w3c.dom.events.Event; import com.sun.perseus.platform.URLResolver; import com.sun.perseus.util.SVGConstants; /** * The <code>MediaElement</code> class is used for audio and video * support in SVG Tiny 1.2. * */ public abstract class MediaElement extends TimeAttributesNode implements DecoratedNode { /** * The media's hyperlink reference, xlink:href attribute * Default is an empty String. */ protected String href = ""; /** * The media format type attribute */ protected String type = ""; /** * The syncBehavior attribute, default is derived from * SVG element's syncBehaviorDefault attribute value. */ protected String syncBehavior; /** * The syncTolerance attribute, default is derived from * SVG element's syncToleranceDefault attribute value. */ protected Time syncTolerance; /** * The syncMaster attribute, default value is false */ protected boolean syncMaster = false; /** * The current audio level, audio-level property */ protected float audioLevel = 1f; /** * Markers are used to keep track of inherited properties, color relative * properties and bolder/lighter font weights. * * 0-22 : property inheritance * 23-24 : color relative * 24 : is bolder marker * 25 : is lighter marker */ protected int markers = ViewportNode.DEFAULT_INHERITANCE; /** * xlink:href is required on MediaElements. */ static final String[][] REQUIRED_TRAITS_NS = { {SVGConstants.XLINK_NAMESPACE_URI, SVGConstants.SVG_HREF_ATTRIBUTE} }; /** * Indicates whether media playback has started. */ private boolean mediaStarted = false; /** * The most recent sample time. */ private long sampleTime; /** * Builds a new timed element that belongs to the given * document. This <code>MediaElement</code> will belong * to the <code>DocumentNode</code>'s time container. * * @param ownerDocument the document this node belongs to. * @param localName the element's local name * @throws IllegalArgumentException if the input ownerDocument is null */ public MediaElement(final DocumentNode ownerDocument, final String localName) { super(ownerDocument, localName); ModelNode mn = ownerDocument.getFirstChildNode(); if (mn != null && mn instanceof SVG) { SVG svg = (SVG)mn; syncBehavior = svg.getSyncBehaviorDefault(); syncTolerance = svg.getSyncToleranceDefault(); } else { // If the document tree does not have a "svg" element in it, just // use the values that we know are defaults in the svg element. syncBehavior = SVGConstants.SVG_CAN_SLIP_VALUE; syncTolerance = new Time(2000); } } /** * This returns the <b>absolute</b> URI, even though * the href may have been a relative URI * * @return this anchor's href, as an absolute URL or * null if the href set was null or if the * absolute URL could not be computed. */ String getHref() { String uriBase = getURIBase(); try { if (uriBase != null) { String url = URLResolver.resolve(uriBase, href); return url; } else { return href; } } catch (IllegalArgumentException mue) { return null; } } /** * Sets the media element's resource reference. * * @param newHref the new media element's href */ void setHref(final String newHref) { if (newHref == null) { throw new IllegalArgumentException(); } if (equal(newHref, href)) { return; } modifyingNode(); this.href = newHref; modifiedNode(); } /** * Returns the current media format type. * * @param returns the current media format type format. */ String getType() { return type; } /** * Set the media format type. * * @param newType the media format type to set * @throws IllegalArgumentException if newType is null */ void setType(String newType) { if (newType == null) { throw new IllegalArgumentException(); } if (equal(newType, type)) { return; } modifyingNode(); this.type = newType; modifiedNode(); } /** * Returns the current audio level. Should be between 0 and 1. * * @return the current audio level, in the [0, 1] range. */ final float getAudioLevel() { return audioLevel; } /** * Sets the current audio level. Should be between 0 and 1. * * @param newAudioLevel the new audio level, in the [0, 1] range. */ final void setAudioLevel(final float newAudioLevel) { if (newAudioLevel < 0 || newAudioLevel > 1) { throw new IllegalArgumentException(); } if (audioLevel == newAudioLevel) { return; } modifyingNode(); this.audioLevel = newAudioLevel; setVolume(audioLevel); modifiedNode(); } /** * MediaElement handles the audio-level, type, syncBehavior, syncTolerance * and syncMaster traits. * * @param traitName the name of the trait which the element may support. * @return true if this element supports the given trait in one of the * trait accessor methods. */ boolean supportsTrait(final String traitName) { if ((SVGConstants.SVG_AUDIO_LEVEL_ATTRIBUTE == traitName) || (SVGConstants.SVG_TYPE_ATTRIBUTE == traitName) || (SVGConstants.SVG_SYNC_BEHAVIOR_ATTRIBUTE == traitName) || (SVGConstants.SVG_SYNC_TOLERANCE_ATTRIBUTE == traitName) || (SVGConstants.SVG_SYNC_MASTER_ATTRIBUTE == traitName)) { return true; } else { return super.supportsTrait(traitName); } } /** * Supported traits: xlink:href * * @param namespaceURI the trait's namespace. * @param traitName the name of the trait which the element may support. * @return true if this element supports the given trait in one of the * trait accessor methods. */ boolean supportsTraitNS(final String namespaceURI, final String traitName) { if ((SVGConstants.XLINK_NAMESPACE_URI == namespaceURI) && (SVGConstants.SVG_HREF_ATTRIBUTE == traitName)) { return true; } else { return super.supportsTraitNS(namespaceURI, traitName); } } /** * The audio-level trait is animatable, so create an appropriate TraitAnim * for it. * * @param traitName the trait name. */ TraitAnim createTraitAnimImpl(final String traitName) { if (SVGConstants.SVG_AUDIO_LEVEL_ATTRIBUTE == traitName) { return new FloatTraitAnim(this, traitName, TRAIT_TYPE_FLOAT); } else { return super.createTraitAnimImpl(traitName); } } /** * xlink:href is animatable, so create an appropriate TraitAnim for it. * * @param traitName the trait name. * @param traitNamespace the trait's namespace. Should not be null. */ TraitAnim createTraitAnimNSImpl(final String traitNamespace, final String traitName) { if ((traitNamespace == SVGConstants.XLINK_NAMESPACE_URI) && (traitName == SVGConstants.SVG_HREF_ATTRIBUTE)) { return new StringTraitAnim(this, traitNamespace, traitName); } return super.createTraitAnimNSImpl(traitNamespace, traitName); } /** * MediaElement handles the audio-level, type, syncBehavior, syncTolerance * and syncMaster traits. * * @param name the requested trait name * @return the requested trait's value. * * @throws DOMException with error code NOT_SUPPORTED_ERROR if the * requested trait is not supported on this element or null. * @throws DOMException with error code TYPE_MISMATCH_ERR if requested * trait's computed value cannot be converted to a String (SVG Tiny only). */ public String getTraitImpl(final String name) throws DOMException { if (SVGConstants.SVG_AUDIO_LEVEL_ATTRIBUTE == name) { return Float.toString(audioLevel); } else if (SVGConstants.SVG_TYPE_ATTRIBUTE == name) { return type; } else if (SVGConstants.SVG_SYNC_BEHAVIOR_ATTRIBUTE == name) { return syncBehavior; } else if (SVGConstants.SVG_SYNC_TOLERANCE_ATTRIBUTE == name) { return Time.toStringTrait(syncTolerance); } else if (SVGConstants.SVG_SYNC_MASTER_ATTRIBUTE == name) { return String.valueOf(syncMaster); } else { return super.getTraitImpl(name); } } /** * MediaElement handles the audio-level, type, syncBehavior, syncTolerance * and syncMaster traits. * * @param name the name of the trait to set. * @param value the value of the trait to set. * * @throws DOMException with error code NOT_SUPPORTED_ERROR if the * requested trait is not supported on this element or null. * @throws DOMException with error code TYPE_MISMATCH_ERR if the requested * trait's value cannot be specified as a String * @throws DOMException with error code INVALID_ACCESS_ERR if the input * value is an invalid value for the given trait or null. * @throws DOMException with error code NO_MODIFICATION_ALLOWED_ERR: if * attempt is made to change readonly trait. */ public void setTraitImpl(final String name, final String value) throws DOMException { if (SVGConstants.SVG_AUDIO_LEVEL_ATTRIBUTE == name) { if (value == null) { throw illegalTraitValue(name, value); } setAudioLevel(parseFloatTrait(name, value)); } else if (SVGConstants.SVG_TYPE_ATTRIBUTE == name) { // type is not animatable setType(value); } else if (SVGConstants.SVG_SYNC_BEHAVIOR_ATTRIBUTE == name) { // syncBehavior is not animatable if (SVGConstants.SVG_CAN_SLIP_VALUE.equals(value) || SVGConstants.SVG_INDEPENDENT_VALUE.equals(value) || SVGConstants.SVG_LOCKED_VALUE.equals(value)) { syncBehavior = value; } else if (SVGConstants.XML_DEFAULT_VALUE.equals(value)) { ModelNode mn = getOwnerDocument().getFirstChildNode(); if (mn != null && mn instanceof SVG) { SVG svg = (SVG)mn; syncBehavior = svg.getSyncBehaviorDefault(); } else { // If the document tree does not contain a "svg" element // that we can get the "syncBehaviorDefault" value from, // use the default that we know is used for // "syncBehaviorDefault" in the "svg" element. syncBehavior = SVGConstants.SVG_CAN_SLIP_VALUE; } } else { throw illegalTraitValue(name, value); } } else if (SVGConstants.SVG_SYNC_TOLERANCE_ATTRIBUTE == name) { // syncTolerance is not animatable if (value.equals(SVGConstants.XML_DEFAULT_VALUE)) { ModelNode mn = getOwnerDocument().getFirstChildNode(); if (mn != null && mn instanceof SVG) { SVG svg = (SVG)mn; syncTolerance = svg.getSyncToleranceDefault(); } else { // If the document tree does not contain a "svg" element // that we can get the "syncToleranceDefault" value from, // use the default that we know is used for // "synToleranceDefault" in the "svg" element. syncTolerance = new Time(2000); } } else { syncTolerance = parseClockTrait(name, value); } } else if (SVGConstants.SVG_SYNC_MASTER_ATTRIBUTE == name) { // syncMaster is not animatable if (SVGConstants.SVG_TRUE_VALUE.equals(value)) { syncMaster = true; } else if (SVGConstants.SVG_FALSE_VALUE.equals(value)) { syncMaster = false; } else { throw illegalTraitValue(name, value); } } else { super.setTraitImpl(name, value); } } /** * MediaElement handles the xlink:href attribute * * @param namespaceURI the URI for the requested trait. * @param name the requested trait's local name (i.e., un-prefixed). * * @return the requested trait's value, as a string. * * @throws DOMException with error code NOT_SUPPORTED_ERROR if the requested * trait is not supported on this element or null. * @throws DOMException with error code TYPE_MISMATCH_ERR if requested * trait's computed value cannot be converted to a String (SVG Tiny only). * @throws SecurityException if the application does not have the necessary * privilege rights to access this (SVG) content. */ String getTraitNSImpl(final String namespaceURI, final String name) throws DOMException { if ((SVGConstants.XLINK_NAMESPACE_URI == namespaceURI) && (SVGConstants.SVG_HREF_ATTRIBUTE == name)) { return href; } else { return super.getTraitNSImpl(namespaceURI, name); } } /** * MediaElement supports the xlink:href trait. * * @param namespaceURI the URI for the trait's namespace. * @param name the trait's local name (i.e., un-prefixed). * @param value the trait's value. * * @throws DOMException with error code NOT_SUPPORTED_ERROR if the * requested trait is not supported on this element or null. * @throws DOMException with error code TYPE_MISMATCH_ERR if the requested * trait's value cannot be specified as a String * @throws DOMException with error code INVALID_ACCESS_ERR if the input * value is an invalid value for the given trait or null. * @throws DOMException with error code NO_MODIFICATION_ALLOWED_ERR: if * attempt is made to change readonly trait. * @throws SecurityException if the application does not have the necessary * privilege rights to access this (SVG) content. */ public void setTraitNSImpl(final String namespaceURI, final String name, final String value) throws DOMException { try { if ((SVGConstants.XLINK_NAMESPACE_URI == namespaceURI) && (SVGConstants.SVG_HREF_ATTRIBUTE == name)) { if (value == null) { throw illegalTraitValue(name, value); } setHref(value); } else { super.setTraitNSImpl(namespaceURI, name, value); } } catch (IllegalArgumentException iae) { throw new DOMException(DOMException.INVALID_ACCESS_ERR, iae.getMessage()); } } /** * @param name the requested trait name (e.g., "audio-level") * @return the requested trait value * * @throws DOMException with error code NOT_SUPPORTED_ERROR if the requested * trait is not supported on this element or null. * @throws DOMException with error code TYPE_MISMATCH_ERR if requested * trait's computed value cannot be converted to a float * @throws SecurityException if the application does not have the necessary * privilege rights to access this (SVG) content. */ float getFloatTraitImpl(final String name) throws DOMException { if (SVGConstants.SVG_AUDIO_LEVEL_ATTRIBUTE == name) { return audioLevel; } else { return super.getFloatTraitImpl(name); } } /** * Set the trait value as float. * * @param name the name of the trait to set. * @param value the value of the trait to set. * * @throws DOMException with error code NOT_SUPPORTED_ERROR if the * requested trait is not supported on this element. * @throws DOMException with error code TYPE_MISMATCH_ERR if the requested * trait's value cannot be specified as a float * @throws DOMException with error code INVALID_ACCESS_ERR if the input * value is an invalid value for the given trait. * @throws SecurityException if the application does not have the necessary * privilege rights to access this (SVG) content. */ public void setFloatTraitImpl(final String name, final float value) throws DOMException { if (SVGConstants.SVG_AUDIO_LEVEL_ATTRIBUTE == name) { setAudioLevel(value); } else { super.setFloatTraitImpl(name, value); } } /** * @param name the name of the trait to convert. * @param value the float trait value to convert. */ String toStringTrait(final String name, final float[][] value) { if (SVGConstants.SVG_AUDIO_LEVEL_ATTRIBUTE == name) { return Float.toString(value[0][0]); } else if (SVGConstants.SVG_TYPE_ATTRIBUTE == name) { return type; } else { return super.toStringTrait(name, value); } } /** * Validates the input trait value. * * @param traitName the name of the trait to be validated. * @param value the value to be validated * @param reqNamespaceURI the namespace of the element requesting validation. * @param reqLocalName the local name of the element requesting validation. * @param reqTraitNamespace the namespace of the trait which has the values * value on the requesting element. * @param reqTraitName the name of the trait which has the values value on * the requesting element. * @throws DOMException with error code INVALID_ACCESS_ERR if the input * value is incompatible with the given trait. */ public float[][] validateFloatArrayTrait(final String traitName, final String value, final String reqNamespaceURI, final String reqLocalName, final String reqTraitNamespace, final String reqTraitName) throws DOMException { if (SVGConstants.SVG_AUDIO_LEVEL_ATTRIBUTE == traitName) { float v = parseFloatTrait(traitName, value); return new float[][] {{v}}; } else { return super.validateFloatArrayTrait(traitName, value, reqNamespaceURI, reqLocalName, reqTraitNamespace, reqTraitName); } } /** * Set the trait value as float. * * @param name the trait's name. * @param value the trait's value. * * @throws DOMException with error code NOT_SUPPORTED_ERROR if the requested * trait is not supported on this element. * @throws DOMException with error code TYPE_MISMATCH_ERR if the requested * trait's value cannot be specified as a float * @throws DOMException with error code INVALID_ACCESS_ERR if the input * value is an invalid value for the given trait. */ void setFloatArrayTrait(final String name, final float[][] value) throws DOMException { if (SVGConstants.SVG_AUDIO_LEVEL_ATTRIBUTE == name) { setAudioLevel(value[0][0]); } else { super.setFloatArrayTrait(name, value); } } /** * Event dispatching override, to capture begin and end events. * * When a begin event occurs, the audio is started. When the end event is * fired, the audio is stopped. When a restart event is dispatched, the * audio is restarted. * * When an end event occurs, the animation removes itself from the target * element and trait's TraitAnim if the animation is not in the frozen * state. * * @param evt the event that occured */ public void dispatchEvent(final ModelEvent evt) { super.dispatchEvent(evt); if (TimedElementSupport.BEGIN_EVENT_TYPE.equals(evt.getType())) { ownerDocument.activeMediaElements.addElement(this); } else if (TimedElementSupport.REPEAT_EVENT_TYPE.equals(evt.getType())) { ownerDocument.activeMediaElements.addElement(this); } else if (TimedElementSupport.END_EVENT_TYPE.equals(evt.getType())) { endMedia(); ownerDocument.activeMediaElements.removeElement(this); } else { } } /** * Clears the input marker. * * @param marker the marker to clear. */ void clearMarker(final int marker) { markers &= ~marker; } /** * @return true if the input marker is set. */ final boolean isMarkerSet(final int marker) { return (markers & marker) != 0; } /** * Sets the input marker. * * @param marker the marker to set. */ void setMarker(final int marker) { markers |= marker; } /** * Check if the property is inherited. * * @param propertyIndex the index of the property whose * inherit status is checked. * @return true if the input property is inherited. False * otherwise */ public boolean isInherited(int propertyIndex) { return isMarkerSet(propertyIndex); } /** * Sets the input property's inheritance status * @param propertyIndex the index of the property whose inherit * status is set * @param inherit the new inherit status for the property at * index propertyIndex. */ public void setInherited(int propertyIndex, boolean inherit) { if (isInherited(propertyIndex) == inherit) { return; } modifyingNode(); setInheritedQuiet(propertyIndex, inherit); if (inherit) { // The property is now inherited. We store the inherited // value on the node, which means we keep the computed value // on the node. Object inheritedValue = getInheritedPropertyState(propertyIndex); setPropertyState(propertyIndex, inheritedValue); // Notify children that the inherited value has changed. propagatePropertyState(propertyIndex, inheritedValue); } // If the value is not inherited, it means that we are in the middle of // specifying a value on the node. So we do not notify descendants, // because this is done in the corresponding methods, e.g., setFill. modifiedNode(); } /** * Implementation. Sets the input property's inheritance status, * but does not send modification events. * * @param propertyIndex the index for the property whose inherited state * is set * @param inherit the new property's state */ protected void setInheritedQuiet(final int propertyIndex, final boolean inherit) { if (inherit) { setMarker(propertyIndex); } else { clearMarker(propertyIndex); } } /** * Sets the input float property's inheritance status * @param propertyIndex the index of the property whose inherit * status is set * @param inherit the new inherit status for the property at * index propertyIndex. */ public void setFloatInherited(int propertyIndex, boolean inherit) { if (isInherited(propertyIndex) == inherit) { return; } modifyingNode(); setInheritedQuiet(propertyIndex, inherit); if (inherit) { // The property is now inherited. We store the inherited // value on the node, which means we keep the computed value // on the node. float inheritedValue = getInheritedFloatPropertyState(propertyIndex); setFloatPropertyState(propertyIndex, inheritedValue); // Notify children that the inherited value has changed. propagateFloatPropertyState(propertyIndex, inheritedValue); } // If the value is not inherited, it means that we are in the middle of // specifying a value on the node. So we do not notify descendants, // because this is done in the corresponding methods, e.g., setFill. modifiedNode(); } /** * Sets the input packed property's inheritance status * @param propertyIndex the index of the property whose inherit * status is set * @param inherit the new inherit status for the property at * index propertyIndex. */ public void setPackedInherited(int propertyIndex, boolean inherit) { if (isInherited(propertyIndex) == inherit) { return; } modifyingNode(); setInheritedQuiet(propertyIndex, inherit); if (inherit) { // The property is now inherited. We store the inherited // value on the node, which means we keep the computed value // on the node. int inheritedValue = getInheritedPackedPropertyState(propertyIndex); setPackedPropertyState(propertyIndex, inheritedValue); // Notify children that the inherited value has changed. propagatePackedPropertyState(propertyIndex, inheritedValue); } // If the value is not inherited, it means that we are in the middle of // specifying a value on the node. So we do not notify descendants, // because this is done in the corresponding methods, e.g., setFill. modifiedNode(); } /** * @return the number of properties on this node * Note: This method will have to be updated when new properties are * supported on this element. */ public int getNumberOfProperties() { return ViewportNode.NUMBER_OF_PROPERTIES; } /* * Note: The method recomputePackedPropertyState() will have to be implemented * if a packed property is to be added to MediaElement or any of its sub-classes. */ /** * Recomputes the given Object-valued property's state given the * new parent property. * * @param propertyIndex index for the property whose value is changing. * @param parentPropertyValue the value that children of this node should * now inherit. * */ protected void recomputePropertyState(final int propertyIndex, final Object parentPropertyValue) { // We do not need to recompute the property value if: // - the property is _not_ inherited // or // - the property is inherited by the new parent property computed value // is the same as the current value. if (!isInherited(propertyIndex) || isPropertyState(propertyIndex, parentPropertyValue)) { // If the property is color relative, the propagation happens // through the color property changes. This means that with // currentColor, we inherit the computed value, not the specified // currentColor indirection. return; } setPropertyState(propertyIndex, parentPropertyValue); propagatePropertyState(propertyIndex, parentPropertyValue); } /** * Recomputes the given float-valued property's state given the new parent * property. * * @param propertyIndex index for the property whose value is changing. * @param parentPropertyValue the value that children of this node should * now inherit. * */ protected void recomputeFloatPropertyState(final int propertyIndex, final float parentPropertyValue) { // We do not need to recompute the property value if: // - the property is _not_ inherited // or // - the property is inherited by the new parent property computed value // is the same as the current value. if (!isInherited(propertyIndex) || isFloatPropertyState(propertyIndex, parentPropertyValue)) { // If the property is color relative, the propagation happens // through the color property changes. This means that with // currentColor, we inherit the computed value, not the specified // currentColor indirection. return; } setFloatPropertyState(propertyIndex, parentPropertyValue); propagateFloatPropertyState(propertyIndex, parentPropertyValue); } /** * Starts the underlying media player and handles repetitions if the sample * time wraps around. */ public void playMedia() { if (!mediaStarted) { try { init(); play(timedElementSupport.lastSampleTime); } catch (Exception e) { System.err.println("Cannot initialize media player"); } mediaStarted = true; } else { // check for repeat if (timedElementSupport.lastSampleTime < sampleTime) { play(timedElementSupport.lastSampleTime); } } if (mediaStarted) { updateFrame(); } sampleTime = timedElementSupport.lastSampleTime; } /** * Stops the underlying media player. */ public void endMedia() { if (mediaStarted) { stop(); mediaStarted = false; } } /** * When a MediaElement is unhooked from the document tree, it should stop * playing if it is currently playing media. In addition it should remove * itself from its ownerDocument's activeMediaElements Vector. */ void nodeUnhookedFromDocumentTree() { super.nodeUnhookedFromDocumentTree(); // If media is playing, stop it endMedia(); // Remove this MediaElement from ownerDocument's list of // activeMediaElements ownerDocument.activeMediaElements.removeElement(this); } /** * Initializes the media player. * * @throws Exception if the initialization fails. */ abstract void init() throws Exception; /** * Starts the media player. * * @param startTime The start time in milliseconds. */ abstract void play(long startTime); /** * Stops the media player. */ abstract void stop(); /** * Closes the media player. */ abstract void close(); /** * Updates the video frame. */ abstract void updateFrame(); /** * Set the volume level using a floating point scale with values between 0.0 * and 1.0. 0.0 is silence; 1.0 is the loudest useful level that this * GainControl supports. * * Should be used by implementations to set the volume on the * associated player abstractions, for example. * * @param volume the volume level to apply to the element. */ abstract void setVolume(float volume); }