forked from RPTools/maptool
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add features to playStream functions
- Move streaming methods to class MediaPlayerAdapter, as discussed in RPTools#668 - Add new parameters: startTime and stopTime to playStream & editStream - playStream with cycleCount 0 preloads the stream but does not play it - Add function getStreamProperties as suggested in RPTools#667 - Add support for volume slider through setGlobalVolume, but the slider needs to be created
- Loading branch information
Showing
3 changed files
with
381 additions
and
189 deletions.
There are no files selected for viewing
346 changes: 346 additions & 0 deletions
346
src/main/java/net/rptools/maptool/client/functions/MediaPlayerAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,346 @@ | ||
/* | ||
* This software Copyright by the RPTools.net development team, and | ||
* licensed under the Affero GPL Version 3 or, at your option, any later | ||
* version. | ||
* | ||
* MapTool Source Code 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. | ||
* | ||
* You should have received a copy of the GNU Affero General Public | ||
* License * along with this source Code. If not, please visit | ||
* <http://www.gnu.org/licenses/> and specifically the Affero license | ||
* text at <http://www.gnu.org/licenses/agpl.html>. | ||
*/ | ||
package net.rptools.maptool.client.functions; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.net.HttpURLConnection; | ||
import java.net.URI; | ||
import java.net.URISyntaxException; | ||
import java.net.URL; | ||
import java.util.HashMap; | ||
import java.util.concurrent.ConcurrentHashMap; | ||
import javafx.application.Platform; | ||
import javafx.scene.media.Media; | ||
import javafx.scene.media.MediaPlayer; | ||
import javafx.util.Duration; | ||
import net.rptools.maptool.language.I18N; | ||
import net.rptools.parser.ParserException; | ||
import net.sf.json.JSONArray; | ||
import net.sf.json.JSONObject; | ||
|
||
/** | ||
* This class handles audio streaming through MediaPlayer. MediaPlayer methods must be run on the | ||
* JavaFX app thread to work, which is why Platform.runLater is used extensively. | ||
* | ||
* @since 1.5.5 | ||
*/ | ||
public class MediaPlayerAdapter { | ||
private static final ConcurrentHashMap<String, MediaPlayerAdapter> mapStreams = | ||
new ConcurrentHashMap<>(); | ||
|
||
private static double globalVolume = 1; | ||
|
||
private final String strUri; | ||
private final Media media; | ||
private final MediaPlayer player; | ||
private Double volume; // stored because player volume also depends on global volume | ||
|
||
private MediaPlayerAdapter(String strUri, Media media) { | ||
this.strUri = strUri; | ||
this.media = media; | ||
this.player = new MediaPlayer(media); | ||
}; | ||
/** | ||
* Start a given stream from its url string. If already streaming the file, dispose of the | ||
* previous stream. | ||
* | ||
* @param strUri the String url of the stream | ||
* @param cycleCount how many times should the stream play. -1: infinite | ||
* @param volume the volume level of the stream (0-1) | ||
* @param start the start time in ms | ||
* @param stop the stop time in ms, -1: file duration | ||
* @return false if the file doesn't exist, true otherwise | ||
* @throws ParserException if issue with file | ||
*/ | ||
public static boolean playStream( | ||
String strUri, int cycleCount, double volume, double start, double stop) | ||
throws ParserException { | ||
final Media media; | ||
try { | ||
if (!uriExists(strUri)) return false; // leave without error message if uri ok but no file | ||
media = new Media(strUri); | ||
} catch (Exception e) { | ||
throw new ParserException( | ||
I18N.getText( | ||
"macro.function.sound.illegalargument", | ||
"playStream", | ||
strUri, | ||
e.getLocalizedMessage())); | ||
} | ||
|
||
// run this on the JavaFX thread | ||
Platform.runLater( | ||
new Runnable() { | ||
@Override | ||
public void run() { | ||
MediaPlayerAdapter adapter = mapStreams.get(strUri); | ||
boolean old = adapter != null; | ||
if (!old) { | ||
adapter = new MediaPlayerAdapter(strUri, media); | ||
mapStreams.put(strUri, adapter); | ||
} | ||
adapter.volume = volume; | ||
MediaPlayer player = adapter.player; | ||
|
||
int newCycle = cycleCount >= 0 ? cycleCount + player.getCurrentCount() : cycleCount; | ||
Duration durStart = new Duration(start); | ||
Duration durStop = stop >= 0 ? new Duration(stop) : player.getMedia().getDuration(); | ||
|
||
player.setCycleCount(newCycle); | ||
player.setVolume(volume * globalVolume); | ||
player.setStartTime(durStart); | ||
player.setStopTime(durStop); | ||
|
||
player.seek(durStart); // start playing from the start | ||
if (cycleCount != 0) { | ||
if (old) player.play(); | ||
else player.setAutoPlay(true); | ||
} else player.stop(); | ||
} | ||
}); | ||
return true; | ||
} | ||
|
||
/** | ||
* Stop a given stream from its url string. | ||
* | ||
* @param strUri the String url of the stream | ||
* @param remove should the stream be disposed | ||
*/ | ||
public static void stopStream(String strUri, boolean remove) { | ||
Platform.runLater( | ||
new Runnable() { | ||
@Override | ||
public void run() { | ||
if (strUri.equals("*")) { | ||
for (HashMap.Entry mapElement : mapStreams.entrySet()) | ||
((MediaPlayerAdapter) mapElement.getValue()).stopStream(remove); | ||
} else { | ||
MediaPlayerAdapter adapter = mapStreams.get(strUri); | ||
if (adapter != null) adapter.stopStream(remove); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Stop the stream. Should be ran from JavaFX app thread. | ||
* | ||
* @param remove should the stream be disposed and map updated | ||
*/ | ||
private void stopStream(boolean remove) { | ||
if (remove) { | ||
player.dispose(); | ||
mapStreams.remove(this.strUri); | ||
} else player.stop(); | ||
} | ||
|
||
/** | ||
* Edit a given stream from its url string. If a parameter is null, no change to that value. | ||
* | ||
* @param strUri the String uri of the stream | ||
* @param cycleCount how many times should the stream play. -1: infinite | ||
* @param volume the volume level of the stream (0-1) | ||
* @param start the start time in ms | ||
* @param stop the stop time in ms, -1: file duration | ||
*/ | ||
public static void editStream( | ||
String strUri, Integer cycleCount, Double volume, Double start, Double stop) { | ||
Platform.runLater( | ||
new Runnable() { | ||
@Override | ||
public void run() { | ||
if (strUri.equals("*")) { | ||
for (HashMap.Entry mapElement : mapStreams.entrySet()) | ||
((MediaPlayerAdapter) mapElement.getValue()) | ||
.editStream(cycleCount, volume, start, stop); | ||
} else { | ||
MediaPlayerAdapter adapter = mapStreams.get(strUri); | ||
if (adapter != null) adapter.editStream(cycleCount, volume, start, stop); | ||
} | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Edit the stream. Should be accessed from JavaFX app thread. If a parameter is null, no change | ||
* to that value. | ||
* | ||
* @param cycleCount how many times should the stream play. -1: infinite | ||
* @param volume the volume level of the stream (0-1) | ||
* @param start the start time in ms | ||
* @param stop the stop time in ms, -1: file duration | ||
*/ | ||
private void editStream(Integer cycleCount, Double volume, Double start, Double stop) { | ||
if (cycleCount != null) { | ||
if (cycleCount == 0) player.stop(); | ||
else { | ||
int newCycle = cycleCount >= 0 ? cycleCount + player.getCurrentCount() : cycleCount; | ||
player.setCycleCount(newCycle); | ||
} | ||
} | ||
if (volume != null) { | ||
this.volume = volume; | ||
player.setVolume(volume * globalVolume); | ||
} | ||
if (start != null) player.setStartTime(new Duration(start)); | ||
if (stop != null) { | ||
Duration durStop = stop >= 0 ? new Duration(stop) : Duration.INDEFINITE; | ||
player.setStopTime(durStop); | ||
} | ||
} | ||
|
||
/** | ||
* Return the existence status of resource from String uri | ||
* | ||
* @param strUri the String uri of the resource | ||
* @return true if resource exists, false otherwise | ||
* @throws IOException if uri is url, but url is incorrect | ||
* @throws URISyntaxException if uri is for local file, but uri is incorrect | ||
*/ | ||
public static boolean uriExists(String strUri) throws IOException, URISyntaxException { | ||
return isWeb(strUri) ? urlExist(strUri) : fileExist(strUri); | ||
} | ||
|
||
/** | ||
* Returns true if the uri is for a web resource, false otherwise | ||
* | ||
* @param strUri the String uri of the resource | ||
* @return true if String uri is URL, false otherwise | ||
*/ | ||
private static boolean isWeb(String strUri) { | ||
String s = strUri.trim().toLowerCase(); | ||
return s.startsWith("http://") || s.startsWith("https://"); | ||
} | ||
|
||
/** | ||
* Return the existence status of web resource from String uri | ||
* | ||
* @param strUri the String uri of the resource | ||
* @return true if resource exists, false otherwise | ||
* @throws IOException if uri is incorrect | ||
*/ | ||
private static boolean urlExist(String strUri) throws IOException { | ||
HttpURLConnection.setFollowRedirects(false); | ||
HttpURLConnection con = (HttpURLConnection) new URL(strUri).openConnection(); | ||
con.setRequestMethod("HEAD"); | ||
return (con.getResponseCode() == HttpURLConnection.HTTP_OK); | ||
} | ||
|
||
/** | ||
* Return the existence status of local resource from String uri | ||
* | ||
* @param strUri the String uri of the resource | ||
* @return true if resource exists, false otherwise | ||
* @throws URISyntaxException if uri is incorrect | ||
*/ | ||
private static boolean fileExist(String strUri) throws URISyntaxException { | ||
return new File(new URI(strUri).getPath()).exists(); | ||
} | ||
|
||
/** | ||
* Return the properties of a stream from its uri | ||
* | ||
* @param strUri the String uri of the stream | ||
* @return JSONObject for one stream, JSONArray of JSONObjects if all streams | ||
*/ | ||
public static Object getStreamProperties(String strUri) { | ||
JSONObject info; | ||
if (strUri.equals("*")) { | ||
JSONArray infoArray = new JSONArray(); | ||
for (HashMap.Entry mapElement : mapStreams.entrySet()) { | ||
info = ((MediaPlayerAdapter) mapElement.getValue()).getInfo(); | ||
if (info != null) infoArray.add(info); | ||
} | ||
return infoArray; | ||
} else { | ||
MediaPlayerAdapter adapter = mapStreams.get(strUri); | ||
if (adapter == null) return ""; | ||
else info = adapter.getInfo(); | ||
if (info == null) return ""; | ||
else return info; | ||
} | ||
} | ||
|
||
/** | ||
* Return the properties of a stream | ||
* | ||
* @return JSONObject of the properties | ||
*/ | ||
private JSONObject getInfo() { | ||
if (player.getStatus() == MediaPlayer.Status.UNKNOWN) return null; | ||
try { | ||
Duration durTotal = media.getDuration(); | ||
Object objTotal; | ||
if (durTotal.equals(Duration.INDEFINITE)) objTotal = -1; | ||
else if (durTotal.equals(Duration.UNKNOWN)) objTotal = "UNKNOWN"; | ||
else objTotal = durTotal.toSeconds(); | ||
|
||
Duration durStop = player.getStopTime(); | ||
Object objStop; | ||
if (durStop.equals(Duration.INDEFINITE) || durStop.equals(Duration.UNKNOWN)) | ||
objStop = objTotal; | ||
else objStop = durStop.toSeconds(); | ||
|
||
JSONObject info = new JSONObject(); | ||
info.put("uri", strUri); | ||
info.put("cycleCount", player.getCycleCount()); | ||
info.put("volume", volume); | ||
info.put("startTime", " " + player.getStartTime().toSeconds()); | ||
info.put("stopTime", " " + objStop); | ||
info.put("currentTime", " " + player.getCurrentTime().toSeconds()); | ||
info.put("totalTime", " " + objTotal); | ||
info.put("bufferTime", " " + player.getBufferProgressTime().toSeconds()); | ||
info.put("currentCount", " " + player.getCurrentCount()); | ||
info.put("status", player.getStatus().toString()); | ||
return info; | ||
} catch (Exception e) { | ||
return null; | ||
} | ||
} | ||
|
||
/** | ||
* Set the global volume, and update the volume of all players | ||
* | ||
* @param volume the global volume (0-1) | ||
*/ | ||
public static void setGlobalVolume(double volume) { | ||
globalVolume = volume; | ||
|
||
Platform.runLater( | ||
new Runnable() { | ||
@Override | ||
public void run() { | ||
for (HashMap.Entry mapElement : mapStreams.entrySet()) | ||
((MediaPlayerAdapter) mapElement.getValue()).updateVolume(); | ||
} | ||
}); | ||
} | ||
|
||
/** Update the volume of the stream */ | ||
private void updateVolume() { | ||
player.setVolume(volume * globalVolume); | ||
} | ||
|
||
/** | ||
* Get the global volume | ||
* | ||
* @return the global volume (0-1) | ||
*/ | ||
public static double getGlobalVolume() { | ||
return globalVolume; | ||
} | ||
} |
Oops, something went wrong.