package chatty.gui.components;
import chatty.Helper;
import chatty.gui.GuiUtil;
import chatty.gui.components.settings.EditorStringSetting;
import chatty.util.Livestreamer;
import chatty.util.Livestreamer.LivestreamerListener;
import chatty.util.StringUtil;
import chatty.util.settings.Settings;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
/**
* Contains settings and information about Livestreamer and the tabs that are
* created when you open a stream, that show output and have have controls to
* select quality or stop/re-run the Livestreamer process.
*
* @author tduva
*/
public class LivestreamerDialog extends JDialog {
private final JButton closeButton = new JButton("Close");
private final JTabbedPane tabs = new JTabbedPane();
private final Window parent;
private final JCheckBox enableContextMenu = new JCheckBox("Enable context menu entry");
private final JCheckBox openDialog = new JCheckBox("Show dialog when opening stream");
private final JTextField qualities = new JTextField(20);
private final EditorStringSetting commandDef;
private final JCheckBox useAuth = new JCheckBox("Use Authorization (Twitch Oauth Token)");
private final JTextField streamInput = new JTextField(30);
private final JButton openStreamButton = new JButton("Open Stream");
private static final String INFO = "Livestreamer is an external program "
+ "you have to install seperately that allows you to watch "
+ "streams of many websites in a player like VLC. "
+ "[help-livestreamer:top More information..]";
private static final String BASE_COMMAND_INFO = "<html><body style='width:340px;font-weight:normal;'>"
+ "Example Usage (setting the window title for VLC):<br />"
+ "<code>livestreamer -p \"'C:\\Program Files (x86)\\VideoLAN\\VLC\\vlc.exe' --meta-title '$stream/$quality'\"</code>"
+ "<br /><br />"
+ "This should point to the Livestreamer executable and can contain "
+ "parameters that should always be included when you run "
+ "Livestreamer via Chatty.<br /><br />"
+ "The url and quality are <em>automatically</em> appended when "
+ "you run Livestreamer via Chatty, but you can use them in other parameters "
+ "via <code>$stream</code>, <code>$url</code> and <code>$quality</code>.</p>";
private final Settings settings;
public LivestreamerDialog(Window parent, LinkLabelListener linkLabelListener,
final Settings settings) {
super(parent);
this.settings = settings;
setTitle("Livestreamer");
this.parent = parent;
setLocationRelativeTo(parent);
setLayout(new BorderLayout());
add(tabs, BorderLayout.CENTER);
closeButton.setMnemonic(KeyEvent.VK_C);
add(closeButton, BorderLayout.SOUTH);
/**
* Info Panel
*/
JPanel infoPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc;
LinkLabel info = new LinkLabel(INFO, linkLabelListener);
info.setPreferredSize(new Dimension(300, 50));
gbc = GuiUtil.makeGbc(0, 0, 2, 1);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1;
infoPanel.add(info, gbc);
gbc = GuiUtil.makeGbc(0, 1, 1, 1, GridBagConstraints.WEST);
gbc.insets = new Insets(5, 5, 0, 5);
infoPanel.add(enableContextMenu, gbc);
gbc = GuiUtil.makeGbc(0, 2, 1, 1, GridBagConstraints.WEST);
gbc.insets = new Insets(0, 15, 5, 5);
infoPanel.add(openDialog, gbc);
gbc = GuiUtil.makeGbc(0, 3, 1, 1, GridBagConstraints.WEST);
gbc.insets = new Insets(5, 5, 0, 5);
infoPanel.add(new JLabel("Context menu qualities (\"Select\" to select quality):"), gbc);
gbc = GuiUtil.makeGbc(0, 4, 1, 1, GridBagConstraints.WEST);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.insets = new Insets(4, 5, 5, 30);
infoPanel.add(qualities, gbc);
gbc = GuiUtil.makeGbc(0, 5, 1, 1, GridBagConstraints.WEST);
gbc.insets = new Insets(5, 5, 0, 5);
infoPanel.add(new JLabel("Base command (Livestreamer path and parameters):"), gbc);
gbc = GuiUtil.makeGbc(0, 6, 1, 1, GridBagConstraints.WEST);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1;
gbc.insets = new Insets(4, 5, 4, 30);
commandDef = new EditorStringSetting(this,
"Base command (Livestreamer path and paramters)",
24, false, false, BASE_COMMAND_INFO);
infoPanel.add(commandDef, gbc);
gbc = GuiUtil.makeGbc(0, 7, 1, 1, GridBagConstraints.WEST);
gbc.insets = new Insets(0, 5, 5, 5);
useAuth.setToolTipText("Supply the Oauth token Chatty uses to authorize for the stream (e.g. to watch sub-only Twitch streams)");
infoPanel.add(useAuth, gbc);
gbc = GuiUtil.makeGbc(0, 8, 2, 1, GridBagConstraints.WEST);
gbc.insets = new Insets(5, 5, 0, 5);
JLabel streamLabel = new JLabel("Enter stream name or URL (or commandline options):");
streamLabel.setLabelFor(streamInput);
infoPanel.add(streamLabel, gbc);
gbc = GuiUtil.makeGbc(0, 9, 1, 1);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1;
infoPanel.add(streamInput, gbc);
gbc = GuiUtil.makeGbc(1, 9, 1, 1);
openStreamButton.setMargin(GuiUtil.SMALL_BUTTON_INSETS);
gbc.fill = GridBagConstraints.HORIZONTAL;
infoPanel.add(openStreamButton, gbc);
tabs.add("Main", infoPanel);
ActionListener buttonAction = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == closeButton) {
setVisible(false);
settings.setString("livestreamerQualities", qualities.getText());
} else if (e.getSource() == openStreamButton
|| e.getSource() == streamInput) {
String stream = streamInput.getText();
if (!stream.isEmpty()) {
open(stream, null);
}
} else if (e.getSource() == enableContextMenu) {
// Only save setting, loading is done from the MainGui
settings.setBoolean("livestreamer", enableContextMenu.isSelected());
} else if (e.getSource() == useAuth) {
settings.setBoolean("livestreamerUseAuth", useAuth.isSelected());
} else if (e.getSource() == openDialog) {
settings.setBoolean("livestreamerShowDialog", openDialog.isSelected());
}
}
};
streamInput.addActionListener(buttonAction);
openStreamButton.addActionListener(buttonAction);
closeButton.addActionListener(buttonAction);
enableContextMenu.addActionListener(buttonAction);
useAuth.addActionListener(buttonAction);
openDialog.addActionListener(buttonAction);
commandDef.setChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
settings.setString("livestreamerCommand", commandDef.getSettingValue());
}
});
pack();
setMinimumSize(getSize());
}
/**
* Opens the given stream and quality. Shows the dialog depending on the
* settings and also loads the settings into the dialog.
*
* @param stream The stream to open
* @param quality The quality to open (null to select quality)
*/
public void open(String stream, String quality) {
if (stream != null) {
String url = "twitch.tv/" + stream;
if (!Helper.validateChannel(stream)) {
url = stream;
}
Item existingItem = getExisitingItem(url, quality);
if (existingItem != null) {
existingItem.start();
tabs.setSelectedComponent(existingItem);
} else {
Item newItem = new Item(url, quality, stream);
tabs.add(StringUtil.shortenTo(stream, -20), newItem);
tabs.setSelectedComponent(newItem);
tabs.setToolTipTextAt(tabs.getSelectedIndex(), stream);
newItem.start();
pack();
}
}
loadSettings();
if (stream == null || quality == null || openDialog.isSelected()) {
setLocationRelativeTo(parent);
setVisible(true);
}
}
/**
* Gets the item of an open tab with the given URL and quality, if the
* process isn't running anymore and the quality isn't null (which would
* mean that the user wants to select the quality).
*
* @param url The URL
* @param quality The quality
* @return An Item with the given requirements, or null if none was found
*/
private Item getExisitingItem(String url, String quality) {
for (Object o : tabs.getComponents()) {
if (o instanceof Item) {
Item item = (Item)o;
if (!item.running && item.quality != null
&& item.quality.equals(quality) && item.url.equals(url)) {
return item;
}
}
}
return null;
}
private void loadSettings() {
enableContextMenu.setSelected(settings.getBoolean("livestreamer"));
this.qualities.setText(settings.getString("livestreamerQualities"));
commandDef.setSettingValue(settings.getString("livestreamerCommand"));
useAuth.setSelected(settings.getBoolean("livestreamerUseAuth"));
openDialog.setSelected(settings.getBoolean("livestreamerShowDialog"));
}
/**
* Manages one instance that contains a stream (or more general any
* parameters) and a quality (which is just another parameter put behind the
* first). Listens to the responses of the process and shows them in the GUI
* and has buttons to control it.
*/
private class Item extends JPanel implements LivestreamerListener,
ActionListener {
private final JButton closeButton = new JButton("Close");
private final JButton retryButton = new JButton("Retry");
private final JTextArea messages = new JTextArea();
private final JLabel info = new JLabel();
private final String url;
private final String stream;
/**
* The quality of the stream, which is just another parameter that is
* put behind the first one.
*/
private String quality;
/**
* Whether the process is currently running.
*/
private boolean running;
/**
* Reference to the current Livestreamer object.
*/
private Livestreamer ls;
private final ActionListener qualityButtonListener = new QualityButtonListener();
private final JPanel buttonPanel = new JPanel();
/**
* Creates a new instance with the {@code url} to open (or any other
* parameters) and the {@code quality} to use.
*
* @param url The {@code url} (or any parameter) to use
* @param quality The {@code quality}, can be {@code null}, which means
* it is supposed to be selected in the dialog
*/
private Item(String url, String quality, String stream) {
this.url = url;
this.quality = quality;
this.stream = stream;
if (quality != null) {
info.setText("Selected quality: "+quality);
}
setLayout(new GridBagLayout());
GridBagConstraints gbc;
gbc = GuiUtil.makeGbc(0, 0, 1, 1);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1;
add(info, gbc);
gbc = GuiUtil.makeGbc(1, 0, 1, 1);
retryButton.setMargin(GuiUtil.SMALL_BUTTON_INSETS);
add(retryButton, gbc);
gbc = GuiUtil.makeGbc(2, 0, 1, 1);
closeButton.setMargin(GuiUtil.SMALL_BUTTON_INSETS);
add(closeButton, gbc);
gbc = GuiUtil.makeGbc(0, 1, 3, 1);
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1;
gbc.weighty = 1;
messages.setEditable(false);
messages.setLineWrap(true);
messages.setWrapStyleWord(true);
JScrollPane scroll = new JScrollPane(messages);
scroll.setPreferredSize(new Dimension(300, 150));
add(scroll, gbc);
gbc = GuiUtil.makeGbc(0, 2, 3, 1);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.weightx = 1;
add(buttonPanel, gbc);
closeButton.addActionListener(this);
retryButton.addActionListener(this);
}
/**
* Adds a mesage to the text area.
*
* @param message
*/
private void addMessage(String message) {
if (quality == null && message.trim().startsWith("Available streams:")) {
parseQualities(message);
}
Document doc = messages.getDocument();
try {
doc.insertString(doc.getLength(), message+"\n", null);
} catch (BadLocationException ex) {
Logger.getLogger(LivestreamerDialog.class.getName()).log(Level.SEVERE, null, ex);
}
}
/**
* Parses the output of Livestreamer that lists the available qualities
* and adds a button for each quality. This may not be very robust
* parsing if the format changes.
*
* @param message
*/
private void parseQualities(String message) {
// Remove everything in ( ), which isn't an actual quality and can
// get in the way otherwise
message = message.replaceAll("\\([^)]*\\)", "");
String[] split = message.split(":");
if (split.length == 2) {
String[] split2 = split[1].split(",");
buttonPanel.removeAll();
for (String part : split2) {
String q = part.trim();
JButton button = new JButton(q);
button.addActionListener(qualityButtonListener);
buttonPanel.add(button);
}
if (getWidth() < buttonPanel.getPreferredSize().width) {
pack();
}
info.setText("Click button to select quality.");
}
}
/**
* Start a new process (if none is currently running), based on the
* current settings.
*/
public void start() {
if (running) {
return;
}
setQualityButtonsEnabled(false);
if (quality == null) {
info.setText("No quality selected yet.");
} else {
info.setText("Selected quality: "+quality);
}
StringBuilder command = new StringBuilder();
command.append(makeBaseCommand());
if (url.contains("twitch.tv") && settings.getBoolean("livestreamerUseAuth")
&& !settings.getString("token").isEmpty()) {
command.append(" --twitch-oauth-token ");
command.append(settings.getString("token"));
}
command.append(" ");
command.append(url);
if (quality != null) {
command.append(" ");
command.append(quality);
}
Livestreamer ls = new Livestreamer(command.toString(), this);
this.ls = ls;
ls.start();
}
private String makeBaseCommand() {
String command = settings.getString("livestreamerCommand");
command = command.replace("$stream", stream);
command = command.replace("$url", url);
if (quality != null) {
command = command.replace("$quality", quality);
}
return command;
}
/**
* Sets the state of the process and changes the GUI accordingly.
*
* @param running
*/
private void setRunning(boolean running) {
if (running) {
closeButton.setText("End process");
} else {
closeButton.setText("Close");
}
retryButton.setEnabled(!running);
setQualityButtonsEnabled(!running);
this.running = running;
// Remove the tab only when the quality was already set, which means
// it should have been the final run, and the dialog isn't open,
// so the user probably doesn't need it anymore.
if (!running && quality != null && !LivestreamerDialog.this.isVisible()) {
tabs.remove(this);
}
}
@Override
public void processStarted(final String command) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
addMessage("COMMAND: "+command);
setRunning(true);
}
});
}
@Override
public void message(final String message) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
addMessage(message.replace("[cli][info] ", ""));
}
});
}
@Override
public void processFinished(final int exitValue) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
addMessage("PROCESS ENDED.");
setRunning(false);
}
});
}
@Override
public void actionPerformed(ActionEvent e) {
if (e.getSource() == closeButton) {
if (running) {
ls.kill();
} else {
tabs.remove(this);
}
} else if (e.getSource() == retryButton) {
if (!running) {
start();
}
}
}
private class QualityButtonListener implements ActionListener {
@Override
public void actionPerformed(ActionEvent e) {
quality = e.getActionCommand();
start();
}
}
private void setQualityButtonsEnabled(boolean enabled) {
for (Component c : buttonPanel.getComponents()) {
c.setEnabled(enabled);
}
}
}
}