forked from j62/ctbrec
244 lines
9.3 KiB
Java
244 lines
9.3 KiB
Java
package ctbrec.ui;
|
|
|
|
import com.iheartradio.m3u8.ParseException;
|
|
import com.iheartradio.m3u8.PlaylistException;
|
|
import ctbrec.*;
|
|
import ctbrec.event.EventBusHolder;
|
|
import ctbrec.io.StreamRedirector;
|
|
import ctbrec.io.UrlUtil;
|
|
import ctbrec.recorder.download.StreamSource;
|
|
import ctbrec.recorder.download.hls.NoStreamFoundException;
|
|
import ctbrec.ui.controls.Dialogs;
|
|
import ctbrec.ui.event.PlayerStartedEvent;
|
|
import ctbrec.variableexpansion.ModelVariableExpander;
|
|
import javafx.scene.Scene;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
import javax.xml.bind.JAXBException;
|
|
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;
|
|
|
|
public class Player {
|
|
private static final Logger LOG = LoggerFactory.getLogger(Player.class);
|
|
private static PlayerThread playerThread;
|
|
private static Scene scene;
|
|
|
|
private Player() {
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
EventBusHolder.BUS.post(new PlayerStartedEvent(model));
|
|
|
|
if (singlePlayer && playerThread != null && playerThread.isRunning()) {
|
|
playerThread.stopThread();
|
|
}
|
|
|
|
playerThread = new PlayerThread(model);
|
|
if (!async) {
|
|
playerThread.join();
|
|
}
|
|
return true;
|
|
} else {
|
|
Dialogs.showError(scene, "Room not public", "Room is currently not public", null);
|
|
return false;
|
|
}
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
LOG.error("Couldn't get stream information for model {}", model, e);
|
|
Dialogs.showError(scene, "Couldn't determine stream URL", e.getLocalizedMessage(), e);
|
|
return false;
|
|
} catch (Exception e) {
|
|
LOG.error("Couldn't get stream information for model {}", model, e);
|
|
Dialogs.showError(scene, "Couldn't determine stream URL", e.getLocalizedMessage(), e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public static void stop() {
|
|
if (playerThread != null) {
|
|
playerThread.stopThread();
|
|
}
|
|
}
|
|
|
|
private static class PlayerThread extends Thread {
|
|
private boolean running = false;
|
|
private Process playerProcess;
|
|
private Recording rec;
|
|
private Model model;
|
|
|
|
PlayerThread(Model model) {
|
|
this.model = model;
|
|
setName(getClass().getName());
|
|
start();
|
|
}
|
|
|
|
PlayerThread(Recording rec) {
|
|
this.rec = rec;
|
|
this.model = rec.getModel();
|
|
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(), model);
|
|
playerProcess = rt.exec(cmdline, OS.getEnvironment(), file.getParentFile());
|
|
} else {
|
|
String url = null;
|
|
if (rec != null) {
|
|
url = getRemoteRecordingUrl(rec, cfg);
|
|
model = rec.getModel();
|
|
} else if (model != null) {
|
|
url = getPlaylistUrl(model);
|
|
}
|
|
LOG.debug("Playing {}", url);
|
|
String[] cmdline = createCmdline(url, model);
|
|
LOG.debug("Player command line: {}", Arrays.toString(cmdline));
|
|
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 StreamRedirector(playerProcess.getInputStream(), OutputStream.nullOutputStream()));
|
|
std.setName("Player stdout pipe");
|
|
std.setDaemon(true);
|
|
std.start();
|
|
Thread err = new Thread(new StreamRedirector(playerProcess.getErrorStream(), OutputStream.nullOutputStream()));
|
|
err.setName("Player stderr pipe");
|
|
err.setDaemon(true);
|
|
err.start();
|
|
|
|
playerProcess.waitFor();
|
|
LOG.debug("Media player finished.");
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
LOG.error("Error in player thread", e);
|
|
Dialogs.showError(scene, "Playback failed", "Couldn't start playback", e);
|
|
} catch (Exception e) {
|
|
LOG.error("Error in player thread", e);
|
|
Dialogs.showError(scene, "Playback failed", "Couldn't start playback", e);
|
|
}
|
|
running = 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 NoStreamFoundException("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();
|
|
}
|
|
|
|
private void expandPlaceHolders(String[] cmdline) {
|
|
ModelVariableExpander expander = new ModelVariableExpander(model, Config.getInstance(), null, null);
|
|
for (int i = 0; i < cmdline.length; i++) {
|
|
var param = cmdline[i];
|
|
param = expander.expand(param);
|
|
cmdline[i] = param;
|
|
}
|
|
}
|
|
|
|
private String[] createCmdline(String mediaSource, Model model) {
|
|
Config cfg = Config.getInstance();
|
|
String params = cfg.getSettings().mediaPlayerParams.trim();
|
|
|
|
String[] cmdline;
|
|
if (params.isEmpty()) {
|
|
cmdline = new String[2];
|
|
} else {
|
|
String[] playerArgs = StringUtil.splitParams(params);
|
|
cmdline = new String[playerArgs.length + 2];
|
|
System.arraycopy(playerArgs, 0, cmdline, 1, playerArgs.length);
|
|
}
|
|
cmdline[0] = cfg.getSettings().mediaPlayer;
|
|
cmdline[cmdline.length - 1] = mediaSource;
|
|
if (model != null) {
|
|
expandPlaceHolders(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();
|
|
}
|
|
}
|
|
}
|
|
|
|
public static void setScene(Scene scene) {
|
|
Player.scene = scene;
|
|
}
|
|
}
|