Package net.pms.encoders

Source Code of net.pms.encoders.VLCVideo$CodecConfig

/*
* PS3 Media Server, for streaming any medias to your PS3.
* Copyright (C) 2008  A.Brochard
*
* This program is 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.encoders;

import com.jgoodies.forms.builder.PanelBuilder;
import com.jgoodies.forms.factories.Borders;
import com.jgoodies.forms.layout.CellConstraints;
import com.jgoodies.forms.layout.FormLayout;
import com.sun.jna.Platform;
import java.awt.ComponentOrientation;
import java.awt.Font;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.io.File;
import java.io.IOException;
import java.util.*;
import javax.swing.*;
import net.pms.Messages;
import net.pms.configuration.PmsConfiguration;
import net.pms.configuration.RendererConfiguration;
import net.pms.dlna.DLNAMediaInfo;
import net.pms.dlna.DLNAResource;
import net.pms.formats.Format;
import net.pms.io.OutputParams;
import net.pms.io.PipeProcess;
import net.pms.io.ProcessWrapper;
import net.pms.io.ProcessWrapperImpl;
import net.pms.network.HTTPResource;
import net.pms.util.FileUtil;
import net.pms.util.FormLayoutUtil;
import net.pms.util.PlayerUtil;
import net.pms.util.ProcessUtil;
import org.apache.commons.lang3.StringUtils;
import static org.apache.commons.lang3.StringUtils.isBlank;
import static org.apache.commons.lang3.StringUtils.isNotBlank;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

// FIXME (breaking change): VLCWebVideo doesn't customize any of this, so everything should be *private*
// TODO (when transcoding to MPEG-2): handle non-MPEG-2 compatible input framerates

/**
* Use VLC as a backend transcoder. Note that 0.x and 1.x versions are
* unsupported (and probably will crash). Only the latest version will be
* supported
*
* @author Leon Blakey <lord.quackstar@gmail.com>
*/
public class VLCVideo extends Player {
  private static final Logger LOGGER = LoggerFactory.getLogger(VLCVideo.class);
  private static final String COL_SPEC = "left:pref, 3dlu, p, 3dlu, 0:grow";
  private static final String ROW_SPEC = "p, 3dlu, p, 3dlu, p";
  public static final String ID = "vlctranscoder";
  protected JTextField scale;
  protected JCheckBox experimentalCodecs;
  protected JCheckBox audioSyncEnabled;
  protected JTextField sampleRate;
  protected JCheckBox sampleRateOverride;
  protected JTextField extraParams;

  protected boolean videoRemux;

  @Deprecated
  public VLCVideo(PmsConfiguration configuration) {
    this();
  }

  public VLCVideo() {
  }

  @Override
  public int purpose() {
    return VIDEO_SIMPLEFILE_PLAYER;
  }

  @Override
  public String id() {
    return ID;
  }

  @Override
  public boolean isTimeSeekable() {
    return true;
  }

  @Override
  public boolean avisynth() {
    return false;
  }

  @Override
  public String[] args() {
    return new String[]{};
  }

  @Override
  public String name() {
    return "VLC";
  }

  @Override
  public int type() {
    return Format.VIDEO;
  }

  @Override
  public String mimeType() {
    // I think?
    return HTTPResource.VIDEO_TRANSCODE;
  }

  @Override
  public String executable() {
    return configuration.getVlcPath();
  }

