/*
* Copyright 2010 Fred Sauer
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.allen_sauer.gwt.voices.client;
import com.google.gwt.core.client.GWT;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Overflow;
import com.google.gwt.dom.client.Style.Position;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.RootPanel;
import com.allen_sauer.gwt.voices.client.ui.FlashMovie;
import com.allen_sauer.gwt.voices.client.ui.VoicesMovie;
import com.allen_sauer.gwt.voices.client.util.DOMUtil;
/**
* Main class with which client code interact in order to create {@link Sound}
* objects, which can be played. In addition, each SoundController defines its
* own default volume and provides the ability to prioritize Flash based sound.
*
* <p>
* For the time being do not create 16 or more SoundControllers as that would
* result in 16+ Flash Players, which triggers an Adobe bug, mentioned <a
* href="http://bugzilla.mozilla.org/show_bug.cgi?id=289873#c41">here</a>.
*/
public class SoundController {
/**
* Enumeration for varying levels of MIME type support.
*/
public enum MimeTypeSupport {
/**
* Play back of the MIME type is known to NOT be supported in this browser,
* based on known capabilities of browsers with the same user agent and
* installed plugins.
*/
MIME_TYPE_NOT_SUPPORTED,
/**
* Play back of the MIME type is known to be supported in this browser,
* based on known capabilities of browsers with the same user agent and
* installed plugins, but this capability has not yet been initialized.
* Usually this is due to a browser plugin, such as <a href=
* 'http://www.adobe.com/products/flashplayer/'>Adobe Flash Player
* < / a > , <a href=
* 'http://www.apple.com/quicktime/download/'>Apple QuickTime</a> or <a
* href=
* 'http://www.microsoft.com/windows/windowsmedia/'>Windows Media Playe
* r < / a > .
*/
MIME_TYPE_SUPPORT_NOT_READY,
/**
* Play back of the MIME type is known to be supported in this browser,
* based on known capabilities of browsers with the same user agent and
* installed plugins.
*/
MIME_TYPE_SUPPORT_READY,
/**
* It is unknown (cannot be determined) whether play back of the MIME type
* is supported in this browser, based on known capabilities of browsers
* with the same user agent and installed plugins.
*/
MIME_TYPE_SUPPORT_UNKNOWN,
}
static final int DEFAULT_BALANCE = 0;
static final boolean DEFAULT_LOOPING = false;
static final int DEFAULT_VOLUME = 100;
static {
setVersion();
}
private static native void setVersion()
/*-{
$wnd.$GWT_VOICES_VERSION = "3.3.0";
}-*/;
/**
* Our DOM sound container which is positioned off screen.
*/
protected final Element soundContainer = DOM.createDiv();
private int defaultVolume = DEFAULT_VOLUME;
private SoundType[] preferredSoundTypes;
private VoicesMovie voicesWrapper;
/**
* We avoid using the usual, module relative {@link GWT#getModuleBaseURL()} to
* force {@literal gwt-voices.swf} to be loaded from the same domain as the
* host page. This avoids cross origin issues when the page contains a
* {@code <base href="http://...">} tag. To override the base URL in your
* code, use {@link SoundController#setGwtVoicesSwfLocation(String)}.
*/
private String gwtVoicesSwfBaseUrl = GWT.getHostPageBaseURL() + GWT.getModuleName() + "/";
/**
* Default constructor to be used by client code.
*/
public SoundController() {
initSoundContainer();
}
/**
* Create a new Sound object using the provided MIME type and URL. To enable
* streaming, or for cross origin content, use {@link #createSound(String, String, boolean, boolean)}.
*
* @param mimeType MIME type of the new Sound object
* @param url location of the new Sound object
* @return a new Sound object
*/
public Sound createSound(String mimeType, String url) {
return createSound(mimeType, url, false, false);
}
/**
* Create a new Sound object using the provided MIME type and URL.
*
* @param mimeType MIME type of the new Sound object
* @param url location of the new Sound object
* @param streaming whether or not to allow play back to start before sound
* has been fully downloaded
* @param crossOrigin true of the resource is cross origin, false for same origin
* @return a new Sound object
*/
public Sound createSound(String mimeType, String url, boolean streaming, boolean crossOrigin) {
Sound sound = createSoundImpl(mimeType, url, streaming, crossOrigin);
sound.setVolume(defaultVolume);
return sound;
}
/**
* Determine the current preferred sound types for new sounds.
*
* @return the current preferred sound types
*
* @deprecated this method will be removed in a future release
*/
@Deprecated
public SoundType[] getPreferredSoundTypes() {
return preferredSoundTypes;
}
/**
* Set the default volume (range <code>0-100</code>) for new sound.
*
* @param defaultVolume the default volume (range <code>0-100</code>) to be
* used for new sounds
*/
public void setDefaultVolume(int defaultVolume) {
this.defaultVolume = defaultVolume;
}
/**
* Set preferred {@link Sound} class: {@link Html5Sound}, {@link FlashSound},
* {@link NativeSound}.
*
* @param soundTypes the preferred sound types
*
* @deprecated this method is a temporary stop-gap, may be retired at any
* time, and may be made to do nothing at all without warning
*/
@Deprecated
public void setPreferredSoundTypes(SoundType... soundTypes) {
for (SoundType s : soundTypes) {
assert s != null;
}
preferredSoundTypes = soundTypes;
}
/**
* Provides a way to set the base URL for the {@literal gwt-voices.swf} file.
* The provided base URL must ends with a trailing {@literal /}.
*
* @param gwtVoicesSwfBaseUrl base URL relative to which
* {@literal gwt-voices.swf} can be found
*/
public void setGwtVoicesSwfLocation(String gwtVoicesSwfBaseUrl) {
assert gwtVoicesSwfBaseUrl.endsWith("/");
this.gwtVoicesSwfBaseUrl = gwtVoicesSwfBaseUrl;
}
/**
* Lazily instantiate Flash Movie so browser plug-in is not unnecessarily
* triggered.
*
* @return the new movie widget
*/
protected VoicesMovie getVoicesMovie() {
if (voicesWrapper == null) {
voicesWrapper = new VoicesMovie(DOMUtil.getUniqueId(), gwtVoicesSwfBaseUrl);
DOM.appendChild(soundContainer, voicesWrapper.getElement());
}
return voicesWrapper;
}
private Sound createSoundImplHtml5(String mimeType, String url, boolean streaming,
boolean crossOrigin) {
if (Html5Sound.getMimeTypeSupport(mimeType) == MimeTypeSupport.MIME_TYPE_SUPPORT_READY) {
return new Html5Sound(mimeType, url, streaming, crossOrigin);
}
return null;
}
private Sound createSoundImpl(String mimeType, String url, boolean streaming, boolean crossOrigin) {
Sound sound = null;
for (SoundType c : preferredSoundTypes) {
switch(c) {
case HTML5:
sound = createSoundImplHtml5(mimeType, url, streaming, crossOrigin);
break;
case FLASH:
sound = createSoundImplFlash(mimeType, url, streaming, crossOrigin);
break;
case NATIVE:
sound = createSoundImplWebAudio(mimeType, url, streaming, crossOrigin);
break;
case WEB_AUDIO:
sound = createSoundImplWebAudio(mimeType, url, streaming, crossOrigin);
break;
}
if (sound != null) {
return sound;
}
}
// Web Audio rocks
sound = createSoundImplWebAudio(mimeType, url, streaming, crossOrigin);
if (sound != null) {
return sound;
}
sound = createSoundImplFlash(mimeType, url, streaming, crossOrigin);
if (sound != null) {
return sound;
}
sound = createSoundImplHtml5(mimeType, url, streaming, crossOrigin);
if (sound != null) {
return sound;
}
// Fallback to native browser audio
sound = new NativeSound(mimeType, url, streaming, crossOrigin, soundContainer);
return sound;
}
private Sound createSoundImplWebAudio(String mimeType, String url, boolean streaming,
boolean crossOrigin) {
if (WebAudioSound.getMimeTypeSupport(mimeType) == MimeTypeSupport.MIME_TYPE_SUPPORT_READY) {
return new WebAudioSound(mimeType, url, streaming, crossOrigin);
}
return null;
}
private FlashSound createSoundImplFlash(String mimeType, String url, boolean streaming,
boolean crossOrigin) {
if (url.startsWith("data:")) {
return null;
}
if (FlashMovie.isExternalInterfaceSupported()) {
VoicesMovie vm = getVoicesMovie();
MimeTypeSupport mimeTypeSupport = vm.getMimeTypeSupport(mimeType);
if (mimeTypeSupport == MimeTypeSupport.MIME_TYPE_SUPPORT_READY
|| mimeTypeSupport == MimeTypeSupport.MIME_TYPE_SUPPORT_NOT_READY) {
FlashSound sound = new FlashSound(mimeType, url, streaming, crossOrigin, vm);
return sound;
}
}
return null;
}
private void initSoundContainer() {
String gwtVoices = Window.Location.getParameter(SoundType.QUERY_PARAMETER_NAME);
if (SoundType.FLASH.getQueryParameterValue().equals(gwtVoices)) {
setPreferredSoundTypes(SoundType.FLASH);
} else if (SoundType.HTML5.getQueryParameterValue().equals(gwtVoices)) {
setPreferredSoundTypes(SoundType.HTML5);
} else if (SoundType.WEB_AUDIO.getQueryParameterValue().equals(gwtVoices)) {
setPreferredSoundTypes(SoundType.WEB_AUDIO);
} else if (SoundType.NATIVE.getQueryParameterValue().equals(gwtVoices)) {
setPreferredSoundTypes(SoundType.NATIVE);
} else {
// For now, prefer Flash over HTML5
setPreferredSoundTypes(SoundType.WEB_AUDIO, SoundType.FLASH, SoundType.HTML5);
}
// place off screen with fixed dimensions and overflow:hidden
RootPanel.getBodyElement().appendChild(soundContainer);
Style style = soundContainer.getStyle();
style.setPosition(Position.ABSOLUTE);
style.setOverflow(Overflow.HIDDEN);
style.setLeft(-500, Unit.PX);
style.setTop(-500, Unit.PX);
style.setWidth(0, Unit.PX);
style.setHeight(0, Unit.PX);
}
}