net.pms.configuration.WebRender.java Source code

Java tutorial

Introduction

Here is the source code for net.pms.configuration.WebRender.java

Source

/*
 * Universal Media Server, for streaming any medias 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.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() };

    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;

    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:wav n:2 m:audio/wav");
        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";
        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 : 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";
        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("-strict");
        cmdList.add("experimental");
        /*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 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();
            }
        }
    }
}