  /**
   * Pick codecs for VLC based on formats the renderer supports;
   *
   * @param renderer The {@link RendererConfiguration}.
   * @return The codec configuration
   */
  protected CodecConfig genConfig(RendererConfiguration renderer) {
    CodecConfig codecConfig = new CodecConfig();
    boolean isXboxOneWebVideo = renderer.isXboxOne() && purpose() == VIDEO_WEBSTREAM_PLAYER;

    if (
      (
        renderer.isTranscodeToWMV() &&
        !renderer.isXbox360()
      ) ||
      isXboxOneWebVideo
    ) {
      // Assume WMV = Xbox 360 = all media renderers with this flag
      LOGGER.debug("Using XBox 360 WMV codecs");
      codecConfig.videoCodec = "wmv2";
      codecConfig.audioCodec = "wma";
      codecConfig.container = "asf";
    } else if (renderer.isTranscodeToH264()) {
      codecConfig.videoCodec = "h264";
      codecConfig.container = "ts";
      videoRemux = true;

      if (renderer.isTranscodeToMPEGTSH264AC3()) {
        LOGGER.debug("Using H.264 and MP2 with MPEG-TS container");
        codecConfig.audioCodec = "a52";
      } else if (renderer.isTranscodeToMPEGTSH264AAC()) {
        LOGGER.debug("Using H.264 and AAC with MPEG-TS container");
        codecConfig.audioCodec = "mp4a";
      }
    } else {
      codecConfig.videoCodec = "mp2v";
      codecConfig.audioCodec = "mp2a";

      if (renderer.isTranscodeToMPEGTS()) {
        LOGGER.debug("Using standard DLNA codecs with an MPEG-TS container");
        codecConfig.container = "ts";
      } else {
        LOGGER.debug("Using standard DLNA codecs with an MPEG-PS (default) container");
        codecConfig.container = "ps";
      }
    }

    LOGGER.trace("Using " + codecConfig.videoCodec + ", " + codecConfig.audioCodec + ", " + codecConfig.container);

    /**
    // Audio sample rate handling
    if (sampleRateOverride.isSelected()) {
      codecConfig.sampleRate = Integer.valueOf(sampleRate.getText());
    }
    */

    // This has caused garbled audio, so only enable when told to
    if (audioSyncEnabled.isSelected()) {
      codecConfig.extraTrans.put("audio-sync", "");
    }
    return codecConfig;
  }

  protected static class CodecConfig {
    String videoCodec;
    String audioCodec;
    String container;
    String extraParams;
    HashMap<String, Object> extraTrans = new HashMap<>();
    int sampleRate;
  }

  protected Map<String, Object> getEncodingArgs(CodecConfig codecConfig, OutputParams params) {
    // See: http://www.videolan.org/doc/streaming-howto/en/ch03.html
    // See: http://wiki.videolan.org/Codec
    Map<String, Object> args = new HashMap<>();

    // Codecs to use
    args.put("vcodec", codecConfig.videoCodec);
    args.put("acodec", codecConfig.audioCodec);

    /**
     * Bitrate in kbit/s
     *
     * TODO: Make this engine smarter with bitrates, see
     * FFMpegVideo.getVideoBitrateOptions() for our best
     * implementation of this.
     */
    if (!videoRemux) {
      args.put("vb", "4096");
    }

    if (codecConfig.audioCodec.equals("mp4a")) {
      args.put("ab", Math.min(configuration.getAudioBitrate(), 320));
    } else {
      args.put("ab", configuration.getAudioBitrate());
    }

    // Video scaling
    args.put("scale", "1.0");

    boolean isXboxOneWebVideo = params.mediaRenderer.isXboxOne() && purpose() == VIDEO_WEBSTREAM_PLAYER;

    // Audio Channels
    int channels = 2;
    if (!isXboxOneWebVideo && params.aid.getAudioProperties().getNumberOfChannels() > 2 && configuration.getAudioChannelCount() == 6) {
      channels = 6;
    }
    args.put("channels", channels);

    // Static sample rate
    // TODO: Does WMA still need a sample rate of 41000 for Xbox compatibility?
    args.put("samplerate", "48000");

    // Recommended on VLC DVD encoding page
    args.put("strict-rc", null);

    // Enable multi-threading
    args.put("threads", "" + configuration.getNumberOfCpuCores());

    // Hardcode subtitles into video
    args.put("soverlay", null);

    // Add extra args
    args.putAll(codecConfig.extraTrans);

    return args;
  }

