/*
* Universal Media Server, for streaming any media to DLNA
* compatible renderers based on the http://www.ps3mediaserver.org.
* Copyright (C) 2012 UMS developers.
*
* This program is a free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; version 2
* of the License only.
*
* This program 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. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package net.pms.configuration;
import com.google.gson.Gson;
import java.io.File;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.pms.Messages;
import net.pms.PMS;
import net.pms.dlna.DLNAMediaInfo;
import net.pms.dlna.DLNAResource;
import net.pms.dlna.virtual.VirtualVideoAction;
import net.pms.encoders.FFMpegVideo;
import net.pms.encoders.Player;
import net.pms.external.StartStopListenerDelegate;
import net.pms.formats.*;
import net.pms.formats.audio.MP3;
import net.pms.formats.image.BMP;
import net.pms.formats.image.GIF;
import net.pms.formats.image.JPG;
import net.pms.formats.image.PNG;
import net.pms.image.ImageFormat;
import net.pms.io.OutputParams;
import net.pms.remote.RemoteUtil;
import net.pms.util.BasicPlayer;
import net.pms.util.StringUtil;
import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DurationFormatUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class WebRender extends DeviceConfiguration implements RendererConfiguration.OutputOverride {
private String user;
private String ip;
@SuppressWarnings("unused")
private int port;
private String ua;
private String defaultMime;
private int browser = 0;
private String platform = null;
private int screenWidth = 0;
private int screenHeight = 0;
private boolean isTouchDevice = false;
private String subLang;
private Gson gson;
private static final PmsConfiguration pmsconfiguration = PMS.getConfiguration();
private static final Logger LOGGER = LoggerFactory.getLogger(WebRender.class);
private static final Format[] supportedFormats = {
new GIF(),
new JPG(),
new MP3(),
new PNG(),
new BMP()
};
private static final Matcher umsInfo = Pattern.compile("platform=(.+)&width=(.+)&height=(.+)&isTouchDevice=(.+)").matcher("");
protected static final int CHROME = 1;
protected static final int MSIE = 2;
protected static final int FIREFOX = 3;
protected static final int SAFARI = 4;
protected static final int PS4 = 5;
protected static final int XBOX1 = 6;
protected static final int OPERA = 7;
protected static final int EDGE = 8;
protected static final int CHROMIUM = 9;
protected static final int VIVALDI = 10;
private StartStopListenerDelegate startStop;
public WebRender(String user) throws ConfigurationException {
super(NOFILE, null);
this.user = user;
ip = "";
port = 0;
ua = "";
fileless = true;
String userFmt = pmsconfiguration.getWebTranscode();
defaultMime = userFmt != null ? ("video/" + userFmt) : RemoteUtil.transMime();
startStop = null;
subLang = "";
if (pmsConfiguration.useWebControl()) {
controls = BasicPlayer.PLAYCONTROL|BasicPlayer.VOLUMECONTROL;
}
gson = new Gson();
push = new ArrayList<>();
}
@Override
public boolean load(File f) {
// FIXME: These are just preliminary
configuration.addProperty(MEDIAPARSERV2, true);
configuration.addProperty(MEDIAPARSERV2_THUMB, true);
configuration.addProperty(SUPPORTED, "f:flv v:h264|hls a:aac m:video/flash");
configuration.addProperty(SUPPORTED, "f:mp4 m:video/mp4");
configuration.addProperty(SUPPORTED, "f:mp3 n:2 m:audio/mpeg");
configuration.addProperty(SUPPORTED, "f:ogg v:theora m:video/ogg");
configuration.addProperty(SUPPORTED, "f:ogg a:vorbis|flac m:audio/ogg");
configuration.addProperty(SUPPORTED, "f:wav n:2 m:audio/wav");
configuration.addProperty(SUPPORTED, "f:webm v:vp8|vp9 m:video/webm");
configuration.addProperty(SUPPORTED, "f:bmp m:image/bmp");
configuration.addProperty(SUPPORTED, "f:jpg m:image/jpeg");
configuration.addProperty(SUPPORTED, "f:png m:image/png");
configuration.addProperty(SUPPORTED, "f:gif m:image/gif");
configuration.addProperty(SUPPORTED, "f:tiff m:image/tiff");
configuration.addProperty(TRANSCODE_AUDIO, MP3);
return true;
}
@Override
public boolean associateIP(InetAddress sa) {
ip = sa.getHostAddress();
return super.associateIP(sa);
}
@Override
public InetAddress getAddress() {
try {
return InetAddress.getByName(ip);
} catch (Exception e) {
return null;
}
}
public void associatePort(int port) {
this.port = port;
}
public void setUA(String ua) {
LOGGER.debug("Setting web client ua: {}", ua);
this.ua = ua.toLowerCase();
}
public static String getBrowserName(int browser) {
switch (browser) {
case CHROME: return "Chrome";
case MSIE: return "Internet Explorer";
case FIREFOX: return "Firefox";
case SAFARI: return "Safari";
case PS4: return "Playstation 4";
case XBOX1: return "Xbox One";
case OPERA: return "Opera";
case EDGE: return "Edge";
case CHROMIUM:return "Chromium";
case VIVALDI: return "Vivaldi";
default: return Messages.getString("PMS.142");
}
}
public static int getBrowser(String userAgent) {
String ua = userAgent.toLowerCase();
return
ua.contains("edge") ? EDGE :
ua.contains("chrome") ? CHROME :
(ua.contains("msie") ||
ua.contains("trident")) ? MSIE :
ua.contains("firefox") ? FIREFOX :
ua.contains("safari") ? SAFARI :
ua.contains("playstation 4") ? PS4 :
ua.contains("xbox one") ? XBOX1 :
ua.contains("opera") ? OPERA :
ua.contains("chromium") ? CHROMIUM :
ua.contains("vivaldi") ? VIVALDI :
0;
}
public void setBrowserInfo(String info, String userAgent) {
setUA(userAgent);
browser = getBrowser(userAgent);
if (info != null && umsInfo.reset(info).find()) {
platform = umsInfo.group(1).toLowerCase();
screenWidth = Integer.valueOf(umsInfo.group(2));
screenHeight = Integer.valueOf(umsInfo.group(3));
isTouchDevice = Boolean.valueOf(umsInfo.group(4));
LOGGER.debug("Setting {} browser info: platform:{}, screen:{}x{}, isTouchDevice:{}",
getRendererName(), platform, screenWidth, screenHeight, isTouchDevice);
}
active = true;
uuid = getConfName() + ":" + ip;
}
@Override
public String getRendererName() {
return (pmsconfiguration.isWebAuthenticate() ? user + "@" : "") + getBrowserName(browser);
}
@Override
public String getConfName() {
return getBrowserName(browser);
}
public int getBrowser() {
return browser;
}
public String getUser() {
return user;
}
@Override
public String getRendererIcon() {
switch (browser) {
case CHROME: return "chrome.png";
case MSIE: return "internetexplorer.png";
case FIREFOX: return "firefox.png";
case SAFARI: return "safari.png";
case PS4: return "ps4.png";
case XBOX1: return "xbox-one.png";
case OPERA: return "opera.png";
case EDGE: return "edge.png";
case CHROMIUM:return "chromium.png";
case VIVALDI: return "vivaldi.png";
default: return super.getRendererIcon();
}
}
@Override
public String toString() {
return getRendererName();
}
@Override
public boolean isMediaInfoThumbnailGeneration() {
return false;
}
@Override
public boolean isLimitFolders() {
// no folder limit on the web clients
return false;
}
public boolean isChromeTrick() {
return browser == CHROME && pmsconfiguration.getWebChrome();
}
public boolean isFirefoxLinuxMp4() {
return browser == FIREFOX && platform != null && platform.contains("linux") && pmsconfiguration.getWebFirefoxLinuxMp4();
}
public boolean isScreenSizeConstrained() {
return (screenWidth != 0 && RemoteUtil.getWidth() > screenWidth) ||
(screenHeight != 0 && RemoteUtil.getHeight() > screenHeight);
}
public int getVideoWidth() {
return isScreenSizeConstrained() ? screenWidth : RemoteUtil.getWidth();
}
public int getVideoHeight() {
return isScreenSizeConstrained() ? screenHeight : RemoteUtil.getHeight();
}
public String getVideoMimeType() {
if (isChromeTrick()) {
return RemoteUtil.MIME_WEBM;
} else if (isFirefoxLinuxMp4()) {
return RemoteUtil.MIME_MP4;
}
return defaultMime;
}
@Override
public int getAutoPlayTmo() {
return 0;
}
@Override
public boolean isNoDynPlsFolder() {
return true;
}
public boolean isLowBitrate() {
// FIXME: this should return true if either network speed or client cpu are slow
boolean slow = false;
try {
// note here if we get a low speed then calcspeed
// will return -1 which will ALWAYS be less that the configed value.
slow = getInt(calculatedSpeed(), 0) < pmsConfiguration.getWebLowSpeed();
} catch (Exception e) {
}
return slow || (screenWidth < 720 && (ua.contains("mobi") || isTouchDevice));
}
@Override
public boolean getOutputOptions(List<String> cmdList, DLNAResource dlna, Player player, OutputParams params) {
if (player instanceof FFMpegVideo) {
if (dlna.getFormat().isVideo()) {
DLNAMediaInfo media = dlna.getMedia();
boolean flash = media != null && "video/flash".equals(media.getMimeType());
if (flash) {
fflashCmds(cmdList, media);
} else {
String mime = getVideoMimeType();
switch (mime) {
case RemoteUtil.MIME_OGG:
ffoggCmd(cmdList);
break;
case RemoteUtil.MIME_MP4:
ffmp4Cmd(cmdList);
break;
case RemoteUtil.MIME_WEBM:
if (isChromeTrick()) {
chromeCmd(cmdList);
} else {
// nothing here yet
} break;
}
}
if (isLowBitrate()) {
cmdList.addAll(((FFMpegVideo) player).getVideoBitrateOptions(dlna, media, params));
}
} else {
// nothing here yet
}
return true;
// } else if (player instanceof MEncoderVideo) {
// // nothing here yet
}
return false;
}
private void fflashCmds(List<String> cmdList, DLNAMediaInfo media) {
// Can't streamcopy if filters are present
boolean canCopy = !(cmdList.contains("-vf") || cmdList.contains("-filter_complex"));
cmdList.add("-c:v");
if (canCopy && media != null && media.getCodecV() != null && media.getCodecV().equals("h264")) {
cmdList.add("copy");
} else {
cmdList.add("flv");
cmdList.add("-qmin");
cmdList.add("2");
cmdList.add("-qmax");
cmdList.add("6");
}
if (canCopy && media != null && media.getFirstAudioTrack() != null && media.getFirstAudioTrack().isAAC()) {
cmdList.add("-c:a");
cmdList.add("copy");
} else {
cmdList.add("-ar");
cmdList.add("44100");
}
cmdList.add("-f");
cmdList.add("flv");
}
private void ffoggCmd(List<String> cmdList) {
/*cmdList.add("-c:v");
cmdList.add("libtheora");*/
cmdList.add("-qscale:v");
cmdList.add("10");
cmdList.add("-acodec");
cmdList.add("libvorbis");
/*cmdList.add("-qscale:a");
cmdList.add("6");*/
/*cmdList.add("-bufsize");
cmdList.add("300k");
cmdList.add("-b:a");
cmdList.add("128k");*/
cmdList.add("-f");
cmdList.add("ogg");
}
private void ffmp4Cmd(List<String> cmdList) {
// see http://stackoverflow.com/questions/8616855/how-to-output-fragmented-mp4-with-ffmpeg
cmdList.add(1, "-re");
cmdList.add("-g");
cmdList.add("52"); // see https://code.google.com/p/stream-m/#FRAGMENT_SIZES
cmdList.add("-c:v");
cmdList.add("libx264");
cmdList.add("-preset");
cmdList.add("ultrafast");
/*cmdList.add("-tune");
cmdList.add("zerolatency");
cmdList.add("-profile:v");
cmdList.add("high");
cmdList.add("-level:v");
cmdList.add("3.1");*/
cmdList.add("-c:a");
cmdList.add("aac");
cmdList.add("-ab");
cmdList.add("16k");
// cmdList.add("-ar");
// cmdList.add("44100");
/*cmdList.add("-pix_fmt");
cmdList.add("yuv420p");*/
// cmdList.add("-frag_duration");
// cmdList.add("300");
// cmdList.add("-frag_size");
// cmdList.add("100");
// cmdList.add("-flags");
// cmdList.add("+aic+mv4");
cmdList.add("-movflags");
cmdList.add("frag_keyframe+empty_moov");
cmdList.add("-f");
cmdList.add("mp4");
}
private void chromeCmd(List<String> cmdList) {
//-c:v libx264 -profile:v high -level 4.1 -map 0:a -c:a libmp3lame -ac 2 -preset ultrafast -b:v 35000k -bufsize 35000k -f matroska
cmdList.add("-c:v");
cmdList.add("libx264");
cmdList.add("-profile:v");
cmdList.add("high");
cmdList.add("-level:v");
cmdList.add("3.1");
cmdList.add("-c:a");
cmdList.add("libmp3lame");
cmdList.add("-ac");
cmdList.add("2");
cmdList.add("-pix_fmt");
cmdList.add("yuv420p");
cmdList.add("-preset");
cmdList.add("ultrafast");
cmdList.add("-f");
cmdList.add("matroska");
}
@SuppressWarnings("unused")
private void ffhlsCmd(List<String> cmdList, DLNAMediaInfo media) {
// Can't streamcopy if filters are present
boolean canCopy = !(cmdList.contains("-vf") || cmdList.contains("-filter_complex"));
cmdList.add("-c:v");
if (canCopy && media != null && media.getCodecV() != null && media.getCodecV().equals("h264")) {
cmdList.add("copy");
} else {
cmdList.add("flv");
cmdList.add("-qmin");
cmdList.add("2");
cmdList.add("-qmax");
cmdList.add("6");
}
if (canCopy && media != null && media.getFirstAudioTrack() != null && media.getFirstAudioTrack().isAAC()) {
cmdList.add("-c:a");
cmdList.add("copy");
} else {
cmdList.add("-ar");
cmdList.add("44100");
}
cmdList.add("-f");
cmdList.add("HLS");
}
public boolean isImageFormatSupported(ImageFormat format) {
if (format == null) {
return false;
}
if (format == ImageFormat.GIF || format == ImageFormat.JPEG || format == ImageFormat.PNG) {
return true;
}
switch (format) {
case BMP:
return
browser == FIREFOX || browser == CHROME ||
browser == CHROMIUM || browser == OPERA ||
browser == MSIE || browser == EDGE || browser == SAFARI;
case TIFF:
return browser == EDGE || browser == CHROMIUM || browser == SAFARI || browser == MSIE;
case WEBP:
return browser == CHROME || browser == CHROMIUM || browser == OPERA;
default:
return false;
}
}
public static boolean supportedFormat(Format f) {
for (Format f1 : supportedFormats) {
if (f.getIdentifier() == f1.getIdentifier() || f1.mimeType().equals(f.mimeType())) {
return true;
}
}
return false;
}
public static boolean supports(DLNAResource dlna) {
if (dlna instanceof VirtualVideoAction) {
return true;
}
DLNAMediaInfo m = dlna.getMedia();
return (m != null && RemoteUtil.directmime(m.getMimeType())) ||
(supportedFormat(dlna.getFormat())) ||
(dlna.getPlayer() instanceof FFMpegVideo);
}
@Override
public String getFFmpegVideoFilterOverride() {
return "scale=" + getVideoWidth() + ":" + getVideoHeight();
}
@Override
public boolean isTranscodeToMPEGTSH264AC3() {
return true;
}
@Override
public boolean isTranscodeToMPEGTSH264AAC() {
return true;
}
@Override
public boolean nox264() {
return true;
}
@Override
public boolean addSubtitles() {
return true;
}
@Override
public BasicPlayer getPlayer() {
if (player == null) {
player = new WebPlayer(this);
}
return player;
}
@Override
public String getSubLanguage() {
if (!useWebSubLang() || StringUtils.isEmpty(subLang)) {
return super.getSubLanguage();
}
return subLang;
}
public void setSubLang(String s) {
subLang = s;
}
private ArrayList<String[]> push;
public void push(String... args) {
push.add(args);
}
public String getPushData() {
String json = "";
if (push.size() > 0) {
json = gson.toJson(push);
push.clear();
}
return json;
}
@Override
public void notify(String type, String msg) {
push("notify", type, msg);
}
public void start(DLNAResource dlna) {
if (getPlayingRes() != dlna) {
stop();
}
setPlayingRes(dlna);
if (startStop == null) {
startStop = new StartStopListenerDelegate(ip);
}
startStop.setRenderer(this);
startStop.start(getPlayingRes());
}
public void stop() {
if (startStop == null) {
return;
}
startStop.stop();
startStop = null;
}
public static class WebPlayer extends BasicPlayer.Logical {
private HashMap<String, String> data;
private Gson gson;
public WebPlayer(WebRender renderer) {
super(renderer);
data = new HashMap<>();
gson = renderer.gson;
LOGGER.debug("Created web player for " + renderer.getRendererName());
}
@Override
public void setURI(String uri, String metadata) {
Playlist.Item item = resolveURI(uri, metadata);
if (item != null) {
DLNAResource r = DLNAResource.getValidResource(item.uri, item.name, renderer);
if (r != null) {
((WebRender)renderer).push("seturl", "/play/" + r.getId());
return;
}
}
LOGGER.debug("Bad uri " + uri);
}
@Override
public void pause() {
((WebRender)renderer).push("control", "pause");
}
@Override
public void play() {
((WebRender)renderer).push("control", "play");
}
@Override
public void stop() {
((WebRender)renderer).push("control", "stop");
}
@Override
public void mute() {
((WebRender)renderer).push("control", "mute");
}
@Override
public void setVolume(int volume) {
((WebRender)renderer).push("control", "setvolume", "" + volume);
}
@Override
public int getControls() {
return renderer.pmsConfiguration.useWebControl() ? PLAYCONTROL|VOLUMECONTROL : 0;
}
@Override
public void start() {
DLNAResource d = renderer.getPlayingRes();
state.name = d.getDisplayName();
if (d.getMedia() != null) {
state.duration = StringUtil.shortTime(d.getMedia().getDurationString(), 4);
}
}
public void setData(String jsonData) {
data = gson.fromJson(jsonData, data.getClass());
String s = data.get("playback");
state.playback = "STOPPED".equals(s) ? STOPPED :
"PLAYING".equals(s) ? PLAYING :
"PAUSED".equals(s) ? PAUSED : -1;
state.mute = "0".equals(data.get("mute")) ? false : true;
s = data.get("volume");
state.volume = s == null ? 0 : Integer.valueOf(s);
long seconds = Integer.valueOf(data.get("position"));
state.position = DurationFormatUtils.formatDuration(seconds * 1000, "HH:mm:ss");
alert();
if (state.playback == STOPPED) {
((WebRender)renderer).stop();
}
}
}
}