ctbrec-5.3.2-experimental/client/src/main/java/ctbrec/ui/Player.java

226 lines
8.3 KiB
Java

package ctbrec.ui;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutionException;
import javax.xml.bind.JAXBException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.iheartradio.m3u8.ParseException;
import com.iheartradio.m3u8.PlaylistException;
import ctbrec.Config;
import ctbrec.Model;
import ctbrec.OS;
import ctbrec.Recording;
import ctbrec.io.StreamRedirectThread;
import ctbrec.io.UrlUtil;
import ctbrec.recorder.download.StreamSource;
import ctbrec.ui.controls.Dialogs;
import javafx.scene.Scene;
public class Player {
private static final Logger LOG = LoggerFactory.getLogger(Player.class);
private static PlayerThread playerThread;
public static Scene scene;
private Player() {
}
private static boolean play(String url, boolean async) {
boolean singlePlayer = Config.getInstance().getSettings().singlePlayer;
try {
if (singlePlayer && playerThread != null && playerThread.isRunning()) {
playerThread.stopThread();
}
playerThread = new PlayerThread(url);
if (!async) {
playerThread.join();
}
return true;
} catch (Exception e1) {
LOG.error("Couldn't start player", e1);
return false;
}
}
public static boolean play(Recording rec) {
boolean singlePlayer = Config.getInstance().getSettings().singlePlayer;
try {
if (singlePlayer && playerThread != null && playerThread.isRunning()) {
playerThread.stopThread();
}
playerThread = new PlayerThread(rec);
return true;
} catch (Exception e1) {
LOG.error("Couldn't start player", e1);
return false;
}
}
public static boolean play(Model model) {
return play(model, true);
}
public static boolean play(Model model, boolean async) {
try {
if (model.isOnline(true)) {
boolean singlePlayer = Config.getInstance().getSettings().singlePlayer;
if (singlePlayer && playerThread != null && playerThread.isRunning()) {
playerThread.stopThread();
}
String playlistUrl = getPlaylistUrl(model);
LOG.debug("Playing {}", playlistUrl);
return Player.play(playlistUrl, async);
} else {
Dialogs.showError(scene, "Room not public", "Room is currently not public", null);
return false;
}
} catch (Exception e1) {
LOG.error("Couldn't get stream information for model {}", model, e1);
Dialogs.showError(scene, "Couldn't determine stream URL", e1.getLocalizedMessage(), e1);
return false;
}
}
private static String getPlaylistUrl(Model model) throws IOException, ExecutionException, ParseException, PlaylistException, JAXBException {
List<StreamSource> sources = model.getStreamSources();
Collections.sort(sources);
StreamSource best;
int maxRes = Config.getInstance().getSettings().maximumResolutionPlayer;
if (maxRes > 0 && !sources.isEmpty()) {
for (Iterator<StreamSource> iterator = sources.iterator(); iterator.hasNext();) {
StreamSource streamSource = iterator.next();
if (streamSource.height > 0 && maxRes < streamSource.height) {
LOG.trace("Res too high {} > {}", streamSource.height, maxRes);
iterator.remove();
}
}
}
if (sources.isEmpty()) {
throw new RuntimeException("No stream left in playlist, because player resolution is set to " + maxRes);
} else {
LOG.debug("{} selected {}", model.getName(), sources.get(sources.size() - 1));
best = sources.get(sources.size() - 1);
}
return best.getMediaPlaylistUrl();
}
public static void stop() {
if (playerThread != null) {
playerThread.stopThread();
}
}
private static class PlayerThread extends Thread {
private boolean running = false;
private Process playerProcess;
private String url;
private Recording rec;
PlayerThread(String url) {
this.url = url;
setName(getClass().getName());
start();
}
PlayerThread(Recording rec) {
this.rec = rec;
setName(getClass().getName());
start();
}
@Override
public void run() {
running = true;
Runtime rt = Runtime.getRuntime();
Config cfg = Config.getInstance();
try {
if (cfg.getSettings().localRecording && rec != null) {
File file = rec.getAbsoluteFile();
String[] cmdline = createCmdline(file.getAbsolutePath());
playerProcess = rt.exec(cmdline, OS.getEnvironment(), file.getParentFile());
} else {
if (rec != null) {
url = getRemoteRecordingUrl(rec, cfg);
}
LOG.debug("Playing {}", url);
String[] cmdline = createCmdline(url);
playerProcess = rt.exec(cmdline);
}
// create threads, which read stdout and stderr of the player process. these are needed,
// because otherwise the internal buffer for these streams fill up and block the process
Thread std = new Thread(new StreamRedirectThread(playerProcess.getInputStream(), OutputStream.nullOutputStream()));
//Thread std = new Thread(new StreamRedirectThread(playerProcess.getInputStream(), System.out));
std.setName("Player stdout pipe");
std.setDaemon(true);
std.start();
Thread err = new Thread(new StreamRedirectThread(playerProcess.getErrorStream(), OutputStream.nullOutputStream()));
//Thread err = new Thread(new StreamRedirectThread(playerProcess.getErrorStream(), System.err));
err.setName("Player stderr pipe");
err.setDaemon(true);
err.start();
playerProcess.waitFor();
LOG.debug("Media player finished.");
} catch (Exception e) {
LOG.error("Error in player thread", e);
Dialogs.showError(scene, "Playback failed", "Couldn't start playback", e);
}
running = false;
}
private String[] createCmdline(String mediaSource) {
Config cfg = Config.getInstance();
String params = cfg.getSettings().mediaPlayerParams.trim();
String[] cmdline = null;
if(!params.isEmpty()) {
String[] playerArgs = params.split(" ");
cmdline = new String[playerArgs.length + 2];
System.arraycopy(playerArgs, 0, cmdline, 1, playerArgs.length);
} else {
cmdline = new String[2];
}
cmdline[0] = cfg.getSettings().mediaPlayer;
cmdline[cmdline.length - 1] = mediaSource;
LOG.debug("Player command line: {}", Arrays.toString(cmdline));
return cmdline;
}
private String getRemoteRecordingUrl(Recording rec, Config cfg)
throws MalformedURLException, InvalidKeyException, NoSuchAlgorithmException, UnsupportedEncodingException {
String hlsBase = Config.getInstance().getServerUrl() + "/hls";
String recUrl = hlsBase + '/' + rec.getId() + (rec.isSingleFile() ? "" : "/playlist.m3u8");
if (cfg.getSettings().requireAuthentication) {
recUrl = UrlUtil.addHmac(recUrl, cfg);
}
return recUrl;
}
public boolean isRunning() {
return running;
}
public void stopThread() {
if (playerProcess != null) {
playerProcess.destroy();
}
}
}
}