  private int[] getVideoBitrateConfig(String bitrate) {
    int bitrates[] = new int[2];

    if (bitrate.contains("(") && bitrate.contains(")")) {
      bitrates[1] = Integer.parseInt(bitrate.substring(bitrate.indexOf('(') + 1, bitrate.indexOf(')')));
    }

    if (bitrate.contains("(")) {
      bitrate = bitrate.substring(0, bitrate.indexOf('(')).trim();
    }

    if (isBlank(bitrate)) {
      bitrate = "0";
    }

    bitrates[0] = (int) Double.parseDouble(bitrate);

    return bitrates;
  }

  /**
   * Returns the video bitrate spec for the current transcode according
   * to the limits/requirements of the renderer and the user's settings.
   *
   * @param dlna
   * @param media the media metadata for the video being streamed. May contain unset/null values (e.g. for web videos).
   * @param params
   * @return a {@link List} of <code>String</code>s representing the video bitrate options for this transcode
   */
  public List<String> getVideoBitrateOptions(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) {
    List<String> videoBitrateOptions = new ArrayList<>();

    int defaultMaxBitrates[] = getVideoBitrateConfig(configuration.getMaximumBitrate());
    int rendererMaxBitrates[] = new int[2];

    boolean isXboxOneWebVideo = params.mediaRenderer.isXboxOne() && purpose() == VIDEO_WEBSTREAM_PLAYER;

    if (StringUtils.isNotEmpty(params.mediaRenderer.getMaxVideoBitrate())) {
      rendererMaxBitrates = getVideoBitrateConfig(params.mediaRenderer.getMaxVideoBitrate());
    }

    // Give priority to the renderer's maximum bitrate setting over the user's setting
    if (rendererMaxBitrates[0] > 0 && rendererMaxBitrates[0] < defaultMaxBitrates[0]) {
      defaultMaxBitrates = rendererMaxBitrates;
    }

    if (params.mediaRenderer.getCBRVideoBitrate() == 0 && params.timeend == 0) {
      // Convert value from Mb to Kb
      defaultMaxBitrates[0] = 1000 * defaultMaxBitrates[0];

      // Halve it since it seems to send up to 1 second of video in advance
      defaultMaxBitrates[0] /= 2;

      int bufSize = 1835;
      boolean bitrateLevel41Limited = false;

      /**
       * Although the maximum bitrate for H.264 Level 4.1 is
       * officially 50,000 kbit/s, some 4.1-capable renderers
       * like the PS3 stutter when video exceeds roughly 31,250
       * kbit/s.
       *
       * We also apply the correct buffer size in this section.
       */
      if (!isXboxOneWebVideo && params.mediaRenderer.isTranscodeToH264()) {
        if (
          params.mediaRenderer.isH264Level41Limited() &&
          defaultMaxBitrates[0] > 31250
        ) {
          defaultMaxBitrates[0] = 31250;
          bitrateLevel41Limited = true;
        }
        bufSize = defaultMaxBitrates[0];
      } else {
        if (media.isHDVideo()) {
          bufSize = defaultMaxBitrates[0] / 3;
        }

        if (bufSize > 7000) {
          bufSize = 7000;
        }

        if (defaultMaxBitrates[1] > 0) {
          bufSize = defaultMaxBitrates[1];
        }

        if (params.mediaRenderer.isDefaultVBVSize() && rendererMaxBitrates[1] == 0) {
          bufSize = 1835;
        }
      }

      if (!bitrateLevel41Limited) {
        // Make room for audio
        // TODO: set correct bitrate when remuxing DTS, like in FFMpegVideo
        if (params.mediaRenderer.isTranscodeToAAC()) {
          defaultMaxBitrates[0] -= Math.min(configuration.getAudioBitrate(), 320);
        } else {
          defaultMaxBitrates[0] -= configuration.getAudioBitrate();
        }

        // Round down to the nearest Mb
        defaultMaxBitrates[0] = defaultMaxBitrates[0] / 1000 * 1000;
      }

      videoBitrateOptions.add("--sout-x264-vbv-bufsize");
      videoBitrateOptions.add(String.valueOf(bufSize));

      videoBitrateOptions.add("--sout-x264-vbv-maxrate");
      videoBitrateOptions.add(String.valueOf(defaultMaxBitrates[0]));
    }

    if (isXboxOneWebVideo || !params.mediaRenderer.isTranscodeToH264()) {
      // Add MPEG-2 quality settings
      String mpeg2Options = configuration.getMPEG2MainSettingsFFmpeg();
      String mpeg2OptionsRenderer = params.mediaRenderer.getCustomFFmpegMPEG2Options();

      // Renderer settings take priority over user settings
      if (isNotBlank(mpeg2OptionsRenderer)) {
        mpeg2Options = mpeg2OptionsRenderer;
      } else if (mpeg2Options.contains("Automatic")) {
        mpeg2Options = "--sout-x264-keyint 5 --sout-avcodec-qscale 1 --sout-avcodec-qmin 2 --sout-avcodec-qmax 3";

        // It has been reported that non-PS3 renderers prefer keyint 5 but prefer it for PS3 because it lowers the average bitrate
        if (params.mediaRenderer.isPS3()) {
          mpeg2Options = "--sout-x264-keyint 25 --sout-avcodec-qscale 1 --sout-avcodec-qmin 2 --sout-avcodec-qmax 3";
        }

        if (mpeg2Options.contains("Wireless") || defaultMaxBitrates[0] < 70) {
          // Lower quality for 720p+ content
          if (media.getWidth() > 1280) {
            mpeg2Options = "--sout-x264-keyint 25 --sout-avcodec-qmin 2 --sout-avcodec-qmax 7";
          } else if (media.getWidth() > 720) {
            mpeg2Options = "--sout-x264-keyint 25 --sout-avcodec-qmin 2 --sout-avcodec-qmax 5";
          }
        }
      }
      String[] customOptions = StringUtils.split(mpeg2Options);
      videoBitrateOptions.addAll(new ArrayList<>(Arrays.asList(customOptions)));
    } else {
      // Add x264 quality settings
      String x264CRF = configuration.getx264ConstantRateFactor();

      // Remove comment from the value
      if (x264CRF.contains("/*")) {
        x264CRF = x264CRF.substring(x264CRF.indexOf("/*"));
      }

      if (x264CRF.contains("Automatic")) {
        x264CRF = "16";

        // Lower CRF for 720p+ content
        if (media.getWidth() > 720) {
          x264CRF = "19";
        }
      }
      videoBitrateOptions.add("--sout-x264-crf");
      videoBitrateOptions.add(x264CRF);
    }
   
    return videoBitrateOptions;
  }

