package net.pms.util; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.io.UnsupportedEncodingException; import java.util.LinkedHashSet; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.swing.DefaultComboBoxModel; import javax.swing.SwingUtilities; import net.pms.PMS; import net.pms.configuration.DeviceConfiguration; import net.pms.dlna.DLNAResource; import net.pms.dlna.RealFile; import net.pms.dlna.virtual.VirtualVideoAction; import static net.pms.network.UPNPHelper.unescape; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public interface BasicPlayer extends ActionListener { public class State { public int playback; public boolean mute; public int volume; public String position, duration; public String name, uri, metadata; public long buffer; } final static int STOPPED = 0; final static int PLAYING = 1; final static int PAUSED = 2; final static int PLAYCONTROL = 1; final static int VOLUMECONTROL = 2; public void setURI(String uri, String metadata); public void pressPlay(String uri, String metadata); public void pressStop(); public void play(); public void pause(); public void stop(); public void next(); public void prev(); public void forward(); public void rewind(); public void mute(); public void setVolume(int volume); public void add(int index, String uri, String name, String metadata, boolean select); public void remove(String uri); public void setBuffer(long mb); public State getState(); public int getControls(); public DefaultComboBoxModel getPlaylist(); public void connect(ActionListener listener); public void disconnect(ActionListener listener); public void alert(); public void start(); public void reset(); public void close(); // An empty implementation with some basic funtionalities defined public static class Minimal implements BasicPlayer { public DeviceConfiguration renderer; protected State state; protected LinkedHashSet<ActionListener> listeners; public Minimal(DeviceConfiguration renderer) { this.renderer = renderer; state = new State(); listeners = new LinkedHashSet<>(); if (renderer.gui != null) { connect(renderer.gui); } reset(); } @Override public void start() { } @Override public void reset() { state.playback = STOPPED; state.position = ""; state.duration = ""; state.name = " "; state.buffer = 0; alert(); } @Override public void connect(ActionListener listener) { if (listener != null) { listeners.add(listener); } } @Override public void disconnect(ActionListener listener) { listeners.remove(listener); if (listeners.isEmpty()) { close(); } } @Override public void alert() { for (ActionListener l : listeners) { l.actionPerformed(new ActionEvent(this, 0, null)); } } @Override public BasicPlayer.State getState() { return state; } @Override public void close() { listeners.clear(); renderer.setPlayer(null); } @Override public void setBuffer(long mb) { state.buffer = mb; alert(); } @Override public void setURI(String uri, String metadata) { } @Override public void pressPlay(String uri, String metadata) { } @Override public void pressStop() { } @Override public void play() { } @Override public void pause() { } @Override public void stop() { } @Override public void next() { } @Override public void prev() { } @Override public void forward() { } @Override public void rewind() { } @Override public void mute() { } @Override public void setVolume(int volume) { } @Override public void add(int index, String uri, String name, String metadata, boolean select) { } @Override public void remove(String uri) { } @Override public int getControls() { return 0; } @Override public DefaultComboBoxModel getPlaylist() { return null; } @Override public void actionPerformed(final ActionEvent e) { } } // An abstract implementation with all internal playback/playlist logic included. // Ideally the entire state-machine resides here and subclasses just implement the // details of communicating with the target device. public static abstract class Logical extends Minimal { public Playlist playlist; protected boolean autoContinue, addAllSiblings, forceStop; protected int lastPlayback; protected int maxVol; public Logical(DeviceConfiguration renderer) { super(renderer); playlist = new Playlist(this); lastPlayback = STOPPED; maxVol = renderer.getMaxVolume(); autoContinue = renderer.isAutoContinue(); addAllSiblings = renderer.isAutoAddAll(); forceStop = false; alert(); initAutoPlay(this); } @Override public abstract void setURI(String uri, String metadata); public Playlist.Item resolveURI(String uri, String metadata) { if (uri != null) { Playlist.Item item; if (metadata != null && metadata.startsWith("<DIDL")) { // If it looks real assume it's valid return new Playlist.Item(uri, null, metadata); } else if ((item = playlist.get(uri)) != null) { // We've played it before return item; } else { // It's new to us, find or create the resource as required. // Note: here metadata (if any) is actually the resource name DLNAResource d = DLNAResource.getValidResource(uri, metadata, renderer); if (d != null) { return new Playlist.Item(d.getURL("", true), d.getDisplayName(), d.getDidlString(renderer)); } } } return null; } @Override public void pressPlay(String uri, String metadata) { forceStop = false; if (state.playback == -1) { // unknown state, we assume it's stopped state.playback = STOPPED; } if (state.playback == PLAYING) { pause(); } else { if (state.playback == STOPPED) { Playlist.Item item = playlist.resolve(uri); if (item != null) { uri = item.uri; metadata = item.metadata; state.name = item.name; } if (uri != null && !uri.equals(state.uri)) { setURI(uri, metadata); } } play(); } } @Override public void pressStop() { forceStop = true; stop(); } @Override public void next() { step(1); } @Override public void prev() { step(-1); } public void step(int n) { if (state.playback != STOPPED) { stop(); } state.playback = STOPPED; playlist.step(n); pressPlay(null, null); } @Override public void alert() { boolean stopping = state.playback == STOPPED && lastPlayback != -1 && lastPlayback != STOPPED; lastPlayback = state.playback; super.alert(); if (stopping && autoContinue && !forceStop) { next(); } } @Override public int getControls() { return renderer.controls; } @Override public DefaultComboBoxModel getPlaylist() { return playlist; } @Override public void add(int index, String uri, String name, String metadata, boolean select) { if (!StringUtils.isBlank(uri)) { if (addAllSiblings && DLNAResource.isResourceUrl(uri)) { DLNAResource d = PMS.getGlobalRepo().get(DLNAResource.parseResourceId(uri)); if (d != null && d.getParent() != null) { List<DLNAResource> list = d.getParent().getChildren(); addAll(index, list, list.indexOf(d)); return; } } playlist.add(index, uri, name, metadata, select); } } public void addAll(int index, List<DLNAResource> list, int selIndex) { for (int i = 0; i < list.size(); i++) { DLNAResource r = list.get(i); if ((r instanceof VirtualVideoAction) || r.isFolder()) { // skip these continue; } playlist.add(index, r.getURL("", true), r.getDisplayName(), r.getDidlString(renderer), i == selIndex); } } @Override public void remove(String uri) { if (!StringUtils.isBlank(uri)) { playlist.remove(uri); } } public void clear() { playlist.removeAllElements(); } private static void initAutoPlay(final Logical player) { String auto = player.renderer.getAutoPlay(); if (StringUtils.isEmpty(auto)) { return; } String[] strs = auto.split(" "); for (String s : strs) { String[] tmp = s.split(":", 2); if (tmp.length != 2) { continue; } if (!player.renderer.getConfName().equalsIgnoreCase(tmp[0])) { continue; } final String folder = tmp[1]; Runnable r = new Runnable() { @Override public void run() { while(PMS.get().getServer().getHost() == null) { try { Thread.sleep(1000); } catch (InterruptedException e) { return; } } RealFile f = new RealFile(new File(folder)); f.discoverChildren(); f.analyzeChildren(-1); player.addAll(-1, f.getChildren(), -1); // add a short delay here since player.add uses swing.invokelater try { Thread.sleep(1000); } catch (Exception e) { } player.pressPlay(null, null); } }; new Thread(r).start(); } } public static class Playlist extends DefaultComboBoxModel { private static final long serialVersionUID = 5934677633834195753L; Logical player; public Playlist(Logical p) { player = p; } public Item get(String uri) { int index = getIndexOf(new Item(uri, null, null)); if (index > -1) { return (Item) getElementAt(index); } return null; } public Item resolve(String uri) { Item item = null; try { Object selected = getSelectedItem(); Item selectedItem = selected instanceof Item ? (Item) selected : null; String selectedName = selectedItem != null ? selectedItem.name : null; // See if we have a matching item for the "uri", which could be: item = (Item) ( // An alias for the currently selected item StringUtils.isBlank(uri) || uri.equals(selectedName) ? selectedItem : // An item index, e.g. '$i$4' uri.startsWith("$i$") ? getElementAt(Integer.valueOf(uri.substring(3))) : // Or an actual uri get(uri)); } catch (Exception e) { } return (item != null && isValid(item, player.renderer)) ? item : null; } public static boolean isValid(Item item, DeviceConfiguration renderer) { if (DLNAResource.isResourceUrl(item.uri)) { // Check existence for resource uris if (PMS.get().getGlobalRepo().exists(DLNAResource.parseResourceId(item.uri))) { return true; } // Repair the item if possible DLNAResource d = DLNAResource.getValidResource(item.uri, item.name, renderer); if (d != null) { item.uri = d.getURL("", true); item.metadata = d.getDidlString(renderer); return true; } return false; } // Assume non-resource uris are valid return true; } public void validate() { for (int i = getSize() - 1; i > -1; i--) { if (!isValid((Item) getElementAt(i), player.renderer)) { removeElementAt(i); } } } public void set(String uri, String name, String metadata) { add(0, uri, name, metadata, true); } public void add(final int index, final String uri, final String name, final String metadata, final boolean select) { if (!StringUtils.isBlank(uri)) { // TODO: check headless mode (should work according to https://java.net/bugzilla/show_bug.cgi?id=2568) SwingUtilities.invokeLater(new Runnable() { public void run() { Item item = resolve(uri); if (item == null) { item = new Item(uri, name, metadata); insertElementAt(item, index > -1 ? index : getSize()); } if (select) { setSelectedItem(item); } } }); } } public void remove(final String uri) { if (!StringUtils.isBlank(uri)) { // TODO: check headless mode SwingUtilities.invokeLater(new Runnable() { public void run() { Item item = resolve(uri); if (item != null) { removeElement(item); } } }); } } public void step(int n) { if (getSize() > 0) { int i = (getIndexOf(getSelectedItem()) + getSize() + n) % getSize(); setSelectedItem(getElementAt(i)); } } @Override protected void fireContentsChanged(Object source, int index0, int index1) { player.alert(); super.fireContentsChanged(source, index0, index1); } @Override protected void fireIntervalAdded(Object source, int index0, int index1) { player.alert(); super.fireIntervalAdded(source, index0, index1); } @Override protected void fireIntervalRemoved(Object source, int index0, int index1) { player.alert(); super.fireIntervalRemoved(source, index0, index1); } public static class Item { private static final Logger LOGGER = LoggerFactory.getLogger(Item.class); public String name, uri, metadata; static final Matcher dctitle = Pattern.compile("<dc:title>(.+)</dc:title>").matcher(""); public Item(String uri, String name, String metadata) { this.uri = uri; this.name = name; this.metadata = metadata; } @Override public String toString() { if (StringUtils.isBlank(name)) { try { name = (! StringUtils.isEmpty(metadata) && dctitle.reset(unescape(metadata)).find()) ? dctitle.group(1) : new File(StringUtils.substringBefore(unescape(uri), "?")).getName(); } catch (UnsupportedEncodingException e) { LOGGER.error("URL decoding error ", e); } } return name; } @Override public boolean equals(Object other) { return other == null ? false : other == this ? true : other instanceof Item ? ((Item)other).uri.equals(uri) : other.toString().equals(uri); } @Override public int hashCode() { return uri.hashCode(); } } } } }