  @Override
  public ProcessWrapper launchTranscode(DLNAResource dlna, DLNAMediaInfo media, OutputParams params) throws IOException {
    final String filename = dlna.getSystemName();
    boolean isWindows = Platform.isWindows();
    setAudioAndSubs(filename, media, params);

    // Make sure we can play this
    CodecConfig config = genConfig(params.mediaRenderer);

    PipeProcess tsPipe = new PipeProcess("VLC" + System.currentTimeMillis() + "." + config.container);
    ProcessWrapper pipe_process = tsPipe.getPipeProcess();

    // XXX it can take a long time for Windows to create a named pipe
    // (and mkfifo can be slow if /tmp isn't memory-mapped), so start this as early as possible
    pipe_process.runInNewThread();
    tsPipe.deleteLater();

    params.input_pipes[0] = tsPipe;
    params.minBufferSize = params.minFileSize;
    params.secondread_minsize = 100000;

    List<String> cmdList = new ArrayList<>();
    cmdList.add(executable());
    cmdList.add("-I");
    cmdList.add("dummy");

    // Disable hardware acceleration which is enabled by default,
    // but for hardware acceleration, user must enable it in "VLC Preferences",
    // until they release documentation for new functionalities introduced in 2.1.4+
    if (!configuration.isGPUAcceleration()) {
      cmdList.add("--avcodec-hw=disabled");
    }

    // Useful for the more esoteric codecs people use
    if (experimentalCodecs.isSelected()) {
      cmdList.add("--sout-avcodec-strict=-2");
    }

    // Stop the DOS box from appearing on windows
    if (isWindows) {
      cmdList.add("--dummy-quiet");
    }

    // File needs to be given before sout, otherwise vlc complains
    cmdList.add(filename);

    String disableSuffix = "track=-1";

    // Handle audio language
    if (params.aid != null) {
      // User specified language at the client, acknowledge it
      if (params.aid.getLang() == null || params.aid.getLang().equals("und")) {
        // VLC doesn't understand "und", so try to get audio track by ID
        cmdList.add("--audio-track=" + params.aid.getId());
      } else {
        cmdList.add("--audio-language=" + params.aid.getLang());
      }
    } else {
      // Not specified, use language from GUI
      // FIXME: VLC does not understand "loc" or "und".
      cmdList.add("--audio-language=" + configuration.getAudioLanguages());
    }

    // Handle subtitle language
    if (params.sid != null) { // User specified language at the client, acknowledge it
      if (params.sid.isExternal()) {
        String externalSubtitlesFileName;

        // External subtitle file
        if (params.sid.isExternalFileUtf16()) {
          try {
            // Convert UTF-16 -> UTF-8
            File convertedSubtitles = new File(configuration.getTempFolder(), "utf8_" + params.sid.getExternalFile().getName());
            FileUtil.convertFileFromUtf16ToUtf8(params.sid.getExternalFile(), convertedSubtitles);
            externalSubtitlesFileName = ProcessUtil.getShortFileNameIfWideChars(convertedSubtitles.getAbsolutePath());
          } catch (IOException e) {
            LOGGER.debug("Error converting file from UTF-16 to UTF-8", e);
            externalSubtitlesFileName = ProcessUtil.getShortFileNameIfWideChars(params.sid.getExternalFile().getAbsolutePath());
          }
        } else {
          externalSubtitlesFileName = ProcessUtil.getShortFileNameIfWideChars(params.sid.getExternalFile().getAbsolutePath());
        }

        if (externalSubtitlesFileName != null) {
          cmdList.add("--sub-file");
          cmdList.add(externalSubtitlesFileName);
        }
      } else if (params.sid.getLang() != null && !params.sid.getLang().equals("und")) { // Load by ID (better)
        cmdList.add("--sub-track=" + params.sid.getId());
      } else { // VLC doesn't understand "und", but does understand a nonexistent track
        cmdList.add("--sub-" + disableSuffix);
      }
    } else {
      cmdList.add("--sub-" + disableSuffix);
    }

    // x264 options
    if (videoRemux) {
      cmdList.add("--sout-x264-preset");
      cmdList.add("superfast");

      cmdList.add("--no-sout-avcodec-hurry-up");

      cmdList.addAll(getVideoBitrateOptions(dlna, media, params));
    }

    // Skip forward if necessary
    if (params.timeseek != 0) {
      cmdList.add("--start-time");
      cmdList.add(String.valueOf(params.timeseek));
    }

    // Generate encoding args
    String separator = "";
    StringBuilder encodingArgsBuilder = new StringBuilder();
    for (Map.Entry<String, Object> curEntry : getEncodingArgs(config, params).entrySet()) {
      encodingArgsBuilder.append(separator);
      encodingArgsBuilder.append(curEntry.getKey());

      if (curEntry.getValue() != null) {
        encodingArgsBuilder.append("=");
        encodingArgsBuilder.append(curEntry.getValue());
      }

      separator = ",";
    }

    // Add our transcode options
    String transcodeSpec = String.format(
      "#transcode{%s}:standard{access=file,mux=%s,dst='%s%s'}",
      encodingArgsBuilder.toString(),
      config.container,
      (isWindows ? "\\\\" : ""),
      tsPipe.getInputPipe()
    );
    cmdList.add("--sout");
    cmdList.add(transcodeSpec);

    // Force VLC to exit when finished
    cmdList.add("vlc://quit");

    // Add any extra parameters
    if (!extraParams.getText().trim().isEmpty()) { // Add each part as a new item
      cmdList.addAll(Arrays.asList(StringUtils.split(extraParams.getText().trim(), " ")));
    }

    // Pass to process wrapper
    String[] cmdArray = new String[cmdList.size()];
    cmdList.toArray(cmdArray);
    cmdArray = finalizeTranscoderArgs(filename, dlna, media, params, cmdArray);
    LOGGER.trace("Finalized args: " + StringUtils.join(cmdArray, " "));
    ProcessWrapperImpl pw = new ProcessWrapperImpl(cmdArray, params);
    pw.attachProcess(pipe_process);

    // TODO: Why is this here?
    try {
      Thread.sleep(150);
    } catch (InterruptedException e) {
    }

    pw.runInNewThread();
    return pw;
  }

  @Override
  public JComponent config() {
    // Apply the orientation for the locale
    Locale locale = new Locale(configuration.getLanguage());
    ComponentOrientation orientation = ComponentOrientation.getOrientation(locale);
    String colSpec = FormLayoutUtil.getColSpec(COL_SPEC, orientation);
    FormLayout layout = new FormLayout(colSpec, ROW_SPEC);
    PanelBuilder builder = new PanelBuilder(layout);
    builder.border(Borders.EMPTY);
    builder.opaque(false);

    CellConstraints cc = new CellConstraints();

    JComponent cmp = builder.addSeparator(Messages.getString("NetworkTab.5"), FormLayoutUtil.flip(cc.xyw(1, 1, 5), colSpec, orientation));
    cmp = (JComponent) cmp.getComponent(0);
    cmp.setFont(cmp.getFont().deriveFont(Font.BOLD));

    experimentalCodecs = new JCheckBox(Messages.getString("VlcTrans.3"), configuration.isVlcExperimentalCodecs());
    experimentalCodecs.setContentAreaFilled(false);
    experimentalCodecs.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        configuration.setVlcExperimentalCodecs(e.getStateChange() == ItemEvent.SELECTED);
      }
    });
    builder.add(experimentalCodecs, FormLayoutUtil.flip(cc.xy(1, 3), colSpec, orientation));

    audioSyncEnabled = new JCheckBox(Messages.getString("MEncoderVideo.2"), configuration.isVlcAudioSyncEnabled());
    audioSyncEnabled.setContentAreaFilled(false);
    audioSyncEnabled.addItemListener(new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        configuration.setVlcAudioSyncEnabled(e.getStateChange() == ItemEvent.SELECTED);
      }
    });
    builder.add(audioSyncEnabled, FormLayoutUtil.flip(cc.xy(1, 5), colSpec, orientation));

    JPanel panel = builder.getPanel();

    // Apply the orientation to the panel and all components in it
    panel.applyComponentOrientation(orientation);

    return panel;
  }

  @Override
  public boolean isCompatible(DLNAResource resource) {
    // Only handle local video - not web video or audio
    if (
      PlayerUtil.isVideo(resource, Format.Identifier.MKV) ||
      PlayerUtil.isVideo(resource, Format.Identifier.MPG)
    ) {
      return true;
    }

    return false;
  }
}
TOP

Related Classes of net.pms.encoders.VLCVideo$CodecConfig

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.